diff options
Diffstat (limited to 'sourcemod')
209 files changed, 60469 insertions, 0 deletions
diff --git a/sourcemod/scripting/distbugfix.sp b/sourcemod/scripting/distbugfix.sp new file mode 100644 index 0000000..d1854b5 --- /dev/null +++ b/sourcemod/scripting/distbugfix.sp @@ -0,0 +1,1592 @@ + +#include <sourcemod> +#include <sdktools> +#include <sdkhooks> +#include <clientprefs> +#include <colors> + +#pragma newdecls required +#pragma semicolon 1 + +#if defined DEBUG +#define DEBUG_CHAT(%1) PrintToChat(%1); +#define DEBUG_CHATALL(%1) PrintToChatAll(%1); +#define DEBUG_CONSOLE(%1) PrintToConsole(%1); +#else +#define DEBUG_CHAT(%1) +#define DEBUG_CHATALL(%1) +#define DEBUG_CONSOLE(%1) +#endif + +#include <gamechaos> +#include <distbugfix> + +char g_jumpTypes[JumpType][] = { + "NONE", + "LJ", + "WJ", + "LAJ", + "BH", + "CBH", +}; + +stock char g_szStrafeType[StrafeType][] = { + "$", // STRAFETYPE_OVERLAP + ".", // STRAFETYPE_NONE + + "â–ˆ", // STRAFETYPE_LEFT + "#", // STRAFETYPE_OVERLAP_LEFT + "H", // STRAFETYPE_NONE_LEFT + + "â–ˆ", // STRAFETYPE_RIGHT + "#", // STRAFETYPE_OVERLAP_RIGHT + "H", // STRAFETYPE_NONE_RIGHT +}; + +stock char g_szStrafeTypeColour[][] = { + "<font color='#FF00FF'>|", // overlap + "<font color='#000000'>|", // none + "<font color='#FFFFFF'>|", // left + "<font color='#00BFBF'>|", // overlap_left + "<font color='#408040'>|", // none_left + "<font color='#FFFFFF'>|", // right + "<font color='#00BFBF'>|", // overlap_right + "<font color='#408040'>|", // none_right +}; + +stock bool g_jumpTypePrintable[JumpType] = { + false, // JUMPTYPE_NONE, + + true, // longjump + true, // weirdjump + true, // ladderjump + true, // bunnyhop + true, // ducked bunnyhop +}; + +stock char g_jumpDirString[JumpDir][] = { + "Forwards", + "Backwards", + "Sideways", + "Sideways" +}; + +stock int g_jumpDirForwardButton[JumpDir] = { + IN_FORWARD, + IN_BACK, + IN_MOVELEFT, + IN_MOVERIGHT, +}; + +stock int g_jumpDirLeftButton[JumpDir] = { + IN_MOVELEFT, + IN_MOVERIGHT, + IN_BACK, + IN_FORWARD, +}; + +stock int g_jumpDirRightButton[JumpDir] = { + IN_MOVERIGHT, + IN_MOVELEFT, + IN_FORWARD, + IN_BACK, +}; + +bool g_lateLoad; + +PlayerData g_pd[MAXPLAYERS + 1]; +PlayerData g_failstatPD[MAXPLAYERS + 1]; +int g_beamSprite; + +ConVar g_airaccelerate; +ConVar g_gravity; +ConVar g_maxvelocity; + +ConVar g_jumpRange[JumpType][2]; + +#include "distbugfix/clientprefs.sp" + +public Plugin myinfo = +{ + name = "Distance Bug Fix", + author = "GameChaos", + description = "Fixes longjump distance bug", + version = DISTBUG_VERSION, + url = "https://bitbucket.org/GameChaos/distbug/src" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_lateLoad = late; + + return APLRes_Success; +} + +public void OnPluginStart() +{ + RegConsoleCmd("sm_distbug", Command_Distbug, "Toggle distbug on/off."); + RegConsoleCmd("sm_distbugversion", Command_Distbugversion, "Print distbug version."); + + RegConsoleCmd("sm_distbugbeam", CommandBeam, "Toggle jump beam."); + RegConsoleCmd("sm_distbugveerbeam", CommandVeerbeam, "Toggle veer beam."); + RegConsoleCmd("sm_distbughudgraph", CommandHudgraph, "Toggle hud strafe graph."); + RegConsoleCmd("sm_strafestats", CommandStrafestats, "Toggle distbug strafestats."); + RegConsoleCmd("sm_distbugstrafegraph", CommandStrafegraph, "Toggle console strafe graph."); + RegConsoleCmd("sm_distbugadvchat", CommandAdvchat, "Toggle advanced chat stats."); + RegConsoleCmd("sm_distbughelp", CommandHelp, "Distbug command list."); + + g_airaccelerate = FindConVar("sv_airaccelerate"); + g_gravity = FindConVar("sv_gravity"); + g_maxvelocity = FindConVar("sv_maxvelocity"); + + g_jumpRange[JUMPTYPE_LJ][0] = CreateConVar("distbug_lj_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_LJ][1] = CreateConVar("distbug_lj_max_dist", "310.0"); + + g_jumpRange[JUMPTYPE_WJ][0] = CreateConVar("distbug_wj_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_WJ][1] = CreateConVar("distbug_wj_max_dist", "390.0"); + + g_jumpRange[JUMPTYPE_LAJ][0] = CreateConVar("distbug_laj_min_dist", "70.0"); + g_jumpRange[JUMPTYPE_LAJ][1] = CreateConVar("distbug_laj_max_dist", "250.0"); + + g_jumpRange[JUMPTYPE_BH][0] = CreateConVar("distbug_bh_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_BH][1] = CreateConVar("distbug_bh_max_dist", "390.0"); + + g_jumpRange[JUMPTYPE_CBH][0] = CreateConVar("distbug_cbh_min_dist", "200.0"); + g_jumpRange[JUMPTYPE_CBH][1] = CreateConVar("distbug_cbh_max_dist", "390.0"); + + AutoExecConfig(.name = DISTBUG_CONFIG_NAME); + + HookEvent("player_jump", Event_PlayerJump); + + OnPluginStart_Clientprefs(); + if (g_lateLoad) + { + for (int client = 0; client <= MaxClients; client++) + { + if (GCIsValidClient(client)) + { + OnClientPutInServer(client); + OnClientCookiesCached(client); + } + } + } +} + +public void OnMapStart() +{ + g_beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt"); +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_PostThinkPost, PlayerPostThink); + g_pd[client].tickCount = 0; +} + +public void OnClientCookiesCached(int client) +{ + OnClientCookiesCached_Clientprefs(client); +} + +public void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (!IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + return; + } + + if (GCIsValidClient(client, true)) + { + bool duckbhop = !!(g_pd[client].flags & FL_DUCKING); + float groundOffset = g_pd[client].position[2] - g_pd[client].lastGroundPos[2]; + JumpType jumpType = JUMPTYPE_NONE; + if (g_pd[client].framesOnGround <= MAX_BHOP_FRAMES) + { + if (g_pd[client].lastGroundPosWalkedOff && groundOffset < 0.0) + { + jumpType = JUMPTYPE_WJ; + } + else + { + if (duckbhop) + { + jumpType = JUMPTYPE_CBH; + } + else + { + jumpType = JUMPTYPE_BH; + } + } + } + else + { + jumpType = JUMPTYPE_LJ; + } + + if (jumpType != JUMPTYPE_NONE) + { + OnPlayerJumped(client, g_pd[client], jumpType); + } + + g_pd[client].lastGroundPos = g_pd[client].lastPosition; + g_pd[client].lastGroundPosWalkedOff = false; + } +} + +public Action Command_Distbugversion(int client, int args) +{ + ReplyToCommand(client, "Distbugfix version: %s", DISTBUG_VERSION); + return Plugin_Handled; +} + +public Action Command_Distbug(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISTBUG_ENABLED); + CPrintToChat(client, "%s Distbug has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandBeam(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_JUMP_BEAM); + CPrintToChat(client, "%s Jump beam has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_JUMP_BEAM) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandVeerbeam(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_VEER_BEAM); + CPrintToChat(client, "%s Veer beam has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_VEER_BEAM) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandHudgraph(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_HUD_GRAPH); + CPrintToChat(client, "%s Hud stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_HUD_GRAPH) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandStrafestats(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISABLE_STRAFE_STATS); + CPrintToChat(client, "%s Strafe stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_STATS) ? "disabled." : "enabled."); + + return Plugin_Handled; +} + +public Action CommandStrafegraph(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISABLE_STRAFE_GRAPH); + CPrintToChat(client, "%s Console strafe graph has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_GRAPH) ? "disabled." : "enabled."); + + return Plugin_Handled; +} + +public Action CommandAdvchat(int client, int args) +{ + ToggleSetting(client, SETTINGS_ADV_CHAT_STATS); + CPrintToChat(client, "%s Advanced chat stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_ADV_CHAT_STATS) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandHelp(int client, int args) +{ + CPrintToChat(client, "%s Look in the console for a list of distbug commands!", CHAT_PREFIX); + PrintToConsole(client, "%s", "Distbug command list:\n" ...\ + "sm_distbug - Toggle distbug on/off.\n" ...\ + "sm_distbugversion - Print distbug version.\n" ...\ + "sm_distbugbeam - Toggle jump beam.\n" ...\ + "sm_distbugveerbeam - Toggle veer beam.\n" ...\ + "sm_distbughudgraph - Toggle hud strafe graph.\n" ...\ + "sm_strafestats - Toggle distbug strafestats.\n" ...\ + "sm_distbugstrafegraph - Toggle console strafe graph.\n" ...\ + "sm_distbugadvchat - Toggle advanced chat stats.\n" ...\ + "sm_distbughelp - Distbug command list.\n"); + return Plugin_Handled; +} + +public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2]) +{ + if (!GCIsValidClient(client, true)) + { + return Plugin_Continue; + } + + if (!IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + return Plugin_Continue; + } + + g_pd[client].lastSidemove = g_pd[client].sidemove; + g_pd[client].lastForwardmove = g_pd[client].forwardmove; + g_pd[client].sidemove = vel[1]; + g_pd[client].forwardmove = vel[0]; + + return Plugin_Continue; +} + +public void PlayerPostThink(int client) +{ + if (!GCIsValidClient(client, true)) + { + return; + } + + int flags = GetEntityFlags(client); + g_pd[client].lastButtons = g_pd[client].buttons; + g_pd[client].buttons = GetClientButtons(client); + g_pd[client].lastFlags = g_pd[client].flags; + g_pd[client].flags = flags; + g_pd[client].lastPosition = g_pd[client].position; + g_pd[client].lastAngles = g_pd[client].angles; + g_pd[client].lastVelocity = g_pd[client].velocity; + GetClientAbsOrigin(client, g_pd[client].position); + GetClientEyeAngles(client, g_pd[client].angles); + GCGetClientVelocity(client, g_pd[client].velocity); + GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", g_pd[client].ladderNormal); + + if (flags & FL_ONGROUND) + { + g_pd[client].framesInAir = 0; + g_pd[client].framesOnGround++; + } + else if (g_pd[client].movetype != MOVETYPE_LADDER) + { + g_pd[client].framesInAir++; + g_pd[client].framesOnGround = 0; + } + + g_pd[client].lastMovetype = g_pd[client].movetype; + g_pd[client].movetype = GetEntityMoveType(client); + g_pd[client].stamina = GCGetClientStamina(client); + g_pd[client].lastStamina = g_pd[client].stamina; + g_pd[client].gravity = GetEntityGravity(client); + + // LJ stuff + if (IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + if (g_pd[client].framesInAir == 1) + { + if (!GCVectorsEqual(g_pd[client].lastGroundPos, g_pd[client].lastPosition)) + { + g_pd[client].lastGroundPos = g_pd[client].lastPosition; + g_pd[client].lastGroundPosWalkedOff = true; + } + } + + bool forwardReleased = (g_pd[client].lastButtons & g_jumpDirForwardButton[g_pd[client].jumpDir]) + && !(g_pd[client].buttons & g_jumpDirForwardButton[g_pd[client].jumpDir]); + if (forwardReleased) + { + g_pd[client].fwdReleaseFrame = g_pd[client].tickCount; + } + + if (!g_pd[client].trackingJump + && g_pd[client].movetype == MOVETYPE_WALK + && g_pd[client].lastMovetype == MOVETYPE_LADDER) + { + OnPlayerJumped(client, g_pd[client], JUMPTYPE_LAJ); + } + + if (g_pd[client].framesOnGround == 1) + { + TrackJump(g_pd[client], g_failstatPD[client]); + OnPlayerLanded(client, g_pd[client], g_failstatPD[client]); + } + + if (g_pd[client].trackingJump) + { + TrackJump(g_pd[client], g_failstatPD[client]); + } + } + g_pd[client].tickCount++; + + +#if defined(DEBUG) + SetHudTextParams(-1.0, 0.2, 0.02, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0); + ShowHudText(client, -1, "pos: %f %f %f", g_pd[client].position[0], g_pd[client].position[1], g_pd[client].position[2]); +#endif +} + +bool IsSpectating(int spectator, int target) +{ + if (spectator != target && GCIsValidClient(spectator)) + { + int specMode = GetEntProp(spectator, Prop_Send, "m_iObserverMode"); + if (specMode == 4 || specMode == 5) + { + if (GetEntPropEnt(spectator, Prop_Send, "m_hObserverTarget") == target) + { + return true; + } + } + } + return false; +} + +void ClientAndSpecsPrintChat(int client, const char[] format, any ...) +{ + static char message[1024]; + VFormat(message, sizeof(message), format, 3); + CPrintToChat(client, "%s", message); + + for (int spec = 1; spec <= MaxClients; spec++) + { + if (IsSpectating(spec, client) && IsSettingEnabled(spec, SETTINGS_DISTBUG_ENABLED)) + { + CPrintToChat(spec, "%s", message); + } + } +} + +void ClientAndSpecsPrintConsole(int client, const char[] format, any ...) +{ + static char message[1024]; + VFormat(message, sizeof(message), format, 3); + PrintToConsole(client, "%s", message); + + for (int spec = 1; spec < MAXPLAYERS; spec++) + { + if (IsSpectating(spec, client) && IsSettingEnabled(spec, SETTINGS_DISTBUG_ENABLED)) + { + PrintToConsole(spec, "%s", message); + } + } +} + +void ResetJump(PlayerData pd) +{ + // NOTE: only resets things that need to be reset + for (int i = 0; i < 3; i++) + { + pd.jumpPos[i] = 0.0; + pd.landPos[i] = 0.0; + } + pd.trackingJump = false; + pd.failedJump = false; + pd.jumpGotFailstats = false; + + // Jump data + // pd.jumpType = JUMPTYPE_NONE; + // NOTE: don't reset jumpType or lastJumpType + pd.jumpMaxspeed = 0.0; + pd.jumpSync = 0.0; + pd.jumpEdge = 0.0; + pd.jumpBlockDist = 0.0; + pd.jumpHeight = 0.0; + pd.jumpAirtime = 0; + pd.jumpOverlap = 0; + pd.jumpDeadair = 0; + pd.jumpAirpath = 0.0; + + pd.strafeCount = 0; + for (int i = 0; i < MAX_STRAFES; i++) + { + pd.strafeSync[i] = 0.0; + pd.strafeGain[i] = 0.0; + pd.strafeLoss[i] = 0.0; + pd.strafeMax[i] = 0.0; + pd.strafeAirtime[i] = 0; + pd.strafeOverlap[i] = 0; + pd.strafeDeadair[i] = 0; + pd.strafeAvgGain[i] = 0.0; + pd.strafeAvgEfficiency[i] = 0.0; + pd.strafeAvgEfficiencyCount[i] = 0; + pd.strafeMaxEfficiency[i] = GC_FLOAT_NEGATIVE_INFINITY; + } +} + +bool IsWishspeedMovingLeft(float forwardspeed, float sidespeed, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS) + { + return sidespeed < 0.0; + } + else if (jumpDir == JUMPDIR_BACKWARDS) + { + return sidespeed > 0.0; + } + else if (jumpDir == JUMPDIR_LEFT) + { + return forwardspeed < 0.0; + } + // else if (jumpDir == JUMPDIR_RIGHT) + return forwardspeed > 0.0; +} + +bool IsWishspeedMovingRight(float forwardspeed, float sidespeed, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS) + { + return sidespeed > 0.0; + } + else if (jumpDir == JUMPDIR_BACKWARDS) + { + return sidespeed < 0.0; + } + else if (jumpDir == JUMPDIR_LEFT) + { + return forwardspeed > 0.0; + } + // else if (jumpDir == JUMPDIR_RIGHT) + return forwardspeed < 0.0; +} + +bool IsNewStrafe(PlayerData pd) +{ + if (pd.jumpDir == JUMPDIR_FORWARDS || pd.jumpDir == JUMPDIR_BACKWARDS) + { + return ((pd.sidemove > 0.0 && pd.lastSidemove <= 0.0) + || (pd.sidemove < 0.0 && pd.lastSidemove >= 0.0)) + && pd.jumpAirtime != 1; + } + // else if (pd.jumpDir == JUMPDIR_LEFT || pd.jumpDir == JUMPDIR_RIGHT) + return ((pd.forwardmove > 0.0 && pd.lastForwardmove <= 0.0) + || (pd.forwardmove < 0.0 && pd.lastForwardmove >= 0.0)) + && pd.jumpAirtime != 1; +} + +void TrackJump(PlayerData pd, PlayerData failstatPD) +{ +#if defined(DEBUG) + SetHudTextParams(-1.0, 0.2, 0.02, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0); + ShowHudText(1, -1, "FOG: %i\njumpAirtime: %i\ntrackingJump: %i", pd.framesOnGround, pd.jumpAirtime, pd.trackingJump); +#endif + + if (pd.framesOnGround > MAX_BHOP_FRAMES + && pd.jumpAirtime && pd.trackingJump) + { + ResetJump(pd); + } + + if (pd.jumpType == JUMPTYPE_NONE + || !g_jumpTypePrintable[pd.jumpType]) + { + pd.trackingJump = false; + return; + } + + if (pd.movetype != MOVETYPE_WALK + && pd.movetype != MOVETYPE_LADDER) + { + ResetJump(pd); + } + + float frametime = GetTickInterval(); + // crusty teleport detection + { + float posDelta[3]; + SubtractVectors(pd.position, pd.lastPosition, posDelta); + + float moveLength = GetVectorLength(posDelta); + // NOTE: 1.73205081 * sv_maxvelocity is the max velocity magnitude you can get. + if (moveLength > g_maxvelocity.FloatValue * 1.73205081 * frametime) + { + ResetJump(pd); + return; + } + } + + int beamIndex = pd.jumpAirtime; + if (beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamX[beamIndex] = pd.position[0]; + pd.jumpBeamY[beamIndex] = pd.position[1]; + pd.jumpBeamColour[beamIndex] = JUMPBEAM_NEUTRAL; + } + pd.jumpAirtime++; + + + float speed = GCGetVectorLength2D(pd.velocity); + if (speed > pd.jumpMaxspeed) + { + pd.jumpMaxspeed = speed; + } + + float lastSpeed = GCGetVectorLength2D(pd.lastVelocity); + if (speed > lastSpeed) + { + pd.jumpSync++; + if (beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_GAIN; + } + } + else if (speed < lastSpeed && beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_LOSS; + } + + if (pd.flags & FL_DUCKING && beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_DUCK; + } + + float height = pd.position[2] - pd.jumpPos[2]; + if (height > pd.jumpHeight) + { + pd.jumpHeight = height; + } + + if (IsOverlapping(pd.buttons, pd.jumpDir)) + { + pd.jumpOverlap++; + } + + if (IsDeadAirtime(pd.buttons, pd.jumpDir)) + { + pd.jumpDeadair++; + } + + // strafestats! + if (pd.strafeCount + 1 < MAX_STRAFES) + { + if (IsNewStrafe(pd)) + { + pd.strafeCount++; + } + + int strafe = pd.strafeCount; + + pd.strafeAirtime[strafe]++; + + if (speed > lastSpeed) + { + pd.strafeSync[strafe] += 1.0; + pd.strafeGain[strafe] += speed - lastSpeed; + } + else if (speed < lastSpeed) + { + pd.strafeLoss[strafe] += lastSpeed - speed; + } + + if (speed > pd.strafeMax[strafe]) + { + pd.strafeMax[strafe] = speed; + } + + if (IsOverlapping(pd.buttons, pd.jumpDir)) + { + pd.strafeOverlap[strafe]++; + } + + if (IsDeadAirtime(pd.buttons, pd.jumpDir)) + { + pd.strafeDeadair[strafe]++; + } + + // efficiency! + { + float maxWishspeed = 30.0; + float airaccelerate = g_airaccelerate.FloatValue; + // NOTE: Assume 250 maxspeed cos this is KZ! + float maxspeed = 250.0; + if (pd.flags & FL_DUCKING) + { + maxspeed *= 0.34; + } + else if (pd.buttons & IN_SPEED) + { + maxspeed *= 0.52; + } + + if (pd.lastStamina > 0) + { + float speedScale = GCFloatClamp(1.0 - pd.lastStamina / 100.0, 0.0, 1.0); + speedScale *= speedScale; + maxspeed *= speedScale; + } + + // calculate zvel 1 tick before pd.lastVelocity and during movement processing + float zvel = pd.lastVelocity[2] + (g_gravity.FloatValue * frametime * 0.5 * pd.gravity); + if (zvel > 0.0 && zvel <= 140.0) + { + maxspeed *= 0.25; + } + + float yawdiff = FloatAbs(GCNormaliseYaw(pd.angles[1] - pd.lastAngles[1])); + float perfectYawDiff = yawdiff; + if (lastSpeed > 0.0) + { + float accelspeed = airaccelerate * maxspeed * frametime; + if (accelspeed > maxWishspeed) + { + accelspeed = maxWishspeed; + } + if (lastSpeed >= maxWishspeed) + { + perfectYawDiff = RadToDeg(ArcSine(accelspeed / lastSpeed)); + } + else + { + perfectYawDiff = 0.0; + } + } + float efficiency = 100.0; + if (perfectYawDiff != 0.0) + { + efficiency = (yawdiff - perfectYawDiff) / perfectYawDiff * 100.0 + 100.0; + } + + pd.strafeAvgEfficiency[strafe] += efficiency; + pd.strafeAvgEfficiencyCount[strafe]++; + if (efficiency > pd.strafeMaxEfficiency[strafe]) + { + pd.strafeMaxEfficiency[strafe] = efficiency; + } + + DEBUG_CONSOLE(1, "%i\t%f\t%f\t%f\t%f\t%f", strafe, (yawdiff - perfectYawDiff), pd.sidemove, yawdiff, perfectYawDiff, speed) + } + } + + // strafe type and mouse graph + if (pd.jumpAirtime - 1 < MAX_JUMP_FRAMES) + { + StrafeType strafeType = STRAFETYPE_NONE; + + bool moveLeft = !!(pd.buttons & g_jumpDirLeftButton[pd.jumpDir]); + bool moveRight = !!(pd.buttons & g_jumpDirRightButton[pd.jumpDir]); + + bool velLeft = IsWishspeedMovingLeft(pd.forwardmove, pd.sidemove, pd.jumpDir); + bool velRight = IsWishspeedMovingRight(pd.forwardmove, pd.sidemove, pd.jumpDir); + bool velIsZero = !velLeft && !velRight; + + if (moveLeft && !moveRight && velLeft) + { + strafeType = STRAFETYPE_LEFT; + } + else if (moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_RIGHT; + } + else if (moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_LEFT; + } + else if (moveRight && moveLeft && velIsZero) + { + strafeType = STRAFETYPE_OVERLAP; + } + else if (moveRight && moveLeft && velLeft) + { + strafeType = STRAFETYPE_OVERLAP_LEFT; + } + else if (moveRight && moveLeft && velRight) + { + strafeType = STRAFETYPE_OVERLAP_RIGHT; + } + else if (!moveRight && !moveLeft && velIsZero) + { + strafeType = STRAFETYPE_NONE; + } + else if (!moveRight && !moveLeft && velLeft) + { + strafeType = STRAFETYPE_NONE_LEFT; + } + else if (!moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_NONE_RIGHT; + } + + pd.strafeGraph[pd.jumpAirtime - 1] = strafeType; + float yawDiff = GCNormaliseYaw(pd.angles[1] - pd.lastAngles[1]); + // Offset index by 2 to align mouse movement with button presses. + int yawIndex = GCIntMax(pd.jumpAirtime - 2, 0); + pd.mouseGraph[yawIndex] = yawDiff; + } + // check for failstat after jump tracking is done + float duckedPos[3]; + duckedPos = pd.position; + if (!(pd.flags & FL_DUCKING)) + { + duckedPos[2] += 9.0; + } + + // only save failed jump if we're at the fail threshold + if ((pd.position[2] < pd.jumpPos[2]) + && (pd.position[2] > pd.jumpPos[2] + (pd.velocity[2] * frametime))) + { + pd.jumpGotFailstats = true; + failstatPD = pd; + } + + // airpath. + // NOTE: Track airpath after failstatPD has been saved, so + // we don't track the last frame of failstats. That should + // happen inside of FinishTrackingJump, because we need the real landing position. + if (!pd.framesOnGround) + { + // NOTE: there's a special case for landing frame. + float delta[3]; + SubtractVectors(pd.position, pd.lastPosition, delta); + pd.jumpAirpath += GCGetVectorLength2D(delta); + } +} + +void OnPlayerFailstat(int client, PlayerData pd) +{ + if (!pd.jumpGotFailstats) + { + ResetJump(pd); + return; + } + + pd.failedJump = true; + + // undo half the gravity + float gravity = g_gravity.FloatValue * pd.gravity; + float frametime = GetTickInterval(); + float fixedVelocity[3]; + fixedVelocity = pd.velocity; + fixedVelocity[2] += gravity * 0.5 * frametime; + + // fix incorrect distance when ducking / unducking at the right time + float lastPosition[3]; + lastPosition = pd.lastPosition; + bool lastDucking = !!(pd.lastFlags & FL_DUCKING); + bool ducking = !!(pd.flags & FL_DUCKING); + if (!lastDucking && ducking) + { + lastPosition[2] += 9.0; + } + else if (lastDucking && !ducking) + { + lastPosition[2] -= 9.0; + } + + GetRealLandingOrigin(pd.jumpPos[2], lastPosition, fixedVelocity, pd.landPos); + pd.jumpDistance = GCGetVectorDistance2D(pd.jumpPos, pd.landPos); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpDistance += 32.0; + } + + FinishTrackingJump(client, pd); + PrintStats(client, pd); + ResetJump(pd); +} + +void OnPlayerJumped(int client, PlayerData pd, JumpType jumpType) +{ + pd.lastJumpType = pd.jumpType; + ResetJump(pd); + pd.jumpType = jumpType; + if (g_jumpTypePrintable[jumpType]) + { + pd.trackingJump = true; + } + + pd.prespeedFog = pd.framesOnGround; + pd.prespeedStamina = pd.stamina; + + // DEBUG_CHAT(1, "jump type: %s last jump type: %s", g_jumpTypes[jumpType], g_jumpTypes[pd.lastJumpType]) + + // jump direction + float speed = GCGetVectorLength2D(pd.velocity); + pd.jumpDir = JUMPDIR_FORWARDS; + // NOTE: Ladderjump pres can be super wild and can generate random + // jump directions, so default to forward for ladderjumps. + if (speed > 50.0 && pd.jumpType != JUMPTYPE_LAJ) + { + float velDir = RadToDeg(ArcTangent2(pd.velocity[1], pd.velocity[0])); + float dir = GCNormaliseYaw(pd.angles[1] - velDir); + + if (GCIsFloatInRange(dir, 45.0, 135.0)) + { + pd.jumpDir = JUMPDIR_RIGHT; + } + if (GCIsFloatInRange(dir, -135.0, -45.0)) + { + pd.jumpDir = JUMPDIR_LEFT; + } + else if (dir > 135.0 || dir < -135.0) + { + pd.jumpDir = JUMPDIR_BACKWARDS; + } + } + + if (jumpType != JUMPTYPE_LAJ) + { + pd.jumpFrame = pd.tickCount; + pd.jumpPos = pd.position; + pd.jumpAngles = pd.angles; + + DEBUG_CHAT(client, "jumppos z: %f", pd.jumpPos[2]) + + pd.jumpPrespeed = GCGetVectorLength2D(pd.velocity); + + pd.jumpGroundZ = pd.jumpPos[2]; + float ground[3]; + if (GCTraceGround(client, pd.jumpPos, ground)) + { + pd.jumpGroundZ = ground[2]; + } + else + { + DEBUG_CHATALL("AAAAAAAAAAAAA") + } + } + else + { + // NOTE: for ladderjump set prespeed and stamina to values that don't get shown + pd.prespeedFog = -1; + pd.prespeedStamina = 0.0; + pd.jumpFrame = pd.tickCount - 1; + pd.jumpPos = pd.lastPosition; + pd.jumpAngles = pd.lastAngles; + + pd.jumpPrespeed = GCGetVectorLength2D(pd.lastVelocity); + + // find ladder top + + float traceOrigin[3]; + // 10 units is the furthest away from the ladder surface you can get while still being on the ladder + traceOrigin[0] = pd.jumpPos[0] - 10.0 * pd.ladderNormal[0]; + traceOrigin[1] = pd.jumpPos[1] - 10.0 * pd.ladderNormal[1]; + traceOrigin[2] = pd.jumpPos[2] + 400.0 * GetTickInterval(); // ~400 ups is the fastest vertical speed on ladders + + float traceEnd[3]; + traceEnd = traceOrigin; + traceEnd[2] = pd.jumpPos[2] - 400.0 * GetTickInterval(); + + float mins[3]; + GetClientMins(client, mins); + + float maxs[3]; + GetClientMaxs(client, maxs); + + TR_TraceHullFilter(traceOrigin, traceEnd, mins, maxs, CONTENTS_LADDER, GCTraceEntityFilterPlayer); + + pd.jumpGroundZ = pd.jumpPos[2]; + if (TR_DidHit()) + { + float result[3]; + TR_GetEndPosition(result); + pd.jumpGroundZ = result[2]; + } + } +} + +void OnPlayerLanded(int client, PlayerData pd, PlayerData failstatPD) +{ + pd.landedDucked = !!(pd.flags & FL_DUCKING); + + if (!pd.trackingJump + || pd.jumpType == JUMPTYPE_NONE + || !g_jumpTypePrintable[pd.jumpType]) + { + ResetJump(pd); + return; + } + + if (pd.jumpType != JUMPTYPE_LAJ) + { + float roughOffset = pd.position[2] - pd.jumpPos[2]; + if (0.0 < roughOffset > 2.0) + { + ResetJump(pd); + return; + } + } + + { + float landGround[3]; + GCTraceGround(client, pd.position, landGround); + pd.landGroundZ = landGround[2]; + } + + float offsetTolerance = 0.0001; + if (!GCIsRoughlyEqual(pd.jumpGroundZ, pd.landGroundZ, offsetTolerance) && pd.jumpGotFailstats) + { + OnPlayerFailstat(client, failstatPD); + return; + } + + float landOrigin[3]; + float gravity = g_gravity.FloatValue * pd.gravity; + float frametime = GetTickInterval(); + float fixedVelocity[3]; + float airOrigin[3]; + + // fix incorrect landing position + float lastPosition[3]; + lastPosition = pd.lastPosition; + bool lastDucking = !!(pd.lastFlags & FL_DUCKING); + bool ducking = !!(pd.flags & FL_DUCKING); + if (!lastDucking && ducking) + { + lastPosition[2] += 9.0; + } + else if (lastDucking && !ducking) + { + lastPosition[2] -= 9.0; + } + + bool isBugged = pd.lastPosition[2] - pd.landGroundZ < 2.0; + if (isBugged) + { + fixedVelocity = pd.velocity; + // NOTE: The 0.5 here removes half the gravity in a tick, because + // in pmove code half the gravity is applied before movement calculation and the other half after it's finished. + // We're trying to fix a bug that happens in the middle of movement code. + fixedVelocity[2] = pd.lastVelocity[2] - gravity * 0.5 * frametime; + airOrigin = lastPosition; + } + else + { + // NOTE: calculate current frame's z velocity + float tempVel[3]; + tempVel = pd.velocity; + tempVel[2] = pd.lastVelocity[2] - gravity * 0.5 * frametime; + // NOTE: calculate velocity after the current frame. + fixedVelocity = tempVel; + fixedVelocity[2] -= gravity * frametime; + + airOrigin = pd.position; + } + + GetRealLandingOrigin(pd.landGroundZ, airOrigin, fixedVelocity, landOrigin); + pd.landPos = landOrigin; + + pd.jumpDistance = (GCGetVectorDistance2D(pd.jumpPos, pd.landPos)); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpDistance += 32.0; + } + + if (GCIsFloatInRange(pd.jumpDistance, + g_jumpRange[pd.jumpType][0].FloatValue, + g_jumpRange[pd.jumpType][1].FloatValue)) + { + FinishTrackingJump(client, pd); + + PrintStats(client, pd); + } + else + { + DEBUG_CHAT(client, "bad jump distance %f", pd.jumpDistance) + } + ResetJump(pd); +} + +void FinishTrackingJump(int client, PlayerData pd) +{ + // finish up stats: + float xAxisVeer = FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + float yAxisVeer = FloatAbs(pd.landPos[1] - pd.jumpPos[1]); + pd.jumpVeer = GCFloatMin(xAxisVeer, yAxisVeer); + + pd.jumpFwdRelease = pd.fwdReleaseFrame - pd.jumpFrame; + pd.jumpSync = (pd.jumpSync / float(pd.jumpAirtime) * 100.0); + + for (int strafe; strafe < pd.strafeCount + 1; strafe++) + { + // average gain + pd.strafeAvgGain[strafe] = (pd.strafeGain[strafe] / pd.strafeAirtime[strafe]); + + // efficiency! + if (pd.strafeAvgEfficiencyCount[strafe]) + { + pd.strafeAvgEfficiency[strafe] /= float(pd.strafeAvgEfficiencyCount[strafe]); + } + else + { + pd.strafeAvgEfficiency[strafe] = GC_FLOAT_NAN; + } + + // sync + + if (pd.strafeAirtime[strafe] != 0.0) + { + pd.strafeSync[strafe] = (pd.strafeSync[strafe] / float(pd.strafeAirtime[strafe]) * 100.0); + } + else + { + pd.strafeSync[strafe] = 0.0; + } + } + + // airpath! + { + float delta[3]; + SubtractVectors(pd.landPos, pd.lastPosition, delta); + pd.jumpAirpath += GCGetVectorLength2D(delta); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpAirpath = (pd.jumpAirpath / (pd.jumpDistance - 32.0)); + } + else + { + pd.jumpAirpath = (pd.jumpAirpath / (pd.jumpDistance)); + } + } + + pd.jumpBlockDist = -1.0; + pd.jumpLandEdge = -9999.9; + pd.jumpEdge = -1.0; + // Calculate block distance and jumpoff edge + if (pd.jumpType != JUMPTYPE_LAJ) + { + int blockAxis = FloatAbs(pd.landPos[1] - pd.jumpPos[1]) > FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + int blockDir = FloatSign(pd.jumpPos[blockAxis] - pd.landPos[blockAxis]); + + float jumpOrigin[3]; + float landOrigin[3]; + jumpOrigin = pd.jumpPos; + landOrigin = pd.landPos; + // move origins 2 units down, so we can touch the side of the lj blocks + jumpOrigin[2] -= 2.0; + landOrigin[2] -= 2.0; + + // extend land origin, so if we fail within 16 units of the block we can still get the block distance. + landOrigin[blockAxis] -= float(blockDir) * 16.0; + + float tempPos[3]; + tempPos = landOrigin; + tempPos[blockAxis] += (jumpOrigin[blockAxis] - landOrigin[blockAxis]) / 2.0; + + float jumpEdge[3]; + GCTraceBlock(tempPos, jumpOrigin, jumpEdge); + + tempPos = jumpOrigin; + tempPos[blockAxis] += (landOrigin[blockAxis] - jumpOrigin[blockAxis]) / 2.0; + + bool block; + float landEdge[3]; + block = GCTraceBlock(tempPos, landOrigin, landEdge); + + if (block) + { + pd.jumpBlockDist = (FloatAbs(landEdge[blockAxis] - jumpEdge[blockAxis]) + 32.0); + pd.jumpLandEdge = ((landEdge[blockAxis] - pd.landPos[blockAxis]) * float(blockDir)); + } + + if (jumpEdge[blockAxis] - tempPos[blockAxis] != 0.0) + { + pd.jumpEdge = FloatAbs(jumpOrigin[blockAxis] - jumpEdge[blockAxis]); + } + } + else + { + int blockAxis = FloatAbs(pd.landPos[1] - pd.jumpPos[1]) > FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + int blockDir = FloatSign(pd.jumpPos[blockAxis] - pd.landPos[blockAxis]); + + // find ladder front + + float traceOrigin[3]; + // 10 units is the furthest away from the ladder surface you can get while still being on the ladder + traceOrigin[0] = pd.jumpPos[0]; + traceOrigin[1] = pd.jumpPos[1]; + traceOrigin[2] = pd.jumpPos[2] - 400.0 * GetTickInterval(); // ~400 ups is the fastest vertical speed on ladders + + // leave enough room to trace the front of the ladder + traceOrigin[blockAxis] += blockDir * 40.0; + + float traceEnd[3]; + traceEnd = traceOrigin; + traceEnd[blockAxis] -= blockDir * 50.0; + + float mins[3]; + GetClientMins(client, mins); + + float maxs[3]; + GetClientMaxs(client, maxs); + maxs[2] = mins[2]; + + TR_TraceHullFilter(traceOrigin, traceEnd, mins, maxs, CONTENTS_LADDER, GCTraceEntityFilterPlayer); + + float jumpEdge[3]; + if (TR_DidHit()) + { + TR_GetEndPosition(jumpEdge); + DEBUG_CHAT(1, "ladder front: %f %f %f", jumpEdge[0], jumpEdge[1], jumpEdge[2]) + + float jumpOrigin[3]; + float landOrigin[3]; + jumpOrigin = pd.jumpPos; + landOrigin = pd.landPos; + // move origins 2 units down, so we can touch the side of the lj blocks + jumpOrigin[2] -= 2.0; + landOrigin[2] -= 2.0; + + // extend land origin, so if we fail within 16 units of the block we can still get the block distance. + landOrigin[blockAxis] -= float(blockDir) * 16.0; + + float tempPos[3]; + tempPos = jumpOrigin; + tempPos[blockAxis] += (landOrigin[blockAxis] - jumpOrigin[blockAxis]) / 2.0; + + float landEdge[3]; + bool land = GCTraceBlock(tempPos, landOrigin, landEdge); + DEBUG_CHAT(1, "tracing from %f %f %f to %f %f %f", tempPos[0], tempPos[1], tempPos[2], landOrigin[0], landOrigin[1], landOrigin[2]) + + if (land) + { + pd.jumpBlockDist = (FloatAbs(landEdge[blockAxis] - jumpEdge[blockAxis])); + pd.jumpLandEdge = ((landEdge[blockAxis] - pd.landPos[blockAxis]) * float(blockDir)); + } + + pd.jumpEdge = FloatAbs(jumpOrigin[blockAxis] - jumpEdge[blockAxis]); + } + } + + // jumpoff angle! + { + float airpathDir[3]; + SubtractVectors(pd.landPos, pd.jumpPos, airpathDir); + NormalizeVector(airpathDir, airpathDir); + + float airpathAngles[3]; + GetVectorAngles(airpathDir, airpathAngles); + float airpathYaw = GCNormaliseYaw(airpathAngles[1]); + + pd.jumpJumpoffAngle = GCNormaliseYaw(airpathYaw - pd.jumpAngles[1]); + } +} + +void PrintStats(int client, PlayerData pd) +{ + // beams! + if (IsSettingEnabled(client, SETTINGS_SHOW_VEER_BEAM)) + { + float beamEnd[3]; + beamEnd[0] = pd.landPos[0]; + beamEnd[1] = pd.jumpPos[1]; + beamEnd[2] = pd.landPos[2]; + float jumpPos[3]; + float landPos[3]; + for (int i = 0; i < 3; i++) + { + jumpPos[i] = pd.jumpPos[i]; + landPos[i] = pd.landPos[i]; + } + + GCTE_SetupBeamPoints(.start = jumpPos, .end = landPos, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {255, 255, 255, 95}); + TE_SendToClient(client); + + // x axis + GCTE_SetupBeamPoints(.start = jumpPos, .end = beamEnd, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {255, 0, 255, 95}); + TE_SendToClient(client); + // y axis + GCTE_SetupBeamPoints(.start = landPos, .end = beamEnd, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {0, 255, 0, 95}); + TE_SendToClient(client); + } + + if (IsSettingEnabled(client, SETTINGS_SHOW_JUMP_BEAM)) + { + float beamPos[3]; + float lastBeamPos[3]; + beamPos[0] = pd.jumpPos[0]; + beamPos[1] = pd.jumpPos[1]; + beamPos[2] = pd.jumpPos[2]; + for (int i = 1; i < pd.jumpAirtime; i++) + { + lastBeamPos = beamPos; + beamPos[0] = pd.jumpBeamX[i]; + beamPos[1] = pd.jumpBeamY[i]; + + int colour[4] = {255, 191, 0, 255}; + if (pd.jumpBeamColour[i] == JUMPBEAM_LOSS) + { + colour = {255, 0, 255, 255}; + } + else if (pd.jumpBeamColour[i] == JUMPBEAM_GAIN) + { + colour = {0, 127, 0, 255}; + } + else if (pd.jumpBeamColour[i] == JUMPBEAM_DUCK) + { + colour = {0, 31, 127, 255}; + } + + GCTE_SetupBeamPoints(.start = lastBeamPos, .end = beamPos, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = colour); + TE_SendToClient(client); + } + } + + char fwdRelease[32] = ""; + if (pd.jumpFwdRelease == 0) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {gr}0"); + } + else if (GCIntAbs(pd.jumpFwdRelease) > 16) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {dr}No"); + } + else if (pd.jumpFwdRelease > 0) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {dr}+%i", pd.jumpFwdRelease); + } + else + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {sb}%i", pd.jumpFwdRelease); + } + + char edge[32] = ""; + char chatEdge[32] = ""; + bool hasEdge = false; + if (pd.jumpEdge >= 0.0 && pd.jumpEdge < MAX_EDGE) + { + FormatEx(edge, sizeof(edge), "Edge: %.4f", pd.jumpEdge); + FormatEx(chatEdge, sizeof(chatEdge), "Edge: {l}%.2f{g}", pd.jumpEdge); + hasEdge = true; + } + + char block[32] = ""; + char chatBlock[32] = ""; + bool hasBlock = false; + if (GCIsFloatInRange(pd.jumpBlockDist, + g_jumpRange[pd.jumpType][0].FloatValue, + g_jumpRange[pd.jumpType][1].FloatValue)) + { + FormatEx(block, sizeof(block), "Block: %i", RoundFloat(pd.jumpBlockDist)); + FormatEx(chatBlock, sizeof(chatBlock), "({l}%i{g})", RoundFloat(pd.jumpBlockDist)); + hasBlock = true; + } + + char landEdge[32] = ""; + bool hasLandEdge = false; + if (FloatAbs(pd.jumpLandEdge) < MAX_EDGE) + { + FormatEx(landEdge, sizeof(landEdge), "Land Edge: %.4f", pd.jumpLandEdge); + hasLandEdge = true; + } + + char fog[32]; + bool hasFOG = false; + if (pd.prespeedFog <= MAX_BHOP_FRAMES && pd.prespeedFog >= 0) + { + FormatEx(fog, sizeof(fog), "FOG: %i", pd.prespeedFog); + hasFOG = true; + } + + char stamina[32]; + bool hasStamina = false; + if (pd.prespeedStamina != 0.0) + { + FormatEx(stamina, sizeof(stamina), "Stamina: %.1f", pd.prespeedStamina); + hasStamina = true; + } + + char offset[32]; + bool hasOffset = false; + if (pd.jumpGroundZ != pd.jumpPos[2]) + { + FormatEx(offset, sizeof(offset), "Ground offset: %.4f", pd.jumpPos[2] - pd.jumpGroundZ); + hasOffset = true; + } + + + //ClientAndSpecsPrintChat(client, "%s", chatStats); + + // TODO: remove jump direction from ladderjumps + char consoleStats[1024]; + FormatEx(consoleStats, sizeof(consoleStats), "\n"...CONSOLE_PREFIX..." %s%s: %.5f [%s%s%s%sVeer: %.4f | %s | Sync: %.2f | Max: %.3f]\n"...\ + "[%s%sPre: %.4f | OL/DA: %i/%i | Jumpoff Angle: %.3f | Airpath: %.4f]\n"...\ + "[Strafes: %i | Airtime: %i | Jump Direction: %s | %s%sHeight: %.4f%s%s%s%s]", + pd.failedJump ? "FAILED " : "", + g_jumpTypes[pd.jumpType], + pd.jumpDistance, + block, + hasBlock ? " | " : "", + edge, + hasEdge ? " | " : "", + pd.jumpVeer, + fwdRelease, + pd.jumpSync, + pd.jumpMaxspeed, + + landEdge, + hasLandEdge ? " | " : "", + pd.jumpPrespeed, + pd.jumpOverlap, + pd.jumpDeadair, + pd.jumpJumpoffAngle, + pd.jumpAirpath, + + pd.strafeCount + 1, + pd.jumpAirtime, + g_jumpDirString[pd.jumpDir], + fog, + hasFOG ? " | " : "", + pd.jumpHeight, + hasOffset ? " | " : "", + offset, + hasStamina ? " | " : "", + stamina + ); + + CRemoveTags(consoleStats, sizeof(consoleStats)); + ClientAndSpecsPrintConsole(client, consoleStats); + + if (!IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_STATS)) + { + ClientAndSpecsPrintConsole(client, " #. Sync Gain Loss Max Air OL DA AvgGain Avg efficiency, (max efficiency)"); + for (int strafe; strafe <= pd.strafeCount && strafe < MAX_STRAFES; strafe++) + { + ClientAndSpecsPrintConsole(client, "%2i. %5.1f%% %6.2f %6.2f %5.1f %3i %3i %3i %3.2f %3i%% (%3i%%)", + strafe + 1, + pd.strafeSync[strafe], + pd.strafeGain[strafe], + pd.strafeLoss[strafe], + pd.strafeMax[strafe], + pd.strafeAirtime[strafe], + pd.strafeOverlap[strafe], + pd.strafeDeadair[strafe], + pd.strafeAvgGain[strafe], + RoundFloat(pd.strafeAvgEfficiency[strafe]), + RoundFloat(pd.strafeMaxEfficiency[strafe]) + ); + } + } + + // hud text + char strafeLeft[512] = ""; + int slIndex; + char strafeRight[512] = ""; + int srIndex; + char mouseLeft[512] = ""; + int mlIndex; + char mouseRight[512] = ""; + int mrIndex; + + char hudStrafeLeft[4096] = ""; + int hslIndex; + char hudStrafeRight[4096] = ""; + int hsrIndex; + char hudMouse[4096] = ""; + int hmIndex; + + char mouseChars[][] = { + "â–„", + "â–ˆ" + }; + char mouseColours[][] = { + "<font color='#FFBF00'>|", + "<font color='#000000'>|", + "<font color='#003FFF'>|" + }; + float mouseSpeedScale = 1.0 / (512.0 * GetTickInterval()); + // nonsensical default values, so that the first comparison check fails + StrafeType lastStrafeTypeLeft = STRAFETYPE_NONE_RIGHT + STRAFETYPE_NONE_RIGHT; + StrafeType lastStrafeTypeRight = STRAFETYPE_NONE_RIGHT + STRAFETYPE_NONE_RIGHT; + int lastMouseIndex = 9999; + for (int i = 0; i < pd.jumpAirtime && i < MAX_JUMP_FRAMES; i++) + { + StrafeType strafeTypeLeft = pd.strafeGraph[i]; + StrafeType strafeTypeRight = pd.strafeGraph[i]; + + if (strafeTypeLeft == STRAFETYPE_RIGHT + || strafeTypeLeft == STRAFETYPE_NONE_RIGHT + || strafeTypeLeft == STRAFETYPE_OVERLAP_RIGHT) + { + strafeTypeLeft = STRAFETYPE_NONE; + } + + if (strafeTypeRight == STRAFETYPE_LEFT + || strafeTypeRight == STRAFETYPE_NONE_LEFT + || strafeTypeRight == STRAFETYPE_OVERLAP_LEFT) + { + strafeTypeRight = STRAFETYPE_NONE; + } + + slIndex += strcopy(strafeLeft[slIndex], sizeof(strafeLeft) - slIndex, g_szStrafeType[strafeTypeLeft]); + srIndex += strcopy(strafeRight[srIndex], sizeof(strafeRight) - srIndex, g_szStrafeType[strafeTypeRight]); + + int charIndex = GCIntMin(RoundToFloor(FloatAbs(pd.mouseGraph[i]) * mouseSpeedScale), 1); + if (pd.mouseGraph[i] == 0.0) + { + mouseLeft[mlIndex++] = '.'; + mouseRight[mrIndex++] = '.'; + } + else if (pd.mouseGraph[i] < 0.0) + { + mouseLeft[mlIndex++] = '.'; + mrIndex += strcopy(mouseRight[mrIndex], sizeof(mouseRight) - mrIndex, mouseChars[charIndex]); + } + else if (pd.mouseGraph[i] > 0.0) + { + mlIndex += strcopy(mouseLeft[mlIndex], sizeof(mouseLeft) - mlIndex, mouseChars[charIndex]); + mouseRight[mrIndex++] = '.'; + } + + if (i == 0) + { + hslIndex += strcopy(hudStrafeLeft, sizeof(hudStrafeLeft), "<font color='#FFFFFF'>L: "); + hsrIndex += strcopy(hudStrafeRight, sizeof(hudStrafeRight), "<font color='#FFFFFF'>R: "); + hmIndex += strcopy(hudMouse, sizeof(hudMouse), "<font color='#FFFFFF'>M: "); + } + + if (lastStrafeTypeLeft != strafeTypeLeft) + { + hslIndex += strcopy(hudStrafeLeft[hslIndex], sizeof(hudStrafeLeft) - hslIndex, g_szStrafeTypeColour[strafeTypeLeft]); + } + else + { + hudStrafeLeft[hslIndex++] = '|'; + } + + if (lastStrafeTypeRight != strafeTypeRight) + { + hsrIndex += strcopy(hudStrafeRight[hsrIndex], sizeof(hudStrafeRight) - hsrIndex, g_szStrafeTypeColour[strafeTypeRight]); + } + else + { + hudStrafeRight[hsrIndex++] = '|'; + } + + int mouseIndex = FloatSign(pd.mouseGraph[i]) + 1; + if (mouseIndex != lastMouseIndex) + { + hmIndex += strcopy(hudMouse[hmIndex], sizeof(hudMouse) - hmIndex, mouseColours[mouseIndex]); + } + else + { + hudMouse[hmIndex++] = '|'; + } + + lastStrafeTypeLeft = strafeTypeLeft; + lastStrafeTypeRight = strafeTypeRight; + lastMouseIndex = mouseIndex; + } + + mouseLeft[mlIndex] = '\0'; + mouseRight[mrIndex] = '\0'; + hudStrafeLeft[hslIndex] = '\0'; + hudStrafeRight[hsrIndex] = '\0'; + hudMouse[hmIndex] = '\0'; + + bool showHudGraph = IsSettingEnabled(client, SETTINGS_SHOW_HUD_GRAPH); + if (showHudGraph) + { + // worst case scenario is roughly 11000 characters :D + char strafeGraph[11000]; + FormatEx(strafeGraph, sizeof(strafeGraph), "<u><span class='fontSize-s'>%s<br>%s<br>%s", hudStrafeLeft, hudStrafeRight, hudMouse); + + // TODO: sometimes just after a previous panel has faded out a new panel can't be shown, fix! + ShowPanel(client, 3, strafeGraph); + } + if (!IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_GRAPH)) + { + ClientAndSpecsPrintConsole(client, "\nStrafe keys:\nL: %s\nR: %s", strafeLeft, strafeRight); + ClientAndSpecsPrintConsole(client, "Mouse movement:\nL: %s\nR: %s\n\n", mouseLeft, mouseRight); + } +} diff --git a/sourcemod/scripting/distbugfix/clientprefs.sp b/sourcemod/scripting/distbugfix/clientprefs.sp new file mode 100644 index 0000000..bee4681 --- /dev/null +++ b/sourcemod/scripting/distbugfix/clientprefs.sp @@ -0,0 +1,51 @@ + + +static Handle distbugCookie; +static int settings[MAXPLAYERS + 1]; + +void OnPluginStart_Clientprefs() +{ + distbugCookie = RegClientCookie("distbugfix_cookie_v2", "cookie for distbugfix", CookieAccess_Private); + if (distbugCookie == INVALID_HANDLE) + { + SetFailState("Couldn't create distbug cookie."); + } +} + +void OnClientCookiesCached_Clientprefs(int client) +{ + char buffer[MAX_COOKIE_SIZE]; + GetClientCookie(client, distbugCookie, buffer, sizeof(buffer)); + + settings[client] = StringToInt(buffer); +} + +void SaveClientCookies(int client) +{ + if (!GCIsValidClient(client) || !AreClientCookiesCached(client)) + { + return; + } + + char buffer[MAX_COOKIE_SIZE]; + IntToString(settings[client], buffer, sizeof(buffer)); + SetClientCookie(client, distbugCookie, buffer); +} + +bool IsSettingEnabled(int client, int setting) +{ + if (GCIsValidClient(client)) + { + return !!(settings[client] & setting); + } + return false; +} + +void ToggleSetting(int client, int setting) +{ + if (GCIsValidClient(client)) + { + settings[client] ^= setting; + SaveClientCookies(client); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat.sp b/sourcemod/scripting/gokz-anticheat.sp new file mode 100644 index 0000000..9925eca --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat.sp @@ -0,0 +1,318 @@ +#include <sourcemod> + +#include <dhooks> + +#include <movementapi> +#include <gokz/anticheat> +#include <gokz/core> + +#include <autoexecconfig> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/localdb> +#include <sourcebanspp> +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Anti-Cheat", + author = "DanZay", + description = "Detects basic player movement cheats", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-anticheat.txt" + +bool gB_GOKZLocalDB; +bool gB_SourceBansPP; +bool gB_SourceBans; +bool gB_GOKZGlobal; + +Handle gH_DHooks_OnTeleport; + +int gI_CmdNum[MAXPLAYERS + 1]; +int gI_LastOriginTeleportCmdNum[MAXPLAYERS + 1]; + +int gI_ButtonCount[MAXPLAYERS + 1]; +int gI_ButtonsIndex[MAXPLAYERS + 1]; +int gI_Buttons[MAXPLAYERS + 1][AC_MAX_BUTTON_SAMPLES]; + +int gI_BhopCount[MAXPLAYERS + 1]; +int gI_BhopIndex[MAXPLAYERS + 1]; +int gI_BhopLastTakeoffCmdnum[MAXPLAYERS + 1]; +int gI_BhopLastRecordedBhopCmdnum[MAXPLAYERS + 1]; +bool gB_BhopHitPerf[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +int gI_BhopPreJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +int gI_BhopPostJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +bool gB_BhopPostJumpInputsPending[MAXPLAYERS + 1]; +bool gB_LastLandingWasValid[MAXPLAYERS + 1]; +bool gB_BindExceptionPending[MAXPLAYERS + 1]; +bool gB_BindExceptionPostPending[MAXPLAYERS + 1]; + +ConVar gCV_gokz_autoban; +ConVar gCV_gokz_autoban_duration_bhop_hack; +ConVar gCV_gokz_autoban_duration_bhop_macro; +ConVar gCV_sv_autobunnyhopping; + +#include "gokz-anticheat/api.sp" +#include "gokz-anticheat/bhop_tracking.sp" +#include "gokz-anticheat/commands.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-anticheat"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-anticheat.phrases"); + + CreateConVars(); + CreateGlobalForwards(); + HookEvents(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = LibraryExists("gokz-localdb"); + gB_SourceBansPP = LibraryExists("sourcebans++"); + gB_SourceBans = LibraryExists("sourcebans"); + gB_GOKZGlobal = LibraryExists("gokz-global"); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = gB_GOKZLocalDB || StrEqual(name, "gokz-localdb"); + gB_SourceBansPP = gB_SourceBansPP || StrEqual(name, "sourcebans++"); + gB_SourceBans = gB_SourceBans || StrEqual(name, "sourcebans"); + gB_GOKZGlobal = gB_GOKZGlobal || StrEqual(name, "gokz-global"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb"); + gB_SourceBansPP = gB_SourceBansPP && !StrEqual(name, "sourcebans++"); + gB_SourceBans = gB_SourceBans && !StrEqual(name, "sourcebans"); + gB_GOKZGlobal = gB_GOKZGlobal && !StrEqual(name, "gokz-global"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_BhopTracking(client); + HookClientEvents(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + gI_CmdNum[client] = cmdnum; + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsPlayerAlive(client) || IsFakeClient(client)) + { + return; + } + + OnPlayerRunCmdPost_BhopTracking(client, buttons, cmdnum); +} + +public MRESReturn DHooks_OnTeleport(int client, Handle params) +{ + // Parameter 1 not null means origin affected + gI_LastOriginTeleportCmdNum[client] = !DHookIsNullParam(params, 1) ? gI_CmdNum[client] : gI_LastOriginTeleportCmdNum[client]; + + // Parameter 3 not null means velocity affected + //gI_LastVelocityTeleportCmdNum[client] = !DHookIsNullParam(params, 3) ? gI_CmdNum[client] : gI_LastVelocityTeleportCmdNum[client]; + + return MRES_Ignored; +} + +public void GOKZ_OnFirstSpawn(int client) +{ + GOKZ_PrintToChat(client, false, "%t", "Anti-Cheat Warning"); +} + +public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + LogSuspicion(client, reason, notes, stats); +} + + + +// =====[ PUBLIC ]===== + +void SuspectPlayer(int client, ACReason reason, const char[] notes, const char[] stats) +{ + Call_OnPlayerSuspected(client, reason, notes, stats); + + if (gB_GOKZLocalDB) + { + GOKZ_DB_SetCheater(client, true); + } + + if (gCV_gokz_autoban.BoolValue) + { + BanSuspect(client, reason); + } +} + + + +// =====[ PRIVATE ]===== + +static void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-anticheat", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_autoban = AutoExecConfig_CreateConVar( + "gokz_autoban", + "1", + "Whether to autoban players when they are suspected of cheating.", + _, + true, + 0.0, + true, + 1.0); + + gCV_gokz_autoban_duration_bhop_hack = AutoExecConfig_CreateConVar( + "gokz_autoban_duration_bhop_hack", + "0", + "Duration of anticheat autobans for bunnyhop hacking in minutes (0 for permanent).", + _, + true, + 0.0); + + gCV_gokz_autoban_duration_bhop_macro = AutoExecConfig_CreateConVar( + "gokz_autoban_duration_bhop_macro", + "43200", // 30 days + "Duration of anticheat autobans for bunnyhop macroing in minutes (0 for permanent).", + _, + true, + 0.0); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + gCV_sv_autobunnyhopping = FindConVar("sv_autobunnyhopping"); +} + +static void HookEvents() +{ + GameData gameData = new GameData("sdktools.games"); + int offset; + + // Setup DHooks OnTeleport for players + offset = gameData.GetOffset("Teleport"); + gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool); + + delete gameData; +} + +static void HookClientEvents(int client) +{ + DHookEntity(gH_DHooks_OnTeleport, true, client); +} + +static void LogSuspicion(int client, ACReason reason, const char[] notes, const char[] stats) +{ + char logPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, logPath, sizeof(logPath), AC_LOG_PATH); + + switch (reason) + { + case ACReason_BhopHack:LogToFileEx(logPath, "%L was suspected of bhop hacking. Notes - %s, Stats - %s", client, notes, stats); + case ACReason_BhopMacro:LogToFileEx(logPath, "%L was suspected of bhop macroing. Notes - %s, Stats - %s", client, notes, stats); + } +} + +static void BanSuspect(int client, ACReason reason) +{ + char banMessage[128]; + char redirectString[64] = "Contact the server administrator for more info"; + + if (gB_GOKZGlobal) + { + redirectString = "Visit http://rules.global-api.com/ for more info"; + } + + switch (reason) + { + case ACReason_BhopHack: + { + FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop hack", redirectString); + AutoBanClient( + client, + gCV_gokz_autoban_duration_bhop_hack.IntValue, + "gokz-anticheat - Bhop hacking", + banMessage); + } + case ACReason_BhopMacro: + { + FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop macro", redirectString); + AutoBanClient( + client, + gCV_gokz_autoban_duration_bhop_macro.IntValue, + "gokz-anticheat - Bhop macroing", + banMessage); + } + } +} + +static void AutoBanClient(int client, int minutes, const char[] reason, const char[] kickMessage) +{ + if (gB_SourceBansPP) + { + SBPP_BanPlayer(0, client, minutes, reason); + } + else if (gB_SourceBans) + { + SBBanPlayer(0, client, minutes, reason); + } + else + { + BanClient(client, minutes, BANFLAG_AUTO, reason, kickMessage, "gokz-anticheat", 0); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/api.sp b/sourcemod/scripting/gokz-anticheat/api.sp new file mode 100644 index 0000000..7f99724 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/api.sp @@ -0,0 +1,174 @@ +static GlobalForward H_OnPlayerSuspected; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnPlayerSuspected = new GlobalForward("GOKZ_AC_OnPlayerSuspected", ET_Ignore, Param_Cell, Param_Cell, Param_String, Param_String); +} + +void Call_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + Call_StartForward(H_OnPlayerSuspected); + Call_PushCell(client); + Call_PushCell(reason); + Call_PushString(notes); + Call_PushString(stats); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_AC_GetSampleSize", Native_GetSampleSize); + CreateNative("GOKZ_AC_GetHitPerf", Native_GetHitPerf); + CreateNative("GOKZ_AC_GetPerfCount", Native_GetPerfCount); + CreateNative("GOKZ_AC_GetPerfRatio", Native_GetPerfRatio); + CreateNative("GOKZ_AC_GetJumpInputs", Native_GetJumpInputs); + CreateNative("GOKZ_AC_GetAverageJumpInputs", Native_GetAverageJumpInputs); + CreateNative("GOKZ_AC_GetPreJumpInputs", Native_GetPreJumpInputs); + CreateNative("GOKZ_AC_GetPostJumpInputs", Native_GetPostJumpInputs); +} + +public int Native_GetSampleSize(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return IntMin(gI_BhopCount[client], AC_MAX_BHOP_SAMPLES); +} + +public int Native_GetHitPerf(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + SortByRecent(gB_BhopHitPerf[client], AC_MAX_BHOP_SAMPLES, perfs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, perfs, sampleSize); + return sampleSize; +} + +public int Native_GetPerfCount(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + GOKZ_AC_GetHitPerf(client, perfs, sampleSize); + + int perfCount = 0; + for (int i = 0; i < sampleSize; i++) + { + if (perfs[i]) + { + perfCount++; + } + } + return perfCount; +} + +public int Native_GetPerfRatio(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as<int>(0.0); + } + + int perfCount = GOKZ_AC_GetPerfCount(client, sampleSize); + return view_as<int>(float(perfCount) / float(sampleSize)); +} + +public int Native_GetJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + + int[] jumpInputs = new int[sampleSize]; + for (int i = 0; i < sampleSize; i++) + { + jumpInputs[i] = preJumpInputs[i] + postJumpInputs[i]; + } + + SetNativeArray(2, jumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetAverageJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as<int>(0.0); + } + + int[] jumpInputs = new int[sampleSize]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, sampleSize); + + int jumpInputCount = 0; + for (int i = 0; i < sampleSize; i++) + { + jumpInputCount += jumpInputs[i]; + } + return view_as<int>(float(jumpInputCount) / float(sampleSize)); +} + +public int Native_GetPreJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, preJumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetPostJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, postJumpInputs, sampleSize); + return sampleSize; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp new file mode 100644 index 0000000..5607b07 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp @@ -0,0 +1,336 @@ +/* + Track player's jump inputs and whether they hit perfect + bunnyhops for a number of their recent bunnyhops. +*/ + + + +// =====[ PUBLIC ]===== + +void PrintBhopCheckToChat(int client, int target) +{ + GOKZ_PrintToChat(client, true, + "{lime}%N {grey}[{lime}%d%%%% {grey}%t | {lime}%.2f {grey}%t]", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average"); + GOKZ_PrintToChat(client, false, + " {grey}%t - %s", + "Pattern", + GenerateScrollPattern(target, 20)); +} + +void PrintBhopCheckToConsole(int client, int target) +{ + PrintToConsole(client, + "%N [%d%% %t | %.2f %t]\n %t - %s", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average", + "Pattern", + GenerateScrollPattern(target, 20, false)); +} + +// Generate 'scroll pattern' +char[] GenerateScrollPattern(int client, int sampleSize = AC_MAX_BHOP_SAMPLES, bool colours = true) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + if (colours) + { + Format(report, sizeof(report), "%s%s%d ", + report, + perfs[i] ? "{green}" : "{default}", + jumpInputs[i]); + } + else + { + Format(report, sizeof(report), "%s%d%s ", + report, + jumpInputs[i], + perfs[i] ? "*" : ""); + } + } + + TrimString(report); + + return report; +} + +// Generate 'scroll pattern' report showing pre and post inputs instead +char[] GenerateScrollPatternEx(int client, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int[] preJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPreJumpInputs(client, preJumpInputs, maxIndex); + int[] postJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPostJumpInputs(client, postJumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + Format(report, sizeof(report), "%s(%d%s%d)", + report, + preJumpInputs[i], + perfs[i] ? "*" : " ", + postJumpInputs[i]); + } + + TrimString(report); + + return report; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_BhopTracking(int client) +{ + ResetBhopStats(client); +} + +void OnPlayerRunCmdPost_BhopTracking(int client, int buttons, int cmdnum) +{ + if (gCV_sv_autobunnyhopping.BoolValue) + { + return; + } + + int nextIndex = NextIndex(gI_BhopIndex[client], AC_MAX_BHOP_SAMPLES); + + // Record buttons BEFORE checking for bhop + RecordButtons(client, buttons); + + // If bhop was last tick, then record the pre bhop inputs. + // Require two times the button sample size since the last + // takeoff to avoid pre and post bhop input overlap. + if (HitBhop(client, cmdnum) + && cmdnum >= gI_BhopLastTakeoffCmdnum[client] + AC_MAX_BUTTON_SAMPLES * 2 + && gB_LastLandingWasValid[client]) + { + gB_BhopHitPerf[client][nextIndex] = Movement_GetHitPerf(client); + gI_BhopPreJumpInputs[client][nextIndex] = CountJumpInputs(client); + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + + // Bind exception + if (gB_BindExceptionPending[client] && cmdnum > Movement_GetLandingCmdNum(client) + AC_MAX_BHOP_GROUND_TICKS) + { + gB_BhopHitPerf[client][nextIndex] = false; + gI_BhopPreJumpInputs[client][nextIndex] = -1; // Special value for binded jumps + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = true; + } + + // Record post bhop inputs once enough ticks have passed + if (gB_BhopPostJumpInputsPending[client] && cmdnum == gI_BhopLastRecordedBhopCmdnum[client] + AC_MAX_BUTTON_SAMPLES) + { + gI_BhopPostJumpInputs[client][nextIndex] = CountJumpInputs(client); + gB_BhopPostJumpInputsPending[client] = false; + gI_BhopIndex[client] = nextIndex; + gI_BhopCount[client]++; + CheckForBhopMacro(client); + gB_BindExceptionPostPending[client] = false; + } + + // Record last jump takeoff time + if (JustJumped(client, cmdnum)) + { + gI_BhopLastTakeoffCmdnum[client] = cmdnum; + gB_BindExceptionPending[client] = false; + if (gB_BindExceptionPostPending[client]) + { + gB_BhopPostJumpInputsPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + } + + if (JustLanded(client, cmdnum)) + { + // These conditions exist to reduce false positives. + + // Telehopping is when the player bunnyhops out of a teleport that has a + // destination very close to the ground. This will, more than usual, + // result in a perfect bunnyhop. This is alleviated by checking if the + // player's origin was affected by a teleport last tick. + + // When a player is pressing up against a slope but not ascending it (e.g. + // palm trees on kz_adv_cursedjourney), they will switch between on ground + // and off ground frequently, which means that if they manage to jump, the + // jump will be recorded as a perfect bunnyhop. To ignore this, we check + // the jump is more than 1 tick duration. + + gB_LastLandingWasValid[client] = cmdnum - gI_LastOriginTeleportCmdNum[client] > 1 + && cmdnum - Movement_GetTakeoffCmdNum(client) > 1; + + // You can still crouch-bind VNL jumps and some people just don't know that + // it doesn't work with the other modes in GOKZ. This can cause false positives + // if the player uses the bind for bhops and mostly presses it too early or + // exactly on time rather than too late. This is supposed to reduce those by + // detecting jumps where you don't get a bhop and have exactly one jump input + // before landing and none after landing. We require the one input to be right + // before the jump to make it a lot harder to fake a binded jump when doing + // a regular longjump. + gB_BindExceptionPending[client] = (CountJumpInputs(client, AC_BINDEXCEPTION_SAMPLES) == 1 && CountJumpInputs(client, AC_MAX_BUTTON_SAMPLES) == 1); + gB_BindExceptionPostPending[client] = false; + } +} + + + +// =====[ PRIVATE ]===== + +static void CheckForBhopMacro(int client) +{ + if (GOKZ_AC_GetPerfCount(client, 19) == 19) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 19)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 28) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 30)); + } + else if (GOKZ_AC_GetPerfCount(client, 20) >= 16 && GOKZ_AC_GetAverageJumpInputs(client, 20) <= 2.0 + EPSILON) + { + SuspectPlayer(client, ACReason_BhopHack, "1's or 2's scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (gI_BhopCount[client] >= 20 && GOKZ_AC_GetPerfCount(client, 20) >= 8 + && GOKZ_AC_GetAverageJumpInputs(client, 20) >= 19.0 - EPSILON) + { + SuspectPlayer(client, ACReason_BhopMacro, "High scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 10 && CheckForRepeatingJumpInputsCount(client, 25, 30) >= 14) + { + SuspectPlayer(client, ACReason_BhopMacro, "Repeating scroll pattern", GenerateBhopBanStats(client, 30)); + } +} + +static char[] GenerateBhopBanStats(int client, int sampleSize) +{ + char stats[512]; + FormatEx(stats, sizeof(stats), + "Perfs: %d/%d, Average: %.2f, Scroll pattern: %s", + GOKZ_AC_GetPerfCount(client, sampleSize), + IntMin(gI_BhopCount[client], sampleSize), + GOKZ_AC_GetAverageJumpInputs(client, sampleSize), + GenerateScrollPatternEx(client, sampleSize)); + return stats; +} + +/** + * Returns -1, or the repeating input count if there if there is + * an input count that repeats for more than the provided ratio. + * + * @param client Client index. + * @param threshold Minimum frequency to be considered 'repeating'. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return The repeating input, or else -1. + */ +static int CheckForRepeatingJumpInputsCount(int client, int threshold, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int maxJumpInputs = AC_MAX_BUTTON_SAMPLES + 1; + int[] jumpInputsFrequency = new int[maxJumpInputs]; + + // Count up all the in jump patterns + for (int i = 0; i < maxIndex; i++) + { + // -1 is a binded jump, those are excluded + if (jumpInputs[i] != -1) + { + jumpInputsFrequency[jumpInputs[i]]++; + } + } + + // Returns i if the given number of the sample size has the same jump input count + for (int i = 1; i < maxJumpInputs; i++) + { + if (jumpInputsFrequency[i] >= threshold) + { + return i; + } + } + + return -1; // -1 if no repeating jump input found +} + +// Reset the tracked bhop stats of the client +static void ResetBhopStats(int client) +{ + gI_ButtonCount[client] = 0; + gI_ButtonsIndex[client] = 0; + gI_BhopCount[client] = 0; + gI_BhopIndex[client] = 0; + gI_BhopLastTakeoffCmdnum[client] = 0; + gI_BhopLastRecordedBhopCmdnum[client] = 0; + gB_BhopPostJumpInputsPending[client] = false; + gB_LastLandingWasValid[client] = false; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; +} + +// Returns true if ther was a jump last tick and was within a number of ticks after landing +static bool HitBhop(int client, int cmdnum) +{ + return JustJumped(client, cmdnum) && Movement_GetTakeoffCmdNum(client) - Movement_GetLandingCmdNum(client) <= AC_MAX_BHOP_GROUND_TICKS; +} + +static bool JustJumped(int client, int cmdnum) +{ + return Movement_GetJumped(client) && Movement_GetTakeoffCmdNum(client) == cmdnum; +} + +static bool JustLanded(int client, int cmdnum) +{ + return Movement_GetLandingCmdNum(client) == cmdnum; +} + +// Records current button inputs +static void RecordButtons(int client, int buttons) +{ + gI_ButtonsIndex[client] = NextIndex(gI_ButtonsIndex[client], AC_MAX_BUTTON_SAMPLES); + gI_Buttons[client][gI_ButtonsIndex[client]] = buttons; + gI_ButtonCount[client]++; +} + +// Counts the number of times buttons went from !IN_JUMP to IN_JUMP +static int CountJumpInputs(int client, int sampleSize = AC_MAX_BUTTON_SAMPLES) +{ + int[] recentButtons = new int[sampleSize]; + SortByRecent(gI_Buttons[client], AC_MAX_BUTTON_SAMPLES, recentButtons, sampleSize, gI_ButtonsIndex[client]); + int maxIndex = IntMin(gI_ButtonCount[client], sampleSize); + int jumps = 0; + + for (int i = 0; i < maxIndex - 1; i++) + { + // If buttons went from !IN_JUMP to IN_JUMP + if (!(recentButtons[i + 1] & IN_JUMP) && recentButtons[i] & IN_JUMP) + { + jumps++; + } + } + return jumps; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/commands.sp b/sourcemod/scripting/gokz-anticheat/commands.sp new file mode 100644 index 0000000..a1fbe2e --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/commands.sp @@ -0,0 +1,76 @@ +void RegisterCommands() +{ + RegAdminCmd("sm_bhopcheck", CommandBhopCheck, ADMFLAG_ROOT, "[KZ] Show bunnyhop stats report including perf ratio and scroll pattern."); +} + +public Action CommandBhopCheck(int client, int args) +{ + if (args == 0) + { + if (GOKZ_AC_GetSampleSize(client) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + PrintBhopCheckToChat(client, client); + } + return Plugin_Handled; + } + + char arg[65]; + GetCmdArg(1, arg, sizeof(arg)); + char targetName[MAX_TARGET_LENGTH]; + int targetList[MAXPLAYERS], targetCount; + bool tnIsML; + + if ((targetCount = ProcessTargetString( + arg, + client, + targetList, + MAXPLAYERS, + COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_NO_BOTS, + targetName, + sizeof(targetName), + tnIsML)) <= 0) + { + ReplyToTargetError(client, targetCount); + return Plugin_Handled; + } + + if (targetCount >= 2) + { + GOKZ_PrintToChat(client, true, "%t", "See Console"); + for (int i = 0; i < targetCount; i++) + { + if (GOKZ_AC_GetSampleSize(targetList[i]) == 0) + { + PrintToConsole(client, "%t", "Not Enough Bhops (Console)", targetList[i]); + } + else + { + PrintBhopCheckToConsole(client, targetList[i]); + } + } + } + else + { + if (GOKZ_AC_GetSampleSize(targetList[0]) == 0) + { + if (targetList[0] == client) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops", targetList[0]); + } + } + else + { + PrintBhopCheckToChat(client, targetList[0]); + } + } + + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-chat.sp b/sourcemod/scripting/gokz-chat.sp new file mode 100644 index 0000000..38820f8 --- /dev/null +++ b/sourcemod/scripting/gokz-chat.sp @@ -0,0 +1,309 @@ +#include <sourcemod> + +#include <cstrike> + +#include <gokz/core> + +#include <autoexecconfig> +#include <morecolors> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <basecomm> +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Chat", + author = "DanZay", + description = "Handles client-triggered chat messages", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-chat.txt" + +bool gB_BaseComm; +char gC_PlayerTags[MAXPLAYERS + 1][32]; +char gC_PlayerTagColors[MAXPLAYERS + 1][16]; + +ConVar gCV_gokz_chat_processing; +ConVar gCV_gokz_connection_messages; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-chat"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-chat.phrases"); + + CreateConVars(); + HookEvents(); + + OnPluginStart_BlockRadio(); + OnPluginStart_BlockChatWheel(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_BaseComm = LibraryExists("basecomm"); +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_BaseComm = gB_BaseComm || StrEqual(name, "basecomm"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_BaseComm = gB_BaseComm && !StrEqual(name, "basecomm"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs) +{ + if (client > 0 && gCV_gokz_chat_processing.BoolValue && IsClientInGame(client)) + { + OnClientSayCommand_ChatProcessing(client, command, sArgs); + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void OnClientConnected(int client) +{ + gC_PlayerTags[client][0] = '\0'; + gC_PlayerTagColors[client][0] = '\0'; +} + +public void OnClientPutInServer(int client) +{ + PrintConnectMessage(client); +} + +public Action OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast) // player_disconnect pre hook +{ + event.BroadcastDisabled = true; // Block disconnection messages + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + PrintDisconnectMessage(client, event); + } + return Plugin_Continue; +} + +public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) // player_team pre hook +{ + event.SetBool("silent", true); // Block join team messages + return Plugin_Continue; +} + + + +// =====[ GENERAL ]===== + +void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-chat", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_chat_processing = AutoExecConfig_CreateConVar("gokz_chat_processing", "1", "Whether GOKZ processes player chat messages.", _, true, 0.0, true, 1.0); + gCV_gokz_connection_messages = AutoExecConfig_CreateConVar("gokz_connection_messages", "1", "Whether GOKZ handles connection and disconnection messages.", _, true, 0.0, true, 1.0); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); +} + +void HookEvents() +{ + HookEvent("player_disconnect", OnPlayerDisconnect, EventHookMode_Pre); + HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre); +} + + + +// =====[ CHAT PROCESSING ]===== + +void OnClientSayCommand_ChatProcessing(int client, const char[] command, const char[] message) +{ + if (gB_BaseComm && BaseComm_IsClientGagged(client) + || UsedBaseChat(client, command, message)) + { + return; + } + + // Resend messages that may have been a command with capital letters + if ((message[0] == '!' || message[0] == '/') && IsCharUpper(message[1])) + { + char loweredMessage[128]; + String_ToLower(message, loweredMessage, sizeof(loweredMessage)); + FakeClientCommand(client, "say %s", loweredMessage); + return; + } + + char sanitisedMessage[128]; + strcopy(sanitisedMessage, sizeof(sanitisedMessage), message); + SanitiseChatInput(sanitisedMessage, sizeof(sanitisedMessage)); + + char sanitisedName[MAX_NAME_LENGTH]; + GetClientName(client, sanitisedName, sizeof(sanitisedName)); + SanitiseChatInput(sanitisedName, sizeof(sanitisedName)); + + if (TrimString(sanitisedMessage) == 0) + { + return; + } + + if (IsSpectating(client)) + { + GOKZ_PrintToChatAll(false, "{default}* %s%s{lime}%s{default} : %s", + gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToConsoleAll("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToServer("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + } + else + { + GOKZ_PrintToChatAll(false, "%s%s{lime}%s{default} : %s", + gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToConsoleAll("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToServer("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + } +} + +bool UsedBaseChat(int client, const char[] command, const char[] message) +{ + // Assuming base chat is in use, check if message will get processed by basechat + if (message[0] != '@') + { + return false; + } + + if (strcmp(command, "say_team", false) == 0) + { + return true; + } + else if (strcmp(command, "say", false) == 0 && CheckCommandAccess(client, "sm_say", ADMFLAG_CHAT)) + { + return true; + } + + return false; +} + +void SanitiseChatInput(char[] message, int maxlength) +{ + Color_StripFromChatText(message, message, maxlength); + //CRemoveColors(message, maxlength); + // Chat gets double formatted, so replace '%' with '%%%%' to end up with '%' + ReplaceString(message, maxlength, "%", "%%%%"); +} + + + +// =====[ CONNECTION MESSAGES ]===== + +void PrintConnectMessage(int client) +{ + if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client)) + { + return; + } + + GOKZ_PrintToChatAll(false, "%t", "Client Connection Message", client); +} + +void PrintDisconnectMessage(int client, Event event) // Hooked to player_disconnect event +{ + if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client)) + { + return; + } + + char reason[128]; + event.GetString("reason", reason, sizeof(reason)); + GOKZ_PrintToChatAll(false, "%t", "Client Disconnection Message", client, reason); +} + + + +// =====[ BLOCK RADIO AND CHATWHEEL]===== + +static char radioCommands[][] = +{ + "coverme", "takepoint", "holdpos", "regroup", "followme", "takingfire", "go", + "fallback", "sticktog", "getinpos", "stormfront", "report", "roger", "enemyspot", + "needbackup", "sectorclear", "inposition", "reportingin", "getout", "negative", + "enemydown", "compliment", "thanks", "cheer", "go_a", "go_b", "sorry", "needrop" +}; + +public void OnPluginStart_BlockRadio() +{ + for (int i = 0; i < sizeof(radioCommands); i++) + { + AddCommandListener(CommandBlock, radioCommands[i]); + } +} + +public void OnPluginStart_BlockChatWheel() +{ + AddCommandListener(CommandBlock, "playerchatwheel"); + AddCommandListener(CommandBlock, "chatwheel_ping"); +} + +public Action CommandBlock(int client, const char[] command, int argc) +{ + return Plugin_Handled; +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_CH_SetChatTag", Native_SetChatTag); +} + +public int Native_SetChatTag(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + + char str[64]; + GetNativeString(2, str, sizeof(str)); + if (str[0] == '\0') + { + // To prevent the space after the mode + FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + else + { + FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s %s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)], str); + } + + GetNativeString(3, gC_PlayerTagColors[client], sizeof(gC_PlayerTagColors[])); + return 0; +} diff --git a/sourcemod/scripting/gokz-core.sp b/sourcemod/scripting/gokz-core.sp new file mode 100644 index 0000000..897403f --- /dev/null +++ b/sourcemod/scripting/gokz-core.sp @@ -0,0 +1,543 @@ +#include <sourcemod> + +#include <clientprefs> +#include <cstrike> +#include <dhooks> +#include <regex> +#include <sdkhooks> +#include <sdktools> + +#include <gokz/core> +#include <movementapi> + +#include <autoexecconfig> +#include <sourcemod-colors> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#include <gokz/kzplayer> +#include <gokz/jumpstats> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Core", + author = "DanZay", + description = "Core plugin of the GOKZ plugin set", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-core.txt" + +Handle gH_ThisPlugin; +Handle gH_DHooks_OnTeleport; +Handle gH_DHooks_SetModel; + +int gI_CmdNum[MAXPLAYERS + 1]; +int gI_TickCount[MAXPLAYERS + 1]; +bool gB_OldOnGround[MAXPLAYERS + 1]; +int gI_OldButtons[MAXPLAYERS + 1]; +int gI_TeleportCmdNum[MAXPLAYERS + 1]; +bool gB_OriginTeleported[MAXPLAYERS + 1]; +bool gB_VelocityTeleported[MAXPLAYERS + 1]; +bool gB_LateLoad; + +ConVar gCV_gokz_chat_prefix; +ConVar gCV_sv_full_alltalk; + +#include "gokz-core/commands.sp" +#include "gokz-core/modes.sp" +#include "gokz-core/misc.sp" +#include "gokz-core/options.sp" +#include "gokz-core/teleports.sp" +#include "gokz-core/triggerfix.sp" +#include "gokz-core/demofix.sp" +#include "gokz-core/teamnumfix.sp" + +#include "gokz-core/map/buttons.sp" +#include "gokz-core/map/triggers.sp" +#include "gokz-core/map/mapfile.sp" +#include "gokz-core/map/prefix.sp" +#include "gokz-core/map/starts.sp" +#include "gokz-core/map/zones.sp" +#include "gokz-core/map/end.sp" + +#include "gokz-core/menus/mode_menu.sp" +#include "gokz-core/menus/options_menu.sp" + +#include "gokz-core/timer/pause.sp" +#include "gokz-core/timer/timer.sp" +#include "gokz-core/timer/virtual_buttons.sp" + +#include "gokz-core/forwards.sp" +#include "gokz-core/natives.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + if (GetEngineVersion() != Engine_CSGO) + { + SetFailState("GOKZ only supports CS:GO servers."); + } + + gH_ThisPlugin = myself; + gB_LateLoad = late; + + CreateNatives(); + RegPluginLibrary("gokz-core"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-core.phrases"); + + CreateGlobalForwards(); + CreateConVars(); + HookEvents(); + RegisterCommands(); + + OnPluginStart_MapTriggers(); + OnPluginStart_MapButtons(); + OnPluginStart_MapStarts(); + OnPluginStart_MapEnd(); + OnPluginStart_MapZones(); + OnPluginStart_Options(); + OnPluginStart_Triggerfix(); + OnPluginStart_Demofix(); + OnPluginStart_MapFile(); + OnPluginStart_TeamNumber(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + OnAllPluginsLoaded_Modes(); + OnAllPluginsLoaded_OptionsMenu(); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + if (AreClientCookiesCached(client)) + { + OnClientCookiesCached(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_Timer(client); + OnClientPutInServer_Pause(client); + OnClientPutInServer_Teleports(client); + OnClientPutInServer_JoinTeam(client); + OnClientPutInServer_FirstSpawn(client); + OnClientPutInServer_VirtualButtons(client); + OnClientPutInServer_Options(client); + OnClientPutInServer_MapTriggers(client); + OnClientPutInServer_Triggerfix(client); + OnClientPutInServer_Noclip(client); + OnClientPutInServer_Turnbinds(client); + HookClientEvents(client); +} + +public void OnClientDisconnect(int client) +{ + OnClientDisconnect_Timer(client); + OnClientDisconnect_ValidJump(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + gI_CmdNum[client] = cmdnum; + gI_TickCount[client] = tickcount; + OnPlayerRunCmd_Triggerfix(client); + OnPlayerRunCmd_MapTriggers(client, buttons); + OnPlayerRunCmd_Turnbinds(client, buttons, tickcount, angles); + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client)) + { + return; + } + + OnPlayerRunCmdPost_VirtualButtons(client, buttons, cmdnum); // Emulate buttons first + OnPlayerRunCmdPost_Timer(client); // This should be first after emulating buttons + OnPlayerRunCmdPost_ValidJump(client); + UpdateTrackingVariables(client, cmdnum, buttons); // This should be last +} + +public void OnClientCookiesCached(int client) +{ + OnClientCookiesCached_Options(client); +} + +public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerSpawn_MapTriggers(client); + OnPlayerSpawn_Modes(client); + OnPlayerSpawn_Pause(client); + OnPlayerSpawn_ValidJump(client); + OnPlayerSpawn_FirstSpawn(client); + OnPlayerSpawn_GodMode(client); + OnPlayerSpawn_PlayerCollision(client); + } +} + +public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerJoinTeam_TeamNumber(event, client); + } + return Plugin_Continue; +} + +public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) // player_death pre hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerDeath_Timer(client); + OnPlayerDeath_ValidJump(client); + OnPlayerDeath_TeamNumber(client); + } + return Plugin_Continue; +} + +public void OnPlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + OnPlayerJump_Triggers(client); +} + +public MRESReturn DHooks_OnTeleport(int client, Handle params) +{ + gB_OriginTeleported[client] = !DHookIsNullParam(params, 1); // Origin affected + gB_VelocityTeleported[client] = !DHookIsNullParam(params, 3); // Velocity affected + OnTeleport_ValidJump(client); + OnTeleport_DelayVirtualButtons(client); + return MRES_Ignored; +} + +public MRESReturn DHooks_OnSetModel(int client, Handle params) +{ + OnSetModel_PlayerCollision(client); + return MRES_Handled; +} + +public void OnCSPlayerSpawnPost(int client) +{ + if (GetEntPropEnt(client, Prop_Send, "m_hGroundEntity") == -1) + { + SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND); + } +} + +public void OnClientPreThinkPost(int client) +{ + OnClientPreThinkPost_UseButtons(client); +} + +public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype) +{ + OnChangeMovetype_Timer(client, newMovetype); + OnChangeMovetype_Pause(client, newMovetype); + OnChangeMovetype_ValidJump(client, oldMovetype, newMovetype); + OnChangeMovetype_MapTriggers(client, newMovetype); +} + +public void Movement_OnStartTouchGround(int client) +{ + OnStartTouchGround_MapZones(client); + OnStartTouchGround_MapTriggers(client); +} + +public void Movement_OnStopTouchGround(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + OnStopTouchGround_ValidJump(client, jumped, ladderJump, jumpbug); + OnStopTouchGround_MapTriggers(client); +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + OnTimerStart_JoinTeam(client); + OnTimerStart_Pause(client); + OnTimerStart_Teleports(client); +} + +public void GOKZ_OnTeleportToStart_Post(int client) +{ + OnTeleportToStart_Timer(client); +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + OnCountedTeleport_VirtualButtons(client); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + Option coreOption; + if (!GOKZ_IsCoreOption(option, coreOption)) + { + return; + } + + OnOptionChanged_Options(client, coreOption, newValue); + OnOptionChanged_Timer(client, coreOption); + OnOptionChanged_Mode(client, coreOption); +} + +public void GOKZ_OnJoinTeam(int client, int team) +{ + OnJoinTeam_Pause(client, team); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + OnMapStart_MapTriggers(); + OnMapStart_KZConfig(); + OnMapStart_Options(); + OnMapStart_Prefix(); + OnMapStart_CourseRegister(); + OnMapStart_MapStarts(); + OnMapStart_MapEnd(); + OnMapStart_VirtualButtons(); + OnMapStart_FixMissingSpawns(); + OnMapStart_Checkpoints(); + OnMapStart_TeamNumber(); + OnMapStart_Demofix(); +} + +public void OnMapEnd() +{ + OnMapEnd_Demofix(); +} + +public void OnGameFrame() +{ + OnGameFrame_TeamNumber(); + OnGameFrame_Triggerfix(); +} + +public void OnConfigsExecuted() +{ + OnConfigsExecuted_TimeLimit(); + OnConfigsExecuted_OptionsMenu(); +} + +public Action OnNormalSound(int clients[MAXPLAYERS], int &numClients, char sample[PLATFORM_MAX_PATH], int &entity, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[PLATFORM_MAX_PATH], int &seed) +{ + if (OnNormalSound_StopSounds(entity) == Plugin_Handled) + { + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + // Don't react to player related entities + if (StrEqual(classname, "predicted_viewmodel") || StrEqual(classname, "item_assaultsuit") + || StrEqual(classname, "cs_bot") || StrEqual(classname, "player") + || StrContains(classname, "weapon") != -1) + { + return; + } + SDKHook(entity, SDKHook_Spawn, OnEntitySpawned); + SDKHook(entity, SDKHook_SpawnPost, OnEntitySpawnedPost); + OnEntityCreated_Triggerfix(entity, classname); +} + +public void OnEntitySpawned(int entity) +{ + OnEntitySpawned_MapTriggers(entity); + OnEntitySpawned_MapButtons(entity); + OnEntitySpawned_MapStarts(entity); + OnEntitySpawned_MapZones(entity); +} + +public void OnEntitySpawnedPost(int entity) +{ + OnEntitySpawnedPost_MapStarts(entity); + OnEntitySpawnedPost_MapEnd(entity); +} + +public void OnClientConnected(int client) +{ + OnClientConnected_Triggerfix(client); +} + +public void OnRoundStart(Event event, const char[] name, bool dontBroadcast) // round_start post no copy hook +{ + if (event == INVALID_HANDLE) + { + OnRoundStart_Timer(); + OnRoundStart_ForceAllTalk(); + OnRoundStart_Demofix(); + return; + } + else + { + char objective[64]; + event.GetString("objective", objective, sizeof(objective)); + /* + External plugins that record GOTV demos can call round_start event to fix demo corruption, + which happens to stop the players' timer. GOKZ should only react on real round start events only. + */ + if (IsRealObjective(objective)) + { + OnRoundStart_Timer(); + OnRoundStart_ForceAllTalk(); + OnRoundStart_Demofix(); + } + } +} + +public Action CS_OnTerminateRound(float &delay, CSRoundEndReason &reason) +{ + return Plugin_Handled; +} + +public void GOKZ_OnModeUnloaded(int mode) +{ + OnModeUnloaded_Options(mode); +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_OptionsMenu(); +} + + + +// =====[ PRIVATE ]===== + +static void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-core", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_chat_prefix = AutoExecConfig_CreateConVar("gokz_chat_prefix", "{green}KZ {grey}| ", "Chat prefix used for GOKZ messages."); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + gCV_sv_full_alltalk = FindConVar("sv_full_alltalk"); + + // Remove unwanted flags from constantly changed mode convars - replication is done manually in mode plugins + for (int i = 0; i < MODECVAR_COUNT; i++) + { + FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_NOTIFY; + FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_REPLICATED; + } +} + +static void HookEvents() +{ + AddCommandsListeners(); + + HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); + HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre); + HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); + HookEvent("player_jump", OnPlayerJump); + HookEvent("round_start", OnRoundStart, EventHookMode_PostNoCopy); + AddNormalSoundHook(OnNormalSound); + + GameData gameData = new GameData("sdktools.games"); + int offset; + + // Setup DHooks OnTeleport for players + offset = gameData.GetOffset("Teleport"); + gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool); + + gameData = new GameData("sdktools.games/engine.csgo"); + offset = gameData.GetOffset("SetEntityModel"); + gH_DHooks_SetModel = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnSetModel); + DHookAddParam(gH_DHooks_SetModel, HookParamType_CharPtr); + + delete gameData; +} + +static void HookClientEvents(int client) +{ + DHookEntity(gH_DHooks_OnTeleport, true, client); + DHookEntity(gH_DHooks_SetModel, true, client); + SDKHook(client, SDKHook_SpawnPost, OnCSPlayerSpawnPost); + SDKHook(client, SDKHook_PreThinkPost, OnClientPreThinkPost); +} + +static void UpdateTrackingVariables(int client, int cmdnum, int buttons) +{ + if (IsPlayerAlive(client)) + { + gB_OldOnGround[client] = Movement_GetOnGround(client); + } + + gI_OldButtons[client] = buttons; + + if (gB_OriginTeleported[client] || gB_VelocityTeleported[client]) + { + gI_TeleportCmdNum[client] = cmdnum; + } + gB_OriginTeleported[client] = false; + gB_VelocityTeleported[client] = false; +} + +static bool IsRealObjective(char[] objective) +{ + return StrEqual(objective, "PRISON ESCAPE") || StrEqual(objective, "DEATHMATCH") + || StrEqual(objective, "BOMB TARGET") || StrEqual(objective, "HOSTAGE RESCUE"); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/commands.sp b/sourcemod/scripting/gokz-core/commands.sp new file mode 100644 index 0000000..6aba82c --- /dev/null +++ b/sourcemod/scripting/gokz-core/commands.sp @@ -0,0 +1,385 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_options", CommandOptions, "[KZ] Open the options menu."); + RegConsoleCmd("sm_o", CommandOptions, "[KZ] Open the options menu."); + RegConsoleCmd("sm_checkpoint", CommandMakeCheckpoint, "[KZ] Set a checkpoint."); + RegConsoleCmd("sm_gocheck", CommandTeleportToCheckpoint, "[KZ] Teleport to your current checkpoint."); + RegConsoleCmd("sm_prev", CommandPrevCheckpoint, "[KZ] Go back a checkpoint."); + RegConsoleCmd("sm_next", CommandNextCheckpoint, "[KZ] Go forward a checkpoint."); + RegConsoleCmd("sm_undo", CommandUndoTeleport, "[KZ] Undo teleport."); + RegConsoleCmd("sm_start", CommandTeleportToStart, "[KZ] Teleport to the start."); + RegConsoleCmd("sm_searchstart", CommandSearchStart, "[KZ] Teleport to the start zone/button of a specified course."); + RegConsoleCmd("sm_end", CommandTeleportToEnd, "[KZ] Teleport to the end."); + RegConsoleCmd("sm_restart", CommandTeleportToStart, "[KZ] Teleport to your start position."); + RegConsoleCmd("sm_r", CommandTeleportToStart, "[KZ] Teleport to your start position."); + RegConsoleCmd("sm_setstartpos", CommandSetStartPos, "[KZ] Set your custom start position to your current position."); + RegConsoleCmd("sm_ssp", CommandSetStartPos, "[KZ] Set your custom start position to your current position."); + RegConsoleCmd("sm_clearstartpos", CommandClearStartPos, "[KZ] Clear your custom start position."); + RegConsoleCmd("sm_csp", CommandClearStartPos, "[KZ] Clear your custom start position."); + RegConsoleCmd("sm_main", CommandMain, "[KZ] Teleport to the start of the main course."); + RegConsoleCmd("sm_m", CommandMain, "[KZ] Teleport to the start of the main course."); + RegConsoleCmd("sm_bonus", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!bonus <#bonus>"); + RegConsoleCmd("sm_b", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!b <#bonus>"); + RegConsoleCmd("sm_pause", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position."); + RegConsoleCmd("sm_resume", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position."); + RegConsoleCmd("sm_stop", CommandStopTimer, "[KZ] Stop your timer."); + RegConsoleCmd("sm_virtualbuttonindicators", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators."); + RegConsoleCmd("sm_vbi", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators."); + RegConsoleCmd("sm_virtualbuttons", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved."); + RegConsoleCmd("sm_vb", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved."); + RegConsoleCmd("sm_mode", CommandMode, "[KZ] Open the movement mode selection menu."); + RegConsoleCmd("sm_vanilla", CommandVanilla, "[KZ] Switch to the Vanilla mode."); + RegConsoleCmd("sm_vnl", CommandVanilla, "[KZ] Switch to the Vanilla mode."); + RegConsoleCmd("sm_v", CommandVanilla, "[KZ] Switch to the Vanilla mode."); + RegConsoleCmd("sm_simplekz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode."); + RegConsoleCmd("sm_skz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode."); + RegConsoleCmd("sm_s", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode."); + RegConsoleCmd("sm_kztimer", CommandKZTimer, "[KZ] Switch to the KZTimer mode."); + RegConsoleCmd("sm_kzt", CommandKZTimer, "[KZ] Switch to the KZTimer mode."); + RegConsoleCmd("sm_k", CommandKZTimer, "[KZ] Switch to the KZTimer mode."); + RegConsoleCmd("sm_nc", CommandToggleNoclip, "[KZ] Toggle noclip."); + RegConsoleCmd("+noclip", CommandEnableNoclip, "[KZ] Noclip on."); + RegConsoleCmd("-noclip", CommandDisableNoclip, "[KZ] Noclip off."); + RegConsoleCmd("sm_ncnt", CommandToggleNoclipNotrigger, "[KZ] Toggle noclip-notrigger."); + RegConsoleCmd("+noclipnt", CommandEnableNoclipNotrigger, "[KZ] Noclip-notrigger on."); + RegConsoleCmd("-noclipnt", CommandDisableNoclipNotrigger, "[KZ] Noclip-notrigger off."); + RegConsoleCmd("sm_sg", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard."); + RegConsoleCmd("sm_safe", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard."); + RegConsoleCmd("sm_safeguard", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard."); + RegConsoleCmd("sm_pro", CommandProSafeGuard, "[KZ] Toggle PRO safeguard."); + RegConsoleCmd("kill", CommandKill); + RegConsoleCmd("killvector", CommandKill); + RegConsoleCmd("explode", CommandKill); + RegConsoleCmd("explodevector", CommandKill); +} + +void AddCommandsListeners() +{ + AddCommandListener(CommandJoinTeam, "jointeam"); +} + +bool SwitchToModeIfAvailable(int client, int mode) +{ + if (!GOKZ_GetModeLoaded(mode)) + { + GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", gC_ModeNames[mode]); + return false; + } + else + { + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return false; + } + GOKZ_SetCoreOption(client, Option_Mode, mode); + return true; + } +} + +public Action CommandKill(int client, int args) +{ + if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + return Plugin_Continue; +} + +public Action CommandOptions(int client, int args) +{ + DisplayOptionsMenu(client); + return Plugin_Handled; +} + +public Action CommandJoinTeam(int client, const char[] command, int argc) +{ + char teamString[4]; + GetCmdArgString(teamString, sizeof(teamString)); + int team = StringToInt(teamString); + + if (team == CS_TEAM_SPECTATOR) + { + if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client)) + { + SendFakeTeamEvent(client); + return Plugin_Handled; + } + } + else if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + SendFakeTeamEvent(client); + return Plugin_Handled; + } + GOKZ_JoinTeam(client, team); + return Plugin_Handled; +} + +public Action CommandMakeCheckpoint(int client, int args) +{ + GOKZ_MakeCheckpoint(client); + return Plugin_Handled; +} + +public Action CommandTeleportToCheckpoint(int client, int args) +{ + GOKZ_TeleportToCheckpoint(client); + return Plugin_Handled; +} + +public Action CommandPrevCheckpoint(int client, int args) +{ + GOKZ_PrevCheckpoint(client); + return Plugin_Handled; +} + +public Action CommandNextCheckpoint(int client, int args) +{ + GOKZ_NextCheckpoint(client); + return Plugin_Handled; +} + +public Action CommandUndoTeleport(int client, int args) +{ + GOKZ_UndoTeleport(client); + return Plugin_Handled; +} + +public Action CommandTeleportToStart(int client, int args) +{ + GOKZ_TeleportToStart(client); + return Plugin_Handled; +} + +public Action CommandSearchStart(int client, int args) +{ + if (args == 0) + { + GOKZ_TeleportToSearchStart(client, GetCurrentCourse(client)); + return Plugin_Handled; + } + else + { + char argCourse[4]; + GetCmdArg(1, argCourse, sizeof(argCourse)); + int course = StringToInt(argCourse); + if (GOKZ_IsValidCourse(course, false)) + { + GOKZ_TeleportToSearchStart(client, course); + } + else if (StrEqual(argCourse, "main", false) || course == 0) + { + GOKZ_TeleportToSearchStart(client, 0); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse); + } + } + return Plugin_Handled; +} + +public Action CommandTeleportToEnd(int client, int args) +{ + if (args == 0) + { + GOKZ_TeleportToEnd(client, GetCurrentCourse(client)); + } + else + { + char argCourse[4]; + GetCmdArg(1, argCourse, sizeof(argCourse)); + int course = StringToInt(argCourse); + if (GOKZ_IsValidCourse(course, false)) + { + GOKZ_TeleportToEnd(client, course); + } + else if (StrEqual(argCourse, "main", false) || course == 0) + { + GOKZ_TeleportToEnd(client, 0); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse); + } + } + return Plugin_Handled; +} + +public Action CommandSetStartPos(int client, int args) +{ + SetStartPositionToCurrent(client, StartPositionType_Custom); + + GOKZ_PrintToChat(client, true, "%t", "Set Custom Start Position"); + if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled) + { + GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Set Start Position"); + } + + return Plugin_Handled; +} + +public Action CommandClearStartPos(int client, int args) +{ + if (ClearCustomStartPosition(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Cleared Custom Start Position"); + } + + return Plugin_Handled; +} + +public Action CommandMain(int client, int args) +{ + TeleportToCourseStart(client, 0); + return Plugin_Handled; +} + +public Action CommandBonus(int client, int args) +{ + if (args == 0) + { // Go to Bonus 1 + TeleportToCourseStart(client, 1); + } + else + { // Go to specified Bonus # + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + TeleportToCourseStart(client, bonus); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandTogglePause(int client, int args) +{ + if (!IsPlayerAlive(client)) + { + GOKZ_RespawnPlayer(client); + } + else + { + TogglePause(client); + } + return Plugin_Handled; +} + +public Action CommandStopTimer(int client, int args) +{ + if (TimerStop(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Timer Stopped"); + } + return Plugin_Handled; +} + +public Action CommandToggleVirtualButtonIndicators(int client, int args) +{ + if (GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled) + { + GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Enabled); + } + else + { + GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Disabled); + } + return Plugin_Handled; +} + +public Action CommandToggleVirtualButtonsLock(int client, int args) +{ + if (ToggleVirtualButtonsLock(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Locked Virtual Buttons"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Unlocked Virtual Buttons"); + } + return Plugin_Handled; +} + +public Action CommandMode(int client, int args) +{ + DisplayModeMenu(client); + return Plugin_Handled; +} + +public Action CommandVanilla(int client, int args) +{ + SwitchToModeIfAvailable(client, Mode_Vanilla); + return Plugin_Handled; +} + +public Action CommandSimpleKZ(int client, int args) +{ + SwitchToModeIfAvailable(client, Mode_SimpleKZ); + return Plugin_Handled; +} + +public Action CommandKZTimer(int client, int args) +{ + SwitchToModeIfAvailable(client, Mode_KZTimer); + return Plugin_Handled; +} + +public Action CommandToggleNoclip(int client, int args) +{ + ToggleNoclip(client); + return Plugin_Handled; +} + +public Action CommandEnableNoclip(int client, int args) +{ + EnableNoclip(client); + return Plugin_Handled; +} + +public Action CommandDisableNoclip(int client, int args) +{ + DisableNoclip(client); + return Plugin_Handled; +} + +public Action CommandToggleNoclipNotrigger(int client, int args) +{ + ToggleNoclipNotrigger(client); + return Plugin_Handled; +} + +public Action CommandEnableNoclipNotrigger(int client, int args) +{ + EnableNoclipNotrigger(client); + return Plugin_Handled; +} + +public Action CommandDisableNoclipNotrigger(int client, int args) +{ + DisableNoclipNotrigger(client); + return Plugin_Handled; +} + +public Action CommandNubSafeGuard(int client, int args) +{ + ToggleNubSafeGuard(client); + return Plugin_Handled; +} + +public Action CommandProSafeGuard(int client, int args) +{ + ToggleProSafeGuard(client); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/demofix.sp b/sourcemod/scripting/gokz-core/demofix.sp new file mode 100644 index 0000000..84a9307 --- /dev/null +++ b/sourcemod/scripting/gokz-core/demofix.sp @@ -0,0 +1,110 @@ +static ConVar CV_EnableDemofix; +static Handle H_DemofixTimer; +static bool mapRunning; + +void OnPluginStart_Demofix() +{ + AddCommandListener(Command_Demorestart, "demorestart"); + CV_EnableDemofix = AutoExecConfig_CreateConVar("gokz_demofix", "1", "Whether GOKZ applies demo record fix to server. (0 = Disabled, 1 = Update warmup period once, 2 = Regularly reset warmup period)", _, true, 0.0, true, 2.0); + CV_EnableDemofix.AddChangeHook(OnDemofixConVarChanged); + // If the map is tweaking the warmup value, we need to rerun the fix again. + FindConVar("mp_warmuptime").AddChangeHook(OnDemofixConVarChanged); + // We assume that the map is already loaded on late load. + if (gB_LateLoad) + { + mapRunning = true; + } +} + +void OnMapStart_Demofix() +{ + mapRunning = true; +} + +void OnMapEnd_Demofix() +{ + mapRunning = false; +} + +void OnRoundStart_Demofix() +{ + DoDemoFix(); +} + +public Action Command_Demorestart(int client, const char[] command, int argc) +{ + FixRecord(client); + return Plugin_Continue; +} + +static void FixRecord(int client) +{ + // For some reasons, demo playback speed is absolute trash without a round_start event. + // So whenever the client starts recording a demo, we create the event and send it to them. + Event e = CreateEvent("round_start", true); + int timelimit = FindConVar("mp_timelimit").IntValue; + e.SetInt("timelimit", timelimit); + e.SetInt("fraglimit", 0); + e.SetString("objective", "demofix"); + + e.FireToClient(client); + delete e; +} + +public void OnDemofixConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + DoDemoFix(); +} + +public Action Timer_EnableDemoRecord(Handle timer) +{ + EnableDemoRecord(); + return Plugin_Continue; +} + +static void DoDemoFix() +{ + if (H_DemofixTimer != null) + { + delete H_DemofixTimer; + } + // Setting the cvar value to 1 can avoid clogging the demo file and slightly increase performance. + switch (CV_EnableDemofix.IntValue) + { + case 0: + { + if (!mapRunning) + { + return; + } + + GameRules_SetProp("m_bWarmupPeriod", 0); + } + case 1: + { + // Set warmup time to 2^31-1, effectively forever + if (FindConVar("mp_warmuptime").IntValue != 2147483647) + { + FindConVar("mp_warmuptime").SetInt(2147483647); + } + EnableDemoRecord(); + } + case 2: + { + H_DemofixTimer = CreateTimer(1.0, Timer_EnableDemoRecord, _, TIMER_REPEAT); + } + } +} + +static void EnableDemoRecord() +{ + // Enable warmup to allow demo recording + // m_fWarmupPeriodEnd is set in the past to hide the timer UI + if (!mapRunning) + { + return; + } + GameRules_SetProp("m_bWarmupPeriod", 1); + GameRules_SetPropFloat("m_fWarmupPeriodStart", GetGameTime() - 1.0); + GameRules_SetPropFloat("m_fWarmupPeriodEnd", GetGameTime() - 1.0); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/forwards.sp b/sourcemod/scripting/gokz-core/forwards.sp new file mode 100644 index 0000000..efb064f --- /dev/null +++ b/sourcemod/scripting/gokz-core/forwards.sp @@ -0,0 +1,401 @@ +static GlobalForward H_OnOptionsLoaded; +static GlobalForward H_OnOptionChanged; +static GlobalForward H_OnTimerStart; +static GlobalForward H_OnTimerStart_Post; +static GlobalForward H_OnTimerEnd; +static GlobalForward H_OnTimerEnd_Post; +static GlobalForward H_OnTimerEndMessage; +static GlobalForward H_OnTimerStopped; +static GlobalForward H_OnPause; +static GlobalForward H_OnPause_Post; +static GlobalForward H_OnResume; +static GlobalForward H_OnResume_Post; +static GlobalForward H_OnMakeCheckpoint; +static GlobalForward H_OnMakeCheckpoint_Post; +static GlobalForward H_OnTeleportToCheckpoint; +static GlobalForward H_OnTeleportToCheckpoint_Post; +static GlobalForward H_OnTeleport; +static GlobalForward H_OnPrevCheckpoint; +static GlobalForward H_OnPrevCheckpoint_Post; +static GlobalForward H_OnNextCheckpoint; +static GlobalForward H_OnNextCheckpoint_Post; +static GlobalForward H_OnTeleportToStart; +static GlobalForward H_OnTeleportToStart_Post; +static GlobalForward H_OnTeleportToEnd; +static GlobalForward H_OnTeleportToEnd_Post; +static GlobalForward H_OnUndoTeleport; +static GlobalForward H_OnUndoTeleport_Post; +static GlobalForward H_OnCountedTeleport_Post; +static GlobalForward H_OnStartPositionSet_Post; +static GlobalForward H_OnJumpValidated; +static GlobalForward H_OnJumpInvalidated; +static GlobalForward H_OnJoinTeam; +static GlobalForward H_OnFirstSpawn; +static GlobalForward H_OnModeLoaded; +static GlobalForward H_OnModeUnloaded; +static GlobalForward H_OnTimerNativeCalledExternally; +static GlobalForward H_OnOptionsMenuCreated; +static GlobalForward H_OnOptionsMenuReady; +static GlobalForward H_OnCourseRegistered; +static GlobalForward H_OnRunInvalidated; +static GlobalForward H_OnEmitSoundToClient; + +void CreateGlobalForwards() +{ + H_OnOptionsLoaded = new GlobalForward("GOKZ_OnOptionsLoaded", ET_Ignore, Param_Cell); + H_OnOptionChanged = new GlobalForward("GOKZ_OnOptionChanged", ET_Ignore, Param_Cell, Param_String, Param_Cell); + H_OnTimerStart = new GlobalForward("GOKZ_OnTimerStart", ET_Hook, Param_Cell, Param_Cell); + H_OnTimerStart_Post = new GlobalForward("GOKZ_OnTimerStart_Post", ET_Ignore, Param_Cell, Param_Cell); + H_OnTimerEnd = new GlobalForward("GOKZ_OnTimerEnd", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell); + H_OnTimerEnd_Post = new GlobalForward("GOKZ_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Cell); + H_OnTimerEndMessage = new GlobalForward("GOKZ_OnTimerEndMessage", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell); + H_OnTimerStopped = new GlobalForward("GOKZ_OnTimerStopped", ET_Ignore, Param_Cell); + H_OnPause = new GlobalForward("GOKZ_OnPause", ET_Hook, Param_Cell); + H_OnPause_Post = new GlobalForward("GOKZ_OnPause_Post", ET_Ignore, Param_Cell); + H_OnResume = new GlobalForward("GOKZ_OnResume", ET_Hook, Param_Cell); + H_OnResume_Post = new GlobalForward("GOKZ_OnResume_Post", ET_Ignore, Param_Cell); + H_OnMakeCheckpoint = new GlobalForward("GOKZ_OnMakeCheckpoint", ET_Hook, Param_Cell); + H_OnMakeCheckpoint_Post = new GlobalForward("GOKZ_OnMakeCheckpoint_Post", ET_Ignore, Param_Cell); + H_OnTeleportToCheckpoint = new GlobalForward("GOKZ_OnTeleportToCheckpoint", ET_Hook, Param_Cell); + H_OnTeleportToCheckpoint_Post = new GlobalForward("GOKZ_OnTeleportToCheckpoint_Post", ET_Ignore, Param_Cell); + H_OnTeleport = new GlobalForward("GOKZ_OnTeleport", ET_Hook, Param_Cell); + H_OnPrevCheckpoint = new GlobalForward("GOKZ_OnPrevCheckpoint", ET_Hook, Param_Cell); + H_OnPrevCheckpoint_Post = new GlobalForward("GOKZ_OnPrevCheckpoint_Post", ET_Ignore, Param_Cell); + H_OnNextCheckpoint = new GlobalForward("GOKZ_OnNextCheckpoint", ET_Hook, Param_Cell); + H_OnNextCheckpoint_Post = new GlobalForward("GOKZ_OnNextCheckpoint_Post", ET_Ignore, Param_Cell); + H_OnTeleportToStart = new GlobalForward("GOKZ_OnTeleportToStart", ET_Hook, Param_Cell, Param_Cell); + H_OnTeleportToStart_Post = new GlobalForward("GOKZ_OnTeleportToStart_Post", ET_Ignore, Param_Cell, Param_Cell); + H_OnTeleportToEnd = new GlobalForward("GOKZ_OnTeleportToEnd", ET_Hook, Param_Cell, Param_Cell); + H_OnTeleportToEnd_Post = new GlobalForward("GOKZ_OnTeleportToEnd_Post", ET_Ignore, Param_Cell, Param_Cell); + H_OnUndoTeleport = new GlobalForward("GOKZ_OnUndoTeleport", ET_Hook, Param_Cell); + H_OnUndoTeleport_Post = new GlobalForward("GOKZ_OnUndoTeleport_Post", ET_Ignore, Param_Cell); + H_OnStartPositionSet_Post = new GlobalForward("GOKZ_OnStartPositionSet_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Array, Param_Array); + H_OnCountedTeleport_Post = new GlobalForward("GOKZ_OnCountedTeleport_Post", ET_Ignore, Param_Cell); + H_OnJumpValidated = new GlobalForward("GOKZ_OnJumpValidated", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell); + H_OnJumpInvalidated = new GlobalForward("GOKZ_OnJumpInvalidated", ET_Ignore, Param_Cell); + H_OnJoinTeam = new GlobalForward("GOKZ_OnJoinTeam", ET_Ignore, Param_Cell, Param_Cell); + H_OnFirstSpawn = new GlobalForward("GOKZ_OnFirstSpawn", ET_Ignore, Param_Cell); + H_OnModeLoaded = new GlobalForward("GOKZ_OnModeLoaded", ET_Ignore, Param_Cell); + H_OnModeUnloaded = new GlobalForward("GOKZ_OnModeUnloaded", ET_Ignore, Param_Cell); + H_OnTimerNativeCalledExternally = new GlobalForward("GOKZ_OnTimerNativeCalledExternally", ET_Event, Param_Cell, Param_Cell); + H_OnOptionsMenuCreated = new GlobalForward("GOKZ_OnOptionsMenuCreated", ET_Ignore, Param_Cell); + H_OnOptionsMenuReady = new GlobalForward("GOKZ_OnOptionsMenuReady", ET_Ignore, Param_Cell); + H_OnCourseRegistered = new GlobalForward("GOKZ_OnCourseRegistered", ET_Ignore, Param_Cell); + H_OnRunInvalidated = new GlobalForward("GOKZ_OnRunInvalidated", ET_Ignore, Param_Cell); + H_OnEmitSoundToClient = new GlobalForward("GOKZ_OnEmitSoundToClient", ET_Hook, Param_Cell, Param_String, Param_FloatByRef, Param_String); +} + +void Call_GOKZ_OnOptionsLoaded(int client) +{ + Call_StartForward(H_OnOptionsLoaded); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnOptionChanged(int client, const char[] option, int optionValue) +{ + Call_StartForward(H_OnOptionChanged); + Call_PushCell(client); + Call_PushString(option); + Call_PushCell(optionValue); + Call_Finish(); +} + +void Call_GOKZ_OnTimerStart(int client, int course, Action &result) +{ + Call_StartForward(H_OnTimerStart); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(result); +} + +void Call_GOKZ_OnTimerStart_Post(int client, int course) +{ + Call_StartForward(H_OnTimerStart_Post); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(); +} + +void Call_GOKZ_OnTimerEnd(int client, int course, float time, int teleportsUsed, Action &result) +{ + Call_StartForward(H_OnTimerEnd); + Call_PushCell(client); + Call_PushCell(course); + Call_PushFloat(time); + Call_PushCell(teleportsUsed); + Call_Finish(result); +} + +void Call_GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + Call_StartForward(H_OnTimerEnd_Post); + Call_PushCell(client); + Call_PushCell(course); + Call_PushFloat(time); + Call_PushCell(teleportsUsed); + Call_Finish(); +} + +void Call_GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed, Action &result) +{ + Call_StartForward(H_OnTimerEndMessage); + Call_PushCell(client); + Call_PushCell(course); + Call_PushFloat(time); + Call_PushCell(teleportsUsed); + Call_Finish(result); +} + +void Call_GOKZ_OnTimerStopped(int client) +{ + Call_StartForward(H_OnTimerStopped); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnPause(int client, Action &result) +{ + Call_StartForward(H_OnPause); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnPause_Post(int client) +{ + Call_StartForward(H_OnPause_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnResume(int client, Action &result) +{ + Call_StartForward(H_OnResume); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnResume_Post(int client) +{ + Call_StartForward(H_OnResume_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnMakeCheckpoint(int client, Action &result) +{ + Call_StartForward(H_OnMakeCheckpoint); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnMakeCheckpoint_Post(int client) +{ + Call_StartForward(H_OnMakeCheckpoint_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnTeleportToCheckpoint(int client, Action &result) +{ + Call_StartForward(H_OnTeleportToCheckpoint); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnTeleportToCheckpoint_Post(int client) +{ + Call_StartForward(H_OnTeleportToCheckpoint_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnTeleport(int client) +{ + Call_StartForward(H_OnTeleport); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnPrevCheckpoint(int client, Action &result) +{ + Call_StartForward(H_OnPrevCheckpoint); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnPrevCheckpoint_Post(int client) +{ + Call_StartForward(H_OnPrevCheckpoint_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnNextCheckpoint(int client, Action &result) +{ + Call_StartForward(H_OnNextCheckpoint); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnNextCheckpoint_Post(int client) +{ + Call_StartForward(H_OnNextCheckpoint_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnTeleportToStart(int client, int course, Action &result) +{ + Call_StartForward(H_OnTeleportToStart); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(result); +} + +void Call_GOKZ_OnTeleportToStart_Post(int client, int course) +{ + Call_StartForward(H_OnTeleportToStart_Post); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(); +} + +void Call_GOKZ_OnTeleportToEnd(int client, int course, Action &result) +{ + Call_StartForward(H_OnTeleportToEnd); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(result); +} + +void Call_GOKZ_OnTeleportToEnd_Post(int client, int course) +{ + Call_StartForward(H_OnTeleportToEnd_Post); + Call_PushCell(client); + Call_PushCell(course); + Call_Finish(); +} + +void Call_GOKZ_OnUndoTeleport(int client, Action &result) +{ + Call_StartForward(H_OnUndoTeleport); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnUndoTeleport_Post(int client) +{ + Call_StartForward(H_OnUndoTeleport_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnCountedTeleport_Post(int client) +{ + Call_StartForward(H_OnCountedTeleport_Post); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3]) +{ + Call_StartForward(H_OnStartPositionSet_Post); + Call_PushCell(client); + Call_PushCell(type); + Call_PushArray(origin, 3); + Call_PushArray(angles, 3); + Call_Finish(); +} + +void Call_GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + Call_StartForward(H_OnJumpValidated); + Call_PushCell(client); + Call_PushCell(jumped); + Call_PushCell(ladderJump); + Call_PushCell(jumpbug); + Call_Finish(); +} + +void Call_GOKZ_OnJumpInvalidated(int client) +{ + Call_StartForward(H_OnJumpInvalidated); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnJoinTeam(int client, int team) +{ + Call_StartForward(H_OnJoinTeam); + Call_PushCell(client); + Call_PushCell(team); + Call_Finish(); +} + +void Call_GOKZ_OnFirstSpawn(int client) +{ + Call_StartForward(H_OnFirstSpawn); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnModeLoaded(int mode) +{ + Call_StartForward(H_OnModeLoaded); + Call_PushCell(mode); + Call_Finish(); +} + +void Call_GOKZ_OnModeUnloaded(int mode) +{ + Call_StartForward(H_OnModeUnloaded); + Call_PushCell(mode); + Call_Finish(); +} + +void Call_GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client, Action &result) +{ + Call_StartForward(H_OnTimerNativeCalledExternally); + Call_PushCell(plugin); + Call_PushCell(client); + Call_Finish(result); +} + +void Call_GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + Call_StartForward(H_OnOptionsMenuCreated); + Call_PushCell(topMenu); + Call_Finish(); +} + +void Call_GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + Call_StartForward(H_OnOptionsMenuReady); + Call_PushCell(topMenu); + Call_Finish(); +} + +void Call_GOKZ_OnCourseRegistered(int course) +{ + Call_StartForward(H_OnCourseRegistered); + Call_PushCell(course); + Call_Finish(); +} + +void Call_GOKZ_OnRunInvalidated(int client) +{ + Call_StartForward(H_OnRunInvalidated); + Call_PushCell(client); + Call_Finish(); +} + +void Call_GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description, Action &result) +{ + Call_StartForward(H_OnEmitSoundToClient); + Call_PushCell(client); + Call_PushString(sample); + Call_PushFloatRef(volume); + Call_PushString(description); + Call_Finish(result); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/buttons.sp b/sourcemod/scripting/gokz-core/map/buttons.sp new file mode 100644 index 0000000..8923fbd --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/buttons.sp @@ -0,0 +1,138 @@ +/* + Hooks between specifically named func_buttons and GOKZ. +*/ + + + +static Regex RE_BonusStartButton; +static Regex RE_BonusEndButton; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapButtons() +{ + RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX); + RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX); +} + +void OnEntitySpawned_MapButtons(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("func_button", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + int course = 0; + if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnPressed", OnStartButtonPress); + RegisterCourseStart(course); + } + else if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnPressed", OnEndButtonPress); + RegisterCourseEnd(course); + } + else if ((course = GetStartButtonBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnPressed", OnBonusStartButtonPress); + RegisterCourseStart(course); + } + else if ((course = GetEndButtonBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnPressed", OnBonusEndButtonPress); + RegisterCourseEnd(course); + } +} + +public void OnStartButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartButtonPress(activator, 0); +} + +public void OnEndButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessEndButtonPress(activator, 0); +} + +public void OnBonusStartButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartButtonBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartButtonPress(activator, course); +} + +public void OnBonusEndButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetEndButtonBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessEndButtonPress(activator, course); +} + + + +// =====[ PRIVATE ]===== + +static void ProcessStartButtonPress(int client, int course) +{ + if (GOKZ_StartTimer(client, course)) + { + // Only calling on success is intended behaviour (and prevents virtual button exploits) + OnStartButtonPress_Teleports(client, course); + OnStartButtonPress_VirtualButtons(client, course); + } +} + +static void ProcessEndButtonPress(int client, int course) +{ + GOKZ_EndTimer(client, course); + OnEndButtonPress_VirtualButtons(client, course); +} + +static int GetStartButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1); +} + +static int GetEndButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/end.sp b/sourcemod/scripting/gokz-core/map/end.sp new file mode 100644 index 0000000..d119084 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/end.sp @@ -0,0 +1,155 @@ +/* + Hooks between specifically named end destinations and GOKZ +*/ + + + +static Regex RE_BonusEndButton; +static Regex RE_BonusEndZone; +static CourseTimerType endType[GOKZ_MAX_COURSES]; +static float endOrigin[GOKZ_MAX_COURSES][3]; +static float endAngles[GOKZ_MAX_COURSES][3]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapEnd() +{ + RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX); + RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX); +} + +void OnEntitySpawnedPost_MapEnd(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + + if (StrEqual("trigger_multiple", buffer, false)) + { + bool isEndZone; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false)) + { + isEndZone = true; + StoreEnd(0, entity, CourseTimerType_ZoneNew); + } + else if (GetEndZoneBonusNumber(entity) != -1) + { + int course = GetEndZoneBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isEndZone = true; + StoreEnd(course, entity, CourseTimerType_ZoneNew); + } + } + } + if (!isEndZone) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer) + { + StoreEnd(trigger.course, entity, CourseTimerType_ZoneLegacy); + } + } + } + else if (StrEqual("func_button", buffer, false)) + { + bool isEndButton; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false)) + { + isEndButton = true; + StoreEnd(0, entity, CourseTimerType_Button); + } + else + { + int course = GetEndButtonBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isEndButton = true; + StoreEnd(course, entity, CourseTimerType_Button); + } + } + } + if (!isEndButton) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer) + { + StoreEnd(trigger.course, entity, CourseTimerType_Button); + } + } + } +} + +void OnMapStart_MapEnd() +{ + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + endType[course] = CourseTimerType_None; + } +} + +bool GetMapEndPosition(int course, float origin[3], float angles[3]) +{ + if (endType[course] == CourseTimerType_None) + { + return false; + } + + origin = endOrigin[course]; + angles = endAngles[course]; + + return true; +} + + + +// =====[ PRIVATE ]===== + +static void StoreEnd(int course, int entity, CourseTimerType type) +{ + // If StoreEnd is called, then there is at least an end position (even though it might not be a valid one) + if (endType[course] < CourseTimerType_Default) + { + endType[course] = CourseTimerType_Default; + } + + // Real zone is always better than "fake" zones which are better than buttons + // as the buttons found in a map with fake zones aren't meant to be visible. + if (endType[course] >= type) + { + return; + } + + float origin[3], distFromCenter[3]; + GetEntityPositions(entity, origin, endOrigin[course], endAngles[course], distFromCenter); + + // If it is a button or the center of the center of the zone is invalid + if (type == CourseTimerType_Button || !IsSpawnValid(endOrigin[course])) + { + // Attempt with various positions around the entity, pick the first valid one. + if (!FindValidPositionAroundTimerEntity(entity, endOrigin[course], endAngles[course], type == CourseTimerType_Button)) + { + endOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone. + return; + } + } + + // Only update the CourseTimerType if a valid position is found. + endType[course] = type; +} + +static int GetEndButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1); +} + +static int GetEndZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1); +} diff --git a/sourcemod/scripting/gokz-core/map/mapfile.sp b/sourcemod/scripting/gokz-core/map/mapfile.sp new file mode 100644 index 0000000..db60e7e --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/mapfile.sp @@ -0,0 +1,502 @@ +/* + Mapping API + + Reads data from the current map file. +*/ + +static Regex RE_BonusStartButton; +static Regex RE_BonusEndButton; + +// NOTE: 4 megabyte array for entity lump reading. +static char gEntityLump[4194304]; + +// =====[ PUBLIC ]===== + +void EntlumpParse(StringMap antiBhopTriggers, StringMap teleportTriggers, StringMap timerButtonTriggers, int &mappingApiVersion) +{ + char mapPath[512]; + GetCurrentMap(mapPath, sizeof(mapPath)); + Format(mapPath, sizeof(mapPath), "maps/%s.bsp", mapPath); + + // https://developer.valvesoftware.com/wiki/Source_BSP_File_Format + + File file = OpenFile(mapPath, "rb"); + if (file != INVALID_HANDLE) + { + int identifier; + file.ReadInt32(identifier); + + if (identifier == GOKZ_BSP_HEADER_IDENTIFIER) + { + // skip version number + file.Seek(4, SEEK_CUR); + + // the entity lump info is the first lump in the array, so we don't need to seek any further. + int offset; + int length; + file.ReadInt32(offset); + file.ReadInt32(length); + + // jump to the start of the entity lump + file.Seek(offset, SEEK_SET); + + int charactersRead = file.ReadString(gEntityLump, sizeof(gEntityLump), length); + delete file; + if (charactersRead >= sizeof(gEntityLump) - 1) + { + PushMappingApiError("ERROR: Entity lump: The map's entity lump is too big! Reduce the amount of entities in your map."); + return; + } + gEntityLump[length] = '\0'; + + int index = 0; + + StringMap entity = new StringMap(); + bool gotWorldSpawn = false; + while (EntlumpParseEntity(entity, gEntityLump, index)) + { + char classname[128]; + char targetName[GOKZ_ENTLUMP_MAX_VALUE]; + entity.GetString("classname", classname, sizeof(classname)); + + if (!gotWorldSpawn && StrEqual("worldspawn", classname, false)) + { + gotWorldSpawn = true; + char versionString[32]; + if (entity.GetString("climb_mapping_api_version", versionString, sizeof(versionString))) + { + if (StringToIntEx(versionString, mappingApiVersion) == 0) + { + PushMappingApiError("ERROR: Entity lump: Couldn't parse Mapping API version from map properties: \"%s\".", versionString); + mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + } + } + else + { + // map doesn't have a mapping api version. + mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + } + } + else if (StrEqual("trigger_multiple", classname, false)) + { + TriggerType triggerType; + if (!gotWorldSpawn || mappingApiVersion != GOKZ_MAPPING_API_VERSION_NONE) + { + if (entity.GetString("targetname", targetName, sizeof(targetName))) + { + // get trigger properties if applicable + triggerType = GetTriggerType(targetName); + if (triggerType == TriggerType_Antibhop) + { + AntiBhopTrigger trigger; + if (GetAntiBhopTriggerEntityProperties(trigger, entity)) + { + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + antiBhopTriggers.SetArray(key, trigger, sizeof(trigger)); + } + } + else if (triggerType == TriggerType_Teleport) + { + TeleportTrigger trigger; + if (GetTeleportTriggerEntityProperties(trigger, entity)) + { + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + teleportTriggers.SetArray(key, trigger, sizeof(trigger)); + } + } + } + } + + // Tracking legacy timer triggers that press the timer buttons upon triggered. + if (triggerType == TriggerType_Invalid) + { + char touchOutput[128]; + ArrayList value; + + if (entity.GetString("OnStartTouch", touchOutput, sizeof(touchOutput))) + { + TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers); + } + else if (entity.GetValue("OnStartTouch", value)) // If there are multiple outputs, we have to check for all of them. + { + for (int i = 0; i < value.Length; i++) + { + value.GetString(i, touchOutput, sizeof(touchOutput)); + TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers); + } + } + } + } + else if (StrEqual("func_button", classname, false)) + { + char pressOutput[128]; + ArrayList value; + + if (entity.GetString("OnPressed", pressOutput, sizeof(pressOutput))) + { + TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers); + } + else if (entity.GetValue("OnPressed", value)) // If there are multiple outputs, we have to check for all of them. + { + for (int i = 0; i < value.Length; i++) + { + value.GetString(i, pressOutput, sizeof(pressOutput)); + TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers); + } + } + } + // clear for next loop + entity.Clear(); + } + delete entity; + } + delete file; + } + else + { + // TODO: do something more elegant + SetFailState("Catastrophic extreme hyperfailure! Mapping API Couldn't open the map file for reading! %s. The map file might be gone or another program is using it.", mapPath); + } +} + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapFile() +{ + char buffer[64]; + char press[8]; + FormatEx(press, sizeof(press), "%s%s", CHAR_ESCAPE, "Press"); + + buffer = GOKZ_BONUS_START_BUTTON_NAME_REGEX; + ReplaceStringEx(buffer, sizeof(buffer), "$", ""); + StrCat(buffer, sizeof(buffer), press); + RE_BonusStartButton = CompileRegex(buffer); + + buffer = GOKZ_BONUS_END_BUTTON_NAME_REGEX; + ReplaceStringEx(buffer, sizeof(buffer), "$", ""); + StrCat(buffer, sizeof(buffer), press); + RE_BonusEndButton = CompileRegex(buffer); +} + + +// =====[ PRIVATE ]===== + +static void EntlumpSkipAllWhiteSpace(char[] entityLump, int &index) +{ + while (IsCharSpace(entityLump[index]) && entityLump[index] != '\0') + { + index++; + } +} + +static int EntlumpGetString(char[] result, int maxLength, int copyCount, char[] entityLump, int entlumpIndex) +{ + int finalLength; + for (int i = 0; i < maxLength - 1 && i < copyCount; i++) + { + if (entityLump[entlumpIndex + i] == '\0') + { + break; + } + result[i] = entityLump[entlumpIndex + i]; + finalLength++; + } + + result[finalLength] = '\0'; + return finalLength; +} + +static EntlumpToken EntlumpGetToken(char[] entityLump, int &entlumpIndex) +{ + EntlumpToken result; + + EntlumpSkipAllWhiteSpace(entityLump, entlumpIndex); + + switch (entityLump[entlumpIndex]) + { + case '{': + { + result.type = EntlumpTokenType_OpenBrace; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '}': + { + result.type = EntlumpTokenType_CloseBrace; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '\0': + { + result.type = EntlumpTokenType_EndOfStream; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '\"': + { + result.type = EntlumpTokenType_Identifier; + int identifierLen; + entlumpIndex++; + for (int i = 0; i < sizeof(result.string) - 1; i++) + { + // NOTE: Unterminated strings can probably never happen, since the map has to be + // loaded by the game first and the engine will fail the load before we get to it. + if (entityLump[entlumpIndex + i] == '\0') + { + result.type = EntlumpTokenType_Unknown; + break; + } + if (entityLump[entlumpIndex + i] == '\"') + { + break; + } + result.string[i] = entityLump[entlumpIndex + i]; + identifierLen++; + } + + entlumpIndex += identifierLen + 1; // +1 to skip over last quotation mark + result.string[identifierLen] = '\0'; + } + default: + { + result.type = EntlumpTokenType_Unknown; + result.string[0] = entityLump[entlumpIndex]; + result.string[1] = '\0'; + } + } + + return result; +} + +static bool EntlumpParseEntity(StringMap result, char[] entityLump, int &entlumpIndex) +{ + EntlumpToken token; + token = EntlumpGetToken(entityLump, entlumpIndex); + if (token.type == EntlumpTokenType_EndOfStream) + { + return false; + } + + // NOTE: The following errors will very very likely never happen, since the entity lump has to be + // loaded by the game first and the engine will fail the load before we get to it. + // But if there's an obscure bug in this code, then we'll know!!! + for (;;) + { + token = EntlumpGetToken(entityLump, entlumpIndex); + switch (token.type) + { + case EntlumpTokenType_OpenBrace: + { + continue; + } + case EntlumpTokenType_Identifier: + { + EntlumpToken valueToken; + valueToken = EntlumpGetToken(entityLump, entlumpIndex); + if (valueToken.type == EntlumpTokenType_Identifier) + { + char tempString[GOKZ_ENTLUMP_MAX_VALUE]; + ArrayList values; + if (result.GetString(token.string, tempString, sizeof(tempString))) + { + result.Remove(token.string); + values = new ArrayList(ByteCountToCells(GOKZ_ENTLUMP_MAX_VALUE)); + values.PushString(tempString); + values.PushString(valueToken.string); + result.SetValue(token.string, values); + } + else if (result.GetValue(token.string, values)) + { + values.PushString(valueToken.string); + } + else + { + result.SetString(token.string, valueToken.string); + } + } + else + { + PushMappingApiError("ERROR: Entity lump: Unexpected token \"%s\".", valueToken.string); + return false; + } + } + case EntlumpTokenType_CloseBrace: + { + break; + } + case EntlumpTokenType_EndOfStream: + { + PushMappingApiError("ERROR: Entity lump: Unexpected end of entity lump! Entity lump parsing failed."); + return false; + } + default: + { + PushMappingApiError("ERROR: Entity lump: Invalid token \"%s\". Entity lump parsing failed.", token.string); + return false; + } + } + } + + return true; +} + +static bool GetHammerIDFromEntityStringMap(int &result, StringMap entity) +{ + char hammerID[32]; + if (!entity.GetString("hammerid", hammerID, sizeof(hammerID)) + || StringToIntEx(hammerID, result) == 0) + { + // if we don't have the hammer id, then we can't match the entity to an existing one! + char origin[64]; + entity.GetString("origin", origin, sizeof(origin)); + PushMappingApiError("ERROR: Failed to parse \"hammerid\" keyvalue on trigger! \"%i\" origin: %s.", result, origin); + return false; + } + return true; +} + +static bool GetAntiBhopTriggerEntityProperties(AntiBhopTrigger result, StringMap entity) +{ + if (!GetHammerIDFromEntityStringMap(result.hammerID, entity)) + { + return false; + } + + char time[32]; + if (!entity.GetString("climb_anti_bhop_time", time, sizeof(time)) + || StringToFloatEx(time, result.time) == 0) + { + result.time = GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY; + } + + return true; +} + +static bool GetTeleportTriggerEntityProperties(TeleportTrigger result, StringMap entity) +{ + if (!GetHammerIDFromEntityStringMap(result.hammerID, entity)) + { + return false; + } + + char buffer[64]; + if (!entity.GetString("climb_teleport_type", buffer, sizeof(buffer)) + || StringToIntEx(buffer, view_as<int>(result.type)) == 0) + { + result.type = GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE; + } + + if (!entity.GetString("climb_teleport_destination", result.tpDestination, sizeof(result.tpDestination))) + { + // We don't want triggers without destinations dangling about, so we need to tell everyone about it!!! + PushMappingApiError("ERROR: Could not find \"climb_teleport_destination\" keyvalue on a climb_teleport trigger! hammer id \"%i\".", + result.hammerID); + return false; + } + + if (!entity.GetString("climb_teleport_delay", buffer, sizeof(buffer)) + || StringToFloatEx(buffer, result.delay) == 0) + { + result.delay = GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY; + } + + if (!entity.GetString("climb_teleport_use_dest_angles", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.useDestAngles) == 0) + { + result.useDestAngles = GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES; + } + + if (!entity.GetString("climb_teleport_reset_speed", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.resetSpeed) == 0) + { + result.resetSpeed = GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED; + } + + if (!entity.GetString("climb_teleport_reorient_player", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.reorientPlayer) == 0) + { + result.reorientPlayer = GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER; + } + + if (!entity.GetString("climb_teleport_relative", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.relativeDestination) == 0) + { + result.relativeDestination = GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION; + } + + // NOTE: Clamping + if (IsBhopTrigger(result.type)) + { + result.delay = FloatMax(result.delay, GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY); + } + else + { + result.delay = FloatMax(result.delay, 0.0); + } + + return true; +} + +static void TimerButtonTriggerCheck(char[] touchOutput, int size, StringMap entity, StringMap timerButtonTriggers) +{ + int course = 0; + char startOutput[128]; + char endOutput[128]; + FormatEx(startOutput, sizeof(startOutput), "%s%s%s", GOKZ_START_BUTTON_NAME, CHAR_ESCAPE, "Press"); + FormatEx(endOutput, sizeof(endOutput), "%s%s%s", GOKZ_END_BUTTON_NAME, CHAR_ESCAPE, "Press"); + if (StrContains(touchOutput, startOutput, false) != -1) + { + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = 0; + trigger.isStartTimer = true; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (StrContains(touchOutput, endOutput, false) != -1) + { + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = 0; + trigger.isStartTimer = false; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (RE_BonusStartButton.Match(touchOutput) > 0) + { + RE_BonusStartButton.GetSubString(1, touchOutput, sizeof(size)); + course = StringToInt(touchOutput); + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = course; + trigger.isStartTimer = true; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (RE_BonusEndButton.Match(touchOutput) > 0) + { + RE_BonusEndButton.GetSubString(1, touchOutput, sizeof(size)); + course = StringToInt(touchOutput); + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = course; + trigger.isStartTimer = false; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/prefix.sp b/sourcemod/scripting/gokz-core/map/prefix.sp new file mode 100644 index 0000000..3ecbf89 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/prefix.sp @@ -0,0 +1,48 @@ +/* + Mapping API - Prefix + + Detects the map's prefix. +*/ + + + +static int currentMapPrefix; + + + +// =====[ PUBLIC ]===== + +int GetCurrentMapPrefix() +{ + return currentMapPrefix; +} + + + +// =====[ LISTENERS ]===== + +void OnMapStart_Prefix() +{ + char map[PLATFORM_MAX_PATH], mapPrefix[PLATFORM_MAX_PATH]; + GetCurrentMapDisplayName(map, sizeof(map)); + + // Get all characters before the first '_' character + for (int i = 0; i < sizeof(mapPrefix); i++) + { + if (map[i] == '\0' || map[i] == '_') + { + break; + } + + mapPrefix[i] = map[i]; + } + + if (StrEqual(mapPrefix[0], "kzpro", false)) + { + currentMapPrefix = MapPrefix_KZPro; + } + else + { + currentMapPrefix = MapPrefix_Other; + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/starts.sp b/sourcemod/scripting/gokz-core/map/starts.sp new file mode 100644 index 0000000..94d5b33 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/starts.sp @@ -0,0 +1,219 @@ +/* + Hooks between start destinations and GOKZ. +*/ + + + +static Regex RE_BonusStart; +static bool startExists[GOKZ_MAX_COURSES]; +static float startOrigin[GOKZ_MAX_COURSES][3]; +static float startAngles[GOKZ_MAX_COURSES][3]; + +// Used for SearchStart +static Regex RE_BonusStartButton; +static Regex RE_BonusStartZone; +static CourseTimerType startType[GOKZ_MAX_COURSES]; +static float searchStartOrigin[GOKZ_MAX_COURSES][3]; +static float searchStartAngles[GOKZ_MAX_COURSES][3]; + +// =====[ EVENTS ]===== + +void OnPluginStart_MapStarts() +{ + RE_BonusStart = CompileRegex(GOKZ_BONUS_START_NAME_REGEX); + RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX); + RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX); +} + +void OnEntitySpawned_MapStarts(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("info_teleport_destination", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + if (StrEqual(GOKZ_START_NAME, buffer, false)) + { + StoreStart(0, entity); + } + else + { + int course = GetStartBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + StoreStart(course, entity); + } + } +} + +void OnEntitySpawnedPost_MapStarts(int entity) +{ + char buffer[32]; + GetEntityClassname(entity, buffer, sizeof(buffer)); + + if (StrEqual("trigger_multiple", buffer, false)) + { + bool isStartZone; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false)) + { + isStartZone = true; + StoreSearchStart(0, entity, CourseTimerType_ZoneNew); + } + else if (GetStartZoneBonusNumber(entity) != -1) + { + int course = GetStartZoneBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isStartZone = true; + StoreSearchStart(course, entity, CourseTimerType_ZoneNew); + } + } + } + if (!isStartZone) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer) + { + StoreSearchStart(trigger.course, entity, CourseTimerType_ZoneLegacy); + } + } + + } + else if (StrEqual("func_button", buffer, false)) + { + bool isStartButton; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false)) + { + isStartButton = true; + StoreSearchStart(0, entity, CourseTimerType_Button); + } + else + { + int course = GetStartButtonBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isStartButton = true; + StoreSearchStart(course, entity, CourseTimerType_Button); + } + } + } + if (!isStartButton) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer) + { + StoreSearchStart(trigger.course, entity, CourseTimerType_Button); + } + } + } +} + +void OnMapStart_MapStarts() +{ + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + startExists[course] = false; + startType[course] = CourseTimerType_None; + } +} + +bool GetMapStartPosition(int course, float origin[3], float angles[3]) +{ + if (!startExists[course]) + { + return false; + } + + origin = startOrigin[course]; + angles = startAngles[course]; + + return true; +} + +bool GetSearchStartPosition(int course, float origin[3], float angles[3]) +{ + if (startType[course] == CourseTimerType_None) + { + return false; + } + + origin = searchStartOrigin[course]; + angles = searchStartAngles[course]; + + return true; +} + +// =====[ PRIVATE ]===== + +static void StoreStart(int course, int entity) +{ + float origin[3], angles[3]; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin); + GetEntPropVector(entity, Prop_Data, "m_angRotation", angles); + angles[2] = 0.0; // Roll should always be 0.0 + + startExists[course] = true; + startOrigin[course] = origin; + startAngles[course] = angles; +} + +static void StoreSearchStart(int course, int entity, CourseTimerType type) +{ + // If StoreSearchStart is called, then there is at least an end position (even though it might not be a valid one) + if (startType[course] < CourseTimerType_Default) + { + startType[course] = CourseTimerType_Default; + } + + // Real zone is always better than "fake" zones which are better than buttons + // as the buttons found in a map with fake zones aren't meant to be visible. + if (startType[course] >= type) + { + return; + } + + float origin[3], distFromCenter[3]; + GetEntityPositions(entity, origin, searchStartOrigin[course], searchStartAngles[course], distFromCenter); + + // If it is a button or the center of the center of the zone is invalid + if (type == CourseTimerType_Button || !IsSpawnValid(searchStartOrigin[course])) + { + // Attempt with various positions around the entity, pick the first valid one. + if (!FindValidPositionAroundTimerEntity(entity, searchStartOrigin[course], searchStartAngles[course], type == CourseTimerType_Button)) + { + searchStartOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone. + return; + } + } + + // Only update the CourseTimerType if a valid position is found. + startType[course] = type; +} + + +static int GetStartBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStart, 1); +} + +static int GetStartButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1); +} + +static int GetStartZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/triggers.sp b/sourcemod/scripting/gokz-core/map/triggers.sp new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/triggers.sp @@ -0,0 +1,855 @@ +/* + Mapping API - Triggers + + Implements trigger related features. +*/ + + + +static float lastTrigMultiTouchTime[MAXPLAYERS + 1]; +static float lastTrigTeleTouchTime[MAXPLAYERS + 1]; +static float lastTouchGroundOrLadderTime[MAXPLAYERS + 1]; +static int lastTouchSingleBhopEntRef[MAXPLAYERS + 1]; +static ArrayList lastTouchSequentialBhopEntRefs[MAXPLAYERS + 1]; +static int triggerTouchCount[MAXPLAYERS + 1]; +static int antiCpTriggerTouchCount[MAXPLAYERS + 1]; +static int antiPauseTriggerTouchCount[MAXPLAYERS + 1]; +static int antiJumpstatTriggerTouchCount[MAXPLAYERS + 1]; +static int mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; +static int bhopTouchCount[MAXPLAYERS + 1]; +static bool jumpedThisTick[MAXPLAYERS + 1]; +static float jumpOrigin[MAXPLAYERS + 1][3]; +static float jumpVelocity[MAXPLAYERS + 1][3]; +static ArrayList triggerTouchList[MAXPLAYERS + 1]; // arraylist of TouchedTrigger that the player is currently touching. this array won't ever get long (unless the mapper does something weird). +static StringMap triggerTouchCounts[MAXPLAYERS + 1]; // stringmap of int touch counts with key being a string of the entity reference. +static StringMap antiBhopTriggers; // stringmap of AntiBhopTrigger with key being a string of the m_iHammerID entprop. +static StringMap teleportTriggers; // stringmap of TeleportTrigger with key being a string of the m_iHammerID entprop. +static StringMap timerButtonTriggers; // stringmap of legacy timer zone triggers with key being a string of the m_iHammerID entprop. +static ArrayList parseErrorStrings; + + + +// =====[ PUBLIC ]===== + +bool BhopTriggersJustTouched(int client) +{ + // NOTE: This is slightly incorrect since we touch triggers in the air, but + // it doesn't matter since we can't checkpoint in the air. + if (bhopTouchCount[client] > 0) + { + return true; + } + // GetEngineTime return changes between calls. We only call it once at the beginning. + float engineTime = GetEngineTime(); + // If the player touches a teleport trigger, increase the delay required + if (engineTime - lastTouchGroundOrLadderTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched ground or ladder + && engineTime - lastTrigMultiTouchTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched trigger_multiple + || engineTime - lastTrigTeleTouchTime[client] < GOKZ_BHOP_NO_CHECKPOINT_TIME) // Just touched trigger_teleport + { + return true; + } + + return Movement_GetMovetype(client) == MOVETYPE_LADDER + && triggerTouchCount[client] > 0 + && engineTime - lastTrigTeleTouchTime[client] < GOKZ_LADDER_NO_CHECKPOINT_TIME; +} + +bool AntiCpTriggerIsTouched(int client) +{ + return antiCpTriggerTouchCount[client] > 0; +} + +bool AntiPauseTriggerIsTouched(int client) +{ + return antiPauseTriggerTouchCount[client] > 0; +} + +void PushMappingApiError(char[] format, any ...) +{ + char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH]; + VFormat(error, sizeof(error), format, 2); + parseErrorStrings.PushString(error); +} + +TriggerType GetTriggerType(char[] targetName) +{ + TriggerType result = TriggerType_Invalid; + + if (StrEqual(targetName, GOKZ_ANTI_BHOP_TRIGGER_NAME)) + { + result = TriggerType_Antibhop; + } + else if (StrEqual(targetName, GOKZ_TELEPORT_TRIGGER_NAME)) + { + result = TriggerType_Teleport; + } + + return result; +} + +bool IsBhopTrigger(TeleportType type) +{ + return type == TeleportType_MultiBhop + || type == TeleportType_SingleBhop + || type == TeleportType_SequentialBhop; +} + +bool IsTimerButtonTrigger(int entity, TimerButtonTrigger trigger) +{ + char hammerID[32]; + bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID)); + if (gotHammerID && timerButtonTriggers.GetArray(hammerID, trigger, sizeof(trigger))) + { + return true; + } + return false; +} + +// =====[ EVENTS ]===== + +void OnPluginStart_MapTriggers() +{ + parseErrorStrings = new ArrayList(ByteCountToCells(GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH)); + antiBhopTriggers = new StringMap(); + teleportTriggers = new StringMap(); + timerButtonTriggers = new StringMap(); +} + +void OnMapStart_MapTriggers() +{ + parseErrorStrings.Clear(); + antiBhopTriggers.Clear(); + teleportTriggers.Clear(); + timerButtonTriggers.Clear(); + mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + EntlumpParse(antiBhopTriggers, teleportTriggers, timerButtonTriggers, mapMappingApiVersion); + + if (mapMappingApiVersion > GOKZ_MAPPING_API_VERSION) + { + SetFailState("Map's mapping api version is too big! Maximum supported version is %i, but map has %i. If you're not on the latest GOKZ version, then update!", + GOKZ_MAPPING_API_VERSION, mapMappingApiVersion); + } +} + +void OnClientPutInServer_MapTriggers(int client) +{ + triggerTouchCount[client] = 0; + antiCpTriggerTouchCount[client] = 0; + antiPauseTriggerTouchCount[client] = 0; + antiJumpstatTriggerTouchCount[client] = 0; + + if (triggerTouchList[client] == null) + { + triggerTouchList[client] = new ArrayList(sizeof(TouchedTrigger)); + } + else + { + triggerTouchList[client].Clear(); + } + + if (triggerTouchCounts[client] == null) + { + triggerTouchCounts[client] = new StringMap(); + } + else + { + triggerTouchCounts[client].Clear(); + } + + bhopTouchCount[client] = 0; + + if (lastTouchSequentialBhopEntRefs[client] == null) + { + lastTouchSequentialBhopEntRefs[client] = new ArrayList(); + } + else + { + lastTouchSequentialBhopEntRefs[client].Clear(); + } +} + +void OnPlayerRunCmd_MapTriggers(int client, int &buttons) +{ + int flags = GetEntityFlags(client); + MoveType moveType = GetEntityMoveType(client); + + // if the player isn't touching any bhop triggers on ground/a ladder, then + // reset the singlebhop and sequential bhop state. + if ((flags & FL_ONGROUND || moveType == MOVETYPE_LADDER) + && bhopTouchCount[client] == 0) + { + ResetBhopState(client); + } + + if (antiJumpstatTriggerTouchCount[client] > 0) + { + if (GetFeatureStatus(FeatureType_Native, "GOKZ_JS_InvalidateJump") == FeatureStatus_Available) + { + GOKZ_JS_InvalidateJump(client); + } + } + + // Check if we're touching any triggers and act accordingly. + // NOTE: Read through the touch list in reverse order, so some + // trigger behaviours will be better. Trust me! + int triggerTouchListLength = triggerTouchList[client].Length; + for (int i = triggerTouchListLength - 1; i >= 0; i--) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + + if (touched.triggerType == TriggerType_Antibhop) + { + TouchAntibhopTrigger(client, touched, buttons, flags); + } + else if (touched.triggerType == TriggerType_Teleport) + { + // Sometimes due to lag or whatever, the player can be + // teleported twice by the same trigger. This fixes that. + if (TouchTeleportTrigger(client, touched, flags)) + { + RemoveTriggerFromTouchList(client, EntRefToEntIndex(touched.entRef)); + i--; + triggerTouchListLength--; + } + } + } + jumpedThisTick[client] = false; +} + +void OnPlayerSpawn_MapTriggers(int client) +{ + // Print trigger errors every time a player spawns so that + // mappers and testers can very easily spot mistakes in names + // and get them fixed asap. + if (parseErrorStrings.Length > 0) + { + char errStart[] = "ERROR: Errors detected when trying to load triggers!"; + CPrintToChat(client, "{red}%s", errStart); + PrintToConsole(client, "\n%s", errStart); + + int length = parseErrorStrings.Length; + for (int err = 0; err < length; err++) + { + char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH]; + parseErrorStrings.GetString(err, error, sizeof(error)); + CPrintToChat(client, "{red}%s", error); + PrintToConsole(client, error); + } + CPrintToChat(client, "{red}If the errors get clipped off in the chat, then look in your developer console!\n"); + } +} + +public void OnPlayerJump_Triggers(int client) +{ + jumpedThisTick[client] = true; + GetClientAbsOrigin(client, jumpOrigin[client]); + Movement_GetVelocity(client, jumpVelocity[client]); +} + +void OnEntitySpawned_MapTriggers(int entity) +{ + char classname[32]; + GetEntityClassname(entity, classname, sizeof(classname)); + char name[64]; + GetEntityName(entity, name, sizeof(name)); + + bool triggerMultiple = StrEqual("trigger_multiple", classname); + if (triggerMultiple) + { + char hammerID[32]; + bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID)); + + if (StrEqual(GOKZ_TELEPORT_TRIGGER_NAME, name)) + { + TeleportTrigger teleportTrigger; + if (gotHammerID && teleportTriggers.GetArray(hammerID, teleportTrigger, sizeof(teleportTrigger))) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnTeleportTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnTeleportTrigTouchEnd_MapTriggers); + } + else + { + PushMappingApiError("ERROR: Couldn't match teleport trigger's Hammer ID %s with any Hammer ID from the map.", hammerID); + } + } + else if (StrEqual(GOKZ_ANTI_BHOP_TRIGGER_NAME, name)) + { + AntiBhopTrigger antiBhopTrigger; + if (gotHammerID && antiBhopTriggers.GetArray(hammerID, antiBhopTrigger, sizeof(antiBhopTrigger))) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiBhopTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiBhopTrigTouchEnd_MapTriggers); + } + else + { + PushMappingApiError("ERROR: Couldn't match antibhop trigger's Hammer ID %s with any Hammer ID from the map.", hammerID); + } + } + else if (StrEqual(GOKZ_BHOP_RESET_TRIGGER_NAME, name)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBhopResetTouchStart_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_CP_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiCpTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiCpTrigTouchEnd_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_PAUSE_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiPauseTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiPauseTrigTouchEnd_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiJumpstatTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiJumpstatTrigTouchEnd_MapTriggers); + } + else + { + // NOTE: SDKHook touch hooks bypass trigger filters. We want that only with + // non mapping api triggers because it prevents checkpointing on bhop blocks. + SDKHook(entity, SDKHook_StartTouchPost, OnTrigMultTouchStart_MapTriggers); + SDKHook(entity, SDKHook_EndTouchPost, OnTrigMultTouchEnd_MapTriggers); + } + } + else if (StrEqual("trigger_teleport", classname)) + { + SDKHook(entity, SDKHook_StartTouchPost, OnTrigTeleTouchStart_MapTriggers); + SDKHook(entity, SDKHook_EndTouchPost, OnTrigTeleTouchEnd_MapTriggers); + } +} + +public void OnAntiBhopTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + int touchCount = IncrementTriggerTouchCount(other, entity); + if (touchCount <= 0) + { + // The trigger has fired a matching endtouch output before + // the starttouch output, so ignore it. + return; + } + + if (jumpedThisTick[other]) + { + TeleportEntity(other, jumpOrigin[other], NULL_VECTOR, jumpVelocity[other]); + } + + AddTriggerToTouchList(other, entity, TriggerType_Antibhop); +} + +public void OnAntiBhopTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + DecrementTriggerTouchCount(other, entity); + RemoveTriggerFromTouchList(other, entity); +} + +public void OnTeleportTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + int touchCount = IncrementTriggerTouchCount(other, entity); + if (touchCount <= 0) + { + // The trigger has fired a matching endtouch output before + // the starttouch output, so ignore it. + return; + } + + char key[32]; + GetEntityHammerIDString(entity, key, sizeof(key)); + TeleportTrigger trigger; + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + bhopTouchCount[other]++; + } + + AddTriggerToTouchList(other, entity, TriggerType_Teleport); +} + +public void OnTeleportTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + DecrementTriggerTouchCount(other, entity); + + char key[32]; + GetEntityHammerIDString(entity, key, sizeof(key)); + TeleportTrigger trigger; + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + bhopTouchCount[other]--; + } + + RemoveTriggerFromTouchList(other, entity); +} + +public void OnBhopResetTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + ResetBhopState(other); +} + +public void OnAntiCpTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiCpTriggerTouchCount[other]++; +} + +public void OnAntiCpTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiCpTriggerTouchCount[other]--; +} + +public void OnAntiPauseTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiPauseTriggerTouchCount[other]++; +} + +public void OnAntiPauseTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiPauseTriggerTouchCount[other]--; +} + +public void OnAntiJumpstatTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiJumpstatTriggerTouchCount[other]++; +} + +public void OnAntiJumpstatTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiJumpstatTriggerTouchCount[other]--; +} + +public void OnTrigMultTouchStart_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + lastTrigMultiTouchTime[other] = GetEngineTime(); + triggerTouchCount[other]++; +} + +public void OnTrigMultTouchEnd_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + triggerTouchCount[other]--; +} + +public void OnTrigTeleTouchStart_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + lastTrigTeleTouchTime[other] = GetEngineTime(); + triggerTouchCount[other]++; +} + +public void OnTrigTeleTouchEnd_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + triggerTouchCount[other]--; +} + +void OnStartTouchGround_MapTriggers(int client) +{ + lastTouchGroundOrLadderTime[client] = GetEngineTime(); + + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + // set the touched tick to the tick that the player touches the ground. + touched.groundTouchTick = gI_TickCount[client]; + triggerTouchList[client].SetArray(i, touched); + } +} + +void OnStopTouchGround_MapTriggers(int client) +{ + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + + if (touched.triggerType == TriggerType_Teleport) + { + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + TeleportTrigger trigger; + // set last touched triggers for single and sequential bhop. + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + if (trigger.type == TeleportType_SequentialBhop) + { + lastTouchSequentialBhopEntRefs[client].Push(touched.entRef); + } + // NOTE: For singlebhops, we don't care which type of bhop we last touched, because + // otherwise jumping back and forth between a multibhop and a singlebhop wouldn't work. + if (i == 0 && IsBhopTrigger(trigger.type)) + { + // We only want to set this once in this loop. + lastTouchSingleBhopEntRef[client] = touched.entRef; + } + } + } + } +} + +void OnChangeMovetype_MapTriggers(int client, MoveType newMovetype) +{ + if (newMovetype == MOVETYPE_LADDER) + { + lastTouchGroundOrLadderTime[client] = GetEngineTime(); + } +} + + + +// =====[ PRIVATE ]===== + +static void AddTriggerToTouchList(int client, int trigger, TriggerType triggerType) +{ + int triggerEntRef = EntIndexToEntRef(trigger); + + TouchedTrigger touched; + touched.triggerType = triggerType; + touched.entRef = triggerEntRef; + touched.startTouchTick = gI_TickCount[client]; + touched.groundTouchTick = -1; + if (GetEntityFlags(client) & FL_ONGROUND) + { + touched.groundTouchTick = gI_TickCount[client]; + } + + triggerTouchList[client].PushArray(touched); +} + +static void RemoveTriggerFromTouchList(int client, int trigger) +{ + int triggerEntRef = EntIndexToEntRef(trigger); + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + if (touched.entRef == triggerEntRef) + { + triggerTouchList[client].Erase(i); + break; + } + } +} + +static int IncrementTriggerTouchCount(int client, int trigger) +{ + int entref = EntIndexToEntRef(trigger); + char szEntref[64]; + FormatEx(szEntref, sizeof(szEntref), "%i", entref); + + int value = 0; + triggerTouchCounts[client].GetValue(szEntref, value); + + value += 1; + triggerTouchCounts[client].SetValue(szEntref, value); + + return value; +} + +static void DecrementTriggerTouchCount(int client, int trigger) +{ + int entref = EntIndexToEntRef(trigger); + char szEntref[64]; + FormatEx(szEntref, sizeof(szEntref), "%i", entref); + + int value = 0; + triggerTouchCounts[client].GetValue(szEntref, value); + + value -= 1; + triggerTouchCounts[client].SetValue(szEntref, value); +} + +static void TouchAntibhopTrigger(int client, TouchedTrigger touched, int &newButtons, int flags) +{ + if (!(flags & FL_ONGROUND)) + { + // Disable jump when the player is in the air. + // This is a very simple way to fix jumpbugging antibhop triggers. + newButtons &= ~IN_JUMP; + return; + } + + if (touched.groundTouchTick == -1) + { + // The player hasn't touched the ground inside this trigger yet. + return; + } + + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + AntiBhopTrigger trigger; + if (antiBhopTriggers.GetArray(key, trigger, sizeof(trigger))) + { + float touchTime = CalculateGroundTouchTime(client, touched); + if (trigger.time == 0.0 || touchTime <= trigger.time) + { + // disable jump + newButtons &= ~IN_JUMP; + } + } +} + +static bool TouchTeleportTrigger(int client, TouchedTrigger touched, int flags) +{ + bool shouldTeleport = false; + + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + TeleportTrigger trigger; + if (!teleportTriggers.GetArray(key, trigger, sizeof(trigger))) + { + // Couldn't get the teleport trigger from the trigger array for some reason. + return shouldTeleport; + } + + bool isBhopTrigger = IsBhopTrigger(trigger.type); + // NOTE: Player hasn't touched the ground inside this trigger yet. + if (touched.groundTouchTick == -1 && isBhopTrigger) + { + return shouldTeleport; + } + + float destOrigin[3]; + float destAngles[3]; + bool gotDestOrigin; + bool gotDestAngles; + int destinationEnt = GetTeleportDestinationAndOrientation(trigger.tpDestination, destOrigin, destAngles, gotDestOrigin, gotDestAngles); + + float triggerOrigin[3]; + bool gotTriggerOrigin = GetEntityAbsOrigin(touched.entRef, triggerOrigin); + + // NOTE: We only use the trigger's origin if we're using a relative destination, so if + // we're not using a relative destination and don't have it, then it's fine. + if (!IsValidEntity(destinationEnt) || !gotDestOrigin + || (!gotTriggerOrigin && trigger.relativeDestination)) + { + PrintToConsole(client, "[KZ] Invalid teleport destination \"%s\" on trigger with hammerID %i.", trigger.tpDestination, trigger.hammerID); + return shouldTeleport; + } + + // NOTE: Find out if we should actually teleport. + if (isBhopTrigger && (flags & FL_ONGROUND)) + { + float touchTime = CalculateGroundTouchTime(client, touched); + if (touchTime > trigger.delay) + { + shouldTeleport = true; + } + else if (trigger.type == TeleportType_SingleBhop) + { + shouldTeleport = lastTouchSingleBhopEntRef[client] == touched.entRef; + } + else if (trigger.type == TeleportType_SequentialBhop) + { + int length = lastTouchSequentialBhopEntRefs[client].Length; + for (int j = 0; j < length; j++) + { + int entRef = lastTouchSequentialBhopEntRefs[client].Get(j); + if (entRef == touched.entRef) + { + shouldTeleport = true; + break; + } + } + } + } + else if (trigger.type == TeleportType_Normal) + { + float touchTime = CalculateStartTouchTime(client, touched); + shouldTeleport = touchTime > trigger.delay || (trigger.delay == 0.0); + } + + if (!shouldTeleport) + { + return shouldTeleport; + } + + bool shouldReorientPlayer = trigger.reorientPlayer + && gotDestAngles && (destAngles[1] != 0.0); + + float zAxis[3]; + zAxis = view_as<float>({0.0, 0.0, 1.0}); + + // NOTE: Work out finalOrigin. + float finalOrigin[3]; + if (trigger.relativeDestination) + { + float playerOrigin[3]; + Movement_GetOrigin(client, playerOrigin); + + float playerOffsetFromTrigger[3]; + SubtractVectors(playerOrigin, triggerOrigin, playerOffsetFromTrigger); + + if (shouldReorientPlayer) + { + // NOTE: rotate player offset by the destination trigger's yaw. + RotateVectorAxis(playerOffsetFromTrigger, zAxis, DegToRad(destAngles[1]), playerOffsetFromTrigger); + } + + AddVectors(destOrigin, playerOffsetFromTrigger, finalOrigin); + } + else + { + finalOrigin = destOrigin; + } + + // NOTE: Work out finalPlayerAngles. + float finalPlayerAngles[3]; + Movement_GetEyeAngles(client, finalPlayerAngles); + if (shouldReorientPlayer) + { + finalPlayerAngles[1] -= destAngles[1]; + + float velocity[3]; + Movement_GetVelocity(client, velocity); + + // NOTE: rotate velocity by the destination trigger's yaw. + RotateVectorAxis(velocity, zAxis, DegToRad(destAngles[1]), velocity); + Movement_SetVelocity(client, velocity); + } + else if (!trigger.reorientPlayer && trigger.useDestAngles) + { + finalPlayerAngles = destAngles; + } + + if (shouldTeleport) + { + TeleportPlayer(client, finalOrigin, finalPlayerAngles, gotDestAngles && trigger.useDestAngles, trigger.resetSpeed); + } + + return shouldTeleport; +} + +static float CalculateGroundTouchTime(int client, TouchedTrigger touched) +{ + float result = float(gI_TickCount[client] - touched.groundTouchTick) * GetTickInterval(); + return result; +} + +static float CalculateStartTouchTime(int client, TouchedTrigger touched) +{ + float result = float(gI_TickCount[client] - touched.startTouchTick) * GetTickInterval(); + return result; +} + +static void ResetBhopState(int client) +{ + lastTouchSingleBhopEntRef[client] = INVALID_ENT_REFERENCE; + lastTouchSequentialBhopEntRefs[client].Clear(); +} + +static bool GetEntityHammerIDString(int entity, char[] buffer, int maxLength) +{ + if (!IsValidEntity(entity)) + { + return false; + } + + if (!HasEntProp(entity, Prop_Data, "m_iHammerID")) + { + return false; + } + + int hammerID = GetEntProp(entity, Prop_Data, "m_iHammerID"); + IntToString(hammerID, buffer, maxLength); + + return true; +} + +// NOTE: returns an entity reference (possibly invalid). +static int GetTeleportDestinationAndOrientation(char[] targetName, float origin[3], float angles[3] = NULL_VECTOR, bool &gotOrigin = false, bool &gotAngles = false) +{ + // NOTE: We're not caching the teleport destination because it could change. + int destination = GOKZFindEntityByName(targetName, .ignorePlayers = true); + if (!IsValidEntity(destination)) + { + return destination; + } + + gotOrigin = GetEntityAbsOrigin(destination, origin); + + if (HasEntProp(destination, Prop_Data, "m_angAbsRotation")) + { + GetEntPropVector(destination, Prop_Data, "m_angAbsRotation", angles); + gotAngles = true; + } + else + { + gotAngles = false; + } + + return destination; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/zones.sp b/sourcemod/scripting/gokz-core/map/zones.sp new file mode 100644 index 0000000..684e42d --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/zones.sp @@ -0,0 +1,183 @@ +/* + Hooks between specifically named trigger_multiples and GOKZ. +*/ + + + +static Regex RE_BonusStartZone; +static Regex RE_BonusEndZone; +static bool touchedGroundSinceTouchingStartZone[MAXPLAYERS + 1]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapZones() +{ + RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX); + RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX); +} + +void OnStartTouchGround_MapZones(int client) +{ + touchedGroundSinceTouchingStartZone[client] = true; +} + +void OnEntitySpawned_MapZones(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("trigger_multiple", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + int course = 0; + if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnStartZoneStartTouch); + HookSingleEntityOutput(entity, "OnEndTouch", OnStartZoneEndTouch); + RegisterCourseStart(course); + } + else if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnEndZoneStartTouch); + RegisterCourseEnd(course); + } + else if ((course = GetStartZoneBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBonusStartZoneStartTouch); + HookSingleEntityOutput(entity, "OnEndTouch", OnBonusStartZoneEndTouch); + RegisterCourseStart(course); + } + else if ((course = GetEndZoneBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBonusEndZoneStartTouch); + RegisterCourseEnd(course); + } +} + +public void OnStartZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartZoneStartTouch(activator, 0); +} + +public void OnStartZoneEndTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartZoneEndTouch(activator, 0); +} + +public void OnEndZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessEndZoneStartTouch(activator, 0); +} + +public void OnBonusStartZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartZoneStartTouch(activator, course); +} + +public void OnBonusStartZoneEndTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartZoneEndTouch(activator, course); +} + +public void OnBonusEndZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetEndZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessEndZoneStartTouch(activator, course); +} + + + +// =====[ PRIVATE ]===== + +static void ProcessStartZoneStartTouch(int client, int course) +{ + touchedGroundSinceTouchingStartZone[client] = Movement_GetOnGround(client); + + GOKZ_StopTimer(client, false); + SetCurrentCourse(client, course); + + OnStartZoneStartTouch_Teleports(client, course); +} + +static void ProcessStartZoneEndTouch(int client, int course) +{ + if (!touchedGroundSinceTouchingStartZone[client]) + { + return; + } + + GOKZ_StartTimer(client, course, true); + GOKZ_ResetVirtualButtonPosition(client, true); +} + +static void ProcessEndZoneStartTouch(int client, int course) +{ + GOKZ_EndTimer(client, course); + GOKZ_ResetVirtualButtonPosition(client, false); +} + +static int GetStartZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1); +} + +static int GetEndZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/menus/mode_menu.sp b/sourcemod/scripting/gokz-core/menus/mode_menu.sp new file mode 100644 index 0000000..934d29c --- /dev/null +++ b/sourcemod/scripting/gokz-core/menus/mode_menu.sp @@ -0,0 +1,40 @@ +/* + Lets players choose their mode. +*/ + + + +// =====[ PUBLIC ]===== + +void DisplayModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_Mode); + menu.SetTitle("%T", "Mode Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, true); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ EVENTS ]===== + +public int MenuHandler_Mode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + GOKZ_SetCoreOption(param1, Option_Mode, param2); + if (GetCameFromOptionsMenu(param1)) + { + DisplayOptionsMenu(param1, TopMenuPosition_LastCategory); + } + } + else if (action == MenuAction_Cancel && GetCameFromOptionsMenu(param1)) + { + DisplayOptionsMenu(param1, TopMenuPosition_LastCategory); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/menus/options_menu.sp b/sourcemod/scripting/gokz-core/menus/options_menu.sp new file mode 100644 index 0000000..240ee81 --- /dev/null +++ b/sourcemod/scripting/gokz-core/menus/options_menu.sp @@ -0,0 +1,174 @@ +/* + TopMenu that allows users to browse categories of options. + + Adds core options to the general category where players + can cycle the value of each core option. +*/ + + + +static TopMenu optionsMenu; +static TopMenuObject catGeneral; +static TopMenuObject itemsGeneral[OPTION_COUNT]; +static bool cameFromOptionsMenu[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void DisplayOptionsMenu(int client, TopMenuPosition position = TopMenuPosition_Start) +{ + optionsMenu.Display(client, position); + cameFromOptionsMenu[client] = false; +} + +TopMenu GetOptionsTopMenu() +{ + return optionsMenu; +} + +bool GetCameFromOptionsMenu(int client) +{ + return cameFromOptionsMenu[client]; +} + + + +// =====[ LISTENERS ]===== + +void OnAllPluginsLoaded_OptionsMenu() +{ + optionsMenu = new TopMenu(TopMenuHandler_Options); + Call_GOKZ_OnOptionsMenuCreated(optionsMenu); + Call_GOKZ_OnOptionsMenuReady(optionsMenu); +} + +void OnConfigsExecuted_OptionsMenu() +{ + SortOptionsMenu(); +} + +void OnOptionsMenuCreated_OptionsMenu() +{ + catGeneral = optionsMenu.AddCategory(GENERAL_OPTION_CATEGORY, TopMenuHandler_Options); +} + +void OnOptionsMenuReady_OptionsMenu() +{ + for (int option = 0; option < view_as<int>(OPTION_COUNT); option++) + { + if (option == view_as<int>(Option_Style)) + { + continue; // TODO Currently hard-coded to skip style + } + itemsGeneral[option] = optionsMenu.AddItem(gC_CoreOptionNames[option], TopMenuHandler_General, catGeneral); + } +} + + + +// =====[ HANDLER ]===== + +public void TopMenuHandler_Options(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == INVALID_TOPMENUOBJECT) + { + Format(buffer, maxlength, "%T", "Options Menu - Title", param); + } + else if (topobj_id == catGeneral) + { + Format(buffer, maxlength, "%T", "Options Menu - General", param); + } + } +} + +public void TopMenuHandler_General(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + Option option = OPTION_INVALID; + for (int i = 0; i < view_as<int>(OPTION_COUNT); i++) + { + if (topobj_id == itemsGeneral[i]) + { + option = view_as<Option>(i); + break; + } + } + + if (option == OPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case Option_Mode: + { + FormatEx(buffer, maxlength, "%T - %s", + gC_CoreOptionPhrases[option], param, + gC_ModeNames[GOKZ_GetCoreOption(param, option)]); + } + case Option_TimerButtonZoneType: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_CoreOptionPhrases[option], param, + gC_TimerButtonZoneTypePhrases[GOKZ_GetCoreOption(param, option)], param); + } + case Option_Safeguard: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_CoreOptionPhrases[option], param, + gC_SafeGuardPhrases[GOKZ_GetCoreOption(param, option)], param); + } + default:FormatToggleableOptionDisplay(param, option, buffer, maxlength); + } + } + else if (action == TopMenuAction_SelectOption) + { + switch (option) + { + case Option_Mode: + { + cameFromOptionsMenu[param] = true; + DisplayModeMenu(param); + } + default: + { + GOKZ_CycleCoreOption(param, option); + optionsMenu.Display(param, TopMenuPosition_LastCategory); + } + } + } +} + + + +// =====[ PRIVATE ]===== + +static void SortOptionsMenu() +{ + char error[256]; + if (!optionsMenu.LoadConfig(GOKZ_CFG_OPTIONS_SORTING, error, sizeof(error))) + { + LogError("Failed to load file: \"%s\". Error: %s", GOKZ_CFG_OPTIONS_SORTING, error); + } +} + +static void FormatToggleableOptionDisplay(int client, Option option, char[] buffer, int maxlength) +{ + if (GOKZ_GetCoreOption(client, option) == 0) + { + FormatEx(buffer, maxlength, "%T - %T", + gC_CoreOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gC_CoreOptionPhrases[option], client, + "Options Menu - Enabled", client); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/misc.sp b/sourcemod/scripting/gokz-core/misc.sp new file mode 100644 index 0000000..a117880 --- /dev/null +++ b/sourcemod/scripting/gokz-core/misc.sp @@ -0,0 +1,803 @@ +/* + Small features that aren't worth splitting into their own file. +*/ + + + +// =====[ GOKZ.CFG ]===== + +void OnMapStart_KZConfig() +{ + char gokzCfgFullPath[PLATFORM_MAX_PATH]; + FormatEx(gokzCfgFullPath, sizeof(gokzCfgFullPath), "cfg/%s", GOKZ_CFG_SERVER); + + if (FileExists(gokzCfgFullPath)) + { + ServerCommand("exec %s", GOKZ_CFG_SERVER); + } + else + { + SetFailState("Failed to load file: \"%s\". Check that it exists.", gokzCfgFullPath); + } +} + + + +// =====[ GODMODE ]===== + +void OnPlayerSpawn_GodMode(int client) +{ + // Stop players from taking damage + SetEntProp(client, Prop_Data, "m_takedamage", 0); + SetEntityFlags(client, GetEntityFlags(client) | FL_GODMODE); +} + + + +// =====[ NOCLIP ]===== + +int noclipReleaseTime[MAXPLAYERS + 1]; + +void ToggleNoclip(int client) +{ + if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP) + { + EnableNoclip(client); + } + else + { + DisableNoclip(client); + } +} + +void EnableNoclip(int client) +{ + if (IsValidClient(client) && IsPlayerAlive(client)) + { + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return; + } + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + GOKZ_StopTimer(client); + } +} + +void DisableNoclip(int client) +{ + if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) + { + noclipReleaseTime[client] = GetGameTickCount(); + Movement_SetMovetype(client, MOVETYPE_WALK); + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + + // Prevents an exploit that would let you noclip out of start zones + RemoveNoclipGroundFlag(client); + } +} + +void ToggleNoclipNotrigger(int client) +{ + if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP) + { + EnableNoclipNotrigger(client); + } + else + { + DisableNoclipNotrigger(client); + } +} + +void EnableNoclipNotrigger(int client) +{ + if (IsValidClient(client) && IsPlayerAlive(client)) + { + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return; + } + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER); + GOKZ_StopTimer(client); + } +} + +void DisableNoclipNotrigger(int client) +{ + if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) + { + noclipReleaseTime[client] = GetGameTickCount(); + Movement_SetMovetype(client, MOVETYPE_WALK); + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + + // Prevents an exploit that would let you noclip out of start zones + RemoveNoclipGroundFlag(client); + } +} + +void RemoveNoclipGroundFlag(int client) +{ + float startPosition[3], endPosition[3]; + GetClientAbsOrigin(client, startPosition); + endPosition = startPosition; + endPosition[2] = startPosition[2] - 2.0; + Handle trace = TR_TraceHullFilterEx( + startPosition, + endPosition, + view_as<float>( { -16.0, -16.0, 0.0 } ), + view_as<float>( { 16.0, 16.0, 72.0 } ), + MASK_PLAYERSOLID, + TraceEntityFilterPlayers, + client); + + if (!TR_DidHit(trace)) + { + SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND); + } + delete trace; +} + +bool JustNoclipped(int client) +{ + return GetGameTickCount() - noclipReleaseTime[client] <= GOKZ_TIMER_START_NOCLIP_TICKS; +} + +void OnClientPutInServer_Noclip(int client) +{ + noclipReleaseTime[client] = 0; +} + +// =====[ TURNBINDS ]===== + +static int turnbindsLastLeftStart[MAXPLAYERS + 1]; +static int turnbindsLastRightStart[MAXPLAYERS + 1]; +static float turnbindsLastValidYaw[MAXPLAYERS + 1]; +static int turnbindsOldButtons[MAXPLAYERS + 1]; + +void OnClientPutInServer_Turnbinds(int client) +{ + turnbindsLastLeftStart[client] = 0; + turnbindsLastRightStart[client] = 0; +} +// Ensures that there is a minimum time between starting to turnbind in one direction +// and then starting to turnbind in the other direction +void OnPlayerRunCmd_Turnbinds(int client, int buttons, int tickcount, float angles[3]) +{ + if (IsFakeClient(client)) + { + return; + } + if (buttons & IN_LEFT && tickcount < turnbindsLastRightStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval())) + { + angles[1] = turnbindsLastValidYaw[client]; + TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR); + buttons = 0; + } + else if (buttons & IN_RIGHT && tickcount < turnbindsLastLeftStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval())) + { + angles[1] = turnbindsLastValidYaw[client]; + TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR); + buttons = 0; + } + else + { + turnbindsLastValidYaw[client] = angles[1]; + + if (!(turnbindsOldButtons[client] & IN_LEFT) && (buttons & IN_LEFT)) + { + turnbindsLastLeftStart[client] = tickcount; + } + + if (!(turnbindsOldButtons[client] & IN_RIGHT) && (buttons & IN_RIGHT)) + { + turnbindsLastRightStart[client] = tickcount; + } + + turnbindsOldButtons[client] = buttons; + } +} + + + +// =====[ PLAYER COLLISION ]===== + +void OnPlayerSpawn_PlayerCollision(int client) +{ + // Let players go through other players + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); +} + +void OnSetModel_PlayerCollision(int client) +{ + // Fix custom models temporarily changing player collisions + SetEntPropVector(client, Prop_Data, "m_vecMins", PLAYER_MINS); + if (GetEntityFlags(client) & FL_DUCKING == 0) + { + SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS); + } + else + { + SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS_DUCKED); + } +} + + +// =====[ FORCE SV_FULL_ALLTALK 1 ]===== + +void OnRoundStart_ForceAllTalk() +{ + gCV_sv_full_alltalk.BoolValue = true; +} + + + +// =====[ ERROR SOUNDS ]===== + +#define SOUND_ERROR "buttons/button10.wav" + +void PlayErrorSound(int client) +{ + if (GOKZ_GetCoreOption(client, Option_ErrorSounds) == ErrorSounds_Enabled) + { + GOKZ_EmitSoundToClient(client, SOUND_ERROR, _, "Error"); + } +} + + + +// =====[ STOP SOUNDS ]===== + +Action OnNormalSound_StopSounds(int entity) +{ + char className[20]; + GetEntityClassname(entity, className, sizeof(className)); + if (StrEqual(className, "func_button", false)) + { + return Plugin_Handled; // No sounds directly from func_button + } + return Plugin_Continue; +} + + + +// =====[ JOIN TEAM HANDLING ]===== + +static bool hasSavedPosition[MAXPLAYERS + 1]; +static float savedOrigin[MAXPLAYERS + 1][3]; +static float savedAngles[MAXPLAYERS + 1][3]; +static bool savedOnLadder[MAXPLAYERS + 1]; + +void OnClientPutInServer_JoinTeam(int client) +{ + // Automatically put the player on a team if he doesn't choose one. + // The mp_force_pick_time convar is the built in way to do this, but that obviously + // does not call GOKZ_JoinTeam which includes a fix for spawning in the void when + // there is no valid spawns available. + CreateTimer(12.0, Timer_ForceJoinTeam, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + hasSavedPosition[client] = false; +} + +public Action Timer_ForceJoinTeam(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client)) + { + int team = GetClientTeam(client); + if (team == CS_TEAM_NONE) + { + GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR, false); + } + } + return Plugin_Stop; +} + +void OnTimerStart_JoinTeam(int client) +{ + hasSavedPosition[client] = false; +} + +void JoinTeam(int client, int newTeam, bool restorePos, bool forceBroadcast = false) +{ + KZPlayer player = KZPlayer(client); + int currentTeam = GetClientTeam(client); + + // Don't use CS_TEAM_NONE + if (newTeam == CS_TEAM_NONE) + { + newTeam = CS_TEAM_SPECTATOR; + } + + if (newTeam == CS_TEAM_SPECTATOR && currentTeam != CS_TEAM_SPECTATOR) + { + if (currentTeam != CS_TEAM_NONE) + { + player.GetOrigin(savedOrigin[client]); + player.GetEyeAngles(savedAngles[client]); + savedOnLadder[client] = player.Movetype == MOVETYPE_LADDER; + hasSavedPosition[client] = true; + } + + if (!player.Paused && !player.CanPause) + { + player.StopTimer(); + } + ChangeClientTeam(client, CS_TEAM_SPECTATOR); + Call_GOKZ_OnJoinTeam(client, newTeam); + } + else if (newTeam == CS_TEAM_CT && currentTeam != CS_TEAM_CT + || newTeam == CS_TEAM_T && currentTeam != CS_TEAM_T) + { + ForcePlayerSuicide(client); + CS_SwitchTeam(client, newTeam); + CS_RespawnPlayer(client); + if (restorePos && hasSavedPosition[client]) + { + TeleportPlayer(client, savedOrigin[client], savedAngles[client]); + if (savedOnLadder[client]) + { + player.Movetype = MOVETYPE_LADDER; + } + } + else + { + player.StopTimer(); + // Just joining a team alone can put you into weird invalid spawns. + // Need to teleport the player to a valid one. + float spawnOrigin[3]; + float spawnAngles[3]; + GetValidSpawn(spawnOrigin, spawnAngles); + TeleportPlayer(client, spawnOrigin, spawnAngles); + } + hasSavedPosition[client] = false; + Call_GOKZ_OnJoinTeam(client, newTeam); + } + else if (forceBroadcast) + { + Call_GOKZ_OnJoinTeam(client, newTeam); + } +} + +void SendFakeTeamEvent(int client) +{ + // Send a fake event to close the team menu + Event event = CreateEvent("player_team"); + event.SetInt("userid", GetClientUserId(client)); + event.FireToClient(client); + event.Cancel(); +} + +// =====[ VALID JUMP TRACKING ]===== + +/* + Valid jump tracking is intended to detect when the player + has performed a normal jump that hasn't been affected by + (unexpected) teleports or other cases that may result in + the player becoming airborne, such as spawning. + + There are ways to trick the plugin, but it is rather + unlikely to happen during normal gameplay. +*/ + +static bool validJump[MAXPLAYERS + 1]; +static float validJumpTeleportOrigin[MAXPLAYERS + 1][3]; +static int lastInvalidatedTick[MAXPLAYERS + 1]; +bool GetValidJump(int client) +{ + return validJump[client]; +} + +static void InvalidateJump(int client) +{ + lastInvalidatedTick[client] = GetGameTickCount(); + if (validJump[client]) + { + validJump[client] = false; + Call_GOKZ_OnJumpInvalidated(client); + } +} + +void OnStopTouchGround_ValidJump(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + if (Movement_GetMovetype(client) == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount()) + { + validJump[client] = true; + Call_GOKZ_OnJumpValidated(client, jumped, ladderJump, jumpbug); + } + else + { + InvalidateJump(client); + } +} + +void OnPlayerRunCmdPost_ValidJump(int client) +{ + if (gB_VelocityTeleported[client] || gB_OriginTeleported[client]) + { + InvalidateJump(client); + } +} + +void OnChangeMovetype_ValidJump(int client, MoveType oldMovetype, MoveType newMovetype) +{ + if (oldMovetype == MOVETYPE_LADDER && newMovetype == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount()) // Ladderjump + { + validJump[client] = true; + Call_GOKZ_OnJumpValidated(client, false, true, false); + } + else + { + InvalidateJump(client); + } +} + +void OnClientDisconnect_ValidJump(int client) +{ + InvalidateJump(client); +} + +void OnPlayerSpawn_ValidJump(int client) +{ + // That should definitely be out of bounds + CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]); + InvalidateJump(client); +} + +void OnPlayerDeath_ValidJump(int client) +{ + InvalidateJump(client); +} + +void OnValidOriginChange_ValidJump(int client, const float origin[3]) +{ + CopyVector(origin, validJumpTeleportOrigin[client]); +} + +void OnTeleport_ValidJump(int client) +{ + float origin[3]; + Movement_GetOrigin(client, origin); + if (gB_OriginTeleported[client] && GetVectorDistance(validJumpTeleportOrigin[client], origin, true) <= EPSILON) + { + gB_OriginTeleported[client] = false; + CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]); + return; + } + + if (gB_OriginTeleported[client]) + { + InvalidateJump(client); + Call_GOKZ_OnTeleport(client); + } + + if (gB_VelocityTeleported[client]) + { + InvalidateJump(client); + } +} + + + +// =====[ FIRST SPAWN ]===== + +static bool hasSpawned[MAXPLAYERS + 1]; + +void OnClientPutInServer_FirstSpawn(int client) +{ + hasSpawned[client] = false; +} + +void OnPlayerSpawn_FirstSpawn(int client) +{ + int team = GetClientTeam(client); + if (!hasSpawned[client] && (team == CS_TEAM_CT || team == CS_TEAM_T)) + { + hasSpawned[client] = true; + Call_GOKZ_OnFirstSpawn(client); + } +} + + + +// =====[ TIME LIMIT ]===== + +void OnConfigsExecuted_TimeLimit() +{ + CreateTimer(1.0, Timer_TimeLimit, _, TIMER_REPEAT); +} + +public Action Timer_TimeLimit(Handle timer) +{ + int timelimit; + if (!GetMapTimeLimit(timelimit) || timelimit == 0) + { + return Plugin_Continue; + } + + int timeleft; + // Check for less than -1 in case we miss 0 - ignore -1 because that means infinite time limit + if (GetMapTimeLeft(timeleft) && (timeleft == 0 || timeleft < -1)) + { + CreateTimer(5.0, Timer_EndRound); // End the round after a delay or it won't end the map + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action Timer_EndRound(Handle timer) +{ + CS_TerminateRound(1.0, CSRoundEnd_Draw, true); + return Plugin_Continue; +} + + + +// =====[ COURSE REGISTER ]===== + +static bool startRegistered[GOKZ_MAX_COURSES]; +static bool endRegistered[GOKZ_MAX_COURSES]; +static bool courseRegistered[GOKZ_MAX_COURSES]; + +bool GetCourseRegistered(int course) +{ + return courseRegistered[course]; +} + +void RegisterCourseStart(int course) +{ + startRegistered[course] = true; + TryRegisterCourse(course); +} + +void RegisterCourseEnd(int course) +{ + endRegistered[course] = true; + TryRegisterCourse(course); +} + +void OnMapStart_CourseRegister() +{ + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + courseRegistered[course] = false; + } +} + +static void TryRegisterCourse(int course) +{ + if (!courseRegistered[course] && startRegistered[course] && endRegistered[course]) + { + courseRegistered[course] = true; + Call_GOKZ_OnCourseRegistered(course); + } +} + + + +// =====[ SPAWN FIXES ]===== + +void OnMapStart_FixMissingSpawns() +{ + int tSpawn = FindEntityByClassname(-1, "info_player_terrorist"); + int ctSpawn = FindEntityByClassname(-1, "info_player_counterterrorist"); + + if (tSpawn == -1 && ctSpawn == -1) + { + LogMessage("Couldn't fix spawns because none exist."); + return; + } + + if (tSpawn == -1 || ctSpawn == -1) + { + float origin[3], angles[3]; + GetValidSpawn(origin, angles); + + int newSpawn = CreateEntityByName((tSpawn == -1) ? "info_player_terrorist" : "info_player_counterterrorist"); + if (DispatchSpawn(newSpawn)) + { + TeleportEntity(newSpawn, origin, angles, NULL_VECTOR); + } + } +} + +// =====[ BUTTONS ]===== + +void OnClientPreThinkPost_UseButtons(int client) +{ + if (GOKZ_GetCoreOption(client, Option_ButtonThroughPlayers) == ButtonThroughPlayers_Enabled && GetEntProp(client, Prop_Data, "m_afButtonPressed") & IN_USE) + { + int entity = FindUseEntity(client); + if (entity != -1) + { + AcceptEntityInput(entity, "Use", client, client, 1); + } + } +} + +static int FindUseEntity(int client) +{ + float fwd[3]; + float angles[3]; + GetClientEyeAngles(client, angles); + GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR); + + Handle trace; + + float eyeOrigin[3]; + GetClientEyePosition(client, eyeOrigin); + int useableContents = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE; + + float endpos[3]; + + // Check if +use trace collide with a player first, so we don't activate any button twice + trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TRFOtherPlayersOnly, client); + if (TR_DidHit(trace)) + { + int ent = TR_GetEntityIndex(trace); + if (ent < 1 || ent > MaxClients) + { + return -1; + } + // Search for a button behind it. + trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + char buffer[20]; + ent = TR_GetEntityIndex(trace); + // Make sure that it is a button, and this button activates when pressed. + // If it is not a button, check its parent to see if it is a button. + bool isButton; + while (ent != -1) + { + GetEntityClassname(ent, buffer, sizeof(buffer)); + if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES) + { + isButton = true; + break; + } + else + { + ent = GetEntPropEnt(ent, Prop_Data, "m_hMoveParent"); + } + } + if (isButton) + { + TR_GetEndPosition(endpos, trace); + float delta[3]; + for (int i = 0; i < 2; i++) + { + delta[i] = endpos[i] - eyeOrigin[i]; + } + // Z distance is treated differently. + float m_vecMins[3]; + float m_vecMaxs[3]; + float m_vecOrigin[3]; + GetEntPropVector(ent, Prop_Send, "m_vecOrigin", m_vecOrigin); + GetEntPropVector(ent, Prop_Send, "m_vecMins", m_vecMins); + GetEntPropVector(ent, Prop_Send, "m_vecMaxs", m_vecMaxs); + + delta[2] = IntervalDistance(endpos[2], m_vecOrigin[2] + m_vecMins[2], m_vecOrigin[2] + m_vecMaxs[2]); + if (GetVectorLength(delta) < 80.0) + { + return ent; + } + } + } + } + + int nearestEntity; + float nearestPoint[3]; + float nearestDist = FLOAT_MAX; + ArrayList entities = new ArrayList(); + TR_EnumerateEntitiesSphere(eyeOrigin, 80.0, 1<<5, AddEntities, entities); + for (int i = 0; i < entities.Length; i++) + { + char buffer[64]; + int ent = entities.Get(i); + GetEntityClassname(ent, buffer, sizeof(buffer)); + // Check if the entity is a button and it is pressable. + if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES) + { + float point[3]; + CalcNearestPoint(ent, eyeOrigin, point); + + float dir[3]; + for (int j = 0; j < 3; j++) + { + dir[j] = point[j] - eyeOrigin[2]; + } + // Check the maximum angle the player can be away from the button. + float minimumDot = GetEntPropFloat(ent, Prop_Send, "m_flUseLookAtAngle"); + NormalizeVector(dir, dir); + float dot = GetVectorDotProduct(dir, fwd); + if (dot < minimumDot) + { + continue; + } + + float dist = CalcDistanceToLine(point, eyeOrigin, fwd); + if (dist < nearestDist) + { + trace = TR_TraceRayFilterEx(eyeOrigin, point, useableContents, RayType_EndPoint, TraceEntityFilterPlayers); + if (TR_GetFraction(trace) == 1.0 || TR_GetEntityIndex(trace) == ent) + { + CopyVector(point, nearestPoint); + nearestDist = dist; + nearestEntity = ent; + } + } + } + } + // We found the closest button, but we still need to check if there is a player in front of it or not. + // In the case that there isn't a player inbetween, we don't return the entity index, because that button will be pressed by the game function anyway. + // If there is, we will press two buttons at once, the "right" button found by this function and the "wrong" button that we only happen to press because + // there is a player in the way. + + trace = TR_TraceRayFilterEx(eyeOrigin, nearestPoint, useableContents, RayType_EndPoint, TRFOtherPlayersOnly); + if (TR_DidHit(trace)) + { + return nearestEntity; + } + return -1; +} + +public bool AddEntities(int entity, ArrayList entities) +{ + entities.Push(entity); + return true; +} + +static float IntervalDistance(float x, float x0, float x1) +{ + if (x0 > x1) + { + float tmp = x0; + x0 = x1; + x1 = tmp; + } + if (x < x0) + { + return x0 - x; + } + else if (x > x1) + { + return x - x1; + } + return 0.0; +} +// TraceRay filter for other players exclusively. +public bool TRFOtherPlayersOnly(int entity, int contentmask, int client) +{ + return (0 < entity <= MaxClients) && (entity != client); +} + +// =====[ SAFE MODE ]===== + +void ToggleNubSafeGuard(int client) +{ + if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledNUB) + { + GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled); + } + else + { + GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledNUB); + } +} + +void ToggleProSafeGuard(int client) +{ + if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO) + { + GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled); + } + else + { + GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledPRO); + } +} diff --git a/sourcemod/scripting/gokz-core/modes.sp b/sourcemod/scripting/gokz-core/modes.sp new file mode 100644 index 0000000..ce6f8ae --- /dev/null +++ b/sourcemod/scripting/gokz-core/modes.sp @@ -0,0 +1,106 @@ +static bool modeLoaded[MODE_COUNT]; +static int modeVersion[MODE_COUNT]; +static bool GOKZHitPerf[MAXPLAYERS + 1]; +static float GOKZTakeoffSpeed[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +bool GetModeLoaded(int mode) +{ + return modeLoaded[mode]; +} + +int GetModeVersion(int mode) +{ + return modeLoaded[mode] ? modeVersion[mode] : -1; +} + +void SetModeLoaded(int mode, bool loaded, int version = -1) +{ + if (!modeLoaded[mode] && loaded) + { + modeLoaded[mode] = true; + modeVersion[mode] = version; + Call_GOKZ_OnModeLoaded(mode); + } + else if (modeLoaded[mode] && !loaded) + { + modeLoaded[mode] = false; + Call_GOKZ_OnModeUnloaded(mode); + } +} + +int GetLoadedModeCount() +{ + int count = 0; + for (int mode = 0; mode < MODE_COUNT; mode++) + { + if (modeLoaded[mode]) + { + count++; + } + } + return count; +} + +int GetALoadedMode() +{ + for (int mode = 0; mode < MODE_COUNT; mode++) + { + if (GOKZ_GetModeLoaded(mode)) + { + return mode; + } + } + return -1; // Uh-oh +} + +bool GetGOKZHitPerf(int client) +{ + return GOKZHitPerf[client]; +} + +void SetGOKZHitPerf(int client, bool hitPerf) +{ + GOKZHitPerf[client] = hitPerf; +} + +float GetGOKZTakeoffSpeed(int client) +{ + return GOKZTakeoffSpeed[client]; +} + +void SetGOKZTakeoffSpeed(int client, float takeoffSpeed) +{ + GOKZTakeoffSpeed[client] = takeoffSpeed; +} + + + +// =====[ EVENTS ]===== + +void OnAllPluginsLoaded_Modes() +{ + if (GetLoadedModeCount() <= 0) + { + SetFailState("At least one GOKZ mode plugin is required."); + } +} + +void OnPlayerSpawn_Modes(int client) +{ + GOKZHitPerf[client] = false; + GOKZTakeoffSpeed[client] = 0.0; +} + +void OnOptionChanged_Mode(int client, Option option) +{ + if (option == Option_Mode) + { + // Remove speed when switching modes + Movement_SetVelocityModifier(client, 1.0); + Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/natives.sp b/sourcemod/scripting/gokz-core/natives.sp new file mode 100644 index 0000000..319810c --- /dev/null +++ b/sourcemod/scripting/gokz-core/natives.sp @@ -0,0 +1,647 @@ +void CreateNatives() +{ + CreateNative("GOKZ_GetModeLoaded", Native_GetModeLoaded); + CreateNative("GOKZ_GetModeVersion", Native_GetModeVersion); + CreateNative("GOKZ_SetModeLoaded", Native_SetModeLoaded); + CreateNative("GOKZ_GetLoadedModeCount", Native_GetLoadedModeCount); + CreateNative("GOKZ_SetMode", Native_SetMode); + CreateNative("GOKZ_PrintToChat", Native_PrintToChat); + CreateNative("GOKZ_PrintToChatAndLog", Native_PrintToChatAndLog); + CreateNative("GOKZ_GetOptionsTopMenu", Native_GetOptionsTopMenu); + CreateNative("GOKZ_GetCourseRegistered", Native_GetCourseRegistered); + + CreateNative("GOKZ_StartTimer", Native_StartTimer); + CreateNative("GOKZ_EndTimer", Native_EndTimer); + CreateNative("GOKZ_StopTimer", Native_StopTimer); + CreateNative("GOKZ_StopTimerAll", Native_StopTimerAll); + CreateNative("GOKZ_TeleportToStart", Native_TeleportToStart); + CreateNative("GOKZ_TeleportToSearchStart", Native_TeleportToSearchStart); + CreateNative("GOKZ_GetVirtualButtonPosition", Native_GetVirtualButtonPosition); + CreateNative("GOKZ_SetVirtualButtonPosition", Native_SetVirtualButtonPosition); + CreateNative("GOKZ_ResetVirtualButtonPosition", Native_ResetVirtualButtonPosition); + CreateNative("GOKZ_LockVirtualButtons", Native_LockVirtualButtons); + CreateNative("GOKZ_GetStartPosition", Native_GetStartPosition); + CreateNative("GOKZ_SetStartPosition", Native_SetStartPosition); + CreateNative("GOKZ_TeleportToEnd", Native_TeleportToEnd); + CreateNative("GOKZ_GetStartPositionType", Native_GetStartPositionType); + CreateNative("GOKZ_SetStartPositionToMapStart", Native_SetStartPositionToMapStart); + CreateNative("GOKZ_MakeCheckpoint", Native_MakeCheckpoint); + CreateNative("GOKZ_GetCanMakeCheckpoint", Native_GetCanMakeCheckpoint); + CreateNative("GOKZ_TeleportToCheckpoint", Native_TeleportToCheckpoint); + CreateNative("GOKZ_GetCanTeleportToCheckpoint", Native_GetCanTeleportToCheckpoint); + CreateNative("GOKZ_PrevCheckpoint", Native_PrevCheckpoint); + CreateNative("GOKZ_GetCanPrevCheckpoint", Native_GetCanPrevCheckpoint); + CreateNative("GOKZ_NextCheckpoint", Native_NextCheckpoint); + CreateNative("GOKZ_GetCanNextCheckpoint", Native_GetCanNextCheckpoint); + CreateNative("GOKZ_UndoTeleport", Native_UndoTeleport); + CreateNative("GOKZ_GetCanUndoTeleport", Native_GetCanUndoTeleport); + CreateNative("GOKZ_Pause", Native_Pause); + CreateNative("GOKZ_GetCanPause", Native_GetCanPause); + CreateNative("GOKZ_Resume", Native_Resume); + CreateNative("GOKZ_GetCanResume", Native_GetCanResume); + CreateNative("GOKZ_TogglePause", Native_TogglePause); + CreateNative("GOKZ_GetCanTeleportToStart", Native_GetCanTeleportToStart); + CreateNative("GOKZ_GetCanTeleportToEnd", Native_GetCanTeleportToEnd); + CreateNative("GOKZ_PlayErrorSound", Native_PlayErrorSound); + CreateNative("GOKZ_SetValidJumpOrigin", Native_SetValidJumpOrigin); + + CreateNative("GOKZ_GetTimerRunning", Native_GetTimerRunning); + CreateNative("GOKZ_GetValidTimer", Native_GetValidTimer); + CreateNative("GOKZ_GetCourse", Native_GetCourse); + CreateNative("GOKZ_SetCourse", Native_SetCourse); + CreateNative("GOKZ_GetPaused", Native_GetPaused); + CreateNative("GOKZ_GetTime", Native_GetTime); + CreateNative("GOKZ_SetTime", Native_SetTime); + CreateNative("GOKZ_InvalidateRun", Native_InvalidateRun); + CreateNative("GOKZ_GetCheckpointCount", Native_GetCheckpointCount); + CreateNative("GOKZ_SetCheckpointCount", Native_SetCheckpointCount); + CreateNative("GOKZ_GetCheckpointData", Native_GetCheckpointData); + CreateNative("GOKZ_SetCheckpointData", Native_SetCheckpointData); + CreateNative("GOKZ_GetUndoTeleportData", Native_GetUndoTeleportData); + CreateNative("GOKZ_SetUndoTeleportData", Native_SetUndoTeleportData); + CreateNative("GOKZ_GetTeleportCount", Native_GetTeleportCount); + CreateNative("GOKZ_SetTeleportCount", Native_SetTeleportCount); + CreateNative("GOKZ_RegisterOption", Native_RegisterOption); + CreateNative("GOKZ_GetOptionProp", Native_GetOptionProp); + CreateNative("GOKZ_SetOptionProp", Native_SetOptionProp); + CreateNative("GOKZ_GetOption", Native_GetOption); + CreateNative("GOKZ_SetOption", Native_SetOption); + CreateNative("GOKZ_GetHitPerf", Native_GetHitPerf); + CreateNative("GOKZ_SetHitPerf", Native_SetHitPerf); + CreateNative("GOKZ_GetTakeoffSpeed", Native_GetTakeoffSpeed); + CreateNative("GOKZ_SetTakeoffSpeed", Native_SetTakeoffSpeed); + CreateNative("GOKZ_GetValidJump", Native_GetValidJump); + CreateNative("GOKZ_JoinTeam", Native_JoinTeam); + + CreateNative("GOKZ_EmitSoundToClient", Native_EmitSoundToClient); +} + +public int Native_GetModeLoaded(Handle plugin, int numParams) +{ + return view_as<int>(GetModeLoaded(GetNativeCell(1))); +} + +public int Native_GetModeVersion(Handle plugin, int numParams) +{ + return view_as<int>(GetModeVersion(GetNativeCell(1))); +} + +public int Native_SetModeLoaded(Handle plugin, int numParams) +{ + SetModeLoaded(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)); + return 0; +} + +public int Native_GetLoadedModeCount(Handle plugin, int numParams) +{ + return GetLoadedModeCount(); +} + +public int Native_SetMode(Handle plugin, int numParams) +{ + return view_as<bool>(SwitchToModeIfAvailable(GetNativeCell(1),GetNativeCell(2))); +} + +public int Native_PrintToChatAndLog(Handle plugin, int numParams) +{ + NativeHelper_PrintToChatOrLog(true); + return 0; +} + +public int Native_PrintToChat(Handle plugin, int numParams) +{ + NativeHelper_PrintToChatOrLog(false); + return 0; +} + +static int NativeHelper_PrintToChatOrLog(bool alwaysLog) +{ + int client = GetNativeCell(1); + bool addPrefix = GetNativeCell(2); + + char buffer[1024]; + SetGlobalTransTarget(client); + FormatNativeString(0, 3, 4, sizeof(buffer), _, buffer); + + // The console (client 0) gets a special treatment + if (client == 0 || (!IsValidClient(client) && !IsClientSourceTV(client)) || alwaysLog) + { + // Strip colors + // We can't regex-replace, so I'm quite sure that's the most efficient way. + // It's also not perfectly safe, we will just assume you never have curly + // braces without a color in beween. + char colorlessBuffer[1024]; + FormatEx(colorlessBuffer, sizeof(colorlessBuffer), "%L: ", client); + int iIn = 0, iOut = strlen(colorlessBuffer); + do + { + if (buffer[iIn] == '{') + { + for (; buffer[iIn] != '}' && iIn < sizeof(buffer) - 2; iIn++){} + if (iIn >= sizeof(buffer) - 2) + { + break; + } + iIn++; + continue; + } + + colorlessBuffer[iOut] = buffer[iIn]; + iIn++; + iOut++; + } while (buffer[iIn] != '\0' && iIn < sizeof(buffer) - 1 && iOut < sizeof(colorlessBuffer) - 1); + colorlessBuffer[iOut] = '\0'; + LogMessage(colorlessBuffer); + } + + if (client != 0) + { + if (addPrefix) + { + char prefix[64]; + gCV_gokz_chat_prefix.GetString(prefix, sizeof(prefix)); + Format(buffer, sizeof(buffer), "%s%s", prefix, buffer); + } + + CPrintToChat(client, "%s", buffer); + } + return 0; +} + +public int Native_GetOptionsTopMenu(Handle plugin, int numParams) +{ + return view_as<int>(GetOptionsTopMenu()); +} + +public int Native_GetCourseRegistered(Handle plugin, int numParams) +{ + return view_as<int>(GetCourseRegistered(GetNativeCell(1))); +} + +public int Native_StartTimer(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + + return view_as<int>(TimerStart(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3))); +} + +public int Native_EndTimer(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + + return view_as<int>(TimerEnd(GetNativeCell(1), GetNativeCell(2))); +} + +public int Native_StopTimer(Handle plugin, int numParams) +{ + return view_as<int>(TimerStop(GetNativeCell(1), GetNativeCell(2))); +} + +public int Native_StopTimerAll(Handle plugin, int numParams) +{ + TimerStopAll(GetNativeCell(1)); + return 0; +} + +public int Native_TeleportToStart(Handle plugin, int numParams) +{ + TeleportToStart(GetNativeCell(1)); + return 0; +} + +public int Native_TeleportToSearchStart(Handle plugin, int numParams) +{ + TeleportToSearchStart(GetNativeCell(1), GetNativeCell(2)); + return 0; +} + +public int Native_GetVirtualButtonPosition(Handle plugin, int numParams) +{ + int course; + float position[3]; + + course = GetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3)); + SetNativeArray(2, position, sizeof(position)); + + return course; +} + +public int Native_SetVirtualButtonPosition(Handle plugin, int numParams) +{ + float position[3]; + + GetNativeArray(2, position, sizeof(position)); + SetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3), view_as<bool>(GetNativeCell(4))); + return 0; +} + +public int Native_ResetVirtualButtonPosition(Handle plugin, int numParams) +{ + ResetVirtualButtonPosition(GetNativeCell(1), GetNativeCell(2)); + return 0; +} + +public int Native_LockVirtualButtons(Handle plugin, int numParams) +{ + LockVirtualButtons(GetNativeCell(1)); + return 0; +} + +public int Native_GetStartPosition(Handle plugin, int numParams) +{ + StartPositionType type; + float position[3], angles[3]; + + type = GetStartPosition(GetNativeCell(1), position, angles); + SetNativeArray(2, position, sizeof(position)); + SetNativeArray(3, angles, sizeof(angles)); + + return view_as<int>(type); +} + +public int Native_SetStartPosition(Handle plugin, int numParams) +{ + float position[3], angles[3]; + + GetNativeArray(3, position, sizeof(position)); + GetNativeArray(4, angles, sizeof(angles)); + SetStartPosition(GetNativeCell(1), GetNativeCell(2), position, angles); + return 0; +} + +public int Native_TeleportToEnd(Handle plugin, int numParams) +{ + TeleportToEnd(GetNativeCell(1), GetNativeCell(2)); + return 0; +} + +public int Native_GetStartPositionType(Handle plugin, int numParams) +{ + return view_as<int>(GetStartPositionType(GetNativeCell(1))); +} + +public int Native_SetStartPositionToMapStart(Handle plugin, int numParams) +{ + return SetStartPositionToMapStart(GetNativeCell(1), GetNativeCell(2)); +} + +public int Native_MakeCheckpoint(Handle plugin, int numParams) +{ + MakeCheckpoint(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanMakeCheckpoint(Handle plugin, int numParams) +{ + return CanMakeCheckpoint(GetNativeCell(1)); +} + +public int Native_TeleportToCheckpoint(Handle plugin, int numParams) +{ + TeleportToCheckpoint(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanTeleportToCheckpoint(Handle plugin, int numParams) +{ + return CanTeleportToCheckpoint(GetNativeCell(1)); +} + +public int Native_PrevCheckpoint(Handle plugin, int numParams) +{ + PrevCheckpoint(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanPrevCheckpoint(Handle plugin, int numParams) +{ + return CanPrevCheckpoint(GetNativeCell(1)); +} + +public int Native_NextCheckpoint(Handle plugin, int numParams) +{ + NextCheckpoint(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanNextCheckpoint(Handle plugin, int numParams) +{ + return CanNextCheckpoint(GetNativeCell(1)); +} + +public int Native_UndoTeleport(Handle plugin, int numParams) +{ + UndoTeleport(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanUndoTeleport(Handle plugin, int numParams) +{ + return CanUndoTeleport(GetNativeCell(1)); +} + +public int Native_Pause(Handle plugin, int numParams) +{ + Pause(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanPause(Handle plugin, int numParams) +{ + return CanPause(GetNativeCell(1)); +} + +public int Native_Resume(Handle plugin, int numParams) +{ + Resume(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanResume(Handle plugin, int numParams) +{ + return CanResume(GetNativeCell(1)); +} + +public int Native_TogglePause(Handle plugin, int numParams) +{ + TogglePause(GetNativeCell(1)); + return 0; +} + +public int Native_GetCanTeleportToStart(Handle plugin, int numParams) +{ + return CanTeleportToStart(GetNativeCell(1)); +} + +public int Native_GetCanTeleportToEnd(Handle plugin, int numParams) +{ + return CanTeleportToEnd(GetNativeCell(1)); +} + +public int Native_PlayErrorSound(Handle plugin, int numParams) +{ + PlayErrorSound(GetNativeCell(1)); + return 0; +} + +public int Native_SetValidJumpOrigin(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + float origin[3]; + GetNativeArray(2, origin, sizeof(origin)); + + // The order is important here! + OnValidOriginChange_ValidJump(client, origin); + + // Using Movement_SetOrigin instead causes considerable lag for spectators + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + return 0; +} + +public int Native_GetTimerRunning(Handle plugin, int numParams) +{ + return view_as<int>(GetTimerRunning(GetNativeCell(1))); +} + +public int Native_GetValidTimer(Handle plugin, int numParams) +{ + return view_as<int>(GetValidTimer(GetNativeCell(1))); +} + +public int Native_GetCourse(Handle plugin, int numParams) +{ + return GetCurrentCourse(GetNativeCell(1)); +} + +public int Native_SetCourse(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + SetCurrentCourse(GetNativeCell(1), GetNativeCell(2)); + return view_as<int>(false); +} + +public int Native_GetPaused(Handle plugin, int numParams) +{ + return view_as<int>(GetPaused(GetNativeCell(1))); +} + +public int Native_GetTime(Handle plugin, int numParams) +{ + return view_as<int>(GetCurrentTime(GetNativeCell(1))); +} + +public int Native_SetTime(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + + SetCurrentTime(GetNativeCell(1), view_as<float>(GetNativeCell(2))); + return view_as<int>(true); +} + +public int Native_InvalidateRun(Handle plugin, int numParams) +{ + InvalidateRun(GetNativeCell(1)); + return view_as<int>(true); +} + +public int Native_GetCheckpointCount(Handle plugin, int numParams) +{ + return GetCheckpointCount(GetNativeCell(1)); +} + +public int Native_SetCheckpointCount(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + SetCheckpointCount(GetNativeCell(1), GetNativeCell(2)); + return view_as<int>(true); +} + +public int Native_GetCheckpointData(Handle plugin, int numParams) +{ + ArrayList temp = GetCheckpointData(GetNativeCell(1)); + Handle cps = CloneHandle(temp, plugin); + delete temp; + return view_as<int>(cps); +} + +public int Native_SetCheckpointData(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + return SetCheckpointData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3)); +} + +public int Native_GetUndoTeleportData(Handle plugin, int numParams) +{ + ArrayList temp = GetUndoTeleportData(GetNativeCell(1)); + Handle utd = CloneHandle(temp, plugin); + delete temp; + return view_as<int>(utd); +} + +public int Native_SetUndoTeleportData(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + return SetUndoTeleportData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3)); +} + +public int Native_GetTeleportCount(Handle plugin, int numParams) +{ + return GetTeleportCount(GetNativeCell(1)); +} + +public int Native_SetTeleportCount(Handle plugin, int numParams) +{ + if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1))) + { + return view_as<int>(false); + } + + SetTeleportCount(GetNativeCell(1), GetNativeCell(2)); + return view_as<int>(true); +} + +public int Native_RegisterOption(Handle plugin, int numParams) +{ + char name[GOKZ_OPTION_MAX_NAME_LENGTH]; + GetNativeString(1, name, sizeof(name)); + char description[255]; + GetNativeString(2, description, sizeof(description)); + return view_as<int>(RegisterOption(name, description, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), GetNativeCell(6))); +} + +public int Native_GetOptionProp(Handle plugin, int numParams) +{ + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + GetNativeString(1, option, sizeof(option)); + OptionProp prop = GetNativeCell(2); + any value = GetOptionProp(option, prop); + + // Return clone of Handle if called by another plugin + if (prop == OptionProp_Cookie && plugin != gH_ThisPlugin) + { + value = CloneHandle(value, plugin); + } + + return value; +} + +public int Native_SetOptionProp(Handle plugin, int numParams) +{ + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + GetNativeString(1, option, sizeof(option)); + OptionProp prop = GetNativeCell(2); + return SetOptionProp(option, prop, GetNativeCell(3)); +} + +public int Native_GetOption(Handle plugin, int numParams) +{ + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + GetNativeString(2, option, sizeof(option)); + return view_as<int>(GetOption(GetNativeCell(1), option)); +} + +public int Native_SetOption(Handle plugin, int numParams) +{ + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + GetNativeString(2, option, sizeof(option)); + return view_as<int>(SetOption(GetNativeCell(1), option, GetNativeCell(3))); +} + +public int Native_GetHitPerf(Handle plugin, int numParams) +{ + return view_as<int>(GetGOKZHitPerf(GetNativeCell(1))); +} + +public int Native_SetHitPerf(Handle plugin, int numParams) +{ + SetGOKZHitPerf(GetNativeCell(1), view_as<bool>(GetNativeCell(2))); + return 0; +} + +public int Native_GetTakeoffSpeed(Handle plugin, int numParams) +{ + return view_as<int>(GetGOKZTakeoffSpeed(GetNativeCell(1))); +} + +public int Native_SetTakeoffSpeed(Handle plugin, int numParams) +{ + SetGOKZTakeoffSpeed(GetNativeCell(1), view_as<float>(GetNativeCell(2))); + return 0; +} + +public int Native_GetValidJump(Handle plugin, int numParams) +{ + return view_as<int>(GetValidJump(GetNativeCell(1))); +} + +public int Native_JoinTeam(Handle plugin, int numParams) +{ + JoinTeam(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3), GetNativeCell(4)); + return 0; +} + +public int Native_EmitSoundToClient(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + + char sample[PLATFORM_MAX_PATH]; + GetNativeString(2, sample, sizeof(sample)); + + float volume = GetNativeCell(3); + float newVolume = volume; + + char description[64]; + GetNativeString(4, description, sizeof(description)); + + Action result; + + Call_GOKZ_OnEmitSoundToClient(client, sample, newVolume, description, result); + if (result == Plugin_Stop) + { + return 0; + } + if (result == Plugin_Changed) + { + EmitSoundToClient(client, sample, _, _, _, _, newVolume); + return 0; + } + EmitSoundToClient(client, sample, _, _, _, _, volume); + return 0; +} + +// =====[ PRIVATE ]===== + +static bool BlockedExternallyCalledTimerNative(Handle plugin, int client) +{ + if (plugin != gH_ThisPlugin) + { + Action result; + Call_GOKZ_OnTimerNativeCalledExternally(plugin, client, result); + if (result != Plugin_Continue) + { + return true; + } + } + return false; +} diff --git a/sourcemod/scripting/gokz-core/options.sp b/sourcemod/scripting/gokz-core/options.sp new file mode 100644 index 0000000..6daef13 --- /dev/null +++ b/sourcemod/scripting/gokz-core/options.sp @@ -0,0 +1,438 @@ +static StringMap optionData; +static StringMap optionDescriptions; + + + +// =====[ PUBLIC ]===== + +bool RegisterOption(const char[] name, const char[] description, OptionType type, any defaultValue, any minValue, any maxValue) +{ + if (!IsValueInRange(type, defaultValue, minValue, maxValue)) + { + LogError("Failed to register option \"%s\" due to invalid default value and value range.", name); + return false; + } + + if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1) + { + LogError("Failed to register option \"%s\" because its name is too long.", name); + return false; + } + + if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1) + { + LogError("Failed to register option \"%s\" because its description is too long.", name); + return false; + } + + ArrayList data; + Cookie cookie; + if (IsRegisteredOption(name)) + { + optionData.GetValue(name, data); + cookie = GetOptionProp(name, OptionProp_Cookie); + } + else + { + data = new ArrayList(1, view_as<int>(OPTIONPROP_COUNT)); + cookie = new Cookie(name, description, CookieAccess_Private); + } + + data.Set(view_as<int>(OptionProp_Cookie), cookie); + data.Set(view_as<int>(OptionProp_Type), type); + data.Set(view_as<int>(OptionProp_DefaultValue), defaultValue); + data.Set(view_as<int>(OptionProp_MinValue), minValue); + data.Set(view_as<int>(OptionProp_MaxValue), maxValue); + + optionData.SetValue(name, data, true); + optionDescriptions.SetString(name, description, true); + + // Support late-loading/registering + for (int client = 1; client <= MaxClients; client++) + { + if (AreClientCookiesCached(client)) + { + LoadOption(client, name); + } + } + + return true; +} + +any GetOptionProp(const char[] option, OptionProp prop) +{ + ArrayList data; + if (!optionData.GetValue(option, data)) + { + LogError("Failed to get option property of unregistered option \"%s\".", option); + return -1; + } + + return data.Get(view_as<int>(prop)); +} + +bool SetOptionProp(const char[] option, OptionProp prop, any newValue) +{ + ArrayList data; + if (!optionData.GetValue(option, data)) + { + LogError("Failed to set property of unregistered option \"%s\".", option); + return false; + } + + if (prop == OptionProp_Cookie) + { + LogError("Failed to set cookie of option \"%s\" as it is read-only."); + return false; + } + + OptionType type = GetOptionProp(option, OptionProp_Type); + any defaultValue = GetOptionProp(option, OptionProp_DefaultValue); + any minValue = GetOptionProp(option, OptionProp_MinValue); + any maxValue = GetOptionProp(option, OptionProp_MaxValue); + + switch (prop) + { + case OptionProp_DefaultValue: + { + if (!IsValueInRange(type, newValue, minValue, maxValue)) + { + LogError("Failed to set default value of option \"%s\" due to invalid default value and value range.", option); + return false; + } + } + case OptionProp_MinValue: + { + if (!IsValueInRange(type, defaultValue, newValue, maxValue)) + { + LogError("Failed to set minimum value of option \"%s\" due to invalid default value and value range.", option); + return false; + } + } + case OptionProp_MaxValue: + { + if (!IsValueInRange(type, defaultValue, minValue, newValue)) + { + LogError("Failed to set maximum value of option \"%s\" due to invalid default value and value range.", option); + return false; + } + } + } + + data.Set(view_as<int>(prop), newValue); + return optionData.SetValue(option, data, true); +} + +any GetOption(int client, const char[] option) +{ + if (!IsRegisteredOption(option)) + { + LogError("Failed to get value of unregistered option \"%s\".", option); + return -1; + } + + Cookie cookie = GetOptionProp(option, OptionProp_Cookie); + OptionType type = GetOptionProp(option, OptionProp_Type); + char value[100]; + cookie.Get(client, value, sizeof(value)); + + if (type == OptionType_Float) + { + return StringToFloat(value); + } + else //if (type == OptionType_Int) + { + return StringToInt(value); + } +} + +bool SetOption(int client, const char[] option, any newValue) +{ + if (!IsRegisteredOption(option)) + { + LogError("Failed to set value of unregistered option \"%s\".", option); + return false; + } + + if (GetOption(client, option) == newValue) + { + return true; + } + + OptionType type = GetOptionProp(option, OptionProp_Type); + any minValue = GetOptionProp(option, OptionProp_MinValue); + any maxValue = GetOptionProp(option, OptionProp_MaxValue); + + if (!IsValueInRange(type, newValue, minValue, maxValue)) + { + LogError("Failed to set value of option \"%s\" because desired value was outside registered value range.", option); + return false; + } + + char newValueString[100]; + if (type == OptionType_Float) + { + FloatToString(newValue, newValueString, sizeof(newValueString)); + } + else //if (type == OptionType_Int) + { + IntToString(newValue, newValueString, sizeof(newValueString)); + } + + Cookie cookie = GetOptionProp(option, OptionProp_Cookie); + cookie.Set(client, newValueString); + + if (IsClientInGame(client)) + { + Call_GOKZ_OnOptionChanged(client, option, newValue); + } + + return true; +} + +bool IsRegisteredOption(const char[] option) +{ + int dummy; + return optionData.GetValue(option, dummy); +} + + + +// =====[ EVENTS ]===== + +void OnPluginStart_Options() +{ + optionData = new StringMap(); + optionDescriptions = new StringMap(); + RegisterOptions(); +} + +void OnClientCookiesCached_Options(int client) +{ + StringMapSnapshot optionDataSnapshot = optionData.Snapshot(); + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + + for (int i = 0; i < optionDataSnapshot.Length; i++) + { + optionDataSnapshot.GetKey(i, option, sizeof(option)); + LoadOption(client, option); + } + + delete optionDataSnapshot; + + Call_GOKZ_OnOptionsLoaded(client); +} + +void OnClientPutInServer_Options(int client) +{ + if (!GetModeLoaded(GOKZ_GetCoreOption(client, Option_Mode))) + { + GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode()); + } +} + +void OnOptionChanged_Options(int client, Option option, int newValue) +{ + if (option == Option_Mode && !GetModeLoaded(newValue)) + { + GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", newValue); + GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode()); + } + else + { + PrintOptionChangeMessage(client, option, newValue); + } +} + +void OnModeUnloaded_Options(int mode) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && GOKZ_GetCoreOption(client, Option_Mode) == mode) + { + GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode()); + } + } +} + +void OnMapStart_Options() +{ + LoadDefaultOptions(); +} + + + +// =====[ PRIVATE ]===== + +static void RegisterOptions() +{ + for (Option option; option < OPTION_COUNT; option++) + { + RegisterOption(gC_CoreOptionNames[option], gC_CoreOptionDescriptions[option], + OptionType_Int, gI_CoreOptionDefaults[option], 0, gI_CoreOptionCounts[option] - 1); + } +} + +static bool IsValueInRange(OptionType type, any value, any minValue, any maxValue) +{ + if (type == OptionType_Float) + { + return FloatCompare(minValue, value) <= 0 && FloatCompare(value, maxValue) <= 0; + } + else //if (type == OptionType_Int) + { + return minValue <= value && value <= maxValue; + } +} + +static void LoadOption(int client, const char[] option) +{ + char valueString[100]; + Cookie cookie = GetOptionProp(option, OptionProp_Cookie); + cookie.Get(client, valueString, sizeof(valueString)); + + // If there's no stored value for the option, set it to default + if (valueString[0] == '\0') + { + SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue)); + return; + } + + OptionType type = GetOptionProp(option, OptionProp_Type); + any minValue = GetOptionProp(option, OptionProp_MinValue); + any maxValue = GetOptionProp(option, OptionProp_MaxValue); + any value; + + // If stored option isn't a valid float or integer, or is out of range, set it to default + if (type == OptionType_Float && StringToFloatEx(valueString, value) == 0) + { + SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue)); + } + else if (type == OptionType_Int && StringToIntEx(valueString, value) == 0) + { + SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue)); + } + else if (!IsValueInRange(type, value, minValue, maxValue)) + { + SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue)); + } +} + +// Load default optionData from a config file, creating one and adding optionData if necessary +static void LoadDefaultOptions() +{ + KeyValues oldKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT); + + if (FileExists(GOKZ_CFG_OPTIONS) && !oldKV.ImportFromFile(GOKZ_CFG_OPTIONS)) + { + LogError("Failed to load file: \"%s\".", GOKZ_CFG_OPTIONS); + delete oldKV; + return; + } + + KeyValues newKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT); // This one will be sorted by option name + StringMapSnapshot optionDataSnapshot = optionData.Snapshot(); + ArrayList optionDataSnapshotArray = new ArrayList(ByteCountToCells(GOKZ_OPTION_MAX_NAME_LENGTH), 0); + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; + char optionDescription[GOKZ_OPTION_MAX_DESC_LENGTH]; + + // Sort the optionData by name + for (int i = 0; i < optionDataSnapshot.Length; i++) + { + optionDataSnapshot.GetKey(i, option, sizeof(option)); + optionDataSnapshotArray.PushString(option); + } + SortADTArray(optionDataSnapshotArray, Sort_Ascending, Sort_String); + + // Get the values from the KeyValues, otherwise set them + for (int i = 0; i < optionDataSnapshotArray.Length; i++) + { + oldKV.Rewind(); + newKV.Rewind(); + optionDataSnapshotArray.GetString(i, option, sizeof(option)); + optionDescriptions.GetString(option, optionDescription, sizeof(optionDescription)); + + newKV.JumpToKey(option, true); + newKV.SetString(GOKZ_CFG_OPTIONS_DESCRIPTION, optionDescription); + + OptionType type = GetOptionProp(option, OptionProp_Type); + if (type == OptionType_Float) + { + if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false)) + { + oldKV.GoBack(); + newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT)); + SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT)); + } + else + { + newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue)); + } + } + else if (type == OptionType_Int) + { + if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false)) + { + oldKV.GoBack(); + newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT)); + SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT)); + } + else + { + newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue)); + } + } + } + + newKV.Rewind(); + newKV.ExportToFile(GOKZ_CFG_OPTIONS); + + delete oldKV; + delete newKV; + delete optionDataSnapshot; + delete optionDataSnapshotArray; +} + +static void PrintOptionChangeMessage(int client, Option option, int newValue) +{ + // NOTE: Not all optionData have a message for when they are changed. + switch (option) + { + case Option_Mode: + { + GOKZ_PrintToChat(client, true, "%t", "Switched Mode", gC_ModeNames[newValue]); + } + case Option_VirtualButtonIndicators: + { + switch (newValue) + { + case VirtualButtonIndicators_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Disable"); + } + case VirtualButtonIndicators_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Enable"); + } + } + } + case Option_Safeguard: + { + switch (newValue) + { + case Safeguard_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Disable"); + } + case Safeguard_EnabledNUB: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (NUB)"); + } + case Safeguard_EnabledPRO: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (PRO)"); + } + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/teamnumfix.sp b/sourcemod/scripting/gokz-core/teamnumfix.sp new file mode 100644 index 0000000..0c5d81d --- /dev/null +++ b/sourcemod/scripting/gokz-core/teamnumfix.sp @@ -0,0 +1,68 @@ +static Handle H_RemovePlayer; +static int teamEntID[4]; +static int oldTeam[MAXPLAYERS + 1]; +static int realTeam[MAXPLAYERS + 1]; + +void OnPluginStart_TeamNumber() +{ + GameData gamedataConf = LoadGameConfigFile("gokz-core.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-core gamedata"); + } + + StartPrepSDKCall(SDKCall_Entity); + PrepSDKCall_SetVirtual(gamedataConf.GetOffset("CCSTeam::RemovePlayer")); + PrepSDKCall_AddParameter(SDKType_CBasePlayer, SDKPass_Pointer); + H_RemovePlayer = EndPrepSDKCall(); + if (H_RemovePlayer == INVALID_HANDLE) + { + SetFailState("Unable to prepare SDKCall for CCSTeam::RemovePlayer!"); + } +} + +void OnMapStart_TeamNumber() +{ + // Fetch the entity ID of team entities and store them. + int team = FindEntityByClassname(MaxClients + 1, "cs_team_manager"); + while (team != -1) + { + int teamNum = GetEntProp(team, Prop_Send, "m_iTeamNum"); + teamEntID[teamNum] = team; + team = FindEntityByClassname(team, "cs_team_manager"); + } +} + +void OnGameFrame_TeamNumber() +{ + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || !IsPlayerAlive(client)) + { + continue; + } + int team = GetEntProp(client, Prop_Data, "m_iTeamNum"); + // If the entprop changed, remove the player from the old team, but make sure it's a valid team first + if (team != oldTeam[client] && oldTeam[client] < 4 && oldTeam[client] > 0) + { + SDKCall(H_RemovePlayer, teamEntID[oldTeam[client]], client); + } + oldTeam[client] = team; + } +} + +void OnPlayerJoinTeam_TeamNumber(Event event, int client) +{ + // If the old team value is invalid, fix it. + if (event.GetInt("oldteam") > 4 || event.GetInt("oldteam") < 0) + { + event.SetInt("oldteam", 0); + } + realTeam[client] = event.GetInt("team"); +} + +void OnPlayerDeath_TeamNumber(int client) +{ + // Switch the client's team to a valid team to prevent crashes. + CS_SwitchTeam(client, realTeam[client]); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/teleports.sp b/sourcemod/scripting/gokz-core/teleports.sp new file mode 100644 index 0000000..764fc6e --- /dev/null +++ b/sourcemod/scripting/gokz-core/teleports.sp @@ -0,0 +1,917 @@ +/* + Checkpoints and teleporting, including ability to go back + to previous checkpoint, go to next checkpoint, and undo. +*/ + + +static ArrayList checkpoints[MAXPLAYERS + 1]; +static int checkpointCount[MAXPLAYERS + 1]; +static int checkpointIndex[MAXPLAYERS + 1]; +static int checkpointIndexStart[MAXPLAYERS + 1]; +static int checkpointIndexEnd[MAXPLAYERS + 1]; +static int teleportCount[MAXPLAYERS + 1]; +static StartPositionType startType[MAXPLAYERS + 1]; +static StartPositionType nonCustomStartType[MAXPLAYERS + 1]; +static float nonCustomStartOrigin[MAXPLAYERS + 1][3]; +static float nonCustomStartAngles[MAXPLAYERS + 1][3]; +static float customStartOrigin[MAXPLAYERS + 1][3]; +static float customStartAngles[MAXPLAYERS + 1][3]; +static float endOrigin[MAXPLAYERS + 1][3]; +static float endAngles[MAXPLAYERS + 1][3]; +static UndoTeleportData undoTeleportData[MAXPLAYERS + 1]; +static float lastRestartAttemptTime[MAXPLAYERS + 1]; + +// =====[ PUBLIC ]===== + +int GetCheckpointCount(int client) +{ + return checkpointCount[client]; +} + +void SetCheckpointCount(int client, int cpCount) +{ + checkpointCount[client] = cpCount; +} + +int GetTeleportCount(int client) +{ + return teleportCount[client]; +} + +void SetTeleportCount(int client, int tpCount) +{ + teleportCount[client] = tpCount; +} + +// CHECKPOINT + +void OnMapStart_Checkpoints() +{ + for (int client = 0; client < MAXPLAYERS + 1; client++) + { + if (checkpoints[client] != INVALID_HANDLE) + { + delete checkpoints[client]; + } + checkpoints[client] = new ArrayList(sizeof(Checkpoint)); + } +} + +void MakeCheckpoint(int client) +{ + if (!CanMakeCheckpoint(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnMakeCheckpoint(client, result); + if (result != Plugin_Continue) + { + return; + } + + // Make Checkpoint + checkpointCount[client]++; + Checkpoint cp; + cp.Create(client); + + if (checkpoints[client] == INVALID_HANDLE) + { + checkpoints[client] = new ArrayList(sizeof(Checkpoint)); + } + + checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS); + checkpointIndexEnd[client] = checkpointIndex[client]; + // The list has yet to be filled up, do PushArray instead of SetArray + if (checkpoints[client].Length < GOKZ_MAX_CHECKPOINTS && checkpointIndex[client] == checkpoints[client].Length) + { + checkpoints[client].PushArray(cp); + // Initialize start and end index for the first checkpoint + if (checkpoints[client].Length == 1) + { + checkpointIndexStart[client] = 0; + checkpointIndexEnd[client] = 0; + } + } + else + { + checkpoints[client].SetArray(checkpointIndex[client], cp); + // The new checkpoint has overridden the oldest checkpoint, move the start index by one. + if (checkpointIndexEnd[client] == checkpointIndexStart[client]) + { + checkpointIndexStart[client] = NextIndex(checkpointIndexStart[client], GOKZ_MAX_CHECKPOINTS); + } + } + + + if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled) + { + GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Checkpoint"); + } + if (GOKZ_GetCoreOption(client, Option_CheckpointMessages) == CheckpointMessages_Enabled) + { + GOKZ_PrintToChat(client, true, "%t", "Make Checkpoint", checkpointCount[client]); + } + + if (!GetTimerRunning(client) && AntiCpTriggerIsTouched(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Anti Checkpoint Area Warning"); + } + + // Call Post Forward + Call_GOKZ_OnMakeCheckpoint_Post(client); +} + +bool CanMakeCheckpoint(int client, bool showError = false) +{ + if (!IsPlayerAlive(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Must Be Alive"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (GetTimerRunning(client) && AntiCpTriggerIsTouched(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Anti Checkpoint Area)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (!Movement_GetOnGround(client) && Movement_GetMovetype(client) != MOVETYPE_LADDER) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Midair)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (BhopTriggersJustTouched(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Just Landed)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + +ArrayList GetCheckpointData(int client) +{ + // Don't clone the entire thing, return an ordered list of checkpoints. + // Doing this should be cleaner, saves memory and should be faster than a full Clone(). + ArrayList checkpointData = new ArrayList(sizeof(Checkpoint)); + if (checkpointIndex[client] == -1) + { + // No checkpoint was made, return empty ArrayList + return checkpointData; + } + for (int i = checkpointIndexStart[client]; i != checkpointIndexEnd[client]; i = NextIndex(i, GOKZ_MAX_CHECKPOINTS)) + { + Checkpoint cp; + checkpoints[client].GetArray(i, cp); + checkpointData.PushArray(cp); + } + return checkpointData; +} + +bool SetCheckpointData(int client, ArrayList cps, int version) +{ + if (version != GOKZ_CHECKPOINT_VERSION) + { + return false; + } + // cps is assumed to be ordered. + if (cps != INVALID_HANDLE) + { + delete checkpoints[client]; + checkpoints[client] = cps.Clone(); + if (cps.Length == 0) + { + checkpointIndexStart[client] = -1; + checkpointIndexEnd[client] = -1; + } + else + { + checkpointIndexStart[client] = 0; + checkpointIndexEnd[client] = checkpoints[client].Length - 1; + } + checkpointIndex[client] = checkpointIndexEnd[client]; + return true; + } + return false; +} + +ArrayList GetUndoTeleportData(int client) +{ + // Enum structs cannot be sent directly over natives, we put it in an ArrayList of one instead. + // We use another struct instead of reusing Checkpoint so normal checkpoints don't use more memory than needed. + ArrayList undoTeleportDataArray = new ArrayList(sizeof(UndoTeleportData)); + undoTeleportDataArray.PushArray(undoTeleportData[client]); + return undoTeleportDataArray; +} + +bool SetUndoTeleportData(int client, ArrayList undoTeleportDataArray, int version) +{ + if (version != GOKZ_CHECKPOINT_VERSION) + { + return false; + } + if (undoTeleportDataArray != INVALID_HANDLE && undoTeleportDataArray.Length == 1) + { + undoTeleportDataArray.GetArray(0, undoTeleportData[client], sizeof(UndoTeleportData)); + return true; + } + return false; +} +// TELEPORT + +void TeleportToCheckpoint(int client) +{ + if (!CanTeleportToCheckpoint(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTeleportToCheckpoint(client, result); + if (result != Plugin_Continue) + { + return; + } + + CheckpointTeleportDo(client); + + // Call Post Forward + Call_GOKZ_OnTeleportToCheckpoint_Post(client); +} + +bool CanTeleportToCheckpoint(int client, bool showError = false) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client) && GOKZ_GetTeleportCount(client) == 0) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (checkpoints[client] == INVALID_HANDLE || checkpoints[client].Length <= 0) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (No Checkpoints)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + + +// PREV CP + +void PrevCheckpoint(int client) +{ + if (!CanPrevCheckpoint(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnPrevCheckpoint(client, result); + if (result != Plugin_Continue) + { + return; + } + + checkpointIndex[client] = PrevIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS); + CheckpointTeleportDo(client); + + // Call Post Forward + Call_GOKZ_OnPrevCheckpoint_Post(client); +} + +bool CanPrevCheckpoint(int client, bool showError = false) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (checkpointIndex[client] == checkpointIndexStart[client]) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Prev CP (No Checkpoints)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + + +// NEXT CP + +void NextCheckpoint(int client) +{ + if (!CanNextCheckpoint(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnNextCheckpoint(client, result); + if (result != Plugin_Continue) + { + return; + } + checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS); + CheckpointTeleportDo(client); + + // Call Post Forward + Call_GOKZ_OnNextCheckpoint_Post(client); +} + +bool CanNextCheckpoint(int client, bool showError = false) +{ + if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (checkpointIndex[client] == checkpointIndexEnd[client]) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Next CP (No Checkpoints)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + + +// RESTART & RESPAWN + +bool CanTeleportToStart(int client, bool showError = false) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + float currentTime = GetEngineTime(); + float timeSinceLastAttempt = currentTime - lastRestartAttemptTime[client]; + float cooldown; + // If the client restarts for the first time or the last attempt is too long ago, restart the cooldown. + if (lastRestartAttemptTime[client] == 0.0 || timeSinceLastAttempt > GOKZ_SAFEGUARD_RESTART_MAX_DELAY) + { + lastRestartAttemptTime[client] = currentTime; + cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY; + } + else + { + cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY - timeSinceLastAttempt; + } + if (cooldown <= 0.0) + { + lastRestartAttemptTime[client] = 0.0; + return true; + } + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked (Temp)", cooldown); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + +void TeleportToStart(int client) +{ + if (!CanTeleportToStart(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTeleportToStart(client, GetCurrentCourse(client), result); + if (result != Plugin_Continue) + { + return; + } + + // Teleport to Start + if (startType[client] == StartPositionType_Spawn) + { + GOKZ_RespawnPlayer(client, .restorePos = false); + // Respawning alone does not guarantee a valid spawn. + float spawnOrigin[3]; + float spawnAngles[3]; + GetValidSpawn(spawnOrigin, spawnAngles); + TeleportPlayer(client, spawnOrigin, spawnAngles); + } + else if (startType[client] == StartPositionType_Custom) + { + TeleportDo(client, customStartOrigin[client], customStartAngles[client]); + } + else + { + TeleportDo(client, nonCustomStartOrigin[client], nonCustomStartAngles[client]); + } + + if (startType[client] != StartPositionType_MapButton + && (!InRangeOfVirtualStart(client) || !CanReachVirtualStart(client))) + { + GOKZ_StopTimer(client, false); + } + + // Call Post Forward + Call_GOKZ_OnTeleportToStart_Post(client, GetCurrentCourse(client)); +} + +void TeleportToSearchStart(int client, int course) +{ + if (!CanTeleportToStart(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTeleportToStart(client, course, result); + if (result != Plugin_Continue) + { + return; + } + + float origin[3], angles[3]; + if (!GetSearchStartPosition(course, origin, angles)) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Start Found"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course); + } + return; + } + GOKZ_StopTimer(client, false); + + TeleportDo(client, origin, angles); + // Call Post Forward + Call_GOKZ_OnTeleportToStart_Post(client, course); +} + +StartPositionType GetStartPosition(int client, float position[3], float angles[3]) +{ + if (startType[client] == StartPositionType_Custom) + { + position = customStartOrigin[client]; + angles = customStartAngles[client]; + } + else if (startType[client] != StartPositionType_Spawn) + { + position = nonCustomStartOrigin[client]; + angles = nonCustomStartAngles[client]; + } + + return startType[client]; +} + +bool TeleportToCourseStart(int client, int course) +{ + if (!CanTeleportToStart(client, true)) + { + return false; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTeleportToStart(client, course, result); + if (result != Plugin_Continue) + { + return false; + } + float origin[3], angles[3]; + + if (!GetMapStartPosition(course, origin, angles)) + { + if (!GetSearchStartPosition(course, origin, angles)) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Start Found"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course); + } + return false; + } + } + + GOKZ_StopTimer(client); + + TeleportDo(client, origin, angles); + + // Call Post Forward + Call_GOKZ_OnTeleportToStart_Post(client, course); + return true; +} + +StartPositionType GetStartPositionType(int client) +{ + return startType[client]; +} + +// Note: Use ClearStartPosition to switch off StartPositionType_Custom +void SetStartPosition(int client, StartPositionType type, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR) +{ + if (type == StartPositionType_Custom) + { + startType[client] = StartPositionType_Custom; + + if (!IsNullVector(origin)) + { + customStartOrigin[client] = origin; + } + + if (!IsNullVector(angles)) + { + customStartAngles[client] = angles; + } + + // Call Post Forward + Call_GOKZ_OnStartPositionSet_Post(client, startType[client], customStartOrigin[client], customStartAngles[client]); + } + else + { + nonCustomStartType[client] = type; + + if (!IsNullVector(origin)) + { + nonCustomStartOrigin[client] = origin; + } + + if (!IsNullVector(angles)) + { + nonCustomStartAngles[client] = angles; + } + + if (startType[client] != StartPositionType_Custom) + { + startType[client] = type; + + // Call Post Forward + Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]); + } + } +} + +void SetStartPositionToCurrent(int client, StartPositionType type) +{ + float origin[3], angles[3]; + Movement_GetOrigin(client, origin); + Movement_GetEyeAngles(client, angles); + + SetStartPosition(client, type, origin, angles); +} + +bool SetStartPositionToMapStart(int client, int course) +{ + float origin[3], angles[3]; + + if (!GetMapStartPosition(course, origin, angles)) + { + return false; + } + + SetStartPosition(client, StartPositionType_MapStart, origin, angles); + + return true; +} + +bool ClearCustomStartPosition(int client) +{ + if (GetStartPositionType(client) != StartPositionType_Custom) + { + return false; + } + + startType[client] = nonCustomStartType[client]; + + // Call Post Forward + Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]); + + return true; +} + + +// TELEPORT TO END + +bool CanTeleportToEnd(int client, bool showError = false) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + +void TeleportToEnd(int client, int course) +{ + if (!CanTeleportToEnd(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTeleportToEnd(client, course, result); + if (result != Plugin_Continue) + { + return; + } + + GOKZ_StopTimer(client, false); + + if (!GetMapEndPosition(course, endOrigin[client], endAngles[client])) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No End Found"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No End Found (Bonus)", course); + } + return; + } + TeleportDo(client, endOrigin[client], endAngles[client]); + + // Call Post Forward + Call_GOKZ_OnTeleportToEnd_Post(client, course); +} + +void SetEndPosition(int client, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR) +{ + if (!IsNullVector(origin)) + { + endOrigin[client] = origin; + } + if (!IsNullVector(angles)) + { + endAngles[client] = angles; + } +} + +bool SetEndPositionToMapEnd(int client, int course) +{ + float origin[3], angles[3]; + + if (!GetMapEndPosition(course, origin, angles)) + { + return false; + } + + SetEndPosition(client, origin, angles); + + return true; +} + + +// UNDO TP + +void UndoTeleport(int client) +{ + if (!CanUndoTeleport(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnUndoTeleport(client, result); + if (result != Plugin_Continue) + { + return; + } + + // Undo Teleport + TeleportDo(client, undoTeleportData[client].origin, undoTeleportData[client].angles); + + // Call Post Forward + Call_GOKZ_OnUndoTeleport_Post(client); +} + +bool CanUndoTeleport(int client, bool showError = false) +{ + if (teleportCount[client] <= 0) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Undo (No Teleports)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (!undoTeleportData[client].lastTeleportOnGround) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Undo (TP Was Midair)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (undoTeleportData[client].lastTeleportInBhopTrigger) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Undo (Just Landed)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (undoTeleportData[client].lastTeleportInAntiCpTrigger) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Undo (AntiCp)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Teleports(int client) +{ + checkpointCount[client] = 0; + checkpointIndex[client] = -1; + checkpointIndexStart[client] = -1; + checkpointIndexEnd[client] = -1; + teleportCount[client] = 0; + startType[client] = StartPositionType_Spawn; + nonCustomStartType[client] = StartPositionType_Spawn; + lastRestartAttemptTime[client] = 0.0; + if (checkpoints[client] != INVALID_HANDLE) + { + checkpoints[client].Clear(); + } + // Set start and end position to main course if we know of it + SetStartPositionToMapStart(client, 0); + SetEndPositionToMapEnd(client, 0); + +} + +void OnTimerStart_Teleports(int client) +{ + checkpointCount[client] = 0; + checkpointIndex[client] = -1; + checkpointIndexStart[client] = -1; + checkpointIndexEnd[client] = -1; + teleportCount[client] = 0; + checkpoints[client].Clear(); +} + +void OnStartButtonPress_Teleports(int client, int course) +{ + SetStartPositionToCurrent(client, StartPositionType_MapButton); + SetEndPositionToMapEnd(client, course); +} + +void OnVirtualStartButtonPress_Teleports(int client) +{ + SetStartPositionToCurrent(client, StartPositionType_MapButton); +} + +void OnStartZoneStartTouch_Teleports(int client, int course) +{ + SetStartPositionToMapStart(client, course); + SetEndPositionToMapEnd(client, course); +} + + + +// =====[ PRIVATE ]===== + +static int PrevIndex(int current, int maximum) +{ + int prev = current - 1; + if (prev < 0) + { + return maximum - 1; + } + return prev; +} + +static void TeleportDo(int client, const float destOrigin[3], const float destAngles[3]) +{ + if (!IsPlayerAlive(client)) + { + GOKZ_RespawnPlayer(client); + } + + // Store information about where player is teleporting from + undoTeleportData[client].Init(client, BhopTriggersJustTouched(client), Movement_GetOnGround(client), AntiCpTriggerIsTouched(client)); + + teleportCount[client]++; + TeleportPlayer(client, destOrigin, destAngles); + // TeleportPlayer needs to be done before undo TP data can be fully updated. + undoTeleportData[client].Update(); + if (GOKZ_GetCoreOption(client, Option_TeleportSounds) == TeleportSounds_Enabled) + { + GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TELEPORT, _, "Teleport"); + } + + // Call Post Foward + Call_GOKZ_OnCountedTeleport_Post(client); +} + +static void CheckpointTeleportDo(int client) +{ + Checkpoint cp; + checkpoints[client].GetArray(checkpointIndex[client], cp); + + TeleportDo(client, cp.origin, cp.angles); + if (cp.groundEnt != INVALID_ENT_REFERENCE) + { + SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", cp.groundEnt); + SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND); + } + // Handle ladder stuff + if (cp.onLadder) + { + SetEntPropVector(client, Prop_Send, "m_vecLadderNormal", cp.ladderNormal); + if (!GOKZ_GetPaused(client)) + { + Movement_SetMovetype(client, MOVETYPE_LADDER); + } + else + { + SetPausedOnLadder(client, true); + } + } + else if (GOKZ_GetPaused(client)) + { + SetPausedOnLadder(client, false); + } +} diff --git a/sourcemod/scripting/gokz-core/timer/pause.sp b/sourcemod/scripting/gokz-core/timer/pause.sp new file mode 100644 index 0000000..92ab1fb --- /dev/null +++ b/sourcemod/scripting/gokz-core/timer/pause.sp @@ -0,0 +1,257 @@ +static bool paused[MAXPLAYERS + 1]; +static bool pausedOnLadder[MAXPLAYERS + 1]; +static float lastPauseTime[MAXPLAYERS + 1]; +static bool hasPausedInThisRun[MAXPLAYERS + 1]; +static float lastResumeTime[MAXPLAYERS + 1]; +static bool hasResumedInThisRun[MAXPLAYERS + 1]; +static float lastDuckValue[MAXPLAYERS + 1]; +static float lastStaminaValue[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +bool GetPaused(int client) +{ + return paused[client]; +} + +void SetPausedOnLadder(int client, bool onLadder) +{ + pausedOnLadder[client] = onLadder; +} + +void Pause(int client) +{ + if (!CanPause(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnPause(client, result); + if (result != Plugin_Continue) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Generic)"); + GOKZ_PlayErrorSound(client); + return; + } + + // Pause + paused[client] = true; + pausedOnLadder[client] = Movement_GetMovetype(client) == MOVETYPE_LADDER; + lastDuckValue[client] = Movement_GetDuckSpeed(client); + lastStaminaValue[client] = GetEntPropFloat(client, Prop_Send, "m_flStamina"); + Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + Movement_SetMovetype(client, MOVETYPE_NONE); + if (GetTimerRunning(client)) + { + hasPausedInThisRun[client] = true; + lastPauseTime[client] = GetEngineTime(); + } + + // Call Post Forward + Call_GOKZ_OnPause_Post(client); +} + +bool CanPause(int client, bool showError = false) +{ + if (paused[client]) + { + return false; + } + + if (GetTimerRunning(client)) + { + if (hasResumedInThisRun[client] + && GetEngineTime() - lastResumeTime[client] < GOKZ_PAUSE_COOLDOWN) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Resumed)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + else if (!Movement_GetOnGround(client) + && !(Movement_GetSpeed(client) == 0 && Movement_GetVerticalVelocity(client) == 0)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Midair)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + else if (BhopTriggersJustTouched(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Landed)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + else if (AntiPauseTriggerIsTouched(client)) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Anti Pause Area)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + } + + return true; +} + +void Resume(int client, bool force = false) +{ + if (!paused[client]) + { + return; + } + if (!force && !CanResume(client, true)) + { + return; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnResume(client, result); + if (result != Plugin_Continue) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Generic)"); + GOKZ_PlayErrorSound(client); + return; + } + + // Resume + if (pausedOnLadder[client]) + { + Movement_SetMovetype(client, MOVETYPE_LADDER); + } + else + { + Movement_SetMovetype(client, MOVETYPE_WALK); + } + + // Prevent noclip exploit + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + paused[client] = false; + if (GetTimerRunning(client)) + { + hasResumedInThisRun[client] = true; + lastResumeTime[client] = GetEngineTime(); + } + Movement_SetDuckSpeed(client, lastDuckValue[client]); + SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]); + + // Call Post Forward + Call_GOKZ_OnResume_Post(client); +} + +bool CanResume(int client, bool showError = false) +{ + if (GetTimerRunning(client) && hasPausedInThisRun[client] + && GetEngineTime() - lastPauseTime[client] < GOKZ_PAUSE_COOLDOWN) + { + if (showError) + { + GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Just Paused)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + return true; +} + +void TogglePause(int client) +{ + if (paused[client]) + { + Resume(client); + } + else + { + Pause(client); + } +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Pause(int client) +{ + paused[client] = false; +} + +void OnTimerStart_Pause(int client) +{ + hasPausedInThisRun[client] = false; + hasResumedInThisRun[client] = false; + Resume(client, true); +} + +void OnChangeMovetype_Pause(int client, MoveType newMovetype) +{ + // Check if player has escaped MOVETYPE_NONE + if (!paused[client] || newMovetype == MOVETYPE_NONE) + { + return; + } + + // Player has escaped MOVETYPE_NONE, so resume + paused[client] = false; + if (GetTimerRunning(client)) + { + hasResumedInThisRun[client] = true; + lastResumeTime[client] = GetEngineTime(); + } + + // Call Post Forward + Call_GOKZ_OnResume_Post(client); +} + +void OnPlayerSpawn_Pause(int client) +{ + if (!paused[client]) + { + return; + } + + // Player has left paused state by spawning in, so resume + paused[client] = false; + if (GetTimerRunning(client)) + { + hasResumedInThisRun[client] = true; + lastResumeTime[client] = GetEngineTime(); + } + + Movement_SetDuckSpeed(client, lastDuckValue[client]); + SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]); + + // Call Post Forward + Call_GOKZ_OnResume_Post(client); +} + +void OnJoinTeam_Pause(int client, int team) +{ + // Only handle joining spectators. Joining other teams is handled by OnPlayerSpawn. + if (team == CS_TEAM_SPECTATOR) + { + paused[client] = true; + + if (GetTimerRunning(client)) + { + hasPausedInThisRun[client] = true; + lastPauseTime[client] = GetEngineTime(); + } + + // Call Post Forward + Call_GOKZ_OnPause_Post(client); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/timer/timer.sp b/sourcemod/scripting/gokz-core/timer/timer.sp new file mode 100644 index 0000000..f6696ac --- /dev/null +++ b/sourcemod/scripting/gokz-core/timer/timer.sp @@ -0,0 +1,368 @@ +static bool timerRunning[MAXPLAYERS + 1]; +static float currentTime[MAXPLAYERS + 1]; +static int currentCourse[MAXPLAYERS + 1]; +static float lastEndTime[MAXPLAYERS + 1]; +static float lastFalseEndTime[MAXPLAYERS + 1]; +static float lastStartSoundTime[MAXPLAYERS + 1]; +static int lastStartMode[MAXPLAYERS + 1]; +static bool validTime[MAXPLAYERS + 1]; + + +// =====[ PUBLIC ]===== + +bool GetTimerRunning(int client) +{ + return timerRunning[client]; +} + +bool GetValidTimer(int client) +{ + return validTime[client]; +} + +float GetCurrentTime(int client) +{ + return currentTime[client]; +} + +void SetCurrentTime(int client, float time) +{ + currentTime[client] = time; + // The timer should be running if time is not negative. + timerRunning[client] = time >= 0.0; +} + +int GetCurrentCourse(int client) +{ + return currentCourse[client]; +} + +void SetCurrentCourse(int client, int course) +{ + currentCourse[client] = course; +} + +int GetCurrentTimeType(int client) +{ + if (GetTeleportCount(client) == 0) + { + return TimeType_Pro; + } + return TimeType_Nub; +} + +bool TimerStart(int client, int course, bool allowMidair = false, bool playSound = true) +{ + if (!IsPlayerAlive(client) + || JustStartedTimer(client) + || JustTeleported(client) + || JustNoclipped(client) + || !IsPlayerValidMoveType(client) + || !allowMidair && (!Movement_GetOnGround(client) || JustLanded(client)) + || allowMidair && !Movement_GetOnGround(client) && (!GOKZ_GetValidJump(client) || GOKZ_GetHitPerf(client)) + || (GOKZ_GetTimerRunning(client) && GOKZ_GetCourse(client) != course)) + { + return false; + } + + // Call Pre Forward + Action result; + Call_GOKZ_OnTimerStart(client, course, result); + if (result != Plugin_Continue) + { + return false; + } + + // Prevent noclip exploit + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + + // Start Timer + currentTime[client] = 0.0; + timerRunning[client] = true; + currentCourse[client] = course; + lastStartMode[client] = GOKZ_GetCoreOption(client, Option_Mode); + validTime[client] = true; + if (playSound) + { + PlayTimerStartSound(client); + } + + // Call Post Forward + Call_GOKZ_OnTimerStart_Post(client, course); + + return true; +} + +bool TimerEnd(int client, int course) +{ + if (!IsPlayerAlive(client)) + { + return false; + } + + if (!timerRunning[client] || course != currentCourse[client]) + { + PlayTimerFalseEndSound(client); + lastFalseEndTime[client] = GetGameTime(); + return false; + } + + float time = GetCurrentTime(client); + int teleportsUsed = GetTeleportCount(client); + + // Call Pre Forward + Action result; + Call_GOKZ_OnTimerEnd(client, course, time, teleportsUsed, result); + if (result != Plugin_Continue) + { + return false; + } + + if (!validTime[client]) + { + PlayTimerFalseEndSound(client); + lastFalseEndTime[client] = GetGameTime(); + TimerStop(client, false); + return false; + } + // End Timer + timerRunning[client] = false; + lastEndTime[client] = GetGameTime(); + PlayTimerEndSound(client); + + if (!IsFakeClient(client)) + { + // Print end timer message + Call_GOKZ_OnTimerEndMessage(client, course, time, teleportsUsed, result); + if (result == Plugin_Continue) + { + PrintEndTimeString(client); + } + } + + // Call Post Forward + Call_GOKZ_OnTimerEnd_Post(client, course, time, teleportsUsed); + + return true; +} + +bool TimerStop(int client, bool playSound = true) +{ + if (!timerRunning[client]) + { + return false; + } + + timerRunning[client] = false; + if (playSound) + { + PlayTimerStopSound(client); + } + + Call_GOKZ_OnTimerStopped(client); + + return true; +} + +void TimerStopAll(bool playSound = true) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client)) + { + TimerStop(client, playSound); + } + } +} + +void PlayTimerStartSound(int client) +{ + if (GetGameTime() - lastStartSoundTime[client] > GOKZ_TIMER_SOUND_COOLDOWN) + { + GOKZ_EmitSoundToClient(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + lastStartSoundTime[client] = GetGameTime(); + } +} + +void InvalidateRun(int client) +{ + if (validTime[client]) + { + validTime[client] = false; + Call_GOKZ_OnRunInvalidated(client); + } +} + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Timer(int client) +{ + timerRunning[client] = false; + currentTime[client] = 0.0; + currentCourse[client] = 0; + lastEndTime[client] = 0.0; + lastFalseEndTime[client] = 0.0; + lastStartSoundTime[client] = 0.0; + lastStartMode[client] = MODE_COUNT; // So it won't equal any mode +} + +void OnPlayerRunCmdPost_Timer(int client) +{ + if (IsPlayerAlive(client) && GetTimerRunning(client) && !GetPaused(client)) + { + currentTime[client] += GetTickInterval(); + } +} + +void OnChangeMovetype_Timer(int client, MoveType newMovetype) +{ + if (!IsValidMovetype(newMovetype)) + { + if (TimerStop(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Noclipped)"); + } + } +} + +void OnTeleportToStart_Timer(int client) +{ + if (GetCurrentMapPrefix() == MapPrefix_KZPro) + { + TimerStop(client, false); + } +} + +void OnClientDisconnect_Timer(int client) +{ + TimerStop(client); +} + +void OnPlayerDeath_Timer(int client) +{ + TimerStop(client); +} + +void OnOptionChanged_Timer(int client, Option option) +{ + if (option == Option_Mode) + { + if (TimerStop(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Changed Mode)"); + } + } +} + +void OnRoundStart_Timer() +{ + TimerStopAll(); +} + + + +// =====[ PRIVATE ]===== + +static bool IsPlayerValidMoveType(int client) +{ + return IsValidMovetype(Movement_GetMovetype(client)); +} + +static bool IsValidMovetype(MoveType movetype) +{ + return movetype == MOVETYPE_WALK + || movetype == MOVETYPE_LADDER + || movetype == MOVETYPE_NONE + || movetype == MOVETYPE_OBSERVER; +} + +static bool JustTeleported(int client) +{ + return gB_OriginTeleported[client] || gB_VelocityTeleported[client] + || gI_CmdNum[client] - gI_TeleportCmdNum[client] <= GOKZ_TIMER_START_GROUND_TICKS; +} + +static bool JustLanded(int client) +{ + return !gB_OldOnGround[client] + || gI_CmdNum[client] - Movement_GetLandingCmdNum(client) <= GOKZ_TIMER_START_NO_TELEPORT_TICKS; +} + +static bool JustStartedTimer(int client) +{ + return timerRunning[client] && GetCurrentTime(client) < EPSILON; +} + +static bool JustEndedTimer(int client) +{ + return GetGameTime() - lastEndTime[client] < 1.0; +} + +static void PlayTimerEndSound(int client) +{ + GOKZ_EmitSoundToClient(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); + GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); +} + +static void PlayTimerFalseEndSound(int client) +{ + if (!JustEndedTimer(client) + && (GetGameTime() - lastFalseEndTime[client]) > GOKZ_TIMER_SOUND_COOLDOWN) + { + GOKZ_EmitSoundToClient(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End"); + GOKZ_EmitSoundToClientSpectators(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End"); + } +} + +static void PlayTimerStopSound(int client) +{ + GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop"); + GOKZ_EmitSoundToClientSpectators(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop"); +} + +static void PrintEndTimeString(int client) +{ + if (GetCurrentCourse(client) == 0) + { + switch (GetCurrentTimeType(client)) + { + case TimeType_Nub: + { + GOKZ_PrintToChatAll(true, "%t", "Beat Map (NUB)", + client, + GOKZ_FormatTime(GetCurrentTime(client)), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + case TimeType_Pro: + { + GOKZ_PrintToChatAll(true, "%t", "Beat Map (PRO)", + client, + GOKZ_FormatTime(GetCurrentTime(client)), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + } + } + else + { + switch (GetCurrentTimeType(client)) + { + case TimeType_Nub: + { + GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (NUB)", + client, + currentCourse[client], + GOKZ_FormatTime(GetCurrentTime(client)), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + case TimeType_Pro: + { + GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (PRO)", + client, + currentCourse[client], + GOKZ_FormatTime(GetCurrentTime(client)), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp new file mode 100644 index 0000000..aa88a9d --- /dev/null +++ b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp @@ -0,0 +1,322 @@ +/* + Most commonly referred to in the KZ community as timer tech. + Lets players press 'virtual' start and end buttons without looking. +*/ + + + +static int beamSprite; +static int haloSprite; +static float lastUsePressTime[MAXPLAYERS + 1]; +static int lastTeleportTick[MAXPLAYERS + 1]; +static bool startedTimerLastTick[MAXPLAYERS + 1]; +static bool onlyNaturalButtonPressed[MAXPLAYERS + 1]; +static int startTimerButtonPressTick[MAXPLAYERS + 1]; +static bool hasEndedTimerSincePressingUse[MAXPLAYERS + 1]; +static bool hasTeleportedSincePressingUse[MAXPLAYERS + 1]; +static bool hasVirtualStartButton[MAXPLAYERS + 1]; +static bool hasVirtualEndButton[MAXPLAYERS + 1]; +static bool wasInEndZone[MAXPLAYERS + 1]; +static float virtualStartOrigin[MAXPLAYERS + 1][3]; +static float virtualEndOrigin[MAXPLAYERS + 1][3]; +static int virtualStartCourse[MAXPLAYERS + 1]; +static int virtualEndCourse[MAXPLAYERS + 1]; +static bool virtualButtonsLocked[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +bool GetHasVirtualStartButton(int client) +{ + return hasVirtualStartButton[client]; +} + +bool GetHasVirtualEndButton(int client) +{ + return hasVirtualEndButton[client]; +} + +bool ToggleVirtualButtonsLock(int client) +{ + virtualButtonsLocked[client] = !virtualButtonsLocked[client]; + return virtualButtonsLocked[client]; +} + +void LockVirtualButtons(int client) +{ + virtualButtonsLocked[client] = true; +} + +int GetVirtualButtonPosition(int client, float position[3], bool isStart) +{ + if (isStart && hasVirtualStartButton[client]) + { + position = virtualStartOrigin[client]; + return virtualStartCourse[client]; + } + else if (!isStart && hasVirtualEndButton[client]) + { + position = virtualEndOrigin[client]; + return virtualEndCourse[client]; + } + + return -1; +} + +void SetVirtualButtonPosition(int client, float position[3], int course, bool isStart) +{ + if (isStart) + { + virtualStartCourse[client] = course; + virtualStartOrigin[client] = position; + hasVirtualStartButton[client] = true; + } + else + { + virtualEndCourse[client] = course; + virtualEndOrigin[client] = position; + hasVirtualEndButton[client] = true; + } +} + +void ResetVirtualButtonPosition(int client, bool isStart) +{ + if (isStart) + { + virtualStartCourse[client] = -1; + virtualStartOrigin[client] = {0.0, 0.0, 0.0}; + hasVirtualStartButton[client] = false; + } + else + { + virtualEndCourse[client] = -1; + virtualEndOrigin[client] = {0.0, 0.0, 0.0}; + hasVirtualEndButton[client] = false; + } +} + +// =====[ EVENTS ]===== + +void OnMapStart_VirtualButtons() +{ + beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt"); + haloSprite = PrecacheModel("materials/sprites/glow01.vmt"); +} + +void OnClientPutInServer_VirtualButtons(int client) +{ + startedTimerLastTick[client] = false; + hasVirtualEndButton[client] = false; + hasVirtualStartButton[client] = false; + virtualButtonsLocked[client] = false; + onlyNaturalButtonPressed[client] = false; + wasInEndZone[client] = false; + startTimerButtonPressTick[client] = 0; +} + +void OnStartButtonPress_VirtualButtons(int client, int course) +{ + if (!virtualButtonsLocked[client] && + lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount()) + { + Movement_GetOrigin(client, virtualStartOrigin[client]); + virtualStartCourse[client] = course; + hasVirtualStartButton[client] = true; + startTimerButtonPressTick[client] = GetGameTickCount(); + } +} + +void OnEndButtonPress_VirtualButtons(int client, int course) +{ + // Prevent setting end virtual button to where it would usually be unreachable + if (IsPlayerStuck(client)) + { + return; + } + + if (!virtualButtonsLocked[client] && + lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount()) + { + Movement_GetOrigin(client, virtualEndOrigin[client]); + virtualEndCourse[client] = course; + hasVirtualEndButton[client] = true; + } +} + +void OnPlayerRunCmdPost_VirtualButtons(int client, int buttons, int cmdnum) +{ + CheckForAndHandleUsage(client, buttons); + UpdateIndicators(client, cmdnum); +} + +void OnCountedTeleport_VirtualButtons(int client) +{ + hasTeleportedSincePressingUse[client] = true; +} + +void OnTeleport_DelayVirtualButtons(int client) +{ + lastTeleportTick[client] = GetGameTickCount(); +} + + + +// =====[ PRIVATE ]===== + +static void CheckForAndHandleUsage(int client, int buttons) +{ + if (buttons & IN_USE && !(gI_OldButtons[client] & IN_USE)) + { + lastUsePressTime[client] = GetGameTime(); + hasEndedTimerSincePressingUse[client] = false; + hasTeleportedSincePressingUse[client] = false; + onlyNaturalButtonPressed[client] = startTimerButtonPressTick[client] == GetGameTickCount(); + } + + bool useCheck = PassesUseCheck(client); + + // Start button + if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) == TimerButtonZoneType_BothZones) + && GetHasVirtualStartButton(client) && InRangeOfVirtualStart(client) && CanReachVirtualStart(client)) + { + if (TimerStart(client, virtualStartCourse[client], .playSound = false)) + { + startedTimerLastTick[client] = true; + OnVirtualStartButtonPress_Teleports(client); + } + } + else if (startedTimerLastTick[client]) + { + // Without that check you get two sounds when pressing the natural timer button + if (!onlyNaturalButtonPressed[client]) + { + PlayTimerStartSound(client); + } + onlyNaturalButtonPressed[client] = false; + startedTimerLastTick[client] = false; + } + + // End button + if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) != TimerButtonZoneType_BothButtons) + && GetHasVirtualEndButton(client) && InRangeOfVirtualEnd(client) && CanReachVirtualEnd(client)) + { + if (!wasInEndZone[client]) + { + TimerEnd(client, virtualEndCourse[client]); + hasEndedTimerSincePressingUse[client] = true; // False end counts as well + wasInEndZone[client] = true; + } + } + else + { + wasInEndZone[client] = false; + } +} + +static bool PassesUseCheck(int client) +{ + if (GetGameTime() - lastUsePressTime[client] < GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME + EPSILON + && !hasEndedTimerSincePressingUse[client] + && !hasTeleportedSincePressingUse[client]) + { + return true; + } + + return false; +} + +bool InRangeOfVirtualStart(int client) +{ + return InRangeOfButton(client, virtualStartOrigin[client]); +} + +static bool InRangeOfVirtualEnd(int client) +{ + return InRangeOfButton(client, virtualEndOrigin[client]); +} + +static bool InRangeOfButton(int client, const float buttonOrigin[3]) +{ + float origin[3]; + Movement_GetOrigin(client, origin); + float distanceToButton = GetVectorDistance(origin, buttonOrigin); + return distanceToButton <= gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)]; +} + +bool CanReachVirtualStart(int client) +{ + return CanReachButton(client, virtualStartOrigin[client]); +} + +static bool CanReachVirtualEnd(int client) +{ + return CanReachButton(client, virtualEndOrigin[client]); +} + +static bool CanReachButton(int client, const float buttonOrigin[3]) +{ + float origin[3]; + Movement_GetOrigin(client, origin); + Handle trace = TR_TraceRayFilterEx(origin, buttonOrigin, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers); + bool didHit = TR_DidHit(trace); + delete trace; + return !didHit; +} + + + +// ===== [ INDICATOR ] ===== + +static void UpdateIndicators(int client, int cmdnum) +{ + if (cmdnum % 128 != 0 || !IsPlayerAlive(client) + || GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled) + { + return; + } + + if (hasVirtualStartButton[client]) + { + DrawIndicator(client, virtualStartOrigin[client], { 0, 255, 0, 255 } ); + } + + if (hasVirtualEndButton[client]) + { + DrawIndicator(client, virtualEndOrigin[client], { 255, 0, 0, 255 } ); + } +} + +static void DrawIndicator(int client, const float origin[3], const int colour[4]) +{ + float radius = gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)]; + if (radius <= EPSILON) // Don't draw circle of radius 0 + { + return; + } + + float x, y, start[3], end[3]; + + // Create the start position for the first part of the beam + start[0] = origin[0] + radius; + start[1] = origin[1]; + start[2] = origin[2]; + + for (int i = 1; i <= 31; i++) // Circle is broken into 31 segments + { + float angle = 2 * PI / 31 * i; + x = radius * Cosine(angle); + y = radius * Sine(angle); + + end[0] = origin[0] + x; + end[1] = origin[1] + y; + end[2] = origin[2]; + + TE_SetupBeamPoints(start, end, beamSprite, haloSprite, 0, 0, 0.97, 0.2, 0.2, 0, 0.0, colour, 0); + TE_SendToClient(client); + + start[0] = end[0]; + start[1] = end[1]; + start[2] = end[2]; + } +} diff --git a/sourcemod/scripting/gokz-core/triggerfix.sp b/sourcemod/scripting/gokz-core/triggerfix.sp new file mode 100644 index 0000000..424928d --- /dev/null +++ b/sourcemod/scripting/gokz-core/triggerfix.sp @@ -0,0 +1,622 @@ + + +// Credits: +// RNGFix made by rio https://github.com/jason-e/rngfix + + +// Engine constants, NOT settings (do not change) +#define LAND_HEIGHT 2.0 // Maximum height above ground at which you can "land" +#define MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface + +static int processMovementTicks[MAXPLAYERS+1]; +static float playerFrameTime[MAXPLAYERS+1]; + +static bool touchingTrigger[MAXPLAYERS+1][2048]; +static int triggerTouchFired[MAXPLAYERS+1][2048]; +static int lastGroundEnt[MAXPLAYERS + 1]; +static bool duckedLastTick[MAXPLAYERS + 1]; +static bool mapTeleportedSequentialTicks[MAXPLAYERS+1]; +static bool jumpBugged[MAXPLAYERS + 1]; +static float jumpBugOrigin[MAXPLAYERS + 1][3]; + +static ConVar cvGravity; + +static Handle acceptInputHookPre; +static Handle processMovementHookPre; +static Address serverGameEnts; +static Handle markEntitiesAsTouching; +static Handle passesTriggerFilters; + +public void OnPluginStart_Triggerfix() +{ + HookEvent("player_jump", Event_PlayerJump); + + cvGravity = FindConVar("sv_gravity"); + if (cvGravity == null) + { + SetFailState("Could not find sv_gravity"); + } + + GameData gamedataConf = LoadGameConfigFile("gokz-core.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-core gamedata"); + } + + // PassesTriggerFilters + StartPrepSDKCall(SDKCall_Entity); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "CBaseTrigger::PassesTriggerFilters")) + { + SetFailState("Failed to get CBaseTrigger::PassesTriggerFilters offset"); + } + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); + passesTriggerFilters = EndPrepSDKCall(); + + if (passesTriggerFilters == null) SetFailState("Unable to prepare SDKCall for CBaseTrigger::PassesTriggerFilters"); + + // CreateInterface + // Thanks SlidyBat and ici + StartPrepSDKCall(SDKCall_Static); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface")) + { + SetFailState("Failed to get CreateInterface"); + } + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + Handle CreateInterface = EndPrepSDKCall(); + + if (CreateInterface == null) + { + SetFailState("Unable to prepare SDKCall for CreateInterface"); + } + + char interfaceName[64]; + + // ProcessMovement + if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IGameMovement interface name"); + } + Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0); + if (!IGameMovement) + { + SetFailState("Failed to get IGameMovement pointer"); + } + + int offset = GameConfGetOffset(gamedataConf, "ProcessMovement"); + if (offset == -1) + { + SetFailState("Failed to get ProcessMovement offset"); + } + + processMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre); + DHookAddParam(processMovementHookPre, HookParamType_CBaseEntity); + DHookAddParam(processMovementHookPre, HookParamType_ObjectPtr); + DHookRaw(processMovementHookPre, false, IGameMovement); + + // MarkEntitiesAsTouching + if (!GameConfGetKeyValue(gamedataConf, "IServerGameEnts", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IServerGameEnts interface name"); + } + serverGameEnts = SDKCall(CreateInterface, interfaceName, 0); + if (!serverGameEnts) + { + SetFailState("Failed to get IServerGameEnts pointer"); + } + + StartPrepSDKCall(SDKCall_Raw); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "IServerGameEnts::MarkEntitiesAsTouching")) + { + SetFailState("Failed to get IServerGameEnts::MarkEntitiesAsTouching offset"); + } + PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer); + markEntitiesAsTouching = EndPrepSDKCall(); + + if (markEntitiesAsTouching == null) + { + SetFailState("Unable to prepare SDKCall for IServerGameEnts::MarkEntitiesAsTouching"); + } + + gamedataConf = LoadGameConfigFile("sdktools.games/engine.csgo"); + offset = gamedataConf.GetOffset("AcceptInput"); + if (offset == -1) + { + SetFailState("Failed to get AcceptInput offset"); + } + + acceptInputHookPre = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput); + DHookAddParam(acceptInputHookPre, HookParamType_CharPtr); + DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity); + DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity); + //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20 + DHookAddParam(acceptInputHookPre, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP); + DHookAddParam(acceptInputHookPre, HookParamType_Int); + + delete CreateInterface; + delete gamedataConf; + + if (gB_LateLoad) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) OnClientPutInServer(client); + } + + char classname[64]; + for (int entity = MaxClients+1; entity < sizeof(touchingTrigger[]); entity++) + { + if (!IsValidEntity(entity)) continue; + GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname)); + HookTrigger(entity, classname); + } + } +} + +public void OnEntityCreated_Triggerfix(int entity, const char[] classname) +{ + if (entity >= sizeof(touchingTrigger[])) + { + return; + } + HookTrigger(entity, classname); +} + +public void OnClientConnected_Triggerfix(int client) +{ + processMovementTicks[client] = 0; + for (int i = 0; i < sizeof(touchingTrigger[]); i++) + { + touchingTrigger[client][i] = false; + triggerTouchFired[client][i] = 0; + } +} + +public void OnClientPutInServer_Triggerfix(int client) +{ + SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink); + DHookEntity(acceptInputHookPre, false, client); +} + +public void OnGameFrame_Triggerfix() +{ + // Loop through all the players and make sure that triggers that are supposed to be fired but weren't now + // get fired properly. + // This must be run OUTSIDE of usercmd, because sometimes usercmd gets delayed heavily. + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && IsPlayerAlive(client) && !CheckWater(client) && + (GetEntityMoveType(client) == MOVETYPE_WALK || GetEntityMoveType(client) == MOVETYPE_LADDER)) + { + DoTriggerFix(client); + + // Reset the Touch tracking. + // We save a bit of performance by putting this inside the loop + // Even if triggerTouchFired is not correct, touchingTrigger still is. + // That should prevent DoTriggerFix from activating the wrong triggers. + // Plus, players respawn where they previously are as well with a timer on, + // so this should not be a big problem. + for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++) + { + triggerTouchFired[client][trigger] = 0; + } + } + } +} + +void OnPlayerRunCmd_Triggerfix(int client) +{ + // Reset the Touch tracking. + // While this is mostly unnecessary, it can also happen that the server runs multiple ticks of player movement at once, + // therefore the triggers need to be checked again. + for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++) + { + triggerTouchFired[client][trigger] = 0; + } +} + +static void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + jumpBugged[client] = !!lastGroundEnt[client]; + if (jumpBugged[client]) + { + GetClientAbsOrigin(client, jumpBugOrigin[client]); + // if player's origin is still in the ducking position then adjust for that. + if (duckedLastTick[client] && !Movement_GetDucking(client)) + { + jumpBugOrigin[client][2] -= 9.0; + } + } +} + +static Action Hook_TriggerStartTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + touchingTrigger[other][entity] = true; + } + + return Plugin_Continue; +} + +static Action Hook_TriggerEndTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + touchingTrigger[other][entity] = false; + } + return Plugin_Continue; +} + +static Action Hook_TriggerTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + triggerTouchFired[other][entity]++; + } + return Plugin_Continue; +} + +static MRESReturn DHook_ProcessMovementPre(Handle hParams) +{ + int client = DHookGetParam(hParams, 1); + + processMovementTicks[client]++; + playerFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue"); + mapTeleportedSequentialTicks[client] = false; + + if (IsPlayerAlive(client)) + { + if (GetEntityMoveType(client) == MOVETYPE_WALK + && !CheckWater(client)) + { + lastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity"); + } + duckedLastTick[client] = Movement_GetDucking(client); + } + + return MRES_Ignored; +} + +static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || CheckWater(client) || + (GetEntityMoveType(client) != MOVETYPE_WALK && GetEntityMoveType(client) != MOVETYPE_LADDER)) + { + return MRES_Ignored; + } + + // Get args + static char param[64]; + static char command[64]; + DHookGetParamString(hParams, 1, command, sizeof(command)); + if (StrEqual(command, "AddOutput")) + { + DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param)); + char kv[16]; + SplitString(param, " ", kv, sizeof(kv)); + // KVs are case insensitive. + // Any of these inputs can change the filter behavior. + if (StrEqual(kv[0], "targetname", false) || StrEqual(kv[0], "teamnumber", false) || StrEqual(kv[0], "classname", false) || StrEqual(command, "ResponseContext", false)) + { + DoTriggerFix(client, true); + } + } + else if (StrEqual(command, "AddContext") || StrEqual(command, "RemoveContext") || StrEqual(command, "ClearContext")) + { + DoTriggerFix(client, true); + } + return MRES_Ignored; +} + +static bool DoTriggerFix(int client, bool filterFix = false) +{ + // Adapted from DoTriggerjumpFix right below. + float landingMins[3], landingMaxs[3]; + float origin[3]; + + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs); + + ArrayList triggers = new ArrayList(); + // Get a list of triggers that we are touching now. + + TR_EnumerateEntitiesHull(origin, origin, landingMins, landingMaxs, true, AddTrigger, triggers); + + bool didSomething = false; + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + if (!touchingTrigger[client][trigger]) + { + // Normally this wouldn't happen, because the trigger should be colliding with the player's hull if it gets here. + continue; + } + char className[64]; + GetEntityClassname(trigger, className, sizeof(className)); + if (StrEqual(className, "trigger_push")) + { + // Completely ignore push triggers. + continue; + } + if (filterFix && SDKCall(passesTriggerFilters, trigger, client) && triggerTouchFired[client][trigger] < GOKZ_MAX_RETOUCH_TRIGGER_COUNT) + { + // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick. + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + + // Player properties might be changed right after this so it will need to be triggered again. + // Triggers changing this filter will loop onto itself infintely so we need to avoid that. + triggerTouchFired[client][trigger]++; + didSomething = true; + } + else if (!triggerTouchFired[client][trigger]) + { + // If the player is still touching the trigger on this tick, and Touch was not called for whatever reason + // in the last tick, we make sure that it is called now. + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + triggerTouchFired[client][trigger]++; + didSomething = true; + } + } + + delete triggers; + + return didSomething; +} + +static bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3]) +{ + // It's possible to land above a trigger but also in another trigger_teleport, have the teleport move you to + // another location, and then the trigger jumping fix wouldn't fire the other trigger you technically landed above, + // but I can't imagine a mapper would ever actually stack triggers like that. + + float origin[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + float landingMaxsBelow[3]; + landingMaxsBelow[0] = landingMaxs[0]; + landingMaxsBelow[1] = landingMaxs[1]; + landingMaxsBelow[2] = origin[2] - landingPoint[2]; + + ArrayList triggers = new ArrayList(); + + // Find triggers that are between us and the ground (using the bounding box quadrant we landed with if applicable). + // This will fail on triggers thinner than 0.03125 unit thick, but it's highly unlikely that a mapper would put a trigger that thin. + TR_EnumerateEntitiesHull(landingPoint, landingPoint, landingMins, landingMaxsBelow, true, AddTrigger, triggers); + + bool didSomething = false; + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + + // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick. + // In case that could cause side-effects, manually keep track of triggers we are actually touching + // and don't re-touch them. + if (touchingTrigger[client][trigger]) + { + continue; + } + + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + didSomething = true; + } + + delete triggers; + + return didSomething; +} + +// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation) +static void Hook_PlayerPostThink(int client) +{ + if (!IsPlayerAlive(client) + || GetEntityMoveType(client) != MOVETYPE_WALK + || CheckWater(client)) + { + return; + } + + bool landed = (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1 + && lastGroundEnt[client] == -1) + || jumpBugged[client]; + + float landingMins[3], landingMaxs[3], landingPoint[3]; + + // Get info about the ground we landed on (if we need to do landing fixes). + if (landed) + { + float origin[3], nrm[3], velocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + GetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity); + + if (jumpBugged[client]) + { + origin = jumpBugOrigin[client]; + } + + GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs); + + float originBelow[3]; + originBelow[0] = origin[0]; + originBelow[1] = origin[1]; + originBelow[2] = origin[2] - LAND_HEIGHT; + + TR_TraceHullFilter(origin, originBelow, landingMins, landingMaxs, MASK_PLAYERSOLID, PlayerFilter); + + if (!TR_DidHit()) + { + // This should never happen, since we know we are on the ground. + landed = false; + } + else + { + TR_GetPlaneNormal(null, nrm); + + if (nrm[2] < MIN_STANDABLE_ZNRM) + { + // This is rare, and how the incline fix should behave isn't entirely clear because maybe we should + // collide with multiple faces at once in this case, but let's just get the ground we officially + // landed on and use that for our ground normal. + + // landingMins and landingMaxs will contain the final values used to find the ground after returning. + if (TracePlayerBBoxForGround(origin, originBelow, landingMins, landingMaxs)) + { + TR_GetPlaneNormal(null, nrm); + } + else + { + // This should also never happen. + landed = false; + } + } + + TR_GetEndPosition(landingPoint); + } + } + + // reset it here because we don't need it again + jumpBugged[client] = false; + + // Must use TR_DidHit because if the unduck origin is closer than 0.03125 units from the ground, + // the trace fraction would return 0.0. + if (landed && TR_DidHit()) + { + DoTriggerjumpFix(client, landingPoint, landingMins, landingMaxs); + // Check if a trigger we just touched put us in the air (probably due to a teleport). + if (GetEntityFlags(client) & FL_ONGROUND == 0) + { + landed = false; + } + } +} + +static bool PlayerFilter(int entity, int mask) +{ + return !(1 <= entity <= MaxClients); +} + +static void HookTrigger(int entity, const char[] classname) +{ + if (StrContains(classname, "trigger_") != -1) + { + SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch); + SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch); + SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTouch); + } +} + +static bool CheckWater(int client) +{ + // The cached water level is updated multiple times per tick, including after movement happens, + // so we can just check the cached value here. + return GetEntProp(client, Prop_Data, "m_nWaterLevel") > 1; +} + +public bool AddTrigger(int entity, ArrayList triggers) +{ + TR_ClipCurrentRayToEntity(MASK_ALL, entity); + if (TR_DidHit()) + { + triggers.Push(entity); + } + + return true; +} + +static bool TracePlayerBBoxForGround(const float origin[3], const float originBelow[3], float mins[3], float maxs[3]) +{ + // See CGameMovement::TracePlayerBBoxForGround() + + float origMins[3], origMaxs[3]; + origMins = mins; + origMaxs = maxs; + + float nrm[3]; + + mins = origMins; + + // -x -y + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) + { + return true; + } + } + + // +x +y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs = origMaxs; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) + { + return true; + } + } + + // -x +y + mins[0] = origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) + { + return true; + } + } + + // +x -y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) + { + return true; + } + } + + return false; +} diff --git a/sourcemod/scripting/gokz-errorboxfixer.sp b/sourcemod/scripting/gokz-errorboxfixer.sp new file mode 100644 index 0000000..fd3f76a --- /dev/null +++ b/sourcemod/scripting/gokz-errorboxfixer.sp @@ -0,0 +1,89 @@ +#include <sourcemod> +#include <sdktools> + +#include <gokz> + +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + +public Plugin myinfo = +{ + name = "GOKZ KZErrorBoxFixer", + author = "1NutWunDeR", + description = "Adds missing models for KZ maps", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-errorboxfixer.txt" + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnMapStart() +{ + AddFileToDownloadsTable("models/kzmod/buttons/stand_button.vtf"); + AddFileToDownloadsTable("models/kzmod/buttons/stand_button.vmt"); + AddFileToDownloadsTable("models/kzmod/buttons/stand_button_normal.vtf"); + AddFileToDownloadsTable("models/kzmod/buttons/standing_button.mdl"); + AddFileToDownloadsTable("models/kzmod/buttons/standing_button.dx90.vtx"); + AddFileToDownloadsTable("models/kzmod/buttons/standing_button.phy"); + AddFileToDownloadsTable("models/kzmod/buttons/standing_button.vvd"); + AddFileToDownloadsTable("models/kzmod/buttons/stone_button.mdl"); + AddFileToDownloadsTable("models/kzmod/buttons/stone_button.dx90.vtx"); + AddFileToDownloadsTable("models/kzmod/buttons/stone_button.phy"); + AddFileToDownloadsTable("models/kzmod/buttons/stone_button.vvd"); + AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.mdl"); + AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.dx90.vtx"); + AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.phy"); + AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.vvd"); + AddFileToDownloadsTable("materials/kzmod/starttimersign.vmt"); + AddFileToDownloadsTable("materials/kzmod/starttimersign.vtf"); + AddFileToDownloadsTable("materials/kzmod/stoptimersign.vmt"); + AddFileToDownloadsTable("materials/kzmod/stoptimersign.vtf"); + AddFileToDownloadsTable("models/props/switch001.mdl"); + AddFileToDownloadsTable("models/props/switch001.vvd"); + AddFileToDownloadsTable("models/props/switch001.phy"); + AddFileToDownloadsTable("models/props/switch001.vtx"); + AddFileToDownloadsTable("models/props/switch001.dx90.vtx"); + AddFileToDownloadsTable("materials/models/props/switch.vmt"); + AddFileToDownloadsTable("materials/models/props/switch.vtf"); + AddFileToDownloadsTable("materials/models/props/switch001.vmt"); + AddFileToDownloadsTable("materials/models/props/switch001.vtf"); + AddFileToDownloadsTable("materials/models/props/switch001_normal.vmt"); + AddFileToDownloadsTable("materials/models/props/switch001_normal.vtf"); + AddFileToDownloadsTable("materials/models/props/switch001_lightwarp.vmt"); + AddFileToDownloadsTable("materials/models/props/switch001_lightwarp.vtf"); + AddFileToDownloadsTable("materials/models/props/switch001_exponent.vmt"); + AddFileToDownloadsTable("materials/models/props/switch001_exponent.vtf"); + AddFileToDownloadsTable("materials/models/props/startkztimer.vmt"); + AddFileToDownloadsTable("materials/models/props/startkztimer.vtf"); + AddFileToDownloadsTable("materials/models/props/stopkztimer.vmt"); + AddFileToDownloadsTable("materials/models/props/stopkztimer.vtf"); + + PrecacheModel("models/kzmod/buttons/stand_button.vmt", true); + PrecacheModel("models/props_wasteland/pipecluster002a.mdl", true); + PrecacheModel("models/kzmod/buttons/standing_button.mdl", true); + PrecacheModel("models/kzmod/buttons/stone_button.mdl", true); + PrecacheModel("materials/kzmod/starttimersign.vmt", true); + PrecacheModel("materials/kzmod/stoptimersign.vmt", true); + PrecacheModel("models/props/switch001.mdl", true); + PrecacheModel("materials/models/props/startkztimer.vmt", true); + PrecacheModel("materials/models/props/stopkztimer.vmt", true); +} diff --git a/sourcemod/scripting/gokz-global.sp b/sourcemod/scripting/gokz-global.sp new file mode 100644 index 0000000..79f8b6a --- /dev/null +++ b/sourcemod/scripting/gokz-global.sp @@ -0,0 +1,740 @@ +#include <sourcemod> + +#include <sdktools> + +#include <GlobalAPI> +#include <gokz/anticheat> +#include <gokz/core> +#include <gokz/global> +#include <gokz/replays> +#include <gokz/momsurffix> + +#include <autoexecconfig> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/localdb> +#include <gokz/localranks> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Global", + author = "DanZay", + description = "Provides centralised records and bans via GlobalAPI", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-global.txt" + +bool gB_GOKZLocalDB; + +bool gB_APIKeyCheck; +bool gB_ModeCheck[MODE_COUNT]; +bool gB_BannedCommandsCheck; +char gC_CurrentMap[64]; +int gI_CurrentMapFileSize; +bool gB_InValidRun[MAXPLAYERS + 1]; +bool gB_GloballyVerified[MAXPLAYERS + 1]; +bool gB_EnforcerOnFreshMap; +bool gB_JustLateLoaded; +int gI_FPSMax[MAXPLAYERS + 1]; +bool gB_waitingForFPSKick[MAXPLAYERS + 1]; +bool gB_MapValidated; +int gI_MapID; +int gI_MapFileSize; +int gI_MapTier; + +ConVar gCV_gokz_settings_enforcer; +ConVar gCV_gokz_warn_for_non_global_map; +ConVar gCV_EnforcedCVar[ENFORCEDCVAR_COUNT]; + +#include "gokz-global/api.sp" +#include "gokz-global/ban_player.sp" +#include "gokz-global/commands.sp" +#include "gokz-global/maptop_menu.sp" +#include "gokz-global/print_records.sp" +#include "gokz-global/send_run.sp" +#include "gokz-global/points.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-global"); + gB_JustLateLoaded = late; + return APLRes_Success; +} + +public void OnPluginStart() +{ + if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON) + { + SetFailState("gokz-global currently only supports 128 tickrate servers."); + } + if (FindCommandLineParam("-insecure") || FindCommandLineParam("-tools")) + { + SetFailState("gokz-global currently only supports VAC-secured servers."); + } + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-global.phrases"); + + gB_APIKeyCheck = false; + gB_MapValidated = false; + gI_MapID = -1; + gI_MapFileSize = -1; + gI_MapTier = -1; + + for (int mode = 0; mode < MODE_COUNT; mode++) + { + gB_ModeCheck[mode] = false; + } + + CreateConVars(); + CreateGlobalForwards(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = LibraryExists("gokz-localdb"); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = gB_GOKZLocalDB || StrEqual(name, "gokz-localdb"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb"); +} + +Action IntegrityChecks(Handle timer) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && !IsFakeClient(client)) + { + QueryClientConVar(client, "fps_max", FPSCheck, client); + QueryClientConVar(client, "m_yaw", MYAWCheck, client); + } + } + + for (int i = 0; i < BANNEDPLUGINCOMMAND_COUNT; i++) + { + if (CommandExists(gC_BannedPluginCommands[i])) + { + Handle bannedIterator = GetPluginIterator(); + char pluginName[128]; + bool foundPlugin = false; + while (MorePlugins(bannedIterator)) + { + Handle bannedPlugin = ReadPlugin(bannedIterator); + GetPluginInfo(bannedPlugin, PlInfo_Name, pluginName, sizeof(pluginName)); + if (StrEqual(pluginName, gC_BannedPlugins[i])) + { + char pluginPath[128]; + GetPluginFilename(bannedPlugin, pluginPath, sizeof(pluginPath)); + ServerCommand("sm plugins unload %s", pluginPath); + char disabledPath[256], enabledPath[256], pluginFile[4][128]; + int subfolders = ExplodeString(pluginPath, "/", pluginFile, sizeof(pluginFile), sizeof(pluginFile[])); + BuildPath(Path_SM, disabledPath, sizeof(disabledPath), "plugins/disabled/%s", pluginFile[subfolders - 1]); + BuildPath(Path_SM, enabledPath, sizeof(enabledPath), "plugins/%s", pluginPath); + RenameFile(disabledPath, enabledPath); + LogError("[KZ] %s cannot be loaded at the same time as gokz-global. %s has been disabled.", pluginName, pluginName); + delete bannedPlugin; + foundPlugin = true; + break; + } + delete bannedPlugin; + } + if (!foundPlugin && gB_BannedCommandsCheck) + { + gB_BannedCommandsCheck = false; + LogError("You can't have a plugin which implements the %s command. Please disable it and reload the map.", gC_BannedPluginCommands[i]); + } + delete bannedIterator; + } + } + + return Plugin_Handled; +} + +public void FPSCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + gI_FPSMax[client] = StringToInt(cvarValue); + if (gI_FPSMax[client] > 0 && gI_FPSMax[client] < GL_FPS_MAX_MIN_VALUE) + { + if (!gB_waitingForFPSKick[client]) + { + gB_waitingForFPSKick[client] = true; + CreateTimer(GL_FPS_MAX_KICK_TIMEOUT, FPSKickPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + GOKZ_PrintToChat(client, true, "%t", "Warn Player fps_max"); + if (GOKZ_GetTimerRunning(client)) + { + GOKZ_StopTimer(client, true); + } + else + { + GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop"); + } + } + } + else + { + gB_waitingForFPSKick[client] = false; + } + } +} + +public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client) && StringToFloat(cvarValue) > GL_MYAW_MAX_VALUE) + { + KickClient(client, "%T", "Kick Player m_yaw", client); + } +} + +Action FPSKickPlayer(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client) && !IsFakeClient(client) && gB_waitingForFPSKick[client]) + { + KickClient(client, "%T", "Kick Player fps_max", client); + } + + return Plugin_Handled; +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + gB_GloballyVerified[client] = false; + gB_waitingForFPSKick[client] = false; + OnClientPutInServer_PrintRecords(client); +} + +// OnClientAuthorized is apparently too early +public void OnClientPostAdminCheck(int client) +{ + ResetPoints(client); + + if (GlobalAPI_IsInit() && !IsFakeClient(client)) + { + CheckClientGlobalBan(client); + UpdatePoints(client); + } +} + +public void GlobalAPI_OnInitialized() +{ + SetupAPI(); +} + + +public Action GOKZ_OnTimerStart(int client, int course) +{ + KZPlayer player = KZPlayer(client); + int mode = player.Mode; + + // We check the timer running to prevent spam when standing inside VB. + if (gCV_gokz_warn_for_non_global_map.BoolValue + && GlobalAPI_HasAPIKey() + && !GlobalsEnabled(mode) + && !GOKZ_GetTimerRunning(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Warn Player Not Global Run"); + } + + return Plugin_Continue; +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + KZPlayer player = KZPlayer(client); + int mode = player.Mode; + gB_InValidRun[client] = GlobalsEnabled(mode); +} + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + if (gB_GloballyVerified[client] && gB_InValidRun[client]) + { + SendTime(client, course, time, teleportsUsed); + } +} + +public Action GOKZ_RP_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay) +{ + if (gB_GloballyVerified[client] && gB_InValidRun[client]) + { + OnReplaySaved_SendReplay(client, replayType, map, course, timeType, time, filePath, tempReplay); + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void GOKZ_OnRunInvalidated(int client) +{ + gB_InValidRun[client] = false; +} + +public void GOKZ_GL_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float runTime) +{ + AnnounceNewTopTime(client, course, mode, timeType, rank, rankOverall); +} + +public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + if (!gB_GloballyVerified[client]) + { + return; + } + + GlobalBanPlayer(client, reason, notes, stats); + gB_GloballyVerified[client] = false; +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + LoadSounds(); + + GetCurrentMapDisplayName(gC_CurrentMap, sizeof(gC_CurrentMap)); + gI_CurrentMapFileSize = GetCurrentMapFileSize(); + + gB_BannedCommandsCheck = true; + + // Prevent just reloading the plugin after messing with the map + if (gB_JustLateLoaded) + { + gB_JustLateLoaded = false; + } + else + { + gB_EnforcerOnFreshMap = true; + } + + // In case of late loading + if (GlobalAPI_IsInit()) + { + GlobalAPI_OnInitialized(); + } + + // Setup a timer to monitor server/client integrity + CreateTimer(1.0, IntegrityChecks, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); +} + +public void OnMapEnd() +{ + // So it doesn't get carried over to the next map + gI_MapID = -1; + for (int client = 1; client < MaxClients; client++) + { + ResetMapPoints(client); + } +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) + && GlobalAPI_IsInit()) + { + UpdatePoints(client); + } +} + +public void GOKZ_OnModeUnloaded(int mode) +{ + gB_ModeCheck[mode] = false; +} + +public Action GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client) +{ + char pluginName[64]; + GetPluginInfo(plugin, PlInfo_Name, pluginName, sizeof(pluginName)); + if (GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + LogMessage("Invalidated %N's run as gokz-core native was called by \"%s\"", client, pluginName); + } + GOKZ_InvalidateRun(client); + return Plugin_Continue; +} + + + +// =====[ PUBLIC ]===== + +bool GlobalsEnabled(int mode) +{ + return gB_APIKeyCheck && gB_BannedCommandsCheck && gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap && MapCheck() && gB_ModeCheck[mode]; +} + +bool MapCheck() +{ + return gB_MapValidated + && gI_MapID > 0 + && gI_MapFileSize == gI_CurrentMapFileSize; +} + +void PrintGlobalCheckToChat(int client) +{ + GOKZ_PrintToChat(client, true, "%t", "Global Check Header"); + GOKZ_PrintToChat(client, false, "%t", "Global Check", + gB_APIKeyCheck ? "{green}✓" : "{darkred}X", + gB_BannedCommandsCheck ? "{green}✓" : "{darkred}X", + gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap ? "{green}✓" : "{darkred}X", + MapCheck() ? "{green}✓" : "{darkred}X", + gB_GloballyVerified[client] ? "{green}✓" : "{darkred}X"); + + char modeCheck[256]; + FormatEx(modeCheck, sizeof(modeCheck), "{purple}%s %s", gC_ModeNames[0], gB_ModeCheck[0] ? "{green}✓" : "{darkred}X"); + for (int i = 1; i < MODE_COUNT; i++) + { + FormatEx(modeCheck, sizeof(modeCheck), "%s {grey}| {purple}%s %s", modeCheck, gC_ModeNames[i], gB_ModeCheck[i] ? "{green}✓" : "{darkred}X"); + } + GOKZ_PrintToChat(client, false, "%s", modeCheck); +} + +void AnnounceNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall) +{ + bool newRecord = false; + + if (timeType == TimeType_Nub && rankOverall != 0) + { + if (rankOverall == 1) + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Record (NUB)", client, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]); + } + newRecord = true; + } + else + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (NUB)", client, rankOverall, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (NUB)", client, rankOverall, course, gC_ModeNamesShort[mode]); + } + } + } + else if (timeType == TimeType_Pro) + { + if (rankOverall != 0) + { + if (rankOverall == 1) + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Record (NUB)", client, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]); + } + newRecord = true; + } + else + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (NUB)", client, rankOverall, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (NUB)", client, rankOverall, course, gC_ModeNamesShort[mode]); + } + } + } + + if (rank == 1) + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Record (PRO)", client, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (PRO)", client, course, gC_ModeNamesShort[mode]); + } + newRecord = true; + } + else + { + if (course == 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (PRO)", client, rank, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (PRO)", client, rank, course, gC_ModeNamesShort[mode]); + } + } + } + + if (newRecord) + { + PlayBeatRecordSound(); + } +} + +void PlayBeatRecordSound() +{ + GOKZ_EmitSoundToAll(GL_SOUND_NEW_RECORD, _, "World Record"); +} + + + +// =====[ PRIVATE ]===== + +static void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-global", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_settings_enforcer = AutoExecConfig_CreateConVar("gokz_settings_enforcer", "1", "Whether GOKZ enforces convars required for global records.", _, true, 0.0, true, 1.0); + gCV_gokz_warn_for_non_global_map = AutoExecConfig_CreateConVar("gokz_warn_for_non_global_map", "1", "Whether or not GOKZ should warn players if the global check does not pass.", _, true, 0.0, true, 1.0); + gCV_gokz_settings_enforcer.AddChangeHook(OnConVarChanged); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + for (int i = 0; i < ENFORCEDCVAR_COUNT; i++) + { + gCV_EnforcedCVar[i] = FindConVar(gC_EnforcedCVars[i]); + gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i]; + gCV_EnforcedCVar[i].AddChangeHook(OnEnforcedConVarChanged); + } +} + +public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (convar == gCV_gokz_settings_enforcer) + { + if (gCV_gokz_settings_enforcer.BoolValue) + { + for (int i = 0; i < ENFORCEDCVAR_COUNT; i++) + { + gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i]; + } + } + else + { + for (int client = 1; client <= MaxClients; client++) + { + gB_InValidRun[client] = false; + } + + // You have to change map before you can re-activate that + gB_EnforcerOnFreshMap = false; + } + } +} + +public void OnEnforcedConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (gCV_gokz_settings_enforcer.BoolValue) + { + for (int i = 0; i < ENFORCEDCVAR_COUNT; i++) + { + if (convar == gCV_EnforcedCVar[i]) + { + gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i]; + return; + } + } + } +} + +static void SetupAPI() +{ + GlobalAPI_GetAuthStatus(GetAuthStatusCallback); + GlobalAPI_GetModes(GetModeInfoCallback); + GlobalAPI_GetMapByName(GetMapCallback, _, gC_CurrentMap); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && !IsFakeClient(client)) + { + CheckClientGlobalBan(client); + } + } +} + +public int GetAuthStatusCallback(JSON_Object auth_json, GlobalAPIRequestData request) +{ + if (request.Failure) + { + LogError("Failed to check API key with Global API."); + return 0; + } + + APIAuth auth = view_as<APIAuth>(auth_json); + if (!auth.IsValid) + { + LogError("Global API key was found to be missing or invalid."); + } + gB_APIKeyCheck = auth.IsValid; + return 0; +} + +public int GetModeInfoCallback(JSON_Object modes, GlobalAPIRequestData request) +{ + if (request.Failure) + { + LogError("Failed to check mode versions with Global API."); + return 0; + } + + if (!modes.IsArray) + { + LogError("GlobalAPI returned a malformed response while looking up the modes."); + return 0; + } + + for (int i = 0; i < modes.Length; i++) + { + APIMode mode = view_as<APIMode>(modes.GetObjectIndexed(i)); + int mode_id = GOKZ_GL_FromGlobalMode(view_as<GlobalMode>(mode.Id)); + if (mode_id == -1) + { + LogError("GlobalAPI returned a malformed mode."); + } + else if (mode.LatestVersion <= GOKZ_GetModeVersion(mode_id)) + { + gB_ModeCheck[mode_id] = true; + } + else + { + char desc[128]; + + gB_ModeCheck[mode_id] = false; + mode.GetLatestVersionDesc(desc, sizeof(desc)); + LogError("Global API requires %s mode version %d (%s). You have version %d (%s).", + gC_ModeNames[mode_id], mode.LatestVersion, desc, GOKZ_GetModeVersion(mode_id), GOKZ_VERSION); + } + } + return 0; +} + +public int GetMapCallback(JSON_Object map_json, GlobalAPIRequestData request) +{ + if (request.Failure || map_json == INVALID_HANDLE) + { + LogError("Failed to get map info."); + return 0; + } + + APIMap map = view_as<APIMap>(map_json); + + gB_MapValidated = map.IsValidated; + gI_MapID = map.Id; + gI_MapFileSize = map.Filesize; + gI_MapTier = map.Difficulty; + + // We don't do that earlier cause we need the map ID + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && IsClientAuthorized(client) && !IsFakeClient(client)) + { + UpdatePoints(client); + } + } + return 0; +} + +void CheckClientGlobalBan(int client) +{ + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GlobalAPI_GetPlayerBySteamId(CheckClientGlobalBan_Callback, client, steamid); +} + +public void CheckClientGlobalBan_Callback(JSON_Object player_json, GlobalAPIRequestData request, int client) +{ + if (!IsValidClient(client)) + { + return; + } + + if (request.Failure) + { + LogError("Failed to get ban info."); + return; + } + + char client_steamid[32], response_steamid[32]; + GetClientAuthId(client, AuthId_Steam2, client_steamid, sizeof(client_steamid)); + + if (!player_json.IsArray || player_json.Length != 1) + { + LogError("Got malformed reply when querying steamid %s", client_steamid); + return; + } + + APIPlayer player = view_as<APIPlayer>(player_json.GetObjectIndexed(0)); + player.GetSteamId(response_steamid, sizeof(response_steamid)); + if (!StrEqual(client_steamid, response_steamid)) + { + return; + } + + gB_GloballyVerified[client] = !player.IsBanned; + + if (player.IsBanned && gB_GOKZLocalDB) + { + GOKZ_DB_SetCheater(client, true); + } +} + +static void LoadSounds() +{ + char downloadPath[PLATFORM_MAX_PATH]; + FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", GL_SOUND_NEW_RECORD); + AddFileToDownloadsTable(downloadPath); + PrecacheSound(GL_SOUND_NEW_RECORD, true); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-global/api.sp b/sourcemod/scripting/gokz-global/api.sp new file mode 100644 index 0000000..23caa15 --- /dev/null +++ b/sourcemod/scripting/gokz-global/api.sp @@ -0,0 +1,142 @@ +static GlobalForward H_OnNewTopTime; +static GlobalForward H_OnPointsUpdated; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnNewTopTime = new GlobalForward("GOKZ_GL_OnNewTopTime", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float); + H_OnPointsUpdated = new GlobalForward("GOKZ_GL_OnPointsUpdated", ET_Ignore, Param_Cell, Param_Cell); +} + +void Call_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float time) +{ + Call_StartForward(H_OnNewTopTime); + Call_PushCell(client); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(timeType); + Call_PushCell(rank); + Call_PushCell(rankOverall); + Call_PushFloat(time); + Call_Finish(); +} + +void Call_OnPointsUpdated(int client, int mode) +{ + Call_StartForward(H_OnPointsUpdated); + Call_PushCell(client); + Call_PushCell(mode); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_GL_PrintRecords", Native_PrintRecords); + CreateNative("GOKZ_GL_DisplayMapTopMenu", Native_DisplayMapTopMenu); + CreateNative("GOKZ_GL_GetPoints", Native_GetPoints); + CreateNative("GOKZ_GL_GetMapPoints", Native_GetMapPoints); + CreateNative("GOKZ_GL_GetRankPoints", Native_GetRankPoints); + CreateNative("GOKZ_GL_GetFinishes", Native_GetFinishes); + CreateNative("GOKZ_GL_UpdatePoints", Native_UpdatePoints); + CreateNative("GOKZ_GL_GetAPIKeyValid", Native_GetAPIKeyValid); + CreateNative("GOKZ_GL_GetPluginsValid", Native_GetPluginsValid); + CreateNative("GOKZ_GL_GetSettingsEnforcerValid", Native_GetSettingsEnforcerValid); + CreateNative("GOKZ_GL_GetMapValid", Native_GetMapValid); + CreateNative("GOKZ_GL_GetPlayerValid", Native_GetPlayerValid); +} + +public int Native_PrintRecords(Handle plugin, int numParams) +{ + char map[33], steamid[32]; + GetNativeString(2, map, sizeof(map)); + GetNativeString(5, steamid, sizeof(steamid)); + + if (StrEqual(map, "")) + { + PrintRecords(GetNativeCell(1), gC_CurrentMap, GetNativeCell(3), GetNativeCell(4), steamid); + } + else + { + PrintRecords(GetNativeCell(1), map, GetNativeCell(3), GetNativeCell(4), steamid); + } + return 0; +} + +public int Native_DisplayMapTopMenu(Handle plugin, int numParams) +{ + char pluginName[32]; + GetPluginFilename(plugin, pluginName, sizeof(pluginName)); + bool localRanksCall = StrEqual(pluginName, "gokz-localranks.smx", false); + + char map[33]; + GetNativeString(2, map, sizeof(map)); + + if (StrEqual(map, "")) + { + DisplayMapTopSubmenu(GetNativeCell(1), gC_CurrentMap, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), localRanksCall); + } + else + { + DisplayMapTopSubmenu(GetNativeCell(1), map, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), localRanksCall); + } + return 0; +} + +public int Native_GetPoints(Handle plugin, int numParams) +{ + return GetPoints(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)); +} + +public int Native_GetMapPoints(Handle plugin, int numParams) +{ + return GetMapPoints(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)); +} + +public int Native_GetRankPoints(Handle plugin, int numParams) +{ + return GetRankPoints(GetNativeCell(1), GetNativeCell(2)); +} + +public int Native_GetFinishes(Handle plugin, int numParams) +{ + return GetFinishes(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)); +} + +public int Native_UpdatePoints(Handle plugin, int numParams) +{ + // We're gonna always force an update here, cause otherwise the call doesn't really make sense + UpdatePoints(GetNativeCell(1), true, GetNativeCell(2)); + return 0; +} + +public int Native_GetAPIKeyValid(Handle plugin, int numParams) +{ + return view_as<int>(gB_APIKeyCheck); +} + +public int Native_GetPluginsValid(Handle plugin, int numParams) +{ + return view_as<int>(gB_BannedCommandsCheck); +} + +public int Native_GetSettingsEnforcerValid(Handle plugin, int numParams) +{ + return view_as<int>(gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap); +} + +public int Native_GetMapValid(Handle plugin, int numParams) +{ + return view_as<int>(MapCheck()); +} + +public int Native_GetPlayerValid(Handle plugin, int numParams) +{ + return view_as<int>(gB_GloballyVerified[GetNativeCell(1)]); +} diff --git a/sourcemod/scripting/gokz-global/ban_player.sp b/sourcemod/scripting/gokz-global/ban_player.sp new file mode 100644 index 0000000..835d9e5 --- /dev/null +++ b/sourcemod/scripting/gokz-global/ban_player.sp @@ -0,0 +1,42 @@ +/* + Globally ban players when they are suspected by gokz-anticheat. +*/ + + + +// =====[ PUBLIC ]===== + +void GlobalBanPlayer(int client, ACReason reason, const char[] notes, const char[] stats) +{ + char playerName[MAX_NAME_LENGTH], steamid[32], ip[32]; + + GetClientName(client, playerName, sizeof(playerName)); + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GetClientIP(client, ip, sizeof(ip)); + + DataPack dp = new DataPack(); + dp.WriteString(playerName); + dp.WriteString(steamid); + + switch (reason) + { + case ACReason_BhopHack:GlobalAPI_CreateBan(BanPlayerCallback, dp, steamid, "bhop_hack", stats, notes, ip); + case ACReason_BhopMacro:GlobalAPI_CreateBan(BanPlayerCallback, dp, steamid, "bhop_macro", stats, notes, ip); + } +} + +public int BanPlayerCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp) +{ + char playerName[MAX_NAME_LENGTH], steamid[32]; + + dp.Reset(); + dp.ReadString(playerName, sizeof(playerName)); + dp.ReadString(steamid, sizeof(steamid)); + delete dp; + + if (request.Failure) + { + LogError("Failed to globally ban %s (%s).", playerName, steamid); + } + return 0; +} diff --git a/sourcemod/scripting/gokz-global/commands.sp b/sourcemod/scripting/gokz-global/commands.sp new file mode 100644 index 0000000..8ee7c32 --- /dev/null +++ b/sourcemod/scripting/gokz-global/commands.sp @@ -0,0 +1,169 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_globalcheck", CommandGlobalCheck, "[KZ] Show whether global records are currently enabled in chat."); + RegConsoleCmd("sm_gc", CommandGlobalCheck, "[KZ] Show whether global records are currently enabled in chat."); + RegConsoleCmd("sm_tier", CommandTier, "[KZ] Show the map's tier in chat."); + RegConsoleCmd("sm_gpb", CommandPrintPBs, "[KZ] Show main course global personal best in chat. Usage: !gpb <map>"); + RegConsoleCmd("sm_gr", CommandPrintRecords, "[KZ] Show main course global record times in chat. Usage: !gr <map>"); + RegConsoleCmd("sm_gwr", CommandPrintRecords, "[KZ] Show main course global record times in chat. Usage: !gwr <map>"); + RegConsoleCmd("sm_gbpb", CommandPrintBonusPBs, "[KZ] Show bonus global personal best in chat. Usage: !gbpb <#bonus> <map>"); + RegConsoleCmd("sm_gbr", CommandPrintBonusRecords, "[KZ] Show bonus global record times in chat. Usage: !bgr <#bonus> <map>"); + RegConsoleCmd("sm_gbwr", CommandPrintBonusRecords, "[KZ] Show bonus global record times in chat. Usage: !bgwr <#bonus> <map>"); + RegConsoleCmd("sm_gmaptop", CommandMapTop, "[KZ] Open a menu showing the top global main course times of a map. Usage: !gmaptop <map>"); + RegConsoleCmd("sm_gbmaptop", CommandBonusMapTop, "[KZ] Open a menu showing the top global bonus times of a map. Usage: !gbmaptop <#bonus> <map>"); +} + +public Action CommandGlobalCheck(int client, int args) +{ + PrintGlobalCheckToChat(client); + return Plugin_Handled; +} + +public Action CommandTier(int client, int args) +{ + if (gI_MapTier != -1) + { + GOKZ_PrintToChat(client, true, "%t", "Map Tier", gC_CurrentMap, gI_MapTier); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Map Tier (Unknown)", gC_CurrentMap); + } + return Plugin_Handled; +} + +public Action CommandPrintPBs(int client, int args) +{ + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + return CommandPrintRecordsHelper(client, args, steamid); +} + +public Action CommandPrintRecords(int client, int args) +{ + return CommandPrintRecordsHelper(client, args); +} + +static Action CommandPrintRecordsHelper(int client, int args, const char[] steamid = DEFAULT_STRING) +{ + KZPlayer player = KZPlayer(client); + int mode = player.Mode; + + if (args == 0) + { // Print record times for current map and their current mode + PrintRecords(client, gC_CurrentMap, 0, mode, steamid); + } + else if (args >= 1) + { // Print record times for specified map and their current mode + char argMap[33]; + GetCmdArg(1, argMap, sizeof(argMap)); + PrintRecords(client, argMap, 0, mode, steamid); + } + return Plugin_Handled; +} + +public Action CommandPrintBonusPBs(int client, int args) +{ + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + return CommandPrintBonusRecordsHelper(client, args, steamid); +} + +public Action CommandPrintBonusRecords(int client, int args) +{ + return CommandPrintBonusRecordsHelper(client, args); +} + +static Action CommandPrintBonusRecordsHelper(int client, int args, const char[] steamid = DEFAULT_STRING) +{ + KZPlayer player = KZPlayer(client); + int mode = player.Mode; + + if (args == 0) + { // Print Bonus 1 record times for current map and their current mode + PrintRecords(client, gC_CurrentMap, 1, mode, steamid); + } + else if (args == 1) + { // Print specified Bonus # record times for current map and their current mode + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + PrintRecords(client, gC_CurrentMap, bonus, mode, steamid); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 2) + { // Print specified Bonus # record times for specified map and their current mode + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + PrintRecords(client, argMap, bonus, mode, steamid); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandMapTop(int client, int args) +{ + if (args <= 0) + { // Open global map top for current map + DisplayMapTopModeMenu(client, gC_CurrentMap, 0); + } + else if (args >= 1) + { // Open global map top for specified map + char argMap[64]; + GetCmdArg(1, argMap, sizeof(argMap)); + DisplayMapTopModeMenu(client, argMap, 0); + } + return Plugin_Handled; +} + +public Action CommandBonusMapTop(int client, int args) +{ + if (args == 0) + { // Open global Bonus 1 top for current map + DisplayMapTopModeMenu(client, gC_CurrentMap, 1); + } + else if (args == 1) + { // Open specified global Bonus # top for current map + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DisplayMapTopModeMenu(client, gC_CurrentMap, bonus); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 2) + { // Open specified global Bonus # top for specified map + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DisplayMapTopModeMenu(client, argMap, bonus); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-global/maptop_menu.sp b/sourcemod/scripting/gokz-global/maptop_menu.sp new file mode 100644 index 0000000..0c71346 --- /dev/null +++ b/sourcemod/scripting/gokz-global/maptop_menu.sp @@ -0,0 +1,249 @@ +/* + Menu with the top global times for a map course and mode. +*/ + +static bool cameFromLocalRanks[MAXPLAYERS + 1]; +static char mapTopMap[MAXPLAYERS + 1][64]; +static int mapTopCourse[MAXPLAYERS + 1]; +static int mapTopMode[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void DisplayMapTopModeMenu(int client, const char[] map, int course) +{ + FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map); + mapTopCourse[client] = course; + + Menu menu = new Menu(MenuHandler_MapTopModeMenu); + MapTopModeMenuSetTitle(client, menu); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +void DisplayMapTopMenu(int client, const char[] map, int course, int mode) +{ + FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map); + mapTopCourse[client] = course; + mapTopMode[client] = mode; + + Menu menu = new Menu(MenuHandler_MapTopMenu); + MapTopMenuSetTitle(client, menu); + MapTopMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +void DisplayMapTopSubmenu(int client, const char[] map, int course, int mode, int timeType, bool fromLocalRanks = false) +{ + char modeStr[32]; + + cameFromLocalRanks[client] = fromLocalRanks; + + DataPack dp = new DataPack(); + dp.WriteCell(GetClientUserId(client)); + dp.WriteCell(timeType); + + FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map); + mapTopCourse[client] = course; + mapTopMode[client] = mode; + GOKZ_GL_GetModeString(mode, modeStr, sizeof(modeStr)); + + // TODO Hard coded 128 tick + // TODO Hard coded cap at top 20 + // TODO Not true NUB yet + GlobalAPI_GetRecordsTop(DisplayMapTopSubmenuCallback, dp, _, _, _, map, 128, course, modeStr, + timeType == TimeType_Nub ? DEFAULT_BOOL : false, _, 0, 20); +} + + + +// =====[ EVENTS ]===== + +public int MenuHandler_MapTopModeMenu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = mode + DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_MapTopMenu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[8]; + menu.GetItem(param2, info, sizeof(info)); + int timeType = StringToInt(info); + DisplayMapTopSubmenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], timeType); + } + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayMapTopModeMenu(param1, mapTopMap[param1], mapTopCourse[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_MapTopSubmenu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + if (cameFromLocalRanks[param1]) + { + GOKZ_LR_ReopenMapTopMenu(param1); + } + else + { + DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1]); + } + } + if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +static void MapTopModeMenuSetTitle(int client, Menu menu) +{ + if (mapTopCourse[client] == 0) + { + menu.SetTitle("%T", "Global Map Top Mode Menu - Title", client, mapTopMap[client]); + } + else + { + menu.SetTitle("%T", "Global Map Top Mode Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client]); + } +} + +static void MapTopMenuSetTitle(int client, Menu menu) +{ + if (mapTopCourse[client] == 0) + { + menu.SetTitle("%T", "Global Map Top Menu - Title", client, mapTopMap[client], gC_ModeNames[mapTopMode[client]]); + } + else + { + menu.SetTitle("%T", "Global Map Top Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]); + } +} + +static void MapTopMenuAddItems(int client, Menu menu) +{ + char display[32]; + for (int i = 0; i < TIMETYPE_COUNT; i++) + { + FormatEx(display, sizeof(display), "%T", "Global Map Top Menu - Top", client, gC_TimeTypeNames[i]); + menu.AddItem(IntToStringEx(i), display); + } +} + +public int DisplayMapTopSubmenuCallback(JSON_Object top, GlobalAPIRequestData request, DataPack dp) +{ + dp.Reset(); + int client = GetClientOfUserId(dp.ReadCell()); + int timeType = dp.ReadCell(); + delete dp; + + if (request.Failure) + { + LogError("Failed to get top records with Global API."); + return 0; + } + + if (!top.IsArray) + { + LogError("GlobalAPI returned a malformed response while looking up the top records."); + return 0; + } + + if (!IsValidClient(client)) + { + return 0; + } + + Menu menu = new Menu(MenuHandler_MapTopSubmenu); + if (mapTopCourse[client] == 0) + { + menu.SetTitle("%T", "Global Map Top Submenu - Title", client, + gC_TimeTypeNames[timeType], mapTopMap[client], gC_ModeNames[mapTopMode[client]]); + } + else + { + menu.SetTitle("%T", "Global Map Top Submenu - Title (Bonus)", client, + gC_TimeTypeNames[timeType], mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]); + } + + if (MapTopSubmenuAddItems(menu, top, timeType) == 0) + { // If no records found + if (timeType == TimeType_Pro) + { + GOKZ_PrintToChat(client, true, "%t", "No Global Times Found (PRO)"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Global Times Found"); + } + + if (cameFromLocalRanks[client]) + { + GOKZ_LR_ReopenMapTopMenu(client); + } + else + { + DisplayMapTopMenu(client, mapTopMap[client], mapTopCourse[client], mapTopMode[client]); + } + } + else + { + menu.Pagination = 5; + menu.Display(client, MENU_TIME_FOREVER); + } + return 0; +} + +// Returns number of record times added to the menu +static int MapTopSubmenuAddItems(Menu menu, JSON_Object records, int timeType) +{ + char playerName[MAX_NAME_LENGTH]; + char display[128]; + + for (int i = 0; i < records.Length; i++) + { + APIRecord record = view_as<APIRecord>(records.GetObjectIndexed(i)); + + record.GetPlayerName(playerName, sizeof(playerName)); + + switch (timeType) + { + case TimeType_Nub: + { + FormatEx(display, sizeof(display), "#%-2d %11s %3d TP %s", + i + 1, GOKZ_FormatTime(record.Time), record.Teleports, playerName); + } + case TimeType_Pro: + { + FormatEx(display, sizeof(display), "#%-2d %11s %s", + i + 1, GOKZ_FormatTime(record.Time), playerName); + } + } + + menu.AddItem("", display, ITEMDRAW_DISABLED); + } + + return records.Length; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-global/points.sp b/sourcemod/scripting/gokz-global/points.sp new file mode 100644 index 0000000..7758318 --- /dev/null +++ b/sourcemod/scripting/gokz-global/points.sp @@ -0,0 +1,147 @@ + +int pointsTotal[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT]; +int finishes[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT]; +int pointsMap[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT]; +int requestsInProgress[MAXPLAYERS + 1]; + + + +void ResetPoints(int client) +{ + for (int mode = 0; mode < MODE_COUNT; mode++) + { + for (int type = 0; type < TIMETYPE_COUNT; type++) + { + pointsTotal[client][mode][type] = -1; + finishes[client][mode][type] = -1; + pointsMap[client][mode][type] = -1; + } + } + requestsInProgress[client] = 0; +} + +void ResetMapPoints(int client) +{ + for (int mode = 0; mode < MODE_COUNT; mode++) + { + for (int type = 0; type < TIMETYPE_COUNT; type++) + { + pointsMap[client][mode][type] = -1; + } + } + requestsInProgress[client] = 0; +} + +int GetRankPoints(int client, int mode) +{ + return pointsTotal[client][mode][TimeType_Nub]; +} + +int GetPoints(int client, int mode, int timeType) +{ + return pointsTotal[client][mode][timeType]; +} + +int GetMapPoints(int client, int mode, int timeType) +{ + return pointsMap[client][mode][timeType]; +} + +int GetFinishes(int client, int mode, int timeType) +{ + return finishes[client][mode][timeType]; +} + +// Note: This only gets 128 tick records +void UpdatePoints(int client, bool force = false, int mode = -1) +{ + if (requestsInProgress[client] != 0) + { + return; + } + + if (mode == -1) + { + mode = GOKZ_GetCoreOption(client, Option_Mode); + } + + if (!force || pointsTotal[client][mode][TimeType_Nub] == -1) + { + GetPlayerRanks(client, mode, TimeType_Nub); + GetPlayerRanks(client, mode, TimeType_Pro); + requestsInProgress[client] += 2; + } + + if (gI_MapID != -1 && (!force || pointsMap[client][mode][TimeType_Nub] == -1)) + { + GetPlayerRanks(client, mode, TimeType_Nub, gI_MapID); + GetPlayerRanks(client, mode, TimeType_Pro, gI_MapID); + requestsInProgress[client] += 2; + } +} + +static void GetPlayerRanks(int client, int mode, int timeType, int mapID = DEFAULT_INT) +{ + char steamid[21]; + int modes[1], mapIDs[1]; + + modes[0] = view_as<int>(GOKZ_GL_GetGlobalMode(mode)); + mapIDs[0] = mapID; + GetClientAuthId(client, AuthId_SteamID64, steamid, sizeof(steamid)); + + DataPack dp = new DataPack(); + dp.WriteCell(GetClientUserId(client)); + dp.WriteCell(mode); + dp.WriteCell(timeType); + dp.WriteCell(mapID == DEFAULT_INT); + GlobalAPI_GetPlayerRanks(UpdatePointsCallback, dp, _, _, _, _, steamid, _, _, + mapIDs, mapID == DEFAULT_INT ? DEFAULT_INT : 1, { 0 }, 1, + modes, 1, { 128 }, 1, timeType == TimeType_Nub ? DEFAULT_BOOL : false, _, _); +} + +static void UpdatePointsCallback(JSON_Object ranks, GlobalAPIRequestData request, DataPack dp) +{ + dp.Reset(); + int client = GetClientOfUserId(dp.ReadCell()); + int mode = dp.ReadCell(); + int timeType = dp.ReadCell(); + bool isTotal = dp.ReadCell(); + delete dp; + + requestsInProgress[client]--; + + if (client == 0) + { + return; + } + + int points, totalFinishes; + if (request.Failure || !ranks.IsArray || ranks.Length == 0) + { + points = 0; + totalFinishes = 0; + } + else + { + APIPlayerRank rank = view_as<APIPlayerRank>(ranks.GetObjectIndexed(0)); + // points = timeType == TimeType_Nub ? rank.PointsOverall : rank.Points; + points = points == -1 ? 0 : rank.Points; + totalFinishes = rank.Finishes == -1 ? 0 : rank.Finishes; + } + + if (isTotal) + { + pointsTotal[client][mode][timeType] = points; + finishes[client][mode][timeType] = totalFinishes; + } + else + { + pointsMap[client][mode][timeType] = points; + } + + // We always do that cause not all of the requests might have failed + if (requestsInProgress[client] == 0) + { + Call_OnPointsUpdated(client, mode); + } +} diff --git a/sourcemod/scripting/gokz-global/print_records.sp b/sourcemod/scripting/gokz-global/print_records.sp new file mode 100644 index 0000000..3966ed5 --- /dev/null +++ b/sourcemod/scripting/gokz-global/print_records.sp @@ -0,0 +1,190 @@ +/* + Prints the global record times for a map course and mode. +*/ + + +static bool inProgress[MAXPLAYERS + 1]; +static bool waitingForOtherCallback[MAXPLAYERS + 1]; +static bool isPBQuery[MAXPLAYERS + 1]; +static char printRecordsMap[MAXPLAYERS + 1][64]; +static int printRecordsCourse[MAXPLAYERS + 1]; +static int printRecordsMode[MAXPLAYERS + 1]; +static bool printRecordsTimeExists[MAXPLAYERS + 1][TIMETYPE_COUNT]; +static float printRecordsTimes[MAXPLAYERS + 1][TIMETYPE_COUNT]; +static char printRecordsPlayerNames[MAXPLAYERS + 1][TIMETYPE_COUNT][MAX_NAME_LENGTH]; + + + +// =====[ PUBLIC ]===== + +void PrintRecords(int client, const char[] map, int course, int mode, const char[] steamid = DEFAULT_STRING) +{ + char mode_str[32]; + + if (inProgress[client]) + { + GOKZ_PrintToChat(client, true, "%t", "Please Wait Before Using Command Again"); + return; + } + + GOKZ_GL_GetModeString(mode, mode_str, sizeof(mode_str)); + + DataPack dpNUB = CreateDataPack(); + dpNUB.WriteCell(GetClientUserId(client)); + dpNUB.WriteCell(TimeType_Nub); + GlobalAPI_GetRecordsTop(PrintRecordsCallback, dpNUB, steamid, _, _, map, 128, course, mode_str, _, _, 0, 1); + + DataPack dpPRO = CreateDataPack(); + dpPRO.WriteCell(GetClientUserId(client)); + dpPRO.WriteCell(TimeType_Pro); + GlobalAPI_GetRecordsTop(PrintRecordsCallback, dpPRO, steamid, _, _, map, 128, course, mode_str, false, _, 0, 1); + + inProgress[client] = true; + waitingForOtherCallback[client] = true; + isPBQuery[client] = !StrEqual(steamid, DEFAULT_STRING); + FormatEx(printRecordsMap[client], sizeof(printRecordsMap[]), map); + printRecordsCourse[client] = course; + printRecordsMode[client] = mode; +} + +public int PrintRecordsCallback(JSON_Object records, GlobalAPIRequestData request, DataPack dp) +{ + dp.Reset(); + int client = GetClientOfUserId(dp.ReadCell()); + int timeType = dp.ReadCell(); + delete dp; + + if (request.Failure) + { + LogError("Failed to retrieve record from the Global API for printing."); + return 0; + } + + if (!IsValidClient(client)) + { + return 0; + } + + if (records.Length <= 0) + { + printRecordsTimeExists[client][timeType] = false; + } + else + { + APIRecord record = view_as<APIRecord>(records.GetObjectIndexed(0)); + printRecordsTimeExists[client][timeType] = true; + printRecordsTimes[client][timeType] = record.Time; + record.GetPlayerName(printRecordsPlayerNames[client][timeType], sizeof(printRecordsPlayerNames[][])); + } + + if (!waitingForOtherCallback[client]) + { + if (isPBQuery[client]) + { + PrintPBsFinally(client); + } + else + { + PrintRecordsFinally(client); + } + inProgress[client] = false; + } + else + { + waitingForOtherCallback[client] = false; + } + return 0; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_PrintRecords(int client) +{ + inProgress[client] = false; +} + + + +// =====[ PRIVATE ]===== + +static void PrintPBsFinally(int client) +{ + // Print GPB header to chat + if (printRecordsCourse[client] == 0) + { + GOKZ_PrintToChat(client, true, "%t", "GPB Header", + printRecordsMap[client], + gC_ModeNamesShort[printRecordsMode[client]]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "GPB Header (Bonus)", + printRecordsMap[client], + printRecordsCourse[client], + gC_ModeNamesShort[printRecordsMode[client]]); + } + + // Print GPB times to chat + if (!printRecordsTimeExists[client][TimeType_Nub]) + { + GOKZ_PrintToChat(client, false, "%t", "No Global Times Found"); + } + else if (!printRecordsTimeExists[client][TimeType_Pro]) + { + GOKZ_PrintToChat(client, false, "%t", "GPB Time - NUB", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]), + printRecordsPlayerNames[client][TimeType_Nub]); + GOKZ_PrintToChat(client, false, "%t", "GPB Time - No PRO Time"); + } + else + { + GOKZ_PrintToChat(client, false, "%t", "GPB Time - NUB", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]), + printRecordsPlayerNames[client][TimeType_Nub]); + GOKZ_PrintToChat(client, false, "%t", "GPB Time - PRO", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Pro]), + printRecordsPlayerNames[client][TimeType_Pro]); + } +} + +static void PrintRecordsFinally(int client) +{ + // Print GWR header to chat + if (printRecordsCourse[client] == 0) + { + GOKZ_PrintToChat(client, true, "%t", "GWR Header", + printRecordsMap[client], + gC_ModeNamesShort[printRecordsMode[client]]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "GWR Header (Bonus)", + printRecordsMap[client], + printRecordsCourse[client], + gC_ModeNamesShort[printRecordsMode[client]]); + } + + // Print GWR times to chat + if (!printRecordsTimeExists[client][TimeType_Nub]) + { + GOKZ_PrintToChat(client, false, "%t", "No Global Times Found"); + } + else if (!printRecordsTimeExists[client][TimeType_Pro]) + { + GOKZ_PrintToChat(client, false, "%t", "GWR Time - NUB", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]), + printRecordsPlayerNames[client][TimeType_Nub]); + GOKZ_PrintToChat(client, false, "%t", "GWR Time - No PRO Time"); + } + else + { + GOKZ_PrintToChat(client, false, "%t", "GWR Time - NUB", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]), + printRecordsPlayerNames[client][TimeType_Nub]); + GOKZ_PrintToChat(client, false, "%t", "GWR Time - PRO", + GOKZ_FormatTime(printRecordsTimes[client][TimeType_Pro]), + printRecordsPlayerNames[client][TimeType_Pro]); + } +} diff --git a/sourcemod/scripting/gokz-global/send_run.sp b/sourcemod/scripting/gokz-global/send_run.sp new file mode 100644 index 0000000..f560958 --- /dev/null +++ b/sourcemod/scripting/gokz-global/send_run.sp @@ -0,0 +1,143 @@ +/* + Sends a run to the global API and delete the replay if it is a temporary replay. +*/ + + + +char storedReplayPath[MAXPLAYERS + 1][512]; +int lastRecordId[MAXPLAYERS + 1], storedCourse[MAXPLAYERS + 1], storedTimeType[MAXPLAYERS + 1], storedUserId[MAXPLAYERS + 1]; +float storedTime[MAXPLAYERS + 1]; +bool deleteRecord[MAXPLAYERS + 1]; + +// =====[ PUBLIC ]===== + +void SendTime(int client, int course, float time, int teleportsUsed) +{ + char steamid[32], modeStr[32]; + KZPlayer player = KZPlayer(client); + int mode = player.Mode; + + if (GlobalsEnabled(mode)) + { + DataPack dp = CreateDataPack(); + dp.WriteCell(GetClientUserId(client)); + dp.WriteCell(course); + dp.WriteCell(mode); + dp.WriteCell(GOKZ_GetTimeTypeEx(teleportsUsed)); + dp.WriteFloat(time); + + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GOKZ_GL_GetModeString(mode, modeStr, sizeof(modeStr)); + GlobalAPI_CreateRecord(SendTimeCallback, dp, steamid, gI_MapID, modeStr, course, 128, teleportsUsed, time); + } +} + +public int SendTimeCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp) +{ + dp.Reset(); + int userID = dp.ReadCell(); + int client = GetClientOfUserId(userID); + int course = dp.ReadCell(); + int mode = dp.ReadCell(); + int timeType = dp.ReadCell(); + float time = dp.ReadFloat(); + delete dp; + + if (!IsValidClient(client)) + { + return 0; + } + + if (request.Failure) + { + LogError("Failed to send a time to the global API."); + return 0; + } + + int top_place = response.GetInt("top_100"); + int top_overall_place = response.GetInt("top_100_overall"); + + if (top_place > 0) + { + Call_OnNewTopTime(client, course, mode, timeType, top_place, top_overall_place, time); + } + + // Don't like doing this here, but seems to be the most efficient place + UpdatePoints(client, true); + + // Check if we can send the replay + lastRecordId[client] = response.GetInt("record_id"); + if (IsReplayReadyToSend(client, course, timeType, time)) + { + SendReplay(client); + } + else + { + storedUserId[client] = userID; + storedCourse[client] = course; + storedTimeType[client] = timeType; + storedTime[client] = time; + } + return 0; +} + +public void OnReplaySaved_SendReplay(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay) +{ + strcopy(storedReplayPath[client], sizeof(storedReplayPath[]), filePath); + if (IsReplayReadyToSend(client, course, timeType, time)) + { + SendReplay(client); + } + else + { + lastRecordId[client] = -1; + storedUserId[client] = GetClientUserId(client); + storedCourse[client] = course; + storedTimeType[client] = timeType; + storedTime[client] = time; + deleteRecord[client] = tempReplay; + } +} + +bool IsReplayReadyToSend(int client, int course, int timeType, float time) +{ + // Not an error, just not ready yet + if (lastRecordId[client] == -1 || storedReplayPath[client][0] == '\0') + { + return false; + } + + if (storedUserId[client] == GetClientUserId(client) && storedCourse[client] == course + && storedTimeType[client] == timeType && storedTime[client] == time) + { + return true; + } + else + { + LogError("Failed to upload replay to the global API. Record mismatch."); + return false; + } +} + +public void SendReplay(int client) +{ + DataPack dp = new DataPack(); + dp.WriteString(deleteRecord[client] ? storedReplayPath[client] : ""); + GlobalAPI_CreateReplayForRecordId(SendReplayCallback, dp, lastRecordId[client], storedReplayPath[client]); + lastRecordId[client] = -1; + storedReplayPath[client] = ""; +} + +public int SendReplayCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp) +{ + // Delete the temporary replay file if needed. + dp.Reset(); + char replayPath[PLATFORM_MAX_PATH]; + dp.ReadString(replayPath, sizeof(replayPath)); + if (replayPath[0] != '\0') + { + DeleteFile(replayPath); + } + delete dp; + return 0; +} diff --git a/sourcemod/scripting/gokz-goto.sp b/sourcemod/scripting/gokz-goto.sp new file mode 100644 index 0000000..7dd67cb --- /dev/null +++ b/sourcemod/scripting/gokz-goto.sp @@ -0,0 +1,231 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdktools> + +#include <gokz/core> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Goto", + author = "DanZay", + description = "Allows players to teleport to another player", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-goto.txt" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-goto"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-goto.phrases"); + + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ GOTO ]===== + +// Returns whether teleport to target was successful +bool GotoPlayer(int client, int target, bool printMessage = true) +{ + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + if (printMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (target == client) + { + if (printMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Goto Failure (Not Yourself)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + if (!IsPlayerAlive(target)) + { + if (printMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Goto Failure (Dead)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + + float targetOrigin[3]; + float targetAngles[3]; + + Movement_GetOrigin(target, targetOrigin); + Movement_GetEyeAngles(target, targetAngles); + + if (!IsPlayerAlive(client)) + { + GOKZ_RespawnPlayer(client); + } + + TeleportPlayer(client, targetOrigin, targetAngles); + + GOKZ_PrintToChat(client, true, "%t", "Goto Success", target); + + if (GOKZ_GetTimerRunning(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Goto)"); + GOKZ_StopTimer(client); + } + + return true; +} + + + +// =====[ GOTO MENU ]===== + +int DisplayGotoMenu(int client) +{ + Menu menu = new Menu(MenuHandler_Goto); + menu.SetTitle("%T", "Goto Menu - Title", client); + int menuItems = GotoMenuAddItems(client, menu); + if (menuItems == 0) + { + delete menu; + } + else + { + menu.Display(client, MENU_TIME_FOREVER); + } + return menuItems; +} + +public int MenuHandler_Goto(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + int target = GetClientOfUserId(StringToInt(info)); + + if (!IsValidClient(target)) + { + GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid"); + GOKZ_PlayErrorSound(param1); + DisplayGotoMenu(param1); + } + else if (!GotoPlayer(param1, target)) + { + DisplayGotoMenu(param1); + } + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +// Returns number of items added to the menu +int GotoMenuAddItems(int client, Menu menu) +{ + char display[MAX_NAME_LENGTH + 4]; + int targetCount = 0; + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || i == client) + { + continue; + } + + if (IsFakeClient(i)) + { + FormatEx(display, sizeof(display), "BOT %N", i); + } + else + { + FormatEx(display, sizeof(display), "%N", i); + } + + menu.AddItem(IntToStringEx(GetClientUserId(i)), display, ITEMDRAW_DEFAULT); + targetCount++; + } + + return targetCount; +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_goto", CommandGoto, "[KZ] Teleport to another player. Usage: !goto <player>"); +} + +public Action CommandGoto(int client, int args) +{ + // If no arguments, display the goto menu + if (args < 1) + { + if (DisplayGotoMenu(client) == 0) + { + // No targets, so show error + GOKZ_PrintToChat(client, true, "%t", "No Players Found"); + GOKZ_PlayErrorSound(client); + } + } + // Otherwise try to teleport to the specified player + else + { + char specifiedPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, specifiedPlayer, sizeof(specifiedPlayer)); + + int target = FindTarget(client, specifiedPlayer, false, false); + if (target != -1) + { + GotoPlayer(client, target); + } + } + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud.sp b/sourcemod/scripting/gokz-hud.sp new file mode 100644 index 0000000..e9db12a --- /dev/null +++ b/sourcemod/scripting/gokz-hud.sp @@ -0,0 +1,334 @@ +#include <sourcemod> + +#include <sdkhooks> +#include <gokz/core> +#include <gokz/hud> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/racing> +#include <gokz/replays> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ HUD", + author = "DanZay", + description = "Provides HUD and UI features", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-hud.txt" + +bool gB_GOKZRacing; +bool gB_GOKZReplays; +bool gB_MenuShowing[MAXPLAYERS + 1]; +int gI_ObserverTarget[MAXPLAYERS + 1]; +bool gB_JBTakeoff[MAXPLAYERS + 1]; +bool gB_FastUpdateRate[MAXPLAYERS + 1]; +int gI_DynamicMenu[MAXPLAYERS + 1]; + +#include "gokz-hud/spectate_text.sp" +#include "gokz-hud/commands.sp" +#include "gokz-hud/hide_weapon.sp" +#include "gokz-hud/info_panel.sp" +#include "gokz-hud/menu.sp" +#include "gokz-hud/options.sp" +#include "gokz-hud/options_menu.sp" +#include "gokz-hud/racing_text.sp" +#include "gokz-hud/speed_text.sp" +#include "gokz-hud/timer_text.sp" +#include "gokz-hud/tp_menu.sp" +#include "gokz-hud/natives.sp" + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-hud"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-hud.phrases"); + + HookEvents(); + RegisterCommands(); + + UpdateSpecList(); + OnPluginStart_RacingText(); + OnPluginStart_SpeedText(); + OnPluginStart_TimerText(); +} + +public void OnPluginEnd() +{ + OnPluginEnd_Menu(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } + + gB_GOKZRacing = LibraryExists("gokz-racing"); + gB_GOKZReplays = LibraryExists("gokz-replays"); + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + gB_GOKZRacing = gB_GOKZRacing || StrEqual(name, "gokz-racing"); + gB_GOKZReplays = gB_GOKZReplays || StrEqual(name, "gokz-replays"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZRacing = gB_GOKZRacing && !StrEqual(name, "gokz-racing"); + gB_GOKZReplays = gB_GOKZReplays && !StrEqual(name, "gokz-replays"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientDisconnect(int client) +{ + gI_ObserverTarget[client] = -1; +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_PostThinkPost, OnPlayerPostThinkPost); +} + +public void OnPlayerPostThinkPost(int client) +{ + KZPlayer player = KZPlayer(client); + gB_JBTakeoff[client] = (gB_JBTakeoff[client] && !player.OnGround && !player.OnLadder && !player.Noclipping) || Movement_GetJumpbugged(client); +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client)) + { + return; + } + + HUDInfo info; + KZPlayer player = KZPlayer(client); + KZPlayer targetPlayer = KZPlayer(player.ObserverTarget); + + // Bots don't need to have their HUD drawn + if (player.Fake) + { + return; + } + + if (player.Alive) + { + SetHUDInfo(player, info, cmdnum); + } + else if (targetPlayer.ID != -1 && !targetPlayer.Fake) + { + SetHUDInfo(targetPlayer, info, cmdnum); + } + else if (targetPlayer.ID != -1 && gB_GOKZReplays) + { + GOKZ_RP_GetPlaybackInfo(targetPlayer.ID, info); + } + else + { + return; + } + + if (!IsValidClient(info.ID)) + { + return; + } + + OnPlayerRunCmdPost_InfoPanel(client, cmdnum, info); + OnPlayerRunCmdPost_RacingText(client, cmdnum); + OnPlayerRunCmdPost_SpeedText(client, cmdnum, info); + OnPlayerRunCmdPost_TimerText(client, cmdnum, info); + OnPlayerRunCmdPost_TPMenu(client, cmdnum, info); +} + +public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerSpawn_HideWeapon(client); + OnPlayerSpawn_Menu(client); + } +} + +public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) // player_death pre hook +{ + event.BroadcastDisabled = true; // Block death notices + return Plugin_Continue; +} + +public void GOKZ_OnJoinTeam(int client, int team) +{ + OnJoinTeam_Menu(client); +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + OnTimerStart_Menu(client); +} + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + OnTimerEnd_TimerText(client); + OnTimerEnd_Menu(client); +} + +public void GOKZ_OnTimerStopped(int client) +{ + OnTimerStopped_TimerText(client); + OnTimerStopped_Menu(client); +} + +public void GOKZ_OnPause_Post(int client) +{ + OnPause_Menu(client); +} + +public void GOKZ_OnResume_Post(int client) +{ + OnResume_Menu(client); +} + +public void GOKZ_OnMakeCheckpoint_Post(int client) +{ + OnMakeCheckpoint_Menu(client); +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + OnCountedTeleport_Menu(client); +} + +public void GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3]) +{ + OnStartPositionSet_Menu(client); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + any hudOption; + if (GOKZ_HUD_IsHUDOption(option, hudOption)) + { + OnOptionChanged_SpeedText(client, hudOption); + OnOptionChanged_TimerText(client, hudOption); + OnOptionChanged_Menu(client, hudOption); + OnOptionChanged_HideWeapon(client, hudOption); + OnOptionChanged_Options(client, hudOption, newValue); + if (hudOption == HUDOption_UpdateRate) + { + gB_FastUpdateRate[client] = GOKZ_HUD_GetOption(client, HUDOption_UpdateRate) == UpdateRate_Fast; + } + else if (hudOption == HUDOption_DynamicMenu) + { + gI_DynamicMenu[client] = GOKZ_HUD_GetOption(client, HUDOption_DynamicMenu); + } + } +} + +public void GOKZ_OnOptionsLoaded(int client) +{ + gB_FastUpdateRate[client] = GOKZ_HUD_GetOption(client, HUDOption_UpdateRate) == UpdateRate_Fast; + gI_DynamicMenu[client] = GOKZ_HUD_GetOption(client, HUDOption_DynamicMenu); +} + +// =====[ OTHER EVENTS ]===== + +public void OnGameFrame() +{ + // Cache the spectator list every few ticks. + if (GetGameTickCount() % 4 == 0) + { + UpdateSpecList(); + } +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void GOKZ_RC_OnRaceInfoChanged(int raceID, RaceInfo prop, int oldValue, int newValue) +{ + OnRaceInfoChanged_RacingText(raceID, prop, newValue); +} + + + +// =====[ PRIVATE ]===== + +static void HookEvents() +{ + HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); + HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); +} + +static void SetHUDInfo(KZPlayer player, HUDInfo info, int cmdnum) +{ + info.TimerRunning = player.TimerRunning; + info.TimeType = player.TimeType; + info.Time = player.Time; + info.Paused = player.Paused; + info.OnGround = player.OnGround; + info.OnLadder = player.OnLadder; + info.Noclipping = player.Noclipping; + info.Ducking = Movement_GetDucking(player.ID); + info.HitBhop = (Movement_GetJumped(player.ID) && Movement_GetTakeoffCmdNum(player.ID) == cmdnum) && Movement_GetTakeoffCmdNum(player.ID) - Movement_GetLandingCmdNum(player.ID) <= HUD_MAX_BHOP_GROUND_TICKS; + info.Speed = player.Speed; + info.ID = player.ID; + info.Jumped = player.Jumped; + info.HitPerf = player.GOKZHitPerf; + info.HitJB = gB_JBTakeoff[info.ID]; + info.TakeoffSpeed = player.GOKZTakeoffSpeed; + info.IsTakeoff = Movement_GetTakeoffCmdNum(player.ID) == cmdnum; + info.Buttons = player.Buttons; + info.CurrentTeleport = player.TeleportCount; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/commands.sp b/sourcemod/scripting/gokz-hud/commands.sp new file mode 100644 index 0000000..fc5ed4e --- /dev/null +++ b/sourcemod/scripting/gokz-hud/commands.sp @@ -0,0 +1,116 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_menu", CommandMenu, "[KZ] Toggle the simple teleport menu."); + RegConsoleCmd("sm_cpmenu", CommandMenu, "[KZ] Toggle the simple teleport menu."); + RegConsoleCmd("sm_adv", CommandToggleAdvancedMenu, "[KZ] Toggle the advanced teleport menu."); + RegConsoleCmd("sm_panel", CommandToggleInfoPanel, "[KZ] Toggle visibility of the centre information panel."); + RegConsoleCmd("sm_timerstyle", CommandToggleTimerStyle, "[KZ] Toggle the style of the timer text."); + RegConsoleCmd("sm_timertype", CommandToggleTimerType, "[KZ] Toggle visibility of your time type."); + RegConsoleCmd("sm_speed", CommandToggleSpeed, "[KZ] Toggle visibility of your speed and jump pre-speed."); + RegConsoleCmd("sm_hideweapon", CommandToggleShowWeapon, "[KZ] Toggle visibility of your weapon."); +} + +public Action CommandMenu(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_TPMenu) != TPMenu_Disabled) + { + GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Disabled); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Simple); + } + return Plugin_Handled; +} + +public Action CommandToggleAdvancedMenu(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_TPMenu) != TPMenu_Advanced) + { + GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Advanced); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Simple); + } + return Plugin_Handled; +} + +public Action CommandToggleInfoPanel(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_InfoPanel) == InfoPanel_Disabled) + { + GOKZ_HUD_SetOption(client, HUDOption_InfoPanel, InfoPanel_Enabled); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_InfoPanel, InfoPanel_Disabled); + } + return Plugin_Handled; +} + +public Action CommandToggleTimerStyle(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Standard) + { + GOKZ_HUD_SetOption(client, HUDOption_TimerStyle, TimerStyle_Precise); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_TimerStyle, TimerStyle_Standard); + } + return Plugin_Handled; +} + +public Action CommandToggleTimerType(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_TimerType) == TimerType_Disabled) + { + GOKZ_HUD_SetOption(client, HUDOption_TimerType, TimerType_Enabled); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_TimerType, TimerType_Disabled); + } + return Plugin_Handled; +} + +public Action CommandToggleSpeed(int client, int args) +{ + int speedText = GOKZ_HUD_GetOption(client, HUDOption_SpeedText); + int infoPanel = GOKZ_HUD_GetOption(client, HUDOption_InfoPanel); + + if (speedText == SpeedText_Disabled) + { + if (infoPanel == InfoPanel_Enabled) + { + GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_InfoPanel); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Bottom); + } + } + else if (infoPanel == InfoPanel_Disabled && speedText == SpeedText_InfoPanel) + { + GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Bottom); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Disabled); + } + return Plugin_Handled; +} + +public Action CommandToggleShowWeapon(int client, int args) +{ + if (GOKZ_HUD_GetOption(client, HUDOption_ShowWeapon) == ShowWeapon_Disabled) + { + GOKZ_HUD_SetOption(client, HUDOption_ShowWeapon, ShowWeapon_Enabled); + } + else + { + GOKZ_HUD_SetOption(client, HUDOption_ShowWeapon, ShowWeapon_Disabled); + } + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/hide_weapon.sp b/sourcemod/scripting/gokz-hud/hide_weapon.sp new file mode 100644 index 0000000..b290d8f --- /dev/null +++ b/sourcemod/scripting/gokz-hud/hide_weapon.sp @@ -0,0 +1,30 @@ +/* + Hides weapon view model. +*/ + + + +// =====[ EVENTS ]===== + +void OnPlayerSpawn_HideWeapon(int client) +{ + UpdateHideWeapon(client); +} + +void OnOptionChanged_HideWeapon(int client, HUDOption option) +{ + if (option == HUDOption_ShowWeapon) + { + UpdateHideWeapon(client); + } +} + + + +// =====[ PRIVATE ]===== + +static void UpdateHideWeapon(int client) +{ + SetEntProp(client, Prop_Send, "m_bDrawViewmodel", + GOKZ_HUD_GetOption(client, HUDOption_ShowWeapon) == ShowWeapon_Enabled); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/info_panel.sp b/sourcemod/scripting/gokz-hud/info_panel.sp new file mode 100644 index 0000000..3563404 --- /dev/null +++ b/sourcemod/scripting/gokz-hud/info_panel.sp @@ -0,0 +1,307 @@ +/* + Displays information using hint text. + + This is manually refreshed whenever player has taken off so that they see + their pre-speed as soon as possible, improving responsiveness. +*/ + + + +static bool infoPanelDuckPressedLast[MAXPLAYERS + 1]; +static bool infoPanelOnGroundLast[MAXPLAYERS + 1]; +static bool infoPanelShowDuckString[MAXPLAYERS + 1]; + + +// =====[ PUBLIC ]===== + +bool IsDrawingInfoPanel(int client) +{ + KZPlayer player = KZPlayer(client); + return player.InfoPanel != InfoPanel_Disabled + && !NothingEnabledInInfoPanel(player); +} + + + +// =====[ EVENTS ]===== + +void OnPlayerRunCmdPost_InfoPanel(int client, int cmdnum, HUDInfo info) +{ + int updateSpeed = gB_FastUpdateRate[client] ? 1 : 10; + if (cmdnum % updateSpeed == 0 || info.IsTakeoff) + { + UpdateInfoPanel(client, info); + } + infoPanelOnGroundLast[info.ID] = info.OnGround; + infoPanelDuckPressedLast[info.ID] = info.Ducking; +} + + + +// =====[ PRIVATE ]===== + +static void UpdateInfoPanel(int client, HUDInfo info) +{ + KZPlayer player = KZPlayer(client); + + if (player.Fake || !IsDrawingInfoPanel(player.ID)) + { + return; + } + char infoPanelText[512]; + FormatEx(infoPanelText, sizeof(infoPanelText), GetInfoPanel(player, info)); + if (infoPanelText[0] != '\0') + { + PrintCSGOHUDText(player.ID, infoPanelText); + } +} + +static bool NothingEnabledInInfoPanel(KZPlayer player) +{ + bool noTimerText = player.TimerText != TimerText_InfoPanel; + bool noSpeedText = player.SpeedText != SpeedText_InfoPanel || player.Paused; + bool noKeys = player.ShowKeys == ShowKeys_Disabled + || player.ShowKeys == ShowKeys_Spectating && player.Alive; + return noTimerText && noSpeedText && noKeys; +} + +static char[] GetInfoPanel(KZPlayer player, HUDInfo info) +{ + char infoPanelText[512]; + FormatEx(infoPanelText, sizeof(infoPanelText), + "%s%s%s%s", + GetSpectatorString(player, info), + GetTimeString(player, info), + GetSpeedString(player, info), + GetKeysString(player, info)); + if (infoPanelText[0] == '\0') + { + return infoPanelText; + } + else + { + Format(infoPanelText, sizeof(infoPanelText), "<font color='#ffffff08'>%s", infoPanelText); + } + TrimString(infoPanelText); + return infoPanelText; +} + +static char[] GetSpectatorString(KZPlayer player, HUDInfo info) +{ + char spectatorString[255]; + if (player.SpecListPosition != SpecListPosition_InfoPanel || player.ShowSpectators == ShowSpecs_Disabled) + { + return spectatorString; + } + // Only return something if the player is alive or observing someone else + if (player.Alive || player.ObserverTarget != -1) + { + FormatEx(spectatorString, sizeof(spectatorString), "%s", FormatSpectatorTextForInfoPanel(player, KZPlayer(info.ID))); + } + return spectatorString; +} + +static char[] GetTimeString(KZPlayer player, HUDInfo info) +{ + char timeString[128]; + if (player.TimerText != TimerText_InfoPanel) + { + timeString = ""; + } + else if (info.TimerRunning) + { + if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled) + { + switch (info.TimeType) + { + case TimeType_Nub: + { + FormatEx(timeString, sizeof(timeString), + "%T: <font color='#ead18a'>%s</font> %s\n", + "Info Panel Text - Time", player.ID, + GOKZ_HUD_FormatTime(player.ID, info.Time), + GetPausedString(player, info)); + } + case TimeType_Pro: + { + FormatEx(timeString, sizeof(timeString), + "%T: <font color='#b5d4ee'>%s</font> %s\n", + "Info Panel Text - Time", player.ID, + GOKZ_HUD_FormatTime(player.ID, info.Time), + GetPausedString(player, info)); + } + } + } + else + { + FormatEx(timeString, sizeof(timeString), + "%T: <font color='#ffffff'>%s</font> %s\n", + "Info Panel Text - Time", player.ID, + GOKZ_HUD_FormatTime(player.ID, info.Time), + GetPausedString(player, info)); + } + } + else + { + FormatEx(timeString, sizeof(timeString), + "%T: <font color='#ea4141'>%T</font> %s\n", + "Info Panel Text - Time", player.ID, + "Info Panel Text - Stopped", player.ID, + GetPausedString(player, info)); + } + return timeString; +} + +static char[] GetPausedString(KZPlayer player, HUDInfo info) +{ + char pausedString[64]; + if (info.Paused) + { + FormatEx(pausedString, sizeof(pausedString), + "(<font color='#ffffff'>%T</font>)", + "Info Panel Text - PAUSED", player.ID); + } + else + { + pausedString = ""; + } + return pausedString; +} + +static char[] GetSpeedString(KZPlayer player, HUDInfo info) +{ + char speedString[128]; + if (player.SpeedText != SpeedText_InfoPanel || info.Paused) + { + speedString = ""; + } + else + { + if (info.OnGround || info.OnLadder || info.Noclipping) + { + FormatEx(speedString, sizeof(speedString), + "%T: <font color='#ffffff'>%.0f</font> u/s\n", + "Info Panel Text - Speed", player.ID, + RoundToPowerOfTen(info.Speed, -2)); + infoPanelShowDuckString[info.ID] = false; + } + else + { + if (GOKZ_HUD_GetOption(player.ID, HUDOption_DeadstrafeColor) == DeadstrafeColor_Enabled + && Movement_GetVerticalVelocity(info.ID) > 0.0 && Movement_GetVerticalVelocity(info.ID) < 140.0) + { + FormatEx(speedString, sizeof(speedString), + "%T: <font color='#ff2020'>%.0f</font> %s\n", + "Info Panel Text - Speed", player.ID, + RoundToPowerOfTen(info.Speed, -2), + GetTakeoffString(info)); + } + else + { + FormatEx(speedString, sizeof(speedString), + "%T: <font color='#ffffff'>%.0f</font> %s\n", + "Info Panel Text - Speed", player.ID, + RoundToPowerOfTen(info.Speed, -2), + GetTakeoffString(info)); + } + } + } + return speedString; +} + +static char[] GetTakeoffString(HUDInfo info) +{ + char takeoffString[96], duckString[32]; + + if (infoPanelShowDuckString[info.ID] + || (infoPanelOnGroundLast[info.ID] + && !info.HitBhop + && info.IsTakeoff + && info.Jumped + && info.Ducking + && (infoPanelDuckPressedLast[info.ID] || GOKZ_GetCoreOption(info.ID, Option_Mode) == Mode_Vanilla))) + { + duckString = " <font color='#71eeb8'>C</font>"; + infoPanelShowDuckString[info.ID] = true; + } + else + { + duckString = ""; + infoPanelShowDuckString[info.ID] = false; + } + + if (info.HitJB) + { + FormatEx(takeoffString, sizeof(takeoffString), + "(<font color='#ffff20'>%.0f</font>)%s", + RoundToPowerOfTen(info.TakeoffSpeed, -2), + duckString); + } + else if (info.HitPerf) + { + FormatEx(takeoffString, sizeof(takeoffString), + "(<font color='#40ff40'>%.0f</font>)%s", + RoundToPowerOfTen(info.TakeoffSpeed, -2), + duckString); + } + else + { + FormatEx(takeoffString, sizeof(takeoffString), + "(<font color='#ffffff'>%.0f</font>)%s", + RoundToPowerOfTen(info.TakeoffSpeed, -2), + duckString); + } + return takeoffString; +} + +static char[] GetKeysString(KZPlayer player, HUDInfo info) +{ + char keysString[64]; + if (player.ShowKeys == ShowKeys_Disabled) + { + keysString = ""; + } + else if (player.ShowKeys == ShowKeys_Spectating && player.Alive) + { + keysString = ""; + } + else + { + int buttons = info.Buttons; + FormatEx(keysString, sizeof(keysString), + "%T: <font color='#ffffff'>%c %c %c %c %c %c</font>\n", + "Info Panel Text - Keys", player.ID, + buttons & IN_MOVELEFT ? 'A' : '_', + buttons & IN_FORWARD ? 'W' : '_', + buttons & IN_BACK ? 'S' : '_', + buttons & IN_MOVERIGHT ? 'D' : '_', + buttons & IN_DUCK ? 'C' : '_', + buttons & IN_JUMP ? 'J' : '_'); + } + return keysString; +} + +// Credits to Franc1sco (https://github.com/Franc1sco/FixHintColorMessages) +void PrintCSGOHUDText(int client, const char[] format) +{ + char buff[HUD_MAX_HINT_SIZE]; + Format(buff, sizeof(buff), "</font>%s", format); + + for (int i = strlen(buff); i < sizeof(buff) - 1; i++) + { + buff[i] = ' '; + } + + buff[sizeof(buff) - 1] = '\0'; + + Protobuf pb = view_as<Protobuf>(StartMessageOne("TextMsg", client, USERMSG_BLOCKHOOKS)); + pb.SetInt("msg_dst", 4); + pb.AddString("params", "#SFUI_ContractKillStart"); + pb.AddString("params", buff); + pb.AddString("params", NULL_STRING); + pb.AddString("params", NULL_STRING); + pb.AddString("params", NULL_STRING); + pb.AddString("params", NULL_STRING); + + EndMessage(); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/menu.sp b/sourcemod/scripting/gokz-hud/menu.sp new file mode 100644 index 0000000..4b7259e --- /dev/null +++ b/sourcemod/scripting/gokz-hud/menu.sp @@ -0,0 +1,96 @@ +/* + Tracks whether a GOKZ HUD menu or panel element is being shown to the client. +*/ + + + +// Update the TP menu i.e. item text, item disabled/enabled +void CancelGOKZHUDMenu(int client) +{ + // Only cancel the menu if we know it's the TP menu + if (gB_MenuShowing[client]) + { + CancelClientMenu(client); + } +} + + + +// =====[ EVENTS ]===== + +void OnPlayerSpawn_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnOptionChanged_Menu(int client, HUDOption option) +{ + if (option == HUDOption_TPMenu || option == HUDOption_TimerText) + { + CancelGOKZHUDMenu(client); + } +} + +void OnTimerStart_Menu(int client) +{ + // Prevent the menu from getting cancelled every tick if player use start timer button zone. + if (GOKZ_GetTime(client) > 0.0) + { + CancelGOKZHUDMenu(client); + } +} + +void OnTimerEnd_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnTimerStopped_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnPause_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnResume_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnMakeCheckpoint_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnCountedTeleport_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnJoinTeam_Menu(int client) +{ + CancelGOKZHUDMenu(client); +} + +void OnStartPositionSet_Menu(int client) +{ + // Prevent the menu from getting cancelled every tick if player use start timer button zone. + if (GOKZ_GetTime(client) > 0.0) + { + CancelGOKZHUDMenu(client); + } +} + +void OnPluginEnd_Menu() +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client)) + { + CancelGOKZHUDMenu(client); + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/natives.sp b/sourcemod/scripting/gokz-hud/natives.sp new file mode 100644 index 0000000..bb877e2 --- /dev/null +++ b/sourcemod/scripting/gokz-hud/natives.sp @@ -0,0 +1,33 @@ +void CreateNatives() +{ + CreateNative("GOKZ_HUD_ForceUpdateTPMenu", Native_ForceUpdateTPMenu); + CreateNative("GOKZ_HUD_GetMenuShowing", Native_GetMenuShowing); + CreateNative("GOKZ_HUD_SetMenuShowing", Native_SetMenuShowing); + CreateNative("GOKZ_HUD_GetMenuSpectatorText", Native_GetSpectatorText); +} + +public int Native_ForceUpdateTPMenu(Handle plugin, int numParams) +{ + SetForceUpdateTPMenu(GetNativeCell(1)); + return 0; +} + +public int Native_GetMenuShowing(Handle plugin, int numParams) +{ + return view_as<int>(gB_MenuShowing[GetNativeCell(1)]); +} + +public int Native_SetMenuShowing(Handle plugin, int numParams) +{ + gB_MenuShowing[GetNativeCell(1)] = view_as<bool>(GetNativeCell(2)); + return 0; +} + +public int Native_GetSpectatorText(Handle plugin, int numParams) +{ + HUDInfo info; + GetNativeArray(2, info, sizeof(HUDInfo)); + KZPlayer player = KZPlayer(GetNativeCell(1)); + FormatNativeString(3, 0, 0, GetNativeCell(4), _, "", FormatSpectatorTextForMenu(player, info)); + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/options.sp b/sourcemod/scripting/gokz-hud/options.sp new file mode 100644 index 0000000..84bbf2b --- /dev/null +++ b/sourcemod/scripting/gokz-hud/options.sp @@ -0,0 +1,190 @@ +/* + Options for controlling appearance and behaviour of HUD and UI. +*/ + + + +// =====[ EVENTS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void OnOptionChanged_Options(int client, HUDOption option, any newValue) +{ + PrintOptionChangeMessage(client, option, newValue); +} + + + +// =====[ PRIVATE ]===== + +static void RegisterOptions() +{ + for (HUDOption option; option < HUDOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_HUDOptionNames[option], gC_HUDOptionDescriptions[option], + OptionType_Int, gI_HUDOptionDefaults[option], 0, gI_HUDOptionCounts[option] - 1); + } +} + +static void PrintOptionChangeMessage(int client, HUDOption option, any newValue) +{ + // NOTE: Not all options have a message for when they are changed. + switch (option) + { + case HUDOption_TPMenu: + { + switch (newValue) + { + case TPMenu_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Disable"); + } + case TPMenu_Simple: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Enable (Simple)"); + } + case TPMenu_Advanced: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Enable (Advanced)"); + } + } + } + case HUDOption_InfoPanel: + { + switch (newValue) + { + case InfoPanel_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Info Panel - Disable"); + } + case InfoPanel_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Info Panel - Enable"); + } + } + } + case HUDOption_TimerStyle: + { + switch (newValue) + { + case TimerStyle_Standard: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Timer Style - Standard"); + } + case TimerStyle_Precise: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Timer Style - Precise"); + } + } + } + case HUDOption_TimerType: + { + switch (newValue) + { + case TimerType_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Timer Type - Disabled"); + } + case TimerType_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Timer Type - Enabled"); + } + } + } + case HUDOption_ShowWeapon: + { + switch (newValue) + { + case ShowWeapon_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Weapon - Disable"); + } + case ShowWeapon_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Weapon - Enable"); + } + } + } + case HUDOption_ShowControls: + { + switch (newValue) + { + case ReplayControls_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Controls - Disable"); + } + case ReplayControls_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Controls - Enable"); + } + } + } + case HUDOption_DeadstrafeColor: + { + switch (newValue) + { + case DeadstrafeColor_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Dead Strafe - Disable"); + } + case DeadstrafeColor_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Dead Strafe - Enable"); + } + } + } + case HUDOption_ShowSpectators: + { + switch (newValue) + { + case ShowSpecs_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Disable"); + } + case ShowSpecs_Number: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Number"); + } + case ShowSpecs_Full: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Full"); + } + } + } + case HUDOption_SpecListPosition: + { + switch (newValue) + { + case SpecListPosition_InfoPanel: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Spectator List Position - Info Panel"); + } + case SpecListPosition_TPMenu: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Spectator List Position - TP Menu"); + } + } + } + case HUDOption_DynamicMenu: + { + switch (newValue) + { + case DynamicMenu_Legacy: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Legacy"); + } + case DynamicMenu_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Disable"); + } + case DynamicMenu_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Enable"); + } + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/options_menu.sp b/sourcemod/scripting/gokz-hud/options_menu.sp new file mode 100644 index 0000000..705df27 --- /dev/null +++ b/sourcemod/scripting/gokz-hud/options_menu.sp @@ -0,0 +1,181 @@ +static TopMenu optionsTopMenu; +static TopMenuObject catHUD; +static TopMenuObject itemsHUD[HUDOPTION_COUNT]; + + + +// =====[ EVENTS ]===== + +void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu) +{ + if (optionsTopMenu == topMenu && catHUD != INVALID_TOPMENUOBJECT) + { + return; + } + + catHUD = topMenu.AddCategory(HUD_OPTION_CATEGORY, TopMenuHandler_Categories); +} + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + // Make sure category exists + if (catHUD == INVALID_TOPMENUOBJECT) + { + GOKZ_OnOptionsMenuCreated(topMenu); + } + + if (optionsTopMenu == topMenu) + { + return; + } + + optionsTopMenu = topMenu; + + // Add HUD option items + for (int option = 0; option < view_as<int>(HUDOPTION_COUNT); option++) + { + itemsHUD[option] = optionsTopMenu.AddItem(gC_HUDOptionNames[option], TopMenuHandler_HUD, catHUD); + } +} + +public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == catHUD) + { + Format(buffer, maxlength, "%T", "Options Menu - HUD", param); + } + } +} + +public void TopMenuHandler_HUD(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + HUDOption option = HUDOPTION_INVALID; + for (int i = 0; i < view_as<int>(HUDOPTION_COUNT); i++) + { + if (topobj_id == itemsHUD[i]) + { + option = view_as<HUDOption>(i); + break; + } + } + + if (option == HUDOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case HUDOption_TPMenu: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_TPMenuPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_ShowKeys: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_ShowKeysPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_TimerText: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_TimerTextPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_TimerStyle: + { + int optionValue = GOKZ_HUD_GetOption(param, option); + if (optionValue == TimerStyle_Precise) + { + FormatEx(buffer, maxlength, "%T - 01:23.45", + gC_HUDOptionPhrases[option], param); + } + else + { + FormatEx(buffer, maxlength, "%T - 1:23", + gC_HUDOptionPhrases[option], param); + } + } + case HUDOption_TimerType: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_TimerTypePhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_SpeedText: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_SpeedTextPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_ShowControls: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_ShowControlsPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_DeadstrafeColor: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_DeadstrafeColorPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_UpdateRate: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_HUDUpdateRatePhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_ShowSpectators: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_ShowSpecsPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_SpecListPosition: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_SpecListPositionPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + case HUDOption_DynamicMenu: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], param, + gC_DynamicMenuPhrases[GOKZ_HUD_GetOption(param, option)], param); + } + default:FormatToggleableOptionDisplay(param, option, buffer, maxlength); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_HUD_CycleOption(param, option); + optionsTopMenu.Display(param, TopMenuPosition_LastCategory); + } +} + + + +// =====[ PRIVATE ]===== + +static void FormatToggleableOptionDisplay(int client, HUDOption option, char[] buffer, int maxlength) +{ + if (GOKZ_HUD_GetOption(client, option) == 0) + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gC_HUDOptionPhrases[option], client, + "Options Menu - Enabled", client); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/racing_text.sp b/sourcemod/scripting/gokz-hud/racing_text.sp new file mode 100644 index 0000000..ac6ea3d --- /dev/null +++ b/sourcemod/scripting/gokz-hud/racing_text.sp @@ -0,0 +1,167 @@ +/* + Uses HUD text to show the race countdown and a start message. + + This is manually refreshed when a race starts to show the start message as + soon as possible, improving responsiveness. +*/ + + + +static Handle racingHudSynchronizer; +static float countdownStartTime[MAXPLAYERS + 1]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_RacingText() +{ + racingHudSynchronizer = CreateHudSynchronizer(); +} + +void OnPlayerRunCmdPost_RacingText(int client, int cmdnum) +{ + int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6; + if (gB_GOKZRacing && cmdnum % updateSpeed == 2) + { + UpdateRacingText(client); + } +} + +void OnRaceInfoChanged_RacingText(int raceID, RaceInfo prop, int newValue) +{ + if (prop != RaceInfo_Status) + { + return; + } + + if (newValue == RaceStatus_Countdown) + { + for (int client = 1; client <= MaxClients; client++) + { + if (GOKZ_RC_GetRaceID(client) == raceID) + { + countdownStartTime[client] = GetGameTime(); + } + } + } + else if (newValue == RaceStatus_Aborting) + { + for (int client = 1; client <= MaxClients; client++) + { + if (GOKZ_RC_GetRaceID(client) == raceID) + { + ClearRacingText(client); + } + } + } + else if (newValue == RaceStatus_Started) + { + for (int client = 1; client <= MaxClients; client++) + { + if (GOKZ_RC_GetRaceID(client) == raceID) + { + UpdateRacingText(client); + } + } + } +} + + + +// =====[ PRIVATE ]===== + +static void UpdateRacingText(int client) +{ + KZPlayer player = KZPlayer(client); + + if (player.Fake) + { + return; + } + + if (player.Alive) + { + ShowRacingText(player, player); + } + else + { + KZPlayer targetPlayer = KZPlayer(player.ObserverTarget); + if (targetPlayer.ID != -1 && !targetPlayer.Fake) + { + ShowRacingText(player, targetPlayer); + } + } +} + +static void ClearRacingText(int client) +{ + ClearSyncHud(client, racingHudSynchronizer); +} + +static void ShowRacingText(KZPlayer player, KZPlayer targetPlayer) +{ + if (GOKZ_RC_GetStatus(targetPlayer.ID) != RacerStatus_Racing) + { + return; + } + + int raceStatus = GOKZ_RC_GetRaceInfo(GOKZ_RC_GetRaceID(targetPlayer.ID), RaceInfo_Status); + if (raceStatus == RaceStatus_Countdown) + { + ShowCountdownText(player, targetPlayer); + } + else if (raceStatus == RaceStatus_Started) + { + ShowStartedText(player, targetPlayer); + } +} + +static void ShowCountdownText(KZPlayer player, KZPlayer targetPlayer) +{ + float timeToStart = (countdownStartTime[targetPlayer.ID] + RC_COUNTDOWN_TIME) - GetGameTime(); + int colour[4]; + GetCountdownColour(timeToStart, colour); + + SetHudTextParams(-1.0, 0.3, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0); + ShowSyncHudText(player.ID, racingHudSynchronizer, "%t\n\n%d", "Get Ready", IntMax(RoundToCeil(timeToStart), 1)); +} + +static void GetCountdownColour(float timeToStart, int buffer[4]) +{ + // From red to green + if (timeToStart >= RC_COUNTDOWN_TIME) + { + buffer[0] = 255; + buffer[1] = 0; + } + else if (timeToStart > RC_COUNTDOWN_TIME / 2.0) + { + buffer[0] = 255; + buffer[1] = RoundFloat(-510.0 / RC_COUNTDOWN_TIME * timeToStart + 510.0); + } + else if (timeToStart > 0.0) + { + buffer[0] = RoundFloat(510.0 / RC_COUNTDOWN_TIME * timeToStart); + buffer[1] = 255; + } + else + { + buffer[0] = 0; + buffer[1] = 255; + } + + buffer[2] = 0; + buffer[3] = 255; +} + +static void ShowStartedText(KZPlayer player, KZPlayer targetPlayer) +{ + if (targetPlayer.TimerRunning) + { + return; + } + + SetHudTextParams(-1.0, 0.3, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), 0, 255, 0, 255, 0, 1.0, 0.0, 0.0); + ShowSyncHudText(player.ID, racingHudSynchronizer, "%t", "Go!"); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/spectate_text.sp b/sourcemod/scripting/gokz-hud/spectate_text.sp new file mode 100644 index 0000000..9700c38 --- /dev/null +++ b/sourcemod/scripting/gokz-hud/spectate_text.sp @@ -0,0 +1,119 @@ +/* + Responsible for spectator list on the HUD. +*/ + +#define SPECATATOR_LIST_MAX_COUNT 5 + +// =====[ PUBLIC ]===== + +char[] FormatSpectatorTextForMenu(KZPlayer player, HUDInfo info) +{ + int specCount; + char spectatorTextString[224]; + if (player.GetHUDOption(HUDOption_ShowSpectators) >= ShowSpecs_Number) + { + for (int i = 1; i <= MaxClients; i++) + { + if (gI_ObserverTarget[i] == info.ID) + { + specCount++; + if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full) + { + char buffer[64]; + if (specCount < SPECATATOR_LIST_MAX_COUNT) + { + GetClientName(i, buffer, sizeof(buffer)); + Format(spectatorTextString, sizeof(spectatorTextString), "%s\n%s", spectatorTextString, buffer); + } + else if (specCount == SPECATATOR_LIST_MAX_COUNT) + { + StrCat(spectatorTextString, sizeof(spectatorTextString), "\n..."); + } + } + } + } + if (specCount > 0) + { + if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full) + { + Format(spectatorTextString, sizeof(spectatorTextString), "%t\n ", "Spectator List - Menu (Full)", specCount, spectatorTextString); + } + else + { + Format(spectatorTextString, sizeof(spectatorTextString), "%t\n ", "Spectator List - Menu (Number)", specCount); + } + } + else + { + FormatEx(spectatorTextString, sizeof(spectatorTextString), ""); + } + } + return spectatorTextString; +} + +char[] FormatSpectatorTextForInfoPanel(KZPlayer player, KZPlayer targetPlayer) +{ + int specCount; + char spectatorTextString[160]; + if (player.GetHUDOption(HUDOption_ShowSpectators) >= ShowSpecs_Number) + { + for (int i = 1; i <= MaxClients; i++) + { + if (gI_ObserverTarget[i] == targetPlayer.ID) + { + specCount++; + if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full) + { + char buffer[64]; + if (specCount < SPECATATOR_LIST_MAX_COUNT) + { + GetClientName(i, buffer, sizeof(buffer)); + if (specCount == 1) + { + Format(spectatorTextString, sizeof(spectatorTextString), "%s", buffer); + } + else + { + Format(spectatorTextString, sizeof(spectatorTextString), "%s, %s", spectatorTextString, buffer); + } + } + else if (specCount == SPECATATOR_LIST_MAX_COUNT) + { + Format(spectatorTextString, sizeof(spectatorTextString), " ..."); + } + } + } + } + if (specCount > 0) + { + if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full) + { + Format(spectatorTextString, sizeof(spectatorTextString), "%t\n", "Spectator List - Info Panel (Full)", specCount, spectatorTextString); + } + else + { + Format(spectatorTextString, sizeof(spectatorTextString), "%t\n", "Spectator List - Info Panel (Number)", specCount); + } + } + else + { + FormatEx(spectatorTextString, sizeof(spectatorTextString), ""); + } + } + return spectatorTextString; +} + +void UpdateSpecList() +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && !IsFakeClient(client)) + { + gI_ObserverTarget[client] = GetObserverTarget(client); + } + else + { + gI_ObserverTarget[client] = -1; + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/speed_text.sp b/sourcemod/scripting/gokz-hud/speed_text.sp new file mode 100644 index 0000000..3f83949 --- /dev/null +++ b/sourcemod/scripting/gokz-hud/speed_text.sp @@ -0,0 +1,141 @@ +/* + Uses HUD text to show current speed somewhere on the screen. + + This is manually refreshed whenever player has taken off so that they see + their pre-speed as soon as possible, improving responsiveness. +*/ + + + +static Handle speedHudSynchronizer; + +static bool speedTextDuckPressedLast[MAXPLAYERS + 1]; +static bool speedTextOnGroundLast[MAXPLAYERS + 1]; +static bool speedTextShowDuckString[MAXPLAYERS + 1]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_SpeedText() +{ + speedHudSynchronizer = CreateHudSynchronizer(); +} + +void OnPlayerRunCmdPost_SpeedText(int client, int cmdnum, HUDInfo info) +{ + int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6; + if (cmdnum % updateSpeed == 0 || info.IsTakeoff) + { + UpdateSpeedText(client, info); + } + speedTextOnGroundLast[info.ID] = info.OnGround; + speedTextDuckPressedLast[info.ID] = info.Ducking; +} + +void OnOptionChanged_SpeedText(int client, HUDOption option) +{ + if (option == HUDOption_SpeedText) + { + ClearSpeedText(client); + } +} + + + +// =====[ PRIVATE ]===== + +static void UpdateSpeedText(int client, HUDInfo info) +{ + KZPlayer player = KZPlayer(client); + + if (player.Fake + || player.SpeedText != SpeedText_Bottom) + { + return; + } + + ShowSpeedText(player, info); +} + +static void ClearSpeedText(int client) +{ + ClearSyncHud(client, speedHudSynchronizer); +} + +static void ShowSpeedText(KZPlayer player, HUDInfo info) +{ + if (info.Paused) + { + return; + } + + int colour[4] = { 255, 255, 255, 0 }; // RGBA + float velZ = Movement_GetVerticalVelocity(info.ID); + if (!info.OnGround && !info.OnLadder && !info.Noclipping) + { + if (GOKZ_HUD_GetOption(player.ID, HUDOption_DeadstrafeColor) == DeadstrafeColor_Enabled && velZ > 0.0 && velZ < 140.0) + { + colour = { 255, 32, 32, 0 }; + } + else if (info.HitPerf) + { + if (info.HitJB) + { + colour = { 255, 255, 32, 0 }; + } + else + { + colour = { 64, 255, 64, 0 }; + } + } + } + + switch (player.SpeedText) + { + case SpeedText_Bottom: + { + // Set params based on the available screen space at max scaling HUD + if (!IsDrawingInfoPanel(player.ID)) + { + SetHudTextParams(-1.0, 0.75, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0); + } + else + { + SetHudTextParams(-1.0, 0.65, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0); + } + } + } + + if (info.OnGround || info.OnLadder || info.Noclipping) + { + ShowSyncHudText(player.ID, speedHudSynchronizer, + "%.0f", + RoundFloat(info.Speed * 10) / 10.0); + speedTextShowDuckString[info.ID] = false; + } + else + { + if (speedTextShowDuckString[info.ID] + || (speedTextOnGroundLast[info.ID] + && !info.HitBhop + && info.IsTakeoff + && info.Jumped + && info.Ducking + && (speedTextDuckPressedLast[info.ID] || GOKZ_GetCoreOption(info.ID, Option_Mode) == Mode_Vanilla))) + { + ShowSyncHudText(player.ID, speedHudSynchronizer, + "%.0f\n (%.0f)C", + RoundToPowerOfTen(info.Speed, -2), + RoundToPowerOfTen(info.TakeoffSpeed, -2)); + speedTextShowDuckString[info.ID] = true; + } + else { + ShowSyncHudText(player.ID, speedHudSynchronizer, + "%.0f\n(%.0f)", + RoundToPowerOfTen(info.Speed, -2), + RoundToPowerOfTen(info.TakeoffSpeed, -2)); + speedTextShowDuckString[info.ID] = false; + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/timer_text.sp b/sourcemod/scripting/gokz-hud/timer_text.sp new file mode 100644 index 0000000..a5fd17b --- /dev/null +++ b/sourcemod/scripting/gokz-hud/timer_text.sp @@ -0,0 +1,135 @@ +/* + Uses HUD text to show current run time somewhere on the screen. + + This is manually refreshed whenever the players' timer is started, ended or + stopped to improve responsiveness. +*/ + + + +static Handle timerHudSynchronizer; + + + +// =====[ PUBLIC ]===== + +char[] FormatTimerTextForMenu(KZPlayer player, HUDInfo info) +{ + char timerTextString[32]; + if (info.TimerRunning) + { + if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled) + { + FormatEx(timerTextString, sizeof(timerTextString), + "%s %s", + gC_TimeTypeNames[info.TimeType], + GOKZ_HUD_FormatTime(player.ID, info.Time)); + } + else + { + FormatEx(timerTextString, sizeof(timerTextString), + "%s", + GOKZ_HUD_FormatTime(player.ID, info.Time)); + } + if (info.Paused) + { + Format(timerTextString, sizeof(timerTextString), "%s (%T)", timerTextString, "Info Panel Text - PAUSED", player.ID); + } + } + return timerTextString; +} + + + +// =====[ EVENTS ]===== + +void OnPluginStart_TimerText() +{ + timerHudSynchronizer = CreateHudSynchronizer(); +} + +void OnPlayerRunCmdPost_TimerText(int client, int cmdnum, HUDInfo info) +{ + int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6; + if (cmdnum % updateSpeed == 1) + { + UpdateTimerText(client, info); + } +} + +void OnOptionChanged_TimerText(int client, HUDOption option) +{ + if (option == HUDOption_TimerText) + { + ClearTimerText(client); + } +} + +void OnTimerEnd_TimerText(int client) +{ + ClearTimerText(client); +} + +void OnTimerStopped_TimerText(int client) +{ + ClearTimerText(client); +} + + +// =====[ PRIVATE ]===== + +static void UpdateTimerText(int client, HUDInfo info) +{ + KZPlayer player = KZPlayer(client); + + if (player.Fake) + { + return; + } + + ShowTimerText(player, info); +} + +static void ClearTimerText(int client) +{ + ClearSyncHud(client, timerHudSynchronizer); +} + +static void ShowTimerText(KZPlayer player, HUDInfo info) +{ + if (!info.TimerRunning) + { + if (player.ID != info.ID) + { + CancelGOKZHUDMenu(player.ID); + } + return; + } + if (player.TimerText == TimerText_Top || player.TimerText == TimerText_Bottom) + { + int colour[4]; // RGBA + if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled) + { + switch (info.TimeType) + { + case TimeType_Nub:colour = { 234, 209, 138, 0 }; + case TimeType_Pro:colour = { 181, 212, 238, 0 }; + } + } + else colour = { 255, 255, 255, 0}; + + switch (player.TimerText) + { + case TimerText_Top: + { + SetHudTextParams(-1.0, 0.07, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0); + } + case TimerText_Bottom: + { + SetHudTextParams(-1.0, 0.9, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0); + } + } + + ShowSyncHudText(player.ID, timerHudSynchronizer, GOKZ_HUD_FormatTime(player.ID, info.Time)); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-hud/tp_menu.sp b/sourcemod/scripting/gokz-hud/tp_menu.sp new file mode 100644 index 0000000..bb78f1e --- /dev/null +++ b/sourcemod/scripting/gokz-hud/tp_menu.sp @@ -0,0 +1,415 @@ +/* + Lets players easily use teleport functionality. + + This menu is displayed whenever the player is alive and there is + currently no other menu displaying. +*/ + + + +#define ITEM_INFO_CHECKPOINT "cp" +#define ITEM_INFO_TELEPORT "tp" +#define ITEM_INFO_PREV "prev" +#define ITEM_INFO_NEXT "next" +#define ITEM_INFO_UNDO "undo" +#define ITEM_INFO_PAUSE "pause" +#define ITEM_INFO_START "start" + +static bool oldCanMakeCP[MAXPLAYERS + 1]; +static bool oldCanTP[MAXPLAYERS + 1]; +static bool oldCanPrevCP[MAXPLAYERS + 1]; +static bool oldCanNextCP[MAXPLAYERS + 1]; +static bool oldCanUndoTP[MAXPLAYERS + 1]; +static bool oldCanPause[MAXPLAYERS + 1]; +static bool oldCanResume[MAXPLAYERS + 1]; +static bool forceRefresh[MAXPLAYERS + 1]; + +// =====[ EVENTS ]===== + +void OnPlayerRunCmdPost_TPMenu(int client, int cmdnum, HUDInfo info) +{ + int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6; + if (cmdnum % updateSpeed == 2) + { + UpdateTPMenu(client, info); + } +} + +public int PanelHandler_Menu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Cancel) + { + gB_MenuShowing[param1] = false; + } + return 0; +} + +public int MenuHandler_TPMenu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + + if (StrEqual(info, ITEM_INFO_CHECKPOINT, false)) + { + GOKZ_MakeCheckpoint(param1); + } + else if (StrEqual(info, ITEM_INFO_TELEPORT, false)) + { + GOKZ_TeleportToCheckpoint(param1); + } + else if (StrEqual(info, ITEM_INFO_PREV, false)) + { + GOKZ_PrevCheckpoint(param1); + } + else if (StrEqual(info, ITEM_INFO_NEXT, false)) + { + GOKZ_NextCheckpoint(param1); + } + else if (StrEqual(info, ITEM_INFO_UNDO, false)) + { + GOKZ_UndoTeleport(param1); + } + else if (StrEqual(info, ITEM_INFO_PAUSE, false)) + { + GOKZ_TogglePause(param1); + } + else if (StrEqual(info, ITEM_INFO_START, false)) + { + GOKZ_TeleportToStart(param1); + } + + // Menu closes when player selects something, so... + gB_MenuShowing[param1] = false; + } + else if (action == MenuAction_Cancel) + { + gB_MenuShowing[param1] = false; + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +// =====[ PUBLIC ]===== +void SetForceUpdateTPMenu(int client) +{ + forceRefresh[client] = true; +} + +// =====[ PRIVATE ]===== + +static void UpdateTPMenu(int client, HUDInfo info) +{ + KZPlayer player = KZPlayer(client); + + if (player.Fake) + { + return; + } + + bool force = forceRefresh[client] + || player.CanMakeCheckpoint != oldCanMakeCP[client] + || player.CanTeleportToCheckpoint != oldCanTP[client] + || player.CanPrevCheckpoint != oldCanPrevCP[client] + || player.CanNextCheckpoint != oldCanNextCP[client] + || player.CanUndoTeleport != oldCanUndoTP[client] + || player.CanPause != oldCanPause[client] + || player.CanResume != oldCanResume[client]; + + + if (player.Alive) + { + if (player.TPMenu != TPMenu_Disabled) + { + if (GetClientMenu(client) == MenuSource_None + || gB_MenuShowing[player.ID] && GetClientAvgLoss(player.ID, NetFlow_Both) > EPSILON + || gB_MenuShowing[player.ID] && player.TimerRunning && !player.Paused && player.TimerText == TimerText_TPMenu + || gB_MenuShowing[player.ID] && force) + { + ShowTPMenu(player, info); + } + } + else + { + // There is no need to update this very often as there's no menu selection to be done here. + if (GetClientMenu(client) == MenuSource_None + || gB_MenuShowing[player.ID] && player.TimerRunning && !player.Paused && player.TimerText == TimerText_TPMenu) + { + ShowPanel(player, info); + } + } + } + else if (player.ObserverTarget != -1) // If the player is spectating someone else + { + // Check if the replay plugin wants to display the replay control menu. + if (!(IsFakeClient(player.ObserverTarget) && gB_GOKZReplays && GOKZ_RP_UpdateReplayControlMenu(client))) + { + ShowPanel(player, info); + } + } + + oldCanMakeCP[client] = player.CanMakeCheckpoint; + oldCanTP[client] = player.CanTeleportToCheckpoint; + oldCanPrevCP[client] = player.CanPrevCheckpoint; + oldCanNextCP[client] = player.CanNextCheckpoint; + oldCanUndoTP[client] = player.CanUndoTeleport; + oldCanPause[client] = player.CanPause; + oldCanResume[client] = player.CanResume; + forceRefresh[client] = false; +} + +static void ShowPanel(KZPlayer player, HUDInfo info) +{ + char panelTitle[256]; + // Spectator List + if (player.ShowSpectators >= ShowSpecs_Number && player.SpecListPosition == SpecListPosition_TPMenu) + { + Format(panelTitle, sizeof(panelTitle), "%s", FormatSpectatorTextForMenu(player, info)); + } + // Timer panel + if (player.TimerText == TimerText_TPMenu && info.TimerRunning) + { + if (panelTitle[0] != '\0') + { + Format(panelTitle, sizeof(panelTitle), "%s \n%s", panelTitle, FormatTimerTextForMenu(player, info)); + } + else + { + Format(panelTitle, sizeof(panelTitle), "%s", FormatTimerTextForMenu(player, info)); + } + if (info.TimeType == TimeType_Nub && info.CurrentTeleport != 0) + { + Format(panelTitle, sizeof(panelTitle), "%s\n%t", panelTitle, "TP Menu - Spectator Teleports", info.CurrentTeleport); + } + } + + if (panelTitle[0] != '\0' && GetClientMenu(player.ID) == MenuSource_None || gB_MenuShowing[player.ID]) + { + Panel panel = new Panel(null); + panel.SetTitle(panelTitle); + panel.Send(player.ID, PanelHandler_Menu, MENU_TIME_FOREVER); + + delete panel; + gB_MenuShowing[player.ID] = true; + } +} + +static void ShowTPMenu(KZPlayer player, HUDInfo info) +{ + Menu menu = new Menu(MenuHandler_TPMenu); + menu.OptionFlags = MENUFLAG_NO_SOUND; + menu.ExitButton = false; + menu.Pagination = MENU_NO_PAGINATION; + TPMenuSetTitle(player, menu, info); + TPMenuAddItems(player, menu); + menu.Display(player.ID, MENU_TIME_FOREVER); + gB_MenuShowing[player.ID] = true; +} + +static void TPMenuSetTitle(KZPlayer player, Menu menu, HUDInfo info) +{ + char title[256]; + if (player.ShowSpectators >= ShowSpecs_Number && player.SpecListPosition == SpecListPosition_TPMenu) + { + Format(title, sizeof(title), "%s", FormatSpectatorTextForMenu(player, info)); + } + if (player.TimerRunning && player.TimerText == TimerText_TPMenu) + { + if (title[0] != '\0') + { + Format(title, sizeof(title), "%s \n%s", title, FormatTimerTextForMenu(player, info)); + } + else + { + Format(title, sizeof(title), "%s", FormatTimerTextForMenu(player, info)); + } + } + if (title[0] != '\0') + { + menu.SetTitle(title); + } +} + +static void TPMenuAddItems(KZPlayer player, Menu menu) +{ + switch (player.TPMenu) + { + case TPMenu_Simple: + { + TPMenuAddItemCheckpoint(player, menu); + TPMenuAddItemTeleport(player, menu); + TPMenuAddItemPause(player, menu); + TPMenuAddItemStart(player, menu); + } + case TPMenu_Advanced: + { + TPMenuAddItemCheckpoint(player, menu); + TPMenuAddItemTeleport(player, menu); + TPMenuAddItemPrevCheckpoint(player, menu); + TPMenuAddItemNextCheckpoint(player, menu); + TPMenuAddItemUndo(player, menu); + TPMenuAddItemPause(player, menu); + TPMenuAddItemStart(player, menu); + } + } +} + +static void TPMenuAddItemCheckpoint(KZPlayer player, Menu menu) +{ + char display[24]; + FormatEx(display, sizeof(display), "%T", "TP Menu - Checkpoint", player.ID); + if (player.TimerRunning) + { + Format(display, sizeof(display), "%s #%d", display, player.CheckpointCount); + } + + // Legacy behavior: Always able to make checkpoint attempts. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Enabled && !player.CanMakeCheckpoint) + { + menu.AddItem(ITEM_INFO_CHECKPOINT, display, ITEMDRAW_DISABLED); + } + else + { + menu.AddItem(ITEM_INFO_CHECKPOINT, display, ITEMDRAW_DEFAULT); + } + +} + +static void TPMenuAddItemTeleport(KZPlayer player, Menu menu) +{ + char display[24]; + FormatEx(display, sizeof(display), "%T", "TP Menu - Teleport", player.ID); + if (player.TimerRunning) + { + Format(display, sizeof(display), "%s #%d", display, player.TeleportCount); + } + + // Legacy behavior: Only able to make TP attempts when there is a checkpoint. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanTeleportToCheckpoint) + { + menu.AddItem(ITEM_INFO_TELEPORT, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_TELEPORT, display, ITEMDRAW_DISABLED); + } +} + +static void TPMenuAddItemPrevCheckpoint(KZPlayer player, Menu menu) +{ + char display[24]; + FormatEx(display, sizeof(display), "%T", "TP Menu - Prev CP", player.ID); + + // Legacy behavior: Only able to do prev CP when there is a previous checkpoint to go back to. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanPrevCheckpoint) + { + menu.AddItem(ITEM_INFO_PREV, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_PREV, display, ITEMDRAW_DISABLED); + } +} + +static void TPMenuAddItemNextCheckpoint(KZPlayer player, Menu menu) +{ + char display[24]; + FormatEx(display, sizeof(display), "%T", "TP Menu - Next CP", player.ID); + + // Legacy behavior: Only able to do prev CP when there is a next checkpoint to go forward to. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanNextCheckpoint) + { + menu.AddItem(ITEM_INFO_NEXT, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_NEXT, display, ITEMDRAW_DISABLED); + } +} + +static void TPMenuAddItemUndo(KZPlayer player, Menu menu) +{ + char display[24]; + FormatEx(display, sizeof(display), "%T", "TP Menu - Undo TP", player.ID); + + // Legacy behavior: Only able to attempt to undo TP when it is allowed. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanUndoTeleport) + { + menu.AddItem(ITEM_INFO_UNDO, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_UNDO, display, ITEMDRAW_DISABLED); + } + +} + +static void TPMenuAddItemPause(KZPlayer player, Menu menu) +{ + char display[24]; + + // Legacy behavior: Always able to attempt to pause. + if (gI_DynamicMenu[player.ID] == DynamicMenu_Enabled) + { + if (player.Paused) + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Resume", player.ID); + if (player.CanResume) + { + menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DISABLED); + } + } + else + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Pause", player.ID); + if (player.CanPause) + { + menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DISABLED); + } + } + } + else + { + if (player.Paused) + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Resume", player.ID); + } + else + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Pause", player.ID); + } + menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT); + } +} + +static void TPMenuAddItemStart(KZPlayer player, Menu menu) +{ + char display[24]; + if (player.StartPositionType == StartPositionType_Spawn) + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Respawn", player.ID); + menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT); + } + else if (player.TimerRunning) + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Restart", player.ID); + menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT); + } + else + { + FormatEx(display, sizeof(display), "%T", "TP Menu - Start", player.ID); + menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-jumpbeam.sp b/sourcemod/scripting/gokz-jumpbeam.sp new file mode 100644 index 0000000..9f5b48d --- /dev/null +++ b/sourcemod/scripting/gokz-jumpbeam.sp @@ -0,0 +1,325 @@ +#include <sourcemod> + +#include <sdktools> + +#include <gokz/core> +#include <gokz/jumpbeam> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Jump Beam", + author = "DanZay", + description = "Provides option to leave behind a trail when in midair", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-jumpbeam.txt" + +float gF_OldOrigin[MAXPLAYERS + 1][3]; +bool gB_OldDucking[MAXPLAYERS + 1]; +int gI_BeamModel; +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemsJB[JBOPTION_COUNT]; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-jumpbeam"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-jumpbeam.phrases"); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client)) + { + return; + } + + OnPlayerRunCmdPost_JumpBeam(client); + UpdateOldVariables(client); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + gI_BeamModel = PrecacheModel("materials/sprites/laserbeam.vmt", true); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + + + +// =====[ GENERAL ]===== + +void UpdateOldVariables(int client) +{ + if (IsPlayerAlive(client)) + { + Movement_GetOrigin(client, gF_OldOrigin[client]); + gB_OldDucking[client] = Movement_GetDucking(client); + } +} + + + +// =====[ JUMP BEAM ]===== + +void OnPlayerRunCmdPost_JumpBeam(int targetClient) +{ + // In this case, spectators are handled from the target + // client's OnPlayerRunCmd call, otherwise the jump + // beam will be all broken up. + + KZPlayer targetPlayer = KZPlayer(targetClient); + + if (targetPlayer.Fake || !targetPlayer.Alive || targetPlayer.OnGround || !targetPlayer.ValidJump) + { + return; + } + + // Send to self + SendJumpBeam(targetPlayer, targetPlayer); + + // Send to spectators + for (int client = 1; client <= MaxClients; client++) + { + KZPlayer player = KZPlayer(client); + if (player.InGame && !player.Alive && player.ObserverTarget == targetClient) + { + SendJumpBeam(player, targetPlayer); + } + } +} + +void SendJumpBeam(KZPlayer player, KZPlayer targetPlayer) +{ + if (player.JBType == JBType_Disabled) + { + return; + } + + switch (player.JBType) + { + case JBType_Feet:SendFeetJumpBeam(player, targetPlayer); + case JBType_Head:SendHeadJumpBeam(player, targetPlayer); + case JBType_FeetAndHead: + { + SendFeetJumpBeam(player, targetPlayer); + SendHeadJumpBeam(player, targetPlayer); + } + case JBType_Ground:SendGroundJumpBeam(player, targetPlayer); + } +} + +void SendFeetJumpBeam(KZPlayer player, KZPlayer targetPlayer) +{ + float origin[3], beamStart[3], beamEnd[3]; + int beamColour[4]; + targetPlayer.GetOrigin(origin); + + beamStart = gF_OldOrigin[targetPlayer.ID]; + beamEnd = origin; + GetJumpBeamColour(targetPlayer, beamColour); + + TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0); + TE_SendToClient(player.ID); +} + +void SendHeadJumpBeam(KZPlayer player, KZPlayer targetPlayer) +{ + float origin[3], beamStart[3], beamEnd[3]; + int beamColour[4]; + targetPlayer.GetOrigin(origin); + + beamStart = gF_OldOrigin[targetPlayer.ID]; + beamEnd = origin; + if (gB_OldDucking[targetPlayer.ID]) + { + beamStart[2] = beamStart[2] + 54.0; + } + else + { + beamStart[2] = beamStart[2] + 72.0; + } + if (targetPlayer.Ducking) + { + beamEnd[2] = beamEnd[2] + 54.0; + } + else + { + beamEnd[2] = beamEnd[2] + 72.0; + } + GetJumpBeamColour(targetPlayer, beamColour); + + TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0); + TE_SendToClient(player.ID); +} + +void SendGroundJumpBeam(KZPlayer player, KZPlayer targetPlayer) +{ + float origin[3], takeoffOrigin[3], beamStart[3], beamEnd[3]; + int beamColour[4]; + targetPlayer.GetOrigin(origin); + targetPlayer.GetTakeoffOrigin(takeoffOrigin); + + beamStart = gF_OldOrigin[targetPlayer.ID]; + beamEnd = origin; + beamStart[2] = takeoffOrigin[2] + 0.1; + beamEnd[2] = takeoffOrigin[2] + 0.1; + GetJumpBeamColour(targetPlayer, beamColour); + + TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0); + TE_SendToClient(player.ID); +} + +void GetJumpBeamColour(KZPlayer targetPlayer, int colour[4]) +{ + float velocity[3]; + targetPlayer.GetVelocity(velocity); + if (targetPlayer.Ducking) + { + colour = { 255, 0, 0, 110 }; // Red + } + else if( velocity[2] < 140 && velocity[2] > 0 ) { + colour = { 255, 220, 0, 110 }; + } + else + { + colour = { 0, 255, 0, 110 }; // Green + } +} + + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void RegisterOptions() +{ + for (JBOption option; option < JBOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_JBOptionNames[option], gC_JBOptionDescriptions[option], + OptionType_Int, gI_JBOptionDefaultValues[option], 0, gI_JBOptionCounts[option] - 1); + } +} + + + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + + for (int option = 0; option < view_as<int>(JBOPTION_COUNT); option++) + { + gTMO_ItemsJB[option] = gTM_Options.AddItem(gC_JBOptionNames[option], TopMenuHandler_General, gTMO_CatGeneral); + } +} + +public void TopMenuHandler_General(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + JBOption option = JBOPTION_INVALID; + for (int i = 0; i < view_as<int>(JBOPTION_COUNT); i++) + { + if (topobj_id == gTMO_ItemsJB[i]) + { + option = view_as<JBOption>(i); + break; + } + } + + if (option == JBOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case JBOption_Type: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_JBOptionPhrases[option], param, + gC_JBTypePhrases[GOKZ_JB_GetOption(param, option)], param); + } + } + } + else if (action == TopMenuAction_SelectOption) + { + switch (option) + { + default: + { + GOKZ_JB_CycleOption(param, option); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-jumpstats.sp b/sourcemod/scripting/gokz-jumpstats.sp new file mode 100644 index 0000000..74d9220 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats.sp @@ -0,0 +1,216 @@ +#include <sourcemod> + +#include <dhooks> +#include <sdkhooks> +#include <sdktools> + +#include <gokz/core> +#include <gokz/jumpstats> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/localdb> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Jumpstats", + author = "DanZay", + description = "Tracks and outputs movement statistics", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-jumpstats.txt" + +// This must be global because it's both used by jump tracking and validating. +bool gB_SpeedJustModifiedExternally[MAXPLAYERS + 1]; + +#include "gokz-jumpstats/api.sp" +#include "gokz-jumpstats/commands.sp" +#include "gokz-jumpstats/distance_tiers.sp" +#include "gokz-jumpstats/jump_reporting.sp" +#include "gokz-jumpstats/jump_tracking.sp" +#include "gokz-jumpstats/jump_validating.sp" +#include "gokz-jumpstats/options.sp" +#include "gokz-jumpstats/options_menu.sp" + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-jumpstats"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-jumpstats.phrases"); + + LoadBroadcastTiers(); + CreateGlobalForwards(); + RegisterCommands(); + + OnPluginStart_JumpTracking(); + OnPluginStart_JumpValidating(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + HookClientEvents(client); + OnClientPutInServer_Options(client); + OnClientPutInServer_JumpTracking(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + OnPlayerRunCmd_JumpTracking(client, buttons, tickcount); + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + OnPlayerRunCmdPost_JumpTracking(client); +} + +public void Movement_OnStartTouchGround(int client) +{ + OnStartTouchGround_JumpTracking(client); +} + +public void GOKZ_OnJumpInvalidated(int client) +{ + OnJumpInvalidated_JumpTracking(client); +} + +public void GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + OnJumpValidated_JumpTracking(client, jumped, ladderJump, jumpbug); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + OnOptionChanged_JumpTracking(client, option); + OnOptionChanged_Options(client, option, newValue); +} + +public void GOKZ_JS_OnLanding(Jump jump) +{ + OnLanding_JumpReporting(jump); +} + +public void GOKZ_JS_OnFailstat(Jump jump) +{ + OnFailstat_FailstatReporting(jump); +} + +public void GOKZ_JS_OnJumpstatAlways(Jump jump) +{ + OnJumpstatAlways_JumpstatAlwaysReporting(jump); +} + +public void GOKZ_JS_OnFailstatAlways(Jump jump) +{ + OnFailstatAlways_FailstatAlwaysReporting(jump); +} + +public void SDKHook_StartTouch_Callback(int client, int touched) // SDKHook_StartTouchPost +{ + OnStartTouch_JumpTracking(client, touched); +} + +public void SDKHook_Touch_CallBack(int client, int touched) +{ + OnTouch_JumpTracking(client); +} + +public void SDKHook_EndTouch_Callback(int client, int touched) // SDKHook_EndTouchPost +{ + OnEndTouch_JumpTracking(client, touched); +} + +public void GOKZ_OnTeleport(int client) +{ + OnTeleport_FailstatAlways(client); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + OnMapStart_JumpReporting(); + OnMapStart_DistanceTiers(); +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void GOKZ_OnSlap(int client) +{ + InvalidateJumpstat(client); +} + + + +// =====[ PRIVATE ]===== + +static void HookClientEvents(int client) +{ + SDKHook(client, SDKHook_StartTouchPost, SDKHook_StartTouch_Callback); + SDKHook(client, SDKHook_TouchPost, SDKHook_Touch_CallBack); + SDKHook(client, SDKHook_EndTouchPost, SDKHook_EndTouch_Callback); +} diff --git a/sourcemod/scripting/gokz-jumpstats/api.sp b/sourcemod/scripting/gokz-jumpstats/api.sp new file mode 100644 index 0000000..2625fda --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/api.sp @@ -0,0 +1,78 @@ +static GlobalForward H_OnTakeoff; +static GlobalForward H_OnLanding; +static GlobalForward H_OnFailstat; +static GlobalForward H_OnJumpstatAlways; +static GlobalForward H_OnFailstatAlways; +static GlobalForward H_OnJumpInvalidated; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnTakeoff = new GlobalForward("GOKZ_JS_OnTakeoff", ET_Ignore, Param_Cell, Param_Cell); + H_OnLanding = new GlobalForward("GOKZ_JS_OnLanding", ET_Ignore, Param_Array); + H_OnFailstat = new GlobalForward("GOKZ_JS_OnFailstat", ET_Ignore, Param_Array); + H_OnJumpstatAlways = new GlobalForward("GOKZ_JS_OnJumpstatAlways", ET_Ignore, Param_Array); + H_OnFailstatAlways = new GlobalForward("GOKZ_JS_OnFailstatAlways", ET_Ignore, Param_Array); + H_OnJumpInvalidated = new GlobalForward("GOKZ_JS_OnJumpInvalidated", ET_Ignore, Param_Cell); +} + +void Call_OnTakeoff(int client, int jumpType) +{ + Call_StartForward(H_OnTakeoff); + Call_PushCell(client); + Call_PushCell(jumpType); + Call_Finish(); +} + +void Call_OnLanding(Jump jump) +{ + Call_StartForward(H_OnLanding); + Call_PushArray(jump, sizeof(jump)); + Call_Finish(); +} + +void Call_OnJumpInvalidated(int client) +{ + Call_StartForward(H_OnJumpInvalidated); + Call_PushCell(client); + Call_Finish(); +} + +void Call_OnFailstat(Jump jump) +{ + Call_StartForward(H_OnFailstat); + Call_PushArray(jump, sizeof(jump)); + Call_Finish(); +} + +void Call_OnJumpstatAlways(Jump jump) +{ + Call_StartForward(H_OnJumpstatAlways); + Call_PushArray(jump, sizeof(jump)); + Call_Finish(); +} + +void Call_OnFailstatAlways(Jump jump) +{ + Call_StartForward(H_OnFailstatAlways); + Call_PushArray(jump, sizeof(jump)); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_JS_InvalidateJump", Native_InvalidateJump); +} + +public int Native_InvalidateJump(Handle plugin, int numParams) +{ + InvalidateJumpstat(GetNativeCell(1)); + return 0; +} diff --git a/sourcemod/scripting/gokz-jumpstats/commands.sp b/sourcemod/scripting/gokz-jumpstats/commands.sp new file mode 100644 index 0000000..991f9e6 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/commands.sp @@ -0,0 +1,28 @@ + +void RegisterCommands() +{ + RegConsoleCmd("sm_jso", CommandJumpstatsOptions, "[KZ] Open the jumpstats options menu."); + RegConsoleCmd("sm_jsalways", CommandAlwaysJumpstats, "[KZ] Toggle the always-on jumpstats."); +} + +public Action CommandJumpstatsOptions(int client, int args) +{ + DisplayJumpstatsOptionsMenu(client); + return Plugin_Handled; +} + +public Action CommandAlwaysJumpstats(int client, int args) +{ + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Enabled) + { + GOKZ_JS_SetOption(client, JSOption_JumpstatsAlways, JSToggleOption_Disabled); + GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Jumpstats Always - Disable"); + } + else + { + GOKZ_JS_SetOption(client, JSOption_JumpstatsAlways, JSToggleOption_Enabled); + GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Jumpstats Always - Enable"); + } + + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp b/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp new file mode 100644 index 0000000..3abe8e9 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp @@ -0,0 +1,118 @@ +/* + Categorises jumps into tiers based on their distance. + Tier thresholds are loaded from a config. +*/ + + + +static float distanceTiers[JUMPTYPE_COUNT - 3][MODE_COUNT][DISTANCETIER_COUNT]; + + + +// =====[ PUBLIC ]===== + +int GetDistanceTier(int jumpType, int mode, float distance, float offset = 0.0) +{ + // No tiers given for 'Invalid' jumps. + if (jumpType == JumpType_Invalid || jumpType == JumpType_FullInvalid + || jumpType == JumpType_Fall || jumpType == JumpType_Other + || jumpType != JumpType_LadderJump && offset < -JS_OFFSET_EPSILON + || distance > JS_MAX_JUMP_DISTANCE) + { + // TODO Give a tier to "Other" jumps + // TODO Give a tier to offset jumps + return DistanceTier_None; + } + + // Get highest tier distance that the jump beats + int tier = DistanceTier_None; + while (tier + 1 < DISTANCETIER_COUNT && distance >= GetDistanceTierDistance(jumpType, mode, tier + 1)) + { + tier++; + } + + return tier; +} + +float GetDistanceTierDistance(int jumpType, int mode, int tier) +{ + return distanceTiers[jumpType][mode][tier]; +} + +bool LoadBroadcastTiers() +{ + char chatTier[16], soundTier[16]; + + KeyValues kv = new KeyValues("broadcast"); + if (!kv.ImportFromFile(JS_CFG_BROADCAST)) + { + return false; + } + + kv.GetString("chat", chatTier, sizeof(chatTier), "ownage"); + kv.GetString("sound", soundTier, sizeof(chatTier), ""); + + for (int tier = 0; tier < sizeof(gC_DistanceTierKeys); tier++) + { + if (StrEqual(chatTier, gC_DistanceTierKeys[tier])) + { + gI_JSOptionDefaults[JSOption_MinChatBroadcastTier] = tier; + } + if (StrEqual(soundTier, gC_DistanceTierKeys[tier])) + { + gI_JSOptionDefaults[JSOption_MinSoundBroadcastTier] = tier; + } + } + + delete kv; + return true; +} + + + +// =====[ EVENTS ]===== + +void OnMapStart_DistanceTiers() +{ + if (!LoadDistanceTiers()) + { + SetFailState("Failed to load file: \"%s\".", JS_CFG_TIERS); + } +} + + + +// =====[ PRIVATE ]===== + +static bool LoadDistanceTiers() +{ + KeyValues kv = new KeyValues("tiers"); + if (!kv.ImportFromFile(JS_CFG_TIERS)) + { + return false; + } + + // It's a bit of a hack to exclude non-tiered jumptypes + for (int jumpType = 0; jumpType < sizeof(gC_JumpTypeKeys) - 3; jumpType++) + { + if (!kv.JumpToKey(gC_JumpTypeKeys[jumpType])) + { + return false; + } + for (int mode = 0; mode < MODE_COUNT; mode++) + { + if (!kv.JumpToKey(gC_ModeKeys[mode])) + { + return false; + } + for (int tier = DistanceTier_Meh; tier < DISTANCETIER_COUNT; tier++) + { + distanceTiers[jumpType][mode][tier] = kv.GetFloat(gC_DistanceTierKeys[tier]); + } + kv.GoBack(); + } + kv.GoBack(); + } + delete kv; + return true; +} diff --git a/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp b/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp new file mode 100644 index 0000000..31a1bb2 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp @@ -0,0 +1,508 @@ +/* + Chat and console reports for jumpstats. +*/ + +static char sounds[DISTANCETIER_COUNT][256]; + + + +// =====[ PUBLIC ]===== + +void PlayJumpstatSound(int client, int tier) +{ + int soundOption = GOKZ_JS_GetOption(client, JSOption_MinSoundTier); + if (tier <= DistanceTier_Meh || soundOption == DistanceTier_None || soundOption > tier) + { + return; + } + + GOKZ_EmitSoundToClient(client, sounds[tier], _, "Jumpstats"); +} + + + +// =====[ EVENTS ]===== + +void OnMapStart_JumpReporting() +{ + if (!LoadSounds()) + { + SetFailState("Failed to load file: \"%s\".", JS_CFG_SOUNDS); + } +} + +void OnLanding_JumpReporting(Jump jump) +{ + int minTier; + int tier = GetDistanceTier(jump.type, GOKZ_GetCoreOption(jump.jumper, Option_Mode), jump.distance, jump.offset); + if (tier == DistanceTier_None) + { + return; + } + + // Report the jumpstat to the client and their spectators + DoJumpstatsReport(jump.jumper, jump, tier); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && client != jump.jumper) + { + if (GetObserverTarget(client) == jump.jumper) + { + DoJumpstatsReport(client, jump, tier); + } + else + { + minTier = GOKZ_JS_GetOption(client, JSOption_MinChatBroadcastTier); + if (minTier != 0 && tier >= minTier) + { + GOKZ_PrintToChat(client, true, "%t", "Broadcast Jumpstat Chat Report", + gC_DistanceTierChatColours[tier], + jump.jumper, + jump.distance, + gC_JumpTypes[jump.originalType]); + DoConsoleReport(client, false, jump, tier, "Console Jump Header"); + } + + minTier = GOKZ_JS_GetOption(client, JSOption_MinSoundBroadcastTier); + if (minTier != 0 && tier >= minTier) + { + PlayJumpstatSound(client, tier); + } + } + } + } +} + +void OnFailstat_FailstatReporting(Jump jump) +{ + int tier = GetDistanceTier(jump.type, GOKZ_GetCoreOption(jump.jumper, Option_Mode), jump.distance); + if (tier == DistanceTier_None) + { + return; + } + + // Report the failstat to the client and their spectators + DoFailstatReport(jump.jumper, jump, tier); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper) + { + DoFailstatReport(client, jump, tier); + } + } +} + +void OnJumpstatAlways_JumpstatAlwaysReporting(Jump jump) +{ + DoJumpstatAlwaysReport(jump.jumper, jump); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper) + { + DoJumpstatAlwaysReport(client, jump); + } + } +} + + +void OnFailstatAlways_FailstatAlwaysReporting(Jump jump) +{ + DoFailstatAlwaysReport(jump.jumper, jump); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper) + { + DoFailstatAlwaysReport(client, jump); + } + } +} + + + +// =====[ PRIVATE ]===== + +static void DoJumpstatsReport(int client, Jump jump, int tier) +{ + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled) + { + return; + } + + DoChatReport(client, false, jump, tier); + DoConsoleReport(client, false, jump, tier, "Console Jump Header"); + PlayJumpstatSound(client, tier); +} + +static void DoFailstatReport(int client, Jump jump, int tier) +{ + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled) + { + return; + } + + DoChatReport(client, true, jump, tier); + DoConsoleReport(client, true, jump, tier, "Console Failstat Header"); +} + +static void DoJumpstatAlwaysReport(int client, Jump jump) +{ + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled || + GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled) + { + return; + } + + DoChatReport(client, false, jump, 1); + DoConsoleReport(client, false, jump, 1, "Console Jump Header"); +} + +static void DoFailstatAlwaysReport(int client, Jump jump) +{ + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled || + GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled) + { + return; + } + + DoChatReport(client, true, jump, 1); + DoConsoleReport(client, true, jump, 1, "Console Failstat Header"); +} + + + + +// CONSOLE REPORT + +static void DoConsoleReport(int client, bool isFailstat, Jump jump, int tier, char[] header) +{ + int minConsoleTier = GOKZ_JS_GetOption(client, JSOption_MinConsoleTier); + if ((minConsoleTier == 0 || minConsoleTier > tier) && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled + || isFailstat && GOKZ_JS_GetOption(client, JSOption_FailstatsConsole) == JSToggleOption_Disabled) + { + return; + } + + char releaseWString[32], blockString[32], edgeString[32], deviationString[32], missString[32]; + + if (jump.originalType == JumpType_LongJump || + jump.originalType == JumpType_LadderJump || + jump.originalType == JumpType_WeirdJump || + jump.originalType == JumpType_LowpreWeirdJump) + { + FormatEx(releaseWString, sizeof(releaseWString), " %s", GetIntConsoleString(client, "W Release", jump.releaseW)); + } + else if (jump.crouchRelease < 20 && jump.crouchRelease > -20) + { + FormatEx(releaseWString, sizeof(releaseWString), " %s", GetIntConsoleString(client, "Crouch Release", jump.crouchRelease)); + } + + if (jump.miss > 0.0) + { + FormatEx(missString, sizeof(missString), " %s", GetFloatConsoleString2(client, "Miss", jump.miss)); + } + + if (jump.block > 0) + { + FormatEx(blockString, sizeof(blockString), " %s", GetIntConsoleString(client, "Block", jump.block)); + FormatEx(deviationString, sizeof(deviationString), " %s", GetFloatConsoleString1(client, "Deviation", jump.deviation)); + } + + if (jump.edge > 0.0 || (jump.block > 0 && jump.edge == 0.0)) + { + FormatEx(edgeString, sizeof(edgeString), " %s", GetFloatConsoleString2(client, "Edge", jump.edge)); + } + + PrintToConsole(client, "%t", header, jump.jumper, jump.distance, gC_JumpTypes[jump.originalType]); + + PrintToConsole(client, "%s%s%s%s %s %s %s %s%s %s %s%s %s %s %s %s %s", + gC_ModeNamesShort[GOKZ_GetCoreOption(jump.jumper, Option_Mode)], + blockString, + edgeString, + missString, + GetIntConsoleString(client, jump.strafes == 1 ? "Strafe" : "Strafes", jump.strafes), + GetSyncConsoleString(client, jump.sync), + GetFloatConsoleString2(client, "Pre", jump.preSpeed), + GetFloatConsoleString2(client, "Max", jump.maxSpeed), + releaseWString, + GetIntConsoleString(client, "Overlap", jump.overlap), + GetIntConsoleString(client, "Dead Air", jump.deadair), + deviationString, + GetWidthConsoleString(client, jump.width, jump.strafes), + GetFloatConsoleString1(client, "Height", jump.height), + GetIntConsoleString(client, "Airtime", jump.duration), + GetFloatConsoleString1(client, "Offset", jump.offset), + GetIntConsoleString(client, "Crouch Ticks", jump.crouchTicks)); + + PrintToConsole(client, " #. %12t%12t%12t%12t%12t%9t%t", "Sync (Table)", "Gain (Table)", "Loss (Table)", "Airtime (Table)", "Width (Table)", "Overlap (Table)", "Dead Air (Table)"); + if (jump.strafes_ticks[0] > 0) + { + PrintToConsole(client, " 0. ---- ----- ----- %3.0f%% ----- -- --", GetStrafeAirtime(jump, 0)); + } + for (int strafe = 1; strafe <= jump.strafes && strafe < JS_MAX_TRACKED_STRAFES; strafe++) + { + PrintToConsole(client, + " %2d. %3.0f%% %5.2f %5.2f %3.0f%% %5.1f° %2d %2d", + strafe, + GetStrafeSync(jump, strafe), + jump.strafes_gain[strafe], + jump.strafes_loss[strafe], + GetStrafeAirtime(jump, strafe), + FloatAbs(jump.strafes_width[strafe]), + jump.strafes_overlap[strafe], + jump.strafes_deadair[strafe]); + } + PrintToConsole(client, ""); // New line +} + +static char[] GetSyncConsoleString(int client, float sync) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), "| %.0f%% %T", sync, "Sync", client); + return resultString; +} + +static char[] GetWidthConsoleString(int client, float width, int strafes) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), "| %.1f° %T", GetAverageStrafeWidth(strafes, width), "Width", client); + return resultString; +} + +// I couldn't really merge those together +static char[] GetFloatConsoleString1(int client, const char[] stat, float value) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), "| %.1f %T", value, stat, client); + return resultString; +} + +static char[] GetFloatConsoleString2(int client, const char[] stat, float value) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), "| %.2f %T", value, stat, client); + return resultString; +} + +static char[] GetIntConsoleString(int client, const char[] stat, int value) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), "| %d %T", value, stat, client); + return resultString; +} + + + +// CHAT REPORT + +static void DoChatReport(int client, bool isFailstat, Jump jump, int tier) +{ + int minChatTier = GOKZ_JS_GetOption(client, JSOption_MinChatTier); + if ((minChatTier == 0 || minChatTier > tier) // 0 means disabled + && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled) + { + return; + } + + char typePostfix[3], color[16], blockStats[32], extBlockStats[32]; + char releaseStats[32], edgeOffset[64], offsetEdge[32], missString[32]; + + if (isFailstat) + { + if (GOKZ_JS_GetOption(client, JSOption_FailstatsChat) == JSToggleOption_Disabled + && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled) + { + return; + } + strcopy(typePostfix, sizeof(typePostfix), "-F"); + strcopy(color, sizeof(color), "{grey}"); + } + else + { + strcopy(color, sizeof(color), gC_DistanceTierChatColours[tier]); + } + + if (jump.block > 0) + { + FormatEx(blockStats, sizeof(blockStats), " | %s", GetFloatChatString(client, "Edge", jump.edge)); + FormatEx(extBlockStats, sizeof(extBlockStats), " | %s", GetFloatChatString(client, "Deviation", jump.deviation)); + } + + if (jump.miss > 0.0) + { + FormatEx(missString, sizeof(missString), " | %s", GetFloatChatString(client, "Miss", jump.miss)); + } + + if (jump.edge > 0.0 || (jump.block > 0 && jump.edge == 0.0)) + { + if (jump.originalType == JumpType_LadderJump) + { + FormatEx(offsetEdge, sizeof(offsetEdge), " | %s", GetFloatChatString(client, "Edge", jump.edge)); + } + else + { + FormatEx(edgeOffset, sizeof(edgeOffset), " | %s", GetFloatChatString(client, "Edge", jump.edge)); + } + } + + if (jump.originalType == JumpType_LongJump || + jump.originalType == JumpType_LadderJump || + jump.originalType == JumpType_WeirdJump) + { + if (jump.releaseW >= 20 || jump.releaseW <= -20) + { + FormatEx(releaseStats, sizeof(releaseStats), " | {red}✗ {grey}W", GetReleaseChatString(client, "W Release", jump.releaseW)); + } + else + { + FormatEx(releaseStats, sizeof(releaseStats), " | %s", GetReleaseChatString(client, "W Release", jump.releaseW)); + } + } + else if (jump.crouchRelease < 20 && jump.crouchRelease > -20) + { + FormatEx(releaseStats, sizeof(releaseStats), " | %s", GetReleaseChatString(client, "Crouch Release", jump.crouchRelease)); + } + + if (jump.originalType == JumpType_LadderJump) + { + FormatEx(edgeOffset, sizeof(edgeOffset), " | %s", GetFloatChatString(client, "Offset Short", jump.offset)); + } + else + { + FormatEx(offsetEdge, sizeof(offsetEdge), " | %s", GetFloatChatString(client, "Offset", jump.offset)); + } + + GOKZ_PrintToChat(client, true, + "%s%s%s{grey}: %s%.1f{grey} | %s | %s%s%s", + color, + gC_JumpTypesShort[jump.originalType], + typePostfix, + color, + jump.distance, + GetStrafesSyncChatString(client, jump.strafes, jump.sync), + GetSpeedChatString(client, jump.preSpeed, jump.maxSpeed), + edgeOffset, + releaseStats); + + if (GOKZ_JS_GetOption(client, JSOption_ExtendedChatReport) == JSToggleOption_Enabled) + { + GOKZ_PrintToChat(client, false, + "%s | %s%s%s | %s | %s%s", + GetIntChatString(client, "Overlap", jump.overlap), + GetIntChatString(client, "Dead Air", jump.deadair), + offsetEdge, + extBlockStats, + GetWidthChatString(client, jump.width, jump.strafes), + GetFloatChatString(client, "Height", jump.height), + missString); + } +} + +static char[] GetStrafesSyncChatString(int client, int strafes, float sync) +{ + char resultString[64]; + FormatEx(resultString, sizeof(resultString), + "{lime}%d{grey} %T ({lime}%.0f%%%%{grey})", + strafes, "Strafes", client, sync); + return resultString; +} + +static char[] GetSpeedChatString(int client, float preSpeed, float maxSpeed) +{ + char resultString[64]; + FormatEx(resultString, sizeof(resultString), + "{lime}%.0f{grey} / {lime}%.0f{grey} %T", + preSpeed, maxSpeed, "Speed", client); + return resultString; +} + +static char[] GetReleaseChatString(int client, char[] releaseType, int release) +{ + char resultString[32]; + if (release == 0) + { + FormatEx(resultString, sizeof(resultString), + "{green}✓{grey} %T", + releaseType, client); + } + else if (release > 0) + { + FormatEx(resultString, sizeof(resultString), + "{red}+%d{grey} %T", + release, + releaseType, client); + } + else + { + FormatEx(resultString, sizeof(resultString), + "{blue}%d{grey} %T", + release, + releaseType, client); + } + return resultString; +} + +static char[] GetWidthChatString(int client, float width, int strafes) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), + "{lime}%.1f°{grey} %T", + GetAverageStrafeWidth(strafes, width), "Width", client); + return resultString; +} + +static float GetAverageStrafeWidth(int strafes, float totalWidth) +{ + if (strafes == 0) + { + return 0.0; + } + + return totalWidth / strafes; +} + +static char[] GetFloatChatString(int client, const char[] stat, float value) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), + "{lime}%.1f{grey} %T", + value, stat, client); + return resultString; +} + +static char[] GetIntChatString(int client, const char[] stat, int value) +{ + char resultString[32]; + FormatEx(resultString, sizeof(resultString), + "{lime}%d{grey} %T", + value, stat, client); + return resultString; +} + + + +// SOUNDS + +static bool LoadSounds() +{ + KeyValues kv = new KeyValues("sounds"); + if (!kv.ImportFromFile(JS_CFG_SOUNDS)) + { + return false; + } + + char downloadPath[256]; + for (int tier = DistanceTier_Impressive; tier < DISTANCETIER_COUNT; tier++) + { + kv.GetString(gC_DistanceTierKeys[tier], sounds[tier], sizeof(sounds[])); + FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", sounds[tier]); + AddFileToDownloadsTable(downloadPath); + PrecacheSound(sounds[tier], true); + } + + delete kv; + return true; +} diff --git a/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp b/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp new file mode 100644 index 0000000..acd9442 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp @@ -0,0 +1,1624 @@ +/* + Tracking of jump type, speed, strafes and more. +*/ + + + +// =====[ STRUCTS ]============================================================ + +enum struct Pose +{ + float position[3]; + float orientation[3]; + float velocity[3]; + float speed; + int duration; + int overlap; + int deadair; + int syncTicks; +} + + + +// =====[ GLOBAL VARIABLES ]=================================================== + +static ArrayList entityTouchList[MAXPLAYERS + 1]; +static int entityTouchDuration[MAXPLAYERS + 1]; +static int lastNoclipTime[MAXPLAYERS + 1]; +static int lastDuckbugTime[MAXPLAYERS + 1]; +static int lastGroundSpeedCappedTime[MAXPLAYERS + 1]; +static int lastMovementProcessedTime[MAXPLAYERS + 1]; +static float lastJumpButtonTime[MAXPLAYERS + 1]; +static bool validCmd[MAXPLAYERS + 1]; // Whether no illegal action is detected +static const float playerMins[3] = { -16.0, -16.0, 0.0 }; +static const float playerMaxs[3] = { 16.0, 16.0, 0.0 }; +static const float playerMinsEx[3] = { -20.0, -20.0, 0.0 }; +static const float playerMaxsEx[3] = { 20.0, 20.0, 0.0 }; +static bool doFailstatAlways[MAXPLAYERS + 1]; +static bool isInAir[MAXPLAYERS + 1]; +static const Jump emptyJump; +static Handle acceptInputHook; + + +// =====[ DEFINITIONS ]======================================================== + +// We cannot return enum structs and it's annoying +// The modulo operator is broken, so we can't access this using negative numbers +// (https://github.com/alliedmodders/sourcepawn/issues/456). We use the method +// described here instead: https://stackoverflow.com/a/42131603/7421666 +#define pose(%1) (poseHistory[this.jumper][((this.poseIndex + (%1)) % JS_FAILSTATS_MAX_TRACKED_TICKS + JS_FAILSTATS_MAX_TRACKED_TICKS) % JS_FAILSTATS_MAX_TRACKED_TICKS]) + + + +// =====[ TRACKING ]=========================================================== + +// We cannot put that into the tracker struct +Pose poseHistory[MAXPLAYERS + 1][JS_FAILSTATS_MAX_TRACKED_TICKS]; + +enum struct JumpTracker +{ + Jump jump; + int jumper; + int jumpoffTick; + int poseIndex; + int strafeDirection; + int lastJumpTick; + int lastTeleportTick; + int lastType; + int lastWPressedTick; + int nextCrouchRelease; + int syncTicks; + int lastCrouchPressedTick; + int tickCount; + bool failstatBlockDetected; + bool failstatFailed; + bool failstatValid; + float failstatBlockHeight; + float takeoffOrigin[3]; + float takeoffVelocity[3]; + float position[3]; + + void Init(int jumper) + { + this.jumper = jumper; + this.jump.jumper = jumper; + this.nextCrouchRelease = 100; + this.tickCount = 0; + } + + + + // =====[ ENTRYPOINTS ]======================================================= + + void Reset(bool jumped, bool ladderJump, bool jumpbug) + { + // We need to do that before we reset the jump cause we need the + // offset and type of the previous jump + this.lastType = this.DetermineType(jumped, ladderJump, jumpbug); + + // We need this for weirdjump w-release + int releaseWTemp = this.jump.releaseW; + + // Reset all stats + this.jump = emptyJump; + this.jump.type = this.lastType; + this.jump.jumper = this.jumper; + this.syncTicks = 0; + this.strafeDirection = StrafeDirection_None; + this.jump.releaseW = 100; + + // We have to show this on the jumpbug stat, not the lj stat + this.jump.crouchRelease = this.nextCrouchRelease; + this.nextCrouchRelease = 100; + + // Handle weirdjump w-release + if (this.jump.type == JumpType_WeirdJump) + { + this.jump.releaseW = releaseWTemp; + } + + // Reset pose history + this.poseIndex = 0; + // Update the first tick if it is a jumpbug. + this.UpdateOnGround(); + } + + void Begin() + { + // Initialize stats + this.CalcTakeoff(); + this.AdjustLowpreJumptypes(); + + this.failstatBlockDetected = this.jump.type != JumpType_LadderJump; + this.failstatFailed = false; + this.failstatValid = false; + this.failstatBlockHeight = this.takeoffOrigin[2]; + + // Store the original type for the always stats + this.jump.originalType = this.jump.type; + + // Notify everyone about the takeoff + Call_OnTakeoff(this.jumper, this.jump.type); + } + + void Update() + { + this.UpdatePoseHistory(); + + float speed = pose(0).speed; + + // Fix certain props that don't give you base velocity + /* + We check for speed reduction for abuse; while prop abuses increase speed, + wall collision will very likely (if not always) result in a speed reduction. + */ + float actualSpeed = GetVectorHorizontalDistance(this.position, pose(-1).position) / GetTickInterval(); + if (FloatAbs(speed - actualSpeed) > JS_SPEED_MODIFICATION_TOLERANCE && this.jump.duration != 0) + { + if (actualSpeed <= pose(-1).speed) + { + pose(0).speed = actualSpeed; + } + // This check is needed if you land via ducking instead of moving (duckbug) + else if (FloatAbs(actualSpeed) > EPSILON) + { + this.Invalidate(); + } + } + // You shouldn't gain any vertical velocity during a jump. + // This would only happen if you get boosted back up somehow, or you edgebugged. + if (!Movement_GetOnGround(this.jumper) && pose(0).velocity[2] > pose(-1).velocity[2]) + { + this.Invalidate(); + } + + this.jump.height = FloatMax(this.jump.height, this.position[2] - this.takeoffOrigin[2]); + this.jump.maxSpeed = FloatMax(this.jump.maxSpeed, speed); + this.jump.crouchTicks += Movement_GetDucking(this.jumper) ? 1 : 0; + this.syncTicks += speed > pose(-1).speed ? 1 : 0; + this.jump.duration++; + + this.UpdateStrafes(); + this.UpdateFailstat(); + this.UpdatePoseStats(); + + this.lastType = this.jump.type; + } + + void End() + { + // The jump is so invalid we don't even have to bother. + // Also check if the player just teleported. + if (this.jump.type == JumpType_FullInvalid || + this.tickCount - this.lastTeleportTick < JS_MIN_TELEPORT_DELAY) + { + return; + } + + // Measure last tick of jumpstat + this.Update(); + + // Fix the edgebug for the current position + Movement_GetNobugLandingOrigin(this.jumper, this.position); + + // There are a couple bugs and exploits we have to check for + this.EndBugfixExploits(); + + // Calculate the last stats + this.jump.distance = this.CalcDistance(); + this.jump.sync = float(this.syncTicks) / float(this.jump.duration) * 100.0; + this.jump.offset = this.position[2] - this.takeoffOrigin[2]; + + this.EndBlockDistance(); + + // Make sure the ladder has no offset for ladder jumps + if (this.jump.type == JumpType_LadderJump) + { + this.TraceLadderOffset(this.position[2]); + } + + // Calculate always-on stats + if (GOKZ_JS_GetOption(this.jumper, JSOption_JumpstatsAlways) == JSToggleOption_Enabled) + { + this.EndAlwaysJumpstats(); + } + + // Call the appropriate functions for either regular or always stats + this.Callback(); + } + + void Invalidate() + { + if (this.jump.type != JumpType_Invalid && + this.jump.type != JumpType_FullInvalid) + { + this.jump.type = JumpType_Invalid; + Call_OnJumpInvalidated(this.jumper); + } + } + + + + // =====[ BEGIN HELPERS ]===================================================== + + void CalcTakeoff() + { + // MovementAPI now correctly calculates the takeoff origin + // and velocity for jumpbugs. What is wrong though, is how + // mode plugins set bhop prespeed. + // Jumpbug takeoff origin is correct. + Movement_GetTakeoffOrigin(this.jumper, this.takeoffOrigin); + Movement_GetTakeoffVelocity(this.jumper, this.takeoffVelocity); + if (this.jump.type == JumpType_Jumpbug || this.jump.type == JumpType_MultiBhop + || this.jump.type == JumpType_Bhop || this.jump.type == JumpType_LowpreBhop + || this.jump.type == JumpType_LowpreWeirdJump || this.jump.type == JumpType_WeirdJump) + { + // Move the origin to the ground. + // The difference can only be 2 units maximum. + float bhopOrigin[3]; + CopyVector(this.takeoffOrigin, bhopOrigin); + bhopOrigin[2] -= 2.0; + TraceHullPosition(this.takeoffOrigin, bhopOrigin, playerMins, playerMaxs, this.takeoffOrigin); + } + + this.jump.preSpeed = Movement_GetTakeoffSpeed(this.jumper); + poseHistory[this.jumper][0].speed = this.jump.preSpeed; + } + + void AdjustLowpreJumptypes() + { + // Exclude SKZ and VNL stats. + if (GOKZ_GetCoreOption(this.jumper, Option_Mode) == Mode_KZTimer) + { + if (this.jump.type == JumpType_Bhop && + this.jump.preSpeed < 360.0) + { + this.jump.type = JumpType_LowpreBhop; + } + else if (this.jump.type == JumpType_WeirdJump && + this.jump.preSpeed < 300.0) + { + this.jump.type = JumpType_LowpreWeirdJump; + } + } + } + + int DetermineType(bool jumped, bool ladderJump, bool jumpbug) + { + if (gB_SpeedJustModifiedExternally[this.jumper] || this.tickCount - this.lastTeleportTick < JS_MIN_TELEPORT_DELAY) + { + return JumpType_Invalid; + } + else if (ladderJump) + { + // Check for ladder gliding. + float curtime = GetGameTime(); + float ignoreLadderJumpTime = GetEntPropFloat(this.jumper, Prop_Data, "m_ignoreLadderJumpTime"); + // Check if the ladder glide period is still active and if the player held jump in that period. + if (ignoreLadderJumpTime > curtime && + ignoreLadderJumpTime - IGNORE_JUMP_TIME < lastJumpButtonTime[this.jumper] && lastJumpButtonTime[this.jumper] < ignoreLadderJumpTime) + { + return JumpType_Invalid; + } + if (jumped) + { + return JumpType_Ladderhop; + } + else + { + return JumpType_LadderJump; + } + } + else if (!jumped) + { + return JumpType_Fall; + } + else if (jumpbug) + { + // Check for no offset + // The origin and offset is now correct, no workaround needed + if (FloatAbs(this.jump.offset) < JS_OFFSET_EPSILON && this.lastType == JumpType_LongJump) + { + return JumpType_Jumpbug; + } + else + { + return JumpType_Invalid; + } + } + else if (this.HitBhop() && !this.HitDuckbugRecently()) + { + // Check for no offset + if (FloatAbs(this.jump.offset) < JS_OFFSET_EPSILON) + { + switch (this.lastType) + { + case JumpType_LongJump:return JumpType_Bhop; + case JumpType_Bhop:return JumpType_MultiBhop; + case JumpType_LowpreBhop:return JumpType_MultiBhop; + case JumpType_MultiBhop:return JumpType_MultiBhop; + default:return JumpType_Other; + } + } + // Check for weird jump + else if (this.lastType == JumpType_Fall && + this.ValidWeirdJumpDropDistance()) + { + return JumpType_WeirdJump; + } + else + { + return JumpType_Other; + } + } + if (this.HitDuckbugRecently() || !this.GroundSpeedCappedRecently()) + { + return JumpType_Invalid; + } + return JumpType_LongJump; + } + + bool HitBhop() + { + return Movement_GetTakeoffCmdNum(this.jumper) - Movement_GetLandingCmdNum(this.jumper) <= JS_MAX_BHOP_GROUND_TICKS; + } + + bool ValidWeirdJumpDropDistance() + { + if (this.jump.offset < -1 * JS_MAX_WEIRDJUMP_FALL_OFFSET) + { + // Don't bother telling them if they fell a very far distance + if (!GetJumpstatsDisabled(this.jumper) && this.jump.offset >= -2 * JS_MAX_WEIRDJUMP_FALL_OFFSET) + { + GOKZ_PrintToChat(this.jumper, true, "%t", "Dropped Too Far (Weird Jump)", -1 * this.jump.offset, JS_MAX_WEIRDJUMP_FALL_OFFSET); + } + return false; + } + return true; + } + + bool HitDuckbugRecently() + { + return this.tickCount - lastDuckbugTime[this.jumper] <= JS_MAX_DUCKBUG_RESET_TICKS; + } + + bool GroundSpeedCappedRecently() + { + // A valid longjump needs to have their ground speed capped the tick right before. + return lastGroundSpeedCappedTime[this.jumper] == lastMovementProcessedTime[this.jumper]; + } + + // =====[ UPDATE HELPERS ]==================================================== + + // We split that up in two functions to get a reference to the pose so we + // don't have to recalculate the pose index all the time. + void UpdatePoseHistory() + { + this.poseIndex++; + this.UpdatePose(pose(0)); + } + + void UpdatePose(Pose p) + { + Movement_GetProcessingOrigin(this.jumper, p.position); + Movement_GetProcessingVelocity(this.jumper, p.velocity); + Movement_GetEyeAngles(this.jumper, p.orientation); + p.speed = GetVectorHorizontalLength(p.velocity); + + // We use the current position in a lot of places, so we store it + // separately to avoid calling 'pose' all the time. + CopyVector(p.position, this.position); + } + + // We split that up in two functions to get a reference to the pose so we + // don't have to recalculate the pose index all the time. We seperate that + // from UpdatePose() cause those stats are not calculated yet when we call that. + void UpdatePoseStats() + { + this.UpdatePoseStats_P(pose(0)); + } + + void UpdatePoseStats_P(Pose p) + { + p.duration = this.jump.duration; + p.syncTicks = this.syncTicks; + p.overlap = this.jump.overlap; + p.deadair = this.jump.deadair; + } + + void UpdateOnGround() + { + // We want accurate values to measure the first tick + this.UpdatePose(poseHistory[this.jumper][0]); + } + + void UpdateRelease() + { + // Using UpdateOnGround doesn't work because + // takeoff tick is calculated after leaving the ground. + this.jumpoffTick = Movement_GetTakeoffTick(this.jumper); + + // We also check IN_BACK cause that happens for backwards ladderjumps + if (Movement_GetButtons(this.jumper) & IN_FORWARD || + Movement_GetButtons(this.jumper) & IN_BACK) + { + this.lastWPressedTick = this.tickCount; + } + else if (this.jump.releaseW > 99) + { + this.jump.releaseW = this.lastWPressedTick - this.jumpoffTick + 1; + } + + if (Movement_GetButtons(this.jumper) & IN_DUCK) + { + this.lastCrouchPressedTick = this.tickCount; + this.nextCrouchRelease = 100; + } + else if (this.nextCrouchRelease > 99) + { + this.nextCrouchRelease = this.lastCrouchPressedTick - this.jumpoffTick - 95; + } + } + + void UpdateStrafes() + { + // Strafe direction + if (Movement_GetTurningLeft(this.jumper) && + this.strafeDirection != StrafeDirection_Left) + { + this.strafeDirection = StrafeDirection_Left; + this.jump.strafes++; + } + else if (Movement_GetTurningRight(this.jumper) && + this.strafeDirection != StrafeDirection_Right) + { + this.strafeDirection = StrafeDirection_Right; + this.jump.strafes++; + } + + // Overlap / Deadair + int buttons = Movement_GetButtons(this.jumper); + int overlap = buttons & IN_MOVERIGHT && buttons & IN_MOVELEFT ? 1 : 0; + int deadair = !(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT) ? 1 : 0; + + // Sync / Gain / Loss + float deltaSpeed = pose(0).speed - pose(-1).speed; + bool gained = deltaSpeed > EPSILON; + bool lost = deltaSpeed < -EPSILON; + + // Width + float width = FloatAbs(CalcDeltaAngle(pose(0).orientation[1], pose(-1).orientation[1])); + + // Overall stats + this.jump.overlap += overlap; + this.jump.deadair += deadair; + this.jump.width += width; + + // Individual stats + if (this.jump.strafes >= JS_MAX_TRACKED_STRAFES) + { + return; + } + + int i = this.jump.strafes; + this.jump.strafes_ticks[i]++; + + this.jump.strafes_overlap[i] += overlap; + this.jump.strafes_deadair[i] += deadair; + this.jump.strafes_loss[i] += lost ? -1 * deltaSpeed : 0.0; + this.jump.strafes_width[i] += width; + + if (gained) + { + this.jump.strafes_gainTicks[i]++; + this.jump.strafes_gain[i] += deltaSpeed; + } + } + + void UpdateFailstat() + { + int coordDist, distSign; + float failstatPosition[3], block[3], traceStart[3]; + + // There's no point in going further if we're already done + if (this.failstatValid || this.failstatFailed) + { + return; + } + + // Get the coordinate system orientation. + GetCoordOrientation(this.position, this.takeoffOrigin, coordDist, distSign); + + // For ladderjumps we have to find the landing block early so we know at which point the jump failed. + // For this, we search for the block 10 units above the takeoff origin, assuming the player already + // traveled a significant enough distance in the direction of the block at this time. + if (!this.failstatBlockDetected && + this.position[2] - this.takeoffOrigin[2] < 10.0 && + this.jump.height > 10.0) + { + this.failstatBlockDetected = true; + + // Setup a trace to search for the block + CopyVector(this.takeoffOrigin, traceStart); + traceStart[2] -= 5.0; + CopyVector(traceStart, block); + traceStart[coordDist] += JS_MIN_LAJ_BLOCK_DISTANCE * distSign; + block[coordDist] += JS_MAX_LAJ_FAILSTAT_DISTANCE * distSign; + + // Search for the block + if (!TraceHullPosition(traceStart, block, playerMins, playerMaxs, block)) + { + // Mark the calculation as failed + this.failstatFailed = true; + return; + } + + // Find the block height + block[2] += 5.0; + this.failstatBlockHeight = this.FindBlockHeight(block, float(distSign) * 17.0, coordDist, 10.0) - 0.031250; + } + + // Only do the calculation once we're below the block level + if (this.position[2] >= this.failstatBlockHeight) + { + // We need that cause we can duck after getting lower than the failstat + // height and still make the block. + this.failstatValid = false; + return; + } + + // Calculate the true origin where the player would have hit the ground. + this.GetFailOrigin(this.failstatBlockHeight, failstatPosition, -1); + + // Calculate the jump distance. + this.jump.distance = FloatAbs(GetVectorHorizontalDistance(failstatPosition, this.takeoffOrigin)); + + // Construct the maximum landing origin, assuming the player reached + // at least the middle of the gap. + CopyVector(this.takeoffOrigin, block); + block[coordDist] = 2 * failstatPosition[coordDist] - this.takeoffOrigin[coordDist]; + block[view_as<int>(!coordDist)] = failstatPosition[view_as<int>(!coordDist)]; + block[2] = this.failstatBlockHeight; + + // Calculate block stats + if ((this.lastType == JumpType_LongJump || + this.lastType == JumpType_Bhop || + this.lastType == JumpType_MultiBhop || + this.lastType == JumpType_Ladderhop || + this.lastType == JumpType_WeirdJump || + this.lastType == JumpType_Jumpbug || + this.lastType == JumpType_LowpreBhop || + this.lastType == JumpType_LowpreWeirdJump) + && this.jump.distance >= JS_MIN_BLOCK_DISTANCE) + { + // Add the player model to the distance. + this.jump.distance += 32.0; + + this.CalcBlockStats(block, true); + } + else if (this.lastType == JumpType_LadderJump && + this.jump.distance >= JS_MIN_LAJ_BLOCK_DISTANCE) + { + this.CalcLadderBlockStats(block, true); + } + else + { + this.failstatFailed = true; + return; + } + + if (this.jump.block > 0) + { + // Calculate the last stats + this.jump.sync = float(this.syncTicks) / float(this.jump.duration) * 100.0; + this.jump.offset = failstatPosition[2] - this.takeoffOrigin[2]; + + // Call the callback for the reporting. + Call_OnFailstat(this.jump); + + // Mark the calculation as successful + this.failstatValid = true; + } + else + { + this.failstatFailed = true; + } + } + + + + // =====[ END HELPERS ]===================================================== + + float CalcDistance() + { + float distance = GetVectorHorizontalDistance(this.takeoffOrigin, this.position); + + // Check whether the distance is NaN + if (distance != distance) + { + this.Invalidate(); + + // We need that for the always stats + float pos[3]; + + // For the always stats it's ok to ignore the bug + Movement_GetOrigin(this.jumper, pos); + + distance = GetVectorHorizontalDistance(this.takeoffOrigin, pos); + } + + if (this.jump.originalType != JumpType_LadderJump) + { + distance += 32.0; + } + return distance; + } + + void EndBlockDistance() + { + if ((this.jump.type == JumpType_LongJump || + this.jump.type == JumpType_Bhop || + this.jump.type == JumpType_MultiBhop || + this.jump.type == JumpType_Ladderhop || + this.jump.type == JumpType_WeirdJump || + this.jump.type == JumpType_Jumpbug || + this.jump.type == JumpType_LowpreBhop || + this.jump.type == JumpType_LowpreWeirdJump) + && this.jump.distance >= JS_MIN_BLOCK_DISTANCE) + { + this.CalcBlockStats(this.position); + } + else if (this.jump.type == JumpType_LadderJump && + this.jump.distance >= JS_MIN_LAJ_BLOCK_DISTANCE) + { + this.CalcLadderBlockStats(this.position); + } + } + + void EndAlwaysJumpstats() + { + // Only calculate that form of edge if the regular block calculations failed + if (this.jump.block == 0 && this.jump.type != JumpType_LadderJump) + { + this.CalcAlwaysEdge(); + } + + // It's possible that the offset calculation failed with the nobug origin + // functions, so we have to fix it when that happens. The offset shouldn't + // be affected by the bug anyway. + if (this.jump.offset != this.jump.offset) + { + Movement_GetOrigin(this.jumper, this.position); + this.jump.offset = this.position[2] - this.takeoffOrigin[2]; + } + } + + void EndBugfixExploits() + { + // Try to prevent a form of booster abuse + if (!this.IsValidAirtime()) + { + this.Invalidate(); + } + } + + bool IsValidAirtime() + { + // Ladderjumps can have pretty much any airtime. + if (this.jump.type == JumpType_LadderJump) + { + return true; + } + + // Ladderhops can have a maximum airtime of 102. + if (this.jump.type == JumpType_Ladderhop + && this.jump.duration <= 102) + { + return true; + } + + // Crouchjumped or perfed longjumps/bhops can have a maximum of 101 airtime + // when the lj bug occurs. Since we've fixed that the airtime is valid. + if (this.jump.duration <= 101) + { + return true; + } + + return false; + } + + void Callback() + { + if (GOKZ_JS_GetOption(this.jumper, JSOption_JumpstatsAlways) == JSToggleOption_Enabled) + { + Call_OnJumpstatAlways(this.jump); + } + else + { + Call_OnLanding(this.jump); + } + } + + + + // =====[ ALWAYS FAILSTATS ]================================================== + + void AlwaysFailstat() + { + bool foundBlock; + int coordDist, distSign; + float traceStart[3], traceEnd[3], tracePos[3], landingPos[3], orientation[3], failOrigin[3]; + + // Check whether the jump was already handled + if (this.jump.type == JumpType_FullInvalid || this.failstatValid) + { + return; + } + + // Initialize the trace boxes + float traceMins[3] = { 0.0, 0.0, 0.0 }; + float traceLongMaxs[3] = { 0.0, 0.0, 200.0 }; + float traceShortMaxs[3] = { 0.0, 0.0, 54.0 }; + + // Clear the stats + this.jump.miss = 0.0; + this.jump.distance = 0.0; + + // Calculate the edge + this.CalcAlwaysEdge(); + + // We will search for the block based on the direction the player was looking + CopyVector(pose(0).orientation, orientation); + + // Get the landing orientation + coordDist = FloatAbs(orientation[0]) < FloatAbs(orientation[1]); + distSign = orientation[coordDist] > 0 ? 1 : -1; + + // Initialize the traces + CopyVector(this.position, traceStart); + CopyVector(this.position, traceEnd); + + // Assume the miss is less than 100 units + traceEnd[coordDist] += 100.0 * distSign; + + // Search for the end block with the long trace + foundBlock = TraceHullPosition(traceStart, traceEnd, traceMins, traceLongMaxs, tracePos); + + // If not even the long trace finds the block, we're out of luck + if (foundBlock) + { + // Search for the block height + tracePos[2] = this.position[2]; + foundBlock = this.TryFindBlockHeight(tracePos, landingPos, coordDist, distSign); + + // Maybe there was a headbanger, try with the short trace instead + if (!foundBlock) + { + if (TraceHullPosition(traceStart, traceEnd, traceMins, traceShortMaxs, tracePos)) + { + // Search for the height again + tracePos[2] = this.position[2]; + foundBlock = this.TryFindBlockHeight(tracePos, landingPos, coordDist, distSign); + } + } + + if (foundBlock) + { + // Search for the last tick the player was above the landing block elevation. + for (int i = 0; i < JS_FAILSTATS_MAX_TRACKED_TICKS; i++) + { + Pose p; + + // This copies it, but it shouldn't be that much of a problem + p = pose(-i); + + if(p.position[2] >= landingPos[2]) + { + // Calculate the correct fail position + this.GetFailOrigin(landingPos[2], failOrigin, -i); + + // Calculate all missing stats + this.jump.miss = FloatAbs(failOrigin[coordDist] - landingPos[coordDist]) - 16.0; + this.jump.distance = GetVectorHorizontalDistance(failOrigin, this.takeoffOrigin); + this.jump.offset = failOrigin[2] - this.takeoffOrigin[2]; + this.jump.duration = p.duration; + this.jump.overlap = p.overlap; + this.jump.deadair = p.deadair; + this.jump.sync = float(p.syncTicks) / float(this.jump.duration) * 100.0; + break; + } + } + } + } + + // Notify everyone about the jump + Call_OnFailstatAlways(this.jump); + + // Fully invalidate the jump cause we failstatted it already + this.jump.type = JumpType_FullInvalid; + } + + void CalcAlwaysEdge() + { + int coordDist, distSign; + float traceStart[3], traceEnd[3], velocity[3]; + float ladderNormal[3], ladderMins[3], ladderMaxs[3]; + + // Ladder jumps have a different definition of edge + if (this.jump.originalType == JumpType_LadderJump) + { + // Get a vector that points outwards from the lader towards the player + GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", ladderNormal); + + // Initialize box to search for the ladder + if (ladderNormal[0] > ladderNormal[1]) + { + ladderMins = view_as<float>({ 0.0, -20.0, 0.0 }); + ladderMaxs = view_as<float>({ 0.0, 20.0, 0.0 }); + coordDist = 0; + } + else + { + ladderMins = view_as<float>({ -20.0, 0.0, 0.0 }); + ladderMaxs = view_as<float>({ 20.0, 0.0, 0.0 }); + coordDist = 1; + } + + // The max the ladder will be away is the player model (16) + danvari tech (10) + a safety unit + CopyVector(this.takeoffOrigin, traceEnd); + traceEnd[coordDist] += 27.0; + + // Search for the ladder + if (TraceHullPosition(this.takeoffOrigin, traceEnd, ladderMins, ladderMaxs, traceEnd)) + { + this.jump.edge = FloatAbs(traceEnd[coordDist] - this.takeoffOrigin[coordDist]) - 16.0; + } + } + else + { + // We calculate the orientation of the takeoff block based on what + // direction the player was moving + CopyVector(this.takeoffVelocity, velocity); + this.jump.edge = -1.0; + + // Calculate the takeoff orientation + coordDist = FloatAbs(velocity[0]) < FloatAbs(velocity[1]); + distSign = velocity[coordDist] > 0 ? 1 : -1; + + // Make sure we hit the jumpoff block + CopyVector(this.takeoffOrigin, traceEnd); + traceEnd[coordDist] -= 16.0 * distSign; + traceEnd[2] -= 1.0; + + // Assume a max edge of 20 + CopyVector(traceEnd, traceStart); + traceStart[coordDist] += 20.0 * distSign; + + // Trace the takeoff block + if (TraceRayPosition(traceStart, traceEnd, traceEnd)) + { + // Check whether the trace was stuck in the block from the beginning + if (FloatAbs(traceEnd[coordDist] - traceStart[coordDist]) > EPSILON) + { + // Block trace ends 0.03125 in front of the actual block. Adjust the edge correctly. + this.jump.edge = FloatAbs(traceEnd[coordDist] - this.takeoffOrigin[coordDist] + (16.0 - 0.03125) * distSign); + } + } + } + } + + bool TryFindBlockHeight(const float position[3], float result[3], int coordDist, int distSign) + { + float traceStart[3], traceEnd[3]; + + // Setup the trace points + CopyVector(position, traceStart); + traceStart[coordDist] += distSign; + CopyVector(traceStart, traceEnd); + + // We search in 54 unit steps + traceStart[2] += 54.0; + + // We search with multiple trace starts in case the landing block has a roof + for (int i = 0; i < 3; i += 1) + { + if (TraceRayPosition(traceStart, traceEnd, result)) + { + // Make sure the trace didn't get stuck right away + if (FloatAbs(result[2] - traceStart[2]) > EPSILON) + { + result[coordDist] -= distSign; + return true; + } + } + + // Try the next are to find the block. We use two different values to have + // some overlap in case the block perfectly aligns with the trace. + traceStart[2] += 54.0; + traceEnd[2] += 53.0; + } + + return false; + } + + + + // =====[ BLOCK STATS HELPERS ]=============================================== + + void CalcBlockStats(float landingOrigin[3], bool checkOffset = false) + { + int coordDist, coordDev, distSign; + float middle[3], startBlock[3], endBlock[3], sweepBoxMin[3], sweepBoxMax[3]; + + // Get the orientation of the block. + GetCoordOrientation(landingOrigin, this.takeoffOrigin, coordDist, distSign); + coordDev = !coordDist; + + // We can't make measurements from within an entity, so we assume the + // player had a remotely reasonable edge and that the middle of the jump + // is not over a block and then start measuring things out from there. + middle[coordDist] = (this.takeoffOrigin[coordDist] + landingOrigin[coordDist]) / 2; + middle[coordDev] = (this.takeoffOrigin[coordDev] + landingOrigin[coordDev]) / 2; + middle[2] = this.takeoffOrigin[2] - 1.0; + + // Get the deviation. + this.jump.deviation = FloatAbs(landingOrigin[coordDev] - this.takeoffOrigin[coordDev]); + + // Setup a sweeping line that starts in the middle and tries to search for the smallest + // block within the deviation of the player. + sweepBoxMin[coordDist] = 0.0; + sweepBoxMin[coordDev] = -this.jump.deviation - 16.0; + sweepBoxMin[2] = 0.0; + sweepBoxMax[coordDist] = 0.0; + sweepBoxMax[coordDev] = this.jump.deviation + 16.0; + sweepBoxMax[2] = 0.0; + + // Modify the takeoff and landing origins to line up with the middle and respect + // the bounding box of the player. + startBlock[coordDist] = this.takeoffOrigin[coordDist] - distSign * 16.0; + // Sometimes you can land 0.03125 units in front of a block, so the trace needs to be extended. + endBlock[coordDist] = landingOrigin[coordDist] + distSign * (16.0 + 0.03125); + startBlock[coordDev] = middle[coordDev]; + endBlock[coordDev] = middle[coordDev]; + startBlock[2] = middle[2]; + endBlock[2] = middle[2]; + + // Search for the blocks + if (!TraceHullPosition(middle, startBlock, sweepBoxMin, sweepBoxMax, startBlock) + || !TraceHullPosition(middle, endBlock, sweepBoxMin, sweepBoxMax, endBlock)) + { + return; + } + + // Make sure the edges of the blocks are parallel. + if (!this.BlockAreEdgesParallel(startBlock, endBlock, this.jump.deviation + 32.0, coordDist, coordDev)) + { + this.jump.block = 0; + this.jump.edge = -1.0; + return; + } + + // Needed for failstats, but you need the endBlock position for that, so we do it here. + if (checkOffset) + { + endBlock[2] += 1.0; + if (FloatAbs(this.FindBlockHeight(endBlock, float(distSign) * 17.0, coordDist, 1.0) - landingOrigin[2]) > JS_OFFSET_EPSILON) + { + return; + } + } + + // Calculate distance and edge. + this.jump.block = RoundFloat(FloatAbs(endBlock[coordDist] - startBlock[coordDist])); + // Block trace ends 0.03125 in front of the actual block. Adjust the edge correctly. + this.jump.edge = FloatAbs(startBlock[coordDist] - this.takeoffOrigin[coordDist] + (16.0 - 0.03125) * distSign); + + // Make it easier to check for blocks that too short + if (this.jump.block < JS_MIN_BLOCK_DISTANCE) + { + this.jump.block = 0; + this.jump.edge = -1.0; + } + } + + void CalcLadderBlockStats(float landingOrigin[3], bool checkOffset = false) + { + int coordDist, coordDev, distSign; + float sweepBoxMin[3], sweepBoxMax[3], blockPosition[3], ladderPosition[3], normalVector[3], endBlock[3], middle[3]; + + // Get the orientation of the block. + GetCoordOrientation(landingOrigin, this.takeoffOrigin, coordDist, distSign); + coordDev = !coordDist; + + // Get the deviation. + this.jump.deviation = FloatAbs(landingOrigin[coordDev] - this.takeoffOrigin[coordDev]); + + // Make sure the ladder is aligned. + GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", normalVector); + if (FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) > EPSILON) + { + return; + } + + // Make sure we'll find the block and ladder. + CopyVector(this.takeoffOrigin, ladderPosition); + CopyVector(landingOrigin, endBlock); + endBlock[2] -= 1.0; + ladderPosition[2] = endBlock[2]; + + // Setup a line to search for the ladder. + sweepBoxMin[coordDist] = 0.0; + sweepBoxMin[coordDev] = -20.0; + sweepBoxMin[2] = 0.0; + sweepBoxMax[coordDist] = 0.0; + sweepBoxMax[coordDev] = 20.0; + sweepBoxMax[2] = 0.0; + middle[coordDist] = ladderPosition[coordDist] + distSign * JS_MIN_LAJ_BLOCK_DISTANCE; + middle[coordDev] = endBlock[coordDev]; + middle[2] = ladderPosition[2]; + + // Search for the ladder. + if (!TraceHullPosition(ladderPosition, middle, sweepBoxMin, sweepBoxMax, ladderPosition)) + { + return; + } + + // Find the block and make sure it's aligned + endBlock[coordDist] += distSign * 16.0; + if (!TraceRayPositionNormal(middle, endBlock, blockPosition, normalVector) + || FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) > EPSILON) + { + return; + } + + // Needed for failstats, but you need the blockPosition for that, so we do it here. + if (checkOffset) + { + blockPosition[2] += 1.0; + if (!this.TraceLadderOffset(this.FindBlockHeight(blockPosition, float(distSign), coordDist, 1.0) - 0.031250)) + { + return; + } + } + + // Calculate distance and edge. + this.jump.block = RoundFloat(FloatAbs(blockPosition[coordDist] - ladderPosition[coordDist])); + this.jump.edge = FloatAbs(this.takeoffOrigin[coordDist] - ladderPosition[coordDist]) - 16.0; + + // Make it easier to check for blocks that too short + if (this.jump.block < JS_MIN_LAJ_BLOCK_DISTANCE) + { + this.jump.block = 0; + this.jump.edge = -1.0; + } + } + + bool TraceLadderOffset(float landingHeight) + { + float traceOrigin[3], traceEnd[3], ladderTop[3], ladderNormal[3]; + + // Get normal vector of the ladder. + GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", ladderNormal); + + // 10 units is the furthest away from the ladder surface you can get while still being on the ladder. + traceOrigin[0] = this.takeoffOrigin[0] - 10.0 * ladderNormal[0]; + traceOrigin[1] = this.takeoffOrigin[1] - 10.0 * ladderNormal[1]; + traceOrigin[2] = this.takeoffOrigin[2] + 5; + + CopyVector(traceOrigin, traceEnd); + traceEnd[2] = this.takeoffOrigin[2] - 10; + + // Search for the ladder + if (!TraceHullPosition(traceOrigin, traceEnd, playerMinsEx, playerMaxsEx, ladderTop) + || FloatAbs(ladderTop[2] - landingHeight) > JS_OFFSET_EPSILON) + { + this.Invalidate(); + return false; + } + return true; + } + + bool BlockTraceAligned(const float origin[3], const float end[3], int coordDist) + { + float normalVector[3]; + if (!TraceRayNormal(origin, end, normalVector)) + { + return false; + } + return FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) <= EPSILON; + } + + bool BlockAreEdgesParallel(const float startBlock[3], const float endBlock[3], float deviation, int coordDist, int coordDev) + { + float start[3], end[3], offset; + + // We use very short rays to find the blocks where they're supposed to be and use + // their normals to determine whether they're parallel or not. + offset = startBlock[coordDist] > endBlock[coordDist] ? 0.1 : -0.1; + + // We search for the blocks on both sides of the player, on one of the sides + // there has to be a valid block. + start[coordDist] = startBlock[coordDist] - offset; + start[coordDev] = startBlock[coordDev] - deviation; + start[2] = startBlock[2]; + + end[coordDist] = startBlock[coordDist] + offset; + end[coordDev] = startBlock[coordDev] - deviation; + end[2] = startBlock[2]; + + if (this.BlockTraceAligned(start, end, coordDist)) + { + start[coordDist] = endBlock[coordDist] + offset; + end[coordDist] = endBlock[coordDist] - offset; + if (this.BlockTraceAligned(start, end, coordDist)) + { + return true; + } + start[coordDist] = startBlock[coordDist] - offset; + end[coordDist] = startBlock[coordDist] + offset; + } + + start[coordDev] = startBlock[coordDev] + deviation; + end[coordDev] = startBlock[coordDev] + deviation; + + if (this.BlockTraceAligned(start, end, coordDist)) + { + start[coordDist] = endBlock[coordDist] + offset; + end[coordDist] = endBlock[coordDist] - offset; + if (this.BlockTraceAligned(start, end, coordDist)) + { + return true; + } + } + + return false; + } + + float FindBlockHeight(const float origin[3], float offset, int coord, float searchArea) + { + float block[3], traceStart[3], traceEnd[3], normalVector[3]; + + // Setup the trace. + CopyVector(origin, traceStart); + traceStart[coord] += offset; + CopyVector(traceStart, traceEnd); + traceStart[2] += searchArea; + traceEnd[2] -= searchArea; + + // Find the block height. + if (!TraceRayPositionNormal(traceStart, traceEnd, block, normalVector) + || FloatAbs(normalVector[2] - 1.0) > EPSILON) + { + return -99999999999999999999.0; // Let's hope that's wrong enough + } + + return block[2]; + } + + void GetFailOrigin(float planeHeight, float result[3], int poseIndex) + { + float newVel[3], oldVel[3]; + + // Calculate the actual velocity. + CopyVector(pose(poseIndex).velocity, oldVel); + ScaleVector(oldVel, GetTickInterval()); + + // Calculate at which percentage of the velocity vector we hit the plane. + float scale = (planeHeight - pose(poseIndex).position[2]) / oldVel[2]; + + // Calculate the position we hit the plane. + CopyVector(oldVel, newVel); + ScaleVector(newVel, scale); + AddVectors(pose(poseIndex).position, newVel, result); + } +} + +static JumpTracker jumpTrackers[MAXPLAYERS + 1]; + + + +// =====[ HELPER FUNCTIONS ]=================================================== + +void GetCoordOrientation(const float vec1[3], const float vec2[3], int &coordDist, int &distSign) +{ + coordDist = FloatAbs(vec1[0] - vec2[0]) < FloatAbs(vec1[1] - vec2[1]); + distSign = vec1[coordDist] > vec2[coordDist] ? 1 : -1; +} + +bool TraceRayPosition(const float traceStart[3], const float traceEnd[3], float position[3]) +{ + Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + TR_GetEndPosition(position, trace); + delete trace; + return true; + } + delete trace; + return false; +} + +static bool TraceRayNormal(const float traceStart[3], const float traceEnd[3], float rayNormal[3]) +{ + Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + TR_GetPlaneNormal(trace, rayNormal); + delete trace; + return true; + } + delete trace; + return false; +} + +static bool TraceRayPositionNormal(const float traceStart[3], const float traceEnd[3], float position[3], float rayNormal[3]) +{ + Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + TR_GetEndPosition(position, trace); + TR_GetPlaneNormal(trace, rayNormal); + delete trace; + return true; + } + delete trace; + return false; +} + +static bool TraceHullPosition(const float traceStart[3], const float traceEnd[3], const float mins[3], const float maxs[3], float position[3]) +{ + Handle trace = TR_TraceHullFilterEx(traceStart, traceEnd, mins, maxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + TR_GetEndPosition(position, trace); + delete trace; + return true; + } + delete trace; + return false; +} + + + +// =====[ EVENTS ]============================================================= + +void OnPluginStart_JumpTracking() +{ + GameData gd = LoadGameConfigFile("sdktools.games/engine.csgo"); + int offset = gd.GetOffset("AcceptInput"); + if (offset == -1) + { + SetFailState("Failed to get AcceptInput offset"); + } + + acceptInputHook = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput); + DHookAddParam(acceptInputHook, HookParamType_CharPtr); + DHookAddParam(acceptInputHook, HookParamType_CBaseEntity); + DHookAddParam(acceptInputHook, HookParamType_CBaseEntity); + //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20 + DHookAddParam(acceptInputHook, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP); + DHookAddParam(acceptInputHook, HookParamType_Int); + delete gd; +} + +void OnOptionChanged_JumpTracking(int client, const char[] option) +{ + if (StrEqual(option, gC_CoreOptionNames[Option_Mode])) + { + jumpTrackers[client].jump.type = JumpType_FullInvalid; + } +} + +void OnClientPutInServer_JumpTracking(int client) +{ + if (entityTouchList[client] != INVALID_HANDLE) + { + delete entityTouchList[client]; + } + entityTouchList[client] = new ArrayList(); + lastNoclipTime[client] = 0; + lastDuckbugTime[client] = 0; + lastJumpButtonTime[client] = 0.0; + jumpTrackers[client].Init(client); + DHookEntity(acceptInputHook, true, client); +} + + +// This was originally meant for invalidating jumpstats but was removed. +void OnJumpInvalidated_JumpTracking(int client) +{ + jumpTrackers[client].Invalidate(); +} + +void OnJumpValidated_JumpTracking(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + if (!validCmd[client]) + { + return; + } + + // Update: Takeoff speed should be always correct with the new MovementAPI. + if (jumped) + { + jumpTrackers[client].lastJumpTick = jumpTrackers[client].tickCount; + } + jumpTrackers[client].Reset(jumped, ladderJump, jumpbug); + jumpTrackers[client].Begin(); +} + +void OnStartTouchGround_JumpTracking(int client) +{ + if (!doFailstatAlways[client]) + { + jumpTrackers[client].End(); + } +} + +void OnStartTouch_JumpTracking(int client, int touched) +{ + if (entityTouchList[client] != INVALID_HANDLE) + { + entityTouchList[client].Push(touched); + // Do not immediately invalidate jumps upon collision. + // Give the player a few ticks of leniency for late ducking. + } +} + +void OnTouch_JumpTracking(int client) +{ + if (entityTouchList[client] != INVALID_HANDLE && entityTouchList[client].Length > 0) + { + entityTouchDuration[client]++; + } + if (!Movement_GetOnGround(client) && entityTouchDuration[client] > JS_TOUCH_GRACE_TICKS) + { + jumpTrackers[client].Invalidate(); + } +} + +void OnEndTouch_JumpTracking(int client, int touched) +{ + if (entityTouchList[client] != INVALID_HANDLE) + { + int index = entityTouchList[client].FindValue(touched); + if (index != -1) + { + entityTouchList[client].Erase(index); + } + if (entityTouchList[client].Length == 0) + { + entityTouchDuration[client] = 0; + } + } +} + +void OnPlayerRunCmd_JumpTracking(int client, int buttons, int tickcount) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) + { + return; + } + + jumpTrackers[client].tickCount = tickcount; + + if (GetClientButtons(client) & IN_JUMP) + { + lastJumpButtonTime[client] = GetGameTime(); + } + + if (CheckNoclip(client)) + { + lastNoclipTime[client] = tickcount; + } + + // Don't bother checking if player is already in air and jumpstat is already invalid + if (Movement_GetOnGround(client) || + jumpTrackers[client].jump.type != JumpType_FullInvalid) + { + UpdateValidCmd(client, buttons); + } +} + +public Action Movement_OnWalkMovePost(int client) +{ + lastGroundSpeedCappedTime[client] = jumpTrackers[client].tickCount; + return Plugin_Continue; +} + +public Action Movement_OnPlayerMovePost(int client) +{ + lastMovementProcessedTime[client] = jumpTrackers[client].tickCount; + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost_JumpTracking(int client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) + { + return; + } + + // Check for always failstats + if (doFailstatAlways[client]) + { + doFailstatAlways[client] = false; + // Prevent TP shenanigans that would trigger failstats + //jumpTypeLast[client] = JumpType_Invalid; + + if (GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Enabled && + isInAir[client]) + { + jumpTrackers[client].AlwaysFailstat(); + } + } + + if (!Movement_GetOnGround(client)) + { + isInAir[client] = true; + jumpTrackers[client].Update(); + } + + if (Movement_GetOnGround(client) || + Movement_GetMovetype(client) == MOVETYPE_LADDER) + { + isInAir[client] = false; + jumpTrackers[client].UpdateOnGround(); + } + + // We always have to track this, no matter if in the air or not + jumpTrackers[client].UpdateRelease(); + + if (Movement_GetDuckbugged(client)) + { + lastDuckbugTime[client] = jumpTrackers[client].tickCount; + } +} + +static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) + { + return MRES_Ignored; + } + + // Get args + static char param[64]; + static char command[64]; + DHookGetParamString(hParams, 1, command, sizeof(command)); + if (StrEqual(command, "AddOutput")) + { + DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param)); + char kv[16]; + SplitString(param, " ", kv, sizeof(kv)); + // KVs are case insensitive. + if (StrEqual(kv[0], "origin", false)) + { + // The player technically did not get "teleported" but the origin gets changed regardless, + // which effectively is a teleport. + OnTeleport_FailstatAlways(client); + } + } + return MRES_Ignored; +} + +// =====[ CHECKS ]===== + +static void UpdateValidCmd(int client, int buttons) +{ + if (!CheckGravity(client) + || !CheckBaseVelocity(client) + || !CheckInWater(client) + || !CheckTurnButtons(buttons)) + { + InvalidateJumpstat(client); + validCmd[client] = false; + } + else + { + validCmd[client] = true; + } + + if (jumpTrackers[client].tickCount - lastNoclipTime[client] < GOKZ_JUMPSTATS_NOCLIP_RESET_TICKS) + { + jumpTrackers[client].jump.type = JumpType_FullInvalid; + } + + if (!CheckLadder(client)) + { + InvalidateJumpstat(client); + } +} + +static bool CheckGravity(int client) +{ + float gravity = Movement_GetGravity(client); + // Allow 1.0 and 0.0 gravity as both values appear during normal gameplay + if (FloatAbs(gravity - 1.0) > EPSILON && FloatAbs(gravity) > EPSILON) + { + return false; + } + return true; +} + +static bool CheckBaseVelocity(int client) +{ + float baseVelocity[3]; + Movement_GetBaseVelocity(client, baseVelocity); + if (FloatAbs(baseVelocity[0]) > EPSILON || + FloatAbs(baseVelocity[1]) > EPSILON || + FloatAbs(baseVelocity[2]) > EPSILON) + { + return false; + } + return true; +} + +static bool CheckInWater(int client) +{ + int waterLevel = GetEntProp(client, Prop_Data, "m_nWaterLevel"); + return waterLevel == 0; +} + +static bool CheckTurnButtons(int buttons) +{ + // Don't allow +left or +right turns binds + return !(buttons & (IN_LEFT | IN_RIGHT)); +} + +static bool CheckNoclip(int client) +{ + return Movement_GetMovetype(client) == MOVETYPE_NOCLIP; +} + +static bool CheckLadder(int client) +{ + return Movement_GetMovetype(client) != MOVETYPE_LADDER; +} + + + +// =====[ EXTERNAL HELPER FUNCTIONS ]========================================== + +void InvalidateJumpstat(int client) +{ + jumpTrackers[client].Invalidate(); +} + +float GetStrafeSync(Jump jump, int strafe) +{ + if (strafe < JS_MAX_TRACKED_STRAFES) + { + return float(jump.strafes_gainTicks[strafe]) + / float(jump.strafes_ticks[strafe]) + * 100.0; + } + else + { + return 0.0; + } +} + +float GetStrafeAirtime(Jump jump, int strafe) +{ + if (strafe < JS_MAX_TRACKED_STRAFES) + { + return float(jump.strafes_ticks[strafe]) + / float(jump.duration) + * 100.0; + } + else + { + return 0.0; + } +} + +void OnTeleport_FailstatAlways(int client) +{ + // We want to synchronize all of that + doFailstatAlways[client] = true; + + // gokz-core does that too, but for some reason we have to do it again + InvalidateJumpstat(client); + + jumpTrackers[client].lastTeleportTick = jumpTrackers[client].tickCount; +} diff --git a/sourcemod/scripting/gokz-jumpstats/jump_validating.sp b/sourcemod/scripting/gokz-jumpstats/jump_validating.sp new file mode 100644 index 0000000..c6835c7 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/jump_validating.sp @@ -0,0 +1,82 @@ +/* + Invalidating invalid jumps, such as ones with a modified velocity. +*/ + +static Handle processMovementHookPost; + +void OnPluginStart_JumpValidating() +{ + Handle gamedataConf = LoadGameConfigFile("gokz-core.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-core gamedata"); + } + + // CreateInterface + // Thanks SlidyBat and ici + StartPrepSDKCall(SDKCall_Static); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface")) + { + SetFailState("Failed to get CreateInterface"); + } + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + Handle CreateInterface = EndPrepSDKCall(); + + if (CreateInterface == null) + { + SetFailState("Unable to prepare SDKCall for CreateInterface"); + } + + char interfaceName[64]; + + // ProcessMovement + if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IGameMovement interface name"); + } + Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0); + if (!IGameMovement) + { + SetFailState("Failed to get IGameMovement pointer"); + } + + int offset = GameConfGetOffset(gamedataConf, "ProcessMovement"); + if (offset == -1) + { + SetFailState("Failed to get ProcessMovement offset"); + } + + processMovementHookPost = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPost); + DHookAddParam(processMovementHookPost, HookParamType_CBaseEntity); + DHookAddParam(processMovementHookPost, HookParamType_ObjectPtr); + DHookRaw(processMovementHookPost, false, IGameMovement); +} + +static MRESReturn DHook_ProcessMovementPost(Handle hParams) +{ + int client = DHookGetParam(hParams, 1); + if (!IsValidClient(client) || IsFakeClient(client)) + { + return MRES_Ignored; + } + float pVelocity[3], velocity[3]; + Movement_GetProcessingVelocity(client, pVelocity); + Movement_GetVelocity(client, velocity); + + gB_SpeedJustModifiedExternally[client] = false; + for (int i = 0; i < 3; i++) + { + if (FloatAbs(pVelocity[i] - velocity[i]) > EPSILON) + { + // The current velocity doesn't match the velocity of the end of movement processing, + // so it must have been modified by something like a trigger. + InvalidateJumpstat(client); + gB_SpeedJustModifiedExternally[client] = true; + break; + } + } + + return MRES_Ignored; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-jumpstats/options.sp b/sourcemod/scripting/gokz-jumpstats/options.sp new file mode 100644 index 0000000..7e0e9e9 --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/options.sp @@ -0,0 +1,86 @@ +/* + Options for jumpstats, including an option to disable it completely. +*/ + + + +// =====[ PUBLIC ]===== + +bool GetJumpstatsDisabled(int client) +{ + return GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled + || (GOKZ_JS_GetOption(client, JSOption_MinChatTier) == DistanceTier_None + && GOKZ_JS_GetOption(client, JSOption_MinConsoleTier) == DistanceTier_None + && GOKZ_JS_GetOption(client, JSOption_MinSoundTier) == DistanceTier_None + && GOKZ_JS_GetOption(client, JSOption_FailstatsConsole) == JSToggleOption_Disabled + && GOKZ_JS_GetOption(client, JSOption_FailstatsChat) == JSToggleOption_Disabled + && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled); +} + + + +// =====[ EVENTS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void OnClientPutInServer_Options(int client) +{ + if (GOKZ_JS_GetOption(client, JSOption_MinSoundTier) == DistanceTier_Meh) + { + GOKZ_JS_SetOption(client, JSOption_MinSoundTier, DistanceTier_Impressive); + } +} + +void OnOptionChanged_Options(int client, const char[] option, any newValue) +{ + JSOption jsOption; + if (GOKZ_JS_IsJSOption(option, jsOption)) + { + if (jsOption == JSOption_MinSoundTier && newValue == DistanceTier_Meh) + { + GOKZ_JS_SetOption(client, JSOption_MinSoundTier, DistanceTier_Impressive); + } + else + { + PrintOptionChangeMessage(client, jsOption, newValue); + } + } +} + + + +// =====[ PRIVATE ]===== + +static void RegisterOptions() +{ + for (JSOption option; option < JSOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_JSOptionNames[option], gC_JSOptionDescriptions[option], + OptionType_Int, gI_JSOptionDefaults[option], 0, gI_JSOptionCounts[option] - 1); + } +} + +static void PrintOptionChangeMessage(int client, JSOption option, any newValue) +{ + // NOTE: Not all options have a message for when they are changed. + switch (option) + { + case JSOption_JumpstatsMaster: + { + switch (newValue) + { + case JSToggleOption_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Master Switch - Enable"); + } + case JSToggleOption_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Master Switch - Disable"); + } + } + } + } +} diff --git a/sourcemod/scripting/gokz-jumpstats/options_menu.sp b/sourcemod/scripting/gokz-jumpstats/options_menu.sp new file mode 100644 index 0000000..903a8bb --- /dev/null +++ b/sourcemod/scripting/gokz-jumpstats/options_menu.sp @@ -0,0 +1,145 @@ +static TopMenu optionsTopMenu; +static TopMenuObject catJumpstats; +static TopMenuObject itemsJumpstats[JSOPTION_COUNT]; + + + +// =====[ PUBLIC ]===== + +void DisplayJumpstatsOptionsMenu(int client) +{ + optionsTopMenu.DisplayCategory(catJumpstats, client); +} + + + +// =====[ EVENTS ]===== + +void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu) +{ + if (optionsTopMenu == topMenu && catJumpstats != INVALID_TOPMENUOBJECT) + { + return; + } + + catJumpstats = topMenu.AddCategory(JS_OPTION_CATEGORY, TopMenuHandler_Categories); +} + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + // Make sure category exists + if (catJumpstats == INVALID_TOPMENUOBJECT) + { + GOKZ_OnOptionsMenuCreated(topMenu); + } + + if (optionsTopMenu == topMenu) + { + return; + } + + optionsTopMenu = topMenu; + + // Add HUD option items + for (int option = 0; option < view_as<int>(JSOPTION_COUNT); option++) + { + itemsJumpstats[option] = optionsTopMenu.AddItem(gC_JSOptionNames[option], TopMenuHandler_HUD, catJumpstats); + } +} + +public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == catJumpstats) + { + Format(buffer, maxlength, "%T", "Options Menu - Jumpstats", param); + } + } +} + +public void TopMenuHandler_HUD(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + JSOption option = JSOPTION_INVALID; + for (int i = 0; i < view_as<int>(JSOPTION_COUNT); i++) + { + if (topobj_id == itemsJumpstats[i]) + { + option = view_as<JSOption>(i); + break; + } + } + + if (option == JSOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + if (option == JSOption_JumpstatsMaster || + option == JSOption_ExtendedChatReport || + option == JSOption_FailstatsConsole || + option == JSOption_FailstatsChat || + option == JSOption_JumpstatsAlways) + { + FormatToggleableOptionDisplay(param, option, buffer, maxlength); + } + else + { + FormatDistanceTierOptionDisplay(param, option, buffer, maxlength); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_JS_CycleOption(param, option); + optionsTopMenu.Display(param, TopMenuPosition_LastCategory); + } +} + + + +// =====[ PRIVATE ]===== + +static void FormatToggleableOptionDisplay(int client, JSOption option, char[] buffer, int maxlength) +{ + if (GOKZ_JS_GetOption(client, option) == JSToggleOption_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + gI_JSOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gI_JSOptionPhrases[option], client, + "Options Menu - Enabled", client); + } +} + +static void FormatDistanceTierOptionDisplay(int client, JSOption option, char[] buffer, int maxlength) +{ + int optionValue = GOKZ_JS_GetOption(client, option); + if (optionValue == DistanceTier_None) // Disabled + { + FormatEx(buffer, maxlength, "%T - %T", + gI_JSOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + // Add a plus sign to anything below the highest tier + if (optionValue < DISTANCETIER_COUNT - 1) + { + FormatEx(buffer, maxlength, "%T - %s+", + gI_JSOptionPhrases[option], client, + gC_DistanceTiers[optionValue]); + } + else + { + FormatEx(buffer, maxlength, "%T - %s", + gI_JSOptionPhrases[option], client, + gC_DistanceTiers[optionValue]); + } + } +} diff --git a/sourcemod/scripting/gokz-localdb.sp b/sourcemod/scripting/gokz-localdb.sp new file mode 100644 index 0000000..d9b07c2 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb.sp @@ -0,0 +1,188 @@ +#include <sourcemod> + +#include <geoip> + +#include <gokz/core> +#include <gokz/localdb> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/jumpstats> +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Local DB", + author = "DanZay", + description = "Provides database for players, maps, courses and times", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-localdb.txt" + +Database gH_DB = null; +DatabaseType g_DBType = DatabaseType_None; +bool gB_ClientSetUp[MAXPLAYERS + 1]; +bool gB_ClientPostAdminChecked[MAXPLAYERS + 1]; +bool gB_Cheater[MAXPLAYERS + 1]; +int gI_PBJSCache[MAXPLAYERS + 1][MODE_COUNT][JUMPTYPE_COUNT][JUMPSTATDB_CACHE_COUNT]; +bool gB_MapSetUp; +int gI_DBCurrentMapID; + +#include "gokz-localdb/api.sp" +#include "gokz-localdb/commands.sp" +#include "gokz-localdb/options.sp" + +#include "gokz-localdb/db/sql.sp" +#include "gokz-localdb/db/helpers.sp" +#include "gokz-localdb/db/cache_js.sp" +#include "gokz-localdb/db/create_tables.sp" +#include "gokz-localdb/db/save_js.sp" +#include "gokz-localdb/db/save_time.sp" +#include "gokz-localdb/db/set_cheater.sp" +#include "gokz-localdb/db/setup_client.sp" +#include "gokz-localdb/db/setup_database.sp" +#include "gokz-localdb/db/setup_map.sp" +#include "gokz-localdb/db/setup_map_courses.sp" +#include "gokz-localdb/db/timer_setup.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-localdb"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-localdb.phrases"); + + CreateGlobalForwards(); + RegisterCommands(); + DB_SetupDatabase(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + char auth[32]; + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientAuthorized(client) && GetClientAuthId(client, AuthId_Engine, auth, sizeof(auth))) + { + OnClientAuthorized(client, auth); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnConfigsExecuted() +{ + DB_SetupMap(); +} + +public void GOKZ_DB_OnMapSetup(int mapID) +{ + DB_SetupMapCourses(); + + for (int client = 1; client < MAXPLAYERS + 1; client++) + { + if (IsValidClient(client) && !IsFakeClient(client) && + gB_ClientPostAdminChecked[client] && + GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled) + { + DB_LoadTimerSetup(client); + } + } +} + +public void OnMapEnd() +{ + gB_MapSetUp = false; +} + +public void OnClientAuthorized(int client, const char[] auth) +{ + DB_SetupClient(client); +} + +public void OnClientPostAdminCheck(int client) +{ + // We need this after OnClientPutInServer cause that's where the VBs get reset + gB_ClientPostAdminChecked[client] = true; + + if (gB_MapSetUp && GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled) + { + DB_LoadTimerSetup(client); + } +} + +public void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater) +{ + DB_CacheJSPBs(client, steamID); +} + +public void GOKZ_OnOptionsLoaded(int client) +{ + if (gB_MapSetUp && gB_ClientPostAdminChecked[client] && GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled) + { + DB_LoadTimerSetup(client); + } +} + +public void OnClientDisconnect(int client) +{ + gB_ClientSetUp[client] = false; + gB_ClientPostAdminChecked[client] = false; +} + +public void GOKZ_OnCourseRegistered(int course) +{ + if (gB_MapSetUp) + { + DB_SetupMapCourse(course); + } +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + int mode = GOKZ_GetCoreOption(client, Option_Mode); + int style = GOKZ_GetCoreOption(client, Option_Style); + DB_SaveTime(client, course, mode, style, time, teleportsUsed); +} + +public void GOKZ_JS_OnLanding(Jump jump) +{ + OnLanding_SaveJumpstat(jump); +} diff --git a/sourcemod/scripting/gokz-localdb/api.sp b/sourcemod/scripting/gokz-localdb/api.sp new file mode 100644 index 0000000..a4bc29c --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/api.sp @@ -0,0 +1,126 @@ +static GlobalForward H_OnDatabaseConnect; +static GlobalForward H_OnClientSetup; +static GlobalForward H_OnMapSetup; +static GlobalForward H_OnTimeInserted; +static GlobalForward H_OnJumpstatPB; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnDatabaseConnect = new GlobalForward("GOKZ_DB_OnDatabaseConnect", ET_Ignore, Param_Cell); + H_OnClientSetup = new GlobalForward("GOKZ_DB_OnClientSetup", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + H_OnMapSetup = new GlobalForward("GOKZ_DB_OnMapSetup", ET_Ignore, Param_Cell); + H_OnTimeInserted = new GlobalForward("GOKZ_DB_OnTimeInserted", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell); + H_OnJumpstatPB = new GlobalForward("GOKZ_DB_OnJumpstatPB", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell); +} + +void Call_OnDatabaseConnect() +{ + Call_StartForward(H_OnDatabaseConnect); + Call_PushCell(g_DBType); + Call_Finish(); +} + +void Call_OnClientSetup(int client, int steamID, bool cheater) +{ + Call_StartForward(H_OnClientSetup); + Call_PushCell(client); + Call_PushCell(steamID); + Call_PushCell(cheater); + Call_Finish(); +} + +void Call_OnMapSetup() +{ + Call_StartForward(H_OnMapSetup); + Call_PushCell(gI_DBCurrentMapID); + Call_Finish(); +} + +void Call_OnTimeInserted(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed) +{ + Call_StartForward(H_OnTimeInserted); + Call_PushCell(client); + Call_PushCell(steamID); + Call_PushCell(mapID); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(style); + Call_PushCell(runTimeMS); + Call_PushCell(teleportsUsed); + Call_Finish(); +} + +void Call_OnJumpstatPB(int client, int jumptype, int mode, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + Call_StartForward(H_OnJumpstatPB); + Call_PushCell(client); + Call_PushCell(jumptype); + Call_PushCell(mode); + Call_PushCell(distance); + Call_PushCell(block); + Call_PushCell(strafes); + Call_PushCell(sync); + Call_PushCell(pre); + Call_PushCell(max); + Call_PushCell(airtime); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_DB_GetDatabase", Native_GetDatabase); + CreateNative("GOKZ_DB_GetDatabaseType", Native_GetDatabaseType); + CreateNative("GOKZ_DB_IsClientSetUp", Native_IsClientSetUp); + CreateNative("GOKZ_DB_IsMapSetUp", Native_IsMapSetUp); + CreateNative("GOKZ_DB_GetCurrentMapID", Native_GetCurrentMapID); + CreateNative("GOKZ_DB_IsCheater", Native_IsCheater); + CreateNative("GOKZ_DB_SetCheater", Native_SetCheater); +} + +public int Native_GetDatabase(Handle plugin, int numParams) +{ + if (gH_DB == null) + { + return view_as<int>(gH_DB); + } + return view_as<int>(CloneHandle(gH_DB)); +} + +public int Native_GetDatabaseType(Handle plugin, int numParams) +{ + return view_as<int>(g_DBType); +} + +public int Native_IsClientSetUp(Handle plugin, int numParams) +{ + return view_as<int>(gB_ClientSetUp[GetNativeCell(1)]); +} + +public int Native_IsMapSetUp(Handle plugin, int numParams) +{ + return view_as<int>(gB_MapSetUp); +} + +public int Native_GetCurrentMapID(Handle plugin, int numParams) +{ + return gI_DBCurrentMapID; +} + +public int Native_IsCheater(Handle plugin, int numParams) +{ + return view_as<int>(gB_Cheater[GetNativeCell(1)]); +} + +public int Native_SetCheater(Handle plugin, int numParams) +{ + DB_SetCheater(GetNativeCell(1), GetNativeCell(2)); + return 0; +} diff --git a/sourcemod/scripting/gokz-localdb/commands.sp b/sourcemod/scripting/gokz-localdb/commands.sp new file mode 100644 index 0000000..2410fc5 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/commands.sp @@ -0,0 +1,199 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_savetimersetup", Command_SaveTimerSetup, "[KZ] Save the current timer setup (virtual buttons and start position)."); + RegConsoleCmd("sm_sts", Command_SaveTimerSetup, "[KZ] Save the current timer setup (virtual buttons and start position)."); + RegConsoleCmd("sm_loadtimersetup", Command_LoadTimerSetup, "[KZ] Load the saved timer setup (virtual buttons and start position)."); + RegConsoleCmd("sm_lts", Command_LoadTimerSetup, "[KZ] Load the saved timer setup (virtual buttons and start position)."); + + RegAdminCmd("sm_setcheater", CommandSetCheater, ADMFLAG_ROOT, "[KZ] Set a SteamID as a cheater. Usage: !setcheater <STEAM_1:X:X>"); + RegAdminCmd("sm_setnotcheater", CommandSetNotCheater, ADMFLAG_ROOT, "[KZ] Set a SteamID as not a cheater. Usage: !setnotcheater <STEAM_1:X:X>"); + RegAdminCmd("sm_deletebestjump", CommandDeleteBestJump, ADMFLAG_ROOT, "[KZ] Remove the top jumpstat of a SteamID. Usage: !deletebestjump <STEAM_1:X:X> <mode> <jump type> <block?>"); + RegAdminCmd("sm_deletealljumps", CommandDeleteAllJumps, ADMFLAG_ROOT, "[KZ] Remove all jumpstats of a SteamID. Usage: !deletealljumps <STEAM_1:X:X>"); + RegAdminCmd("sm_deletejump", CommandDeleteJump, ADMFLAG_ROOT, "[KZ] Remove a jumpstat by it's id. Usage: !deletejump <id>"); + RegAdminCmd("sm_deletetime", CommandDeleteTime, ADMFLAG_ROOT, "[KZ] Remove a time by it's id. Usage: !deletetime <id>"); +} + +public Action Command_SaveTimerSetup(int client, int args) +{ + DB_SaveTimerSetup(client); + return Plugin_Handled; +} + +public Action Command_LoadTimerSetup(int client, int args) +{ + DB_LoadTimerSetup(client, true); + return Plugin_Handled; +} + +public Action CommandSetCheater(int client, int args) +{ + if (args == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No SteamID specified"); + return Plugin_Handled; + } + + char steamID2[64]; + GetCmdArgString(steamID2, sizeof(steamID2)); + int steamAccountID = Steam2ToSteamAccountID(steamID2); + if (steamAccountID == -1) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID"); + } + else + { + DB_SetCheaterSteamID(client, steamAccountID, true); + } + + return Plugin_Handled; +} + +public Action CommandSetNotCheater(int client, int args) +{ + if (args == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No SteamID specified"); + } + + char steamID2[64]; + GetCmdArgString(steamID2, sizeof(steamID2)); + int steamAccountID = Steam2ToSteamAccountID(steamID2); + if (steamAccountID == -1) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID"); + } + else + { + DB_SetCheaterSteamID(client, steamAccountID, false); + } + + return Plugin_Handled; +} + +public Action CommandDeleteBestJump(int client, int args) +{ + if (args < 3) + { + GOKZ_PrintToChat(client, true, "%t", "Delete Best Jump Usage"); + return Plugin_Handled; + } + + int steamAccountID, isBlock, mode, jumpType; + char query[1024], split[4][32]; + + // Get arguments + split[3][0] = '\0'; + GetCmdArgString(query, sizeof(query)); + ExplodeString(query, " ", split, 4, 32, false); + + // SteamID32 + steamAccountID = Steam2ToSteamAccountID(split[0]); + if (steamAccountID == -1) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID"); + return Plugin_Handled; + } + + // Mode + for (mode = 0; mode < MODE_COUNT; mode++) + { + if (StrEqual(split[1], gC_ModeNames[mode]) || StrEqual(split[1], gC_ModeNamesShort[mode], false)) + { + break; + } + } + if (mode == MODE_COUNT) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Mode"); + return Plugin_Handled; + } + + // Jumptype + for (jumpType = 0; jumpType < JUMPTYPE_COUNT; jumpType++) + { + if (StrEqual(split[2], gC_JumpTypes[jumpType]) || StrEqual(split[2], gC_JumpTypesShort[jumpType], false)) + { + break; + } + } + if (jumpType == JUMPTYPE_COUNT) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Jumptype"); + return Plugin_Handled; + } + + // Is it a block jump? + isBlock = StrEqual(split[3], "yes", false) || StrEqual(split[3], "true", false) || StrEqual(split[3], "1"); + + DB_DeleteBestJump(client, steamAccountID, jumpType, mode, isBlock); + + return Plugin_Handled; +} + +public Action CommandDeleteAllJumps(int client, int args) +{ + if (args < 1) + { + GOKZ_PrintToChat(client, true, "%t", "Delete All Jumps Usage"); + return Plugin_Handled; + } + + int steamAccountID; + char steamid[32]; + + GetCmdArgString(steamid, sizeof(steamid)); + steamAccountID = Steam2ToSteamAccountID(steamid); + if (steamAccountID == -1) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID"); + return Plugin_Handled; + } + + DB_DeleteAllJumps(client, steamAccountID); + + return Plugin_Handled; +} + +public Action CommandDeleteJump(int client, int args) +{ + if (args < 1) + { + GOKZ_PrintToChat(client, true, "%t", "Delete Jump Usage"); + return Plugin_Handled; + } + + char buffer[24]; + int jumpID; + GetCmdArgString(buffer, sizeof(buffer)); + if (StringToIntEx(buffer, jumpID) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Jump ID"); + return Plugin_Handled; + } + + DB_DeleteJump(client, jumpID); + + return Plugin_Handled; +} + +public Action CommandDeleteTime(int client, int args) +{ + if (args < 1) + { + GOKZ_PrintToChat(client, true, "%t", "Delete Time Usage"); + return Plugin_Handled; + } + + char buffer[24]; + int timeID; + GetCmdArgString(buffer, sizeof(buffer)); + if (StringToIntEx(buffer, timeID) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Time ID"); + return Plugin_Handled; + } + + DB_DeleteTime(client, timeID); + + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-localdb/db/cache_js.sp b/sourcemod/scripting/gokz-localdb/db/cache_js.sp new file mode 100644 index 0000000..b0df708 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/cache_js.sp @@ -0,0 +1,67 @@ +/* + Caches the player's personal best jumpstats. +*/ + + + +void DB_CacheJSPBs(int client, int steamID) +{ + ClearCache(client); + + char query[1024]; + + Transaction txn = SQL_CreateTransaction(); + + FormatEx(query, sizeof(query), sql_jumpstats_getpbs, steamID); + txn.AddQuery(query); + + FormatEx(query, sizeof(query), sql_jumpstats_getblockpbs, steamID, steamID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CacheJSPBs, DB_TxnFailure_Generic, GetClientUserId(client), DBPrio_High); +} + +public void DB_TxnSuccess_CacheJSPBs(Handle db, int userID, int numQueries, Handle[] results, any[] queryData) +{ + int client = GetClientOfUserId(userID); + if (client < 1 || client > MaxClients || !IsClientAuthorized(client) || IsFakeClient(client)) + { + return; + } + + int distance, mode, jumpType, block; + + while (SQL_FetchRow(results[0])) + { + distance = SQL_FetchInt(results[0], 0); + mode = SQL_FetchInt(results[0], 1); + jumpType = SQL_FetchInt(results[0], 2); + + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = block; + } + + while (SQL_FetchRow(results[1])) + { + distance = SQL_FetchInt(results[1], 0); + mode = SQL_FetchInt(results[1], 1); + jumpType = SQL_FetchInt(results[1], 2); + block = SQL_FetchInt(results[1], 3); + + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = distance; + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = block; + } +} + +void ClearCache(int client) +{ + for (int mode = 0; mode < MODE_COUNT; mode += 1) + { + for (int type = 0; type < JUMPTYPE_COUNT; type += 1) + { + for (int cache = 0; cache < JUMPSTATDB_CACHE_COUNT; cache += 1) + { + gI_PBJSCache[client][mode][type][cache] = 0; + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/create_tables.sp b/sourcemod/scripting/gokz-localdb/db/create_tables.sp new file mode 100644 index 0000000..2138830 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/create_tables.sp @@ -0,0 +1,36 @@ +/* + Table creation and alteration. +*/ + + + +void DB_CreateTables() +{ + Transaction txn = SQL_CreateTransaction(); + + switch (g_DBType) + { + case DatabaseType_SQLite: + { + txn.AddQuery(sqlite_players_create); + txn.AddQuery(sqlite_maps_create); + txn.AddQuery(sqlite_mapcourses_create); + txn.AddQuery(sqlite_times_create); + txn.AddQuery(sqlite_jumpstats_create); + txn.AddQuery(sqlite_vbpos_create); + txn.AddQuery(sqlite_startpos_create); + } + case DatabaseType_MySQL: + { + txn.AddQuery(mysql_players_create); + txn.AddQuery(mysql_maps_create); + txn.AddQuery(mysql_mapcourses_create); + txn.AddQuery(mysql_times_create); + txn.AddQuery(mysql_jumpstats_create); + txn.AddQuery(mysql_vbpos_create); + txn.AddQuery(mysql_startpos_create); + } + } + + SQL_ExecuteTransaction(gH_DB, txn, _, DB_TxnFailure_Generic, _, DBPrio_High); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/helpers.sp b/sourcemod/scripting/gokz-localdb/db/helpers.sp new file mode 100644 index 0000000..1eff866 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/helpers.sp @@ -0,0 +1,18 @@ +/* + Database helper functions and callbacks. +*/ + + + +/* Error report callback for failed transactions */ +public void DB_TxnFailure_Generic(Handle db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) +{ + LogError("Database transaction error: %s", error); +} + +/* Error report callback for failed transactions which deletes the DataPack */ +public void DB_TxnFailure_Generic_DataPack(Handle db, DataPack data, int numQueries, const char[] error, int failIndex, any[] queryData) +{ + delete data; + LogError("Database transaction error: %s", error); +} diff --git a/sourcemod/scripting/gokz-localdb/db/save_js.sp b/sourcemod/scripting/gokz-localdb/db/save_js.sp new file mode 100644 index 0000000..1d50754 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/save_js.sp @@ -0,0 +1,291 @@ +/* + Inserts or updates the player's jumpstat into the database. +*/ + + + +public void OnLanding_SaveJumpstat(Jump jump) +{ + int mode = GOKZ_GetCoreOption(jump.jumper, Option_Mode); + + // No tiers given for 'Invalid' jumps. + if (jump.type == JumpType_Invalid || jump.type == JumpType_FullInvalid + || jump.type == JumpType_Fall || jump.type == JumpType_Other + || jump.type != JumpType_LadderJump && jump.offset < -JS_OFFSET_EPSILON + || jump.distance > JS_MAX_JUMP_DISTANCE + || jump.type == JumpType_LadderJump && jump.distance < JS_MIN_LAJ_BLOCK_DISTANCE + || jump.type != JumpType_LadderJump && jump.distance < JS_MIN_BLOCK_DISTANCE) + { + return; + } + + char query[1024]; + DataPack data; + int steamid = GetSteamAccountID(jump.jumper); + int int_dist = RoundToNearest(jump.distance * GOKZ_DB_JS_DISTANCE_PRECISION); + + // Non-block + if (gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Distance] == 0 + || int_dist > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Distance]) + { + data = JSRecord_FillDataPack(jump, steamid, mode, false); + Transaction txn_noblock = SQL_CreateTransaction(); + FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jump.type, mode, 0); + txn_noblock.AddQuery(query); + SQL_ExecuteTransaction(gH_DB, txn_noblock, DB_TxnSuccess_LookupJSRecordForSave, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); + } + + // Block + if (jump.block > 0 + && (gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block] == 0 + || (jump.block > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block] + || jump.block == gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block] + && int_dist > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_BlockDistance]))) + { + data = JSRecord_FillDataPack(jump, steamid, mode, true); + Transaction txn_block = SQL_CreateTransaction(); + FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jump.type, mode, 1); + txn_block.AddQuery(query); + SQL_ExecuteTransaction(gH_DB, txn_block, DB_TxnSuccess_LookupJSRecordForSave, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); + } +} + +static DataPack JSRecord_FillDataPack(Jump jump, int steamid, int mode, bool blockJump) +{ + DataPack data = new DataPack(); + data.WriteCell(jump.jumper); + data.WriteCell(steamid); + data.WriteCell(jump.type); + data.WriteCell(mode); + data.WriteCell(RoundToNearest(jump.distance * GOKZ_DB_JS_DISTANCE_PRECISION)); + data.WriteCell(blockJump ? jump.block : 0); + data.WriteCell(jump.strafes); + data.WriteCell(RoundToNearest(jump.sync * GOKZ_DB_JS_SYNC_PRECISION)); + data.WriteCell(RoundToNearest(jump.preSpeed * GOKZ_DB_JS_PRE_PRECISION)); + data.WriteCell(RoundToNearest(jump.maxSpeed * GOKZ_DB_JS_MAX_PRECISION)); + data.WriteCell(RoundToNearest(jump.duration * GetTickInterval() * GOKZ_DB_JS_AIRTIME_PRECISION)); + return data; +} + +public void DB_TxnSuccess_LookupJSRecordForSave(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = data.ReadCell(); + int steamid = data.ReadCell(); + int jumpType = data.ReadCell(); + int mode = data.ReadCell(); + int distance = data.ReadCell(); + int block = data.ReadCell(); + int strafes = data.ReadCell(); + int sync = data.ReadCell(); + int pre = data.ReadCell(); + int max = data.ReadCell(); + int airtime = data.ReadCell(); + + if (!IsValidClient(client)) + { + delete data; + return; + } + + char query[1024]; + int rows = SQL_GetRowCount(results[0]); + if (rows == 0) + { + FormatEx(query, sizeof(query), sql_jumpstats_insert, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime); + } + else + { + SQL_FetchRow(results[0]); + int rec_distance = SQL_FetchInt(results[0], JumpstatDB_Lookup_Distance); + int rec_block = SQL_FetchInt(results[0], JumpstatDB_Lookup_Block); + + if (rec_block == 0) + { + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = rec_distance; + } + else + { + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = rec_block; + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = rec_distance; + } + + if (block < rec_block || block == rec_block && distance < rec_distance) + { + delete data; + return; + } + + if (rows < GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER) + { + FormatEx(query, sizeof(query), sql_jumpstats_insert, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime); + } + else + { + for (int i = 1; i < GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER; i++) + { + SQL_FetchRow(results[0]); + } + int min_rec_id = SQL_FetchInt(results[0], JumpstatDB_Lookup_JumpID); + FormatEx(query, sizeof(query), sql_jumpstats_update, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime, min_rec_id); + } + + } + + Transaction txn = SQL_CreateTransaction(); + txn.AddQuery(query); + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveJSRecord, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_SaveJSRecord(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = data.ReadCell(); + data.ReadCell(); + int jumpType = data.ReadCell(); + int mode = data.ReadCell(); + int distance = data.ReadCell(); + int block = data.ReadCell(); + int strafes = data.ReadCell(); + int sync = data.ReadCell(); + int pre = data.ReadCell(); + int max = data.ReadCell(); + int airtime = data.ReadCell(); + delete data; + + if (!IsValidClient(client) || GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled) + { + return; + } + + float distanceFloat = float(distance) / GOKZ_DB_JS_DISTANCE_PRECISION; + float syncFloat = float(sync) / GOKZ_DB_JS_SYNC_PRECISION; + float preFloat = float(pre) / GOKZ_DB_JS_PRE_PRECISION; + float maxFloat = float(max) / GOKZ_DB_JS_MAX_PRECISION; + + if (block == 0) + { + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = distance; + GOKZ_PrintToChat(client, true, "%t", "Jump Record", + client, + gC_JumpTypes[jumpType], + distanceFloat, + gC_ModeNamesShort[mode]); + } + else + { + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = block; + gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = distance; + GOKZ_PrintToChat(client, true, "%t", "Block Jump Record", + client, + block, + gC_JumpTypes[jumpType], + distanceFloat, + gC_ModeNamesShort[mode], + block); + } + + Call_OnJumpstatPB(client, jumpType, mode, distanceFloat, block, strafes, syncFloat, preFloat, maxFloat, airtime); +} + +public void DB_DeleteBestJump(int client, int steamAccountID, int jumpType, int mode, int isBlock) +{ + DataPack data = new DataPack(); + data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console + data.WriteCell(steamAccountID); + data.WriteCell(jumpType); + data.WriteCell(mode); + data.WriteCell(isBlock); + + char query[1024]; + + FormatEx(query, sizeof(query), sql_jumpstats_deleterecord, steamAccountID, jumpType, mode, isBlock); + + Transaction txn = SQL_CreateTransaction(); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_BestJumpDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_BestJumpDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + char blockString[16] = ""; + + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamAccountID = data.ReadCell(); + int jumpType = data.ReadCell(); + int mode = data.ReadCell(); + bool isBlock = data.ReadCell() == 1; + delete data; + + if (isBlock) + { + FormatEx(blockString, sizeof(blockString), "%T ", "Block", client); + } + + ClearCache(client); + + GOKZ_PrintToChatAndLog(client, true, "%t", "Best Jump Deleted", + gC_ModeNames[mode], + blockString, + gC_JumpTypes[jumpType], + steamAccountID & 1, + steamAccountID >> 1); +} + +public void DB_DeleteAllJumps(int client, int steamAccountID) +{ + DataPack data = new DataPack(); + data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console + data.WriteCell(steamAccountID); + + char query[1024]; + + FormatEx(query, sizeof(query), sql_jumpstats_deleteallrecords, steamAccountID); + + Transaction txn = SQL_CreateTransaction(); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_AllJumpsDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_AllJumpsDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamAccountID = data.ReadCell(); + delete data; + + ClearCache(client); + + GOKZ_PrintToChatAndLog(client, true, "%t", "All Jumps Deleted", + steamAccountID & 1, + steamAccountID >> 1); +} + +public void DB_DeleteJump(int client, int jumpID) +{ + DataPack data = new DataPack(); + data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console + data.WriteCell(jumpID); + + char query[1024]; + FormatEx(query, sizeof(query), sql_jumpstats_deletejump, jumpID); + + Transaction txn = SQL_CreateTransaction(); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_JumpDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_JumpDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int jumpID = data.ReadCell(); + delete data; + + GOKZ_PrintToChatAndLog(client, true, "%t", "Jump Deleted", + jumpID); +} diff --git a/sourcemod/scripting/gokz-localdb/db/save_time.sp b/sourcemod/scripting/gokz-localdb/db/save_time.sp new file mode 100644 index 0000000..84589a5 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/save_time.sp @@ -0,0 +1,83 @@ +/* + Inserts the player's time into the database. +*/ + + + +void DB_SaveTime(int client, int course, int mode, int style, float runTime, int teleportsUsed) +{ + if (IsFakeClient(client)) + { + return; + } + + char query[1024]; + int steamID = GetSteamAccountID(client); + int mapID = GOKZ_DB_GetCurrentMapID(); + int runTimeMS = GOKZ_DB_TimeFloatToInt(runTime); + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(steamID); + data.WriteCell(mapID); + data.WriteCell(course); + data.WriteCell(mode); + data.WriteCell(style); + data.WriteCell(runTimeMS); + data.WriteCell(teleportsUsed); + + Transaction txn = SQL_CreateTransaction(); + + // Save runTime to DB + FormatEx(query, sizeof(query), sql_times_insert, steamID, mode, style, runTimeMS, teleportsUsed, mapID, course); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveTime, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal); +} + +public void DB_TxnSuccess_SaveTime(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamID = data.ReadCell(); + int mapID = data.ReadCell(); + int course = data.ReadCell(); + int mode = data.ReadCell(); + int style = data.ReadCell(); + int runTimeMS = data.ReadCell(); + int teleportsUsed = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + Call_OnTimeInserted(client, steamID, mapID, course, mode, style, runTimeMS, teleportsUsed); +} + +public void DB_DeleteTime(int client, int timeID) +{ + DataPack data = new DataPack(); + data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console + data.WriteCell(timeID); + + char query[1024]; + FormatEx(query, sizeof(query), sql_times_delete, timeID); + + Transaction txn = SQL_CreateTransaction(); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_TimeDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_TimeDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int timeID = data.ReadCell(); + delete data; + + GOKZ_PrintToChatAndLog(client, true, "%t", "Time Deleted", + timeID); +} diff --git a/sourcemod/scripting/gokz-localdb/db/set_cheater.sp b/sourcemod/scripting/gokz-localdb/db/set_cheater.sp new file mode 100644 index 0000000..c63f161 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/set_cheater.sp @@ -0,0 +1,64 @@ +/* + Sets whether player is a cheater in the database. +*/ + + + +void DB_SetCheater(int cheaterClient, bool cheater) +{ + if (gB_Cheater[cheaterClient] == cheater) + { + return; + } + + gB_Cheater[cheaterClient] = cheater; + + DataPack data = new DataPack(); + data.WriteCell(-1); + data.WriteCell(GetSteamAccountID(cheaterClient)); + data.WriteCell(cheater); + + char query[128]; + + Transaction txn = SQL_CreateTransaction(); + + FormatEx(query, sizeof(query), sql_players_set_cheater, cheater ? 1 : 0, GetSteamAccountID(cheaterClient)); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetCheater, DB_TxnFailure_Generic_DataPack, data, DBPrio_High); +} + +void DB_SetCheaterSteamID(int client, int cheaterSteamID, bool cheater) +{ + DataPack data = new DataPack(); + data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console + data.WriteCell(cheaterSteamID); + data.WriteCell(cheater); + + char query[128]; + + Transaction txn = SQL_CreateTransaction(); + + FormatEx(query, sizeof(query), sql_players_set_cheater, cheater ? 1 : 0, cheaterSteamID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetCheater, DB_TxnFailure_Generic_DataPack, data, DBPrio_High); +} + +public void DB_TxnSuccess_SetCheater(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamID = data.ReadCell(); + bool cheater = view_as<bool>(data.ReadCell()); + delete data; + + if (cheater) + { + GOKZ_PrintToChatAndLog(client, true, "%t", "Set Cheater", steamID & 1, steamID >> 1); + } + else + { + GOKZ_PrintToChatAndLog(client, true, "%t", "Set Not Cheater", steamID & 1, steamID >> 1); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/setup_client.sp b/sourcemod/scripting/gokz-localdb/db/setup_client.sp new file mode 100644 index 0000000..848be87 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/setup_client.sp @@ -0,0 +1,99 @@ +/* + Inserts the player into the database, or else updates their information. +*/ + + + +void DB_SetupClient(int client) +{ + if (IsFakeClient(client)) + { + return; + } + + // Setup Client Step 1 - Upsert them into Players Table + char query[1024], name[MAX_NAME_LENGTH], nameEscaped[MAX_NAME_LENGTH * 2 + 1], clientIP[16], country[45]; + + int steamID = GetSteamAccountID(client); + if (!GetClientName(client, name, MAX_NAME_LENGTH)) + { + LogMessage("Couldn't get name of %L.", client); + name = "Unknown"; + } + SQL_EscapeString(gH_DB, name, nameEscaped, MAX_NAME_LENGTH * 2 + 1); + if (!GetClientIP(client, clientIP, sizeof(clientIP))) + { + LogMessage("Couldn't get IP of %L.", client); + clientIP = "Unknown"; + } + if (!GeoipCountry(clientIP, country, sizeof(country))) + { + LogMessage("Couldn't get country of %L (%s).", client, clientIP); + country = "Unknown"; + } + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(steamID); + + Transaction txn = SQL_CreateTransaction(); + + // Insert/Update player into Players table + switch (g_DBType) + { + case DatabaseType_SQLite: + { + // UPDATE OR IGNORE + FormatEx(query, sizeof(query), sqlite_players_update, nameEscaped, country, clientIP, steamID); + txn.AddQuery(query); + // INSERT OR IGNORE + FormatEx(query, sizeof(query), sqlite_players_insert, nameEscaped, country, clientIP, steamID); + txn.AddQuery(query); + } + case DatabaseType_MySQL: + { + // INSERT ... ON DUPLICATE KEY ... + FormatEx(query, sizeof(query), mysql_players_upsert, nameEscaped, country, clientIP, steamID); + txn.AddQuery(query); + } + } + + FormatEx(query, sizeof(query), sql_players_get_cheater, steamID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetupClient, DB_TxnFailure_Generic_DataPack, data, DBPrio_High); +} + +public void DB_TxnSuccess_SetupClient(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamID = data.ReadCell(); + delete data; + + if (client == 0 || !IsClientAuthorized(client)) + { + return; + } + + switch (g_DBType) + { + case DatabaseType_SQLite: + { + if (SQL_FetchRow(results[2])) + { + gB_Cheater[client] = SQL_FetchInt(results[2], 0) == 1; + } + } + case DatabaseType_MySQL: + { + if (SQL_FetchRow(results[1])) + { + gB_Cheater[client] = SQL_FetchInt(results[1], 0) == 1; + } + } + } + + gB_ClientSetUp[client] = true; + Call_OnClientSetup(client, steamID, gB_Cheater[client]); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/setup_database.sp b/sourcemod/scripting/gokz-localdb/db/setup_database.sp new file mode 100644 index 0000000..4965541 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/setup_database.sp @@ -0,0 +1,34 @@ +/* + Set up the connection to the local database. +*/ + + + +void DB_SetupDatabase() +{ + char error[255]; + gH_DB = SQL_Connect("gokz", true, error, sizeof(error)); + if (gH_DB == null) + { + SetFailState("Database connection failed. Error: \"%s\".", error); + } + + char databaseType[8]; + SQL_ReadDriver(gH_DB, databaseType, sizeof(databaseType)); + if (strcmp(databaseType, "sqlite", false) == 0) + { + g_DBType = DatabaseType_SQLite; + } + else if (strcmp(databaseType, "mysql", false) == 0) + { + g_DBType = DatabaseType_MySQL; + } + else + { + SetFailState("Incompatible database driver. Use SQLite or MySQL."); + } + + DB_CreateTables(); + + Call_OnDatabaseConnect(); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/setup_map.sp b/sourcemod/scripting/gokz-localdb/db/setup_map.sp new file mode 100644 index 0000000..a02e2d2 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/setup_map.sp @@ -0,0 +1,71 @@ +/* + Inserts the map information into the database. + Retrieves the MapID of the map and stores it in a global variable. +*/ + + + +void DB_SetupMap() +{ + gB_MapSetUp = false; + + char query[1024]; + + char map[PLATFORM_MAX_PATH]; + GetCurrentMapDisplayName(map, sizeof(map)); + + char escapedMap[PLATFORM_MAX_PATH * 2 + 1]; + SQL_EscapeString(gH_DB, map, escapedMap, sizeof(escapedMap)); + + Transaction txn = SQL_CreateTransaction(); + + // Insert/Update map into database + switch (g_DBType) + { + case DatabaseType_SQLite: + { + // UPDATE OR IGNORE + FormatEx(query, sizeof(query), sqlite_maps_update, escapedMap); + txn.AddQuery(query); + // INSERT OR IGNORE + FormatEx(query, sizeof(query), sqlite_maps_insert, escapedMap); + txn.AddQuery(query); + } + case DatabaseType_MySQL: + { + // INSERT ... ON DUPLICATE KEY ... + FormatEx(query, sizeof(query), mysql_maps_upsert, escapedMap); + txn.AddQuery(query); + } + } + // Retrieve mapID of map name + FormatEx(query, sizeof(query), sql_maps_findid, escapedMap, escapedMap); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetupMap, DB_TxnFailure_Generic, 0, DBPrio_High); +} + +public void DB_TxnSuccess_SetupMap(Handle db, any data, int numQueries, Handle[] results, any[] queryData) +{ + switch (g_DBType) + { + case DatabaseType_SQLite: + { + if (SQL_FetchRow(results[2])) + { + gI_DBCurrentMapID = SQL_FetchInt(results[2], 0); + gB_MapSetUp = true; + Call_OnMapSetup(); + } + } + case DatabaseType_MySQL: + { + if (SQL_FetchRow(results[1])) + { + gI_DBCurrentMapID = SQL_FetchInt(results[1], 0); + gB_MapSetUp = true; + Call_OnMapSetup(); + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp b/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp new file mode 100644 index 0000000..69bb89e --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp @@ -0,0 +1,45 @@ +/* + Inserts the map's courses into the database. +*/ + + + +void DB_SetupMapCourses() +{ + char query[512]; + + Transaction txn = SQL_CreateTransaction(); + + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + if (!GOKZ_GetCourseRegistered(course)) + { + continue; + } + + switch (g_DBType) + { + case DatabaseType_SQLite:FormatEx(query, sizeof(query), sqlite_mapcourses_insert, gI_DBCurrentMapID, course); + case DatabaseType_MySQL:FormatEx(query, sizeof(query), mysql_mapcourses_insert, gI_DBCurrentMapID, course); + } + txn.AddQuery(query); + } + + SQL_ExecuteTransaction(gH_DB, txn, INVALID_FUNCTION, DB_TxnFailure_Generic, _, DBPrio_High); +} + +void DB_SetupMapCourse(int course) +{ + char query[512]; + + Transaction txn = SQL_CreateTransaction(); + + switch (g_DBType) + { + case DatabaseType_SQLite:FormatEx(query, sizeof(query), sqlite_mapcourses_insert, gI_DBCurrentMapID, course); + case DatabaseType_MySQL:FormatEx(query, sizeof(query), mysql_mapcourses_insert, gI_DBCurrentMapID, course); + } + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, INVALID_FUNCTION, DB_TxnFailure_Generic, _, DBPrio_High); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localdb/db/sql.sp b/sourcemod/scripting/gokz-localdb/db/sql.sp new file mode 100644 index 0000000..46ea5e3 --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/sql.sp @@ -0,0 +1,406 @@ +/* + SQL query templates. +*/ + + + +// =====[ PLAYERS ]===== + +char sqlite_players_create[] = "\ +CREATE TABLE IF NOT EXISTS Players ( \ + SteamID32 INTEGER NOT NULL, \ + Alias TEXT, \ + Country TEXT, \ + IP TEXT, \ + Cheater INTEGER NOT NULL DEFAULT '0', \ + LastPlayed TIMESTAMP NULL DEFAULT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Player PRIMARY KEY (SteamID32))"; + +char mysql_players_create[] = "\ +CREATE TABLE IF NOT EXISTS Players ( \ + SteamID32 INTEGER UNSIGNED NOT NULL, \ + Alias VARCHAR(32), \ + Country VARCHAR(45), \ + IP VARCHAR(15), \ + Cheater TINYINT UNSIGNED NOT NULL DEFAULT '0', \ + LastPlayed TIMESTAMP NULL DEFAULT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Player PRIMARY KEY (SteamID32))"; + +char sqlite_players_insert[] = "\ +INSERT OR IGNORE INTO Players (Alias, Country, IP, SteamID32, LastPlayed) \ + VALUES ('%s', '%s', '%s', %d, CURRENT_TIMESTAMP)"; + +char sqlite_players_update[] = "\ +UPDATE OR IGNORE Players \ + SET Alias='%s', Country='%s', IP='%s', LastPlayed=CURRENT_TIMESTAMP \ + WHERE SteamID32=%d"; + +char mysql_players_upsert[] = "\ +INSERT INTO Players (Alias, Country, IP, SteamID32, LastPlayed) \ + VALUES ('%s', '%s', '%s', %d, CURRENT_TIMESTAMP) \ + ON DUPLICATE KEY UPDATE \ + SteamID32=VALUES(SteamID32), Alias=VALUES(Alias), Country=VALUES(Country), \ + IP=VALUES(IP), LastPlayed=VALUES(LastPlayed)"; + +char sql_players_get_cheater[] = "\ +SELECT Cheater \ + FROM Players \ + WHERE SteamID32=%d"; + +char sql_players_set_cheater[] = "\ +UPDATE Players \ + SET Cheater=%d \ + WHERE SteamID32=%d"; + + + +// =====[ MAPS ]===== + +char sqlite_maps_create[] = "\ +CREATE TABLE IF NOT EXISTS Maps ( \ + MapID INTEGER NOT NULL, \ + Name VARCHAR(32) NOT NULL UNIQUE, \ + LastPlayed TIMESTAMP NULL DEFAULT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Maps PRIMARY KEY (MapID))"; + +char mysql_maps_create[] = "\ +CREATE TABLE IF NOT EXISTS Maps ( \ + MapID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \ + Name VARCHAR(32) NOT NULL UNIQUE, \ + LastPlayed TIMESTAMP NULL DEFAULT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Maps PRIMARY KEY (MapID))"; + +char sqlite_maps_insert[] = "\ +INSERT OR IGNORE INTO Maps (Name, LastPlayed) \ + VALUES ('%s', CURRENT_TIMESTAMP)"; + +char sqlite_maps_update[] = "\ +UPDATE OR IGNORE Maps \ + SET LastPlayed=CURRENT_TIMESTAMP \ + WHERE Name='%s'"; + +char mysql_maps_upsert[] = "\ +INSERT INTO Maps (Name, LastPlayed) \ + VALUES ('%s', CURRENT_TIMESTAMP) \ + ON DUPLICATE KEY UPDATE \ + LastPlayed=CURRENT_TIMESTAMP"; + +char sql_maps_findid[] = "\ +SELECT MapID, Name \ + FROM Maps \ + WHERE Name LIKE '%%%s%%' \ + ORDER BY (Name='%s') DESC, LENGTH(Name) \ + LIMIT 1"; + + + +// =====[ MAPCOURSES ]===== + +char sqlite_mapcourses_create[] = "\ +CREATE TABLE IF NOT EXISTS MapCourses ( \ + MapCourseID INTEGER NOT NULL, \ + MapID INTEGER NOT NULL, \ + Course INTEGER NOT NULL, \ + Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_MapCourses PRIMARY KEY (MapCourseID), \ + CONSTRAINT UQ_MapCourses_MapIDCourse UNIQUE (MapID, Course), \ + CONSTRAINT FK_MapCourses_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char mysql_mapcourses_create[] = "\ +CREATE TABLE IF NOT EXISTS MapCourses ( \ + MapCourseID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \ + MapID INTEGER UNSIGNED NOT NULL, \ + Course INTEGER UNSIGNED NOT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_MapCourses PRIMARY KEY (MapCourseID), \ + CONSTRAINT UQ_MapCourses_MapIDCourse UNIQUE (MapID, Course), \ + CONSTRAINT FK_MapCourses_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char sqlite_mapcourses_insert[] = "\ +INSERT OR IGNORE INTO MapCourses (MapID, Course) \ + VALUES (%d, %d)"; + +char mysql_mapcourses_insert[] = "\ +INSERT IGNORE INTO MapCourses (MapID, Course) \ + VALUES (%d, %d)"; + + + +// =====[ TIMES ]===== + +char sqlite_times_create[] = "\ +CREATE TABLE IF NOT EXISTS Times ( \ + TimeID INTEGER NOT NULL, \ + SteamID32 INTEGER NOT NULL, \ + MapCourseID INTEGER NOT NULL, \ + Mode INTEGER NOT NULL, \ + Style INTEGER NOT NULL, \ + RunTime INTEGER NOT NULL, \ + Teleports INTEGER NOT NULL, \ + Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Times PRIMARY KEY (TimeID), \ + CONSTRAINT FK_Times_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \ + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT FK_Times_MapCourseID \ + FOREIGN KEY (MapCourseID) REFERENCES MapCourses(MapCourseID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char mysql_times_create[] = "\ +CREATE TABLE IF NOT EXISTS Times ( \ + TimeID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \ + SteamID32 INTEGER UNSIGNED NOT NULL, \ + MapCourseID INTEGER UNSIGNED NOT NULL, \ + Mode TINYINT UNSIGNED NOT NULL, \ + Style TINYINT UNSIGNED NOT NULL, \ + RunTime INTEGER UNSIGNED NOT NULL, \ + Teleports SMALLINT UNSIGNED NOT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Times PRIMARY KEY (TimeID), \ + CONSTRAINT FK_Times_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \ + ON UPDATE CASCADE ON DELETE CASCADE, \ + CONSTRAINT FK_Times_MapCourseID FOREIGN KEY (MapCourseID) REFERENCES MapCourses(MapCourseID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char sql_times_insert[] = "\ +INSERT INTO Times (SteamID32, MapCourseID, Mode, Style, RunTime, Teleports) \ + SELECT %d, MapCourseID, %d, %d, %d, %d \ + FROM MapCourses \ + WHERE MapID=%d AND Course=%d"; + +char sql_times_delete[] = "\ +DELETE FROM Times \ + WHERE TimeID=%d"; + + + +// =====[ JUMPSTATS ]===== + +char sqlite_jumpstats_create[] = "\ +CREATE TABLE IF NOT EXISTS Jumpstats ( \ + JumpID INTEGER NOT NULL, \ + SteamID32 INTEGER NOT NULL, \ + JumpType INTEGER NOT NULL, \ + Mode INTEGER NOT NULL, \ + Distance INTEGER NOT NULL, \ + IsBlockJump INTEGER NOT NULL, \ + Block INTEGER NOT NULL, \ + Strafes INTEGER NOT NULL, \ + Sync INTEGER NOT NULL, \ + Pre INTEGER NOT NULL, \ + Max INTEGER NOT NULL, \ + Airtime INTEGER NOT NULL, \ + Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Jumpstats PRIMARY KEY (JumpID), \ + CONSTRAINT FK_Jumpstats_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char mysql_jumpstats_create[] = "\ +CREATE TABLE IF NOT EXISTS Jumpstats ( \ + JumpID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \ + SteamID32 INTEGER UNSIGNED NOT NULL, \ + JumpType TINYINT UNSIGNED NOT NULL, \ + Mode TINYINT UNSIGNED NOT NULL, \ + Distance INTEGER UNSIGNED NOT NULL, \ + IsBlockJump TINYINT UNSIGNED NOT NULL, \ + Block SMALLINT UNSIGNED NOT NULL, \ + Strafes INTEGER UNSIGNED NOT NULL, \ + Sync INTEGER UNSIGNED NOT NULL, \ + Pre INTEGER UNSIGNED NOT NULL, \ + Max INTEGER UNSIGNED NOT NULL, \ + Airtime INTEGER UNSIGNED NOT NULL, \ + Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ + CONSTRAINT PK_Jumpstats PRIMARY KEY (JumpID), \ + CONSTRAINT FK_Jumpstats_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char sql_jumpstats_insert[] = "\ +INSERT INTO Jumpstats (SteamID32, JumpType, Mode, Distance, IsBlockJump, Block, Strafes, Sync, Pre, Max, Airtime) \ + VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)"; + +char sql_jumpstats_update[] = "\ +UPDATE Jumpstats \ + SET \ + SteamID32=%d, \ + JumpType=%d, \ + Mode=%d, \ + Distance=%d, \ + IsBlockJump=%d, \ + Block=%d, \ + Strafes=%d, \ + Sync=%d, \ + Pre=%d, \ + Max=%d, \ + Airtime=%d \ + WHERE \ + JumpID=%d"; + +char sql_jumpstats_getrecord[] = "\ +SELECT JumpID, Distance, Block \ + FROM \ + Jumpstats \ + WHERE \ + SteamID32=%d AND \ + JumpType=%d AND \ + Mode=%d AND \ + IsBlockJump=%d \ + ORDER BY Block DESC, Distance DESC"; + +char sql_jumpstats_deleterecord[] = "\ +DELETE \ + FROM \ + Jumpstats \ + WHERE \ + JumpID = \ + ( SELECT * FROM ( \ + SELECT JumpID \ + FROM \ + Jumpstats \ + WHERE \ + SteamID32=%d AND \ + JumpType=%d AND \ + Mode=%d AND \ + IsBlockJump=%d \ + ORDER BY Block DESC, Distance DESC \ + LIMIT 1 \ + ) AS tmp \ + )"; + +char sql_jumpstats_deleteallrecords[] = "\ +DELETE \ + FROM \ + Jumpstats \ + WHERE \ + SteamID32 = %d;"; + +char sql_jumpstats_deletejump[] = "\ +DELETE \ + FROM \ + Jumpstats \ + WHERE \ + JumpID = %d;"; + +char sql_jumpstats_getpbs[] = "\ +SELECT MAX(Distance), Mode, JumpType \ + FROM \ + Jumpstats \ + WHERE \ + SteamID32=%d \ + GROUP BY \ + Mode, JumpType"; + +char sql_jumpstats_getblockpbs[] = "\ +SELECT MAX(js.Distance), js.Mode, js.JumpType, js.Block \ + FROM \ + Jumpstats js \ + INNER JOIN \ + ( \ + SELECT Mode, JumpType, MAX(BLOCK) Block \ + FROM \ + Jumpstats \ + WHERE \ + IsBlockJump=1 AND \ + SteamID32=%d \ + GROUP BY \ + Mode, JumpType \ + ) pb \ + ON \ + js.Mode=pb.Mode AND \ + js.JumpType=pb.JumpType AND \ + js.Block=pb.Block \ + WHERE \ + js.SteamID32=%d \ + GROUP BY \ + js.Mode, js.JumpType, js.Block"; + + + +// =====[ VB POSITIONS ]===== + +char sqlite_vbpos_create[] = "\ +CREATE TABLE IF NOT EXISTS VBPosition ( \ + SteamID32 INTEGER NOT NULL, \ + MapID INTEGER NOT NULL, \ + X REAL NOT NULL, \ + Y REAL NOT NULL, \ + Z REAL NOT NULL, \ + Course INTEGER NOT NULL, \ + IsStart INTEGER NOT NULL, \ + CONSTRAINT PK_VBPosition PRIMARY KEY (SteamID32, MapID, IsStart), \ + CONSTRAINT FK_VBPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \ + CONSTRAINT FK_VBPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char mysql_vbpos_create[] = "\ +CREATE TABLE IF NOT EXISTS VBPosition ( \ + SteamID32 INTEGER UNSIGNED NOT NULL, \ + MapID INTEGER UNSIGNED NOT NULL, \ + X REAL NOT NULL, \ + Y REAL NOT NULL, \ + Z REAL NOT NULL, \ + Course INTEGER NOT NULL, \ + IsStart INTEGER NOT NULL, \ + CONSTRAINT PK_VBPosition PRIMARY KEY (SteamID32, MapID, IsStart), \ + CONSTRAINT FK_VBPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \ + CONSTRAINT FK_VBPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char sql_vbpos_upsert[] = "\ +REPLACE INTO VBPosition (SteamID32, MapID, X, Y, Z, Course, IsStart) \ + VALUES (%d, %d, %f, %f, %f, %d, %d)"; + +char sql_vbpos_get[] = "\ +SELECT SteamID32, MapID, Course, IsStart, X, Y, Z \ + FROM \ + VBPosition \ + WHERE \ + SteamID32 = %d AND \ + MapID = %d"; + + + +// =====[ START POSITIONS ]===== + +char sqlite_startpos_create[] = "\ +CREATE TABLE IF NOT EXISTS StartPosition ( \ + SteamID32 INTEGER NOT NULL, \ + MapID INTEGER NOT NULL, \ + X REAL NOT NULL, \ + Y REAL NOT NULL, \ + Z REAL NOT NULL, \ + Angle0 REAL NOT NULL, \ + Angle1 REAL NOT NULL, \ + CONSTRAINT PK_StartPosition PRIMARY KEY (SteamID32, MapID), \ + CONSTRAINT FK_StartPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \ + CONSTRAINT FK_StartPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char mysql_startpos_create[] = "\ +CREATE TABLE IF NOT EXISTS StartPosition ( \ + SteamID32 INTEGER UNSIGNED NOT NULL, \ + MapID INTEGER UNSIGNED NOT NULL, \ + X REAL NOT NULL, \ + Y REAL NOT NULL, \ + Z REAL NOT NULL, \ + Angle0 REAL NOT NULL, \ + Angle1 REAL NOT NULL, \ + CONSTRAINT PK_StartPosition PRIMARY KEY (SteamID32, MapID), \ + CONSTRAINT FK_StartPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \ + CONSTRAINT FK_StartPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \ + ON UPDATE CASCADE ON DELETE CASCADE)"; + +char sql_startpos_upsert[] = "\ +REPLACE INTO StartPosition (SteamID32, MapID, X, Y, Z, Angle0, Angle1) \ + VALUES (%d, %d, %f, %f, %f, %f, %f)"; + +char sql_startpos_get[] = "\ +SELECT SteamID32, MapID, X, Y, Z, Angle0, Angle1 \ + FROM \ + StartPosition \ + WHERE \ + SteamID32 = %d AND \ + MapID = %d"; diff --git a/sourcemod/scripting/gokz-localdb/db/timer_setup.sp b/sourcemod/scripting/gokz-localdb/db/timer_setup.sp new file mode 100644 index 0000000..b123eeb --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/db/timer_setup.sp @@ -0,0 +1,167 @@ + +// ===== [ SAVE TIMER SETUP ] ===== + +void DB_SaveTimerSetup(int client) +{ + bool txnHasQuery = false; + int course; + float position[3], angles[3]; + + if (!IsValidClient(client)) + { + return; + } + + int steamid = GetSteamAccountID(client); + DataPack data = new DataPack(); + + data.WriteCell(client); + data.WriteCell(steamid); + + char query[1024]; + Transaction txn = SQL_CreateTransaction(); + + if (GOKZ_GetStartPosition(client, position, angles) == StartPositionType_Custom) + { + FormatEx(query, sizeof(query), sql_startpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], angles[0], angles[1]); + txn.AddQuery(query); + txnHasQuery = true; + } + + course = GOKZ_GetVirtualButtonPosition(client, position, true); + if (course != -1) + { + FormatEx(query, sizeof(query), sql_vbpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], course, 1); + txn.AddQuery(query); + txnHasQuery = true; + } + + course = GOKZ_GetVirtualButtonPosition(client, position, false); + if (course != -1) + { + FormatEx(query, sizeof(query), sql_vbpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], course, 0); + txn.AddQuery(query); + txnHasQuery = true; + } + + if (txnHasQuery) + { + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveTimerSetup, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); + } + else + { + delete data; + delete txn; + } +} + +public void DB_TxnSuccess_SaveTimerSetup(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = data.ReadCell(); + int steamid = data.ReadCell(); + delete data; + + if (!IsValidClient(client) || steamid != GetSteamAccountID(client)) + { + return; + } + + GOKZ_PrintToChat(client, true, "%t", "Timer Setup Saved"); +} + + + +// ===== [ LOAD TIMER SETUP ] ===== + +void DB_LoadTimerSetup(int client, bool doChatMessage = false) +{ + if (!IsValidClient(client)) + { + return; + } + + int steamid = GetSteamAccountID(client); + + DataPack data = new DataPack(); + data.WriteCell(client); + data.WriteCell(steamid); + data.WriteCell(doChatMessage); + + char query[1024]; + Transaction txn = SQL_CreateTransaction(); + + // Virtual Buttons + FormatEx(query, sizeof(query), sql_vbpos_get, steamid, gI_DBCurrentMapID); + txn.AddQuery(query); + + // Start Position + FormatEx(query, sizeof(query), sql_startpos_get, steamid, gI_DBCurrentMapID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_LoadTimerSetup, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal); +} + +public void DB_TxnSuccess_LoadTimerSetup(Handle db, DataPack data, int numQueries, DBResultSet[] results, any[] queryData) +{ + data.Reset(); + int client = data.ReadCell(); + int steamid = data.ReadCell(); + bool doChatMessage = data.ReadCell(); + delete data; + + if (!IsValidClient(client) || steamid != GetSteamAccountID(client)) + { + return; + } + + int course; + bool isStart, vbSetup = false; + float position[3], angles[3]; + + if (results[0].RowCount > 0 && results[0].FetchRow()) + { + position[0] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionX); + position[1] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionY); + position[2] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionZ); + course = results[0].FetchInt(TimerSetupDB_GetVBPos_Course); + isStart = results[0].FetchInt(TimerSetupDB_GetVBPos_IsStart) == 1; + + GOKZ_SetVirtualButtonPosition(client, position, course, isStart); + vbSetup = true; + } + + if (results[0].RowCount > 1 && results[0].FetchRow()) + { + position[0] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionX); + position[1] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionY); + position[2] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionZ); + course = results[0].FetchInt(TimerSetupDB_GetVBPos_Course); + isStart = results[0].FetchInt(TimerSetupDB_GetVBPos_IsStart) == 1; + + GOKZ_SetVirtualButtonPosition(client, position, course, isStart); + vbSetup = true; + } + + if (results[1].RowCount > 0 && results[1].FetchRow()) + { + position[0] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionX); + position[1] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionY); + position[2] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionZ); + angles[0] = results[1].FetchFloat(TimerSetupDB_GetStartPos_Angle0); + angles[1] = results[1].FetchFloat(TimerSetupDB_GetStartPos_Angle1); + angles[2] = 0.0; + + GOKZ_SetStartPosition(client, StartPositionType_Custom, position, angles); + } + + if (vbSetup) + { + GOKZ_LockVirtualButtons(client); + } + + if (doChatMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Timer Setup Loaded"); + } +} diff --git a/sourcemod/scripting/gokz-localdb/options.sp b/sourcemod/scripting/gokz-localdb/options.sp new file mode 100644 index 0000000..2a8240a --- /dev/null +++ b/sourcemod/scripting/gokz-localdb/options.sp @@ -0,0 +1,90 @@ + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void RegisterOptions() +{ + for (DBOption option; option < DBOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_DBOptionNames[option], gC_DBOptionDescriptions[option], + OptionType_Int, gI_DBOptionDefaultValues[option], 0, gI_DBOptionCounts[option] - 1); + } +} + + + +// =====[ OPTIONS MENU ]===== + +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemsDB[DBOPTION_COUNT]; + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + + for (int option = 0; option < view_as<int>(DBOPTION_COUNT); option++) + { + gTMO_ItemsDB[option] = gTM_Options.AddItem(gC_DBOptionNames[option], TopMenuHandler_DB, gTMO_CatGeneral); + } +} + +public void TopMenuHandler_DB(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + DBOption option = DBOPTION_COUNT; + for (int i = 0; i < view_as<int>(DBOPTION_COUNT); i++) + { + if (topobj_id == gTMO_ItemsDB[i]) + { + option = view_as<DBOption>(i); + break; + } + } + + if (option == DBOPTION_COUNT) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case DBOption_AutoLoadTimerSetup: + { + FormatToggleableOptionDisplay(param, DBOption_AutoLoadTimerSetup, buffer, maxlength); + } + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, gC_DBOptionNames[option]); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +} + +void FormatToggleableOptionDisplay(int client, DBOption option, char[] buffer, int maxlength) +{ + if (GOKZ_GetOption(client, gC_DBOptionNames[option]) == DBOption_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + gC_DBOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gC_DBOptionPhrases[option], client, + "Options Menu - Enabled", client); + } +} diff --git a/sourcemod/scripting/gokz-localranks.sp b/sourcemod/scripting/gokz-localranks.sp new file mode 100644 index 0000000..e2fd06d --- /dev/null +++ b/sourcemod/scripting/gokz-localranks.sp @@ -0,0 +1,263 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdktools> + +#include <gokz/core> +#include <gokz/localdb> +#include <gokz/localranks> + +#include <sourcemod-colors> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/global> +#include <gokz/jumpstats> +#include <gokz/replays> +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Local Ranks", + author = "DanZay", + description = "Extends and provides in-game functionality for local database", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-localranks.txt" + +bool gB_GOKZGlobal; +Database gH_DB = null; +DatabaseType g_DBType = DatabaseType_None; +bool gB_RecordExistsCache[GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT]; +float gF_RecordTimesCache[GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT]; +bool gB_RecordMissed[MAXPLAYERS + 1][TIMETYPE_COUNT]; +bool gB_PBExistsCache[MAXPLAYERS + 1][GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT]; +float gF_PBTimesCache[MAXPLAYERS + 1][GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT]; +bool gB_PBMissed[MAXPLAYERS + 1][TIMETYPE_COUNT]; +char gC_BeatRecordSound[256]; + + +#include "gokz-localranks/api.sp" +#include "gokz-localranks/commands.sp" +#include "gokz-localranks/misc.sp" + +#include "gokz-localranks/db/sql.sp" +#include "gokz-localranks/db/helpers.sp" +#include "gokz-localranks/db/cache_pbs.sp" +#include "gokz-localranks/db/cache_records.sp" +#include "gokz-localranks/db/create_tables.sp" +#include "gokz-localranks/db/get_completion.sp" +#include "gokz-localranks/db/js_top.sp" +#include "gokz-localranks/db/map_top.sp" +#include "gokz-localranks/db/player_top.sp" +#include "gokz-localranks/db/print_average.sp" +#include "gokz-localranks/db/print_js.sp" +#include "gokz-localranks/db/print_pbs.sp" +#include "gokz-localranks/db/print_records.sp" +#include "gokz-localranks/db/process_new_time.sp" +#include "gokz-localranks/db/recent_records.sp" +#include "gokz-localranks/db/update_ranked_map_pool.sp" +#include "gokz-localranks/db/display_js.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-localranks"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-localranks.phrases"); + + CreateGlobalForwards(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZGlobal = LibraryExists("gokz-global"); + + gH_DB = GOKZ_DB_GetDatabase(); + if (gH_DB != null) + { + g_DBType = GOKZ_DB_GetDatabaseType(); + DB_CreateTables(); + CompletionMVPStarsUpdateAll(); + } + + if (GOKZ_DB_IsMapSetUp()) + { + GOKZ_DB_OnMapSetup(GOKZ_DB_GetCurrentMapID()); + } + + for (int i = 1; i <= MaxClients; i++) + { + if (GOKZ_DB_IsClientSetUp(i)) + { + GOKZ_DB_OnClientSetup(i, GetSteamAccountID(i), GOKZ_DB_IsCheater(i)); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZGlobal = gB_GOKZGlobal || StrEqual(name, "gokz-global"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZGlobal = gB_GOKZGlobal && !StrEqual(name, "gokz-global"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + UpdateRecordMissed(client); + UpdatePBMissed(client); + return Plugin_Continue; +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + ResetRecordMissed(client); + ResetPBMissed(client); +} + +public void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater) +{ + if (GOKZ_DB_IsMapSetUp()) + { + DB_CachePBs(client, steamID); + CompletionMVPStarsUpdate(client); + } +} + +public void GOKZ_DB_OnTimeInserted(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed) +{ + if (GOKZ_DB_IsCheater(client)) + { + DB_CachePBs(client, GetSteamAccountID(client)); + } + else + { + DB_ProcessNewTime(client, steamID, mapID, course, mode, style, runTimeMS, teleportsUsed); + } +} + +public void GOKZ_LR_OnTimeProcessed( + int client, + int steamID, + int mapID, + int course, + int mode, + int style, + float runTime, + int teleportsUsed, + bool firstTime, + float pbDiff, + int rank, + int maxRank, + bool firstTimePro, + float pbDiffPro, + int rankPro, + int maxRankPro) +{ + if (mapID != GOKZ_DB_GetCurrentMapID()) + { + return; + } + + AnnounceNewTime(client, course, mode, runTime, teleportsUsed, firstTime, pbDiff, rank, maxRank, firstTimePro, pbDiffPro, rankPro, maxRankPro); + + if (mode == GOKZ_GetDefaultMode() && firstTimePro) + { + CompletionMVPStarsUpdate(client); + } + + // If new PB, update PB cache + if (firstTime || firstTimePro || pbDiff < 0.0 || pbDiffPro < 0.0) + { + DB_CachePBs(client, GetSteamAccountID(client)); + } +} + +public void GOKZ_LR_OnNewRecord(int client, int steamID, int mapID, int course, int mode, int style, int recordType) +{ + if (mapID != GOKZ_DB_GetCurrentMapID()) + { + return; + } + + AnnounceNewRecord(client, course, mode, recordType); + DB_CacheRecords(mapID); +} + +public void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType) +{ + DoPBMissedReport(client, pbTime, recordType); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + PrecacheAnnouncementSounds(); +} + +public void GOKZ_DB_OnDatabaseConnect(DatabaseType DBType) +{ + gH_DB = GOKZ_DB_GetDatabase(); + g_DBType = DBType; + DB_CreateTables(); + CompletionMVPStarsUpdateAll(); +} + +public void GOKZ_DB_OnMapSetup(int mapID) +{ + DB_CacheRecords(mapID); + + for (int client = 1; client <= MaxClients; client++) + { + if (GOKZ_DB_IsClientSetUp(client)) + { + DB_CachePBs(client, GetSteamAccountID(client)); + } + } +} + +public Action GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed) +{ + if (GOKZ_DB_IsCheater(client)) + { + return Plugin_Continue; + } + + // Block timer end messages from GOKZ Core - this plugin handles them + return Plugin_Stop; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/api.sp b/sourcemod/scripting/gokz-localranks/api.sp new file mode 100644 index 0000000..34b3ece --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/api.sp @@ -0,0 +1,120 @@ +static GlobalForward H_OnTimeProcessed; +static GlobalForward H_OnNewRecord; +static GlobalForward H_OnRecordMissed; +static GlobalForward H_OnPBMissed; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnTimeProcessed = new GlobalForward("GOKZ_LR_OnTimeProcessed", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell); + H_OnNewRecord = new GlobalForward("GOKZ_LR_OnNewRecord", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Float, Param_Cell); + H_OnRecordMissed = new GlobalForward("GOKZ_LR_OnRecordMissed", ET_Ignore, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Cell); + H_OnPBMissed = new GlobalForward("GOKZ_LR_OnPBMissed", ET_Ignore, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Cell); +} + +void Call_OnTimeProcessed( + int client, + int steamID, + int mapID, + int course, + int mode, + int style, + float runTime, + int teleports, + bool firstTime, + float pbDiff, + int rank, + int maxRank, + bool firstTimePro, + float pbDiffPro, + int rankPro, + int maxRankPro) +{ + Call_StartForward(H_OnTimeProcessed); + Call_PushCell(client); + Call_PushCell(steamID); + Call_PushCell(mapID); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(style); + Call_PushFloat(runTime); + Call_PushCell(teleports); + Call_PushCell(firstTime); + Call_PushFloat(pbDiff); + Call_PushCell(rank); + Call_PushCell(maxRank); + Call_PushCell(firstTimePro); + Call_PushFloat(pbDiffPro); + Call_PushCell(rankPro); + Call_PushCell(maxRankPro); + Call_Finish(); +} + +void Call_OnNewRecord(int client, int steamID, int mapID, int course, int mode, int style, int recordType, float pbDiff, int teleportsUsed) +{ + Call_StartForward(H_OnNewRecord); + Call_PushCell(client); + Call_PushCell(steamID); + Call_PushCell(mapID); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(style); + Call_PushCell(recordType); + Call_PushFloat(pbDiff); + Call_PushCell(teleportsUsed); + Call_Finish(); +} + +void Call_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType) +{ + Call_StartForward(H_OnRecordMissed); + Call_PushCell(client); + Call_PushFloat(recordTime); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(style); + Call_PushCell(recordType); + Call_Finish(); +} + +void Call_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType) +{ + Call_StartForward(H_OnPBMissed); + Call_PushCell(client); + Call_PushFloat(pbTime); + Call_PushCell(course); + Call_PushCell(mode); + Call_PushCell(style); + Call_PushCell(recordType); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_LR_GetRecordMissed", Native_GetRecordMissed); + CreateNative("GOKZ_LR_GetPBMissed", Native_GetPBMissed); + CreateNative("GOKZ_LR_ReopenMapTopMenu", Native_ReopenMapTopMenu); +} + +public int Native_GetRecordMissed(Handle plugin, int numParams) +{ + return view_as<int>(gB_RecordMissed[GetNativeCell(1)][GetNativeCell(2)]); +} + +public int Native_GetPBMissed(Handle plugin, int numParams) +{ + return view_as<int>(gB_PBMissed[GetNativeCell(1)][GetNativeCell(2)]); +} + +public int Native_ReopenMapTopMenu(Handle plugin, int numParams) +{ + ReopenMapTopMenu(GetNativeCell(1)); + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/commands.sp b/sourcemod/scripting/gokz-localranks/commands.sp new file mode 100644 index 0000000..44063af --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/commands.sp @@ -0,0 +1,506 @@ +static float lastCommandTime[MAXPLAYERS + 1]; + + + +void RegisterCommands() +{ + RegConsoleCmd("sm_top", CommandTop, "[KZ] Open a menu showing the top record holders."); + RegConsoleCmd("sm_maptop", CommandMapTop, "[KZ] Open a menu showing the top main course times of a map. Usage: !maptop <map>"); + RegConsoleCmd("sm_bmaptop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !bmaptop <#bonus> <map>"); + RegConsoleCmd("sm_bonustop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !bonustop <#bonus> <map>"); + RegConsoleCmd("sm_btop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !btop <#bonus> <map>"); + RegConsoleCmd("sm_pb", CommandPB, "[KZ] Show PB main course times and ranks in chat. Usage: !pb <map> <player>"); + RegConsoleCmd("sm_bpb", CommandBPB, "[KZ] Show PB bonus times and ranks in chat. Usage: !bpb <#bonus> <map> <player>"); + RegConsoleCmd("sm_wr", CommandWR, "[KZ] Show main course record times in chat. Usage: !wr <map>"); + RegConsoleCmd("sm_bwr", CommandBWR, "[KZ] Show bonus record times in chat. Usage: !bwr <#bonus> <map>"); + RegConsoleCmd("sm_avg", CommandAVG, "[KZ] Show the average main course run time in chat. Usage !avg <map>"); + RegConsoleCmd("sm_bavg", CommandBAVG, "[KZ] Show the average bonus run time in chat. Usage !bavg <#bonus> <map>"); + RegConsoleCmd("sm_pc", CommandPC, "[KZ] Show course completion in chat. Usage: !pc <player>"); + RegConsoleCmd("sm_rr", CommandRecentRecords, "[KZ] Open a menu showing recently broken records."); + RegConsoleCmd("sm_latest", CommandRecentRecords, "[KZ] Open a menu showing recently broken records."); + + RegConsoleCmd("sm_ljpb", CommandLJPB, "[KZ] Show PB Long Jump in chat. Usage: !ljpb <jumper>"); + RegConsoleCmd("sm_bhpb", CommandBHPB, "[KZ] Show PB Bunnyhop in chat. Usage: !bhpb <jumper>"); + RegConsoleCmd("sm_lbhpb", CommandLBHPB, "[KZ] Show PB Lowpre Bunnyhop in chat. Usage: !lbhpb <jumper>"); + RegConsoleCmd("sm_mbhpb", CommandMBHPB, "[KZ] Show PB Multi Bunnyhop in chat. Usage: !mbhpb <jumper>"); + RegConsoleCmd("sm_wjpb", CommandWJPB, "[KZ] Show PB Weird Jump in chat. Usage: !wjpb <jumper>"); + RegConsoleCmd("sm_lwjpb", CommandLWJPB, "[KZ] Show PB Lowpre Weird Jump in chat. Usage: !lwjpb <jumper>"); + RegConsoleCmd("sm_lajpb", CommandLAJPB, "[KZ] Show PB Ladder Jump in chat. Usage: !lajpb <jumper>"); + RegConsoleCmd("sm_lahpb", CommandLAHPB, "[KZ] Show PB Ladderhop in chat. Usage: !lahpb <jumper>"); + RegConsoleCmd("sm_jbpb", CommandJBPB, "[KZ] Show PB Jumpbug in chat. Usage: !jbpb <jumper>"); + RegConsoleCmd("sm_js", CommandJS, "[KZ] Open a menu showing jumpstat PBs. Usage: !js <jumper>"); + RegConsoleCmd("sm_jumpstats", CommandJS, "[KZ] Open a menu showing jumpstat PBs. Usage: !jumpstats <jumper>"); + RegConsoleCmd("sm_jstop", CommandJSTop, "[KZ] Open a menu showing the top jumpstats."); + RegConsoleCmd("sm_jumptop", CommandJSTop, "[KZ] Open a menu showing the top jumpstats."); + + RegAdminCmd("sm_updatemappool", CommandUpdateMapPool, ADMFLAG_ROOT, "[KZ] Update the ranked map pool with the list of maps in cfg/sourcemod/gokz/gokz-localranks-mappool.cfg."); +} + +public Action CommandTop(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + DisplayPlayerTopModeMenu(client); + return Plugin_Handled; +} + +public Action CommandMapTop(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Open map top for current map + DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), 0); + } + else if (args >= 1) + { // Open map top for specified map + char specifiedMap[33]; + GetCmdArg(1, specifiedMap, sizeof(specifiedMap)); + DB_OpenMapTopModeMenu_FindMap(client, specifiedMap, 0); + } + return Plugin_Handled; +} + +public Action CommandBMapTop(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Open Bonus 1 top for current map + DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), 1); + } + else if (args == 1) + { // Open specified Bonus # top for current map + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), bonus); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 2) + { // Open specified bonus top for specified map + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_OpenMapTopModeMenu_FindMap(client, argMap, bonus); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandPB(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print their PBs for current map and their current mode + DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GOKZ_GL_PrintRecords(client, "", 0, GOKZ_GetCoreOption(client, Option_Mode), steamid); + } + } + else if (args == 1) + { // Print their PBs for specified map and their current mode + char argMap[33]; + GetCmdArg(1, argMap, sizeof(argMap)); + DB_PrintPBs_FindMap(client, GetSteamAccountID(client), argMap, 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + else if (args >= 2) + { // Print specified player's PBs for specified map and their current mode + char argMap[33], argPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, argMap, sizeof(argMap)); + GetCmdArg(2, argPlayer, sizeof(argPlayer)); + DB_PrintPBs_FindPlayerAndMap(client, argPlayer, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + return Plugin_Handled; +} + +public Action CommandBPB(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print their Bonus 1 PBs for current map and their current mode + DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GOKZ_GL_PrintRecords(client, "", 1, GOKZ_GetCoreOption(client, Option_Mode), steamid); + } + } + else if (args == 1) + { // Print their specified Bonus # PBs for current map and their current mode + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + GOKZ_GL_PrintRecords(client, "", bonus, GOKZ_GetCoreOption(client, Option_Mode), steamid); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args == 2) + { // Print their specified Bonus # PBs for specified map and their current mode + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintPBs_FindMap(client, GetSteamAccountID(client), argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 3) + { // Print specified player's specified Bonus # PBs for specified map and their current mode + char argBonus[4], argMap[33], argPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + GetCmdArg(3, argPlayer, sizeof(argPlayer)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintPBs_FindPlayerAndMap(client, argPlayer, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandWR(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print record times for current map and their current mode + DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + GOKZ_GL_PrintRecords(client, "", 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + } + else if (args >= 1) + { // Print record times for specified map and their current mode + char argMap[33]; + GetCmdArg(1, argMap, sizeof(argMap)); + DB_PrintRecords_FindMap(client, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + return Plugin_Handled; +} + +public Action CommandBWR(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print Bonus 1 record times for current map and their current mode + DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + GOKZ_GL_PrintRecords(client, "", 1, GOKZ_GetCoreOption(client, Option_Mode)); + } + } + else if (args == 1) + { // Print specified Bonus # record times for current map and their current mode + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode)); + if (gB_GOKZGlobal) + { + GOKZ_GL_PrintRecords(client, "", bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 2) + { // Print specified Bonus # record times for specified map and their current mode + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintRecords_FindMap(client, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandAVG(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print average times for current map and their current mode + DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + else if (args >= 1) + { // Print average times for specified map and their current mode + char argMap[33]; + GetCmdArg(1, argMap, sizeof(argMap)); + DB_PrintAverage_FindMap(client, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode)); + } + return Plugin_Handled; +} + +public Action CommandBAVG(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args == 0) + { // Print Bonus 1 average times for current map and their current mode + DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode)); + } + else if (args == 1) + { // Print specified Bonus # average times for current map and their current mode + char argBonus[4]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + else if (args >= 2) + { // Print specified Bonus # average times for specified map and their current mode + char argBonus[4], argMap[33]; + GetCmdArg(1, argBonus, sizeof(argBonus)); + GetCmdArg(2, argMap, sizeof(argMap)); + int bonus = StringToInt(argBonus); + if (GOKZ_IsValidCourse(bonus, true)) + { + DB_PrintAverage_FindMap(client, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode)); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus); + } + } + return Plugin_Handled; +} + +public Action CommandPC(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args < 1) + { + DB_GetCompletion(client, GetSteamAccountID(client), GOKZ_GetCoreOption(client, Option_Mode), true); + } + else if (args >= 1) + { // Print record times for specified map and their current mode + char argPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, argPlayer, sizeof(argPlayer)); + DB_GetCompletion_FindPlayer(client, argPlayer, GOKZ_GetCoreOption(client, Option_Mode)); + } + return Plugin_Handled; +} + +public Action CommandRecentRecords(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + DisplayRecentRecordsModeMenu(client); + return Plugin_Handled; +} + +public Action CommandUpdateMapPool(int client, int args) +{ + DB_UpdateRankedMapPool(client); + return Plugin_Handled; +} + +public Action CommandLJPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_LongJump); + return Plugin_Handled; +} + +public Action CommandBHPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_Bhop); + return Plugin_Handled; +} + +public Action CommandLBHPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_LowpreBhop); + return Plugin_Handled; +} + +public Action CommandMBHPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_MultiBhop); + return Plugin_Handled; +} + +public Action CommandWJPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_WeirdJump); + return Plugin_Handled; +} + +public Action CommandLWJPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_LowpreWeirdJump); + return Plugin_Handled; +} + +public Action CommandLAJPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_LadderJump); + return Plugin_Handled; +} + +public Action CommandLAHPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_Ladderhop); + return Plugin_Handled; +} + +public Action CommandJBPB(int client, int args) +{ + DisplayJumpstatRecordCommand(client, args, JumpType_Jumpbug); + return Plugin_Handled; +} + +public Action CommandJS(int client, int args) +{ + if (IsSpammingCommands(client)) + { + return Plugin_Handled; + } + + if (args < 1) + { + DB_OpenJumpStatsModeMenu(client, GetSteamAccountID(client)); + } + else if (args >= 1) + { + char argPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, argPlayer, sizeof(argPlayer)); + DB_OpenJumpStatsModeMenu_FindPlayer(client, argPlayer); + } + return Plugin_Handled; +} + +public Action CommandJSTop(int client, int args) +{ + DisplayJumpTopModeMenu(client); + return Plugin_Handled; +} + +void DisplayJumpstatRecordCommand(int client, int args, int jumpType) +{ + if (args >= 1) + { + char argJumper[33]; + GetCmdArg(1, argJumper, sizeof(argJumper)); + DisplayJumpstatRecord(client, jumpType, argJumper); + } + else + { + DisplayJumpstatRecord(client, jumpType); + } +} + + + +// =====[ PRIVATE ]===== + +bool IsSpammingCommands(int client, bool printMessage = true) +{ + float currentTime = GetEngineTime(); + float timeSinceLastCommand = currentTime - lastCommandTime[client]; + if (timeSinceLastCommand < LR_COMMAND_COOLDOWN) + { + if (printMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Please Wait Before Using Command", LR_COMMAND_COOLDOWN - timeSinceLastCommand + 0.1); + } + return true; + } + + // Not spamming commands - all good! + lastCommandTime[client] = currentTime; + return false; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp b/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp new file mode 100644 index 0000000..12c3ed2 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp @@ -0,0 +1,62 @@ +/* + Caches the player's personal best times on the map. +*/ + + + +void DB_CachePBs(int client, int steamID) +{ + char query[1024]; + + Transaction txn = SQL_CreateTransaction(); + + // Reset PB exists array + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + for (int mode = 0; mode < MODE_COUNT; mode++) + { + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + gB_PBExistsCache[client][course][mode][timeType] = false; + } + } + } + + int mapID = GOKZ_DB_GetCurrentMapID(); + + // Get Map PBs + FormatEx(query, sizeof(query), sql_getpbs, steamID, mapID); + txn.AddQuery(query); + // Get PRO PBs + FormatEx(query, sizeof(query), sql_getpbspro, steamID, mapID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CachePBs, DB_TxnFailure_Generic, GetClientUserId(client), DBPrio_High); +} + +public void DB_TxnSuccess_CachePBs(Handle db, int userID, int numQueries, Handle[] results, any[] queryData) +{ + int client = GetClientOfUserId(userID); + if (client < 1 || client > MaxClients || !IsClientAuthorized(client) || IsFakeClient(client)) + { + return; + } + + int course, mode; + + while (SQL_FetchRow(results[0])) + { + course = SQL_FetchInt(results[0], 1); + mode = SQL_FetchInt(results[0], 2); + gB_PBExistsCache[client][course][mode][TimeType_Nub] = true; + gF_PBTimesCache[client][course][mode][TimeType_Nub] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 0)); + } + + while (SQL_FetchRow(results[1])) + { + course = SQL_FetchInt(results[1], 1); + mode = SQL_FetchInt(results[1], 2); + gB_PBExistsCache[client][course][mode][TimeType_Pro] = true; + gF_PBTimesCache[client][course][mode][TimeType_Pro] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[1], 0)); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/cache_records.sp b/sourcemod/scripting/gokz-localranks/db/cache_records.sp new file mode 100644 index 0000000..611b13c --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/cache_records.sp @@ -0,0 +1,54 @@ +/* + Caches the record times on the map. +*/ + + + +void DB_CacheRecords(int mapID) +{ + char query[1024]; + + Transaction txn = SQL_CreateTransaction(); + + // Reset record exists array + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + for (int mode = 0; mode < MODE_COUNT; mode++) + { + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + gB_RecordExistsCache[course][mode][timeType] = false; + } + } + } + + // Get Map WRs + FormatEx(query, sizeof(query), sql_getwrs, mapID); + txn.AddQuery(query); + // Get PRO WRs + FormatEx(query, sizeof(query), sql_getwrspro, mapID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CacheRecords, DB_TxnFailure_Generic, _, DBPrio_High); +} + +public void DB_TxnSuccess_CacheRecords(Handle db, any data, int numQueries, Handle[] results, any[] queryData) +{ + int course, mode; + + while (SQL_FetchRow(results[0])) + { + course = SQL_FetchInt(results[0], 1); + mode = SQL_FetchInt(results[0], 2); + gB_RecordExistsCache[course][mode][TimeType_Nub] = true; + gF_RecordTimesCache[course][mode][TimeType_Nub] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 0)); + } + + while (SQL_FetchRow(results[1])) + { + course = SQL_FetchInt(results[1], 1); + mode = SQL_FetchInt(results[1], 2); + gB_RecordExistsCache[course][mode][TimeType_Pro] = true; + gF_RecordTimesCache[course][mode][TimeType_Pro] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[1], 0)); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/create_tables.sp b/sourcemod/scripting/gokz-localranks/db/create_tables.sp new file mode 100644 index 0000000..a15f67c --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/create_tables.sp @@ -0,0 +1,27 @@ +/* + Table creation and alteration. +*/ + + + +void DB_CreateTables() +{ + Transaction txn = SQL_CreateTransaction(); + + // Create/alter database tables + switch (g_DBType) + { + case DatabaseType_SQLite: + { + txn.AddQuery(sqlite_maps_alter1); + } + case DatabaseType_MySQL: + { + txn.AddQuery(mysql_maps_alter1); + } + } + + // No error logs for this transaction as it will always throw an error + // if the column already exists, which is more annoying than helpful. + SQL_ExecuteTransaction(gH_DB, txn, _, _, _, DBPrio_High); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/display_js.sp b/sourcemod/scripting/gokz-localranks/db/display_js.sp new file mode 100644 index 0000000..779148f --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/display_js.sp @@ -0,0 +1,325 @@ +/* + Displays player's best jumpstats in a menu. +*/ + +static int jumpStatsTargetSteamID[MAXPLAYERS + 1]; +static char jumpStatsTargetAlias[MAXPLAYERS + 1][MAX_NAME_LENGTH]; +static int jumpStatsMode[MAXPLAYERS + 1]; + + + +// =====[ JUMPSTATS MODE ]===== + +void DB_OpenJumpStatsModeMenu(int client, int targetSteamID) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(targetSteamID); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve name of target + FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenJumpStatsModeMenu, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenJumpStatsModeMenu(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int targetSteamID = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + // Get name of target + if (!SQL_FetchRow(results[0])) + { + return; + } + SQL_FetchString(results[0], 0, jumpStatsTargetAlias[client], sizeof(jumpStatsTargetAlias[])); + + jumpStatsTargetSteamID[client] = targetSteamID; + DisplayJumpStatsModeMenu(client); +} + +void DB_OpenJumpStatsModeMenu_FindPlayer(int client, const char[] target) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(target); + + DB_FindPlayer(target, DB_TxnSuccess_OpenJumpStatsModeMenu_FindPlayer, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenJumpStatsModeMenu_FindPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char playerSearch[33]; + data.ReadString(playerSearch, sizeof(playerSearch)); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + else if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { + DB_OpenJumpStatsModeMenu(client, SQL_FetchInt(results[0], 0)); + } +} + + + +// =====[ MENUS ]===== + +static void DisplayJumpStatsModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_JumpStatsMode); + menu.SetTitle("%T", "Jump Stats Mode Menu - Title", client, jumpStatsTargetAlias[client]); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void DisplayJumpStatsBlockTypeMenu(int client, int mode) +{ + jumpStatsMode[client] = mode; + + Menu menu = new Menu(MenuHandler_JumpStatsBlockType); + menu.SetTitle("%T", "Jump Stats Block Type Menu - Title", client, jumpStatsTargetAlias[client], gC_ModeNames[jumpStatsMode[client]]); + JumpStatsBlockTypeMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void JumpStatsBlockTypeMenuAddItems(int client, Menu menu) +{ + char str[64]; + FormatEx(str, sizeof(str), "%T", "Jump Records", client); + menu.AddItem("jump", str); + FormatEx(str, sizeof(str), "%T %T", "Block", client, "Jump Records", client); + menu.AddItem("blockjump", str); +} + + + +// =====[ JUMPSTATS ]===== + +void DB_OpenJumpStats(int client, int targetSteamID, int mode, int blockType) +{ + char query[1024]; + Transaction txn = SQL_CreateTransaction(); + + // Get alias + FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID); + txn.AddQuery(query); + + // Get jumpstat pbs + if (blockType == 0) + { + FormatEx(query, sizeof(query), sql_jumpstats_getpbs, targetSteamID, mode); + } + else + { + FormatEx(query, sizeof(query), sql_jumpstats_getblockpbs, targetSteamID, mode); + } + txn.AddQuery(query); + + DataPack datapack = new DataPack(); + datapack.WriteCell(GetClientUserId(client)); + datapack.WriteCell(targetSteamID); + datapack.WriteCell(mode); + datapack.WriteCell(blockType); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenJumpStats, DB_TxnFailure_Generic_DataPack, datapack, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenJumpStats(Handle db, DataPack datapack, int numQueries, Handle[] results, any[] queryData) +{ + datapack.Reset(); + int client = GetClientOfUserId(datapack.ReadCell()); + int targetSteamID = datapack.ReadCell(); + int mode = datapack.ReadCell(); + int blockType = datapack.ReadCell(); + delete datapack; + + if (!IsValidClient(client)) + { + return; + } + + // Get target name + if (!SQL_FetchRow(results[0])) + { + return; + } + char alias[MAX_NAME_LENGTH]; + SQL_FetchString(results[0], 0, alias, sizeof(alias)); + + if (SQL_GetRowCount(results[1]) == 0) + { + if (blockType == 0) + { + GOKZ_PrintToChat(client, true, "%T", "Jump Stats Menu - No Jump Stats", client, alias); + } + else + { + GOKZ_PrintToChat(client, true, "%T", "Jump Stats Menu - No Block Jump Stats", client, alias); + } + + DisplayJumpStatsBlockTypeMenu(client, mode); + return; + } + + Menu menu = new Menu(MenuHandler_JumpStatsSubmenu); + if (blockType == 0) + { + menu.SetTitle("%T", "Jump Stats Submenu - Title (Jump)", client, alias, gC_ModeNames[mode]); + } + else + { + menu.SetTitle("%T", "Jump Stats Submenu - Title (Block Jump)", client, alias, gC_ModeNames[mode]); + } + + char buffer[128], admin[64]; + bool clientIsAdmin = CheckCommandAccess(client, "sm_deletejump", ADMFLAG_ROOT, false); + + if (blockType == 0) + { + FormatEx(buffer, sizeof(buffer), "%T", "Jump Stats - Jump Console Header", + client, gC_ModeNames[mode], alias, targetSteamID & 1, targetSteamID >> 1); + PrintToConsole(client, "%s", buffer); + int titleLength = strlen(buffer); + strcopy(buffer, sizeof(buffer), "----------------------------------------------------------------"); + buffer[titleLength] = '\0'; + PrintToConsole(client, "%s", buffer); + + while (SQL_FetchRow(results[1])) + { + int jumpid = SQL_FetchInt(results[1], JumpstatDB_PBMenu_JumpID); + int jumpType = SQL_FetchInt(results[1], JumpstatDB_PBMenu_JumpType); + float distance = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Distance) / GOKZ_DB_JS_DISTANCE_PRECISION; + int strafes = SQL_FetchInt(results[1], JumpstatDB_PBMenu_Strafes); + float sync = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Sync) / GOKZ_DB_JS_SYNC_PRECISION; + float pre = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Pre) / GOKZ_DB_JS_PRE_PRECISION; + float max = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Max) / GOKZ_DB_JS_MAX_PRECISION; + float airtime = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Air) / GOKZ_DB_JS_AIRTIME_PRECISION; + + FormatEx(buffer, sizeof(buffer), "%0.4f %s", distance, gC_JumpTypes[jumpType]); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + FormatEx(buffer, sizeof(buffer), "%8s", gC_JumpTypesShort[jumpType]); + buffer[3] = '\0'; + + if (clientIsAdmin) + { + FormatEx(admin, sizeof(admin), "<id: %d>", jumpid); + } + + PrintToConsole(client, "%s %0.4f [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s", + buffer, distance, strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", admin); + } + } + else + { + FormatEx(buffer, sizeof(buffer), "%T", "Jump Stats - Block Jump Console Header", + client, gC_ModeNames[mode], alias, targetSteamID & 1, targetSteamID >> 1); + PrintToConsole(client, "%s", buffer); + int titleLength = strlen(buffer); + strcopy(buffer, sizeof(buffer), "----------------------------------------------------------------"); + buffer[titleLength] = '\0'; + PrintToConsole(client, "%s", buffer); + + while (SQL_FetchRow(results[1])) + { + int jumpid = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_JumpID); + int jumpType = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_JumpType); + int block = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_Block); + float distance = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Distance) / GOKZ_DB_JS_DISTANCE_PRECISION; + int strafes = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_Strafes); + float sync = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Sync) / GOKZ_DB_JS_SYNC_PRECISION; + float pre = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Pre) / GOKZ_DB_JS_PRE_PRECISION; + float max = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Max) / GOKZ_DB_JS_MAX_PRECISION; + float airtime = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Air) / GOKZ_DB_JS_AIRTIME_PRECISION; + + FormatEx(buffer, sizeof(buffer), "%d %T (%0.4f) %s", block, "Block", client, distance, gC_JumpTypes[jumpType]); + menu.AddItem("", buffer, ITEMDRAW_DISABLED); + + FormatEx(buffer, sizeof(buffer), "%8s", gC_JumpTypesShort[jumpType]); + buffer[3] = '\0'; + + if (clientIsAdmin) + { + FormatEx(admin, sizeof(admin), "<id: %d>", jumpid); + } + + PrintToConsole(client, "%s %d %t (%0.4f) [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s", + buffer, block, "Block", distance, strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", admin); + } + } + + PrintToConsole(client, ""); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ MENU HANDLERS ]===== + +public int MenuHandler_JumpStatsMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = mode + DisplayJumpStatsBlockTypeMenu(param1, param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_JumpStatsBlockType(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = blockType + DB_OpenJumpStats(param1, jumpStatsTargetSteamID[param1], jumpStatsMode[param1], param2); + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayJumpStatsModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_JumpStatsSubmenu(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayJumpStatsBlockTypeMenu(param1, jumpStatsMode[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/get_completion.sp b/sourcemod/scripting/gokz-localranks/db/get_completion.sp new file mode 100644 index 0000000..fbc76e7 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/get_completion.sp @@ -0,0 +1,155 @@ +/* + Gets the number and percentage of maps completed. +*/ + + + +void DB_GetCompletion(int client, int targetSteamID, int mode, bool print) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(targetSteamID); + data.WriteCell(mode); + data.WriteCell(print); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve Alias of SteamID + FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID); + txn.AddQuery(query); + // Get total number of ranked main courses + txn.AddQuery(sql_getcount_maincourses); + // Get number of main course completions + FormatEx(query, sizeof(query), sql_getcount_maincoursescompleted, targetSteamID, mode); + txn.AddQuery(query); + // Get number of main course completions (PRO) + FormatEx(query, sizeof(query), sql_getcount_maincoursescompletedpro, targetSteamID, mode); + txn.AddQuery(query); + + // Get total number of ranked bonuses + txn.AddQuery(sql_getcount_bonuses); + // Get number of bonus completions + FormatEx(query, sizeof(query), sql_getcount_bonusescompleted, targetSteamID, mode); + txn.AddQuery(query); + // Get number of bonus completions (PRO) + FormatEx(query, sizeof(query), sql_getcount_bonusescompletedpro, targetSteamID, mode); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_GetCompletion, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_GetCompletion(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int targetSteamID = data.ReadCell(); + int mode = data.ReadCell(); + bool print = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + char playerName[MAX_NAME_LENGTH]; + int totalMainCourses, completions, completionsPro; + int totalBonuses, bonusCompletions, bonusCompletionsPro; + + // Get Player Name from results + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, playerName, sizeof(playerName)); + } + + // Get total number of main courses + if (SQL_FetchRow(results[1])) + { + totalMainCourses = SQL_FetchInt(results[1], 0); + } + // Get completed main courses + if (SQL_FetchRow(results[2])) + { + completions = SQL_FetchInt(results[2], 0); + } + // Get completed main courses (PRO) + if (SQL_FetchRow(results[3])) + { + completionsPro = SQL_FetchInt(results[3], 0); + } + + // Get total number of bonuses + if (SQL_FetchRow(results[4])) + { + totalBonuses = SQL_FetchInt(results[4], 0); + } + // Get completed bonuses + if (SQL_FetchRow(results[5])) { + bonusCompletions = SQL_FetchInt(results[5], 0); + } + // Get completed bonuses (PRO) + if (SQL_FetchRow(results[6])) + { + bonusCompletionsPro = SQL_FetchInt(results[6], 0); + } + + // Print completion message to chat if specified + if (print) + { + if (totalMainCourses + totalBonuses == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Ranked Maps"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Map Completion", + playerName, + completions, totalMainCourses, completionsPro, totalMainCourses, + bonusCompletions, totalBonuses, bonusCompletionsPro, totalBonuses, + gC_ModeNamesShort[mode]); + } + } + + // Set scoreboard MVP stars to percentage PRO completion of server's default mode + if (totalMainCourses + totalBonuses != 0 && targetSteamID == GetSteamAccountID(client) && mode == GOKZ_GetDefaultMode()) + { + CS_SetMVPCount(client, RoundToFloor(float(completionsPro + bonusCompletionsPro) / float(totalMainCourses + totalBonuses) * 100.0)); + } +} + +void DB_GetCompletion_FindPlayer(int client, const char[] target, int mode) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(target); + data.WriteCell(mode); + + DB_FindPlayer(target, DB_TxnSuccess_GetCompletion_FindPlayer, data, DBPrio_Low); +} + +public void DB_TxnSuccess_GetCompletion_FindPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char playerSearch[33]; + data.ReadString(playerSearch, sizeof(playerSearch)); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + else if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { + DB_GetCompletion(client, SQL_FetchInt(results[0], 0), mode, true); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/helpers.sp b/sourcemod/scripting/gokz-localranks/db/helpers.sp new file mode 100644 index 0000000..670a420 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/helpers.sp @@ -0,0 +1,91 @@ +/* + Database helper functions and callbacks. +*/ + + + +/* Error report callback for failed transactions */ +public void DB_TxnFailure_Generic(Handle db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) +{ + LogError("Database transaction error: %s", error); +} + +/* Error report callback for failed transactions which deletes the DataPack */ +public void DB_TxnFailure_Generic_DataPack(Handle db, DataPack data, int numQueries, const char[] error, int failIndex, any[] queryData) +{ + delete data; + LogError("Database transaction error: %s", error); +} + +/* Used to search the database for a player name and return their PlayerID and alias + + For SQLTxnSuccess onSuccess: + results[0] - 0:PlayerID, 1:Alias +*/ +void DB_FindPlayer(const char[] playerSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal) +{ + char query[1024], playerEscaped[MAX_NAME_LENGTH * 2 + 1]; + SQL_EscapeString(gH_DB, playerSearch, playerEscaped, sizeof(playerEscaped)); + + String_ToLower(playerEscaped, playerEscaped, sizeof(playerEscaped)); + + Transaction txn = SQL_CreateTransaction(); + + // Look for player name and retrieve their PlayerID + FormatEx(query, sizeof(query), sql_players_searchbyalias, playerEscaped, playerEscaped); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority); +} + +/* Used to search the database for a map name and return its MapID and name + + For SQLTxnSuccess onSuccess: + results[0] - 0:MapID, 1:Name +*/ +void DB_FindMap(const char[] mapSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal) +{ + char query[1024], mapEscaped[129]; + SQL_EscapeString(gH_DB, mapSearch, mapEscaped, sizeof(mapEscaped)); + + Transaction txn = SQL_CreateTransaction(); + + // Look for map name and retrieve it's MapID + FormatEx(query, sizeof(query), sql_maps_searchbyname, mapEscaped, mapEscaped); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority); +} + +/* Used to search the database for a player name and return their PlayerID and alias, + and search the database for a map name and return its MapID and name + + For SQLTxnSuccess onSuccess: + results[0] - 0:PlayerID, 1:Alias + results[1] - 0:MapID, 1:Name +*/ +void DB_FindPlayerAndMap(const char[] playerSearch, const char[] mapSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal) +{ + char query[1024], mapEscaped[129], playerEscaped[MAX_NAME_LENGTH * 2 + 1]; + SQL_EscapeString(gH_DB, playerSearch, playerEscaped, sizeof(playerEscaped)); + SQL_EscapeString(gH_DB, mapSearch, mapEscaped, sizeof(mapEscaped)); + + String_ToLower(playerEscaped, playerEscaped, sizeof(playerEscaped)); + + Transaction txn = SQL_CreateTransaction(); + + // Look for player name and retrieve their PlayerID + FormatEx(query, sizeof(query), sql_players_searchbyalias, playerEscaped, playerEscaped); + txn.AddQuery(query); + // Look for map name and retrieve it's MapID + FormatEx(query, sizeof(query), sql_maps_searchbyname, mapEscaped, mapEscaped); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority); +} + +// Used to convert the Account ID to the SteamID we can use for a Global API query +int GetSteam2FromAccountId(char[] result, int maxlen, int account_id) +{ + return Format(result, maxlen, "STEAM_1:%d:%d", view_as<bool>(account_id % 2), account_id / 2); +} diff --git a/sourcemod/scripting/gokz-localranks/db/js_top.sp b/sourcemod/scripting/gokz-localranks/db/js_top.sp new file mode 100644 index 0000000..a336a90 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/js_top.sp @@ -0,0 +1,286 @@ + +static int jumpTopMode[MAXPLAYERS + 1]; +static int jumpTopType[MAXPLAYERS + 1]; +static int blockNums[MAXPLAYERS + 1][JS_TOP_RECORD_COUNT]; +static int jumpInfo[MAXPLAYERS + 1][JS_TOP_RECORD_COUNT][3]; + + + +void DB_OpenJumpTop(int client, int mode, int jumpType, int blockType) +{ + char query[1024]; + + Transaction txn = SQL_CreateTransaction(); + + FormatEx(query, sizeof(query), sql_jumpstats_gettop, jumpType, mode, blockType, jumpType, mode, blockType, JS_TOP_RECORD_COUNT); + txn.AddQuery(query); + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(mode); + data.WriteCell(jumpType); + data.WriteCell(blockType); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_GetJumpTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +void DB_TxnSuccess_GetJumpTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int mode = data.ReadCell(); + int type = data.ReadCell(); + int blockType = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + jumpTopMode[client] = mode; + jumpTopType[client] = type; + + int rows = SQL_GetRowCount(results[0]); + if (rows == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Jumpstats Found"); + DisplayJumpTopBlockTypeMenu(client, mode, type); + return; + } + + char display[128], alias[33], title[65], admin[65]; + int jumpid, steamid, block, strafes; + float distance, sync, pre, max, airtime; + + bool clientIsAdmin = CheckCommandAccess(client, "sm_deletejump", ADMFLAG_ROOT, false); + + Menu menu = new Menu(MenuHandler_JumpTopList); + menu.Pagination = 5; + + if (blockType == 0) + { + menu.SetTitle("%T", "Jump Top Submenu - Title (Jump)", client, gC_ModeNames[mode], gC_JumpTypes[type]); + + FormatEx(title, sizeof(title), "%s %s %T", gC_ModeNames[mode], gC_JumpTypes[type], "Top", client); + strcopy(display, sizeof(display), "----------------------------------------------------------------"); + display[strlen(title)] = '\0'; + + PrintToConsole(client, title); + PrintToConsole(client, display); + + for (int i = 0; i < rows; i++) + { + SQL_FetchRow(results[0]); + jumpid = SQL_FetchInt(results[0], JumpstatDB_Top20_JumpID); + steamid = SQL_FetchInt(results[0], JumpstatDB_Top20_SteamID); + SQL_FetchString(results[0], JumpstatDB_Top20_Alias, alias, sizeof(alias)); + distance = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Distance)) / GOKZ_DB_JS_DISTANCE_PRECISION; + strafes = SQL_FetchInt(results[0], JumpstatDB_Top20_Strafes); + sync = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Sync)) / GOKZ_DB_JS_SYNC_PRECISION; + pre = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Pre)) / GOKZ_DB_JS_PRE_PRECISION; + max = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Max)) / GOKZ_DB_JS_MAX_PRECISION; + airtime = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Air)) / GOKZ_DB_JS_AIRTIME_PRECISION; + + FormatEx(display, sizeof(display), "#%-2d %.4f %s", i + 1, distance, alias); + + menu.AddItem(IntToStringEx(i), display); + + if (clientIsAdmin) + { + FormatEx(admin, sizeof(admin), "<id: %d>", jumpid); + } + + PrintToConsole(client, "#%-2d %.4f %s <STEAM_1:%d:%d> [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s", + i + 1, distance, alias, steamid & 1, steamid >> 1, + strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", + admin); + + jumpInfo[client][i][0] = steamid; + jumpInfo[client][i][1] = type; + jumpInfo[client][i][2] = mode; + blockNums[client][i] = 0; + } + } + else + { + menu.SetTitle("%T", "Jump Top Submenu - Title (Block Jump)", client, gC_ModeNames[mode], gC_JumpTypes[type]); + + FormatEx(title, sizeof(title), "%s %T %s %T", gC_ModeNames[mode], "Block", client, gC_JumpTypes[type], "Top", client); + strcopy(display, sizeof(display), "----------------------------------------------------------------"); + display[strlen(title)] = '\0'; + + PrintToConsole(client, title); + PrintToConsole(client, display); + + for (int i = 0; i < rows; i++) + { + SQL_FetchRow(results[0]); + jumpid = SQL_FetchInt(results[0], JumpstatDB_Top20_JumpID); + steamid = SQL_FetchInt(results[0], JumpstatDB_Top20_SteamID); + SQL_FetchString(results[0], JumpstatDB_Top20_Alias, alias, sizeof(alias)); + block = SQL_FetchInt(results[0], JumpstatDB_Top20_Block); + distance = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Distance)) / GOKZ_DB_JS_DISTANCE_PRECISION; + strafes = SQL_FetchInt(results[0], JumpstatDB_Top20_Strafes); + sync = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Sync)) / GOKZ_DB_JS_SYNC_PRECISION; + pre = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Pre)) / GOKZ_DB_JS_PRE_PRECISION; + max = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Max)) / GOKZ_DB_JS_MAX_PRECISION; + airtime = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Air)) / GOKZ_DB_JS_AIRTIME_PRECISION; + + FormatEx(display, sizeof(display), "#%-2d %d %T (%.4f) %s", i + 1, block, "Block", client, distance, alias); + menu.AddItem(IntToStringEx(i), display); + + if (clientIsAdmin) + { + FormatEx(admin, sizeof(admin), "<id: %d>", jumpid); + } + + PrintToConsole(client, "#%-2d %d %t (%.4f) %s <STEAM_1:%d:%d> [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s", + i + 1, block, "Block", distance, alias, steamid & 1, steamid >> 1, + strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", + admin); + + jumpInfo[client][i][0] = steamid; + jumpInfo[client][i][1] = type; + jumpInfo[client][i][2] = mode; + blockNums[client][i] = block; + } + } + menu.Display(client, MENU_TIME_FOREVER); + PrintToConsole(client, ""); +} + +// =====[ MENUS ]===== + +void DisplayJumpTopModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_JumpTopMode); + menu.SetTitle("%T", "Jump Top Mode Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +void DisplayJumpTopTypeMenu(int client, int mode) +{ + jumpTopMode[client] = mode; + + Menu menu = new Menu(MenuHandler_JumpTopType); + menu.SetTitle("%T", "Jump Top Type Menu - Title", client, gC_ModeNames[jumpTopMode[client]]); + JumpTopTypeMenuAddItems(menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void JumpTopTypeMenuAddItems(Menu menu) +{ + char display[32]; + for (int i = 0; i < JUMPTYPE_COUNT - 3; i++) + { + FormatEx(display, sizeof(display), "%s", gC_JumpTypes[i]); + menu.AddItem(IntToStringEx(i), display); + } +} + +void DisplayJumpTopBlockTypeMenu(int client, int mode, int type) +{ + jumpTopMode[client] = mode; + jumpTopType[client] = type; + + Menu menu = new Menu(MenuHandler_JumpTopBlockType); + menu.SetTitle("%T", "Jump Top Block Type Menu - Title", client, gC_ModeNames[jumpTopMode[client]], gC_JumpTypes[jumpTopType[client]]); + JumpTopBlockTypeMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void JumpTopBlockTypeMenuAddItems(int client, Menu menu) +{ + char str[64]; + FormatEx(str, sizeof(str), "%T", "Jump Records", client); + menu.AddItem("jump", str); + FormatEx(str, sizeof(str), "%T %T", "Block", client, "Jump Records", client); + menu.AddItem("blockjump", str); +} + + + +// =====[ MENU HANDLERS ]===== + +public int MenuHandler_JumpTopMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = mode + DisplayJumpTopTypeMenu(param1, param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_JumpTopType(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = type + DisplayJumpTopBlockTypeMenu(param1, jumpTopMode[param1], param2); + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayJumpTopModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_JumpTopBlockType(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = block type + DB_OpenJumpTop(param1, jumpTopMode[param1], jumpTopType[param1], param2); + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayJumpTopTypeMenu(param1, jumpTopMode[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_JumpTopList(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char path[PLATFORM_MAX_PATH]; + if (blockNums[param1][param2] == 0) + { + BuildPath(Path_SM, path, sizeof(path), + "%s/%d/%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, jumpInfo[param1][param2][0], jumpTopType[param1], gC_ModeNamesShort[jumpInfo[param1][param2][2]], gC_StyleNamesShort[0], RP_FILE_EXTENSION); + } + else + { + BuildPath(Path_SM, path, sizeof(path), + "%s/%d/%s/%d_%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, jumpInfo[param1][param2][0], RP_DIRECTORY_BLOCKJUMPS, jumpTopType[param1], blockNums[param1][param2], gC_ModeNamesShort[jumpInfo[param1][param2][2]], gC_StyleNamesShort[0], RP_FILE_EXTENSION); + } + GOKZ_RP_LoadJumpReplay(param1, path); + } + + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayJumpTopBlockTypeMenu(param1, jumpTopMode[param1], jumpTopType[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} diff --git a/sourcemod/scripting/gokz-localranks/db/map_top.sp b/sourcemod/scripting/gokz-localranks/db/map_top.sp new file mode 100644 index 0000000..5b296e9 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/map_top.sp @@ -0,0 +1,388 @@ +/* + Opens a menu with the top times for a map course and mode. +*/ + + + +#define ITEM_INFO_GLOBAL_TOP_NUB "gn" +#define ITEM_INFO_GLOBAL_TOP_PRO "gp" + +static char mapTopMap[MAXPLAYERS + 1][64]; +static int mapTopMapID[MAXPLAYERS + 1]; +static int mapTopCourse[MAXPLAYERS + 1]; +static int mapTopMode[MAXPLAYERS + 1]; + + + +// =====[ MAP TOP MODE ]===== + +void DB_OpenMapTopModeMenu(int client, int mapID, int course) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(mapID); + data.WriteCell(course); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve Map Name of MapID + FormatEx(query, sizeof(query), sql_maps_getname, mapID); + txn.AddQuery(query); + // Check for existence of map course with that MapID and Course + FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenMapTopModeMenu, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenMapTopModeMenu(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int mapID = data.ReadCell(); + int course = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + // Get name of map + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, mapTopMap[client], sizeof(mapTopMap[])); + } + // Check if the map course exists in the database + if (SQL_GetRowCount(results[1]) == 0) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapTopMap[client]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapTopMap[client], course); + } + return; + } + + mapTopMapID[client] = mapID; + mapTopCourse[client] = course; + DisplayMapTopModeMenu(client); +} + +void DB_OpenMapTopModeMenu_FindMap(int client, const char[] mapSearch, int course) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(mapSearch); + data.WriteCell(course); + + DB_FindMap(mapSearch, DB_TxnSuccess_OpenMapTopModeMenu_FindMap, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenMapTopModeMenu_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char mapSearch[33]; + data.ReadString(mapSearch, sizeof(mapSearch)); + int course = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { // Result is the MapID + DB_OpenMapTopModeMenu(client, SQL_FetchInt(results[0], 0), course); + } +} + + + +// =====[ MAP TOP ]===== + +void DB_OpenMapTop(int client, int mapID, int course, int mode, int timeType) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteCell(mode); + data.WriteCell(timeType); + + Transaction txn = SQL_CreateTransaction(); + + // Get map name + FormatEx(query, sizeof(query), sql_maps_getname, mapID); + txn.AddQuery(query); + // Check for existence of map course with that MapID and Course + FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course); + txn.AddQuery(query); + + // Get top times for each time type + switch (timeType) + { + case TimeType_Nub:FormatEx(query, sizeof(query), sql_getmaptop, mapID, course, mode, LR_MAP_TOP_CUTOFF); + case TimeType_Pro:FormatEx(query, sizeof(query), sql_getmaptoppro, mapID, course, mode, LR_MAP_TOP_CUTOFF); + } + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenMapTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenMapTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + int mode = data.ReadCell(); + int timeType = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + // Get map name from results + char mapName[64]; + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, mapName, sizeof(mapName)); + } + // Check if the map course exists in the database + if (SQL_GetRowCount(results[1]) == 0) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course); + } + return; + } + + // Check if there are any times + if (SQL_GetRowCount(results[2]) == 0) + { + switch (timeType) + { + case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "No Times Found"); + case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "No Times Found (PRO)"); + } + DisplayMapTopMenu(client, mode); + return; + } + + Menu menu = new Menu(MenuHandler_MapTopSubmenu); + menu.Pagination = 5; + + // Set submenu title + if (course == 0) + { + menu.SetTitle("%T", "Map Top Submenu - Title", client, + LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[timeType], mapName, gC_ModeNames[mode]); + } + else + { + menu.SetTitle("%T", "Map Top Submenu - Title (Bonus)", client, + LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[timeType], mapName, course, gC_ModeNames[mode]); + } + + // Add submenu items + char display[128], title[65], admin[65]; + char playerName[MAX_NAME_LENGTH]; + float runTime; + int timeid, steamid, teleports, rank = 0; + + bool clientIsAdmin = CheckCommandAccess(client, "sm_deletetime", ADMFLAG_ROOT, false); + + FormatEx(title, sizeof(title), "%s %s %s %T", gC_ModeNames[mode], mapName, gC_TimeTypeNames[timeType], "Top", client); + strcopy(display, sizeof(display), "----------------------------------------------------------------"); + display[strlen(title)] = '\0'; + PrintToConsole(client, title); + PrintToConsole(client, display); + + while (SQL_FetchRow(results[2])) + { + rank++; + timeid = SQL_FetchInt(results[2], 0); + steamid = SQL_FetchInt(results[2], 1); + SQL_FetchString(results[2], 2, playerName, sizeof(playerName)); + runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 3)); + + if (clientIsAdmin) + { + FormatEx(admin, sizeof(admin), "<id: %d>", timeid); + } + + switch (timeType) + { + case TimeType_Nub: + { + teleports = SQL_FetchInt(results[2], 4); + FormatEx(display, sizeof(display), "#%-2d %11s %3d TP %s", + rank, GOKZ_FormatTime(runTime), teleports, playerName); + + PrintToConsole(client, "#%-2d %11s %3d TP %s <STEAM_1:%d:%d> %s", + rank, GOKZ_FormatTime(runTime), teleports, playerName, steamid & 1, steamid >> 1, admin); + } + case TimeType_Pro: + { + FormatEx(display, sizeof(display), "#%-2d %11s %s", + rank, GOKZ_FormatTime(runTime), playerName); + + PrintToConsole(client, "#%-2d %11s %s <STEAM_1:%d:%d> %s", + rank, GOKZ_FormatTime(runTime), playerName, steamid & 1, steamid >> 1, admin); + } + } + menu.AddItem("", display, ITEMDRAW_DISABLED); + } + + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ MENUS ]===== + +void DisplayMapTopModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_MapTopMode); + MapTopModeMenuSetTitle(client, menu); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void MapTopModeMenuSetTitle(int client, Menu menu) +{ + if (mapTopCourse[client] == 0) + { + menu.SetTitle("%T", "Map Top Mode Menu - Title", client, mapTopMap[client]); + } + else + { + menu.SetTitle("%T", "Map Top Mode Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client]); + } +} + +void DisplayMapTopMenu(int client, int mode) +{ + mapTopMode[client] = mode; + + Menu menu = new Menu(MenuHandler_MapTop); + if (mapTopCourse[client] == 0) + { + menu.SetTitle("%T", "Map Top Menu - Title", client, + mapTopMap[client], gC_ModeNames[mapTopMode[client]]); + } + else + { + menu.SetTitle("%T", "Map Top Menu - Title (Bonus)", client, + mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]); + } + MapTopMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void MapTopMenuAddItems(int client, Menu menu) +{ + char display[32]; + for (int i = 0; i < TIMETYPE_COUNT; i++) + { + FormatEx(display, sizeof(display), "%T", "Map Top Menu - Top", client, LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[i]); + menu.AddItem(IntToStringEx(i), display); + } + if (gB_GOKZGlobal) + { + FormatEx(display, sizeof(display), "%T", "Map Top Menu - Global Top", client, gC_TimeTypeNames[TimeType_Nub]); + menu.AddItem(ITEM_INFO_GLOBAL_TOP_NUB, display); + + FormatEx(display, sizeof(display), "%T", "Map Top Menu - Global Top", client, gC_TimeTypeNames[TimeType_Pro]); + menu.AddItem(ITEM_INFO_GLOBAL_TOP_PRO, display); + } +} + +void ReopenMapTopMenu(int client) +{ + DisplayMapTopMenu(client, mapTopMode[client]); +} + + + +// =====[ MENU HANDLERS ]===== + +public int MenuHandler_MapTopMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + // param1 = client, param2 = mode + DisplayMapTopMenu(param1, param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_MapTop(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[8]; + menu.GetItem(param2, info, sizeof(info)); + + if (gB_GOKZGlobal && StrEqual(info, ITEM_INFO_GLOBAL_TOP_NUB)) + { + GOKZ_GL_DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], TimeType_Nub); + } + else if (gB_GOKZGlobal && StrEqual(info, ITEM_INFO_GLOBAL_TOP_PRO)) + { + GOKZ_GL_DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], TimeType_Pro); + } + else + { + int timeType = StringToInt(info); + DB_OpenMapTop(param1, mapTopMapID[param1], mapTopCourse[param1], mapTopMode[param1], timeType); + } + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayMapTopModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_MapTopSubmenu(Menu menu, MenuAction action, int param1, int param2) +{ + // TODO Menu item info is player's SteamID32, but is currently not used + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + ReopenMapTopMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/player_top.sp b/sourcemod/scripting/gokz-localranks/db/player_top.sp new file mode 100644 index 0000000..0348eec --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/player_top.sp @@ -0,0 +1,165 @@ +/* + Opens a menu with top record holders of a time type and mode. +*/ + + + +static int playerTopMode[MAXPLAYERS + 1]; + + + +void DB_OpenPlayerTop(int client, int timeType, int mode) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(timeType); + data.WriteCell(mode); + + Transaction txn = SQL_CreateTransaction(); + + // Get top players + switch (timeType) + { + case TimeType_Nub: + { + FormatEx(query, sizeof(query), sql_gettopplayers, mode, LR_PLAYER_TOP_CUTOFF); + txn.AddQuery(query); + } + case TimeType_Pro: + { + FormatEx(query, sizeof(query), sql_gettopplayerspro, mode, LR_PLAYER_TOP_CUTOFF); + txn.AddQuery(query); + } + } + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenPlayerTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenPlayerTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int timeType = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + switch (timeType) + { + case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "Player Top - No Times"); + case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "Player Top - No Times (PRO)"); + } + DisplayPlayerTopMenu(client, playerTopMode[client]); + return; + } + + Menu menu = new Menu(MenuHandler_PlayerTopSubmenu); + menu.Pagination = 5; + + // Set submenu title + menu.SetTitle("%T", "Player Top Submenu - Title", client, + LR_PLAYER_TOP_CUTOFF, gC_TimeTypeNames[timeType], gC_ModeNames[mode]); + + // Add submenu items + char display[256]; + int rank = 0; + while (SQL_FetchRow(results[0])) + { + rank++; + char playerString[33]; + SQL_FetchString(results[0], 1, playerString, sizeof(playerString)); + FormatEx(display, sizeof(display), "#%-2d %s (%d)", rank, playerString, SQL_FetchInt(results[0], 2)); + menu.AddItem(IntToStringEx(SQL_FetchInt(results[0], 0)), display, ITEMDRAW_DISABLED); + } + + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ MENUS ]===== + +void DisplayPlayerTopModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_PlayerTopMode); + menu.SetTitle("%T", "Player Top Mode Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +void DisplayPlayerTopMenu(int client, int mode) +{ + playerTopMode[client] = mode; + + Menu menu = new Menu(MenuHandler_PlayerTop); + menu.SetTitle("%T", "Player Top Menu - Title", client, gC_ModeNames[mode]); + PlayerTopMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void PlayerTopMenuAddItems(int client, Menu menu) +{ + char display[32]; + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + FormatEx(display, sizeof(display), "%T", "Player Top Menu - Top", client, + LR_PLAYER_TOP_CUTOFF, gC_TimeTypeNames[timeType]); + menu.AddItem("", display, ITEMDRAW_DEFAULT); + } +} + + + +// =====[ MENU HANLDERS ]===== + +public int MenuHandler_PlayerTopMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + DisplayPlayerTopMenu(param1, param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_PlayerTop(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + DB_OpenPlayerTop(param1, param2, playerTopMode[param1]); + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayPlayerTopModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_PlayerTopSubmenu(Menu menu, MenuAction action, int param1, int param2) +{ + // Menu item info is player's SteamID32, but is currently not used + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayPlayerTopMenu(param1, playerTopMode[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/print_average.sp b/sourcemod/scripting/gokz-localranks/db/print_average.sp new file mode 100644 index 0000000..7f7c4e4 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/print_average.sp @@ -0,0 +1,152 @@ +/* + Gets the average personal best time of a course. +*/ + + + +void DB_PrintAverage(int client, int mapID, int course, int mode) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteCell(mode); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve Map Name of MapID + FormatEx(query, sizeof(query), sql_maps_getname, mapID); + txn.AddQuery(query); + // Check for existence of map course with that MapID and Course + FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course); + txn.AddQuery(query); + // Get Average PB Time + FormatEx(query, sizeof(query), sql_getaverage, mapID, course, mode); + txn.AddQuery(query); + // Get Average PRO PB Time + FormatEx(query, sizeof(query), sql_getaverage_pro, mapID, course, mode); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintAverage, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintAverage(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + char mapName[33]; + int mapCompletions, mapCompletionsPro; + float averageTime, averageTimePro; + + // Get Map Name from results + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, mapName, sizeof(mapName)); + } + if (SQL_GetRowCount(results[1]) == 0) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course); + } + return; + } + + // Get number of completions and average time + if (SQL_FetchRow(results[2])) + { + mapCompletions = SQL_FetchInt(results[2], 1); + if (mapCompletions > 0) + { + averageTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 0)); + } + } + + // Get number of completions and average time (PRO) + if (SQL_FetchRow(results[3])) + { + mapCompletionsPro = SQL_FetchInt(results[3], 1); + if (mapCompletions > 0) + { + averageTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 0)); + } + } + + // Print average time header to chat + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Average Time Header", mapName, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Average Time Header (Bonus)", mapName, course, gC_ModeNamesShort[mode]); + } + + if (mapCompletions == 0) + { + CPrintToChat(client, "%t", "No Times Found"); + } + else if (mapCompletionsPro == 0) + { + CPrintToChat(client, "%t, %t", + "Average Time - NUB", GOKZ_FormatTime(averageTime), mapCompletions, + "Average Time - No PRO Time"); + } + else + { + CPrintToChat(client, "%t, %t", + "Average Time - NUB", GOKZ_FormatTime(averageTime), mapCompletions, + "Average Time - PRO", GOKZ_FormatTime(averageTimePro), mapCompletionsPro); + } +} + +void DB_PrintAverage_FindMap(int client, const char[] mapSearch, int course, int mode) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(mapSearch); + data.WriteCell(course); + data.WriteCell(mode); + + DB_FindMap(mapSearch, DB_TxnSuccess_PrintAverage_FindMap, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintAverage_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char mapSearch[33]; + data.ReadString(mapSearch, sizeof(mapSearch)); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { // Result is the MapID + DB_PrintAverage(client, SQL_FetchInt(results[0], 0), course, mode); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/print_js.sp b/sourcemod/scripting/gokz-localranks/db/print_js.sp new file mode 100644 index 0000000..e694a95 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/print_js.sp @@ -0,0 +1,108 @@ +/* + Prints the player's personal best jumps. +*/ + + + +void DisplayJumpstatRecord(int client, int jumpType, char[] jumper = "") +{ + int mode = GOKZ_GetCoreOption(client, Option_Mode); + + int steamid; + char alias[33]; + if (StrEqual(jumper, "")) + { + steamid = GetSteamAccountID(client); + FormatEx(alias, sizeof(alias), "%N", client); + + DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 0); + DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 1); + } + else + { + DataPack data = new DataPack(); + data.WriteCell(client); + data.WriteCell(jumpType); + data.WriteCell(mode); + data.WriteString(jumper); + + DB_FindPlayer(jumper, DB_JS_TxnSuccess_LookupPlayer, data); + } +} + +public void DB_JS_TxnSuccess_LookupPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + char jumper[MAX_NAME_LENGTH]; + + data.Reset(); + int client = data.ReadCell(); + int jumpType = data.ReadCell(); + int mode = data.ReadCell(); + data.ReadString(jumper, sizeof(jumper)); + delete data; + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Player Not Found", jumper); + return; + } + + char alias[33]; + SQL_FetchRow(results[0]); + int steamid = SQL_FetchInt(results[0], JumpstatDB_FindPlayer_SteamID32); + SQL_FetchString(results[0], JumpstatDB_FindPlayer_Alias, alias, sizeof(alias)); + + DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 0); + DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 1); +} + +void DB_JS_OpenPlayerRecord(int client, int steamid, char[] alias, int jumpType, int mode, int block) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(client); + data.WriteString(alias); + data.WriteCell(jumpType); + data.WriteCell(mode); + + Transaction txn = SQL_CreateTransaction(); + FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jumpType, mode, block); + txn.AddQuery(query); + SQL_ExecuteTransaction(gH_DB, txn, DB_JS_TxnSuccess_OpenPlayerRecord, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_JS_TxnSuccess_OpenPlayerRecord(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + char alias[33]; + data.Reset(); + int client = data.ReadCell(); + data.ReadString(alias, sizeof(alias)); + int jumpType = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Jumpstats Found"); + return; + } + + SQL_FetchRow(results[0]); + float distance = float(SQL_FetchInt(results[0], JumpstatDB_Lookup_Distance)) / 10000; + int block = SQL_FetchInt(results[0], JumpstatDB_Lookup_Block); + + if (block == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Jump Record", gC_ModeNamesShort[mode], gC_JumpTypes[jumpType], alias, distance); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Block Jump Record", gC_ModeNamesShort[mode], gC_JumpTypes[jumpType], alias, block, distance); + } +} diff --git a/sourcemod/scripting/gokz-localranks/db/print_pbs.sp b/sourcemod/scripting/gokz-localranks/db/print_pbs.sp new file mode 100644 index 0000000..0d541d4 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/print_pbs.sp @@ -0,0 +1,266 @@ +/* + Prints the player's personal times on a map course and given mode. +*/ + + + +void DB_PrintPBs(int client, int targetSteamID, int mapID, int course, int mode) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteCell(mode); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve Alias of SteamID + FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID); + txn.AddQuery(query); + // Retrieve Map Name of MapID + FormatEx(query, sizeof(query), sql_maps_getname, mapID); + txn.AddQuery(query); + // Check for existence of map course with that MapID and Course + FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course); + txn.AddQuery(query); + + // Get PB + FormatEx(query, sizeof(query), sql_getpb, targetSteamID, mapID, course, mode, 1); + txn.AddQuery(query); + // Get Rank + FormatEx(query, sizeof(query), sql_getmaprank, mapID, course, mode, targetSteamID, mapID, course, mode); + txn.AddQuery(query); + // Get Number of Players with Times + FormatEx(query, sizeof(query), sql_getlowestmaprank, mapID, course, mode); + txn.AddQuery(query); + + // Get PRO PB + FormatEx(query, sizeof(query), sql_getpbpro, targetSteamID, mapID, course, mode, 1); + txn.AddQuery(query); + // Get PRO Rank + FormatEx(query, sizeof(query), sql_getmaprankpro, mapID, course, mode, targetSteamID, mapID, course, mode); + txn.AddQuery(query); + // Get Number of Players with PRO Times + FormatEx(query, sizeof(query), sql_getlowestmaprankpro, mapID, course, mode); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintPBs, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintPBs(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + char playerName[MAX_NAME_LENGTH], mapName[33]; + + bool hasPB = false; + bool hasPBPro = false; + + float runTime; + int teleportsUsed; + int rank; + int maxRank; + + float runTimePro; + int rankPro; + int maxRankPro; + + // Get Player Name from results + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, playerName, sizeof(playerName)); + } + // Get Map Name from results + if (SQL_FetchRow(results[1])) + { + SQL_FetchString(results[1], 0, mapName, sizeof(mapName)); + } + if (SQL_GetRowCount(results[2]) == 0) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course); + } + return; + } + + // Get PB info from results + if (SQL_GetRowCount(results[3]) > 0) + { + hasPB = true; + if (SQL_FetchRow(results[3])) + { + runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 0)); + teleportsUsed = SQL_FetchInt(results[3], 1); + } + if (SQL_FetchRow(results[4])) + { + rank = SQL_FetchInt(results[4], 0); + } + if (SQL_FetchRow(results[5])) + { + maxRank = SQL_FetchInt(results[5], 0); + } + } + // Get PB info (Pro) from results + if (SQL_GetRowCount(results[6]) > 0) + { + hasPBPro = true; + if (SQL_FetchRow(results[6])) + { + runTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[6], 0)); + } + if (SQL_FetchRow(results[7])) + { + rankPro = SQL_FetchInt(results[7], 0); + } + if (SQL_FetchRow(results[8])) + { + maxRankPro = SQL_FetchInt(results[8], 0); + } + } + + // Print PB header to chat + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "PB Header", playerName, mapName, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "PB Header (Bonus)", playerName, mapName, course, gC_ModeNamesShort[mode]); + } + + // Print PB times to chat + if (!hasPB) + { + CPrintToChat(client, "%t", "PB Time - No Times"); + } + else if (!hasPBPro) + { + CPrintToChat(client, "%t", "PB Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, rank, maxRank); + CPrintToChat(client, "%t", "PB Time - No PRO Time"); + } + else if (teleportsUsed == 0) + { // Their MAP PB has 0 teleports, and is therefore also their PRO PB + CPrintToChat(client, "%t", "PB Time - NUB and PRO", GOKZ_FormatTime(runTime), rank, maxRank, rankPro, maxRankPro); + } + else + { + CPrintToChat(client, "%t", "PB Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, rank, maxRank); + CPrintToChat(client, "%t", "PB Time - PRO", GOKZ_FormatTime(runTimePro), rankPro, maxRankPro); + } +} + +void DB_PrintPBs_FindMap(int client, int targetSteamID, const char[] mapSearch, int course, int mode) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(targetSteamID); + data.WriteString(mapSearch); + data.WriteCell(course); + data.WriteCell(mode); + + DB_FindMap(mapSearch, DB_TxnSuccess_PrintPBs_FindMap, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintPBs_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int targetSteamID = data.ReadCell(); + char mapSearch[33]; + data.ReadString(mapSearch, sizeof(mapSearch)); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + // Check if the map course exists in the database + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { // Result is the MapID + DB_PrintPBs(client, targetSteamID, SQL_FetchInt(results[0], 0), course, mode); + if (gB_GOKZGlobal) + { + char map[33], steamid[32]; + SQL_FetchString(results[0], 1, map, sizeof(map)); + GetSteam2FromAccountId(steamid, sizeof(steamid), targetSteamID); + GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode), steamid); + } + } +} + +void DB_PrintPBs_FindPlayerAndMap(int client, const char[] playerSearch, const char[] mapSearch, int course, int mode) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(playerSearch); + data.WriteString(mapSearch); + data.WriteCell(course); + data.WriteCell(mode); + + DB_FindPlayerAndMap(playerSearch, mapSearch, DB_TxnSuccess_PrintPBs_FindPlayerAndMap, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintPBs_FindPlayerAndMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char playerSearch[MAX_NAME_LENGTH]; + data.ReadString(playerSearch, sizeof(playerSearch)); + char mapSearch[33]; + data.ReadString(mapSearch, sizeof(mapSearch)); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch); + return; + } + else if (SQL_GetRowCount(results[1]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch); + return; + } + else if (SQL_FetchRow(results[0]) && SQL_FetchRow(results[1])) + { + int accountid = SQL_FetchInt(results[0], 0); + DB_PrintPBs(client, accountid, SQL_FetchInt(results[1], 0), course, mode); + if (gB_GOKZGlobal) + { + char map[33], steamid[32]; + SQL_FetchString(results[1], 1, map, sizeof(map)); + GetSteam2FromAccountId(steamid, sizeof(steamid), accountid); + GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode), steamid); + } + } +} diff --git a/sourcemod/scripting/gokz-localranks/db/print_records.sp b/sourcemod/scripting/gokz-localranks/db/print_records.sp new file mode 100644 index 0000000..b5de03b --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/print_records.sp @@ -0,0 +1,173 @@ +/* + Prints the record times on a map course and given mode. +*/ + + + +void DB_PrintRecords(int client, int mapID, int course, int mode) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteCell(mode); + + Transaction txn = SQL_CreateTransaction(); + + // Retrieve Map Name of MapID + FormatEx(query, sizeof(query), sql_maps_getname, mapID); + txn.AddQuery(query); + // Check for existence of map course with that MapID and Course + FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course); + txn.AddQuery(query); + + // Get Map WR + FormatEx(query, sizeof(query), sql_getmaptop, mapID, course, mode, 1); + txn.AddQuery(query); + // Get PRO WR + FormatEx(query, sizeof(query), sql_getmaptoppro, mapID, course, mode, 1); + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintRecords, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintRecords(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + char mapName[33]; + + bool mapHasRecord = false; + bool mapHasRecordPro = false; + + char recordHolder[33]; + float runTime; + int teleportsUsed; + + char recordHolderPro[33]; + float runTimePro; + + // Get Map Name from results + if (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, mapName, sizeof(mapName)); + } + // Check if the map course exists in the database + if (SQL_GetRowCount(results[1]) == 0) + { + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course); + } + return; + } + + // Get WR info from results + if (SQL_GetRowCount(results[2]) > 0) + { + mapHasRecord = true; + if (SQL_FetchRow(results[2])) + { + SQL_FetchString(results[2], 2, recordHolder, sizeof(recordHolder)); + runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 3)); + teleportsUsed = SQL_FetchInt(results[2], 4); + } + } + // Get Pro WR info from results + if (SQL_GetRowCount(results[3]) > 0) + { + mapHasRecordPro = true; + if (SQL_FetchRow(results[3])) + { + SQL_FetchString(results[3], 2, recordHolderPro, sizeof(recordHolderPro)); + runTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 3)); + } + } + + // Print WR header to chat + if (course == 0) + { + GOKZ_PrintToChat(client, true, "%t", "WR Header", mapName, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "WR Header (Bonus)", mapName, course, gC_ModeNamesShort[mode]); + } + + // Print WR times to chat + if (!mapHasRecord) + { + CPrintToChat(client, "%t", "No Times Found"); + } + else if (!mapHasRecordPro) + { + CPrintToChat(client, "%t", "WR Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, recordHolder); + CPrintToChat(client, "%t", "WR Time - No PRO Time"); + } + else if (teleportsUsed == 0) + { + CPrintToChat(client, "%t", "WR Time - NUB and PRO", GOKZ_FormatTime(runTimePro), recordHolderPro); + } + else + { + CPrintToChat(client, "%t", "WR Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, recordHolder); + CPrintToChat(client, "%t", "WR Time - PRO", GOKZ_FormatTime(runTimePro), recordHolderPro); + } +} + +void DB_PrintRecords_FindMap(int client, const char[] mapSearch, int course, int mode) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteString(mapSearch); + data.WriteCell(course); + data.WriteCell(mode); + + DB_FindMap(mapSearch, DB_TxnSuccess_PrintRecords_FindMap, data, DBPrio_Low); +} + +public void DB_TxnSuccess_PrintRecords_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + char mapSearch[33]; + data.ReadString(mapSearch, sizeof(mapSearch)); + int course = data.ReadCell(); + int mode = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (SQL_GetRowCount(results[0]) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch); + return; + } + else if (SQL_FetchRow(results[0])) + { // Result is the MapID + DB_PrintRecords(client, SQL_FetchInt(results[0], 0), course, mode); + if (gB_GOKZGlobal) + { + char map[33]; + SQL_FetchString(results[0], 1, map, sizeof(map)); + GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode)); + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/process_new_time.sp b/sourcemod/scripting/gokz-localranks/db/process_new_time.sp new file mode 100644 index 0000000..f9aaf73 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/process_new_time.sp @@ -0,0 +1,157 @@ +/* + Processes a newly submitted time, determining if the player beat their + personal best and if they beat the map course and mode's record time. +*/ + + + +void DB_ProcessNewTime(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(steamID); + data.WriteCell(mapID); + data.WriteCell(course); + data.WriteCell(mode); + data.WriteCell(style); + data.WriteCell(runTimeMS); + data.WriteCell(teleportsUsed); + + Transaction txn = SQL_CreateTransaction(); + + // Get Top 2 PBs + FormatEx(query, sizeof(query), sql_getpb, steamID, mapID, course, mode, 2); + txn.AddQuery(query); + // Get Rank + FormatEx(query, sizeof(query), sql_getmaprank, mapID, course, mode, steamID, mapID, course, mode); + txn.AddQuery(query); + // Get Number of Players with Times + FormatEx(query, sizeof(query), sql_getlowestmaprank, mapID, course, mode); + txn.AddQuery(query); + + if (teleportsUsed == 0) + { + // Get Top 2 PRO PBs + FormatEx(query, sizeof(query), sql_getpbpro, steamID, mapID, course, mode, 2); + txn.AddQuery(query); + // Get PRO Rank + FormatEx(query, sizeof(query), sql_getmaprankpro, mapID, course, mode, steamID, mapID, course, mode); + txn.AddQuery(query); + // Get Number of Players with PRO Times + FormatEx(query, sizeof(query), sql_getlowestmaprankpro, mapID, course, mode); + txn.AddQuery(query); + } + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_ProcessTimerEnd, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal); +} + +public void DB_TxnSuccess_ProcessTimerEnd(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int steamID = data.ReadCell(); + int mapID = data.ReadCell(); + int course = data.ReadCell(); + int mode = data.ReadCell(); + int style = data.ReadCell(); + int runTimeMS = data.ReadCell(); + int teleportsUsed = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + bool firstTime = SQL_GetRowCount(results[0]) == 1; + int pbDiff = 0; + int rank = -1; + int maxRank = -1; + if (!firstTime) + { + SQL_FetchRow(results[0]); + int pb = SQL_FetchInt(results[0], 0); + if (runTimeMS == pb) // New time is new PB + { + SQL_FetchRow(results[0]); + int oldPB = SQL_FetchInt(results[0], 0); + pbDiff = runTimeMS - oldPB; + } + else // Didn't beat PB + { + pbDiff = runTimeMS - pb; + } + } + // Get NUB Rank + SQL_FetchRow(results[1]); + rank = SQL_FetchInt(results[1], 0); + SQL_FetchRow(results[2]); + maxRank = SQL_FetchInt(results[2], 0); + + // Repeat for PRO Runs + bool firstTimePro = false; + int pbDiffPro = 0; + int rankPro = -1; + int maxRankPro = -1; + if (teleportsUsed == 0) + { + firstTimePro = SQL_GetRowCount(results[3]) == 1; + if (!firstTimePro) + { + SQL_FetchRow(results[3]); + int pb = SQL_FetchInt(results[3], 0); + if (runTimeMS == pb) // New time is new PB + { + SQL_FetchRow(results[3]); + int oldPB = SQL_FetchInt(results[3], 0); + pbDiffPro = runTimeMS - oldPB; + } + else // Didn't beat PB + { + pbDiffPro = runTimeMS - pb; + } + } + // Get PRO Rank + SQL_FetchRow(results[4]); + rankPro = SQL_FetchInt(results[4], 0); + SQL_FetchRow(results[5]); + maxRankPro = SQL_FetchInt(results[5], 0); + } + + // Call OnTimeProcessed forward + Call_OnTimeProcessed( + client, + steamID, + mapID, + course, + mode, + style, + GOKZ_DB_TimeIntToFloat(runTimeMS), + teleportsUsed, + firstTime, + GOKZ_DB_TimeIntToFloat(pbDiff), + rank, + maxRank, + firstTimePro, + GOKZ_DB_TimeIntToFloat(pbDiffPro), + rankPro, + maxRankPro); + + // Call OnNewRecord forward + bool newWR = (firstTime || pbDiff < 0) && rank == 1; + bool newWRPro = (firstTimePro || pbDiffPro < 0) && rankPro == 1; + if (newWR && newWRPro) + { + Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_NubAndPro, GOKZ_DB_TimeIntToFloat(pbDiffPro), teleportsUsed); + } + else if (newWR) + { + Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_Nub, GOKZ_DB_TimeIntToFloat(pbDiff), teleportsUsed); + } + else if (newWRPro) + { + Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_Pro, GOKZ_DB_TimeIntToFloat(pbDiffPro), teleportsUsed); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-localranks/db/recent_records.sp b/sourcemod/scripting/gokz-localranks/db/recent_records.sp new file mode 100644 index 0000000..939b132 --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/recent_records.sp @@ -0,0 +1,171 @@ +/* + Opens the menu with a list of recently broken records for the given mode + and time type. +*/ + + + +static int recentRecordsMode[MAXPLAYERS + 1]; + + + +void DB_OpenRecentRecords(int client, int mode, int timeType) +{ + char query[1024]; + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(mode); + data.WriteCell(timeType); + + Transaction txn = SQL_CreateTransaction(); + + switch (timeType) + { + case TimeType_Nub:FormatEx(query, sizeof(query), sql_getrecentrecords, mode, LR_PLAYER_TOP_CUTOFF); + case TimeType_Pro:FormatEx(query, sizeof(query), sql_getrecentrecords_pro, mode, LR_PLAYER_TOP_CUTOFF); + } + txn.AddQuery(query); + + SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenRecentRecords, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low); +} + +public void DB_TxnSuccess_OpenRecentRecords(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int mode = data.ReadCell(); + int timeType = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + // Check if there are any times + if (SQL_GetRowCount(results[0]) == 0) + { + switch (timeType) + { + case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "No Times Found"); + case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "No Times Found (PRO)"); + } + return; + } + + Menu menu = new Menu(MenuHandler_RecentRecordsSubmenu); + menu.Pagination = 5; + + // Set submenu title + menu.SetTitle("%T", "Recent Records Submenu - Title", client, + gC_TimeTypeNames[timeType], gC_ModeNames[mode]); + + // Add submenu items + char display[256], mapName[64], playerName[33]; + int course; + float runTime; + + while (SQL_FetchRow(results[0])) + { + SQL_FetchString(results[0], 0, mapName, sizeof(mapName)); + course = SQL_FetchInt(results[0], 1); + SQL_FetchString(results[0], 3, playerName, sizeof(playerName)); + runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 4)); + + if (course == 0) + { + FormatEx(display, sizeof(display), "%s - %s (%s)", + mapName, playerName, GOKZ_FormatTime(runTime)); + } + else + { + FormatEx(display, sizeof(display), "%s B%d - %s (%s)", + mapName, course, playerName, GOKZ_FormatTime(runTime)); + } + + menu.AddItem(IntToStringEx(SQL_FetchInt(results[0], 2)), display, ITEMDRAW_DISABLED); + } + + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ MENUS ]===== + +void DisplayRecentRecordsModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_RecentRecordsMode); + menu.SetTitle("%T", "Recent Records Mode Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + +void DisplayRecentRecordsTimeTypeMenu(int client, int mode) +{ + recentRecordsMode[client] = mode; + + Menu menu = new Menu(MenuHandler_RecentRecordsTimeType); + menu.SetTitle("%T", "Recent Records Menu - Title", client, gC_ModeNames[recentRecordsMode[client]]); + RecentRecordsTimeTypeAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void RecentRecordsTimeTypeAddItems(int client, Menu menu) +{ + char display[32]; + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + FormatEx(display, sizeof(display), "%T", "Recent Records Menu - Record Type", client, gC_TimeTypeNames[timeType]); + menu.AddItem("", display, ITEMDRAW_DEFAULT); + } +} + + + +// =====[ MENU HANDLERS ]===== + +public int MenuHandler_RecentRecordsMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + DisplayRecentRecordsTimeTypeMenu(param1, param2); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_RecentRecordsTimeType(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + DB_OpenRecentRecords(param1, recentRecordsMode[param1], param2); + } + else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayRecentRecordsModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_RecentRecordsSubmenu(Menu menu, MenuAction action, int param1, int param2) +{ + // TODO Menu item info is course's MapCourseID, but is currently not used + if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) + { + DisplayRecentRecordsTimeTypeMenu(param1, recentRecordsMode[param1]); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} diff --git a/sourcemod/scripting/gokz-localranks/db/sql.sp b/sourcemod/scripting/gokz-localranks/db/sql.sp new file mode 100644 index 0000000..f768e9a --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/sql.sp @@ -0,0 +1,411 @@ +/* + SQL query templates. +*/ + + + +// =====[ MAPS ]===== + +char sqlite_maps_alter1[] = "\ +ALTER TABLE Maps \ + ADD InRankedPool INTEGER NOT NULL DEFAULT '0'"; + +char mysql_maps_alter1[] = "\ +ALTER TABLE Maps \ + ADD InRankedPool TINYINT NOT NULL DEFAULT '0'"; + +char sqlite_maps_insertranked[] = "\ +INSERT OR IGNORE INTO Maps \ + (InRankedPool, Name) \ + VALUES (%d, '%s')"; + +char sqlite_maps_updateranked[] = "\ +UPDATE OR IGNORE Maps \ + SET InRankedPool=%d \ + WHERE Name = '%s'"; + +char mysql_maps_upsertranked[] = "\ +INSERT INTO Maps (InRankedPool, Name) \ + VALUES (%d, '%s') \ + ON DUPLICATE KEY UPDATE \ + InRankedPool=VALUES(InRankedPool)"; + +char sql_maps_reset_mappool[] = "\ +UPDATE Maps \ + SET InRankedPool=0"; + +char sql_maps_getname[] = "\ +SELECT Name \ + FROM Maps \ + WHERE MapID=%d"; + +char sql_maps_searchbyname[] = "\ +SELECT MapID, Name \ + FROM Maps \ + WHERE Name LIKE '%%%s%%' \ + ORDER BY (Name='%s') DESC, LENGTH(Name) \ + LIMIT 1"; + + + +// =====[ PLAYERS ]===== + +char sql_players_getalias[] = "\ +SELECT Alias \ + FROM Players \ + WHERE SteamID32=%d"; + +char sql_players_searchbyalias[] = "\ +SELECT SteamID32, Alias \ + FROM Players \ + WHERE Players.Cheater=0 AND LOWER(Alias) LIKE '%%%s%%' \ + ORDER BY (LOWER(Alias)='%s') DESC, LastPlayed DESC \ + LIMIT 1"; + + + +// =====[ MAPCOURSES ]===== + +char sql_mapcourses_findid[] = "\ +SELECT MapCourseID \ + FROM MapCourses \ + WHERE MapID=%d AND Course=%d"; + + + +// =====[ GENERAL ]===== + +char sql_getpb[] = "\ +SELECT Times.RunTime, Times.Teleports \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d \ + ORDER BY Times.RunTime \ + LIMIT %d"; + +char sql_getpbpro[] = "\ +SELECT Times.RunTime \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0 \ + ORDER BY Times.RunTime \ + LIMIT %d"; + +char sql_getmaptop[] = "\ +SELECT t.TimeID, t.SteamID32, p.Alias, t.RunTime AS PBTime, t.Teleports \ + FROM Times t \ + INNER JOIN MapCourses mc ON mc.MapCourseID=t.MapCourseID \ + INNER JOIN Players p ON p.SteamID32=t.SteamID32 \ + LEFT OUTER JOIN Times t2 ON t2.SteamID32=t.SteamID32 \ + AND t2.MapCourseID=t.MapCourseID AND t2.Mode=t.Mode AND t2.RunTime<t.RunTime \ + WHERE t2.TimeID IS NULL AND p.Cheater=0 AND mc.MapID=%d AND mc.Course=%d AND t.Mode=%d \ + ORDER BY PBTime \ + LIMIT %d"; + +char sql_getmaptoppro[] = "\ +SELECT t.TimeID, t.SteamID32, p.Alias, t.RunTime AS PBTime, t.Teleports \ + FROM Times t \ + INNER JOIN MapCourses mc ON mc.MapCourseID=t.MapCourseID \ + INNER JOIN Players p ON p.SteamID32=t.SteamID32 \ + LEFT OUTER JOIN Times t2 ON t2.SteamID32=t.SteamID32 AND t2.MapCourseID=t.MapCourseID \ + AND t2.Mode=t.Mode AND t2.RunTime<t.RunTime AND t.Teleports=0 AND t2.Teleports=0 \ + WHERE t2.TimeID IS NULL AND p.Cheater=0 AND mc.MapID=%d \ + AND mc.Course=%d AND t.Mode=%d AND t.Teleports=0 \ + ORDER BY PBTime \ + LIMIT %d"; + +char sql_getwrs[] = "\ +SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d \ + GROUP BY MapCourses.Course, Times.Mode"; + +char sql_getwrspro[] = "\ +SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND Times.Teleports=0 \ + GROUP BY MapCourses.Course, Times.Mode"; + +char sql_getpbs[] = "\ +SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \ + GROUP BY MapCourses.Course, Times.Mode"; + +char sql_getpbspro[] = "\ +SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + WHERE Times.SteamID32=%d AND MapCourses.MapID=%d AND Times.Teleports=0 \ + GROUP BY MapCourses.Course, Times.Mode"; + +char sql_getmaprank[] = "\ +SELECT COUNT(DISTINCT Times.SteamID32) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND MapCourses.Course=%d \ + AND Times.Mode=%d AND Times.RunTime < \ + (SELECT MIN(Times.RunTime) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND Times.SteamID32=%d AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d) \ + + 1"; + +char sql_getmaprankpro[] = "\ +SELECT COUNT(DISTINCT Times.SteamID32) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND MapCourses.Course=%d \ + AND Times.Mode=%d AND Times.Teleports=0 \ + AND Times.RunTime < \ + (SELECT MIN(Times.RunTime) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND Times.SteamID32=%d AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0) \ + + 1"; + +char sql_getlowestmaprank[] = "\ +SELECT COUNT(DISTINCT Times.SteamID32) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d"; + +char sql_getlowestmaprankpro[] = "\ +SELECT COUNT(DISTINCT Times.SteamID32) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0"; + +char sql_getcount_maincourses[] = "\ +SELECT COUNT(*) \ + FROM MapCourses \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course=0"; + +char sql_getcount_maincoursescompleted[] = "\ +SELECT COUNT(DISTINCT Times.MapCourseID) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course=0 \ + AND Times.SteamID32=%d AND Times.Mode=%d"; + +char sql_getcount_maincoursescompletedpro[] = "\ +SELECT COUNT(DISTINCT Times.MapCourseID) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course=0 \ + AND Times.SteamID32=%d AND Times.Mode=%d AND Times.Teleports=0"; + +char sql_getcount_bonuses[] = "\ +SELECT COUNT(*) \ + FROM MapCourses \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course>0"; + +char sql_getcount_bonusescompleted[] = "\ +SELECT COUNT(DISTINCT Times.MapCourseID) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course>0 \ + AND Times.SteamID32=%d AND Times.Mode=%d"; + +char sql_getcount_bonusescompletedpro[] = "\ +SELECT COUNT(DISTINCT Times.MapCourseID) \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + WHERE Maps.InRankedPool=1 AND MapCourses.Course>0 \ + AND Times.SteamID32=%d AND Times.Mode=%d AND Times.Teleports=0"; + +char sql_gettopplayers[] = "\ +SELECT Players.SteamID32, Players.Alias, COUNT(*) AS RecordCount \ + FROM Times \ + INNER JOIN \ + (SELECT Times.MapCourseID, Times.Mode, MIN(Times.RunTime) AS RecordTime \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND Maps.InRankedPool=1 AND MapCourses.Course=0 \ + AND Times.Mode=%d \ + GROUP BY Times.MapCourseID) Records \ + ON Times.MapCourseID=Records.MapCourseID AND Times.Mode=Records.Mode AND Times.RunTime=Records.RecordTime \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + GROUP BY Players.SteamID32, Players.Alias \ + ORDER BY RecordCount DESC \ + LIMIT %d"; // Doesn't include bonuses + +char sql_gettopplayerspro[] = "\ +SELECT Players.SteamID32, Players.Alias, COUNT(*) AS RecordCount \ + FROM Times \ + INNER JOIN \ + (SELECT Times.MapCourseID, Times.Mode, MIN(Times.RunTime) AS RecordTime \ + FROM Times \ + INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \ + INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + WHERE Players.Cheater=0 AND Maps.InRankedPool=1 AND MapCourses.Course=0 \ + AND Times.Mode=%d AND Times.Teleports=0 \ + GROUP BY Times.MapCourseID) Records \ + ON Times.MapCourseID=Records.MapCourseID AND Times.Mode=Records.Mode AND Times.RunTime=Records.RecordTime AND Times.Teleports=0 \ + INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \ + GROUP BY Players.SteamID32, Players.Alias \ + ORDER BY RecordCount DESC \ + LIMIT %d"; // Doesn't include bonuses + +char sql_getaverage[] = "\ +SELECT AVG(PBTime), COUNT(*) \ + FROM \ + (SELECT MIN(Times.RunTime) AS PBTime \ + FROM Times \ + INNER JOIN MapCourses ON Times.MapCourseID=MapCourses.MapCourseID \ + INNER JOIN Players ON Times.SteamID32=Players.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d \ + GROUP BY Times.SteamID32) AS PBTimes"; + +char sql_getaverage_pro[] = "\ +SELECT AVG(PBTime), COUNT(*) \ + FROM \ + (SELECT MIN(Times.RunTime) AS PBTime \ + FROM Times \ + INNER JOIN MapCourses ON Times.MapCourseID=MapCourses.MapCourseID \ + INNER JOIN Players ON Times.SteamID32=Players.SteamID32 \ + WHERE Players.Cheater=0 AND MapCourses.MapID=%d \ + AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0 \ + GROUP BY Times.SteamID32) AS PBTimes"; + +char sql_getrecentrecords[] = "\ +SELECT Maps.Name, MapCourses.Course, MapCourses.MapCourseID, Players.Alias, a.RunTime \ + FROM Times AS a \ + INNER JOIN MapCourses ON a.MapCourseID=MapCourses.MapCourseID \ + INNER JOIN Maps ON MapCourses.MapID=Maps.MapID \ + INNER JOIN Players ON a.SteamID32=Players.SteamID32 \ + WHERE Players.Cheater=0 AND Maps.InRankedPool AND a.Mode=%d \ + AND NOT EXISTS \ + (SELECT * \ + FROM Times AS b \ + WHERE a.MapCourseID=b.MapCourseID AND a.Mode=b.Mode \ + AND a.Created>b.Created AND a.RunTime>b.RunTime) \ + ORDER BY a.TimeID DESC \ + LIMIT %d"; + +char sql_getrecentrecords_pro[] = "\ +SELECT Maps.Name, MapCourses.Course, MapCourses.MapCourseID, Players.Alias, a.RunTime \ + FROM Times AS a \ + INNER JOIN MapCourses ON a.MapCourseID=MapCourses.MapCourseID \ + INNER JOIN Maps ON MapCourses.MapID=Maps.MapID \ + INNER JOIN Players ON a.SteamID32=Players.SteamID32 \ + WHERE Players.Cheater=0 AND Maps.InRankedPool AND a.Mode=%d AND a.Teleports=0 \ + AND NOT EXISTS \ + (SELECT * \ + FROM Times AS b \ + WHERE b.Teleports=0 AND a.MapCourseID=b.MapCourseID AND a.Mode=b.Mode \ + AND a.Created>b.Created AND a.RunTime>b.RunTime) \ + ORDER BY a.TimeID DESC \ + LIMIT %d"; + + + +// =====[ JUMPSTATS ]===== + +char sql_jumpstats_gettop[] = "\ +SELECT j.JumpID, p.SteamID32, p.Alias, j.Block, j.Distance, j.Strafes, j.Sync, j.Pre, j.Max, j.Airtime \ + FROM \ + Jumpstats j \ + INNER JOIN \ + Players p ON \ + p.SteamID32=j.SteamID32 AND \ + p.Cheater = 0 \ + INNER JOIN \ + ( \ + SELECT j.SteamID32, j.JumpType, j.Mode, j.IsBlockJump, MAX(j.Distance) BestDistance \ + FROM \ + Jumpstats j \ + INNER JOIN \ + ( \ + SELECT SteamID32, MAX(Block) AS MaxBlockDist \ + FROM \ + Jumpstats \ + WHERE \ + JumpType = %d AND \ + Mode = %d AND \ + IsBlockJump = %d \ + GROUP BY SteamID32 \ + ) MaxBlock ON \ + j.SteamID32 = MaxBlock.SteamID32 AND \ + j.Block = MaxBlock.MaxBlockDist \ + WHERE \ + j.JumpType = %d AND \ + j.Mode = %d AND \ + j.IsBlockJump = %d \ + GROUP BY j.SteamID32, j.JumpType, j.Mode, j.IsBlockJump \ + ) MaxDist ON \ + j.SteamID32 = MaxDist.SteamID32 AND \ + j.JumpType = MaxDist.JumpType AND \ + j.Mode = MaxDist.Mode AND \ + j.IsBlockJump = MaxDist.IsBlockJump AND \ + j.Distance = MaxDist.BestDistance \ + ORDER BY j.Block DESC, j.Distance DESC \ + LIMIT %d"; + +char sql_jumpstats_getrecord[] = "\ +SELECT JumpID, Distance, Block \ + FROM \ + Jumpstats rec \ + WHERE \ + SteamID32 = %d AND \ + JumpType = %d AND \ + Mode = %d AND \ + IsBlockJump = %d \ + ORDER BY Block DESC, Distance DESC"; + +char sql_jumpstats_getpbs[] = "\ +SELECT b.JumpID, b.JumpType, b.Distance, b.Strafes, b.Sync, b.Pre, b.Max, b.Airtime \ + FROM Jumpstats b \ + INNER JOIN ( \ + SELECT a.SteamID32, a.Mode, a.JumpType, MAX(a.Distance) Distance \ + FROM Jumpstats a \ + WHERE a.SteamID32=%d AND a.Mode=%d AND NOT a.IsBlockJump \ + GROUP BY a.JumpType, a.Mode, a.SteamID32 \ + ) a ON a.JumpType=b.JumpType AND a.Distance=b.Distance \ + WHERE a.SteamID32=b.SteamID32 AND a.Mode=b.Mode AND NOT b.IsBlockJump \ + ORDER BY b.JumpType"; + +char sql_jumpstats_getblockpbs[] = "\ +SELECT c.JumpID, c.JumpType, c.Block, c.Distance, c.Strafes, c.Sync, c.Pre, c.Max, c.Airtime \ + FROM Jumpstats c \ + INNER JOIN ( \ + SELECT a.SteamID32, a.Mode, a.JumpType, a.Block, MAX(b.Distance) Distance \ + FROM Jumpstats b \ + INNER JOIN ( \ + SELECT a.SteamID32, a.Mode, a.JumpType, MAX(a.Block) Block \ + FROM Jumpstats a \ + WHERE a.SteamID32=%d AND a.Mode=%d AND a.IsBlockJump \ + GROUP BY a.JumpType, a.Mode, a.SteamID32 \ + ) a ON a.JumpType=b.JumpType AND a.Block=b.Block \ + WHERE a.SteamID32=b.SteamID32 AND a.Mode=b.Mode AND b.IsBlockJump \ + GROUP BY a.JumpType, a.Mode, a.SteamID32, a.Block \ + ) b ON b.JumpType=c.JumpType AND b.Block=c.Block AND b.Distance=c.Distance \ + WHERE b.SteamID32=c.SteamID32 AND b.Mode=c.Mode AND c.IsBlockJump \ + ORDER BY c.JumpType"; diff --git a/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp b/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp new file mode 100644 index 0000000..ee9bd6d --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp @@ -0,0 +1,104 @@ +/* + Inserts a list of maps read from a file into the Maps table, + and updates them to be part of the ranked map pool. +*/ + + + +void DB_UpdateRankedMapPool(int client) +{ + File file = OpenFile(LR_CFG_MAP_POOL, "r"); + if (file == null) + { + LogError("Failed to load file: '%s'.", LR_CFG_MAP_POOL); + if (IsValidClient(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - Error"); + } + return; + } + + char map[256]; + int mapsCount = 0; + + Transaction txn = new Transaction(); + + // Reset all maps to be unranked + txn.AddQuery(sql_maps_reset_mappool); + + // Insert/Update maps in gokz-localranks-mappool.cfg to be ranked + while (file.ReadLine(map, sizeof(map))) + { + TrimString(map); + String_ToLower(map, map, sizeof(map)); + + // Ignore blank lines and comments + if (map[0] == '\0' || map[0] == ';' || (map[0] == '/' && map[1] == '/')) + { + continue; + } + + mapsCount++; + + switch (g_DBType) + { + case DatabaseType_SQLite: + { + char updateQuery[512]; + gH_DB.Format(updateQuery, sizeof(updateQuery), sqlite_maps_updateranked, 1, map); + + char insertQuery[512]; + gH_DB.Format(insertQuery, sizeof(insertQuery), sqlite_maps_insertranked, 1, map); + + txn.AddQuery(updateQuery); + txn.AddQuery(insertQuery); + } + case DatabaseType_MySQL: + { + char query[512]; + gH_DB.Format(query, sizeof(query), mysql_maps_upsertranked, 1, map); + + txn.AddQuery(query); + } + } + } + + delete file; + + if (mapsCount == 0) + { + LogError("No maps found in file: '%s'.", LR_CFG_MAP_POOL); + + if (IsValidClient(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - No Maps In File"); + GOKZ_PlayErrorSound(client); + } + + delete txn; + return; + } + + // Pass client user ID (or -1) as data + int data = -1; + if (IsValidClient(client)) + { + data = GetClientUserId(client); + } + + gH_DB.Execute(txn, DB_TxnSuccess_UpdateRankedMapPool, DB_TxnFailure_Generic, data); +} + +public void DB_TxnSuccess_UpdateRankedMapPool(Handle db, int userid, int numQueries, Handle[] results, any[] queryData) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client)) + { + LogMessage("The ranked map pool was updated by %L.", client); + GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - Success"); + } + else + { + LogMessage("The ranked map pool was updated."); + } +} diff --git a/sourcemod/scripting/gokz-localranks/misc.sp b/sourcemod/scripting/gokz-localranks/misc.sp new file mode 100644 index 0000000..0c6d96c --- /dev/null +++ b/sourcemod/scripting/gokz-localranks/misc.sp @@ -0,0 +1,319 @@ +/* + Miscellaneous functions. +*/ + + + +// =====[ COMPLETION MVP STARS ]===== + +void CompletionMVPStarsUpdate(int client) +{ + DB_GetCompletion(client, GetSteamAccountID(client), GOKZ_GetDefaultMode(), false); +} + +void CompletionMVPStarsUpdateAll() +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && !IsFakeClient(client)) + { + CompletionMVPStarsUpdate(client); + } + } +} + + + +// =====[ ANNOUNCEMENTS ]===== + +void PrecacheAnnouncementSounds() +{ + if (!LoadSounds()) + { + SetFailState("Failed to load file: \"%s\".", LR_CFG_SOUNDS); + } +} + +static bool LoadSounds() +{ + KeyValues kv = new KeyValues("sounds"); + if (!kv.ImportFromFile(LR_CFG_SOUNDS)) + { + return false; + } + + char downloadPath[256]; + + kv.GetString("beatrecord", gC_BeatRecordSound, sizeof(gC_BeatRecordSound)); + FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", gC_BeatRecordSound); + AddFileToDownloadsTable(downloadPath); + PrecacheSound(gC_BeatRecordSound, true); + + delete kv; + return true; +} + +static void PlayBeatRecordSound() +{ + GOKZ_EmitSoundToAll(gC_BeatRecordSound, _, "Server Record"); +} + +void AnnounceNewTime( + int client, + int course, + int mode, + float runTime, + int teleportsUsed, + bool firstTime, + float pbDiff, + int rank, + int maxRank, + bool firstTimePro, + float pbDiffPro, + int rankPro, + int maxRankPro) +{ + // Main Course + if (course == 0) + { + // Main Course PRO Times + if (teleportsUsed == 0) + { + if (firstTimePro) + { + GOKZ_PrintToChatAll(true, "%t", "New Time - First Time (PRO)", + client, GOKZ_FormatTime(runTime), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + else if (pbDiffPro < 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Time - Beat PB (PRO)", + client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiffPro)), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Time - Miss PB (PRO)", + client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiffPro), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + } + // Main Course NUB Times + else + { + if (firstTime) + { + GOKZ_PrintToChatAll(true, "%t", "New Time - First Time", + client, GOKZ_FormatTime(runTime), rank, maxRank, gC_ModeNamesShort[mode]); + } + else if (pbDiff < 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Time - Beat PB", + client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiff)), rank, maxRank, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Time - Miss PB", + client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiff), rank, maxRank, gC_ModeNamesShort[mode]); + } + } + } + // Bonus Course + else + { + // Bonus Course PRO Times + if (teleportsUsed == 0) + { + if (firstTimePro) + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - First Time (PRO)", + client, course, GOKZ_FormatTime(runTime), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + else if (pbDiffPro < 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Beat PB (PRO)", + client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiffPro)), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Miss PB (PRO)", + client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiffPro), rankPro, maxRankPro, gC_ModeNamesShort[mode]); + } + } + // Bonus Course NUB Times + else + { + if (firstTime) + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - First Time", + client, course, GOKZ_FormatTime(runTime), rank, maxRank, gC_ModeNamesShort[mode]); + } + else if (pbDiff < 0) + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Beat PB", + client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiff)), rank, maxRank, gC_ModeNamesShort[mode]); + } + else + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Miss PB", + client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiff), rank, maxRank, gC_ModeNamesShort[mode]); + } + } + } +} + +void AnnounceNewRecord(int client, int course, int mode, int recordType) +{ + if (course == 0) + { + switch (recordType) + { + case RecordType_Nub: + { + GOKZ_PrintToChatAll(true, "%t", "New Record (NUB)", client, gC_ModeNamesShort[mode]); + } + case RecordType_Pro: + { + GOKZ_PrintToChatAll(true, "%t", "New Record (PRO)", client, gC_ModeNamesShort[mode]); + } + case RecordType_NubAndPro: + { + GOKZ_PrintToChatAll(true, "%t", "New Record (NUB and PRO)", client, gC_ModeNamesShort[mode]); + } + } + } + else + { + switch (recordType) + { + case RecordType_Nub: + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]); + } + case RecordType_Pro: + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (PRO)", client, course, gC_ModeNamesShort[mode]); + } + case RecordType_NubAndPro: + { + GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (NUB and PRO)", client, course, course, gC_ModeNamesShort[mode]); + } + } + } + + PlayBeatRecordSound(); // Play sound! +} + + + +// =====[ MISSED RECORD TRACKING ]===== + +void ResetRecordMissed(int client) +{ + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + gB_RecordMissed[client][timeType] = false; + } +} + +void UpdateRecordMissed(int client) +{ + if (!GOKZ_GetTimerRunning(client) || gB_RecordMissed[client][TimeType_Nub] && gB_RecordMissed[client][TimeType_Pro]) + { + return; + } + + int course = GOKZ_GetCourse(client); + int mode = GOKZ_GetCoreOption(client, Option_Mode); + float currentTime = GOKZ_GetTime(client); + + bool nubRecordExists = gB_RecordExistsCache[course][mode][TimeType_Nub]; + float nubRecordTime = gF_RecordTimesCache[course][mode][TimeType_Nub]; + bool nubRecordMissed = gB_RecordMissed[client][TimeType_Nub]; + bool proRecordExists = gB_RecordExistsCache[course][mode][TimeType_Pro]; + float proRecordTime = gF_RecordTimesCache[course][mode][TimeType_Pro]; + bool proRecordMissed = gB_RecordMissed[client][TimeType_Pro]; + + if (nubRecordExists && !nubRecordMissed && currentTime >= nubRecordTime) + { + gB_RecordMissed[client][TimeType_Nub] = true; + + // Check if nub record is also the pro record, and call the forward appropriately + if (proRecordExists && FloatAbs(nubRecordTime - proRecordTime) < EPSILON) + { + gB_RecordMissed[client][TimeType_Pro] = true; + Call_OnRecordMissed(client, nubRecordTime, course, mode, Style_Normal, RecordType_NubAndPro); + } + else + { + Call_OnRecordMissed(client, nubRecordTime, course, mode, Style_Normal, RecordType_Nub); + } + } + else if (proRecordExists && !proRecordMissed && currentTime >= proRecordTime) + { + gB_RecordMissed[client][TimeType_Pro] = true; + Call_OnRecordMissed(client, proRecordTime, course, mode, Style_Normal, RecordType_Pro); + } +} + + + +// =====[ MISSED PB TRACKING ]===== + +#define MISSED_PB_SOUND "buttons/button18.wav" + +void ResetPBMissed(int client) +{ + for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++) + { + gB_PBMissed[client][timeType] = false; + } +} + +void UpdatePBMissed(int client) +{ + if (!GOKZ_GetTimerRunning(client) || gB_PBMissed[client][TimeType_Nub] && gB_PBMissed[client][TimeType_Pro]) + { + return; + } + + int course = GOKZ_GetCourse(client); + int mode = GOKZ_GetCoreOption(client, Option_Mode); + float currentTime = GOKZ_GetTime(client); + + bool nubPBExists = gB_PBExistsCache[client][course][mode][TimeType_Nub]; + float nubPBTime = gF_PBTimesCache[client][course][mode][TimeType_Nub]; + bool nubPBMissed = gB_PBMissed[client][TimeType_Nub]; + bool proPBExists = gB_PBExistsCache[client][course][mode][TimeType_Pro]; + float proPBTime = gF_PBTimesCache[client][course][mode][TimeType_Pro]; + bool proPBMissed = gB_PBMissed[client][TimeType_Pro]; + + if (nubPBExists && !nubPBMissed && currentTime >= nubPBTime) + { + gB_PBMissed[client][TimeType_Nub] = true; + + // Check if nub PB is also the pro PB, and call the forward appropriately + if (proPBExists && FloatAbs(nubPBTime - proPBTime) < EPSILON) + { + gB_PBMissed[client][TimeType_Pro] = true; + Call_OnPBMissed(client, nubPBTime, course, mode, Style_Normal, RecordType_NubAndPro); + } + else + { + Call_OnPBMissed(client, nubPBTime, course, mode, Style_Normal, RecordType_Nub); + } + } + else if (proPBExists && !proPBMissed && currentTime >= proPBTime) + { + gB_PBMissed[client][TimeType_Pro] = true; + Call_OnPBMissed(client, proPBTime, course, mode, Style_Normal, RecordType_Pro); + } +} + +void DoPBMissedReport(int client, float pbTime, int recordType) +{ + switch (recordType) + { + case RecordType_Nub:GOKZ_PrintToChat(client, true, "%t", "Missed PB (NUB)", GOKZ_FormatTime(pbTime)); + case RecordType_Pro:GOKZ_PrintToChat(client, true, "%t", "Missed PB (PRO)", GOKZ_FormatTime(pbTime)); + case RecordType_NubAndPro:GOKZ_PrintToChat(client, true, "%t", "Missed PB (NUB and PRO)", GOKZ_FormatTime(pbTime)); + } + GOKZ_EmitSoundToClient(client, MISSED_PB_SOUND, _, "Missed PB"); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-measure.sp b/sourcemod/scripting/gokz-measure.sp new file mode 100644 index 0000000..2360625 --- /dev/null +++ b/sourcemod/scripting/gokz-measure.sp @@ -0,0 +1,82 @@ +#include <sourcemod> + +#include <sdktools> + +#include <gokz/core> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + +/* + Lets players measure the distance between two points. + Credits to DaFox (https://forums.alliedmods.net/showthread.php?t=88830?t=88830) +*/ + + +public Plugin myinfo = +{ + name = "GOKZ Measure", + author = "DanZay", + description = "Provides tools for measuring things", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-measure.txt" +#define MEASURE_MIN_DIST 0.01 +int gI_BeamModel; +bool gB_Measuring[MAXPLAYERS + 1]; +bool gB_MeasurePosSet[MAXPLAYERS + 1][2]; +float gF_MeasurePos[MAXPLAYERS + 1][2][3]; +float gF_MeasureNormal[MAXPLAYERS + 1][2][3]; +Handle gH_P2PRed[MAXPLAYERS + 1]; +Handle gH_P2PGreen[MAXPLAYERS + 1]; + +#include "gokz-measure/measurer.sp" +#include "gokz-measure/commands.sp" +#include "gokz-measure/measure_menu.sp" + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-measure"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-measure.phrases"); + + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + gI_BeamModel = PrecacheModel("materials/sprites/laserbeam.vmt", true); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-measure/commands.sp b/sourcemod/scripting/gokz-measure/commands.sp new file mode 100644 index 0000000..5fe3028 --- /dev/null +++ b/sourcemod/scripting/gokz-measure/commands.sp @@ -0,0 +1,49 @@ +void RegisterCommands() +{ + RegConsoleCmd("+measure", CommandMeasureStart, "[KZ] Set the measure origin."); + RegConsoleCmd("-measure", CommandMeasureEnd, "[KZ] Set the measure origin."); + RegConsoleCmd("sm_measure", CommandMeasureMenu, "[KZ] Open the measurement menu."); + RegConsoleCmd("sm_measuremenu", CommandMeasureMenu, "[KZ] Open the measurement menu."); + RegConsoleCmd("sm_measureblock", CommandMeasureBlock, "[KZ] Measure the block distance."); +} + +public Action CommandMeasureMenu(int client, int args) +{ + DisplayMeasureMenu(client); + return Plugin_Handled; +} + +public Action CommandMeasureStart(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + gB_Measuring[client] = true; + MeasureGetPos(client, 0); + return Plugin_Handled; +} + +public Action CommandMeasureEnd(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + gB_Measuring[client] = false; + MeasureGetPos(client, 1); + MeasureDistance(client, MEASURE_MIN_DIST); + CreateTimer(4.9, Timer_DeletePoints, GetClientUserId(client)); + return Plugin_Handled; +} + +public Action CommandMeasureBlock(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + MeasureBlock(client); + CreateTimer(4.9, Timer_DeletePoints, GetClientUserId(client)); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-measure/measure_menu.sp b/sourcemod/scripting/gokz-measure/measure_menu.sp new file mode 100644 index 0000000..cf9deb3 --- /dev/null +++ b/sourcemod/scripting/gokz-measure/measure_menu.sp @@ -0,0 +1,82 @@ +#define ITEM_INFO_POINT_A "a" +#define ITEM_INFO_POINT_B "b" +#define ITEM_INFO_GET_DISTANCE "get" +#define ITEM_INFO_GET_BLOCK_DISTANCE "block" + +// =====[ PUBLIC ]===== + +void DisplayMeasureMenu(int client, bool reset = true) +{ + if (reset) + { + MeasureResetPos(client); + } + + Menu menu = new Menu(MenuHandler_Measure); + menu.SetTitle("%T", "Measure Menu - Title", client); + MeasureMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ EVENTS ]===== + +public int MenuHandler_Measure(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + + if (StrEqual(info, ITEM_INFO_POINT_A, false)) + { + MeasureGetPos(param1, 0); + } + else if (StrEqual(info, ITEM_INFO_POINT_B, false)) + { + MeasureGetPos(param1, 1); + } + else if (StrEqual(info, ITEM_INFO_GET_DISTANCE, false)) + { + MeasureDistance(param1); + } + else if (StrEqual(info, ITEM_INFO_GET_BLOCK_DISTANCE, false)) + { + if (!MeasureBlock(param1)) + { + DisplayMeasureMenu(param1, false); + } + } + + DisplayMeasureMenu(param1, false); + } + else if (action == MenuAction_Cancel) + { + MeasureResetPos(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +static void MeasureMenuAddItems(int client, Menu menu) +{ + char display[32]; + + FormatEx(display, sizeof(display), "%T", "Measure Menu - Point A", client); + menu.AddItem(ITEM_INFO_POINT_A, display); + FormatEx(display, sizeof(display), "%T", "Measure Menu - Point B", client); + menu.AddItem(ITEM_INFO_POINT_B, display); + FormatEx(display, sizeof(display), "%T\n ", "Measure Menu - Get Distance", client); + menu.AddItem(ITEM_INFO_GET_DISTANCE, display); + FormatEx(display, sizeof(display), "%T", "Measure Menu - Get Block Distance", client); + menu.AddItem(ITEM_INFO_GET_BLOCK_DISTANCE, display); +} + diff --git a/sourcemod/scripting/gokz-measure/measurer.sp b/sourcemod/scripting/gokz-measure/measurer.sp new file mode 100644 index 0000000..f88e79c --- /dev/null +++ b/sourcemod/scripting/gokz-measure/measurer.sp @@ -0,0 +1,231 @@ +// =====[ PUBLIC ]===== + +void MeasureGetPos(int client, int arg) +{ + float origin[3]; + float angles[3]; + + GetClientEyePosition(client, origin); + GetClientEyeAngles(client, angles); + + MeasureGetPosEx(client, arg, origin, angles); +} + +void MeasureResetPos(int client) +{ + delete gH_P2PRed[client]; + delete gH_P2PGreen[client]; + + gB_MeasurePosSet[client][0] = false; + gB_MeasurePosSet[client][1] = false; + + gF_MeasurePos[client][0][0] = 0.0; // This is stupid. + gF_MeasurePos[client][0][1] = 0.0; + gF_MeasurePos[client][0][2] = 0.0; + gF_MeasurePos[client][1][0] = 0.0; + gF_MeasurePos[client][1][1] = 0.0; + gF_MeasurePos[client][1][2] = 0.0; +} + +bool MeasureBlock(int client) +{ + float angles[3]; + MeasureGetPos(client, 0); + GetVectorAngles(gF_MeasureNormal[client][0], angles); + MeasureGetPosEx(client, 1, gF_MeasurePos[client][0], angles); + AddVectors(gF_MeasureNormal[client][0], gF_MeasureNormal[client][1], angles); + if (GetVectorLength(angles, true) > EPSILON || + FloatAbs(gF_MeasureNormal[client][0][2]) > EPSILON || + FloatAbs(gF_MeasureNormal[client][1][2]) > EPSILON) + { + GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Blocks not aligned)"); + GOKZ_PlayErrorSound(client); + return false; + } + GOKZ_PrintToChat(client, true, "%t", "Block Measure Result", RoundFloat(GetVectorHorizontalDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1]))); + MeasureBeam(client, gF_MeasurePos[client][0], gF_MeasurePos[client][1], 5.0, 0.2, 200, 200, 200); + return true; +} + +bool MeasureDistance(int client, float minDistToMeasureBlock = -1.0) +{ + // Find Distance + if (gB_MeasurePosSet[client][0] && gB_MeasurePosSet[client][1]) + { + float horizontalDist = GetVectorHorizontalDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1]); + float effectiveDist = CalcEffectiveDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1]); + float verticalDist = gF_MeasurePos[client][1][2] - gF_MeasurePos[client][0][2]; + if (minDistToMeasureBlock >= 0.0 && (horizontalDist <= minDistToMeasureBlock && verticalDist <= minDistToMeasureBlock)) + { + return MeasureBlock(client); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Measure Result", horizontalDist, effectiveDist, verticalDist); + MeasureBeam(client, gF_MeasurePos[client][0], gF_MeasurePos[client][1], 5.0, 0.2, 200, 200, 200); + } + return true; + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Points Not Set)"); + GOKZ_PlayErrorSound(client); + return false; + } +} + +// =====[ TIMERS ]===== + +public Action Timer_P2PRed(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client)) + { + P2PXBeam(client, 0); + } + return Plugin_Continue; +} + +public Action Timer_P2PGreen(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client)) + { + P2PXBeam(client, 1); + } + return Plugin_Continue; +} + +public Action Timer_DeletePoints(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (!gB_Measuring[client]) + { + MeasureResetPos(client); + } + return Plugin_Continue; +} + + +// =====[ PRIVATES ]===== +static void P2PXBeam(int client, int arg) +{ + float Origin0[3]; + float Origin1[3]; + float Origin2[3]; + float Origin3[3]; + + Origin0[0] = (gF_MeasurePos[client][arg][0] + 8.0); + Origin0[1] = (gF_MeasurePos[client][arg][1] + 8.0); + Origin0[2] = gF_MeasurePos[client][arg][2]; + + Origin1[0] = (gF_MeasurePos[client][arg][0] - 8.0); + Origin1[1] = (gF_MeasurePos[client][arg][1] - 8.0); + Origin1[2] = gF_MeasurePos[client][arg][2]; + + Origin2[0] = (gF_MeasurePos[client][arg][0] + 8.0); + Origin2[1] = (gF_MeasurePos[client][arg][1] - 8.0); + Origin2[2] = gF_MeasurePos[client][arg][2]; + + Origin3[0] = (gF_MeasurePos[client][arg][0] - 8.0); + Origin3[1] = (gF_MeasurePos[client][arg][1] + 8.0); + Origin3[2] = gF_MeasurePos[client][arg][2]; + + if (arg == 0) + { + MeasureBeam(client, Origin0, Origin1, 0.97, 0.2, 0, 255, 0); + MeasureBeam(client, Origin2, Origin3, 0.97, 0.2, 0, 255, 0); + } + else + { + MeasureBeam(client, Origin0, Origin1, 0.97, 0.2, 255, 0, 0); + MeasureBeam(client, Origin2, Origin3, 0.97, 0.2, 255, 0, 0); + } +} + +static void MeasureGetPosEx(int client, int arg, float origin[3], float angles[3]) +{ + Handle trace = TR_TraceRayFilterEx(origin, angles, MASK_PLAYERSOLID, RayType_Infinite, TraceEntityFilterPlayers, client); + + if (!TR_DidHit(trace)) + { + delete trace; + GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Not Aiming at Solid)"); + GOKZ_PlayErrorSound(client); + return; + } + + TR_GetEndPosition(gF_MeasurePos[client][arg], trace); + TR_GetPlaneNormal(trace, gF_MeasureNormal[client][arg]); + delete trace; + + if (arg == 0) + { + delete gH_P2PRed[client]; + gB_MeasurePosSet[client][0] = true; + gH_P2PRed[client] = CreateTimer(1.0, Timer_P2PRed, GetClientUserId(client), TIMER_REPEAT); + P2PXBeam(client, 0); + } + else + { + delete gH_P2PGreen[client]; + gH_P2PGreen[client] = null; + gB_MeasurePosSet[client][1] = true; + P2PXBeam(client, 1); + gH_P2PGreen[client] = CreateTimer(1.0, Timer_P2PGreen, GetClientUserId(client), TIMER_REPEAT); + } +} + +static void MeasureBeam(int client, float vecStart[3], float vecEnd[3], float life, float width, int r, int g, int b) +{ + TE_Start("BeamPoints"); + TE_WriteNum("m_nModelIndex", gI_BeamModel); + TE_WriteNum("m_nHaloIndex", 0); + TE_WriteNum("m_nStartFrame", 0); + TE_WriteNum("m_nFrameRate", 0); + TE_WriteFloat("m_fLife", life); + TE_WriteFloat("m_fWidth", width); + TE_WriteFloat("m_fEndWidth", width); + TE_WriteNum("m_nFadeLength", 0); + TE_WriteFloat("m_fAmplitude", 0.0); + TE_WriteNum("m_nSpeed", 0); + TE_WriteNum("r", r); + TE_WriteNum("g", g); + TE_WriteNum("b", b); + TE_WriteNum("a", 255); + TE_WriteNum("m_nFlags", 0); + TE_WriteVector("m_vecStartPoint", vecStart); + TE_WriteVector("m_vecEndPoint", vecEnd); + TE_SendToClient(client); +} + +// Calculates the minimum equivalent jumpstat distance to go between the two points +static float CalcEffectiveDistance(const float pointA[3], const float pointB[3]) +{ + float Ax = FloatMin(pointA[0], pointB[0]); + float Bx = FloatMax(pointA[0], pointB[0]); + float Ay = FloatMin(pointA[1], pointB[1]); + float By = FloatMax(pointA[1], pointB[1]); + + if (Bx - Ax < 32.0) + { + Ax = Bx; + } + else + { + Ax = Ax + 16.0; + Bx = Bx - 16.0; + } + + if (By - Ay < 32.0) + { + Ay = By; + } + else + { + Ay = Ay + 16.0; + By = By - 16.0; + } + + return SquareRoot(Pow(Ax - Bx, 2.0) + Pow(Ay - By, 2.0)) + 32.0; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-mode-kztimer.sp b/sourcemod/scripting/gokz-mode-kztimer.sp new file mode 100644 index 0000000..272652b --- /dev/null +++ b/sourcemod/scripting/gokz-mode-kztimer.sp @@ -0,0 +1,709 @@ +#include <sourcemod> + +#include <sdkhooks> +#include <sdktools> +#include <dhooks> + +#include <movementapi> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/core> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Mode - KZTimer", + author = "DanZay", + description = "KZTimer mode for GOKZ", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-kztimer.txt" + +#define MODE_VERSION 217 +#define DUCK_SPEED_NORMAL 8.0 +#define PRE_VELMOD_MAX 1.104 // Calculated 276/250 +#define PERF_SPEED_CAP 380.0 + +float gF_ModeCVarValues[MODECVAR_COUNT] = +{ + 6.5, // sv_accelerate + 0.0, // sv_accelerate_use_weapon_speed + 100.0, // sv_airaccelerate + 30.0, // sv_air_max_wishspeed + 1.0, // sv_enablebunnyhopping + 5.0, // sv_friction + 800.0, // sv_gravity + 301.993377, // sv_jump_impulse + 1.0, // sv_ladder_scale_speed + 0.0, // sv_ledge_mantle_helper + 320.0, // sv_maxspeed + 2000.0, // sv_maxvelocity + 0.0, // sv_staminajumpcost + 0.0, // sv_staminalandcost + 0.0, // sv_staminamax + 0.0, // sv_staminarecoveryrate + 0.7, // sv_standable_normal + 0.0, // sv_timebetweenducks + 0.7, // sv_walkable_normal + 10.0, // sv_wateraccelerate + 0.8, // sv_water_movespeed_multiplier + 0.0, // sv_water_swim_mode + 0.0, // sv_weapon_encumbrance_per_item + 0.0 // sv_weapon_encumbrance_scale +}; + +bool gB_GOKZCore; +ConVar gCV_ModeCVar[MODECVAR_COUNT]; +float gF_PreVelMod[MAXPLAYERS + 1]; +float gF_PreVelModLastChange[MAXPLAYERS + 1]; +float gF_RealPreVelMod[MAXPLAYERS + 1]; +int gI_PreTickCounter[MAXPLAYERS + 1]; +Handle gH_GetPlayerMaxSpeed; +DynamicDetour gH_CanUnduck; +int gI_TickCount[MAXPLAYERS + 1]; +DynamicDetour gH_AirAccelerate; +int gI_OldButtons[MAXPLAYERS + 1]; +int gI_OldFlags[MAXPLAYERS + 1]; +bool gB_OldOnGround[MAXPLAYERS + 1]; +float gF_OldVelocity[MAXPLAYERS + 1][3]; +bool gB_Jumpbugged[MAXPLAYERS + 1]; +int gI_OffsetCGameMovement_player; + + + +// =====[ PLUGIN EVENTS ]===== + +public void OnPluginStart() +{ + if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON) + { + SetFailState("gokz-mode-kztimer only supports 128 tickrate servers."); + } + HookEvents(); + CreateConVars(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + if (LibraryExists("gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_KZTimer, true, MODE_VERSION); + } + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnPluginEnd() +{ + if (gB_GOKZCore) + { + GOKZ_SetModeLoaded(Mode_KZTimer, false); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + else if (StrEqual(name, "gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_KZTimer, true, MODE_VERSION); + } +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + if (IsValidClient(client)) + { + HookClientEvents(client); + } + if (IsUsingMode(client)) + { + ReplicateConVars(client); + } +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + RemoveCrouchJumpBind(player, buttons); + gF_RealPreVelMod[player.ID] = CalcPrestrafeVelMod(player); + ReduceDuckSlowdown(player); + FixWaterBoost(player, buttons); + FixDisplacementStuck(player); + + gB_Jumpbugged[player.ID] = false; + gI_OldButtons[player.ID] = buttons; + gI_OldFlags[player.ID] = GetEntityFlags(client); + gB_OldOnGround[player.ID] = Movement_GetOnGround(client); + gI_TickCount[player.ID] = tickcount; + Movement_GetVelocity(client, gF_OldVelocity[client]); + return Plugin_Continue; +} + +public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return MRES_Ignored; + } + + DHookSetReturn(hReturn, SPEED_NORMAL * gF_RealPreVelMod[client]); + return MRES_Supercede; +} + +public MRESReturn DHooks_OnAirAccelerate_Pre(Address pThis, DHookParam hParams) +{ + int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player); + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return MRES_Ignored; + } + + // NOTE: Prestrafing changes GetPlayerMaxSpeed, which changes + // air acceleration, so remove gF_PreVelMod[client] from wishspeed/maxspeed. + // This also applies to when the player is ducked: their wishspeed is + // 85 and with prestrafing can be ~93. + float wishspeed = DHookGetParam(hParams, 2); + if (gF_PreVelMod[client] > 1.0) + { + DHookSetParam(hParams, 2, wishspeed / gF_PreVelMod[client]); + return MRES_ChangedHandled; + } + + return MRES_Ignored; +} + +public MRESReturn DHooks_OnCanUnduck_Pre(Address pThis, DHookReturn hReturn) +{ + int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player); + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return MRES_Ignored; + } + // Just landed fully ducked, you can't unduck. + if (Movement_GetLandingTick(client) == (gI_TickCount[client] - 1) && GetEntPropFloat(client, Prop_Send, "m_flDuckAmount") >= 1.0 && GetEntProp(client, Prop_Send, "m_bDucked")) + { + hReturn.Value = false; + return MRES_Supercede; + } + return MRES_Ignored; +} + +public void SDKHook_OnClientPreThink_Post(int client) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return; + } + + // Don't tweak convars if GOKZ isn't running + if (gB_GOKZCore) + { + TweakConVars(); + } +} + +public Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + return SlopeFix(client, origin, velocity); +} + +public Action Movement_OnJumpPre(int client, float origin[3], float velocity[3]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + return TweakJump(player, velocity); +} + +public Action Movement_OnJumpPost(int client) +{ + if (!IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore) + { + player.GOKZHitPerf = player.HitPerf; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } + return Plugin_Continue; +} + +public void Movement_OnStopTouchGround(int client) +{ + if (!IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore) + { + player.GOKZHitPerf = player.HitPerf; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } +} + +public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore && newMovetype == MOVETYPE_WALK) + { + player.GOKZHitPerf = false; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_KZTimer) + { + ReplicateConVars(client); + } +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + KZPlayer player = KZPlayer(client); + ResetPrestrafeVelMod(player); +} + + + +// =====[ GENERAL ]===== + +bool IsUsingMode(int client) +{ + // If GOKZ core isn't loaded, then apply mode at all times + return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_KZTimer; +} + +void HookEvents() +{ + GameData gameData = LoadGameConfigFile("movementapi.games"); + if (gameData == INVALID_HANDLE) + { + SetFailState("Failed to find movementapi.games config"); + } + + int offset = gameData.GetOffset("GetPlayerMaxSpeed"); + if (offset == -1) + { + SetFailState("Failed to get GetPlayerMaxSpeed offset"); + } + gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed); + + gH_AirAccelerate = DynamicDetour.FromConf(gameData, "CGameMovement::AirAccelerate"); + if (gH_AirAccelerate == INVALID_HANDLE) + { + SetFailState("Failed to find CGameMovement::AirAccelerate function signature"); + } + + if (!gH_AirAccelerate.Enable(Hook_Pre, DHooks_OnAirAccelerate_Pre)) + { + SetFailState("Failed to enable detour on CGameMovement::AirAccelerate"); + } + + char buffer[16]; + if (!gameData.GetKeyValue("CGameMovement::player", buffer, sizeof(buffer))) + { + SetFailState("Failed to get CGameMovement::player offset."); + } + gI_OffsetCGameMovement_player = StringToInt(buffer); + + gameData = LoadGameConfigFile("gokz-core.games"); + gH_CanUnduck = DynamicDetour.FromConf(gameData, "CCSGameMovement::CanUnduck"); + if (gH_CanUnduck == INVALID_HANDLE) + { + SetFailState("Failed to find CCSGameMovement::CanUnduck function signature"); + } + + if (!gH_CanUnduck.Enable(Hook_Pre, DHooks_OnCanUnduck_Pre)) + { + SetFailState("Failed to enable detour on CCSGameMovement::CanUnduck"); + } + delete gameData; +} + +// =====[ CONVARS ]===== + +void CreateConVars() +{ + for (int cvar = 0; cvar < MODECVAR_COUNT; cvar++) + { + gCV_ModeCVar[cvar] = FindConVar(gC_ModeCVars[cvar]); + } +} + +void TweakConVars() +{ + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i]; + } +} + +void ReplicateConVars(int client) +{ + // Replicate convars only when player changes mode in GOKZ + // so that lagg isn't caused by other players using other + // modes, and also as an optimisation. + + if (IsFakeClient(client)) + { + return; + } + + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i])); + } +} + + + +// =====[ VELOCITY MODIFIER ]===== + +void HookClientEvents(int client) +{ + DHookEntity(gH_GetPlayerMaxSpeed, true, client); + SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post); +} + +// Adapted from KZTimerGlobal +float CalcPrestrafeVelMod(KZPlayer player) +{ + if (!player.OnGround) + { + return gF_PreVelMod[player.ID]; + } + + if (!player.Turning) + { + if (GetEngineTime() - gF_PreVelModLastChange[player.ID] > 0.2) + { + gF_PreVelMod[player.ID] = 1.0; + gF_PreVelModLastChange[player.ID] = GetEngineTime(); + } + else if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX + 0.007) + { + return PRE_VELMOD_MAX - 0.001; // Returning without setting the variable is intentional + } + } + else if ((player.Buttons & IN_MOVELEFT || player.Buttons & IN_MOVERIGHT) && player.Speed > 248.9) + { + float increment = 0.0009; + if (gF_PreVelMod[player.ID] > 1.04) + { + increment = 0.001; + } + + bool forwards = GetClientMovingDirection(player.ID, false) > 0.0; + + if ((player.Buttons & IN_MOVERIGHT && player.TurningRight || player.TurningLeft && !forwards) + || (player.Buttons & IN_MOVELEFT && player.TurningLeft || player.TurningRight && !forwards)) + { + gI_PreTickCounter[player.ID]++; + + if (gI_PreTickCounter[player.ID] < 75) + { + gF_PreVelMod[player.ID] += increment; + if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX) + { + if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX + 0.007) + { + gF_PreVelMod[player.ID] = PRE_VELMOD_MAX - 0.001; + } + else + { + gF_PreVelMod[player.ID] -= 0.007; + } + } + gF_PreVelMod[player.ID] += increment; + } + else + { + gF_PreVelMod[player.ID] -= 0.0045; + gI_PreTickCounter[player.ID] -= 2; + + if (gF_PreVelMod[player.ID] < 1.0) + { + gF_PreVelMod[player.ID] = 1.0; + gI_PreTickCounter[player.ID] = 0; + } + } + } + else + { + gF_PreVelMod[player.ID] -= 0.04; + + if (gF_PreVelMod[player.ID] < 1.0) + { + gF_PreVelMod[player.ID] = 1.0; + } + } + + gF_PreVelModLastChange[player.ID] = GetEngineTime(); + } + else + { + gI_PreTickCounter[player.ID] = 0; + return 1.0; // Returning without setting the variable is intentional + } + + return gF_PreVelMod[player.ID]; +} + +// Adapted from KZTimerGlobal +float GetClientMovingDirection(int client, bool ladder) +{ + float fVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", fVelocity); + + float fEyeAngles[3]; + GetClientEyeAngles(client, fEyeAngles); + + if (fEyeAngles[0] > 70.0)fEyeAngles[0] = 70.0; + if (fEyeAngles[0] < -70.0)fEyeAngles[0] = -70.0; + + float fViewDirection[3]; + + if (ladder) + { + GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", fViewDirection); + } + else + { + GetAngleVectors(fEyeAngles, fViewDirection, NULL_VECTOR, NULL_VECTOR); + } + + NormalizeVector(fVelocity, fVelocity); + NormalizeVector(fViewDirection, fViewDirection); + + float direction = GetVectorDotProduct(fVelocity, fViewDirection); + if (ladder) + { + direction = direction * -1; + } + return direction; +} + +void ResetPrestrafeVelMod(KZPlayer player) +{ + gF_PreVelMod[player.ID] = 1.0; + gI_PreTickCounter[player.ID] = 0; +} + + + +// =====[ SLOPEFIX ]===== + +// ORIGINAL AUTHORS : Mev & Blacky +// URL : https://forums.alliedmods.net/showthread.php?p=2322788 +// NOTE : Modified by DanZay for this plugin + +Action SlopeFix(int client, float origin[3], float velocity[3]) +{ + KZPlayer player = KZPlayer(client); + // Check if player landed on the ground + if (Movement_GetOnGround(client) && !gB_OldOnGround[client]) + { + float vMins[] = {-16.0, -16.0, 0.0}; + // Always use ducked hull as the real hull size isn't updated yet. + // Might cause slight issues in extremely rare scenarios. + float vMaxs[] = {16.0, 16.0, 54.0}; + + float vEndPos[3]; + vEndPos[0] = origin[0]; + vEndPos[1] = origin[1]; + vEndPos[2] = origin[2] - gF_ModeCVarValues[ModeCVar_MaxVelocity]; + + // Set up and do tracehull to find out if the player landed on a slope + TR_TraceHullFilter(origin, vEndPos, vMins, vMaxs, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitSelf, client); + + if (TR_DidHit()) + { + // Gets the normal vector of the surface under the player + float vPlane[3], vLast[3]; + player.GetLandingVelocity(vLast); + TR_GetPlaneNormal(null, vPlane); + + // Make sure it's not flat ground and not a surf ramp (1.0 = flat ground, < 0.7 = surf ramp) + if (0.7 <= vPlane[2] < 1.0) + { + /* + Copy the ClipVelocity function from sdk2013 + (https://mxr.alliedmods.net/hl2sdk-sdk2013/source/game/shared/gamemovement.cpp#3145) + With some minor changes to make it actually work + */ + + float fBackOff = GetVectorDotProduct(vLast, vPlane); + + float change, vVel[3]; + for (int i; i < 2; i++) + { + change = vPlane[i] * fBackOff; + vVel[i] = vLast[i] - change; + } + + float fAdjust = GetVectorDotProduct(vVel, vPlane); + if (fAdjust < 0.0) + { + for (int i; i < 2; i++) + { + vVel[i] -= (vPlane[i] * fAdjust); + } + } + + vVel[2] = 0.0; + vLast[2] = 0.0; + + // Make sure the player is going down a ramp by checking if they actually will gain speed from the boost + if (GetVectorLength(vVel) > GetVectorLength(vLast)) + { + CopyVector(vVel, velocity); + player.SetLandingVelocity(velocity); + return Plugin_Changed; + } + } + } + } + return Plugin_Continue; +} + +public bool TraceRayDontHitSelf(int entity, int mask, any data) +{ + return entity != data && !(0 < entity <= MaxClients); +} + + + +// =====[ JUMPING ]===== + +Action TweakJump(KZPlayer player, float velocity[3]) +{ + if (player.HitPerf) + { + if (GetVectorHorizontalLength(velocity) > PERF_SPEED_CAP) + { + SetVectorHorizontalLength(velocity, PERF_SPEED_CAP); + return Plugin_Changed; + } + } + return Plugin_Continue; +} +// =====[ OTHER ]===== + +void FixWaterBoost(KZPlayer player, int buttons) +{ + if (GetEntProp(player.ID, Prop_Send, "m_nWaterLevel") >= 2) // WL_Waist = 2 + { + // If duck is being pressed and we're not already ducking or on ground + if (GetEntityFlags(player.ID) & (FL_DUCKING | FL_ONGROUND) == 0 + && buttons & IN_DUCK && ~gI_OldButtons[player.ID] & IN_DUCK) + { + float newOrigin[3]; + Movement_GetOrigin(player.ID, newOrigin); + newOrigin[2] += 9.0; + + TR_TraceHullFilter(newOrigin, newOrigin, view_as<float>({-16.0, -16.0, 0.0}), view_as<float>({16.0, 16.0, 54.0}), MASK_PLAYERSOLID, TraceEntityFilterPlayers); + if (!TR_DidHit()) + { + TeleportEntity(player.ID, newOrigin, NULL_VECTOR, NULL_VECTOR); + } + } + } +} + +void FixDisplacementStuck(KZPlayer player) +{ + int flags = GetEntityFlags(player.ID); + bool unducked = ~flags & FL_DUCKING && gI_OldFlags[player.ID] & FL_DUCKING; + + float standingMins[] = {-16.0, -16.0, 0.0}; + float standingMaxs[] = {16.0, 16.0, 72.0}; + + if (unducked) + { + // check if we're stuck after unducking and if we're stuck then force duck + float origin[3]; + Movement_GetOrigin(player.ID, origin); + TR_TraceHullFilter(origin, origin, standingMins, standingMaxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + + if (TR_DidHit()) + { + player.SetVelocity(gF_OldVelocity[player.ID]); + SetEntProp(player.ID, Prop_Send, "m_bDucking", true); + } + } +} + +void RemoveCrouchJumpBind(KZPlayer player, int &buttons) +{ + if (player.OnGround && buttons & IN_JUMP && !(gI_OldButtons[player.ID] & IN_JUMP) && !(gI_OldButtons[player.ID] & IN_DUCK)) + { + buttons &= ~IN_DUCK; + } +} + +void ReduceDuckSlowdown(KZPlayer player) +{ + if (GetEntProp(player.ID, Prop_Data, "m_afButtonReleased") & IN_DUCK) + { + Movement_SetDuckSpeed(player.ID, DUCK_SPEED_NORMAL); + } +} diff --git a/sourcemod/scripting/gokz-mode-simplekz.sp b/sourcemod/scripting/gokz-mode-simplekz.sp new file mode 100644 index 0000000..99d76ac --- /dev/null +++ b/sourcemod/scripting/gokz-mode-simplekz.sp @@ -0,0 +1,846 @@ +#include <sourcemod> + +#include <sdkhooks> +#include <sdktools> +#include <dhooks> + +#include <movementapi> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/core> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Mode - SimpleKZ", + author = "DanZay", + description = "SimpleKZ mode for GOKZ", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-simplekz.txt" + +#define MODE_VERSION 21 +#define PS_MAX_REWARD_TURN_RATE 0.703125 // Degrees per tick (90 degrees per second) +#define PS_MAX_TURN_RATE_DECREMENT 0.015625 // Degrees per tick (2 degrees per second) +#define PS_SPEED_MAX 26.54321 // Units +#define PS_SPEED_INCREMENT 0.35 // Units per tick +#define PS_SPEED_DECREMENT_MIDAIR 0.2824 // Units per tick (lose PS_SPEED_MAX in 0 offset jump i.e. 94 ticks) +#define PS_GRACE_TICKS 3 // No. of ticks allowed to fail prestrafe checks when prestrafing - helps players with low fps +#define DUCK_SPEED_NORMAL 8.0 +#define DUCK_SPEED_MINIMUM 6.0234375 // Equal to if you just ducked/unducked for the first time in a while + +float gF_ModeCVarValues[MODECVAR_COUNT] = +{ + 6.5, // sv_accelerate + 0.0, // sv_accelerate_use_weapon_speed + 100.0, // sv_airaccelerate + 30.0, // sv_air_max_wishspeed + 1.0, // sv_enablebunnyhopping + 5.2, // sv_friction + 800.0, // sv_gravity + 301.993377, // sv_jump_impulse + 1.0, // sv_ladder_scale_speed + 0.0, // sv_ledge_mantle_helper + 320.0, // sv_maxspeed + 3500.0, // sv_maxvelocity + 0.0, // sv_staminajumpcost + 0.0, // sv_staminalandcost + 0.0, // sv_staminamax + 0.0, // sv_staminarecoveryrate + 0.7, // sv_standable_normal + 0.0, // sv_timebetweenducks + 0.7, // sv_walkable_normal + 10.0, // sv_wateraccelerate + 0.8, // sv_water_movespeed_multiplier + 0.0, // sv_water_swim_mode + 0.0, // sv_weapon_encumbrance_per_item + 0.0 // sv_weapon_encumbrance_scale +}; + +bool gB_GOKZCore; +ConVar gCV_ModeCVar[MODECVAR_COUNT]; +bool gB_HitTweakedPerf[MAXPLAYERS + 1]; +int gI_Cmdnum[MAXPLAYERS + 1]; +float gF_PSBonusSpeed[MAXPLAYERS + 1]; +float gF_PSVelMod[MAXPLAYERS + 1]; +float gF_PSVelModLanding[MAXPLAYERS + 1]; +bool gB_PSTurningLeft[MAXPLAYERS + 1]; +float gF_PSTurnRate[MAXPLAYERS + 1]; +int gI_PSTicksSinceIncrement[MAXPLAYERS + 1]; +Handle gH_GetPlayerMaxSpeed; +DynamicDetour gH_CanUnduck; +int gI_TickCount[MAXPLAYERS + 1]; +DynamicDetour gH_AirAccelerate; +int gI_OldButtons[MAXPLAYERS + 1]; +int gI_OldFlags[MAXPLAYERS + 1]; +bool gB_OldOnGround[MAXPLAYERS + 1]; +float gF_OldOrigin[MAXPLAYERS + 1][3]; +float gF_OldAngles[MAXPLAYERS + 1][3]; +float gF_OldVelocity[MAXPLAYERS + 1][3]; +int gI_LastJumpButtonCmdnum[MAXPLAYERS + 1]; +int gI_OffsetCGameMovement_player; + + + +// =====[ PLUGIN EVENTS ]===== + +public void OnPluginStart() +{ + if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON) + { + SetFailState("gokz-mode-simplekz only supports 128 tickrate servers."); + } + HookEvents(); + CreateConVars(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + if (LibraryExists("gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_SimpleKZ, true, MODE_VERSION); + } + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnPluginEnd() +{ + if (gB_GOKZCore) + { + GOKZ_SetModeLoaded(Mode_SimpleKZ, false); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + else if (StrEqual(name, "gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_SimpleKZ, true, MODE_VERSION); + } +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + ResetClient(client); + if (IsValidClient(client)) + { + HookClientEvents(client); + } + if (IsUsingMode(client)) + { + ReplicateConVars(client); + } +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + RemoveCrouchJumpBind(player, buttons); + ReduceDuckSlowdown(player); + CalcPrestrafeVelMod(player, angles); + FixWaterBoost(player, buttons); + FixDisplacementStuck(player); + + gB_HitTweakedPerf[player.ID] = false; + gI_Cmdnum[player.ID] = cmdnum; + gI_OldButtons[player.ID] = buttons; + gI_OldFlags[player.ID] = GetEntityFlags(player.ID); + gB_OldOnGround[player.ID] = player.OnGround; + gI_TickCount[player.ID] = tickcount; + player.GetOrigin(gF_OldOrigin[player.ID]); + player.GetEyeAngles(gF_OldAngles[player.ID]); + player.GetVelocity(gF_OldVelocity[player.ID]); + + return Plugin_Continue; +} + +public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn) +{ + if (!IsUsingMode(client)) + { + return MRES_Ignored; + } + DHookSetReturn(hReturn, SPEED_NORMAL * gF_PSVelMod[client]); + return MRES_Supercede; +} + +public MRESReturn DHooks_OnAirAccelerate_Pre(Address pThis, DHookParam hParams) +{ + int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player); + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return MRES_Ignored; + } + + // NOTE: Prestrafing changes GetPlayerMaxSpeed, which changes + // air acceleration, so remove gF_PreVelMod[client] from wishspeed/maxspeed. + // This also applies to when the player is ducked: their wishspeed is + // 85 and with prestrafing can be ~93. + float wishspeed = DHookGetParam(hParams, 2); + if (gF_PSVelMod[client] > 1.0) + { + DHookSetParam(hParams, 2, wishspeed / gF_PSVelMod[client]); + return MRES_ChangedHandled; + } + + return MRES_Ignored; +} + +public MRESReturn DHooks_OnCanUnduck_Pre(Address pThis, DHookReturn hReturn) +{ + int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player); + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return MRES_Ignored; + } + // Just landed fully ducked, you can't unduck. + if (Movement_GetLandingTick(client) == (gI_TickCount[client] - 1) && GetEntPropFloat(client, Prop_Send, "m_flDuckAmount") >= 1.0 && GetEntProp(client, Prop_Send, "m_bDucked")) + { + hReturn.Value = false; + return MRES_Supercede; + } + return MRES_Ignored; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || !IsUsingMode(client)) + { + return; + } + + if (buttons & IN_JUMP) + { + gI_LastJumpButtonCmdnum[client] = cmdnum; + } +} + +public void SDKHook_OnClientPreThink_Post(int client) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return; + } + + // Don't tweak convars if GOKZ isn't running + if (gB_GOKZCore) + { + TweakConVars(); + } +} + +public void Movement_OnStartTouchGround(int client) +{ + if (!IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + gF_PSVelModLanding[player.ID] = gF_PSVelMod[player.ID]; +} + +public Action Movement_OnJumpPre(int client, float origin[3], float velocity[3]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + return TweakJump(player, origin, velocity); +} + +public Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3]) +{ + if (!IsPlayerAlive(client) || !IsUsingMode(client)) + { + return Plugin_Continue; + } + return SlopeFix(client, origin, velocity); +} + +public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype) +{ + if (!IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore && newMovetype == MOVETYPE_WALK) + { + player.GOKZHitPerf = false; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_SimpleKZ) + { + ReplicateConVars(client); + } +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + ResetClient(client); +} + + + +// =====[ GENERAL ]===== + +bool IsUsingMode(int client) +{ + // If GOKZ core isn't loaded, then apply mode at all times + return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_SimpleKZ; +} + +void ResetClient(int client) +{ + KZPlayer player = KZPlayer(client); + ResetVelMod(player); +} + +void HookEvents() +{ + GameData gameData = LoadGameConfigFile("movementapi.games"); + if (gameData == INVALID_HANDLE) + { + SetFailState("Failed to find movementapi.games config"); + } + + int offset = gameData.GetOffset("GetPlayerMaxSpeed"); + if (offset == -1) + { + SetFailState("Failed to get GetPlayerMaxSpeed offset"); + } + gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed); + + gH_AirAccelerate = DynamicDetour.FromConf(gameData, "CGameMovement::AirAccelerate"); + if (gH_AirAccelerate == INVALID_HANDLE) + { + SetFailState("Failed to find CGameMovement::AirAccelerate function signature"); + } + + if (!gH_AirAccelerate.Enable(Hook_Pre, DHooks_OnAirAccelerate_Pre)) + { + SetFailState("Failed to enable detour on CGameMovement::AirAccelerate"); + } + + char buffer[16]; + if (!gameData.GetKeyValue("CGameMovement::player", buffer, sizeof(buffer))) + { + SetFailState("Failed to get CGameMovement::player offset."); + } + gI_OffsetCGameMovement_player = StringToInt(buffer); + + gameData = LoadGameConfigFile("gokz-core.games"); + gH_CanUnduck = DynamicDetour.FromConf(gameData, "CCSGameMovement::CanUnduck"); + if (gH_CanUnduck == INVALID_HANDLE) + { + SetFailState("Failed to find CCSGameMovement::CanUnduck function signature"); + } + + if (!gH_CanUnduck.Enable(Hook_Pre, DHooks_OnCanUnduck_Pre)) + { + SetFailState("Failed to enable detour on CCSGameMovement::CanUnduck"); + } + delete gameData; +} + +// =====[ CONVARS ]===== + +void CreateConVars() +{ + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i] = FindConVar(gC_ModeCVars[i]); + } +} + +void TweakConVars() +{ + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i]; + } +} + +void ReplicateConVars(int client) +{ + /* + Replicate convars only when player changes mode in GOKZ + so that lagg isn't caused by other players using other + modes, and also as an optimisation. + */ + + if (IsFakeClient(client)) + { + return; + } + + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i])); + } +} + + + +// =====[ VELOCITY MODIFIER ]===== + +void HookClientEvents(int client) +{ + DHookEntity(gH_GetPlayerMaxSpeed, true, client); + SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post); +} + +void ResetVelMod(KZPlayer player) +{ + gF_PSBonusSpeed[player.ID] = 0.0; + gF_PSVelMod[player.ID] = 1.0; + gF_PSTurnRate[player.ID] = 0.0; +} + +void CalcPrestrafeVelMod(KZPlayer player, const float angles[3]) +{ + gI_PSTicksSinceIncrement[player.ID]++; + + // Short circuit if speed is 0 (also avoids divide by 0 errors) + if (player.Speed < EPSILON) + { + ResetVelMod(player); + return; + } + + // Current speed without bonus + float baseSpeed = FloatMin(SPEED_NORMAL, player.Speed / gF_PSVelMod[player.ID]); + + float newBonusSpeed = gF_PSBonusSpeed[player.ID]; + + // If player is in mid air, decrement their velocity modifier + if (!player.OnGround) + { + newBonusSpeed -= PS_SPEED_DECREMENT_MIDAIR; + } + // If player is turning at the required speed, and has the correct button inputs, reward it + else if (player.Turning && ValidPrestrafeButtons(player)) + { + // If player changes their prestrafe direction, reset it + if (player.TurningLeft && !gB_PSTurningLeft[player.ID] + || player.TurningRight && gB_PSTurningLeft[player.ID]) + { + ResetVelMod(player); + newBonusSpeed = 0.0; + } + + // Keep track of the direction of the turn + gB_PSTurningLeft[player.ID] = player.TurningLeft; + + // Step one of calculating new turn rate + float newTurningRate = FloatAbs(CalcDeltaAngle(gF_OldAngles[player.ID][1], angles[1])); + + // If no turning for just a few ticks, then forgive and calculate reward based on that no. of ticks + if (gI_PSTicksSinceIncrement[player.ID] <= PS_GRACE_TICKS) + { + // This turn occurred over multiple ticks, so scale appropriately + // Also cap turn rate at maximum reward turn rate + newTurningRate = FloatMin(PS_MAX_REWARD_TURN_RATE, + newTurningRate / gI_PSTicksSinceIncrement[player.ID]); + + // Limit how fast turn rate can decrease (also scaled appropriately) + gF_PSTurnRate[player.ID] = FloatMax(newTurningRate, + gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT * gI_PSTicksSinceIncrement[player.ID]); + + newBonusSpeed += CalcPreRewardSpeed(gF_PSTurnRate[player.ID], baseSpeed) * gI_PSTicksSinceIncrement[player.ID]; + } + else + { + // Cap turn rate at maximum reward turn rate + newTurningRate = FloatMin(PS_MAX_REWARD_TURN_RATE, newTurningRate); + + // Limit how fast turn rate can decrease + gF_PSTurnRate[player.ID] = FloatMax(newTurningRate, + gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT); + + // This is normal turning behaviour + newBonusSpeed += CalcPreRewardSpeed(gF_PSTurnRate[player.ID], baseSpeed); + } + + gI_PSTicksSinceIncrement[player.ID] = 0; + } + else if (gI_PSTicksSinceIncrement[player.ID] > PS_GRACE_TICKS) + { + // They definitely aren't turning, but limit how fast turn rate can decrease + gF_PSTurnRate[player.ID] = FloatMax(0.0, + gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT); + } + + if (newBonusSpeed < 0.0) + { + // Keep velocity modifier positive + newBonusSpeed = 0.0; + } + else + { + // Scale the bonus speed based on current base speed and turn rate + float baseSpeedScaleFactor = baseSpeed / SPEED_NORMAL; // Max 1.0 + float turnRateScaleFactor = FloatMin(1.0, gF_PSTurnRate[player.ID] / PS_MAX_REWARD_TURN_RATE); + float scaledMaxBonusSpeed = PS_SPEED_MAX * baseSpeedScaleFactor * turnRateScaleFactor; + newBonusSpeed = FloatMin(newBonusSpeed, scaledMaxBonusSpeed); + } + + gF_PSBonusSpeed[player.ID] = newBonusSpeed; + gF_PSVelMod[player.ID] = 1.0 + (newBonusSpeed / baseSpeed); +} + +bool ValidPrestrafeButtons(KZPlayer player) +{ + bool forwardOrBack = player.Buttons & (IN_FORWARD | IN_BACK) && !(player.Buttons & IN_FORWARD && player.Buttons & IN_BACK); + bool leftOrRight = player.Buttons & (IN_MOVELEFT | IN_MOVERIGHT) && !(player.Buttons & IN_MOVELEFT && player.Buttons & IN_MOVERIGHT); + return forwardOrBack || leftOrRight; +} + +float CalcPreRewardSpeed(float yawDiff, float baseSpeed) +{ + // Formula + float reward; + if (yawDiff >= PS_MAX_REWARD_TURN_RATE) + { + reward = PS_SPEED_INCREMENT; + } + else + { + reward = PS_SPEED_INCREMENT * (yawDiff / PS_MAX_REWARD_TURN_RATE); + } + + return reward * baseSpeed / SPEED_NORMAL; +} + + + + +// =====[ JUMPING ]===== + +Action TweakJump(KZPlayer player, float origin[3], float velocity[3]) +{ + // TakeoffCmdnum and TakeoffSpeed is not defined here because the player technically hasn't taken off yet. + int cmdsSinceLanding = gI_Cmdnum[player.ID] - player.LandingCmdNum; + gB_HitTweakedPerf[player.ID] = cmdsSinceLanding <= 1 + || cmdsSinceLanding <= 3 && gI_Cmdnum[player.ID] - gI_LastJumpButtonCmdnum[player.ID] <= 3; + + if (gB_HitTweakedPerf[player.ID]) + { + if (cmdsSinceLanding <= 1) + { + NerfRealPerf(player, origin); + } + + ApplyTweakedTakeoffSpeed(player, velocity); + + if (cmdsSinceLanding > 1 || player.TakeoffSpeed > SPEED_NORMAL) + { + // Restore prestrafe lost due to briefly being on the ground + gF_PSVelMod[player.ID] = gF_PSVelModLanding[player.ID]; + } + return Plugin_Changed; + } + return Plugin_Continue; +} + +public Action Movement_OnJumpPost(int client) +{ + if (!IsUsingMode(client)) + { + return Plugin_Continue; + } + KZPlayer player = KZPlayer(client); + player.GOKZHitPerf = gB_HitTweakedPerf[player.ID]; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + return Plugin_Continue; +} + +public void Movement_OnStopTouchGround(int client) +{ + if (!IsUsingMode(client)) + { + return; + } + KZPlayer player = KZPlayer(client); + player.GOKZHitPerf = gB_HitTweakedPerf[player.ID]; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; +} + +void NerfRealPerf(KZPlayer player, float origin[3]) +{ + // Not worth worrying about if player is already falling + // player.VerticalVelocity is not updated yet! Use processing velocity. + float velocity[3]; + Movement_GetProcessingVelocity(player.ID, velocity); + if (velocity[2] < EPSILON) + { + return; + } + + // Work out where the ground was when they bunnyhopped + float startPosition[3], endPosition[3], mins[3], maxs[3], groundOrigin[3]; + + startPosition = origin; + + endPosition = startPosition; + endPosition[2] = endPosition[2] - 2.0; // Should be less than 2.0 units away + + GetEntPropVector(player.ID, Prop_Send, "m_vecMins", mins); + GetEntPropVector(player.ID, Prop_Send, "m_vecMaxs", maxs); + + Handle trace = TR_TraceHullFilterEx( + startPosition, + endPosition, + mins, + maxs, + MASK_PLAYERSOLID, + TraceEntityFilterPlayers, + player.ID); + + // This is expected to always hit, previously this can fail upon jumpbugs. + if (TR_DidHit(trace)) + { + TR_GetEndPosition(groundOrigin, trace); + origin[2] = groundOrigin[2]; + } + + delete trace; +} + +void ApplyTweakedTakeoffSpeed(KZPlayer player, float velocity[3]) +{ + // Note that resulting velocity has same direction as landing velocity, not current velocity + // because current velocity direction can change drastically in just one tick (eg. walls) + // and it doesnt make sense for the new velocity to push you in that direction. + + float newVelocity[3], baseVelocity[3]; + player.GetLandingVelocity(newVelocity); + player.GetBaseVelocity(baseVelocity); + SetVectorHorizontalLength(newVelocity, CalcTweakedTakeoffSpeed(player)); + AddVectors(newVelocity, baseVelocity, newVelocity); // For backwards compatibility + velocity[0] = newVelocity[0]; + velocity[1] = newVelocity[1]; +} + +// Takeoff speed assuming player has met the conditions to need tweaking +float CalcTweakedTakeoffSpeed(KZPlayer player) +{ + // Formula + if (player.LandingSpeed > SPEED_NORMAL) + { + return FloatMin(player.LandingSpeed, (0.2 * player.LandingSpeed + 200) * gF_PSVelModLanding[player.ID]); + } + return player.LandingSpeed; +} + + + +// =====[ SLOPEFIX ]===== + +// ORIGINAL AUTHORS : Mev & Blacky +// URL : https://forums.alliedmods.net/showthread.php?p=2322788 +// NOTE : Modified by DanZay for this plugin + +Action SlopeFix(int client, float origin[3], float velocity[3]) +{ + KZPlayer player = KZPlayer(client); + // Check if player landed on the ground + if (Movement_GetOnGround(client) && !gB_OldOnGround[client]) + { + float vMins[] = {-16.0, -16.0, 0.0}; + // Always use ducked hull as the real hull size isn't updated yet. + // Might cause slight issues in extremely rare scenarios. + float vMaxs[] = {16.0, 16.0, 54.0}; + + float vEndPos[3]; + vEndPos[0] = origin[0]; + vEndPos[1] = origin[1]; + vEndPos[2] = origin[2] - gF_ModeCVarValues[ModeCVar_MaxVelocity]; + + // Set up and do tracehull to find out if the player landed on a slope + TR_TraceHullFilter(origin, vEndPos, vMins, vMaxs, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitSelf, client); + + if (TR_DidHit()) + { + // Gets the normal vector of the surface under the player + float vPlane[3], vLast[3]; + player.GetLandingVelocity(vLast); + TR_GetPlaneNormal(null, vPlane); + + // Make sure it's not flat ground and not a surf ramp (1.0 = flat ground, < 0.7 = surf ramp) + if (0.7 <= vPlane[2] < 1.0) + { + /* + Copy the ClipVelocity function from sdk2013 + (https://mxr.alliedmods.net/hl2sdk-sdk2013/source/game/shared/gamemovement.cpp#3145) + With some minor changes to make it actually work + */ + + float fBackOff = GetVectorDotProduct(vLast, vPlane); + + float change, vVel[3]; + for (int i; i < 2; i++) + { + change = vPlane[i] * fBackOff; + vVel[i] = vLast[i] - change; + } + + float fAdjust = GetVectorDotProduct(vVel, vPlane); + if (fAdjust < 0.0) + { + for (int i; i < 2; i++) + { + vVel[i] -= (vPlane[i] * fAdjust); + } + } + + vVel[2] = 0.0; + vLast[2] = 0.0; + + // Make sure the player is going down a ramp by checking if they actually will gain speed from the boost + if (GetVectorLength(vVel) > GetVectorLength(vLast)) + { + CopyVector(vVel, velocity); + player.SetLandingVelocity(velocity); + return Plugin_Changed; + } + } + } + } + return Plugin_Continue; +} + +public bool TraceRayDontHitSelf(int entity, int mask, any data) +{ + return entity != data && !(0 < entity <= MaxClients); +} + + + +// =====[ OTHER ]===== + +void FixWaterBoost(KZPlayer player, int buttons) +{ + if (GetEntProp(player.ID, Prop_Send, "m_nWaterLevel") >= 2) // WL_Waist = 2 + { + // If duck is being pressed and we're not already ducking or on ground + if (GetEntityFlags(player.ID) & (FL_DUCKING | FL_ONGROUND) == 0 + && buttons & IN_DUCK && ~gI_OldButtons[player.ID] & IN_DUCK) + { + float newOrigin[3]; + Movement_GetOrigin(player.ID, newOrigin); + newOrigin[2] += 9.0; + + TR_TraceHullFilter(newOrigin, newOrigin, view_as<float>( { -16.0, -16.0, 0.0 } ), view_as<float>( { 16.0, 16.0, 54.0 } ), MASK_PLAYERSOLID, TraceEntityFilterPlayers); + if (!TR_DidHit()) + { + TeleportEntity(player.ID, newOrigin, NULL_VECTOR, NULL_VECTOR); + } + } + } +} + +void FixDisplacementStuck(KZPlayer player) +{ + int flags = GetEntityFlags(player.ID); + bool unducked = ~flags & FL_DUCKING && gI_OldFlags[player.ID] & FL_DUCKING; + + float standingMins[] = {-16.0, -16.0, 0.0}; + float standingMaxs[] = {16.0, 16.0, 72.0}; + + if (unducked) + { + // check if we're stuck after unducking and if we're stuck then force duck + float origin[3]; + Movement_GetOrigin(player.ID, origin); + TR_TraceHullFilter(origin, origin, standingMins, standingMaxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + + if (TR_DidHit()) + { + player.SetVelocity(gF_OldVelocity[player.ID]); + SetEntProp(player.ID, Prop_Send, "m_bDucking", true); + } + } +} + +void RemoveCrouchJumpBind(KZPlayer player, int &buttons) +{ + if (player.OnGround && buttons & IN_JUMP && !(gI_OldButtons[player.ID] & IN_JUMP) && !(gI_OldButtons[player.ID] & IN_DUCK)) + { + buttons &= ~IN_DUCK; + } +} + +void ReduceDuckSlowdown(KZPlayer player) +{ + /* + Duck speed is reduced by the game upon ducking or unducking. + The goal here is to accept that duck speed is reduced, but + stop it from being reduced further when spamming duck. + + This is done by enforcing a minimum duck speed equivalent to + the value as if the player only ducked once. When not in not + in the middle of ducking, duck speed is reset to its normal + value in effort to reduce the number of times the minimum + duck speed is enforced. This should reduce noticeable lagg. + */ + + if (!GetEntProp(player.ID, Prop_Send, "m_bDucking") + && player.DuckSpeed < DUCK_SPEED_NORMAL - EPSILON) + { + player.DuckSpeed = DUCK_SPEED_NORMAL; + } + else if (player.DuckSpeed < DUCK_SPEED_MINIMUM - EPSILON) + { + player.DuckSpeed = DUCK_SPEED_MINIMUM; + } +} diff --git a/sourcemod/scripting/gokz-mode-vanilla.sp b/sourcemod/scripting/gokz-mode-vanilla.sp new file mode 100644 index 0000000..aecc201 --- /dev/null +++ b/sourcemod/scripting/gokz-mode-vanilla.sp @@ -0,0 +1,291 @@ +#include <sourcemod> + +#include <sdkhooks> +#include <sdktools> +#include <dhooks> + +#include <movementapi> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/core> +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Mode - Vanilla", + author = "DanZay", + description = "Vanilla mode for GOKZ", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-vanilla.txt" + +#define MODE_VERSION 17 + +float gF_ModeCVarValues[MODECVAR_COUNT] = +{ + 5.5, // sv_accelerate + 1.0, // sv_accelerate_use_weapon_speed + 12.0, // sv_airaccelerate + 30.0, // sv_air_max_wishspeed + 0.0, // sv_enablebunnyhopping + 5.2, // sv_friction + 800.0, // sv_gravity + 301.993377, // sv_jump_impulse + 0.78, // sv_ladder_scale_speed + 1.0, // sv_ledge_mantle_helper + 320.0, // sv_maxspeed + 3500.0, // sv_maxvelocity + 0.080, // sv_staminajumpcost + 0.050, // sv_staminalandcost + 80.0, // sv_staminamax + 60.0, // sv_staminarecoveryrate + 0.7, // sv_standable_normal + 0.4, // sv_timebetweenducks + 0.7, // sv_walkable_normal + 10.0, // sv_wateraccelerate + 0.8, // sv_water_movespeed_multiplier + 0.0, // sv_water_swim_mode + 0.85, // sv_weapon_encumbrance_per_item + 0.0 // sv_weapon_encumbrance_scale +}; + +bool gB_GOKZCore; +ConVar gCV_ModeCVar[MODECVAR_COUNT]; +bool gB_ProcessingMaxSpeed[MAXPLAYERS + 1]; +Handle gH_GetPlayerMaxSpeed; +Handle gH_GetPlayerMaxSpeed_SDKCall; + +// =====[ PLUGIN EVENTS ]===== + +public void OnPluginStart() +{ + CreateConVars(); + HookEvents(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + if (LibraryExists("gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_Vanilla, true, MODE_VERSION); + } + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnPluginEnd() +{ + if (gB_GOKZCore) + { + GOKZ_SetModeLoaded(Mode_Vanilla, false); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + else if (StrEqual(name, "gokz-core")) + { + gB_GOKZCore = true; + GOKZ_SetModeLoaded(Mode_Vanilla, true, MODE_VERSION); + } +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + if (IsValidClient(client)) + { + HookClientEvents(client); + } + if (IsUsingMode(client)) + { + ReplicateConVars(client); + } +} + +void HookClientEvents(int client) +{ + DHookEntity(gH_GetPlayerMaxSpeed, true, client); + SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post); +} + +public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn) +{ + if (!IsUsingMode(client) || gB_ProcessingMaxSpeed[client]) + { + return MRES_Ignored; + } + gB_ProcessingMaxSpeed[client] = true; + float maxSpeed = SDKCall(gH_GetPlayerMaxSpeed_SDKCall, client); + // Prevent players from running faster than 250u/s + if (maxSpeed > SPEED_NORMAL) + { + DHookSetReturn(hReturn, SPEED_NORMAL); + gB_ProcessingMaxSpeed[client] = false; + return MRES_Supercede; + } + gB_ProcessingMaxSpeed[client] = false; + return MRES_Ignored; +} + +public void SDKHook_OnClientPreThink_Post(int client) +{ + if (!IsUsingMode(client)) + { + return; + } + + // Don't tweak convars if GOKZ isn't running + if (gB_GOKZCore) + { + TweakConVars(); + } +} + +public Action Movement_OnJumpPost(int client) +{ + if (!IsUsingMode(client)) + { + return Plugin_Continue; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore) + { + player.GOKZHitPerf = player.HitPerf; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } + return Plugin_Continue; +} +public void Movement_OnStopTouchGround(int client, bool jumped) +{ + if (!IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore) + { + player.GOKZHitPerf = player.HitPerf; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } +} + +public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype) +{ + if (!IsUsingMode(client)) + { + return; + } + + KZPlayer player = KZPlayer(client); + if (gB_GOKZCore && newMovetype == MOVETYPE_WALK) + { + player.GOKZHitPerf = false; + player.GOKZTakeoffSpeed = player.TakeoffSpeed; + } +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_Vanilla) + { + ReplicateConVars(client); + } +} + + + +// =====[ GENERAL ]===== + +bool IsUsingMode(int client) +{ + // If GOKZ core isn't loaded, then apply mode at all times + return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_Vanilla; +} + +void HookEvents() +{ + GameData gameData = LoadGameConfigFile("movementapi.games"); + int offset = gameData.GetOffset("GetPlayerMaxSpeed"); + if (offset == -1) + { + SetFailState("Failed to get GetPlayerMaxSpeed offset"); + } + gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed); + + StartPrepSDKCall(SDKCall_Player); + PrepSDKCall_SetFromConf(gameData, SDKConf_Virtual, "GetPlayerMaxSpeed"); + PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_ByValue); + gH_GetPlayerMaxSpeed_SDKCall = EndPrepSDKCall(); +} + + +// =====[ CONVARS ]===== + +void CreateConVars() +{ + for (int cvar = 0; cvar < MODECVAR_COUNT; cvar++) + { + gCV_ModeCVar[cvar] = FindConVar(gC_ModeCVars[cvar]); + } +} + +void TweakConVars() +{ + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i]; + } +} + +void ReplicateConVars(int client) +{ + // Replicate convars only when player changes mode in GOKZ + // so that lagg isn't caused by other players using other + // modes, and also as an optimisation. + + if (IsFakeClient(client)) + { + return; + } + + for (int i = 0; i < MODECVAR_COUNT; i++) + { + gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i])); + } +} diff --git a/sourcemod/scripting/gokz-momsurffix.sp b/sourcemod/scripting/gokz-momsurffix.sp new file mode 100644 index 0000000..2738e90 --- /dev/null +++ b/sourcemod/scripting/gokz-momsurffix.sp @@ -0,0 +1,724 @@ +#include "sourcemod" +#include "sdktools" +#include "sdkhooks" +#include "dhooks" + +#include <gokz/core> +#include <gokz/momsurffix> + +#define SNAME "[gokz-momsurffix] " +#define GAME_DATA_FILE "gokz-momsurffix.games" +//#define DEBUG_PROFILE +//#define DEBUG_MEMTEST + +public Plugin myinfo = { + name = "GOKZ Momsurffix", + author = "GAMMA CASE", + description = "Ported surf fix from momentum mod. Modified for GOKZ by GameChaos. Original source code: https://github.com/GAMMACASE/MomSurfFix", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define FLT_EPSILON 1.192092896e-07 +#define MAX_CLIP_PLANES 5 + +#define ASM_PATCH_LEN 17 +#define ASM_START_OFFSET 100 + +#define WALKABLE_PLANE_NORMAL 0.7 + +enum OSType +{ + OSUnknown = -1, + OSWindows = 1, + OSLinux = 2 +}; + +OSType gOSType; +EngineVersion gEngineVersion; + +#define ASSERTUTILS_FAILSTATE_FUNC SetFailStateCustom +#define MEMUTILS_PLUGINENDCALL +#include "glib/memutils" +#undef MEMUTILS_PLUGINENDCALL + +#include "momsurffix/utils.sp" +#include "momsurffix/baseplayer.sp" +#include "momsurffix/gametrace.sp" +#include "momsurffix/gamemovement.sp" + +ConVar gBounce; + +float vec3_origin[3] = {0.0, 0.0, 0.0}; +bool gBasePlayerLoadedTooEarly; + +#if defined DEBUG_PROFILE +#include "profiler" +Profiler gProf; +ArrayList gProfData; +float gProfTime; + +void PROF_START() +{ + if(gProf) + gProf.Start(); +} + +void PROF_STOP(int idx) +{ + if(gProf) + { + gProf.Stop(); + Prof_Check(idx); + } +} + +#else +#define PROF_START%1; +#define PROF_STOP%1; +#endif + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-momsurffix"); + return APLRes_Success; +} + +public void OnPluginStart() +{ +#if defined DEBUG_MEMTEST + RegAdminCmd("sm_mom_dumpmempool", SM_Dumpmempool, ADMFLAG_ROOT, "Dumps active momory pool. Mainly for debugging."); +#endif +#if defined DEBUG_PROFILE + RegAdminCmd("sm_mom_prof", SM_Prof, ADMFLAG_ROOT, "Profiles performance of some expensive parts. Mainly for debugging."); +#endif + + gBounce = FindConVar("sv_bounce"); + ASSERT_MSG(gBounce, "\"sv_bounce\" convar wasn't found!"); + + GameData gd = new GameData(GAME_DATA_FILE); + ASSERT_FINAL(gd); + + ValidateGameAndOS(gd); + + InitUtils(gd); + InitGameTrace(gd); + gBasePlayerLoadedTooEarly = InitBasePlayer(gd); + InitGameMovement(gd); + + SetupDhooks(gd); + + delete gd; +} + +public void OnMapStart() +{ + if(gBasePlayerLoadedTooEarly) + { + GameData gd = new GameData(GAME_DATA_FILE); + LateInitBasePlayer(gd); + gBasePlayerLoadedTooEarly = false; + delete gd; + } +} + +public void OnPluginEnd() +{ + CleanUpUtils(); +} + +#if defined DEBUG_MEMTEST +public Action SM_Dumpmempool(int client, int args) +{ + DumpMemoryUsage(); + + return Plugin_Handled; +} +#endif + +#if defined DEBUG_PROFILE +public Action SM_Prof(int client, int args) +{ + if(args < 1) + { + ReplyToCommand(client, SNAME..."Usage: sm_prof <seconds>"); + return Plugin_Handled; + } + + char buff[32]; + GetCmdArg(1, buff, sizeof(buff)); + gProfTime = StringToFloat(buff); + + if(gProfTime <= 0.1) + { + ReplyToCommand(client, SNAME..."Time should be higher then 0.1 seconds."); + return Plugin_Handled; + } + + gProfData = new ArrayList(3); + gProf = new Profiler(); + CreateTimer(gProfTime, Prof_Check_Timer, client); + + ReplyToCommand(client, SNAME..."Profiler started, awaiting %.2f seconds.", gProfTime); + + return Plugin_Handled; +} + +stock void Prof_Check(int idx) +{ + int idx2; + if(gProfData.Length - 1 < idx) + { + idx2 = gProfData.Push(gProf.Time); + gProfData.Set(idx2, 1, 1); + gProfData.Set(idx2, idx, 2); + } + else + { + idx2 = gProfData.FindValue(idx, 2); + + gProfData.Set(idx2, view_as<float>(gProfData.Get(idx2)) + gProf.Time); + gProfData.Set(idx2, gProfData.Get(idx2, 1) + 1, 1); + } +} + +public Action Prof_Check_Timer(Handle timer, int client) +{ + ReplyToCommand(client, SNAME..."Profiler finished:"); + if(gProfData.Length == 0) + ReplyToCommand(client, SNAME..."There was no profiling data..."); + + for(int i = 0; i < gProfData.Length; i++) + ReplyToCommand(client, SNAME..."[%i] Avg time: %f | Calls: %i", i, view_as<float>(gProfData.Get(i)) / float(gProfData.Get(i, 1)), gProfData.Get(i, 1)); + + delete gProf; + delete gProfData; + + return Plugin_Handled; +} +#endif + +void ValidateGameAndOS(GameData gd) +{ + gOSType = view_as<OSType>(gd.GetOffset("OSType")); + ASSERT_FINAL_MSG(gOSType != OSUnknown, "Failed to get OS type or you are trying to load it on unsupported OS!"); + + gEngineVersion = GetEngineVersion(); + ASSERT_FINAL_MSG(gEngineVersion == Engine_CSS || gEngineVersion == Engine_CSGO, "Only CSGO and CSS are supported by this plugin!"); +} + +void SetupDhooks(GameData gd) +{ + Handle dhook = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Int, ThisPointer_Address); + + DHookSetFromConf(dhook, gd, SDKConf_Signature, "CGameMovement::TryPlayerMove"); + DHookAddParam(dhook, HookParamType_Int); + DHookAddParam(dhook, HookParamType_Int); + + ASSERT(DHookEnableDetour(dhook, false, TryPlayerMove_Dhook)); +} + +public MRESReturn TryPlayerMove_Dhook(Address pThis, Handle hReturn, Handle hParams) +{ + Address pFirstDest = DHookGetParam(hParams, 1); + Address pFirstTrace = DHookGetParam(hParams, 2); + + DHookSetReturn(hReturn, TryPlayerMove(view_as<CGameMovement>(pThis), view_as<Vector>(pFirstDest), view_as<CGameTrace>(pFirstTrace))); + + return MRES_Supercede; +} + +int TryPlayerMove(CGameMovement pThis, Vector pFirstDest, CGameTrace pFirstTrace) +{ + float original_velocity[3], primal_velocity[3], fixed_origin[3], valid_plane[3], new_velocity[3], end[3], dir[3]; + float allFraction, d, time_left = GetGameFrameTime(), planes[MAX_CLIP_PLANES][3]; + int bumpcount, blocked, numplanes, numbumps = 8, i, j, h; + bool stuck_on_ramp, has_valid_plane; + CGameTrace pm = CGameTrace(); + + Vector vecVelocity = pThis.mv.m_vecVelocity; + vecVelocity.ToArray(original_velocity); + vecVelocity.ToArray(primal_velocity); + Vector vecAbsOrigin = pThis.mv.m_vecAbsOrigin; + vecAbsOrigin.ToArray(fixed_origin); + + Vector plane_normal; + static Vector alloced_vector, alloced_vector2; + + if(alloced_vector.Address == Address_Null) + alloced_vector = Vector(); + + if(alloced_vector2.Address == Address_Null) + alloced_vector2 = Vector(); + + const float rampInitialRetraceLength = 0.03125; + for(bumpcount = 0; bumpcount < numbumps; bumpcount++) + { + if(vecVelocity.LengthSqr() == 0.0) + break; + + if(stuck_on_ramp) + { + if(!has_valid_plane) + { + plane_normal = pm.plane.normal; + if(!CloseEnough(VectorToArray(plane_normal), view_as<float>({0.0, 0.0, 0.0})) && + !IsEqual(valid_plane, VectorToArray(plane_normal))) + { + plane_normal.ToArray(valid_plane); + has_valid_plane = true; + } + else + { + for(i = numplanes; i-- > 0;) + { + if(!CloseEnough(planes[i], view_as<float>({0.0, 0.0, 0.0})) && + FloatAbs(planes[i][0]) <= 1.0 && FloatAbs(planes[i][1]) <= 1.0 && FloatAbs(planes[i][2]) <= 1.0 && + !IsEqual(valid_plane, planes[i])) + { + VectorCopy(planes[i], valid_plane); + has_valid_plane = true; + break; + } + } + } + } + + if(has_valid_plane) + { + alloced_vector.FromArray(valid_plane); + if(valid_plane[2] >= WALKABLE_PLANE_NORMAL && valid_plane[2] <= 1.0) + { + ClipVelocity(pThis, vecVelocity, alloced_vector, vecVelocity, 1.0); + vecVelocity.ToArray(original_velocity); + } + else + { + ClipVelocity(pThis, vecVelocity, alloced_vector, vecVelocity, 1.0 + gBounce.FloatValue * (1.0 - pThis.player.m_surfaceFriction)); + vecVelocity.ToArray(original_velocity); + } + alloced_vector.ToArray(valid_plane); + } + //TODO: should be replaced with normal solution!! Currently hack to fix issue #1. + else if((vecVelocity.z < -6.25 || vecVelocity.z > 0.0)) + { + //Quite heavy part of the code, should not be triggered much or else it'll impact performance by a lot!!! + float offsets[3]; + offsets[0] = (float(bumpcount) * 2.0) * -rampInitialRetraceLength; + offsets[2] = (float(bumpcount) * 2.0) * rampInitialRetraceLength; + int valid_planes = 0; + + VectorCopy(view_as<float>({0.0, 0.0, 0.0}), valid_plane); + + float offset[3], offset_mins[3], offset_maxs[3], buff[3]; + static Ray_t ray; + + // Keep this variable allocated only once + // since ray.Init should take care of removing any left garbage values + if(ray.Address == Address_Null) + ray = Ray_t(); + + for(i = 0; i < 3; i++) + { + for(j = 0; j < 3; j++) + { + for(h = 0; h < 3; h++) + { + PROF_START(); + offset[0] = offsets[i]; + offset[1] = offsets[j]; + offset[2] = offsets[h]; + + offset_mins = offset; + ScaleVector(offset_mins, 0.5); + offset_maxs = offset; + ScaleVector(offset_maxs, 0.5); + + if(offset[0] > 0.0) + offset_mins[0] /= 2.0; + if(offset[1] > 0.0) + offset_mins[1] /= 2.0; + if(offset[2] > 0.0) + offset_mins[2] /= 2.0; + + if(offset[0] < 0.0) + offset_maxs[0] /= 2.0; + if(offset[1] < 0.0) + offset_maxs[1] /= 2.0; + if(offset[2] < 0.0) + offset_maxs[2] /= 2.0; + PROF_STOP(0); + + PROF_START(); + AddVectors(fixed_origin, offset, buff); + SubtractVectors(end, offset, offset); + if(gEngineVersion == Engine_CSGO) + { + SubtractVectors(VectorToArray(GetPlayerMins(pThis)), offset_mins, offset_mins); + AddVectors(VectorToArray(GetPlayerMaxs(pThis)), offset_maxs, offset_maxs); + } + else + { + SubtractVectors(VectorToArray(GetPlayerMinsCSS(pThis, alloced_vector)), offset_mins, offset_mins); + AddVectors(VectorToArray(GetPlayerMaxsCSS(pThis, alloced_vector2)), offset_maxs, offset_maxs); + } + PROF_STOP(1); + + PROF_START(); + ray.Init(buff, offset, offset_mins, offset_maxs); + PROF_STOP(2); + + PROF_START(); + UTIL_TraceRay(ray, MASK_PLAYERSOLID, pThis, COLLISION_GROUP_PLAYER_MOVEMENT, pm); + PROF_STOP(3); + + PROF_START(); + plane_normal = pm.plane.normal; + + if(FloatAbs(plane_normal.x) <= 1.0 && FloatAbs(plane_normal.y) <= 1.0 && + FloatAbs(plane_normal.z) <= 1.0 && pm.fraction > 0.0 && pm.fraction < 1.0 && !pm.startsolid) + { + valid_planes++; + AddVectors(valid_plane, VectorToArray(plane_normal), valid_plane); + } + PROF_STOP(4); + } + } + } + + if(valid_planes != 0 && !CloseEnough(valid_plane, view_as<float>({0.0, 0.0, 0.0}))) + { + has_valid_plane = true; + NormalizeVector(valid_plane, valid_plane); + continue; + } + } + + if(has_valid_plane) + { + VectorMA(fixed_origin, rampInitialRetraceLength, valid_plane, fixed_origin); + } + else + { + stuck_on_ramp = false; + continue; + } + } + + VectorMA(fixed_origin, time_left, VectorToArray(vecVelocity), end); + + if(pFirstDest.Address != Address_Null && IsEqual(end, VectorToArray(pFirstDest))) + { + pm.Free(); + pm = pFirstTrace; + } + else + { + alloced_vector2.FromArray(end); + + if(stuck_on_ramp && has_valid_plane) + { + alloced_vector.FromArray(fixed_origin); + TracePlayerBBox(pThis, alloced_vector, alloced_vector2, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, pm); + pm.plane.normal.FromArray(valid_plane); + } + else + { + TracePlayerBBox(pThis, vecAbsOrigin, alloced_vector2, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, pm); + } + } + + if(bumpcount > 0 && pThis.player.m_hGroundEntity == view_as<Address>(-1) && !IsValidMovementTrace(pThis, pm)) + { + has_valid_plane = false; + stuck_on_ramp = true; + continue; + } + + if(pm.fraction > 0.0) + { + if((bumpcount == 0 || pThis.player.m_hGroundEntity != view_as<Address>(-1)) && numbumps > 0 && pm.fraction == 1.0) + { + CGameTrace stuck = CGameTrace(); + TracePlayerBBox(pThis, pm.endpos, pm.endpos, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, stuck); + + if((stuck.startsolid || stuck.fraction != 1.0) && bumpcount == 0) + { + has_valid_plane = false; + stuck_on_ramp = true; + + stuck.Free(); + continue; + } + else if(stuck.startsolid || stuck.fraction != 1.0) + { + vecVelocity.FromArray(vec3_origin); + + stuck.Free(); + break; + } + + stuck.Free(); + } + + has_valid_plane = false; + stuck_on_ramp = false; + + vecVelocity.ToArray(original_velocity); + vecAbsOrigin.FromArray(VectorToArray(pm.endpos)); + vecAbsOrigin.ToArray(fixed_origin); + allFraction += pm.fraction; + numplanes = 0; + } + + if(CloseEnoughFloat(pm.fraction, 1.0)) + break; + + MoveHelper().AddToTouched(pm, vecVelocity); + + if(pm.plane.normal.z >= WALKABLE_PLANE_NORMAL) + blocked |= 1; + + if(CloseEnoughFloat(pm.plane.normal.z, 0.0)) + blocked |= 2; + + time_left -= time_left * pm.fraction; + + if(numplanes >= MAX_CLIP_PLANES) + { + vecVelocity.FromArray(vec3_origin); + break; + } + + pm.plane.normal.ToArray(planes[numplanes]); + numplanes++; + + if(numplanes == 1 && pThis.player.m_MoveType == MOVETYPE_WALK && pThis.player.m_hGroundEntity != view_as<Address>(-1)) + { + Vector vec1 = Vector(); + PROF_START(); + if(planes[0][2] >= WALKABLE_PLANE_NORMAL) + { + vec1.FromArray(original_velocity); + alloced_vector2.FromArray(planes[0]); + alloced_vector.FromArray(new_velocity); + ClipVelocity(pThis, vec1, alloced_vector2, alloced_vector, 1.0); + alloced_vector.ToArray(original_velocity); + alloced_vector.ToArray(new_velocity); + } + else + { + vec1.FromArray(original_velocity); + alloced_vector2.FromArray(planes[0]); + alloced_vector.FromArray(new_velocity); + ClipVelocity(pThis, vec1, alloced_vector2, alloced_vector, 1.0 + gBounce.FloatValue * (1.0 - pThis.player.m_surfaceFriction)); + alloced_vector.ToArray(new_velocity); + } + PROF_STOP(5); + + vecVelocity.FromArray(new_velocity); + VectorCopy(new_velocity, original_velocity); + + vec1.Free(); + } + else + { + for(i = 0; i < numplanes; i++) + { + alloced_vector2.FromArray(original_velocity); + alloced_vector.FromArray(planes[i]); + ClipVelocity(pThis, alloced_vector2, alloced_vector, vecVelocity, 1.0); + alloced_vector.ToArray(planes[i]); + + for(j = 0; j < numplanes; j++) + if(j != i) + if(vecVelocity.Dot(planes[j]) < 0.0) + break; + + if(j == numplanes) + break; + } + + if(i != numplanes) + { + + } + else + { + if(numplanes != 2) + { + vecVelocity.FromArray(vec3_origin); + break; + } + + // Fun fact time: these next five lines of code fix (vertical) rampbug + if(CloseEnough(planes[0], planes[1])) + { + // Why did the above return true? Well, when surfing, you can "clip" into the + // ramp, due to the ramp not pushing you away enough, and when that happens, + // a surfer cries. So the game thinks the surfer is clipping along two of the exact + // same planes. So what we do here is take the surfer's original velocity, + // and add the along the normal of the surf ramp they're currently riding down, + // essentially pushing them away from the ramp. + + // NOTE: the following comment is here for context: + // NOTE: Technically the 20.0 here can be 2.0, but that causes "jitters" sometimes, so I found + // 20 to be pretty safe and smooth. If it causes any unforeseen consequences, tweak it! + VectorMA(original_velocity, 2.0, planes[0], new_velocity); + vecVelocity.x = new_velocity[0]; + vecVelocity.y = new_velocity[1]; + // Note: We don't want the player to gain any Z boost/reduce from this, gravity should be the + // only force working in the Z direction! + + // Lastly, let's get out of here before the following lines of code make the surfer lose speed. + + break; + } + + GetVectorCrossProduct(planes[0], planes[1], dir); + NormalizeVector(dir, dir); + + d = vecVelocity.Dot(dir); + + ScaleVector(dir, d); + vecVelocity.FromArray(dir); + } + + d = vecVelocity.Dot(primal_velocity); + if(d <= 0.0) + { + vecVelocity.FromArray(vec3_origin); + break; + } + } + } + + if(CloseEnoughFloat(allFraction, 0.0)) + vecVelocity.FromArray(vec3_origin); + + pm.Free(); + return blocked; +} + +stock void VectorMA(float start[3], float scale, float dir[3], float dest[3]) +{ + dest[0] = start[0] + dir[0] * scale; + dest[1] = start[1] + dir[1] * scale; + dest[2] = start[2] + dir[2] * scale; +} + +stock void VectorCopy(float from[3], float to[3]) +{ + to[0] = from[0]; + to[1] = from[1]; + to[2] = from[2]; +} + +stock float[] VectorToArray(Vector vec) +{ + float ret[3]; + vec.ToArray(ret); + return ret; +} + +stock bool IsEqual(float a[3], float b[3]) +{ + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2]; +} + +stock bool CloseEnough(float a[3], float b[3], float eps = FLT_EPSILON) +{ + return FloatAbs(a[0] - b[0]) <= eps && + FloatAbs(a[1] - b[1]) <= eps && + FloatAbs(a[2] - b[2]) <= eps; +} + +stock bool CloseEnoughFloat(float a, float b, float eps = FLT_EPSILON) +{ + return FloatAbs(a - b) <= eps; +} + +public void SetFailStateCustom(const char[] fmt, any ...) +{ + char buff[512]; + VFormat(buff, sizeof(buff), fmt, 2); + + CleanUpUtils(); + + char ostype[32]; + switch(gOSType) + { + case OSLinux: ostype = "LIN"; + case OSWindows: ostype = "WIN"; + default: ostype = "UNK"; + } + + SetFailState("[%s | %i] %s", ostype, gEngineVersion, buff); +} + +// 0-2 are axial planes +#define PLANE_X 0 +#define PLANE_Y 1 +#define PLANE_Z 2 + +// 3-5 are non-axial planes snapped to the nearest +#define PLANE_ANYX 3 +#define PLANE_ANYY 4 +#define PLANE_ANYZ 5 + +stock bool IsValidMovementTrace(CGameMovement pThis, CGameTrace tr) +{ + if(tr.allsolid || tr.startsolid) + return false; + + // This fixes pixelsurfs in a kind of scuffed way + Vector plane_normal = tr.plane.normal; + if(CloseEnoughFloat(tr.fraction, 0.0) + && tr.plane.type >= PLANE_Z // axially aligned vertical planes (not floors) can be pixelsurfs! + && plane_normal.z < WALKABLE_PLANE_NORMAL) // if plane isn't walkable + { + return false; + } + + if(FloatAbs(plane_normal.x) > 1.0 || FloatAbs(plane_normal.y) > 1.0 || FloatAbs(plane_normal.z) > 1.0) + return false; + + CGameTrace stuck = CGameTrace(); + + TracePlayerBBox(pThis, tr.endpos, tr.endpos, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, stuck); + if(stuck.startsolid || !CloseEnoughFloat(stuck.fraction, 1.0)) + { + stuck.Free(); + return false; + } + + stuck.Free(); + return true; +} + +stock void UTIL_TraceRay(Ray_t ray, int mask, CGameMovement gm, int collisionGroup, CGameTrace trace) +{ + if(gEngineVersion == Engine_CSGO) + { + CTraceFilterSimple filter = LockTraceFilter(gm, collisionGroup); + + gm.m_nTraceCount++; + ITraceListData tracelist = gm.m_pTraceListData; + + if(tracelist.Address != Address_Null && tracelist.CanTraceRay(ray)) + TraceRayAgainstLeafAndEntityList(ray, tracelist, mask, filter, trace); + else + TraceRay(ray, mask, filter, trace); + + UnlockTraceFilter(gm, filter); + } + else if(gEngineVersion == Engine_CSS) + { + CTraceFilterSimple filter = CTraceFilterSimple(); + filter.Init(LookupEntity(gm.mv.m_nPlayerHandle), collisionGroup); + + TraceRay(ray, mask, filter, trace); + + filter.Free(); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-paint.sp b/sourcemod/scripting/gokz-paint.sp new file mode 100644 index 0000000..3de93a7 --- /dev/null +++ b/sourcemod/scripting/gokz-paint.sp @@ -0,0 +1,410 @@ +#include <sourcemod> + +#include <gokz/core> +#include <gokz/paint> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +// Credit to SlidyBat for a large part of the painting code (https://forums.alliedmods.net/showthread.php?p=2541664) +// Credit to Cabbage McGravel of the MomentumMod team for making the textures + +public Plugin myinfo = +{ + name = "GOKZ Paint", + author = "zealain", + description = "Provides client sided paint for visibility", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-paint.txt" + +char gC_PaintColors[][32] = +{ + "paint_red", + "paint_white", + "paint_black", + "paint_blue", + "paint_brown", + "paint_green", + "paint_yellow", + "paint_purple" +}; + +char gC_PaintSizePostfix[][8] = +{ + "_small", + "_med", + "_large" +}; + +int gI_Decals[sizeof(gC_PaintColors)][sizeof(gC_PaintSizePostfix)]; +float gF_LastPaintPos[MAXPLAYERS + 1][3]; +bool gB_IsPainting[MAXPLAYERS + 1]; + +TopMenu gTM_Options; +TopMenuObject gTMO_CatPaint; +TopMenuObject gTMO_ItemsPaint[PAINTOPTION_COUNT]; + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-paint"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-paint.phrases"); + + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ EVENTS ]===== + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void OnMapStart() +{ + char buffer[PLATFORM_MAX_PATH]; + + AddFileToDownloadsTable("materials/gokz/paint/paint_decal.vtf"); + for (int color = 0; color < sizeof(gC_PaintColors); color++) + { + for (int size = 0; size < sizeof(gC_PaintSizePostfix); size++) + { + Format(buffer, sizeof(buffer), "gokz/paint/%s%s.vmt", gC_PaintColors[color], gC_PaintSizePostfix[size]); + gI_Decals[color][size] = PrecachePaint(buffer); + } + } + + CreateTimer(0.1, Timer_Paint, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); +} + + + +// =====[ PAINT ]===== + +void Paint(int client) +{ + if (!IsValidClient(client) || + IsFakeClient(client)) + { + return; + } + + float position[3]; + bool hit = GetPlayerEyeViewPoint(client, position); + + if (!hit || GetVectorDistance(position, gF_LastPaintPos[client], true) < MIN_PAINT_SPACING) + { + return; + } + + int paint = GOKZ_GetOption(client, gC_PaintOptionNames[PaintOption_Color]); + int size = GOKZ_GetOption(client, gC_PaintOptionNames[PaintOption_Size]); + + TE_SetupWorldDecal(position, gI_Decals[paint][size]); + TE_SendToClient(client); + + gF_LastPaintPos[client] = position; +} + +public Action Timer_Paint(Handle timer) +{ + for (int client = 1; client <= MAXPLAYERS; client++) + { + if (gB_IsPainting[client]) + { + Paint(client); + } + } + return Plugin_Continue; +} + +void TE_SetupWorldDecal(const float origin[3], int index) +{ + TE_Start("World Decal"); + TE_WriteVector("m_vecOrigin", origin); + TE_WriteNum("m_nIndex", index); +} + +int PrecachePaint(char[] filename) +{ + char path[PLATFORM_MAX_PATH]; + Format(path, sizeof(path), "materials/%s", filename); + AddFileToDownloadsTable(path); + + return PrecacheDecal(filename, true); +} + +bool GetPlayerEyeViewPoint(int client, float position[3]) +{ + float angles[3]; + GetClientEyeAngles(client, angles); + + float origin[3]; + GetClientEyePosition(client, origin); + + Handle trace = TR_TraceRayFilterEx(origin, angles, MASK_PLAYERSOLID, RayType_Infinite, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + TR_GetEndPosition(position, trace); + delete trace; + return true; + } + delete trace; + return false; +} + + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void RegisterOptions() +{ + for (PaintOption option; option < PAINTOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_PaintOptionNames[option], gC_PaintOptionDescriptions[option], + OptionType_Int, gI_PaintOptionDefaults[option], 0, gI_PaintOptionCounts[option] - 1); + } +} + + + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu && gTMO_CatPaint != INVALID_TOPMENUOBJECT) + { + return; + } + + gTMO_CatPaint = topMenu.AddCategory(PAINT_OPTION_CATEGORY, TopMenuHandler_Categories); +} + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + // Make sure category exists + if (gTMO_CatPaint == INVALID_TOPMENUOBJECT) + { + GOKZ_OnOptionsMenuCreated(topMenu); + } + + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + + // Add gokz-paint option items + for (int option = 0; option < view_as<int>(PAINTOPTION_COUNT); option++) + { + gTMO_ItemsPaint[option] = gTM_Options.AddItem(gC_PaintOptionNames[option], TopMenuHandler_Paint, gTMO_CatPaint); + } +} + +void DisplayPaintOptionsMenu(int client) +{ + gTM_Options.DisplayCategory(gTMO_CatPaint, client); +} + +public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == gTMO_CatPaint) + { + Format(buffer, maxlength, "%T", "Options Menu - Paint", param); + } + } +} + +public void TopMenuHandler_Paint(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + PaintOption option = PAINTOPTION_INVALID; + for (int i = 0; i < view_as<int>(PAINTOPTION_COUNT); i++) + { + if (topobj_id == gTMO_ItemsPaint[i]) + { + option = view_as<PaintOption>(i); + break; + } + } + + if (option == PAINTOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case PaintOption_Color: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_PaintOptionPhrases[option], param, + gC_PaintColorPhrases[GOKZ_GetOption(param, gC_PaintOptionNames[option])], param); + } + case PaintOption_Size: + { + FormatEx(buffer, maxlength, "%T - %T", + gC_PaintOptionPhrases[option], param, + gC_PaintSizePhrases[GOKZ_GetOption(param, gC_PaintOptionNames[option])], param); + } + } + } + else if (action == TopMenuAction_SelectOption) + { + if (option == PaintOption_Color) + { + DisplayColorMenu(param); + } + else + { + GOKZ_CycleOption(param, gC_PaintOptionNames[option]); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } + } +} + +void DisplayColorMenu(int client) +{ + char buffer[32]; + + Menu menu = new Menu(MenuHandler_PaintColor); + menu.ExitButton = true; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Paint Color Menu - Title", client); + + for (int i = 0; i < PAINTCOLOR_COUNT; i++) + { + FormatEx(buffer, sizeof(buffer), "%T", gC_PaintColorPhrases[i], client); + menu.AddItem(gC_PaintColors[i], buffer); + } + + menu.Display(client, MENU_TIME_FOREVER); +} + +int MenuHandler_PaintColor(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_Select: + { + char item[32]; + menu.GetItem(param2, item, sizeof(item)); + + for (int i = 0; i < PAINTCOLOR_COUNT; i++) + { + if (StrEqual(gC_PaintColors[i], item)) + { + GOKZ_SetOption(param1, gC_PaintOptionNames[PaintOption_Color], i); + DisplayPaintOptionsMenu(param1); + return 0; + } + } + } + + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack) + { + DisplayPaintOptionsMenu(param1); + } + } + + case MenuAction_End: + { + delete menu; + } + } + + return 0; +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("+paint", CommandPaintStart, "[KZ] Start painting."); + RegConsoleCmd("-paint", CommandPaintEnd, "[KZ] Stop painting."); + RegConsoleCmd("sm_paint", CommandPaint, "[KZ] Place a paint."); + RegConsoleCmd("sm_paintoptions", CommandPaintOptions, "[KZ] Open the paint options."); +} + +public Action CommandPaintStart(int client, int args) +{ + gB_IsPainting[client] = true; + return Plugin_Handled; +} + +public Action CommandPaintEnd(int client, int args) +{ + gB_IsPainting[client] = false; + return Plugin_Handled; +} + +public Action CommandPaint(int client, int args) +{ + Paint(client); + return Plugin_Handled; +} + +public Action CommandPaintOptions(int client, int args) +{ + DisplayPaintOptionsMenu(client); + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-pistol.sp b/sourcemod/scripting/gokz-pistol.sp new file mode 100644 index 0000000..53f79d9 --- /dev/null +++ b/sourcemod/scripting/gokz-pistol.sp @@ -0,0 +1,303 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdktools> + +#include <gokz/core> +#include <gokz/pistol> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Pistol", + author = "DanZay", + description = "Allows players to pick a pistol to KZ with", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-pistols.txt" + +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemPistol; +bool gB_CameFromOptionsMenu[MAXPLAYERS + 1]; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-pistol"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-pistol.phrases"); + + HookEvents(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + UpdatePistol(client); + } +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + if (StrEqual(option, PISTOL_OPTION_NAME)) + { + UpdatePistol(client); + } +} + + + +// =====[ OTHER EVENTS ]===== + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + + + +// =====[ GENERAL ]===== + +void HookEvents() +{ + HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); +} + + + +// =====[ PISTOL ]===== + +void UpdatePistol(int client) +{ + GivePistol(client, GOKZ_GetOption(client, PISTOL_OPTION_NAME)); +} + +void GivePistol(int client, int pistol) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client) + || GetClientTeam(client) == CS_TEAM_NONE) + { + return; + } + + int playerTeam = GetClientTeam(client); + bool switchedTeams = false; + + // Switch teams to the side that buys that gun so that gun skins load + if (gI_PistolTeams[pistol] == CS_TEAM_CT && playerTeam != CS_TEAM_CT) + { + CS_SwitchTeam(client, CS_TEAM_CT); + switchedTeams = true; + } + else if (gI_PistolTeams[pistol] == CS_TEAM_T && playerTeam != CS_TEAM_T) + { + CS_SwitchTeam(client, CS_TEAM_T); + switchedTeams = true; + } + + // Give the player this pistol (or remove it) + int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + if (currentPistol != -1) + { + RemovePlayerItem(client, currentPistol); + } + + if (pistol == Pistol_Disabled) + { + // Force switch to knife to avoid weird behaviour + // Doesn't use EquipPlayerWeapon because server hangs when player spawns + FakeClientCommand(client, "use weapon_knife"); + } + else + { + GivePlayerItem(client, gC_PistolClassNames[pistol]); + } + + // Go back to original team + if (switchedTeams) + { + CS_SwitchTeam(client, playerTeam); + } +} + + + +// =====[ PISTOL MENU ]===== + +void DisplayPistolMenu(int client, int atItem = 0, bool fromOptionsMenu = false) +{ + Menu menu = new Menu(MenuHandler_Pistol); + menu.SetTitle("%T", "Pistol Menu - Title", client); + PistolMenuAddItems(client, menu); + menu.DisplayAt(client, atItem, MENU_TIME_FOREVER); + + gB_CameFromOptionsMenu[client] = fromOptionsMenu; +} + +public int MenuHandler_Pistol(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + GOKZ_SetOption(param1, PISTOL_OPTION_NAME, param2); + DisplayPistolMenu(param1, param2 / 6 * 6, gB_CameFromOptionsMenu[param1]); // Re-display menu at same spot + } + else if (action == MenuAction_Cancel && gB_CameFromOptionsMenu[param1]) + { + gTM_Options.Display(param1, TopMenuPosition_LastCategory); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +void PistolMenuAddItems(int client, Menu menu) +{ + int selectedPistol = GOKZ_GetOption(client, PISTOL_OPTION_NAME); + char display[32]; + + for (int pistol = 0; pistol < PISTOL_COUNT; pistol++) + { + if (pistol == Pistol_Disabled) + { + FormatEx(display, sizeof(display), "%T", "Options Menu - Disabled", client); + } + else + { + FormatEx(display, sizeof(display), "%s", gC_PistolNames[pistol]); + } + + // Add asterisk to selected pistol + if (pistol == selectedPistol) + { + Format(display, sizeof(display), "%s*", display); + } + + menu.AddItem("", display, ITEMDRAW_DEFAULT); + } +} + + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOption(); +} + +void RegisterOption() +{ + GOKZ_RegisterOption(PISTOL_OPTION_NAME, PISTOL_OPTION_DESCRIPTION, + OptionType_Int, Pistol_USPS, 0, PISTOL_COUNT - 1); +} + + + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + gTMO_ItemPistol = gTM_Options.AddItem(PISTOL_OPTION_NAME, TopMenuHandler_Pistol, gTMO_CatGeneral); +} + +public void TopMenuHandler_Pistol(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (topobj_id != gTMO_ItemPistol) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + int pistol = GOKZ_GetOption(param, PISTOL_OPTION_NAME); + if (pistol == Pistol_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - Pistol", param, + "Options Menu - Disabled", param); + } + else + { + FormatEx(buffer, maxlength, "%T - %s", + "Options Menu - Pistol", param, + gC_PistolNames[pistol]); + } + } + else if (action == TopMenuAction_SelectOption) + { + DisplayPistolMenu(param, _, true); + } +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_pistol", CommandPistolMenu, "[KZ] Open the pistol selection menu."); +} + +public Action CommandPistolMenu(int client, int args) +{ + DisplayPistolMenu(client); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-playermodels.sp b/sourcemod/scripting/gokz-playermodels.sp new file mode 100644 index 0000000..237b7df --- /dev/null +++ b/sourcemod/scripting/gokz-playermodels.sp @@ -0,0 +1,198 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdktools> + +#include <gokz/core> + +#include <autoexecconfig> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Player Models", + author = "DanZay", + description = "Sets player's model upon spawning", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-playermodels.txt" +#define PLAYER_MODEL_T "models/player/tm_leet_varianta.mdl" +#define PLAYER_MODEL_CT "models/player/ctm_idf_variantc.mdl" +#define PLAYER_MODEL_T_BOT "models/player/custom_player/legacy/tm_leet_varianta.mdl" +#define PLAYER_MODEL_CT_BOT "models/player/custom_player/legacy/ctm_idf_variantc.mdl" +ConVar gCV_gokz_player_models_alpha; +ConVar gCV_sv_disable_immunity_alpha; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-playermodels"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + CreateConVars(); + HookEvents(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + // Can't use a timer here because it's not precise enough. We want exactly 2 ticks of delay! + // 2 ticks is the minimum amount of time after which gloves will work. + // The reason we need precision is because SetEntityModel momentarily resets the + // player hull to standing (or something along those lines), so when a player + // spawns/gets reset to a crouch tunnel where there's a trigger less than 18 units from the top + // of the ducked player hull, then they touch that trigger! SetEntityModel interferes with the + // fix for that (JoinTeam in gokz-core/misc calls TeleportPlayer in gokz.inc, which fixes that bug). + RequestFrame(RequestFrame_UpdatePlayerModel, GetClientUserId(client)); + } +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + PrecachePlayerModels(); +} + + + +// =====[ GENERAL ]===== + +void HookEvents() +{ + HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); +} + + + +// =====[ CONVARS ]===== + +void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-playermodels", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_player_models_alpha = AutoExecConfig_CreateConVar("gokz_player_models_alpha", "65", "Amount of alpha (transparency) to set player models to.", _, true, 0.0, true, 255.0); + gCV_gokz_player_models_alpha.AddChangeHook(OnConVarChanged); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + gCV_sv_disable_immunity_alpha = FindConVar("sv_disable_immunity_alpha"); +} + +public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (convar == gCV_gokz_player_models_alpha) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && IsPlayerAlive(client)) + { + UpdatePlayerModelAlpha(client); + } + } + } +} + + + +// =====[ PLAYER MODELS ]===== + +public void RequestFrame_UpdatePlayerModel(int userid) +{ + RequestFrame(RequestFrame_UpdatePlayerModel2, userid); +} + +public void RequestFrame_UpdatePlayerModel2(int userid) +{ + int client = GetClientOfUserId(userid); + if (!IsValidClient(client) || !IsPlayerAlive(client)) + { + return; + } + // Bots are unaffected by the bobbing animation caused by the new models. + switch (GetClientTeam(client)) + { + case CS_TEAM_T: + { + if (IsFakeClient(client)) + { + SetEntityModel(client, PLAYER_MODEL_T_BOT); + } + else + { + SetEntityModel(client, PLAYER_MODEL_T); + } + } + case CS_TEAM_CT: + { + if (IsFakeClient(client)) + { + SetEntityModel(client, PLAYER_MODEL_CT_BOT); + } + else + { + SetEntityModel(client, PLAYER_MODEL_CT); + } + } + } + + UpdatePlayerModelAlpha(client); +} + +void UpdatePlayerModelAlpha(int client) +{ + SetEntityRenderMode(client, RENDER_TRANSCOLOR); + SetEntityRenderColor(client, _, _, _, gCV_gokz_player_models_alpha.IntValue); +} + +void PrecachePlayerModels() +{ + gCV_sv_disable_immunity_alpha.IntValue = 1; // Ensures player transparency works + + PrecacheModel(PLAYER_MODEL_T, true); + PrecacheModel(PLAYER_MODEL_CT, true); + PrecacheModel(PLAYER_MODEL_T_BOT, true); + PrecacheModel(PLAYER_MODEL_CT_BOT, true); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-profile.sp b/sourcemod/scripting/gokz-profile.sp new file mode 100644 index 0000000..9d92e61 --- /dev/null +++ b/sourcemod/scripting/gokz-profile.sp @@ -0,0 +1,396 @@ +#include <sourcemod> + +#include <cstrike> + +#include <gokz/core> +#include <gokz/profile> +#include <gokz/global> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> +#include <gokz/chat> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Profile", + author = "zealain", + description = "Player profiles and ranks based on local and global data.", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-profile.txt" + +int gI_Rank[MAXPLAYERS + 1][MODE_COUNT]; +bool gB_Localranks; +bool gB_Chat; + +#include "gokz-profile/options.sp" +#include "gokz-profile/profile.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-profile"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-profile.phrases"); + CreateGlobalForwards(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_Localranks = LibraryExists("gokz-localranks"); + gB_Chat = LibraryExists("gokz-chat"); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && !IsFakeClient(client)) + { + UpdateRank(client, GOKZ_GetCoreOption(client, Option_Mode)); + } + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_Localranks = gB_Localranks || StrEqual(name, "gokz-localranks"); + gB_Chat = gB_Chat || StrEqual(name, "gokz-chat"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_Localranks = gB_Localranks && !StrEqual(name, "gokz-localranks"); + gB_Chat = gB_Chat && !StrEqual(name, "gokz-chat"); +} + + + +// =====[ EVENTS ]===== + +public Action OnClientCommandKeyValues(int client, KeyValues kv) +{ + // Block clan tag changes - Credit: GoD-Tony (https://forums.alliedmods.net/showpost.php?p=2337679&postcount=6) + char cmd[16]; + if (kv.GetSectionName(cmd, sizeof(cmd)) && StrEqual(cmd, "ClanTagChanged", false)) + { + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void OnRebuildAdminCache(AdminCachePart part) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && !IsFakeClient(client)) + { + int mode = GOKZ_GetCoreOption(client, Option_Mode); + UpdateRank(client, mode); + } + } +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionsLoaded(int client) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + int mode = GOKZ_GetCoreOption(client, Option_Mode); + UpdateTags(client, gI_Rank[client][mode], mode); + } +} + +public void OnClientConnected(int client) +{ + for (int mode = 0; mode < MODE_COUNT; mode++) + { + gI_Rank[client][mode] = 0; + } + Profile_OnClientConnected(client); +} + +public void OnClientDisconnect(int client) +{ + Profile_OnClientDisconnect(client); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + Option coreOption; + if (GOKZ_IsCoreOption(option, coreOption) && coreOption == Option_Mode) + { + UpdateRank(client, newValue); + } + else if (StrEqual(option, gC_ProfileOptionNames[ProfileOption_ShowRankChat], true) + || StrEqual(option, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag], true) + || StrEqual(option, gC_ProfileOptionNames[ProfileOption_TagType], true)) + { + UpdateRank(client, GOKZ_GetCoreOption(client, Option_Mode)); + } +} + +public void GOKZ_GL_OnPointsUpdated(int client, int mode) +{ + UpdateRank(client, mode); + Profile_OnPointsUpdated(client, mode); +} + +public void UpdateRank(int client, int mode) +{ + if (!IsValidClient(client) || IsFakeClient(client)) + { + return; + } + + int tagType = GetAvailableTagTypeOrDefault(client); + + if (tagType != ProfileTagType_Rank) + { + char clanTag[64], chatTag[32], color[64]; + + if (tagType == ProfileTagType_Admin) + { + FormatEx(clanTag, sizeof(clanTag), "[%s %T]", gC_ModeNamesShort[mode], "Tag - Admin", client); + FormatEx(chatTag, sizeof(chatTag), "%T", "Tag - Admin", client); + color = TAG_COLOR_ADMIN; + } + if (tagType == ProfileTagType_VIP) + { + FormatEx(clanTag, sizeof(clanTag), "[%s %T]", gC_ModeNamesShort[mode], "Tag - VIP", client); + FormatEx(chatTag, sizeof(chatTag), "%T", "Tag - VIP", client); + color = TAG_COLOR_VIP; + } + + if (GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag]) != ProfileOptionBool_Enabled) + { + FormatEx(clanTag, sizeof(clanTag), "[%s]", gC_ModeNamesShort[mode]); + } + CS_SetClientClanTag(client, clanTag); + + if (gB_Chat) + { + if (GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankChat]) == ProfileOptionBool_Enabled) + { + GOKZ_CH_SetChatTag(client, chatTag, color); + } + else + { + GOKZ_CH_SetChatTag(client, "", "{default}"); + } + } + return; + } + + int points = GOKZ_GL_GetRankPoints(client, mode); + int rank; + for (rank = 1; rank < RANK_COUNT; rank++) + { + if (points < gI_rankThreshold[mode][rank]) + { + break; + } + } + rank--; + + if (GOKZ_GetCoreOption(client, Option_Mode) == mode) + { + if (points == -1) + { + UpdateTags(client, -1, mode); + } + else + { + UpdateTags(client, rank, mode); + } + } + + if (gI_Rank[client][mode] != rank) + { + gI_Rank[client][mode] = rank; + Call_OnRankUpdated(client, mode, rank); + } +} + +void UpdateTags(int client, int rank, int mode) +{ + char str[64]; + if (rank != -1 && + GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag]) == ProfileOptionBool_Enabled) + { + FormatEx(str, sizeof(str), "[%s %s]", gC_ModeNamesShort[mode], gC_rankName[rank]); + CS_SetClientClanTag(client, str); + } + else + { + FormatEx(str, sizeof(str), "[%s]", gC_ModeNamesShort[mode]); + CS_SetClientClanTag(client, str); + } + + if (gB_Chat) + { + if (rank != -1 && + GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankChat]) == ProfileOptionBool_Enabled) + { + GOKZ_CH_SetChatTag(client, gC_rankName[rank], gC_rankColor[rank]); + } + else + { + GOKZ_CH_SetChatTag(client, "", "{default}"); + } + } +} + +bool CanUseTagType(int client, int tagType) +{ + switch (tagType) + { + case ProfileTagType_Rank: return true; + case ProfileTagType_VIP: return CheckCommandAccess(client, "gokz_flag_vip", ADMFLAG_CUSTOM1); + case ProfileTagType_Admin: return CheckCommandAccess(client, "gokz_flag_admin", ADMFLAG_GENERIC); + default: return false; + } +} + +int GetAvailableTagTypeOrDefault(int client) +{ + int tagType = GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_TagType]); + if (!CanUseTagType(client, tagType)) + { + return ProfileTagType_Rank; + } + + return tagType; +} + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_profile", CommandProfile, "[KZ] Show the profile of a player. Usage: !profile <player>"); + RegConsoleCmd("sm_p", CommandProfile, "[KZ] Show the profile of a player. Usage: !p <player>"); + RegConsoleCmd("sm_profileoptions", CommandProfileOptions, "[KZ] Show the profile options."); + RegConsoleCmd("sm_pfo", CommandProfileOptions, "[KZ] Show the profile options."); + RegConsoleCmd("sm_ranks", CommandRanks, "[KZ] Show all the available ranks."); +} + +public Action CommandProfile(int client, int args) +{ + if (args == 0) + { + ShowProfile(client, client); + } + else + { + char playerName[64]; + GetCmdArgString(playerName, sizeof(playerName)); + int player = FindTarget(client, playerName, true, false); + if (player != -1) + { + ShowProfile(client, player); + } + } + return Plugin_Handled; +} + +public Action CommandProfileOptions(int client, int args) +{ + DisplayProfileOptionsMenu(client); + return Plugin_Handled; +} + +public Action CommandRanks(int client, int args) +{ + char rankBuffer[256]; + char buffer[256]; + int mode = GOKZ_GetCoreOption(client, Option_Mode); + + Format(buffer, sizeof(buffer), "%s: ", gC_ModeNamesShort[mode]); + + for (int i = 0; i < RANK_COUNT; i++) { + Format(rankBuffer, sizeof(rankBuffer), "%s%s (%d) ", gC_rankColor[i], gC_rankName[i], gI_rankThreshold[mode][i]); + StrCat(buffer, sizeof(buffer), rankBuffer); + + if (i > 0 && i % 3 == 0) { + GOKZ_PrintToChat(client, true, buffer); + Format(buffer, sizeof(buffer), "%s: ", gC_ModeNamesShort[mode]); + } + } + + GOKZ_PrintToChat(client, true, buffer); + + return Plugin_Handled; +} + + +// =====[ FORWARDS ]===== + +static GlobalForward H_OnRankUpdated; + + +void CreateGlobalForwards() +{ + H_OnRankUpdated = new GlobalForward("GOKZ_PF_OnRankUpdated", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); +} + +void Call_OnRankUpdated(int client, int mode, int rank) +{ + Call_StartForward(H_OnRankUpdated); + Call_PushCell(client); + Call_PushCell(mode); + Call_PushCell(rank); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_PF_GetRank", Native_GetRank); +} + +public int Native_GetRank(Handle plugin, int numParams) +{ + return gI_Rank[GetNativeCell(1)][GetNativeCell(2)]; +} diff --git a/sourcemod/scripting/gokz-profile/options.sp b/sourcemod/scripting/gokz-profile/options.sp new file mode 100644 index 0000000..de8da51 --- /dev/null +++ b/sourcemod/scripting/gokz-profile/options.sp @@ -0,0 +1,128 @@ + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void RegisterOptions() +{ + for (ProfileOption option; option < PROFILEOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_ProfileOptionNames[option], gC_ProfileOptionDescriptions[option], + OptionType_Int, gI_ProfileOptionDefaults[option], 0, gI_ProfileOptionCounts[option] - 1); + } +} + + + +// =====[ OPTIONS MENU ]===== + +TopMenu gTM_Options; +TopMenuObject gTMO_CatProfile; +TopMenuObject gTMO_ItemsProfile[PROFILEOPTION_COUNT]; + +void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu && gTMO_CatProfile != INVALID_TOPMENUOBJECT) + { + return; + } + + gTMO_CatProfile = topMenu.AddCategory(PROFILE_OPTION_CATEGORY, TopMenuHandler_Categories); +} + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + // Make sure category exists + if (gTMO_CatProfile == INVALID_TOPMENUOBJECT) + { + GOKZ_OnOptionsMenuCreated(topMenu); + } + + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + + // Add gokz-profile option items + for (int option = 0; option < view_as<int>(PROFILEOPTION_COUNT); option++) + { + gTMO_ItemsProfile[option] = gTM_Options.AddItem(gC_ProfileOptionNames[option], TopMenuHandler_Profile, gTMO_CatProfile); + } +} + +void DisplayProfileOptionsMenu(int client) +{ + if (gTMO_CatProfile != INVALID_TOPMENUOBJECT) + { + gTM_Options.DisplayCategory(gTMO_CatProfile, client); + } +} + +public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == gTMO_CatProfile) + { + Format(buffer, maxlength, "%T", "Options Menu - Profile", param); + } + } +} + +public void TopMenuHandler_Profile(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + ProfileOption option = PROFILEOPTION_INVALID; + for (int i = 0; i < view_as<int>(PROFILEOPTION_COUNT); i++) + { + if (topobj_id == gTMO_ItemsProfile[i]) + { + option = view_as<ProfileOption>(i); + break; + } + } + + if (option == PROFILEOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + if (option == ProfileOption_TagType) + { + FormatEx(buffer, maxlength, "%T - %T", + gC_ProfileOptionPhrases[option], param, + gC_ProfileTagTypePhrases[GOKZ_GetOption(param, gC_ProfileOptionNames[option])], param); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gC_ProfileOptionPhrases[option], param, + gC_ProfileBoolPhrases[GOKZ_GetOption(param, gC_ProfileOptionNames[option])], param); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, gC_ProfileOptionNames[option]); + + if (option == ProfileOption_TagType) + { + for (int i = 0; i < PROFILETAGTYPE_COUNT; i++) + { + int tagType = GOKZ_GetOption(param, gC_ProfileOptionNames[option]); + if (!CanUseTagType(param, tagType)) + { + GOKZ_CycleOption(param, gC_ProfileOptionNames[option]); + } + } + } + + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +} + diff --git a/sourcemod/scripting/gokz-profile/profile.sp b/sourcemod/scripting/gokz-profile/profile.sp new file mode 100644 index 0000000..fabdb0e --- /dev/null +++ b/sourcemod/scripting/gokz-profile/profile.sp @@ -0,0 +1,222 @@ + +#define ITEM_INFO_NAME "name" +#define ITEM_INFO_MODE "mode" +#define ITEM_INFO_RANK "rank" +#define ITEM_INFO_POINTS "points" + +int profileTargetPlayer[MAXPLAYERS]; +int profileMode[MAXPLAYERS]; +bool profileWaitingForUpdate[MAXPLAYERS]; + + + +// =====[ PUBLIC ]===== + +void ShowProfile(int client, int player = 0) +{ + if (player != 0) + { + profileTargetPlayer[client] = player; + profileMode[client] = GOKZ_GetCoreOption(player, Option_Mode); + } + + if (GOKZ_GL_GetRankPoints(profileTargetPlayer[client], profileMode[client]) < 0) + { + if (!profileWaitingForUpdate[client]) + { + GOKZ_GL_UpdatePoints(profileTargetPlayer[client], profileMode[client]); + profileWaitingForUpdate[client] = true; + } + return; + } + + profileWaitingForUpdate[client] = false; + Menu menu = new Menu(MenuHandler_Profile); + menu.SetTitle("%T - %N", "Profile Menu - Title", client, profileTargetPlayer[client]); + ProfileMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ EVENTS ]===== + +void Profile_OnClientConnected(int client) +{ + profileTargetPlayer[client] = 0; + profileWaitingForUpdate[client] = false; +} + +void Profile_OnClientDisconnect(int client) +{ + profileTargetPlayer[client] = 0; + profileWaitingForUpdate[client] = false; +} + +void Profile_OnPointsUpdated(int player, int mode) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (profileWaitingForUpdate[client] + && profileTargetPlayer[client] == player + && profileMode[client] == mode) + { + ShowProfile(client); + } + } +} + +public int MenuHandler_Profile(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + + if (StrEqual(info, ITEM_INFO_MODE, false)) + { + if (++profileMode[param1] == MODE_COUNT) + { + profileMode[param1] = 0; + } + } + else if (StrEqual(info, ITEM_INFO_RANK, false)) + { + ShowRankInfo(param1); + return 0; + } + else if (StrEqual(info, ITEM_INFO_POINTS, false)) + { + ShowPointsInfo(param1); + return 0; + } + + ShowProfile(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +static void ProfileMenuAddItems(int client, Menu menu) +{ + char display[32]; + int player = profileTargetPlayer[client]; + int mode = profileMode[client]; + + FormatEx(display, sizeof(display), "%T: %s", + "Profile Menu - Mode", client, gC_ModeNames[mode]); + menu.AddItem(ITEM_INFO_MODE, display); + + FormatEx(display, sizeof(display), "%T: %s", + "Profile Menu - Rank", client, gC_rankName[gI_Rank[player][mode]]); + menu.AddItem(ITEM_INFO_RANK, display); + + FormatEx(display, sizeof(display), "%T: %d", + "Profile Menu - Points", client, GOKZ_GL_GetRankPoints(player, mode)); + menu.AddItem(ITEM_INFO_POINTS, display); +} + +static void ShowRankInfo(int client) +{ + Menu menu = new Menu(MenuHandler_RankInfo); + menu.SetTitle("%T - %N", "Rank Info Menu - Title", client, profileTargetPlayer[client]); + RankInfoMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void RankInfoMenuAddItems(int client, Menu menu) +{ + char display[32]; + int player = profileTargetPlayer[client]; + int mode = profileMode[client]; + + FormatEx(display, sizeof(display), "%T: %s", + "Rank Info Menu - Current Rank", client, gC_rankName[gI_Rank[player][mode]]); + menu.AddItem("", display); + + int next_rank = gI_Rank[player][mode] + 1; + if (next_rank == RANK_COUNT) + { + FormatEx(display, sizeof(display), "%T: -", + "Rank Info Menu - Next Rank", client); + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T: 0", + "Rank Info Menu - Points needed", client); + menu.AddItem("", display); + } + else + { + FormatEx(display, sizeof(display), "%T: %s", + "Rank Info Menu - Next Rank", client, gC_rankName[next_rank]); + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T: %d", + "Rank Info Menu - Points needed", client, gI_rankThreshold[mode][next_rank] - GOKZ_GL_GetRankPoints(player, mode)); + menu.AddItem("", display); + } +} + +static int MenuHandler_RankInfo(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Cancel) + { + ShowProfile(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +static void ShowPointsInfo(int client) +{ + Menu menu = new Menu(MenuHandler_PointsInfo); + menu.SetTitle("%T - %N", "Points Info Menu - Title", client, profileTargetPlayer[client]); + PointsInfoMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +static void PointsInfoMenuAddItems(int client, Menu menu) +{ + char display[32]; + int player = profileTargetPlayer[client]; + int mode = profileMode[client]; + + FormatEx(display, sizeof(display), "%T: %d", + "Points Info Menu - Overall Points", client, GOKZ_GL_GetPoints(player, mode, TimeType_Nub)); + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T: %d", + "Points Info Menu - Pro Points", client, GOKZ_GL_GetPoints(player, mode, TimeType_Pro)); + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T: %d", + "Points Info Menu - Overall Completion", client, GOKZ_GL_GetFinishes(player, mode, TimeType_Nub)); + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T: %d", + "Points Info Menu - Pro Completion", client, GOKZ_GL_GetFinishes(player, mode, TimeType_Pro)); + menu.AddItem("", display); +} + +static int MenuHandler_PointsInfo(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Cancel) + { + ShowProfile(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} diff --git a/sourcemod/scripting/gokz-quiet.sp b/sourcemod/scripting/gokz-quiet.sp new file mode 100644 index 0000000..06cc246 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet.sp @@ -0,0 +1,151 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdkhooks> +#include <dhooks> + +#include <gokz/core> +#include <gokz/quiet> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Quiet", + author = "DanZay", + description = "Provides options for a quieter KZ experience", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-quiet.txt" + + +#include "gokz-quiet/ambient.sp" +#include "gokz-quiet/soundscape.sp" +#include "gokz-quiet/hideplayers.sp" +#include "gokz-quiet/falldamage.sp" +#include "gokz-quiet/gokz-sounds.sp" +#include "gokz-quiet/options.sp" + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-quiet"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + OnPluginStart_HidePlayers(); + OnPluginStart_FallDamage(); + OnPluginStart_Ambient(); + + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-quiet.phrases"); + + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + GOKZ_OnJoinTeam(client, GetClientTeam(client)); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void GOKZ_OnJoinTeam(int client, int team) +{ + OnJoinTeam_HidePlayers(client, team); +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client)) + { + return; + } + + OnPlayerRunCmdPost_Soundscape(client); +} + + +// =====[ OTHER EVENTS ]===== + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + any qtOption; + if (GOKZ_QT_IsQTOption(option, qtOption)) + { + OnOptionChanged_Options(client, qtOption, newValue); + } +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(topMenu); +} + +// =====[ STOP SOUNDS ]===== + +void StopSounds(int client) +{ + ClientCommand(client, "snd_playsounds Music.StopAllExceptMusic"); + GOKZ_PrintToChat(client, true, "%t", "Stopped Sounds"); +} + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_hide", CommandToggleShowPlayers, "[KZ] Toggle the visibility of other players."); + RegConsoleCmd("sm_stopsound", CommandStopSound, "[KZ] Stop all sounds e.g. map soundscapes (music)."); +} + +public Action CommandStopSound(int client, int args) +{ + StopSounds(client); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-quiet/ambient.sp b/sourcemod/scripting/gokz-quiet/ambient.sp new file mode 100644 index 0000000..a67e20e --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/ambient.sp @@ -0,0 +1,100 @@ +/* + Hide sound effect from ambient_generics. + Credit to Haze - https://github.com/Haze1337/Sound-Manager +*/ + +Handle getPlayerSlot; + +void OnPluginStart_Ambient() +{ + HookSendSound(); +} +static void HookSendSound() +{ + GameData gd = LoadGameConfigFile("gokz-quiet.games"); + + DynamicDetour sendSoundDetour = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Void, ThisPointer_Address); + DHookSetFromConf(sendSoundDetour, gd, SDKConf_Signature, "CGameClient::SendSound"); + DHookAddParam(sendSoundDetour, HookParamType_ObjectPtr); + DHookAddParam(sendSoundDetour, HookParamType_Bool); + if (!DHookEnableDetour(sendSoundDetour, false, DHooks_OnSendSound)) + { + SetFailState("Couldn't enable CGameClient::SendSound detour."); + } + + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(gd, SDKConf_Virtual, "CBaseClient::GetPlayerSlot"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + getPlayerSlot = EndPrepSDKCall(); + if (getPlayerSlot == null) + { + SetFailState("Could not initialize call to CBaseClient::GetPlayerSlot."); + } +} + + +/*struct SoundInfo_t +{ + Vector vOrigin; Offset: 0 | Size: 12 + Vector vDirection Offset: 12 | Size: 12 + Vector vListenerOrigin; Offset: 24 | Size: 12 + const char *pszName; Offset: 36 | Size: 4 + float fVolume; Offset: 40 | Size: 4 + float fDelay; Offset: 44 | Size: 4 + float fTickTime; Offset: 48 | Size: 4 + int nSequenceNumber; Offset: 52 | Size: 4 + int nEntityIndex; Offset: 56 | Size: 4 + int nChannel; Offset: 60 | Size: 4 + int nPitch; Offset: 64 | Size: 4 + int nFlags; Offset: 68 | Size: 4 + unsigned int nSoundNum; Offset: 72 | Size: 4 + int nSpeakerEntity; Offset: 76 | Size: 4 + int nRandomSeed; Offset: 80 | Size: 4 + soundlevel_t Soundlevel; Offset: 84 | Size: 4 + bool bIsSentence; Offset: 88 | Size: 1 + bool bIsAmbient; Offset: 89 | Size: 1 + bool bLooping; Offset: 90 | Size: 1 +};*/ + +//void CGameClient::SendSound( SoundInfo_t &sound, bool isReliable ) +public MRESReturn DHooks_OnSendSound(Address pThis, Handle hParams) +{ + // Check volume + float volume = DHookGetParamObjectPtrVar(hParams, 1, 40, ObjectValueType_Float); + if(volume == 0.0) + { + return MRES_Ignored; + } + + Address pIClient = pThis + view_as<Address>(0x4); + int client = view_as<int>(SDKCall(getPlayerSlot, pIClient)) + 1; + + if(!IsValidClient(client)) + { + return MRES_Ignored; + } + + bool isAmbient = DHookGetParamObjectPtrVar(hParams, 1, 89, ObjectValueType_Bool); + if (!isAmbient) + { + return MRES_Ignored; + } + + float newVolume; + if (GOKZ_QT_GetOption(client, QTOption_AmbientSounds) == -1 || GOKZ_QT_GetOption(client, QTOption_AmbientSounds) == 10) + { + newVolume = volume; + } + else + { + float volumeFactor = float(GOKZ_QT_GetOption(client, QTOption_AmbientSounds)) * 0.1; + newVolume = volume * volumeFactor; + } + + if (newVolume <= 0.0) + { + return MRES_Supercede; + } + DHookSetParamObjectPtrVar(hParams, 1, 40, ObjectValueType_Float, newVolume); + return MRES_ChangedHandled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-quiet/falldamage.sp b/sourcemod/scripting/gokz-quiet/falldamage.sp new file mode 100644 index 0000000..6bd0533 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/falldamage.sp @@ -0,0 +1,40 @@ +/* + Toggle player's fall damage sounds. +*/ + +void OnPluginStart_FallDamage() +{ + AddNormalSoundHook(Hook_NormalSound); +} + +static Action Hook_NormalSound(int clients[MAXPLAYERS], int& numClients, char sample[PLATFORM_MAX_PATH], int& entity, int& channel, float& volume, int& level, int& pitch, int& flags, char soundEntry[PLATFORM_MAX_PATH], int& seed) +{ + if (!StrEqual(soundEntry, "Player.FallDamage")) + { + return Plugin_Continue; + } + + for (int i = 0; i < numClients; i++) + { + int client = clients[i]; + if (!IsValidClient(client)) + { + continue; + } + int clientArray[1]; + clientArray[0] = client; + float newVolume; + if (GOKZ_QT_GetOption(client, QTOption_FallDamageSound) == -1 || GOKZ_QT_GetOption(client, QTOption_FallDamageSound) == 10) + { + newVolume = volume; + } + else + { + float volumeFactor = float(GOKZ_QT_GetOption(client, QTOption_FallDamageSound)) * 0.1; + newVolume = volume * volumeFactor; + } + + EmitSoundEntry(clientArray, 1, soundEntry, sample, entity, channel, level, seed, flags, newVolume, pitch); + } + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-quiet/gokz-sounds.sp b/sourcemod/scripting/gokz-quiet/gokz-sounds.sp new file mode 100644 index 0000000..02cf681 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/gokz-sounds.sp @@ -0,0 +1,71 @@ +/* + Volume options for various GOKZ sounds. +*/ + +public Action GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description) +{ + int volumeFactor = 10; + if (StrEqual(description, "Checkpoint") || StrEqual(description, "Set Start Position")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_CheckpointVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "Checkpoint")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_TeleportVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "Timer Start") || StrEqual(description, "Timer End") || StrEqual(description, "Timer False End") || StrEqual(description, "Missed PB")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_TimerVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "Error")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_ErrorVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "Server Record")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_ServerRecordVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "World Record")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_WorldRecordVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + else if (StrEqual(description, "Jumpstats")) + { + volumeFactor = GOKZ_QT_GetOption(client, QTOption_JumpstatsVolume); + if (volumeFactor == -1) + { + return Plugin_Continue; + } + } + + if (volumeFactor == 10) + { + return Plugin_Continue; + } + volume *= float(volumeFactor) * 0.1; + return Plugin_Changed; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-quiet/hideplayers.sp b/sourcemod/scripting/gokz-quiet/hideplayers.sp new file mode 100644 index 0000000..65736f0 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/hideplayers.sp @@ -0,0 +1,309 @@ +/* + Hide sounds and effects from other players. +*/ + +void OnPluginStart_HidePlayers() +{ + AddNormalSoundHook(Hook_NormalSound); + AddTempEntHook("Shotgun Shot", Hook_ShotgunShot); + AddTempEntHook("EffectDispatch", Hook_EffectDispatch); + HookUserMessage(GetUserMessageId("WeaponSound"), Hook_WeaponSound, true); + + // Lateload support + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client)) + { + OnJoinTeam_HidePlayers(client, GetClientTeam(client)); + } + } +} + +void OnJoinTeam_HidePlayers(int client, int team) +{ + // Make sure client is only ever hooked once + SDKUnhook(client, SDKHook_SetTransmit, OnSetTransmitClient); + + if (team == CS_TEAM_T || team == CS_TEAM_CT) + { + SDKHook(client, SDKHook_SetTransmit, OnSetTransmitClient); + } +} + +Action CommandToggleShowPlayers(int client, int args) +{ + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Disabled) + { + GOKZ_SetOption(client, gC_QTOptionNames[QTOption_ShowPlayers], ShowPlayers_Enabled); + } + else + { + GOKZ_SetOption(client, gC_QTOptionNames[QTOption_ShowPlayers], ShowPlayers_Disabled); + } + return Plugin_Handled; +} + +// =====[ PRIVATE ]===== + +// Hide most of the other players' actions. This function is expensive. +static Action OnSetTransmitClient(int entity, int client) +{ + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Disabled + && entity != client + && entity != GetObserverTarget(client)) + { + return Plugin_Handled; + } + return Plugin_Continue; +} + +// Hide reload sounds. Required if other players were visible at one point during the gameplay. +static Action Hook_WeaponSound(UserMsg msg_id, Protobuf msg, const int[] players, int playersNum, bool reliable, bool init) +{ + int newClients[MAXPLAYERS], newTotal = 0; + int entidx = msg.ReadInt("entidx"); + for (int i = 0; i < playersNum; i++) + { + int client = players[i]; + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled + || entidx == client + || entidx == GetObserverTarget(client)) + { + newClients[newTotal] = client; + newTotal++; + } + } + + // Nothing's changed, let the engine handle it. + if (newTotal == playersNum) + { + return Plugin_Continue; + } + // No one to send to so it doesn't matter if we block or not. We block just to end the function early. + if (newTotal == 0) + { + return Plugin_Handled; + } + // Only way to modify the recipient list is to RequestFrame and create our own user message. + char path[PLATFORM_MAX_PATH]; + msg.ReadString("sound", path, sizeof(path)); + int flags = USERMSG_BLOCKHOOKS; + if (reliable) + { + flags |= USERMSG_RELIABLE; + } + if (init) + { + flags |= USERMSG_INITMSG; + } + + DataPack dp = new DataPack(); + dp.WriteCell(msg_id); + dp.WriteCell(newTotal); + dp.WriteCellArray(newClients, newTotal); + dp.WriteCell(flags); + dp.WriteCell(entidx); + dp.WriteFloat(msg.ReadFloat("origin_x")); + dp.WriteFloat(msg.ReadFloat("origin_y")); + dp.WriteFloat(msg.ReadFloat("origin_z")); + dp.WriteString(path); + dp.WriteFloat(msg.ReadFloat("timestamp")); + + RequestFrame(RequestFrame_WeaponSound, dp); + return Plugin_Handled; +} + +static void RequestFrame_WeaponSound(DataPack dp) +{ + dp.Reset(); + + UserMsg msg_id = dp.ReadCell(); + int newTotal = dp.ReadCell(); + int newClients[MAXPLAYERS]; + dp.ReadCellArray(newClients, newTotal); + int flags = dp.ReadCell(); + + Protobuf newMsg = view_as<Protobuf>(StartMessageEx(msg_id, newClients, newTotal, flags)); + + newMsg.SetInt("entidx", dp.ReadCell()); + newMsg.SetFloat("origin_x", dp.ReadFloat()); + newMsg.SetFloat("origin_y", dp.ReadFloat()); + newMsg.SetFloat("origin_z", dp.ReadFloat()); + char path[PLATFORM_MAX_PATH]; + dp.ReadString(path, sizeof(path)); + newMsg.SetString("sound", path); + newMsg.SetFloat("timestamp", dp.ReadFloat()); + + EndMessage(); + + delete dp; +} + +// Hide various sounds that don't get blocked by SetTransmit hook. +static Action Hook_NormalSound(int clients[MAXPLAYERS], int& numClients, char sample[PLATFORM_MAX_PATH], int& entity, int& channel, float& volume, int& level, int& pitch, int& flags, char soundEntry[PLATFORM_MAX_PATH], int& seed) +{ + if (StrContains(sample, "Player.EquipArmor") != -1 || StrContains(sample, "BaseCombatCharacter.AmmoPickup") != -1) + { + // When the sound is emitted, the owner of these entities are not set yet. + // Hence we cannot do the entity parent stuff below. + // In that case, we just straight up block armor and ammo pickup sounds. + return Plugin_Stop; + } + int ent = entity; + while (ent > MAXPLAYERS) + { + // Block some gun and knife sounds by trying to find its parent entity. + ent = GetEntPropEnt(ent, Prop_Send, "moveparent"); + if (ent < MAXPLAYERS) + { + break; + } + else if (ent == -1) + { + return Plugin_Continue; + } + } + int numNewClients = 0; + for (int i = 0; i < numClients; i++) + { + int client = clients[i]; + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled + || ent == client + || ent == GetObserverTarget(client)) + { + clients[numNewClients] = client; + numNewClients++; + } + } + + if (numNewClients != numClients) + { + numClients = numNewClients; + return Plugin_Changed; + } + + return Plugin_Continue; +} + +// Hide firing sounds. +static Action Hook_ShotgunShot(const char[] te_name, const int[] players, int numClients, float delay) +{ + int newClients[MAXPLAYERS], newTotal = 0; + for (int i = 0; i < numClients; i++) + { + int client = players[i]; + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled + || TE_ReadNum("m_iPlayer") + 1 == GetObserverTarget(client)) + { + newClients[newTotal] = client; + newTotal++; + } + } + + // Noone wants the sound + if (newTotal == 0) + { + return Plugin_Stop; + } + + // Nothing's changed, let the engine handle it. + if (newTotal == numClients) + { + return Plugin_Continue; + } + + float origin[3]; + TE_ReadVector("m_vecOrigin", origin); + + float angles[2]; + angles[0] = TE_ReadFloat("m_vecAngles[0]"); + angles[1] = TE_ReadFloat("m_vecAngles[1]"); + + int weapon = TE_ReadNum("m_weapon"); + int mode = TE_ReadNum("m_iMode"); + int seed = TE_ReadNum("m_iSeed"); + int player = TE_ReadNum("m_iPlayer"); + float inaccuracy = TE_ReadFloat("m_fInaccuracy"); + float recoilIndex = TE_ReadFloat("m_flRecoilIndex"); + float spread = TE_ReadFloat("m_fSpread"); + int itemIdx = TE_ReadNum("m_nItemDefIndex"); + int soundType = TE_ReadNum("m_iSoundType"); + + TE_Start("Shotgun Shot"); + TE_WriteVector("m_vecOrigin", origin); + TE_WriteFloat("m_vecAngles[0]", angles[0]); + TE_WriteFloat("m_vecAngles[1]", angles[1]); + TE_WriteNum("m_weapon", weapon); + TE_WriteNum("m_iMode", mode); + TE_WriteNum("m_iSeed", seed); + TE_WriteNum("m_iPlayer", player); + TE_WriteFloat("m_fInaccuracy", inaccuracy); + TE_WriteFloat("m_flRecoilIndex", recoilIndex); + TE_WriteFloat("m_fSpread", spread); + TE_WriteNum("m_nItemDefIndex", itemIdx); + TE_WriteNum("m_iSoundType", soundType); + + // Send the TE and stop the engine from processing its own. + TE_Send(newClients, newTotal, delay); + return Plugin_Stop; +} + +// Hide knife and blood effect caused by other players. +static Action Hook_EffectDispatch(const char[] te_name, const int[] players, int numClients, float delay) +{ + // Block bullet impact effects. + int effIndex = TE_ReadNum("m_iEffectName"); + if (effIndex != EFFECT_IMPACT && effIndex != EFFECT_KNIFESLASH) + { + return Plugin_Continue; + } + int newClients[MAXPLAYERS], newTotal = 0; + for (int i = 0; i < numClients; i++) + { + int client = players[i]; + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled) + { + newClients[newTotal] = client; + newTotal++; + } + } + // Noone wants the sound + if (newTotal == 0) + { + return Plugin_Stop; + } + + // Nothing's changed, let the engine handle it. + if (newTotal == numClients) + { + return Plugin_Continue; + } + float origin[3], start[3]; + origin[0] = TE_ReadFloat("m_vOrigin.x"); + origin[1] = TE_ReadFloat("m_vOrigin.y"); + origin[2] = TE_ReadFloat("m_vOrigin.z"); + start[0] = TE_ReadFloat("m_vStart.x"); + start[1] = TE_ReadFloat("m_vStart.y"); + start[2] = TE_ReadFloat("m_vStart.z"); + int flags = TE_ReadNum("m_fFlags"); + float scale = TE_ReadFloat("m_flScale"); + int surfaceProp = TE_ReadNum("m_nSurfaceProp"); + int damageType = TE_ReadNum("m_nDamageType"); + int entindex = TE_ReadNum("entindex"); + int positionsAreRelativeToEntity = TE_ReadNum("m_bPositionsAreRelativeToEntity"); + + TE_Start("EffectDispatch"); + TE_WriteNum("m_iEffectName", effIndex); + TE_WriteFloatArray("m_vOrigin.x", origin, 3); + TE_WriteFloatArray("m_vStart.x", start, 3); + TE_WriteFloat("m_flScale", scale); + TE_WriteNum("m_nSurfaceProp", surfaceProp); + TE_WriteNum("m_nDamageType", damageType); + TE_WriteNum("entindex", entindex); + TE_WriteNum("m_bPositionsAreRelativeToEntity", positionsAreRelativeToEntity); + TE_WriteNum("m_fFlags", flags); + + // Send the TE and stop the engine from processing its own. + TE_Send(newClients, newTotal, delay); + return Plugin_Stop; +} diff --git a/sourcemod/scripting/gokz-quiet/options.sp b/sourcemod/scripting/gokz-quiet/options.sp new file mode 100644 index 0000000..a9da3d7 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/options.sp @@ -0,0 +1,206 @@ +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOptions(); +} + +void RegisterOptions() +{ + for (QTOption option; option < QTOPTION_COUNT; option++) + { + GOKZ_RegisterOption(gC_QTOptionNames[option], gC_QTOptionDescriptions[option], + OptionType_Int, gI_QTOptionDefaultValues[option], 0, gI_QTOptionCounts[option] - 1); + } +} + +void OnOptionChanged_Options(int client, QTOption option, any newValue) +{ + if (option == QTOption_Soundscapes && newValue == Soundscapes_Enabled) + { + EnableSoundscape(client); + } + PrintOptionChangeMessage(client, option, newValue); +} + +void PrintOptionChangeMessage(int client, QTOption option, any newValue) +{ + switch (option) + { + case QTOption_ShowPlayers: + { + switch (newValue) + { + case ShowPlayers_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Players - Disable"); + } + case ShowPlayers_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Show Players - Enable"); + } + } + } + case QTOption_Soundscapes: + { + switch (newValue) + { + case Soundscapes_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Soundscapes - Disable"); + } + case Soundscapes_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Soundscapes - Enable"); + } + } + } + } +} + +// =====[ OPTIONS MENU ]===== + +TopMenu gTM_Options; +TopMenuObject gTMO_CatQuiet; +TopMenuObject gTMO_ItemsQuiet[QTOPTION_COUNT]; + +void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu && gTMO_CatQuiet != INVALID_TOPMENUOBJECT) + { + return; + } + + gTMO_CatQuiet = topMenu.AddCategory(QUIET_OPTION_CATEGORY, TopMenuHandler_Categories); +} + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + // Make sure category exists + if (gTMO_CatQuiet == INVALID_TOPMENUOBJECT) + { + GOKZ_OnOptionsMenuCreated(topMenu); + } + + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + + // Add gokz-profile option items + for (int option = 0; option < view_as<int>(QTOPTION_COUNT); option++) + { + gTMO_ItemsQuiet[option] = gTM_Options.AddItem(gC_QTOptionNames[option], TopMenuHandler_QT, gTMO_CatQuiet); + } +} + +public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle) + { + if (topobj_id == gTMO_CatQuiet) + { + Format(buffer, maxlength, "%T", "Options Menu - Quiet", param); + } + } +} + +public void TopMenuHandler_QT(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + QTOption option = QTOPTION_INVALID; + for (int i = 0; i < view_as<int>(QTOPTION_COUNT); i++) + { + if (topobj_id == gTMO_ItemsQuiet[i]) + { + option = view_as<QTOption>(i); + break; + } + } + + if (option == QTOPTION_INVALID) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + switch (option) + { + case QTOption_ShowPlayers: + { + FormatToggleableOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_Soundscapes: + { + FormatToggleableOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_FallDamageSound: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_AmbientSounds: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_CheckpointVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_TeleportVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_TimerVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_ErrorVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_ServerRecordVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_WorldRecordVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + case QTOption_JumpstatsVolume: + { + FormatVolumeOptionDisplay(param, option, buffer, maxlength); + } + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, gC_QTOptionNames[option]); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +} + +void FormatToggleableOptionDisplay(int client, QTOption option, char[] buffer, int maxlength) +{ + if (GOKZ_GetOption(client, gC_QTOptionNames[option]) == 0) + { + FormatEx(buffer, maxlength, "%T - %T", + gC_QTOptionPhrases[option], client, + "Options Menu - Disabled", client); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + gC_QTOptionPhrases[option], client, + "Options Menu - Enabled", client); + } +} + +void FormatVolumeOptionDisplay(int client, QTOption option, char[] buffer, int maxlength) +{ + // Assume 10% volume steps. + FormatEx(buffer, maxlength, "%T - %i%", + gC_QTOptionPhrases[option], client, + GOKZ_QT_GetOption(client, option) * 10); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-quiet/soundscape.sp b/sourcemod/scripting/gokz-quiet/soundscape.sp new file mode 100644 index 0000000..f320ad3 --- /dev/null +++ b/sourcemod/scripting/gokz-quiet/soundscape.sp @@ -0,0 +1,30 @@ +/* + Toggle soundscapes. +*/ + +static int currentSoundscapeIndex[MAXPLAYERS + 1] = {BLANK_SOUNDSCAPEINDEX, ...}; + +void EnableSoundscape(int client) +{ + if (currentSoundscapeIndex[client] != BLANK_SOUNDSCAPEINDEX) + { + SetEntProp(client, Prop_Data, "soundscapeIndex", currentSoundscapeIndex[client]); + } +} + +void OnPlayerRunCmdPost_Soundscape(int client) +{ + int soundscapeIndex = GetEntProp(client, Prop_Data, "soundscapeIndex"); + if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_Soundscapes]) == Soundscapes_Disabled) + { + if (soundscapeIndex != BLANK_SOUNDSCAPEINDEX) + { + currentSoundscapeIndex[client] = soundscapeIndex; + } + SetEntProp(client, Prop_Data, "soundscapeIndex", BLANK_SOUNDSCAPEINDEX); + } + else + { + currentSoundscapeIndex[client] = soundscapeIndex; + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing.sp b/sourcemod/scripting/gokz-racing.sp new file mode 100644 index 0000000..416d28a --- /dev/null +++ b/sourcemod/scripting/gokz-racing.sp @@ -0,0 +1,174 @@ +#include <sourcemod> + +#include <gokz/core> +#include <gokz/racing> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Racing", + author = "DanZay", + description = "Lets players race against each other", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-racing.txt" + +#include "gokz-racing/announce.sp" +#include "gokz-racing/api.sp" +#include "gokz-racing/commands.sp" +#include "gokz-racing/duel_menu.sp" +#include "gokz-racing/race.sp" +#include "gokz-racing/race_menu.sp" +#include "gokz-racing/racer.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-racing"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-racing.phrases"); + + CreateGlobalForwards(); + RegisterCommands(); + + OnPluginStart_Race(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_Racer(client); +} + +public void OnClientDisconnect(int client) +{ + OnClientDisconnect_Racer(client); +} + +public Action GOKZ_OnTimerStart(int client, int course) +{ + Action action = OnTimerStart_Racer(client, course); + if (action != Plugin_Continue) + { + return action; + } + + return Plugin_Continue; +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + OnTimerStart_Post_Racer(client); +} + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + FinishRacer(client, course); +} + +public Action GOKZ_OnMakeCheckpoint(int client) +{ + Action action = OnMakeCheckpoint_Racer(client); + if (action != Plugin_Continue) + { + return action; + } + + return Plugin_Continue; +} + +public void GOKZ_OnMakeCheckpoint_Post(int client) +{ + OnMakeCheckpoint_Post_Racer(client); +} + +public Action GOKZ_OnUndoTeleport(int client) +{ + Action action = OnUndoTeleport_Racer(client); + if (action != Plugin_Continue) + { + return action; + } + + return Plugin_Continue; +} + +public void GOKZ_RC_OnFinish(int client, int raceID, int place) +{ + OnFinish_Announce(client, raceID, place); + OnFinish_Race(raceID); +} + +public void GOKZ_RC_OnSurrender(int client, int raceID) +{ + OnSurrender_Announce(client, raceID); +} + +public void GOKZ_RC_OnRequestReceived(int client, int raceID) +{ + OnRequestReceived_Announce(client, raceID); +} + +public void GOKZ_RC_OnRequestAccepted(int client, int raceID) +{ + OnRequestAccepted_Announce(client, raceID); + OnRequestAccepted_Race(raceID); +} + +public void GOKZ_RC_OnRequestDeclined(int client, int raceID, bool timeout) +{ + OnRequestDeclined_Announce(client, raceID, timeout); + OnRequestDeclined_Race(raceID); +} + + + +// =====[ OTHER EVENTS ]===== + +public void GOKZ_RC_OnRaceStarted(int raceID) +{ + OnRaceStarted_Announce(raceID); +} + +public void GOKZ_RC_OnRaceAborted(int raceID) +{ + OnRaceAborted_Announce(raceID); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/announce.sp b/sourcemod/scripting/gokz-racing/announce.sp new file mode 100644 index 0000000..7b6a920 --- /dev/null +++ b/sourcemod/scripting/gokz-racing/announce.sp @@ -0,0 +1,229 @@ +/* + Chat messages of race and racer events. +*/ + + + +// =====[ PUBLIC ]===== + +/** + * Prints a message to chat for all clients in a race, formatting colours + * and optionally adding the chat prefix. If using the chat prefix, specify + * a colour at the beginning of the message e.g. "{default}Hello!". + * + * @param raceID ID of the race. + * @param specs Whether to also include racer spectators. + * @param addPrefix Whether to add the chat prefix. + * @param format Formatting rules. + * @param any Variable number of format parameters. + */ +void PrintToChatAllInRace(int raceID, bool specs, bool addPrefix, const char[] format, any...) +{ + char buffer[1024]; + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && GetRaceID(client) == raceID) + { + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), format, 5); + GOKZ_PrintToChat(client, addPrefix, buffer); + + if (specs) + { + for (int target = 1; target <= MaxClients; target++) + { + if (IsClientInGame(target) && GetObserverTarget(target) == client && GetRaceID(target) != raceID) + { + SetGlobalTransTarget(target); + VFormat(buffer, sizeof(buffer), format, 5); + GOKZ_PrintToChat(target, addPrefix, buffer); + } + } + } + } + } +} + + + +// =====[ EVENTS ]===== + +void OnFinish_Announce(int client, int raceID, int place) +{ + switch (GetRaceInfo(raceID, RaceInfo_Type)) + { + case RaceType_Normal: + { + if (place == 1) + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Won", client); + } + else + { + ArrayList unfinishedRacers = GetUnfinishedRacers(raceID); + if (unfinishedRacers.Length >= 1) + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Placed", client, place); + } + else + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Lost", client, place); + } + delete unfinishedRacers; + } + } + case RaceType_Duel: + { + ArrayList unfinishedRacers = GetUnfinishedRacers(raceID); + if (unfinishedRacers.Length == 1) + { + int opponent = unfinishedRacers.Get(0); + GOKZ_PrintToChatAll(true, "%t", "Duel Won", client, opponent); + } + delete unfinishedRacers; + } + } +} + +void OnSurrender_Announce(int client, int raceID) +{ + switch (GetRaceInfo(raceID, RaceInfo_Type)) + { + case RaceType_Normal: + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Surrendered", client); + } + case RaceType_Duel: + { + ArrayList unfinishedRacers = GetUnfinishedRacers(raceID); + if (unfinishedRacers.Length == 1) + { + int opponent = unfinishedRacers.Get(0); + GOKZ_PrintToChatAll(true, "%t", "Duel Surrendered", client, opponent); + } + delete unfinishedRacers; + } + } +} + +void OnRequestReceived_Announce(int client, int raceID) +{ + int host = GetRaceHost(raceID); + + switch (GetRaceInfo(raceID, RaceInfo_Type)) + { + case RaceType_Normal: + { + GOKZ_PrintToChat(client, true, "%t", "Race Request Received", host); + } + case RaceType_Duel: + { + GOKZ_PrintToChat(client, true, "%t", "Duel Request Received", host); + } + } + + int cpRule = GetRaceInfo(raceID, RaceInfo_CheckpointRule); + int cdRule = GetRaceInfo(raceID, RaceInfo_CooldownRule); + int mode = GetRaceInfo(raceID, RaceInfo_Mode); + int course = GetRaceInfo(raceID, RaceInfo_Course); + + char courseStr[32]; + if (course == 0) + { + FormatEx(courseStr, sizeof(courseStr), "%T", "Race Rules - Main Course", client); + } + else + { + FormatEx(courseStr, sizeof(courseStr), "%T %d", "Race Rules - Bonus Course", client, course); + } + + if (cpRule == -1 && cdRule == 0) + { + GOKZ_PrintToChat(client, false, "%t", "Race Rules - Unlimited", gC_ModeNames[mode], courseStr); + } + if (cpRule == -1 && cdRule > 0) + { + GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Cooldown", gC_ModeNames[mode], courseStr, cdRule); + } + if (cpRule == 0) + { + GOKZ_PrintToChat(client, false, "%t", "Race Rules - No Checkpoints", gC_ModeNames[mode], courseStr); + } + if (cpRule > 0 && cdRule == 0) + { + GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Checkpoints", gC_ModeNames[mode], courseStr, cpRule); + } + if (cpRule > 0 && cdRule > 0) + { + GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited", gC_ModeNames[mode], courseStr, cpRule, cdRule); + } + + GOKZ_PrintToChat(client, false, "%t", "You Have Seconds To Accept", RoundFloat(RC_REQUEST_TIMEOUT_TIME)); +} + +void OnRequestAccepted_Announce(int client, int raceID) +{ + int host = GetRaceHost(raceID); + + switch (GetRaceInfo(raceID, RaceInfo_Type)) + { + case RaceType_Normal: + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Request Accepted", client, host); + } + case RaceType_Duel: + { + GOKZ_PrintToChatAll(true, "%t", "Duel Request Accepted", client, host); + } + } +} + +void OnRequestDeclined_Announce(int client, int raceID, bool timeout) +{ + int host = GetRaceHost(raceID); + + if (timeout) + { + switch (GetRaceInfo(raceID, RaceInfo_Type)) + { + case RaceType_Normal: + { + GOKZ_PrintToChat(client, true, "%t", "Race Request Not Accepted In Time (Target)"); + GOKZ_PrintToChat(host, true, "%t", "Race Request Not Accepted In Time (Host)", client); + } + case RaceType_Duel: + { + GOKZ_PrintToChat(client, true, "%t", "Duel Request Not Accepted In Time (Target)"); + GOKZ_PrintToChat(host, true, "%t", "Duel Request Not Accepted In Time (Host)", client); + } + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "You Have Declined"); + GOKZ_PrintToChat(host, true, "%t", "Player Has Declined", client); + } +} + +void OnRaceStarted_Announce(int raceID) +{ + if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Normal) + { + PrintToChatAllInRace(raceID, true, true, "%t", "Race Host Started Countdown", GetRaceHost(raceID)); + } +} + +void OnRaceAborted_Announce(int raceID) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && GetRaceID(client) == raceID) + { + GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted"); + if (GetStatus(client) == RacerStatus_Racing) + { + GOKZ_PlayErrorSound(client); + } + } + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/api.sp b/sourcemod/scripting/gokz-racing/api.sp new file mode 100644 index 0000000..13d82a3 --- /dev/null +++ b/sourcemod/scripting/gokz-racing/api.sp @@ -0,0 +1,107 @@ +static GlobalForward H_OnFinish; +static GlobalForward H_OnSurrender; +static GlobalForward H_OnRequestReceived; +static GlobalForward H_OnRequestAccepted; +static GlobalForward H_OnRequestDeclined; +static GlobalForward H_OnRaceRegistered; +static GlobalForward H_OnRaceInfoChanged; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnFinish = new GlobalForward("GOKZ_RC_OnFinish", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + H_OnSurrender = new GlobalForward("GOKZ_RC_OnSurrender", ET_Ignore, Param_Cell, Param_Cell); + H_OnRequestReceived = new GlobalForward("GOKZ_RC_OnRequestReceived", ET_Ignore, Param_Cell, Param_Cell); + H_OnRequestAccepted = new GlobalForward("GOKZ_RC_OnRequestAccepted", ET_Ignore, Param_Cell, Param_Cell); + H_OnRequestDeclined = new GlobalForward("GOKZ_RC_OnRequestDeclined", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + H_OnRaceRegistered = new GlobalForward("GOKZ_RC_OnRaceRegistered", ET_Ignore, Param_Cell); + H_OnRaceInfoChanged = new GlobalForward("GOKZ_RC_OnRaceInfoChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell); +} + +void Call_OnFinish(int client, int raceID, int place) +{ + Call_StartForward(H_OnFinish); + Call_PushCell(client); + Call_PushCell(raceID); + Call_PushCell(place); + Call_Finish(); +} + +void Call_OnSurrender(int client, int raceID) +{ + Call_StartForward(H_OnSurrender); + Call_PushCell(client); + Call_PushCell(raceID); + Call_Finish(); +} + +void Call_OnRequestReceived(int client, int raceID) +{ + Call_StartForward(H_OnRequestReceived); + Call_PushCell(client); + Call_PushCell(raceID); + Call_Finish(); +} + +void Call_OnRequestAccepted(int client, int raceID) +{ + Call_StartForward(H_OnRequestAccepted); + Call_PushCell(client); + Call_PushCell(raceID); + Call_Finish(); +} + +void Call_OnRequestDeclined(int client, int raceID, bool timeout) +{ + Call_StartForward(H_OnRequestDeclined); + Call_PushCell(client); + Call_PushCell(raceID); + Call_PushCell(timeout); + Call_Finish(); +} + +void Call_OnRaceRegistered(int raceID) +{ + Call_StartForward(H_OnRaceRegistered); + Call_PushCell(raceID); + Call_Finish(); +} + +void Call_OnRaceInfoChanged(int raceID, RaceInfo infoIndex, int oldValue, int newValue) +{ + Call_StartForward(H_OnRaceInfoChanged); + Call_PushCell(raceID); + Call_PushCell(infoIndex); + Call_PushCell(oldValue); + Call_PushCell(newValue); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_RC_GetRaceInfo", Native_GetRaceInfo); + CreateNative("GOKZ_RC_GetStatus", Native_GetStatus); + CreateNative("GOKZ_RC_GetRaceID", Native_GetRaceID); +} + +public int Native_GetRaceInfo(Handle plugin, int numParams) +{ + return GetRaceInfo(GetNativeCell(1), GetNativeCell(2)); +} + +public int Native_GetStatus(Handle plugin, int numParams) +{ + return GetStatus(GetNativeCell(1)); +} + +public int Native_GetRaceID(Handle plugin, int numParams) +{ + return GetRaceID(GetNativeCell(1)); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/commands.sp b/sourcemod/scripting/gokz-racing/commands.sp new file mode 100644 index 0000000..9fbd7ab --- /dev/null +++ b/sourcemod/scripting/gokz-racing/commands.sp @@ -0,0 +1,47 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_accept", CommandAccept, "[KZ] Accept an incoming race request."); + RegConsoleCmd("sm_decline", CommandDecline, "[KZ] Decline an incoming race request."); + RegConsoleCmd("sm_surrender", CommandSurrender, "[KZ] Surrender your race."); + RegConsoleCmd("sm_duel", CommandDuel, "[KZ] Open the duel menu."); + RegConsoleCmd("sm_challenge", CommandDuel, "[KZ] Open the duel menu."); + RegConsoleCmd("sm_abort", CommandAbort, "[KZ] Abort the race you are hosting."); + + RegAdminCmd("sm_race", CommandRace, ADMFLAG_RESERVATION, "[KZ] Open the race hosting menu."); +} + +public Action CommandAccept(int client, int args) +{ + AcceptRequest(client); + return Plugin_Handled; +} + +public Action CommandDecline(int client, int args) +{ + DeclineRequest(client); + return Plugin_Handled; +} + +public Action CommandSurrender(int client, int args) +{ + SurrenderRacer(client); + return Plugin_Handled; +} + +public Action CommandDuel(int client, int args) +{ + DisplayDuelMenu(client); + return Plugin_Handled; +} + +public Action CommandAbort(int client, int args) +{ + AbortHostedRace(client); + return Plugin_Handled; +} + +public Action CommandRace(int client, int args) +{ + DisplayRaceMenu(client); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/duel_menu.sp b/sourcemod/scripting/gokz-racing/duel_menu.sp new file mode 100644 index 0000000..44c519f --- /dev/null +++ b/sourcemod/scripting/gokz-racing/duel_menu.sp @@ -0,0 +1,534 @@ +/* + A menu for initiating 1v1 races. +*/ + + + +#define ITEM_INFO_CHALLENGE "ch" +#define ITEM_INFO_ABORT "ab" +#define ITEM_INFO_MODE "md" +#define ITEM_INFO_COURSE "co" +#define ITEM_INFO_TELEPORT "tp" + +static int duelMenuMode[MAXPLAYERS + 1]; +static int duelMenuCourse[MAXPLAYERS + 1]; +static int duelMenuCheckpointLimit[MAXPLAYERS + 1]; +static int duelMenuCheckpointCooldown[MAXPLAYERS + 1]; + + + +// =====[ PICK MODE ]===== + +void DisplayDuelMenu(int client, bool reset = true) +{ + if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Duel)) + { + GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race"); + GOKZ_PlayErrorSound(client); + return; + } + + if (reset) + { + duelMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode); + } + + Menu menu = new Menu(MenuHandler_Duel); + menu.SetTitle("%T", "Duel Menu - Title", client); + DuelMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_Duel(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + + if (StrEqual(info, ITEM_INFO_CHALLENGE, false)) + { + if (!DisplayDuelOpponentMenu(param1)) + { + DisplayDuelMenu(param1, false); + } + } + else if (StrEqual(info, ITEM_INFO_ABORT, false)) + { + AbortHostedRace(param1); + DisplayDuelMenu(param1, false); + } + else if (StrEqual(info, ITEM_INFO_MODE, false)) + { + DisplayDuelModeMenu(param1); + } + else if (StrEqual(info, ITEM_INFO_COURSE, false)) + { + int course = duelMenuCourse[param1]; + do + { + course++; + if (!GOKZ_IsValidCourse(course)) + { + course = 0; + } + } while (!GOKZ_GetCourseRegistered(course) && course != duelMenuCourse[param1]); + duelMenuCourse[param1] = course; + DisplayDuelMenu(param1, false); + } + else if (StrEqual(info, ITEM_INFO_TELEPORT, false)) + { + DisplayDuelCheckpointMenu(param1); + } + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +void DuelMenuAddItems(int client, Menu menu) +{ + char display[64]; + + menu.RemoveAllItems(); + + FormatEx(display, sizeof(display), "%T", "Duel Menu - Choose Opponent", client); + menu.AddItem(ITEM_INFO_CHALLENGE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + + FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client); + menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + FormatEx(display, sizeof(display), "%s", gC_ModeNames[duelMenuMode[client]]); + menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + + if (duelMenuCourse[client] == 0) + { + FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client); + } + else + { + FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, duelMenuCourse[client]); + } + menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + + FormatEx(display, sizeof(display), "%s", GetDuelRuleSummary(client, duelMenuCheckpointLimit[client], duelMenuCheckpointCooldown[client])); + menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); +} + + + +// =====[ MODE MENU ]===== + +static void DisplayDuelModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_DuelMode); + menu.ExitButton = false; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Mode Rule Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, true); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_DuelMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + duelMenuMode[param1] = param2; + DisplayDuelMenu(param1, false); + } + else if (action == MenuAction_Cancel) + { + DisplayDuelMenu(param1, false); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ CHECKPOINT MENU ]===== + +static void DisplayDuelCheckpointMenu(int client) +{ + Menu menu = new Menu(MenuHandler_DuelCheckpoint); + menu.ExitButton = false; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client); + DuelCheckpointMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_DuelCheckpoint(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case CheckpointRule_None: + { + duelMenuCheckpointCooldown[param1] = 0; + duelMenuCheckpointLimit[param1] = 0; + DisplayDuelMenu(param1, false); + } + case CheckpointRule_Limit: + { + DisplayCheckpointLimitMenu(param1); + } + case CheckpointRule_Cooldown: + { + DisplayCheckpointCooldownMenu(param1); + } + case CheckpointRule_Unlimited: + { + duelMenuCheckpointCooldown[param1] = 0; + duelMenuCheckpointLimit[param1] = -1; + DisplayDuelMenu(param1, false); + } + } + } + else if (action == MenuAction_Cancel) + { + DisplayDuelMenu(param1, false); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +void DuelCheckpointMenuAddItems(int client, Menu menu) +{ + char display[32]; + + menu.RemoveAllItems(); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client); + menu.AddItem("", display); + + if (duelMenuCheckpointLimit[client] == -1) + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client); + } + else + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, duelMenuCheckpointLimit[client]); + } + menu.AddItem("", display); + + if (duelMenuCheckpointCooldown[client] == 0) + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client); + } + else + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, duelMenuCheckpointCooldown[client]); + } + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client); + menu.AddItem("", display); +} + + + +// =====[ CP LIMIT MENU ]===== + +static void DisplayCheckpointLimitMenu(int client) +{ + char display[32]; + + Menu menu = new Menu(MenuHandler_DuelCheckpointLimit); + menu.ExitButton = false; + menu.ExitBackButton = true; + + if (duelMenuCheckpointLimit[client] == -1) + { + menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client); + } + else + { + menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, duelMenuCheckpointLimit[client]); + } + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client); + menu.AddItem("+1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client); + menu.AddItem("+5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client); + menu.AddItem("-1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client); + menu.AddItem("-5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client); + menu.AddItem("Unlimited", display); + + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_DuelCheckpointLimit(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char item[32]; + menu.GetItem(param2, item, sizeof(item)); + if (StrEqual(item, "+1")) + { + if (duelMenuCheckpointLimit[param1] == -1) + { + duelMenuCheckpointLimit[param1]++; + } + duelMenuCheckpointLimit[param1]++; + } + if (StrEqual(item, "+5")) + { + if (duelMenuCheckpointLimit[param1] == -1) + { + duelMenuCheckpointLimit[param1]++; + } + duelMenuCheckpointLimit[param1] += 5; + } + if (StrEqual(item, "-1")) + { + duelMenuCheckpointLimit[param1]--; + } + if (StrEqual(item, "-5")) + { + duelMenuCheckpointLimit[param1] -= 5; + } + if (StrEqual(item, "Unlimited")) + { + duelMenuCheckpointLimit[param1] = -1; + DisplayDuelCheckpointMenu(param1); + return 0; + } + + duelMenuCheckpointLimit[param1] = duelMenuCheckpointLimit[param1] < 0 ? 0 : duelMenuCheckpointLimit[param1]; + DisplayCheckpointLimitMenu(param1); + } + else if (action == MenuAction_Cancel) + { + DisplayDuelCheckpointMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ CP COOLDOWN MENU ]===== + +static void DisplayCheckpointCooldownMenu(int client) +{ + char display[32]; + + Menu menu = new Menu(MenuHandler_DuelCPCooldown); + menu.ExitButton = false; + menu.ExitBackButton = true; + + if (duelMenuCheckpointCooldown[client] == -1) + { + menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client); + } + else + { + menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, duelMenuCheckpointCooldown[client]); + } + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client); + menu.AddItem("+1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client); + menu.AddItem("+5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client); + menu.AddItem("-1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client); + menu.AddItem("-5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client); + menu.AddItem("None", display); + + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_DuelCPCooldown(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char item[32]; + menu.GetItem(param2, item, sizeof(item)); + if (StrEqual(item, "+1")) + { + if (duelMenuCheckpointCooldown[param1] == -1) + { + duelMenuCheckpointCooldown[param1]++; + } + duelMenuCheckpointCooldown[param1]++; + } + if (StrEqual(item, "+5")) + { + if (duelMenuCheckpointCooldown[param1] == -1) + { + duelMenuCheckpointCooldown[param1]++; + } + duelMenuCheckpointCooldown[param1] += 5; + } + if (StrEqual(item, "-1")) + { + duelMenuCheckpointCooldown[param1]--; + } + if (StrEqual(item, "-5")) + { + duelMenuCheckpointCooldown[param1] -= 5; + } + if (StrEqual(item, "None")) + { + duelMenuCheckpointCooldown[param1] = 0; + DisplayDuelCheckpointMenu(param1); + return 0; + } + + duelMenuCheckpointCooldown[param1] = duelMenuCheckpointCooldown[param1] < 0 ? 0 : duelMenuCheckpointCooldown[param1]; + DisplayCheckpointCooldownMenu(param1); + } + else if (action == MenuAction_Cancel) + { + DisplayDuelCheckpointMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ OPPONENT MENU ]===== + +static bool DisplayDuelOpponentMenu(int client) +{ + Menu menu = new Menu(MenuHandler_DuelOpponent); + menu.ExitButton = false; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Duel Opponent Selection Menu - Title", client); + if (DuelOpponentMenuAddItems(client, menu) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Opponents Available"); + GOKZ_PlayErrorSound(client); + delete menu; + return false; + } + menu.Display(client, MENU_TIME_FOREVER); + + return true; +} + +static int DuelOpponentMenuAddItems(int client, Menu menu) +{ + int count = 0; + for (int i = 1; i <= MaxClients; i++) + { + char display[MAX_NAME_LENGTH]; + if (i != client && IsClientInGame(i) && !IsFakeClient(i) && !InRace(i)) + { + FormatEx(display, sizeof(display), "%N", i); + menu.AddItem(IntToStringEx(GetClientUserId(i)), display); + count++; + } + } + return count; +} + +public int MenuHandler_DuelOpponent(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + int target = GetClientOfUserId(StringToInt(info)); + if (IsValidClient(target)) + { + if (SendDuelRequest(param1, target)) + { + GOKZ_PrintToChat(param1, true, "%t", "Duel Request Sent", target); + } + else + { + DisplayDuelOpponentMenu(param1); + } + } + else + { + GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid"); + GOKZ_PlayErrorSound(param1); + DisplayDuelOpponentMenu(param1); + } + } + else if (action == MenuAction_Cancel) + { + DisplayDuelMenu(param1, false); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +static bool SendDuelRequest(int host, int target) +{ + if (InRace(target)) + { + GOKZ_PrintToChat(host, true, "%t", "Player Already In A Race", target); + GOKZ_PlayErrorSound(host); + return false; + } + + HostRace(host, RaceType_Duel, duelMenuCourse[host], duelMenuMode[host], duelMenuCheckpointLimit[host], duelMenuCheckpointCooldown[host]); + return SendRequest(host, target); +} + + + +// =====[ PRIVATE ]===== + +char[] GetDuelRuleSummary(int client, int checkpointLimit, int checkpointCooldown) +{ + char rulesString[64]; + if (checkpointLimit == -1 && checkpointCooldown == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client); + } + else if (checkpointLimit > 0 && checkpointCooldown == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit); + } + else if (checkpointLimit == -1 && checkpointCooldown > 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown); + } + else if (checkpointLimit > 0 && checkpointCooldown > 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown); + } + else if (checkpointLimit == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client); + } + + return rulesString; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/race.sp b/sourcemod/scripting/gokz-racing/race.sp new file mode 100644 index 0000000..5ddc2a8 --- /dev/null +++ b/sourcemod/scripting/gokz-racing/race.sp @@ -0,0 +1,221 @@ +/* + Race info storing and accessing using a StringMap. + Each race is given a unique race ID when created. + See the RaceInfo enum for what information is accessible. + See the RaceStatus enum for possible race states. +*/ + + + +static StringMap raceInfo; +static int lastRaceID; + + + +// =====[ GENERAL ]===== + +int GetRaceInfo(int raceID, RaceInfo prop) +{ + ArrayList info; + if (raceInfo.GetValue(IntToStringEx(raceID), info)) + { + return info.Get(view_as<int>(prop)); + } + else + { + return -1; + } +} + +static bool SetRaceInfo(int raceID, RaceInfo prop, int value) +{ + ArrayList info; + if (raceInfo.GetValue(IntToStringEx(raceID), info)) + { + int oldValue = info.Get(view_as<int>(prop)); + if (oldValue != value) + { + info.Set(view_as<int>(prop), value); + Call_OnRaceInfoChanged(raceID, prop, oldValue, value); + } + return true; + } + else + { + return false; + } +} + +int IncrementFinishedRacerCount(int raceID) +{ + int finishedRacers = GetRaceInfo(raceID, RaceInfo_FinishedRacerCount) + 1; + SetRaceInfo(raceID, RaceInfo_FinishedRacerCount, finishedRacers); + return finishedRacers; +} + +int GetRaceHost(int raceID) +{ + return GetClientOfUserId(GetRaceInfo(raceID, RaceInfo_HostUserID)); +} + +ArrayList GetUnfinishedRacers(int raceID) +{ + ArrayList racers = new ArrayList(); + for (int i = 1; i <= MaxClients; i++) + { + if (GetRaceID(i) == raceID && !IsFinished(i)) + { + racers.Push(i); + } + } + return racers; +} + +int GetUnfinishedRacersCount(int raceID) +{ + ArrayList racers = GetUnfinishedRacers(raceID); + int count = racers.Length; + delete racers; + return count; +} + +ArrayList GetAcceptedRacers(int raceID) +{ + ArrayList racers = new ArrayList(); + for (int i = 1; i <= MaxClients; i++) + { + if (GetRaceID(i) == raceID && IsAccepted(i)) + { + racers.Push(i); + } + } + return racers; +} + +int GetAcceptedRacersCount(int raceID) +{ + ArrayList racers = GetAcceptedRacers(raceID); + int count = racers.Length; + delete racers; + return count; +} + + + +// =====[ REGISTRATION ]===== + +int RegisterRace(int host, int type, int course, int mode, int checkpointRule, int cooldownRule) +{ + int raceID = ++lastRaceID; + + ArrayList info = new ArrayList(1, view_as<int>(RACEINFO_COUNT)); + info.Set(view_as<int>(RaceInfo_ID), raceID); + info.Set(view_as<int>(RaceInfo_Status), RaceStatus_Pending); + info.Set(view_as<int>(RaceInfo_HostUserID), GetClientUserId(host)); + info.Set(view_as<int>(RaceInfo_FinishedRacerCount), 0); + info.Set(view_as<int>(RaceInfo_Type), type); + info.Set(view_as<int>(RaceInfo_Course), course); + info.Set(view_as<int>(RaceInfo_Mode), mode); + info.Set(view_as<int>(RaceInfo_CheckpointRule), checkpointRule); + info.Set(view_as<int>(RaceInfo_CooldownRule), cooldownRule); + + raceInfo.SetValue(IntToStringEx(raceID), info); + + Call_OnRaceRegistered(raceID); + + return raceID; +} + +static void UnregisterRace(int raceID) +{ + ArrayList info; + if (raceInfo.GetValue(IntToStringEx(raceID), info)) + { + delete info; + raceInfo.Remove(IntToStringEx(raceID)); + } +} + + + +// =====[ START ]===== + +bool StartRace(int raceID) +{ + SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Countdown); + + for (int client = 1; client <= MaxClients; client++) + { + if (GetRaceID(client) == raceID) + { + StartRacer(client); + GOKZ_PrintToChat(client, true, "%t", "Race Countdown Started"); + } + } + + CreateTimer(RC_COUNTDOWN_TIME, Timer_EndCountdown, raceID); + + return true; +} + +public Action Timer_EndCountdown(Handle timer, int raceID) +{ + SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Started); + return Plugin_Continue; +} + + + +// =====[ ABORT ]===== + +bool AbortRace(int raceID) +{ + SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Aborting); + + for (int client = 1; client <= MaxClients; client++) + { + if (GetRaceID(client) == raceID) + { + AbortRacer(client); + GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted"); + GOKZ_PlayErrorSound(client); + } + } + + UnregisterRace(raceID); + + return true; +} + + + +// =====[ EVENTS ]===== + +void OnPluginStart_Race() +{ + raceInfo = new StringMap(); +} + +void OnFinish_Race(int raceID) +{ + if (GetUnfinishedRacersCount(raceID) == 0) + { + UnregisterRace(raceID); + } +} + +void OnRequestAccepted_Race(int raceID) +{ + if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel) + { + StartRace(raceID); + } +} + +void OnRequestDeclined_Race(int raceID) +{ + if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel) + { + AbortRace(raceID); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/race_menu.sp b/sourcemod/scripting/gokz-racing/race_menu.sp new file mode 100644 index 0000000..0518a2a --- /dev/null +++ b/sourcemod/scripting/gokz-racing/race_menu.sp @@ -0,0 +1,464 @@ +/* + A menu for hosting big races. +*/ + + + +#define ITEM_INFO_START "st" +#define ITEM_INFO_ABORT "ab" +#define ITEM_INFO_INVITE "iv" +#define ITEM_INFO_MODE "md" +#define ITEM_INFO_COURSE "co" +#define ITEM_INFO_TELEPORT "tp" + +static int raceMenuMode[MAXPLAYERS + 1]; +static int raceMenuCourse[MAXPLAYERS + 1]; +static int raceMenuCheckpointLimit[MAXPLAYERS + 1]; +static int raceMenuCheckpointCooldown[MAXPLAYERS + 1]; + + + +// =====[ PICK MODE ]===== + +void DisplayRaceMenu(int client, bool reset = true) +{ + if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Normal)) + { + GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race"); + GOKZ_PlayErrorSound(client); + return; + } + + if (reset) + { + raceMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode); + raceMenuCourse[client] = 0; + } + + Menu menu = new Menu(MenuHandler_Race); + menu.SetTitle("%T", "Race Menu - Title", client); + RaceMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_Race(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + + if (StrEqual(info, ITEM_INFO_START, false)) + { + if (!StartHostedRace(param1)) + { + DisplayRaceMenu(param1, false); + } + } + else if (StrEqual(info, ITEM_INFO_ABORT, false)) + { + AbortHostedRace(param1); + DisplayRaceMenu(param1, false); + } + else if (StrEqual(info, ITEM_INFO_INVITE, false)) + { + if (!InRace(param1)) + { + HostRace(param1, RaceType_Normal, raceMenuCourse[param1], raceMenuMode[param1], raceMenuCheckpointLimit[param1], raceMenuCheckpointCooldown[param1]); + } + + SendRequestAll(param1); + GOKZ_PrintToChat(param1, true, "%t", "You Invited Everyone"); + DisplayRaceMenu(param1, false); + } + else if (StrEqual(info, ITEM_INFO_MODE, false)) + { + DisplayRaceModeMenu(param1); + } + else if (StrEqual(info, ITEM_INFO_COURSE, false)) + { + int course = raceMenuCourse[param1]; + do + { + course++; + if (!GOKZ_IsValidCourse(course)) + { + course = 0; + } + } while (!GOKZ_GetCourseRegistered(course) && course != raceMenuCourse[param1]); + raceMenuCourse[param1] = course; + DisplayRaceMenu(param1, false); + } + else if (StrEqual(info, ITEM_INFO_TELEPORT, false)) + { + DisplayRaceCheckpointMenu(param1); + } + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +void RaceMenuAddItems(int client, Menu menu) +{ + char display[32]; + + menu.RemoveAllItems(); + + bool pending = GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Pending; + FormatEx(display, sizeof(display), "%T", "Race Menu - Start Race", client); + menu.AddItem(ITEM_INFO_START, display, (InRace(client) && pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + FormatEx(display, sizeof(display), "%T", "Race Menu - Invite Everyone", client); + menu.AddItem(ITEM_INFO_INVITE, display, (!InRace(client) || pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client); + menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + FormatEx(display, sizeof(display), "%s", gC_ModeNames[raceMenuMode[client]]); + menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + + if (raceMenuCourse[client] == 0) + { + FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client); + } + else + { + FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, raceMenuCourse[client]); + } + menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + + FormatEx(display, sizeof(display), "%s", GetRaceRuleSummary(client, raceMenuCheckpointLimit[client], raceMenuCheckpointCooldown[client])); + menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); +} + + + +// =====[ MODE MENU ]===== + +static void DisplayRaceModeMenu(int client) +{ + Menu menu = new Menu(MenuHandler_RaceMode); + menu.ExitButton = false; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Mode Rule Menu - Title", client); + GOKZ_MenuAddModeItems(client, menu, true); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_RaceMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + raceMenuMode[param1] = param2; + DisplayRaceMenu(param1, false); + } + else if (action == MenuAction_Cancel) + { + DisplayRaceMenu(param1, false); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ CHECKPOINT MENU ]===== + +static void DisplayRaceCheckpointMenu(int client) +{ + Menu menu = new Menu(MenuHandler_RaceCheckpoint); + menu.ExitButton = false; + menu.ExitBackButton = true; + menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client); + RaceCheckpointMenuAddItems(client, menu); + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_RaceCheckpoint(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case CheckpointRule_None: + { + raceMenuCheckpointLimit[param1] = 0; + raceMenuCheckpointCooldown[param1] = 0; + DisplayRaceMenu(param1, false); + } + case CheckpointRule_Limit: + { + DisplayCheckpointLimitMenu(param1); + } + case CheckpointRule_Cooldown: + { + DisplayCheckpointCooldownMenu(param1); + } + case CheckpointRule_Unlimited: + { + raceMenuCheckpointLimit[param1] = -1; + raceMenuCheckpointCooldown[param1] = 0; + DisplayRaceMenu(param1, false); + } + } + } + else if (action == MenuAction_Cancel) + { + DisplayRaceMenu(param1, false); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +void RaceCheckpointMenuAddItems(int client, Menu menu) +{ + char display[32]; + + menu.RemoveAllItems(); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client); + menu.AddItem("", display); + + if (raceMenuCheckpointLimit[client] == -1) + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client); + } + else + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, raceMenuCheckpointLimit[client]); + } + menu.AddItem("", display); + + if (raceMenuCheckpointCooldown[client] == 0) + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client); + } + else + { + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, raceMenuCheckpointCooldown[client]); + } + menu.AddItem("", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client); + menu.AddItem("", display); +} + + + +// =====[ CP LIMIT MENU ]===== + +static void DisplayCheckpointLimitMenu(int client) +{ + char display[32]; + + Menu menu = new Menu(MenuHandler_RaceCheckpointLimit); + menu.ExitButton = false; + menu.ExitBackButton = true; + + if (raceMenuCheckpointLimit[client] == -1) + { + menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client); + } + else + { + menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, raceMenuCheckpointLimit[client]); + } + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client); + menu.AddItem("+1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client); + menu.AddItem("+5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client); + menu.AddItem("-1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client); + menu.AddItem("-5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client); + menu.AddItem("Unlimited", display); + + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_RaceCheckpointLimit(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char item[32]; + menu.GetItem(param2, item, sizeof(item)); + if (StrEqual(item, "+1")) + { + if (raceMenuCheckpointLimit[param1] == -1) + { + raceMenuCheckpointLimit[param1]++; + } + raceMenuCheckpointLimit[param1]++; + } + if (StrEqual(item, "+5")) + { + if (raceMenuCheckpointLimit[param1] == -1) + { + raceMenuCheckpointLimit[param1]++; + } + raceMenuCheckpointLimit[param1] += 5; + } + if (StrEqual(item, "-1")) + { + raceMenuCheckpointLimit[param1]--; + } + if (StrEqual(item, "-5")) + { + raceMenuCheckpointLimit[param1] -= 5; + } + if (StrEqual(item, "Unlimited")) + { + raceMenuCheckpointLimit[param1] = -1; + DisplayRaceCheckpointMenu(param1); + return 0; + } + + raceMenuCheckpointLimit[param1] = raceMenuCheckpointLimit[param1] < 0 ? 0 : raceMenuCheckpointLimit[param1]; + DisplayCheckpointLimitMenu(param1); + } + else if (action == MenuAction_Cancel) + { + DisplayRaceCheckpointMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ CP COOLDOWN MENU ]===== + +static void DisplayCheckpointCooldownMenu(int client) +{ + char display[32]; + + Menu menu = new Menu(MenuHandler_RaceCPCooldown); + menu.ExitButton = false; + menu.ExitBackButton = true; + + if (raceMenuCheckpointCooldown[client] == -1) + { + menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client); + } + else + { + menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, raceMenuCheckpointCooldown[client]); + } + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client); + menu.AddItem("+1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client); + menu.AddItem("+5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client); + menu.AddItem("-1", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client); + menu.AddItem("-5", display); + + FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client); + menu.AddItem("None", display); + + menu.Display(client, MENU_TIME_FOREVER); +} + +public int MenuHandler_RaceCPCooldown(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char item[32]; + menu.GetItem(param2, item, sizeof(item)); + if (StrEqual(item, "+1")) + { + if (raceMenuCheckpointCooldown[param1] == -1) + { + raceMenuCheckpointCooldown[param1]++; + } + raceMenuCheckpointCooldown[param1]++; + } + if (StrEqual(item, "+5")) + { + if (raceMenuCheckpointCooldown[param1] == -1) + { + raceMenuCheckpointCooldown[param1]++; + } + raceMenuCheckpointCooldown[param1] += 5; + } + if (StrEqual(item, "-1")) + { + raceMenuCheckpointCooldown[param1]--; + } + if (StrEqual(item, "-5")) + { + raceMenuCheckpointCooldown[param1] -= 5; + } + if (StrEqual(item, "None")) + { + raceMenuCheckpointCooldown[param1] = 0; + DisplayRaceCheckpointMenu(param1); + return 0; + } + + raceMenuCheckpointCooldown[param1] = raceMenuCheckpointCooldown[param1] < 0 ? 0 : raceMenuCheckpointCooldown[param1]; + DisplayCheckpointCooldownMenu(param1); + } + else if (action == MenuAction_Cancel) + { + DisplayRaceCheckpointMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +char[] GetRaceRuleSummary(int client, int checkpointLimit, int checkpointCooldown) +{ + char rulesString[64]; + if (checkpointLimit == -1 && checkpointCooldown == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client); + } + else if (checkpointLimit > 0 && checkpointCooldown == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit); + } + else if (checkpointLimit == -1 && checkpointCooldown > 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown); + } + else if (checkpointLimit > 0 && checkpointCooldown > 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown); + } + else if (checkpointLimit == 0) + { + FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client); + } + + return rulesString; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-racing/racer.sp b/sourcemod/scripting/gokz-racing/racer.sp new file mode 100644 index 0000000..eb4ff3f --- /dev/null +++ b/sourcemod/scripting/gokz-racing/racer.sp @@ -0,0 +1,439 @@ +/* + Functions that affect the state of clients participating in a race. + See the RacerStatus enum for possible states. +*/ + + + +static int racerStatus[MAXPLAYERS + 1]; +static int racerRaceID[MAXPLAYERS + 1]; +static float lastTimerStartTime[MAXPLAYERS + 1]; +static float lastCheckpointTime[MAXPLAYERS + 1]; + + + +// =====[ EVENTS ]===== + +Action OnTimerStart_Racer(int client, int course) +{ + if (InCountdown(client) + || InStartedRace(client) && (!InRaceMode(client) || !IsRaceCourse(client, course))) + { + return Plugin_Stop; + } + + return Plugin_Continue; +} + +Action OnTimerStart_Post_Racer(int client) +{ + lastTimerStartTime[client] = GetGameTime(); + return Plugin_Continue; +} + +Action OnMakeCheckpoint_Racer(int client) +{ + if (GOKZ_GetTimerRunning(client) && InStartedRace(client)) + { + int checkpointRule = GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule); + if (checkpointRule == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Checkpoints Not Allowed During Race"); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + else if (checkpointRule != -1 && GOKZ_GetCheckpointCount(client) >= checkpointRule) + { + GOKZ_PrintToChat(client, true, "%t", "No Checkpoints Left"); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + + float cooldownRule = float(GetRaceInfo(GetRaceID(client), RaceInfo_CooldownRule)); + float timeSinceLastCheckpoint = FloatMin( + GetGameTime() - lastTimerStartTime[client], + GetGameTime() - lastCheckpointTime[client]); + if (timeSinceLastCheckpoint < cooldownRule) + { + GOKZ_PrintToChat(client, true, "%t", "Checkpoint On Cooldown", (cooldownRule - timeSinceLastCheckpoint)); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +void OnMakeCheckpoint_Post_Racer(int client) +{ + lastCheckpointTime[client] = GetGameTime(); +} + +Action OnUndoTeleport_Racer(int client) +{ + if (GOKZ_GetTimerRunning(client) + && InStartedRace(client) + && GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule) != -1) + { + GOKZ_PrintToChat(client, true, "%t", "Undo TP Not Allowed During Race"); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + + return Plugin_Continue; +} + + + +// =====[ GENERAL ]===== + +int GetStatus(int client) +{ + return racerStatus[client]; +} + +int GetRaceID(int client) +{ + return racerRaceID[client]; +} + +bool InRace(int client) +{ + return GetStatus(client) != RacerStatus_Available; +} + +bool InStartedRace(int client) +{ + return GetStatus(client) == RacerStatus_Racing; +} + +bool InCountdown(int client) +{ + return GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Countdown; +} + +bool InRaceMode(int client) +{ + return GOKZ_GetCoreOption(client, Option_Mode) == GetRaceInfo(GetRaceID(client), RaceInfo_Mode); +} + +bool IsRaceCourse(int client, int course) +{ + return course == GetRaceInfo(GetRaceID(client), RaceInfo_Course); +} + +bool IsFinished(int client) +{ + int status = GetStatus(client); + return status == RacerStatus_Finished || status == RacerStatus_Surrendered; +} + +bool IsAccepted(int client) +{ + return GetStatus(client) == RacerStatus_Accepted; +} + +bool IsRaceHost(int client) +{ + return GetRaceHost(GetRaceID(client)) == client; +} + +static void ResetRacer(int client) +{ + racerStatus[client] = RacerStatus_Available; + racerRaceID[client] = -1; + lastTimerStartTime[client] = 0.0; + lastCheckpointTime[client] = 0.0; +} + +static void ResetRacersInRace(int raceID) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (racerRaceID[client] == raceID) + { + ResetRacer(client); + } + } +} + + + +// =====[ RACING ]===== + +void StartRacer(int client) +{ + if (racerStatus[client] == RacerStatus_Pending) + { + DeclineRequest(client, true); + return; + } + + if (racerStatus[client] != RacerStatus_Accepted) + { + return; + } + + racerStatus[client] = RacerStatus_Racing; + + // Prepare the racer + GOKZ_StopTimer(client); + GOKZ_SetCoreOption(client, Option_Mode, GetRaceInfo(racerRaceID[client], RaceInfo_Mode)); + + int course = GetRaceInfo(racerRaceID[client], RaceInfo_Course); + if (GOKZ_SetStartPositionToMapStart(client, course)) + { + GOKZ_TeleportToStart(client); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Start Found", course); + } +} + +bool FinishRacer(int client, int course) +{ + if (racerStatus[client] != RacerStatus_Racing || + course != GetRaceInfo(racerRaceID[client], RaceInfo_Course)) + { + return false; + } + + racerStatus[client] = RacerStatus_Finished; + + int raceID = racerRaceID[client]; + int place = IncrementFinishedRacerCount(raceID); + + Call_OnFinish(client, raceID, place); + + CheckRaceFinished(raceID); + + return true; +} + +bool SurrenderRacer(int client) +{ + if (racerStatus[client] == RacerStatus_Available + || racerStatus[client] == RacerStatus_Surrendered) + { + return false; + } + + racerStatus[client] = RacerStatus_Surrendered; + + int raceID = racerRaceID[client]; + + Call_OnSurrender(client, raceID); + + CheckRaceFinished(raceID); + + return true; +} + +// Auto-finish last remaining racer, and reset everyone if no one is left +static void CheckRaceFinished(int raceID) +{ + ArrayList remainingRacers = GetUnfinishedRacers(raceID); + if (remainingRacers.Length == 1) + { + int lastRacer = remainingRacers.Get(0); + FinishRacer(lastRacer, GetRaceInfo(racerRaceID[lastRacer], RaceInfo_Course)); + } + else if (remainingRacers.Length == 0) + { + ResetRacersInRace(raceID); + } + delete remainingRacers; +} + +bool AbortRacer(int client) +{ + if (racerStatus[client] == RacerStatus_Available) + { + return false; + } + + ResetRacer(client); + + return true; +} + + + +// =====[ HOSTING ]===== + +int HostRace(int client, int type, int course, int mode, int checkpointRule, int cooldownRule) +{ + if (InRace(client)) + { + return -1; + } + + int raceID = RegisterRace(client, type, course, mode, checkpointRule, cooldownRule); + racerRaceID[client] = raceID; + racerStatus[client] = RacerStatus_Accepted; + + return raceID; +} + +bool StartHostedRace(int client) +{ + if (!InRace(client) || !IsRaceHost(client)) + { + GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race"); + GOKZ_PlayErrorSound(client); + return false; + } + + int raceID = racerRaceID[client]; + + if (GetRaceInfo(raceID, RaceInfo_Status) != RaceStatus_Pending) + { + GOKZ_PrintToChat(client, true, "%t", "Race Already Started"); + GOKZ_PlayErrorSound(client); + return false; + } + + if (GetAcceptedRacersCount(raceID) <= 1) + { + GOKZ_PrintToChat(client, true, "%t", "No One Accepted"); + GOKZ_PlayErrorSound(client); + return false; + } + + return StartRace(raceID); +} + +bool AbortHostedRace(int client) +{ + if (!InRace(client) || !IsRaceHost(client)) + { + GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race"); + GOKZ_PlayErrorSound(client); + return false; + } + + int raceID = racerRaceID[client]; + + return AbortRace(raceID); +} + + + +// =====[ REQUESTS ]===== + +bool SendRequest(int host, int target) +{ + if (IsFakeClient(target) || target == host || InRace(target) + || !IsRaceHost(host) || GetRaceInfo(racerRaceID[host], RaceInfo_Status) != RaceStatus_Pending) + { + return false; + } + + int raceID = racerRaceID[host]; + + racerRaceID[target] = raceID; + racerStatus[target] = RacerStatus_Pending; + + // Host callback + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(host)); + data.WriteCell(GetClientUserId(target)); + data.WriteCell(raceID); + CreateTimer(RC_REQUEST_TIMEOUT_TIME, Timer_RequestTimeout, data); + + Call_OnRequestReceived(target, raceID); + + return true; +} + +public Action Timer_RequestTimeout(Handle timer, DataPack data) +{ + data.Reset(); + int host = GetClientOfUserId(data.ReadCell()); + int target = GetClientOfUserId(data.ReadCell()); + int raceID = data.ReadCell(); + delete data; + + if (!IsValidClient(host) || racerRaceID[host] != raceID + || !IsValidClient(target) || racerRaceID[target] != raceID) + { + return Plugin_Continue; + } + + // If haven't accepted by now, auto decline the race + if (racerStatus[target] == RacerStatus_Pending) + { + DeclineRequest(target, true); + } + return Plugin_Continue; +} + +int SendRequestAll(int host) +{ + int sentCount = 0; + for (int target = 1; target <= MaxClients; target++) + { + if (IsClientInGame(target) && SendRequest(host, target)) + { + sentCount++; + } + } + return sentCount; +} + +bool AcceptRequest(int client) +{ + if (GetStatus(client) != RacerStatus_Pending) + { + return false; + } + + racerStatus[client] = RacerStatus_Accepted; + + Call_OnRequestAccepted(client, racerRaceID[client]); + + return true; +} + +bool DeclineRequest(int client, bool timeout = false) +{ + if (GetStatus(client) != RacerStatus_Pending) + { + return false; + } + + int raceID = racerRaceID[client]; + ResetRacer(client); + + Call_OnRequestDeclined(client, raceID, timeout); + + return true; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Racer(int client) +{ + ResetRacer(client); +} + +void OnClientDisconnect_Racer(int client) +{ + // Abort if player was the host of the race, else surrender + if (InRace(client)) + { + if (IsRaceHost(client) && GetRaceInfo(racerRaceID[client], RaceInfo_Status) == RaceStatus_Pending) + { + AbortRace(racerRaceID[client]); + } + else + { + SurrenderRacer(client); + } + } + + ResetRacer(client); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays.sp b/sourcemod/scripting/gokz-replays.sp new file mode 100644 index 0000000..bc826c4 --- /dev/null +++ b/sourcemod/scripting/gokz-replays.sp @@ -0,0 +1,397 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdkhooks> +#include <sdktools> +#include <dhooks> + +#include <movementapi> + +#include <gokz/core> +#include <gokz/localranks> +#include <gokz/replays> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <gokz/hud> +#include <gokz/jumpstats> +#include <gokz/localdb> +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + +//#define DEBUG + + + +public Plugin myinfo = +{ + name = "GOKZ Replays", + author = "DanZay", + description = "Records runs to disk and allows playback using bots", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-replays.txt" + +bool gB_GOKZHUD; +bool gB_GOKZLocalDB; +char gC_CurrentMap[64]; +int gI_CurrentMapFileSize; +bool gB_HideNameChange; +bool gB_NubRecordMissed[MAXPLAYERS + 1]; +ArrayList g_ReplayInfoCache; +Address gA_BotDuckAddr; +int gI_BotDuckPatchRestore[40]; // Size of patched section in gamedata +int gI_BotDuckPatchLength; + +DynamicDetour gH_DHooks_TeamFull; + +#include "gokz-replays/commands.sp" +#include "gokz-replays/nav.sp" +#include "gokz-replays/playback.sp" +#include "gokz-replays/recording.sp" +#include "gokz-replays/replay_cache.sp" +#include "gokz-replays/replay_menu.sp" +#include "gokz-replays/api.sp" +#include "gokz-replays/controls.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-replays"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-replays.phrases"); + + CreateGlobalForwards(); + HookEvents(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = LibraryExists("gokz-localdb"); + gB_GOKZHUD = LibraryExists("gokz-hud"); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = gB_GOKZLocalDB || StrEqual(name, "gokz-localdb"); + gB_GOKZHUD = gB_GOKZHUD || StrEqual(name, "gokz-hud"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb"); + gB_GOKZHUD = gB_GOKZHUD && !StrEqual(name, "gokz-hud"); +} + +public void OnPluginEnd() +{ + // Restore bot auto duck behavior. + if (gA_BotDuckAddr == Address_Null) + { + return; + } + for (int i = 0; i < gI_BotDuckPatchLength; i++) + { + StoreToAddress(gA_BotDuckAddr + view_as<Address>(i), gI_BotDuckPatchRestore[i], NumberType_Int8); + } +} + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + UpdateCurrentMap(); // Do first + OnMapStart_Nav(); + OnMapStart_Recording(); + OnMapStart_ReplayCache(); +} + +public void OnConfigsExecuted() +{ + FindConVar("mp_autoteambalance").BoolValue = false; + FindConVar("mp_limitteams").IntValue = 0; + // Stop the bots! + FindConVar("bot_stop").BoolValue = true; + FindConVar("bot_chatter").SetString("off"); + FindConVar("bot_zombie").BoolValue = true; + FindConVar("bot_join_after_player").BoolValue = false; + FindConVar("bot_quota_mode").SetString("normal"); + FindConVar("bot_quota").Flags &= ~FCVAR_NOTIFY; + FindConVar("bot_quota").Flags &= ~FCVAR_REPLICATED; +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + // Block trigger and door interaction for bots + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + + // trigger_once | trigger_multiple.. etc + // func_door | func_door_rotating + if (StrContains(classname, "trigger_") != -1 || StrContains(classname, "_door") != -1) + { + SDKHook(entity, SDKHook_StartTouch, HookTriggers); + SDKHook(entity, SDKHook_EndTouch, HookTriggers); + SDKHook(entity, SDKHook_Touch, HookTriggers); + } +} + +public Action HookTriggers(int entity, int other) +{ + if (other >= 1 && other <= MaxClients && IsFakeClient(other)) + { + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action Hook_SayText2(UserMsg msg_id, any msg, const int[] players, int playersNum, bool reliable, bool init) +{ + // Name change supression + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + if (!gB_HideNameChange) + { + return Plugin_Continue; + } + + char msgName[24]; + Protobuf pbmsg = msg; + pbmsg.ReadString("msg_name", msgName, sizeof(msgName)); + if (StrEqual(msgName, "#Cstrike_Name_Change")) + { + gB_HideNameChange = false; + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public MRESReturn DHooks_OnTeamFull_Pre(Address pThis, DHookReturn hReturn, DHookParam hParams) +{ + DHookSetReturn(hReturn, false); + return MRES_Supercede; +} + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_Playback(client); + OnClientPutInServer_Recording(client); +} + +public void OnClientAuthorized(int client, const char[] auth) +{ + OnClientAuthorized_Recording(client); +} + +public void OnClientDisconnect(int client) +{ + OnClientDisconnect_Playback(client); + OnClientDisconnect_Recording(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + if (!IsFakeClient(client)) + { + return Plugin_Continue; + } + OnPlayerRunCmd_Playback(client, buttons, vel, angles); + return Plugin_Changed; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + OnPlayerRunCmdPost_Playback(client); + OnPlayerRunCmdPost_Recording(client, buttons, tickcount, vel, mouse); + OnPlayerRunCmdPost_ReplayControls(client, cmdnum); +} + +public Action GOKZ_OnTimerStart(int client, int course) +{ + Action action = GOKZ_OnTimerStart_Recording(client); + if (action != Plugin_Continue) + { + return action; + } + + return Plugin_Continue; +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + gB_NubRecordMissed[client] = false; + GOKZ_OnTimerStart_Post_Recording(client); +} + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + GOKZ_OnTimerEnd_Recording(client, course, time, teleportsUsed); +} + +public void GOKZ_OnPause_Post(int client) +{ + GOKZ_OnPause_Recording(client); +} + +public void GOKZ_OnResume_Post(int client) +{ + GOKZ_OnResume_Recording(client); +} + +public void GOKZ_OnTimerStopped(int client) +{ + GOKZ_OnTimerStopped_Recording(client); +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + GOKZ_OnCountedTeleport_Recording(client); +} + +public void GOKZ_LR_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType) +{ + if (recordType == RecordType_Nub) + { + gB_NubRecordMissed[client] = true; + } + GOKZ_LR_OnRecordMissed_Recording(client, recordType); +} + +public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason) +{ + GOKZ_AC_OnPlayerSuspected_Recording(client, reason); +} + +public void GOKZ_DB_OnJumpstatPB(int client, int jumptype, int mode, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + GOKZ_DB_OnJumpstatPB_Recording(client, jumptype, distance, block, strafes, sync, pre, max, airtime); +} + +public void GOKZ_OnOptionsLoaded(int client) +{ + if (IsFakeClient(client)) + { + GOKZ_OnOptionsLoaded_Playback(client); + } +} + +// =====[ PRIVATE ]===== + +static void HookEvents() +{ + HookUserMessage(GetUserMessageId("SayText2"), Hook_SayText2, true); + GameData gameData = LoadGameConfigFile("gokz-replays.games"); + + gH_DHooks_TeamFull = DynamicDetour.FromConf(gameData, "CCSGameRules::TeamFull"); + if (gH_DHooks_TeamFull == INVALID_HANDLE) + { + SetFailState("Failed to find CCSGameRules::TeamFull function signature"); + } + + if (!gH_DHooks_TeamFull.Enable(Hook_Pre, DHooks_OnTeamFull_Pre)) + { + SetFailState("Failed to enable detour on CCSGameRules::TeamFull"); + } + + // Remove bot auto duck behavior. + gA_BotDuckAddr = gameData.GetAddress("BotDuck"); + gI_BotDuckPatchLength = gameData.GetOffset("BotDuckPatchLength"); + for (int i = 0; i < gI_BotDuckPatchLength; i++) + { + gI_BotDuckPatchRestore[i] = LoadFromAddress(gA_BotDuckAddr + view_as<Address>(i), NumberType_Int8); + StoreToAddress(gA_BotDuckAddr + view_as<Address>(i), 0x90, NumberType_Int8); + } + delete gameData; +} + +static void UpdateCurrentMap() +{ + GetCurrentMapDisplayName(gC_CurrentMap, sizeof(gC_CurrentMap)); + gI_CurrentMapFileSize = GetCurrentMapFileSize(); +} + + +// =====[ PUBLIC ]===== + +// NOTE: These serialisation functions were made because the internal data layout of enum structs can change. +void TickDataToArray(ReplayTickData tickData, any result[RP_V2_TICK_DATA_BLOCKSIZE]) +{ + // NOTE: HAS to match ReplayTickData exactly! + result[0] = tickData.deltaFlags; + result[1] = tickData.deltaFlags2; + result[2] = tickData.vel[0]; + result[3] = tickData.vel[1]; + result[4] = tickData.vel[2]; + result[5] = tickData.mouse[0]; + result[6] = tickData.mouse[1]; + result[7] = tickData.origin[0]; + result[8] = tickData.origin[1]; + result[9] = tickData.origin[2]; + result[10] = tickData.angles[0]; + result[11] = tickData.angles[1]; + result[12] = tickData.angles[2]; + result[13] = tickData.velocity[0]; + result[14] = tickData.velocity[1]; + result[15] = tickData.velocity[2]; + result[16] = tickData.flags; + result[17] = tickData.packetsPerSecond; + result[18] = tickData.laggedMovementValue; + result[19] = tickData.buttonsForced; +} + +void TickDataFromArray(any array[RP_V2_TICK_DATA_BLOCKSIZE], ReplayTickData result) +{ + // NOTE: HAS to match ReplayTickData exactly! + result.deltaFlags = array[0]; + result.deltaFlags2 = array[1]; + result.vel[0] = array[2]; + result.vel[1] = array[3]; + result.vel[2] = array[4]; + result.mouse[0] = array[5]; + result.mouse[1] = array[6]; + result.origin[0] = array[7]; + result.origin[1] = array[8]; + result.origin[2] = array[9]; + result.angles[0] = array[10]; + result.angles[1] = array[11]; + result.angles[2] = array[12]; + result.velocity[0] = array[13]; + result.velocity[1] = array[14]; + result.velocity[2] = array[15]; + result.flags = array[16]; + result.packetsPerSecond = array[17]; + result.laggedMovementValue = array[18]; + result.buttonsForced = array[19]; +} diff --git a/sourcemod/scripting/gokz-replays/api.sp b/sourcemod/scripting/gokz-replays/api.sp new file mode 100644 index 0000000..3c115e1 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/api.sp @@ -0,0 +1,78 @@ +static GlobalForward H_OnReplaySaved; +static GlobalForward H_OnReplayDiscarded; +static GlobalForward H_OnTimerEnd_Post; + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_RP_GetPlaybackInfo", Native_RP_GetPlaybackInfo); + CreateNative("GOKZ_RP_LoadJumpReplay", Native_RP_LoadJumpReplay); + CreateNative("GOKZ_RP_UpdateReplayControlMenu", Native_RP_UpdateReplayControlMenu); +} + +public int Native_RP_GetPlaybackInfo(Handle plugin, int numParams) +{ + HUDInfo info; + GetPlaybackState(GetNativeCell(1), info); + SetNativeArray(2, info, sizeof(HUDInfo)); + return 1; +} + +public int Native_RP_LoadJumpReplay(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(2, len); + char[] path = new char[len + 1]; + GetNativeString(2, path, len + 1); + int botClient = LoadReplayBot(GetNativeCell(1), path); + return botClient; +} + +public int Native_RP_UpdateReplayControlMenu(Handle plugin, int numParams) +{ + return view_as<int>(UpdateReplayControlMenu(GetNativeCell(1))); +} + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnReplaySaved = new GlobalForward("GOKZ_RP_OnReplaySaved", ET_Event, Param_Cell, Param_Cell, Param_String, Param_Cell, Param_Cell, Param_Float, Param_String, Param_Cell); + H_OnReplayDiscarded = new GlobalForward("GOKZ_RP_OnReplayDiscarded", ET_Ignore, Param_Cell); + H_OnTimerEnd_Post = new GlobalForward("GOKZ_RP_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_String, Param_Cell, Param_Float, Param_Cell); +} + +Action Call_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay) +{ + Action result; + Call_StartForward(H_OnReplaySaved); + Call_PushCell(client); + Call_PushCell(replayType); + Call_PushString(map); + Call_PushCell(course); + Call_PushCell(timeType); + Call_PushFloat(time); + Call_PushString(filePath); + Call_PushCell(tempReplay); + Call_Finish(result); + return result; +} + +void Call_OnReplayDiscarded(int client) +{ + Call_StartForward(H_OnReplayDiscarded); + Call_PushCell(client); + Call_Finish(); +} + +void Call_OnTimerEnd_Post(int client, const char[] filePath, int course, float time, int teleportsUsed) +{ + Call_StartForward(H_OnTimerEnd_Post); + Call_PushCell(client); + Call_PushString(filePath); + Call_PushCell(course); + Call_PushFloat(time); + Call_PushCell(teleportsUsed); + Call_Finish(); +} diff --git a/sourcemod/scripting/gokz-replays/commands.sp b/sourcemod/scripting/gokz-replays/commands.sp new file mode 100644 index 0000000..43251f6 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/commands.sp @@ -0,0 +1,55 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_replay", CommandReplay, "[KZ] Open the replay loading menu."); + RegConsoleCmd("sm_replaycontrols", CommandReplayControls, "[KZ] Toggle the replay control menu."); + RegConsoleCmd("sm_rpcontrols", CommandReplayControls, "[KZ] Toggle the replay control menu."); + RegConsoleCmd("sm_replaygoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss)."); + RegConsoleCmd("sm_rpgoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss)."); +} + +public Action CommandReplay(int client, int args) +{ + DisplayReplayModeMenu(client); + return Plugin_Handled; +} + +public Action CommandReplayControls(int client, int args) +{ + ToggleReplayControls(client); + return Plugin_Handled; +} + +public Action CommandReplayGoto(int client, int args) +{ + int seconds; + char timeString[32], split[3][32]; + + GetCmdArgString(timeString, sizeof(timeString)); + int res = ExplodeString(timeString, ":", split, 3, 32, false); + switch (res) + { + case 1: + { + seconds = StringToInt(split[0]); + } + + case 2: + { + seconds = StringToInt(split[0]) * 60 + StringToInt(split[1]); + } + + case 3: + { + seconds = StringToInt(split[0]) * 3600 + StringToInt(split[1]) * 60 + StringToInt(split[2]); + } + + default: + { + GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time"); + return Plugin_Handled; + } + } + + TrySkipToTime(client, seconds); + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-replays/controls.sp b/sourcemod/scripting/gokz-replays/controls.sp new file mode 100644 index 0000000..cda7f07 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/controls.sp @@ -0,0 +1,224 @@ +/* + Lets player control the replay bot. +*/ + +#define ITEM_INFO_PAUSE "pause" +#define ITEM_INFO_SKIP "skip" +#define ITEM_INFO_REWIND "rewind" +#define ITEM_INFO_FREECAM "freecam" + +static int controllingPlayer[RP_MAX_BOTS]; +static int botTeleports[RP_MAX_BOTS]; +static bool showReplayControls[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void OnPlayerRunCmdPost_ReplayControls(int client, int cmdnum) +{ + // Let the HUD plugin takes care of this if possible. + if (cmdnum % 6 == 3 && !gB_GOKZHUD) + { + UpdateReplayControlMenu(client); + } +} + +bool UpdateReplayControlMenu(int client) +{ + if (!IsValidClient(client) || IsFakeClient(client)) + { + return false; + } + + int botClient = GetObserverTarget(client); + int bot = GetBotFromClient(botClient); + if (bot == -1) + { + return false; + } + + if (!IsReplayBotControlled(bot, botClient) && !InBreather(bot)) + { + CancelReplayControlsForBot(bot); + controllingPlayer[bot] = client; + } + else if (controllingPlayer[bot] != client) + { + return false; + } + + if (showReplayControls[client] && + GOKZ_HUD_GetOption(client, HUDOption_ShowControls) == ReplayControls_Enabled) + { + // We have to update this often if bot uses teleports. + if (GetClientMenu(client) == MenuSource_None || + GOKZ_HUD_GetMenuShowing(client) && GetClientAvgLoss(client, NetFlow_Both) > EPSILON || + GOKZ_HUD_GetMenuShowing(client) && GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu || + GOKZ_HUD_GetMenuShowing(client) && PlaybackGetTeleports(bot) > 0) + { + botTeleports[bot] = PlaybackGetTeleports(bot); + ShowReplayControlMenu(client, bot); + } + return true; + } + return false; +} + +void ShowReplayControlMenu(int client, int bot) +{ + char text[256]; + + Menu menu = new Menu(MenuHandler_ReplayControls); + menu.OptionFlags = MENUFLAG_NO_SOUND; + menu.Pagination = MENU_NO_PAGINATION; + menu.ExitButton = true; + if (gB_GOKZHUD) + { + if (GOKZ_HUD_GetOption(client, HUDOption_ShowSpectators) != ShowSpecs_Disabled && + GOKZ_HUD_GetOption(client, HUDOption_SpecListPosition) == SpecListPosition_TPMenu) + { + HUDInfo info; + GetPlaybackState(client, info); + GOKZ_HUD_GetMenuSpectatorText(client, info, text, sizeof(text)); + } + if (GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu) + { + Format(text, sizeof(text), "%s\n%T - %s", text, "Replay Controls - Title", client, + GOKZ_FormatTime(GetPlaybackTime(bot), GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Precise)); + } + else + { + Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client); + } + } + else + { + Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client); + } + + + if (botTeleports[bot] > 0) + { + Format(text, sizeof(text), "%s\n%T", text, "Replay Controls - Teleports", client, botTeleports[bot]); + } + + menu.SetTitle(text); + + if (PlaybackPaused(bot)) + { + FormatEx(text, sizeof(text), "%T", "Replay Controls - Resume", client); + menu.AddItem(ITEM_INFO_PAUSE, text); + } + else + { + FormatEx(text, sizeof(text), "%T", "Replay Controls - Pause", client); + menu.AddItem(ITEM_INFO_PAUSE, text); + } + + FormatEx(text, sizeof(text), "%T", "Replay Controls - Skip", client); + menu.AddItem(ITEM_INFO_SKIP, text); + + FormatEx(text, sizeof(text), "%T\n ", "Replay Controls - Rewind", client); + menu.AddItem(ITEM_INFO_REWIND, text); + + FormatEx(text, sizeof(text), "%T", "Replay Controls - Freecam", client); + menu.AddItem(ITEM_INFO_FREECAM, text); + + menu.Display(client, MENU_TIME_FOREVER); + + if (gB_GOKZHUD) + { + GOKZ_HUD_SetMenuShowing(client, true); + } +} + +void ToggleReplayControls(int client) +{ + if (showReplayControls[client]) + { + CancelReplayControls(client); + } + else + { + showReplayControls[client] = true; + } +} + +void EnableReplayControls(int client) +{ + showReplayControls[client] = true; +} + +bool IsReplayBotControlled(int bot, int botClient) +{ + return IsValidClient(controllingPlayer[bot]) && + (GetObserverTarget(controllingPlayer[bot]) == botClient || + GetEntProp(controllingPlayer[bot], Prop_Send, "m_iObserverMode") == 6); +} + +int MenuHandler_ReplayControls(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_Select: + { + if (!IsValidClient(param1)) + { + return; + } + + int bot = GetBotFromClient(GetObserverTarget(param1)); + if (bot == -1 || controllingPlayer[bot] != param1) + { + return; + } + + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + if (StrEqual(info, ITEM_INFO_PAUSE, false)) + { + PlaybackTogglePause(bot); + } + else if (StrEqual(info, ITEM_INFO_SKIP, false)) + { + PlaybackSkipForward(bot); + } + else if (StrEqual(info, ITEM_INFO_REWIND, false)) + { + PlaybackSkipBack(bot); + } + else if (StrEqual(info, ITEM_INFO_FREECAM, false)) + { + SetEntProp(param1, Prop_Send, "m_iObserverMode", 6); + } + GOKZ_HUD_SetMenuShowing(param1, false); + } + case MenuAction_Cancel: + { + GOKZ_HUD_SetMenuShowing(param1, false); + if (param2 == MenuCancel_Exit) + { + CancelReplayControls(param1); + } + } + case MenuAction_End: + { + delete menu; + } + } +} + +void CancelReplayControls(int client) +{ + if (IsValidClient(client) && showReplayControls[client]) + { + CancelClientMenu(client); + showReplayControls[client] = false; + } +} + +void CancelReplayControlsForBot(int bot) +{ + CancelReplayControls(controllingPlayer[bot]); +} diff --git a/sourcemod/scripting/gokz-replays/nav.sp b/sourcemod/scripting/gokz-replays/nav.sp new file mode 100644 index 0000000..4e73c2f --- /dev/null +++ b/sourcemod/scripting/gokz-replays/nav.sp @@ -0,0 +1,97 @@ +/* + Ensures that there is .nav file for the map so the server + does not to auto-generating one. +*/ + + + +// =====[ EVENTS ]===== + +void OnMapStart_Nav() +{ + if (!CheckForNavFile()) + { + GenerateNavFile(); + } +} + + + +// =====[ PRIVATE ]===== + +static bool CheckForNavFile() +{ + // Make sure there's a nav file + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + + char mapPath[PLATFORM_MAX_PATH]; + GetCurrentMap(mapPath, sizeof(mapPath)); + + char navFilePath[PLATFORM_MAX_PATH]; + FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath); + + return FileExists(navFilePath); +} + +static void GenerateNavFile() +{ + // Generate (copy a) .nav file for the map + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + + char mapPath[PLATFORM_MAX_PATH]; + GetCurrentMap(mapPath, sizeof(mapPath)); + + char[] navFilePath = new char[PLATFORM_MAX_PATH]; + FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath); + + if (!FileExists(RP_NAV_FILE)) + { + SetFailState("Failed to load file: \"%s\". Check that it exists.", RP_NAV_FILE); + } + File_Copy(RP_NAV_FILE, navFilePath); + ForceChangeLevel(gC_CurrentMap, "[gokz-replays] Generate .nav file."); +} + +/* + * Copies file source to destination + * Based on code of javalia: + * http://forums.alliedmods.net/showthread.php?t=159895 + * + * Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + * + * @param source Input file + * @param destination Output file + */ +static bool File_Copy(const char[] source, const char[] destination) +{ + File file_source = OpenFile(source, "rb"); + + if (file_source == null) + { + return false; + } + + File file_destination = OpenFile(destination, "wb"); + + if (file_destination == null) + { + delete file_source; + + return false; + } + + int[] buffer = new int[32]; + int cache = 0; + + while (!IsEndOfFile(file_source)) + { + cache = ReadFile(file_source, buffer, 32, 1); + + file_destination.Write(buffer, cache, 1); + } + + delete file_source; + delete file_destination; + + return true; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/playback.sp b/sourcemod/scripting/gokz-replays/playback.sp new file mode 100644 index 0000000..b3f6865 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/playback.sp @@ -0,0 +1,1501 @@ +/* + Bot replay playback logic and processes. + + The recorded files are read and their information and tick data + stored into variables. A bot is then used to playback the recorded + data by setting it's origin, velocity, etc. in OnPlayerRunCmd. +*/ + + + +static int preAndPostRunTickCount; + +static int playbackTick[RP_MAX_BOTS]; +static ArrayList playbackTickData[RP_MAX_BOTS]; +static bool inBreather[RP_MAX_BOTS]; +static float breatherStartTime[RP_MAX_BOTS]; + +// Original bot caller, needed for OnClientPutInServer callback +static int botCaller[RP_MAX_BOTS]; +// Original bot name after creation by bot_add, needed for bot removal +static char botName[RP_MAX_BOTS][MAX_NAME_LENGTH]; +static bool botInGame[RP_MAX_BOTS]; +static int botClient[RP_MAX_BOTS]; +static bool botDataLoaded[RP_MAX_BOTS]; +static int botReplayType[RP_MAX_BOTS]; +static int botReplayVersion[RP_MAX_BOTS]; +static int botSteamAccountID[RP_MAX_BOTS]; +static int botCourse[RP_MAX_BOTS]; +static int botMode[RP_MAX_BOTS]; +static int botStyle[RP_MAX_BOTS]; +static float botTime[RP_MAX_BOTS]; +static int botTimeTicks[RP_MAX_BOTS]; +static char botAlias[RP_MAX_BOTS][MAX_NAME_LENGTH]; +static bool botPaused[RP_MAX_BOTS]; +static bool botPlaybackPaused[RP_MAX_BOTS]; +static int botKnife[RP_MAX_BOTS]; +static int botWeapon[RP_MAX_BOTS]; +static int botJumpType[RP_MAX_BOTS]; +static float botJumpDistance[RP_MAX_BOTS]; +static int botJumpBlockDistance[RP_MAX_BOTS]; + +static int timeOnGround[RP_MAX_BOTS]; +static int timeInAir[RP_MAX_BOTS]; +static int botTeleportsUsed[RP_MAX_BOTS]; +static int botCurrentTeleport[RP_MAX_BOTS]; +static int botButtons[RP_MAX_BOTS]; +static MoveType botMoveType[RP_MAX_BOTS]; +static float botTakeoffSpeed[RP_MAX_BOTS]; +static float botSpeed[RP_MAX_BOTS]; +static float botLastOrigin[RP_MAX_BOTS][3]; +static bool hitBhop[RP_MAX_BOTS]; +static bool hitPerf[RP_MAX_BOTS]; +static bool botJumped[RP_MAX_BOTS]; +static bool botIsTakeoff[RP_MAX_BOTS]; +static bool botJustTeleported[RP_MAX_BOTS]; +static float botLandingSpeed[RP_MAX_BOTS]; + + + +// =====[ PUBLIC ]===== + +// Returns the client index of the replay bot, or -1 otherwise +int LoadReplayBot(int client, char[] path) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return -1; + } + } + int bot; + if (GetBotsInUse() < RP_MAX_BOTS) + { + bot = GetUnusedBot(); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Bots Available"); + GOKZ_PlayErrorSound(client); + return -1; + } + + if (bot == -1) + { + LogError("Unused bot could not be found even though only %d out of %d are known to be in use.", + GetBotsInUse(), RP_MAX_BOTS); + GOKZ_PlayErrorSound(client); + return -1; + } + + if (!LoadPlayback(client, bot, path)) + { + GOKZ_PlayErrorSound(client); + return -1; + } + + ServerCommand("bot_add"); + botCaller[bot] = client; + return botClient[bot]; +} + +// Passes the current state of the replay into the HUDInfo struct +void GetPlaybackState(int client, HUDInfo info) +{ + int bot, i; + for(i = 0; i < RP_MAX_BOTS; i++) + { + bot = botClient[i] == client ? i : bot; + } + if (i == RP_MAX_BOTS + 1) return; + + if (playbackTickData[bot] == INVALID_HANDLE) + { + return; + } + + info.TimerRunning = botReplayType[bot] == ReplayType_Jump ? false : true; + if (botReplayVersion[bot] == 1) + { + info.Time = playbackTick[bot] * GetTickInterval(); + } + else if (botReplayVersion[bot] == 2) + { + if (playbackTick[bot] < preAndPostRunTickCount) + { + info.Time = 0.0; + } + else if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount) + { + info.Time = botTime[bot]; + } + else if (playbackTick[bot] >= preAndPostRunTickCount) + { + info.Time = (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval(); + } + } + info.TimerRunning = true; + info.TimeType = botTeleportsUsed[bot] > 0 ? TimeType_Nub : TimeType_Pro; + info.Speed = botSpeed[bot]; + info.Paused = false; + info.OnLadder = (botMoveType[bot] == MOVETYPE_LADDER); + info.Noclipping = false; + info.OnGround = Movement_GetOnGround(client); + info.Ducking = botButtons[bot] & IN_DUCK > 0; + info.ID = botClient[bot]; + info.Jumped = botJumped[bot]; + info.HitBhop = hitBhop[bot]; + info.HitPerf = hitPerf[bot]; + info.Buttons = botButtons[bot]; + info.TakeoffSpeed = botTakeoffSpeed[bot]; + info.IsTakeoff = botIsTakeoff[bot] && !Movement_GetOnGround(client); + info.CurrentTeleport = botCurrentTeleport[bot]; +} + +int GetBotFromClient(int client) +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + return bot; + } + } + return -1; +} + +bool InBreather(int bot) +{ + return inBreather[bot]; +} + +bool PlaybackPaused(int bot) +{ + return botPlaybackPaused[bot]; +} + +void PlaybackTogglePause(int bot) +{ + if(botPlaybackPaused[bot]) + { + botPlaybackPaused[bot] = false; + } + else + { + botPlaybackPaused[bot] = true; + } +} + +void PlaybackSkipForward(int bot) +{ + if (playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval()) < playbackTickData[bot].Length) + { + PlaybackSkipToTick(bot, playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval())); + } +} + +void PlaybackSkipBack(int bot) +{ + if (playbackTick[bot] < RoundToZero(RP_SKIP_TIME / GetTickInterval())) + { + PlaybackSkipToTick(bot, 0); + } + else + { + PlaybackSkipToTick(bot, playbackTick[bot] - RoundToZero(RP_SKIP_TIME / GetTickInterval())); + } +} + +int PlaybackGetTeleports(int bot) +{ + return botCurrentTeleport[bot]; +} + +void TrySkipToTime(int client, int seconds) +{ + if (!IsValidClient(client)) + { + return; + } + + int tick = seconds * 128 + preAndPostRunTickCount; + int bot = GetBotFromClient(GetObserverTarget(client)); + + if (tick >= 0 && tick < playbackTickData[bot].Length) + { + PlaybackSkipToTick(bot, tick); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time"); + } +} + +float GetPlaybackTime(int bot) +{ + if (playbackTick[bot] < preAndPostRunTickCount) + { + return 0.0; + } + if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount) + { + return botTime[bot]; + } + if (playbackTick[bot] >= preAndPostRunTickCount) + { + return (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval(); + } + + return 0.0; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Playback(int client) +{ + if (!IsFakeClient(client) || IsClientSourceTV(client)) + { + return; + } + + // Check if an unassigned bot has joined, and assign it + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Also check if the bot was created by us. + if (!botInGame[bot] && botCaller[bot] != 0) + { + botInGame[bot] = true; + botClient[bot] = client; + GetClientName(client, botName[bot], sizeof(botName[])); + // The bot won't receive its weapons properly if we don't wait a frame + RequestFrame(SetBotStuff, bot); + if (IsValidClient(botCaller[bot])) + { + MakePlayerSpectate(botCaller[bot], botClient[bot]); + botCaller[bot] = 0; + } + break; + } + } +} + +void OnClientDisconnect_Playback(int client) +{ + for (int bot; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] != client) + { + continue; + } + + botInGame[bot] = false; + if (playbackTickData[bot] != null) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + } + } +} + +void OnPlayerRunCmd_Playback(int client, int &buttons, float vel[3], float angles[3]) +{ + if (!IsFakeClient(client)) + { + return; + } + + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Check if not the bot we're looking for + if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot]) + { + continue; + } + + switch (botReplayVersion[bot]) + { + case 1: PlaybackVersion1(client, bot, buttons); + case 2: PlaybackVersion2(client, bot, buttons, vel, angles); + } + break; + } +} + +void OnPlayerRunCmdPost_Playback(int client) +{ + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Check if not the bot we're looking for + if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot]) + { + continue; + } + if (botReplayVersion[bot] == 2) + { + PlaybackVersion2Post(client, bot); + } + break; + } +} + +void GOKZ_OnOptionsLoaded_Playback(int client) +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + // Reset its movement options as it might be wrongfully changed + GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]); + GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]); + } + } +} +// =====[ PRIVATE ]===== + +// Returns false if there was a problem loading the playback e.g. doesn't exist +static bool LoadPlayback(int client, int bot, char[] path) +{ + if (!FileExists(path)) + { + GOKZ_PrintToChat(client, true, "%t", "No Replay Found"); + return false; + } + + File file = OpenFile(path, "rb"); + + // Check magic number in header + int magicNumber; + file.ReadInt32(magicNumber); + if (magicNumber != RP_MAGIC_NUMBER) + { + LogError("Failed to load invalid replay file: \"%s\".", path); + delete file; + return false; + } + + // Check replay format version + int formatVersion; + file.ReadInt8(formatVersion); + switch(formatVersion) + { + case 1: + { + botReplayVersion[bot] = 1; + if (!LoadFormatVersion1Replay(file, bot)) + { + return false; + } + } + case 2: + { + botReplayVersion[bot] = 2; + if (!LoadFormatVersion2Replay(file, client, bot)) + { + return false; + } + } + + default: + { + LogError("Failed to load replay file with unsupported format version: \"%s\".", path); + delete file; + return false; + } + } + + return true; +} + +static bool LoadFormatVersion1Replay(File file, int bot) +{ + // Old replays only support runs, not jumps + botReplayType[bot] = ReplayType_Run; + + int length; + + // GOKZ version + file.ReadInt8(length); + char[] gokzVersion = new char[length + 1]; + file.ReadString(gokzVersion, length, length); + gokzVersion[length] = '\0'; + + // Map name + file.ReadInt8(length); + char[] mapName = new char[length + 1]; + file.ReadString(mapName, length, length); + mapName[length] = '\0'; + + // Some integers... + file.ReadInt32(botCourse[bot]); + file.ReadInt32(botMode[bot]); + file.ReadInt32(botStyle[bot]); + + // Old replays don't store the weapon information + botKnife[bot] = CS_WeaponIDToItemDefIndex(CSWeapon_KNIFE); + botWeapon[bot] = (botMode[bot] == Mode_Vanilla) ? -1 : CS_WeaponIDToItemDefIndex(CSWeapon_USP_SILENCER); + + // Time + int timeAsInt; + file.ReadInt32(timeAsInt); + botTime[bot] = view_as<float>(timeAsInt); + + // Some integers... + file.ReadInt32(botTeleportsUsed[bot]); + file.ReadInt32(botSteamAccountID[bot]); + + // SteamID2 + file.ReadInt8(length); + char[] steamID2 = new char[length + 1]; + file.ReadString(steamID2, length, length); + steamID2[length] = '\0'; + + // IP + file.ReadInt8(length); + char[] IP = new char[length + 1]; + file.ReadString(IP, length, length); + IP[length] = '\0'; + + // Alias + file.ReadInt8(length); + file.ReadString(botAlias[bot], sizeof(botAlias[]), length); + botAlias[bot][length] = '\0'; + + // Read tick data + file.ReadInt32(length); + + // Setup playback tick data array list + if (playbackTickData[bot] == null) + { + playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData)), length); + } + else + { // Make sure it's all clear and the correct size + playbackTickData[bot].Clear(); + playbackTickData[bot].Resize(length); + } + + // The replay has no replay data, this shouldn't happen normally, + // but this would cause issues in other code, so we don't even try to load this. + if (length == 0) + { + delete file; + return false; + } + + any tickData[RP_V1_TICK_DATA_BLOCKSIZE]; + for (int i = 0; i < length; i++) + { + file.Read(tickData, RP_V1_TICK_DATA_BLOCKSIZE, 4); + playbackTickData[bot].Set(i, view_as<float>(tickData[0]), 0); // origin[0] + playbackTickData[bot].Set(i, view_as<float>(tickData[1]), 1); // origin[1] + playbackTickData[bot].Set(i, view_as<float>(tickData[2]), 2); // origin[2] + playbackTickData[bot].Set(i, view_as<float>(tickData[3]), 3); // angles[0] + playbackTickData[bot].Set(i, view_as<float>(tickData[4]), 4); // angles[1] + playbackTickData[bot].Set(i, view_as<int>(tickData[5]), 5); // buttons + playbackTickData[bot].Set(i, view_as<int>(tickData[6]), 6); // flags + } + + playbackTick[bot] = 0; + botDataLoaded[bot] = true; + + delete file; + return true; +} + +static bool LoadFormatVersion2Replay(File file, int client, int bot) +{ + int length; + + // Replay type + int replayType; + file.ReadInt8(replayType); + + // GOKZ version + file.ReadInt8(length); + char[] gokzVersion = new char[length + 1]; + file.ReadString(gokzVersion, length, length); + gokzVersion[length] = '\0'; + + // Map name + file.ReadInt8(length); + char[] mapName = new char[length + 1]; + file.ReadString(mapName, length, length); + mapName[length] = '\0'; + if (!StrEqual(mapName, gC_CurrentMap)) + { + GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Map", mapName); + delete file; + return false; + } + + // Map filesize + int mapFileSize; + file.ReadInt32(mapFileSize); + + // Server IP + int serverIP; + file.ReadInt32(serverIP); + + // Timestamp + int timestamp; + file.ReadInt32(timestamp); + + // Player Alias + file.ReadInt8(length); + file.ReadString(botAlias[bot], sizeof(botAlias[]), length); + botAlias[bot][length] = '\0'; + + // Player Steam ID + int steamID; + file.ReadInt32(steamID); + + // Mode + file.ReadInt8(botMode[bot]); + + // Style + file.ReadInt8(botStyle[bot]); + + // Player Sensitivity + int intPlayerSensitivity; + file.ReadInt32(intPlayerSensitivity); + float playerSensitivity = view_as<float>(intPlayerSensitivity); + + // Player MYAW + int intPlayerMYaw; + file.ReadInt32(intPlayerMYaw); + float playerMYaw = view_as<float>(intPlayerMYaw); + + // Tickrate + int tickrateAsInt; + file.ReadInt32(tickrateAsInt); + float tickrate = view_as<float>(tickrateAsInt); + if (tickrate != RoundToZero(1 / GetTickInterval())) + { + GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Tickrate", tickrate, (RoundToZero(1 / GetTickInterval()))); + delete file; + return false; + } + + // Tick Count + int tickCount; + file.ReadInt32(tickCount); + + // The replay has no replay data, this shouldn't happen normally, + // but this would cause issues in other code, so we don't even try to load this. + if (tickCount == 0) + { + delete file; + return false; + } + + // Equipped Weapon + file.ReadInt32(botWeapon[bot]); + + // Equipped Knife + file.ReadInt32(botKnife[bot]); + + // Big spit to console + PrintToConsole(client, "Replay Type: %d\nGOKZ Version: %s\nMap Name: %s\nMap Filesize: %d\nServer IP: %d\nTimestamp: %d\nPlayer Alias: %s\nPlayer Steam ID: %d\nMode: %d\nStyle: %d\nPlayer Sensitivity: %f\nPlayer m_yaw: %f\nTickrate: %f\nTick Count: %d\nWeapon: %d\nKnife: %d", replayType, gokzVersion, mapName, mapFileSize, serverIP, timestamp, botAlias[bot], steamID, botMode[bot], botStyle[bot], playerSensitivity, playerMYaw, tickrate, tickCount, botWeapon[bot], botKnife[bot]); + + switch(replayType) + { + case ReplayType_Run: + { + // Time + int timeAsInt; + file.ReadInt32(timeAsInt); + botTime[bot] = view_as<float>(timeAsInt); + botTimeTicks[bot] = RoundToNearest(botTime[bot] * tickrate); + + // Course + file.ReadInt8(botCourse[bot]); + + // Teleports Used + file.ReadInt32(botTeleportsUsed[bot]); + + // Type + botReplayType[bot] = ReplayType_Run; + + // Finish spit to console + PrintToConsole(client, "Time: %f\nCourse: %d\nTeleports Used: %d", botTime[bot], botCourse[bot], botTeleportsUsed[bot]); + } + case ReplayType_Cheater: + { + // Reason + int reason; + file.ReadInt8(reason); + + // Type + botReplayType[bot] = ReplayType_Cheater; + + // Finish spit to console + PrintToConsole(client, "AC Reason: %s", gC_ACReasons[reason]); + } + case ReplayType_Jump: + { + // Jump Type + file.ReadInt8(botJumpType[bot]); + + // Distance + file.ReadInt32(view_as<int>(botJumpDistance[bot])); + + // Block Distance + file.ReadInt32(botJumpBlockDistance[bot]); + + // Strafe Count + int strafeCount; + file.ReadInt8(strafeCount); + + // Sync + float sync; + file.ReadInt32(view_as<int>(sync)); + + // Pre + float pre; + file.ReadInt32(view_as<int>(pre)); + + // Max + float max; + file.ReadInt32(view_as<int>(max)); + + // Airtime + int airtime; + file.ReadInt32(airtime); + + // Type + botReplayType[bot] = ReplayType_Jump; + + // Finish spit to console + PrintToConsole(client, "Jump Type: %s\nJump Distance: %f\nBlock Distance: %d\nStrafe Count: %d\nSync: %f\n Pre: %f\nMax: %f\nAirtime: %d", + gC_JumpTypes[botJumpType[bot]], botJumpDistance[bot], botJumpBlockDistance[bot], strafeCount, sync, pre, max, airtime); + } + } + + // Tick Data + // Setup playback tick data array list + if (playbackTickData[bot] == null) + { + playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData))); + } + else + { + playbackTickData[bot].Clear(); + } + + // Read tick data + preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME / GetTickInterval()); + any tickDataArray[RP_V2_TICK_DATA_BLOCKSIZE]; + for (int i = 0; i < tickCount; i++) + { + file.ReadInt32(tickDataArray[RPDELTA_DELTAFLAGS]); + + for (int index = 1; index < sizeof(tickDataArray); index++) + { + int currentFlag = (1 << index); + if (tickDataArray[RPDELTA_DELTAFLAGS] & currentFlag) + { + file.ReadInt32(tickDataArray[index]); + } + } + + ReplayTickData tickData; + TickDataFromArray(tickDataArray, tickData); + // HACK: Jump replays don't record proper length sometimes. I don't know why. + // This leads to oversized replays full of 0s at the end. + // So, we do this horrible check to dodge that issue. + if (tickData.origin[0] == 0 && tickData.origin[1] == 0 && tickData.origin[2] == 0 && tickData.angles[0] == 0 && tickData.angles[1] == 0) + { + break; + } + playbackTickData[bot].PushArray(tickData); + } + + playbackTick[bot] = 0; + botDataLoaded[bot] = true; + + delete file; + + return true; +} + +static void PlaybackVersion1(int client, int bot, int &buttons) +{ + int size = playbackTickData[bot].Length; + float repOrigin[3], repAngles[3]; + int repButtons, repFlags; + + // If first or last frame of the playback + if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1)) + { + // Move the bot and pause them at that tick + repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0); + repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1); + repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2); + repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3); + repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4); + TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + if (!inBreather[bot]) + { + // Start the breather period + inBreather[bot] = true; + breatherStartTime[bot] = GetEngineTime(); + if (playbackTick[bot] == (size - 1)) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); + } + } + else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME) + { + // End the breather period + inBreather[bot] = false; + botPlaybackPaused[bot] = false; + if (playbackTick[bot] == 0) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + } + // Start the bot if first tick. Clear bot if last tick. + playbackTick[bot]++; + if (playbackTick[bot] == size) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + } + } + } + else + { + // Check whether somebody is actually spectating the bot + int spec; + for (spec = 1; spec < MAXPLAYERS + 1; spec++) + { + if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot]) + { + break; + } + } + if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot])) + { + playbackTickData[bot].Clear(); + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + return; + } + + // Load in the next tick + repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0); + repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1); + repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2); + repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3); + repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4); + repButtons = playbackTickData[bot].Get(playbackTick[bot], 5); + repFlags = playbackTickData[bot].Get(playbackTick[bot], 6); + + // Check if the replay is paused + if (botPlaybackPaused[bot]) + { + TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + return; + } + + // Set velocity to travel from current origin to recorded origin + float currentOrigin[3], velocity[3]; + Movement_GetOrigin(client, currentOrigin); + MakeVectorFromPoints(currentOrigin, repOrigin, velocity); + ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate + TeleportEntity(client, NULL_VECTOR, repAngles, velocity); + + // We need the velocity directly from the replay to calculate the speeds + // for the HUD. + MakeVectorFromPoints(botLastOrigin[bot], repOrigin, velocity); + ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate + CopyVector(repOrigin, botLastOrigin[bot]); + + botSpeed[bot] = GetVectorHorizontalLength(velocity); + buttons = repButtons; + botButtons[bot] = repButtons; + + // Should the bot be ducking?! + if (repButtons & IN_DUCK || repFlags & FL_DUCKING) + { + buttons |= IN_DUCK; + } + + // If the replay file says the bot's on the ground, then fine! Unless you're going too fast... + // Note that we don't mind if replay file says bot isn't on ground but the bot is. + if (repFlags & FL_ONGROUND && Movement_GetSpeed(client) < SPEED_NORMAL * 2) + { + if (timeInAir[bot] > 0) + { + botLandingSpeed[bot] = botSpeed[bot]; + timeInAir[bot] = 0; + botIsTakeoff[bot] = false; + botJumped[bot] = false; + hitBhop[bot] = false; + hitPerf[bot] = false; + if (!Movement_GetOnGround(client)) + { + timeOnGround[bot] = 0; + } + } + + SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND); + Movement_SetMovetype(client, MOVETYPE_WALK); + + timeOnGround[bot]++; + botTakeoffSpeed[bot] = botSpeed[bot]; + } + else + { + if (timeInAir[bot] == 0) + { + botIsTakeoff[bot] = true; + botJumped[bot] = botButtons[bot] & IN_JUMP > 0; + hitBhop[bot] = (timeOnGround[bot] <= RP_MAX_BHOP_GROUND_TICKS) && botJumped[bot]; + + if (botMode[bot] == Mode_SimpleKZ) + { + hitPerf[bot] = timeOnGround[bot] < 3 && botJumped[bot]; + } + else + { + hitPerf[bot] = timeOnGround[bot] < 2 && botJumped[bot]; + } + + if (hitPerf[bot]) + { + if (botMode[bot] == Mode_SimpleKZ) + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], (0.2 * botLandingSpeed[bot] + 200)); + } + else if (botMode[bot] == Mode_KZTimer) + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 380.0); + } + else + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 286.0); + } + } + } + else + { + botJumped[bot] = false; + botIsTakeoff[bot] = false; + } + + timeInAir[bot]++; + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + } + + playbackTick[bot]++; + } +} +void PlaybackVersion2(int client, int bot, int &buttons, float vel[3], float angles[3]) +{ + int size = playbackTickData[bot].Length; + ReplayTickData prevTickData; + ReplayTickData currentTickData; + + // If first or last frame of the playback + if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1)) + { + // Move the bot and pause them at that tick + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + if (!inBreather[bot]) + { + // Start the breather period + inBreather[bot] = true; + breatherStartTime[bot] = GetEngineTime(); + } + else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME) + { + // End the breather period + inBreather[bot] = false; + botPlaybackPaused[bot] = false; + + // Start the bot if first tick. Clear bot if last tick. + playbackTick[bot]++; + if (playbackTick[bot] == size) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + } + } + } + else + { + // Check whether somebody is actually spectating the bot + int spec; + for (spec = 1; spec < MAXPLAYERS + 1; spec++) + { + if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot]) + { + break; + } + } + if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot])) + { + playbackTickData[bot].Clear(); + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + return; + } + + // Load in the next tick + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + + // Check if the replay is paused + if (botPlaybackPaused[bot]) + { + TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + return; + } + + // Play timer start/end sound, if necessary. Reset teleports + if (playbackTick[bot] == preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + botCurrentTeleport[bot] = 0; + } + if (playbackTick[bot] == botTimeTicks[bot] + preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); + } + // We use the previous position/velocity data to recreate sounds accurately. + // This might not be necessary as we already did do this in OnPlayerRunCmdPost of last tick, + // but we do it again just in case the values don't match up somehow (eg. collision with moving objects?) + TeleportEntity(client, NULL_VECTOR, prevTickData.angles, prevTickData.velocity); + // TeleportEntity does not set the absolute origin and velocity so we need to do it + // to prevent inaccurate eye position interpolation. + SetEntPropVector(client, Prop_Data, "m_vecVelocity", prevTickData.velocity); + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", prevTickData.velocity); + + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", prevTickData.origin); + SetEntPropVector(client, Prop_Data, "m_vecOrigin", prevTickData.origin); + + + // Set buttons and potential inputs. + int newButtons; + if (currentTickData.flags & RP_IN_ATTACK) + { + newButtons |= IN_ATTACK; + } + if (currentTickData.flags & RP_IN_ATTACK2) + { + newButtons |= IN_ATTACK2; + } + if (currentTickData.flags & RP_IN_JUMP) + { + newButtons |= IN_JUMP; + } + if (currentTickData.flags & RP_IN_DUCK || currentTickData.flags & RP_FL_DUCKING) + { + newButtons |= IN_DUCK; + } + // Few assumptions here because the replay doesn't track them: Player doesn't use +klook or +strafe. + // If the assumptions are wrong we will just end up with wrong sound prediction, no big deal. + if (currentTickData.flags & RP_IN_FORWARD) + { + newButtons |= IN_FORWARD; + vel[0] += RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_BACK) + { + newButtons |= IN_BACK; + vel[0] -= RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_MOVELEFT) + { + newButtons |= IN_MOVELEFT; + vel[1] -= RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_MOVERIGHT) + { + newButtons |= IN_MOVERIGHT; + vel[1] += RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_LEFT) + { + newButtons |= IN_LEFT; + } + if (currentTickData.flags & RP_IN_RIGHT) + { + newButtons |= IN_RIGHT; + } + if (currentTickData.flags & RP_IN_RELOAD) + { + newButtons |= IN_RELOAD; + } + if (currentTickData.flags & RP_IN_SPEED) + { + newButtons |= IN_SPEED; + } + buttons = newButtons; + botButtons[bot] = buttons; + // The angles might be wrong if the player teleports, but this should only affect sound prediction. + angles = currentTickData.angles; + + // Set the bot's MoveType + MoveType replayMoveType = view_as<MoveType>(prevTickData.flags & RP_MOVETYPE_MASK); + botMoveType[bot] = replayMoveType; + if (replayMoveType == MOVETYPE_WALK) + { + Movement_SetMovetype(client, MOVETYPE_WALK); + } + else if (replayMoveType == MOVETYPE_LADDER) + { + botPaused[bot] = false; + Movement_SetMovetype(client, MOVETYPE_LADDER); + } + else + { + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + } + // Set some variables + if (currentTickData.flags & RP_TELEPORT_TICK) + { + botJustTeleported[bot] = true; + botCurrentTeleport[bot]++; + } + + if (currentTickData.flags & RP_TAKEOFF_TICK) + { + hitPerf[bot] = currentTickData.flags & RP_HIT_PERF > 0; + botIsTakeoff[bot] = true; + botTakeoffSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity); + } + + if ((currentTickData.flags & RP_SECONDARY_EQUIPPED) && !IsCurrentWeaponSecondary(client)) + { + int item = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + if (item != -1) + { + char name[64]; + GetEntityClassname(item, name, sizeof(name)); + FakeClientCommand(client, "use %s", name); + } + } + else if (!(currentTickData.flags & RP_SECONDARY_EQUIPPED) && IsCurrentWeaponSecondary(client)) + { + int item = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE); + if (item != -1) + { + char name[64]; + GetEntityClassname(item, name, sizeof(name)); + FakeClientCommand(client, "use %s", name); + } + } + + #if defined DEBUG + if(!botPlaybackPaused[bot]) + { + PrintToServer("Tick: %d", playbackTick[bot]); + PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NONE"); + + if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK"); + if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2"); + if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP"); + if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK"); + if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD"); + if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK"); + if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT"); + if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT"); + if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT"); + if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT"); + if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD"); + if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED"); + if(currentTickData.flags & RP_IN_USE) PrintToServer("IN_USE"); + if(currentTickData.flags & RP_IN_BULLRUSH) PrintToServer("IN_BULLRUSH"); + + if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND"); + if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING"); + if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM"); + if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0"); + if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT"); + if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF"); + if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF"); + if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED"); + PrintToServer("=============================================================="); + } + #endif + } +} + +void PlaybackVersion2Post(int client, int bot) +{ + if (botPlaybackPaused[bot]) + { + return; + } + int size = playbackTickData[bot].Length; + if (playbackTick[bot] != 0 && playbackTick[bot] != (size - 1)) + { + ReplayTickData currentTickData; + ReplayTickData prevTickData; + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + + // TeleportEntity does not set the absolute origin and velocity so we need to do it + // to prevent inaccurate eye position interpolation. + SetEntPropVector(client, Prop_Data, "m_vecVelocity", currentTickData.velocity); + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", currentTickData.velocity); + + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", currentTickData.origin); + SetEntPropVector(client, Prop_Data, "m_vecOrigin", currentTickData.origin); + + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[0]", currentTickData.angles[0]); + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[1]", currentTickData.angles[1]); + + MoveType replayMoveType = view_as<MoveType>(currentTickData.flags & RP_MOVETYPE_MASK); + botMoveType[bot] = replayMoveType; + int entityFlags = GetEntityFlags(client); + if (replayMoveType == MOVETYPE_WALK) + { + if (currentTickData.flags & RP_FL_ONGROUND) + { + SetEntityFlags(client, entityFlags | FL_ONGROUND); + botPaused[bot] = false; + // The bot is on the ground, so there must be a ground entity attributed to the bot. + int groundEnt = GetEntPropEnt(client, Prop_Send, "m_hGroundEntity"); + if (groundEnt == -1 && botJustTeleported[bot]) + { + SetEntPropFloat(client, Prop_Send, "m_flFallVelocity", 0.0); + float endPosition[3], mins[3], maxs[3]; + GetEntPropVector(client, Prop_Send, "m_vecMaxs", maxs); + GetEntPropVector(client, Prop_Send, "m_vecMins", mins); + endPosition = currentTickData.origin; + endPosition[2] -= 2.0; + TR_TraceHullFilter(currentTickData.origin, endPosition, mins, maxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + + // This should always hit. + if (TR_DidHit()) + { + groundEnt = TR_GetEntityIndex(); + SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", groundEnt); + } + } + } + else + { + botJustTeleported[bot] = false; + } + } + + if (currentTickData.flags & RP_UNDER_WATER) + { + SetEntityFlags(client, entityFlags | FL_INWATER); + } + + botSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity); + playbackTick[bot]++; + } +} + +// Set the bot client's GOKZ options, clan tag and name based on the loaded replay data +static void SetBotStuff(int bot) +{ + if (!botInGame[bot] || !botDataLoaded[bot]) + { + return; + } + + int client = botClient[bot]; + + // Set its movement options just in case it could negatively affect the playback + GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]); + GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]); + + // Clan tag and name + SetBotClanTag(bot); + SetBotName(bot); + + // Bot takes one tick after being put in server to be able to respawn. + RequestFrame(RequestFrame_SetBotStuff, GetClientUserId(client)); +} + +public void RequestFrame_SetBotStuff(int userid) +{ + int client = GetClientOfUserId(userid); + if (!client) + { + return; + } + int bot; + for (bot = 0; bot <= RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + break; + } + else if (bot == RP_MAX_BOTS) + { + return; + } + } + // Set the bot's team based on if it's NUB or PRO + if (botReplayType[bot] == ReplayType_Run + && GOKZ_GetTimeTypeEx(botTeleportsUsed[bot]) == TimeType_Pro) + { + GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true); + } + else + { + GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true); + } + // Set bot weapons + // Always start by removing the pistol and knife + int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + if (currentPistol != -1) + { + RemovePlayerItem(client, currentPistol); + } + + int currentKnife = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE); + if (currentKnife != -1) + { + RemovePlayerItem(client, currentKnife); + } + + char weaponName[128]; + // Give the bot the knife stored in the replay + /* + if (botKnife[bot] != 0) + { + CS_WeaponIDToAlias(CS_ItemDefIndexToID(botKnife[bot]), weaponName, sizeof(weaponName)); + Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName); + GivePlayerItem(client, weaponName); + } + else + { + GivePlayerItem(client, "weapon_knife"); + } + */ + // We are currently not doing that, as it would require us to disable the + // FollowCSGOServerGuidelines failsafe if the bot has a non-standard knife. + GivePlayerItem(client, "weapon_knife"); + + // Give the bot the pistol stored in the replay + if (botWeapon[bot] != -1) + { + CS_WeaponIDToAlias(CS_ItemDefIndexToID(botWeapon[bot]), weaponName, sizeof(weaponName)); + Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName); + GivePlayerItem(client, weaponName); + } + + botCurrentTeleport[bot] = 0; +} + +static void SetBotClanTag(int bot) +{ + char tag[MAX_NAME_LENGTH]; + + if (botReplayType[bot] == ReplayType_Run) + { + if (botCourse[bot] == 0) + { + // KZT PRO + FormatEx(tag, sizeof(tag), "%s %s", + gC_ModeNamesShort[botMode[bot]], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); + } + else + { + // KZT B2 PRO + FormatEx(tag, sizeof(tag), "%s B%d %s", + gC_ModeNamesShort[botMode[bot]], botCourse[bot], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); + } + } + else if (botReplayType[bot] == ReplayType_Jump) + { + // KZT LJ + FormatEx(tag, sizeof(tag), "%s %s", + gC_ModeNamesShort[botMode[bot]], gC_JumpTypesShort[botJumpType[bot]]); + } + else + { + // KZT + FormatEx(tag, sizeof(tag), "%s", + gC_ModeNamesShort[botMode[bot]]); + } + + CS_SetClientClanTag(botClient[bot], tag); +} + +static void SetBotName(int bot) +{ + char name[MAX_NAME_LENGTH]; + + if (botReplayType[bot] == ReplayType_Run) + { + // DanZay (01:23.45) + FormatEx(name, sizeof(name), "%s (%s)", + botAlias[bot], GOKZ_FormatTime(botTime[bot])); + } + else if (botReplayType[bot] == ReplayType_Jump) + { + if (botJumpBlockDistance[bot] == 0) + { + // DanZay (291.44) + FormatEx(name, sizeof(name), "%s (%.2f)", + botAlias[bot], botJumpDistance[bot]); + } + else + { + // DanZay (291.44 on 289 block) + FormatEx(name, sizeof(name), "%s (%.2f on %d block)", + botAlias[bot], botJumpDistance[bot], botJumpBlockDistance[bot]); + } + } + else + { + // DanZay + FormatEx(name, sizeof(name), "%s", + botAlias[bot]); + } + + gB_HideNameChange = true; + SetClientName(botClient[bot], name); +} + +// Returns the number of bots that are currently replaying +static int GetBotsInUse() +{ + int botsInUse = 0; + for (int bot; bot < RP_MAX_BOTS; bot++) + { + if (botInGame[bot] && botDataLoaded[bot]) + { + botsInUse++; + } + } + return botsInUse; +} + +// Returns a bot that isn't currently replaying, or -1 if no unused bots found +static int GetUnusedBot() +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (!botInGame[bot]) + { + return bot; + } + } + return -1; +} + +static void PlaybackSkipToTick(int bot, int tick) +{ + if (botReplayVersion[bot] == 1) + { + // Load in the next tick + float repOrigin[3], repAngles[3]; + repOrigin[0] = playbackTickData[bot].Get(tick, 0); + repOrigin[1] = playbackTickData[bot].Get(tick, 1); + repOrigin[2] = playbackTickData[bot].Get(tick, 2); + repAngles[0] = playbackTickData[bot].Get(tick, 3); + repAngles[1] = playbackTickData[bot].Get(tick, 4); + + TeleportEntity(botClient[bot], repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + } + else if (botReplayVersion[bot] == 2) + { + // Load in the next tick + ReplayTickData currentTickData; + playbackTickData[bot].GetArray(tick, currentTickData); + + TeleportEntity(botClient[bot], currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + int direction = tick < playbackTick[bot] ? -1 : 1; + for (int i = playbackTick[bot]; i != tick; i += direction) + { + playbackTickData[bot].GetArray(i, currentTickData); + if (currentTickData.flags & RP_TELEPORT_TICK) + { + botCurrentTeleport[bot] += direction; + } + } + + #if defined DEBUG + PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NONE)) PrintToServer("MOVETYPE_NONE"); + + if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK"); + if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2"); + if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP"); + if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK"); + if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD"); + if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK"); + if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT"); + if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT"); + if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT"); + if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT"); + if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD"); + if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED"); + if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND"); + if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING"); + if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM"); + if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0"); + if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT"); + if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF"); + if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF"); + if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED"); + PrintToServer("=============================================================="); + #endif + } + + Movement_SetMovetype(botClient[bot], MOVETYPE_NOCLIP); + playbackTick[bot] = tick; +} + +static bool IsCurrentWeaponSecondary(int client) +{ + int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + return activeWeaponEnt == secondaryEnt; +} + +static void MakePlayerSpectate(int client, int bot) +{ + GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR); + SetEntProp(client, Prop_Send, "m_iObserverMode", 4); + SetEntPropEnt(client, Prop_Send, "m_hObserverTarget", bot); + + int clientUserID = GetClientUserId(client); + DataPack data = new DataPack(); + data.WriteCell(clientUserID); + data.WriteCell(GetClientUserId(bot)); + CreateTimer(0.1, Timer_UpdateBotName, GetClientUserId(bot)); + EnableReplayControls(client); +} + +public Action Timer_UpdateBotName(Handle timer, int botUID) +{ + Event e = CreateEvent("spec_target_updated"); + e.SetInt("userid", botUID); + e.Fire(); + return Plugin_Continue; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/recording.sp b/sourcemod/scripting/gokz-replays/recording.sp new file mode 100644 index 0000000..babbd5e --- /dev/null +++ b/sourcemod/scripting/gokz-replays/recording.sp @@ -0,0 +1,990 @@ +/* + Bot replay recording logic and processes. + + Records data every time OnPlayerRunCmdPost is called. + If the player doesn't have their timer running, it keeps track + of the last 2 minutes of their actions. If a player is banned + while their timer isn't running, those 2 minutes are saved. + If the player has their timer running, the recording is done from + the beginning of the run. If the player can no longer beat their PB, + then the recording goes back to only keeping track of the last + two minutes. Upon beating their PB, a temporary binary file will be + written with a 'header' containing information about the run, + followed by the recorded tick data from OnPlayerRunCmdPost. + The binary file will be permanently locally saved on the server + if the run beats the server record. +*/ + +static float tickrate; +static int preAndPostRunTickCount; +static int maxCheaterReplayTicks; +static int recordingIndex[MAXPLAYERS + 1]; +static float playerSensitivity[MAXPLAYERS + 1]; +static float playerMYaw[MAXPLAYERS + 1]; +static bool isTeleportTick[MAXPLAYERS + 1]; +static ReplaySaveState replaySaveState[MAXPLAYERS + 1]; +static bool recordingPaused[MAXPLAYERS + 1]; +static bool postRunRecording[MAXPLAYERS + 1]; +static ArrayList recordedRecentData[MAXPLAYERS + 1]; +static ArrayList recordedRunData[MAXPLAYERS + 1]; +static ArrayList recordedPostRunData[MAXPLAYERS + 1]; +static Handle runningRunBreatherTimer[MAXPLAYERS + 1]; +static ArrayList runningJumpstatTimers[MAXPLAYERS + 1]; + +// =====[ EVENTS ]===== + +void OnMapStart_Recording() +{ + CreateReplaysDirectory(gC_CurrentMap); + tickrate = 1/GetTickInterval(); + preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME * tickrate); + maxCheaterReplayTicks = RoundToCeil(RP_MAX_CHEATER_REPLAY_LENGTH * tickrate); +} + +void OnClientPutInServer_Recording(int client) +{ + ClearClientRecordingState(client); +} + +void OnClientAuthorized_Recording(int client) +{ + // Apparently the client isn't valid yet here, so we can't check for that! + if(!IsFakeClient(client)) + { + // Create directory path for player if not exists + char replayPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d", RP_DIRECTORY_JUMPS, GetSteamAccountID(client)); + if (!DirExists(replayPath)) + { + CreateDirectory(replayPath, 511); + } + BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d/%s", RP_DIRECTORY_JUMPS, GetSteamAccountID(client), RP_DIRECTORY_BLOCKJUMPS); + if (!DirExists(replayPath)) + { + CreateDirectory(replayPath, 511); + } + } +} + +void OnClientDisconnect_Recording(int client) +{ + // Stop exceptions if OnClientPutInServer was never ran for this client id. + // As long as the arrays aren't null we'll be fine. + if (runningJumpstatTimers[client] == null) + { + return; + } + + // Trigger all timers early + if(!IsFakeClient(client)) + { + if (runningRunBreatherTimer[client] != INVALID_HANDLE) + { + TriggerTimer(runningRunBreatherTimer[client], false); + } + + // We have to clone the array because the timer callback removes the timer + // from the array we're running over, and doing weird tricks is scary. + ArrayList timers = runningJumpstatTimers[client].Clone(); + for (int i = 0; i < timers.Length; i++) + { + Handle timer = timers.Get(i); + TriggerTimer(timer, false); + } + delete timers; + } + + ClearClientRecordingState(client); +} + +void OnPlayerRunCmdPost_Recording(int client, int buttons, int tickCount, const float vel[3], const int mouse[2]) +{ + if (!IsValidClient(client) || IsFakeClient(client) || !IsPlayerAlive(client) || recordingPaused[client]) + { + return; + } + + ReplayTickData tickData; + + Movement_GetOrigin(client, tickData.origin); + + tickData.mouse = mouse; + tickData.vel = vel; + Movement_GetVelocity(client, tickData.velocity); + Movement_GetEyeAngles(client, tickData.angles); + tickData.flags = EncodePlayerFlags(client, buttons, tickCount); + tickData.packetsPerSecond = GetClientAvgPackets(client, NetFlow_Incoming); + tickData.laggedMovementValue = GetEntPropFloat(client, Prop_Send, "m_flLaggedMovementValue"); + tickData.buttonsForced = GetEntProp(client, Prop_Data, "m_afButtonForced"); + + // HACK: Reset teleport tick marker. Too bad! + if (isTeleportTick[client]) + { + isTeleportTick[client] = false; + } + + if (replaySaveState[client] != ReplaySave_Disabled) + { + int runTick = GetArraySize(recordedRunData[client]); + if (runTick < RP_MAX_DURATION) + { + // Resize might fail if the timer exceed the max duration, + // as it is not guaranteed to allocate more than 1GB of contiguous memory, + // causing mass lag spikes that kick everyone out of the server. + // We can still attempt to save the rest of the recording though. + recordedRunData[client].Resize(runTick + 1); + recordedRunData[client].SetArray(runTick, tickData); + } + } + if (postRunRecording[client]) + { + int tick = GetArraySize(recordedPostRunData[client]); + if (tick < RP_MAX_DURATION) + { + recordedPostRunData[client].Resize(tick + 1); + recordedPostRunData[client].SetArray(tick, tickData); + } + } + + int tick = recordingIndex[client]; + if (recordedRecentData[client].Length < maxCheaterReplayTicks) + { + recordedRecentData[client].Resize(recordedRecentData[client].Length + 1); + recordingIndex[client] = recordingIndex[client] + 1 == maxCheaterReplayTicks ? 0 : recordingIndex[client] + 1; + } + else + { + recordingIndex[client] = RecordingIndexAdd(client, 1); + } + + recordedRecentData[client].SetArray(tick, tickData); +} + +Action GOKZ_OnTimerStart_Recording(int client) +{ + // Hack to fix an exception when starting the timer on the very + // first tick after loading the plugin. + if (recordedRecentData[client].Length == 0) + { + return Plugin_Handled; + } + + return Plugin_Continue; +} + +void GOKZ_OnTimerStart_Post_Recording(int client) +{ + replaySaveState[client] = ReplaySave_Local; + StartRunRecording(client); +} + +void GOKZ_OnTimerEnd_Recording(int client, int course, float time, int teleportsUsed) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteFloat(time); + data.WriteCell(teleportsUsed); + data.WriteCell(replaySaveState[client]); + // The previous run breather still did not finish, end it now or + // we will start overwriting the data. + if (runningRunBreatherTimer[client] != INVALID_HANDLE) + { + TriggerTimer(runningRunBreatherTimer[client], false); + } + + replaySaveState[client] = ReplaySave_Disabled; + postRunRecording[client] = true; + + // Swap recordedRunData and recordedPostRunData. + // This lets new runs start immediately, before the post-run breather is + // finished recording. + ArrayList tmp = recordedPostRunData[client]; + recordedPostRunData[client] = recordedRunData[client]; + recordedRunData[client] = tmp; + recordedRunData[client].Clear(); + + runningRunBreatherTimer[client] = CreateTimer(RP_PLAYBACK_BREATHER_TIME, Timer_EndRecording, data); + if (runningRunBreatherTimer[client] == INVALID_HANDLE) + { + LogError("Could not create a timer so can't end the run replay recording"); + } +} + +public Action Timer_EndRecording(Handle timer, DataPack data) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + float time = data.ReadFloat(); + int teleportsUsed = data.ReadCell(); + ReplaySaveState saveState = data.ReadCell(); + delete data; + + // The client left after the run was done but before the post-run + // breather had the chance to finish. This should not happen, as we + // trigger all running timers on disconnect. + if (!IsValidClient(client)) + { + return Plugin_Stop; + } + + runningRunBreatherTimer[client] = INVALID_HANDLE; + postRunRecording[client] = false; + + if (gB_GOKZLocalDB && GOKZ_DB_IsCheater(client)) + { + // Replay might be submitted globally, but will not be saved locally. + saveState = ReplaySave_Temp; + } + + char path[PLATFORM_MAX_PATH]; + if (SaveRecordingOfRun(path, client, course, time, teleportsUsed, saveState == ReplaySave_Temp)) + { + Call_OnTimerEnd_Post(client, path, course, time, teleportsUsed); + } + else + { + Call_OnTimerEnd_Post(client, "", course, time, teleportsUsed); + } + + return Plugin_Stop; +} + +void GOKZ_OnPause_Recording(int client) +{ + PauseRecording(client); +} + +void GOKZ_OnResume_Recording(int client) +{ + ResumeRecording(client); +} + +void GOKZ_OnTimerStopped_Recording(int client) +{ + replaySaveState[client] = ReplaySave_Disabled; +} + +void GOKZ_OnCountedTeleport_Recording(int client) +{ + if (gB_NubRecordMissed[client]) + { + replaySaveState[client] = ReplaySave_Disabled; + } + + isTeleportTick[client] = true; +} + +void GOKZ_LR_OnRecordMissed_Recording(int client, int recordType) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + // If missed PRO record or both records, then can no longer beat a server record + if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro) + { + replaySaveState[client] = ReplaySave_Temp; + } + + // If on a NUB run and missed NUB record, then can no longer beat a server record + // Otherwise wait to see if they teleport before stopping the recording + if (recordType == RecordType_Nub) + { + if (GOKZ_GetTeleportCount(client) > 0) + { + replaySaveState[client] = ReplaySave_Temp; + } + } +} + +public void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + // If missed PRO record or both records, then can no longer beat PB + if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro) + { + replaySaveState[client] = ReplaySave_Disabled; + } + + // If on a NUB run and missed NUB record, then can no longer beat PB + // Otherwise wait to see if they teleport before stopping the recording + if (recordType == RecordType_Nub) + { + if (GOKZ_GetTeleportCount(client) > 0) + { + replaySaveState[client] = ReplaySave_Disabled; + } + } +} + +void GOKZ_AC_OnPlayerSuspected_Recording(int client, ACReason reason) +{ + SaveRecordingOfCheater(client, reason); +} + +void GOKZ_DB_OnJumpstatPB_Recording(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(jumptype); + data.WriteFloat(distance); + data.WriteCell(block); + data.WriteCell(strafes); + data.WriteFloat(sync); + data.WriteFloat(pre); + data.WriteFloat(max); + data.WriteCell(airtime); + + Handle timer = CreateTimer(RP_PLAYBACK_BREATHER_TIME, SaveJump, data); + if (timer != INVALID_HANDLE) + { + runningJumpstatTimers[client].Push(timer); + } + else + { + LogError("Could not create a timer so can't save jumpstat pb replay"); + } +} + +public Action SaveJump(Handle timer, DataPack data) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int jumptype = data.ReadCell(); + float distance = data.ReadFloat(); + int block = data.ReadCell(); + int strafes = data.ReadCell(); + float sync = data.ReadFloat(); + float pre = data.ReadFloat(); + float max = data.ReadFloat(); + int airtime = data.ReadCell(); + delete data; + + // The client left after the jump was done but before the post-jump + // breather had the chance to finish. This should not happen, as we + // trigger all running timers on disconnect. + if (!IsValidClient(client)) + { + return Plugin_Stop; + } + + RemoveFromRunningTimers(client, timer); + + SaveRecordingOfJump(client, jumptype, distance, block, strafes, sync, pre, max, airtime); + return Plugin_Stop; +} + + + +// =====[ PRIVATE ]===== + +static void ClearClientRecordingState(int client) +{ + recordingIndex[client] = 0; + playerSensitivity[client] = -1.0; + playerMYaw[client] = -1.0; + isTeleportTick[client] = false; + replaySaveState[client] = ReplaySave_Disabled; + recordingPaused[client] = false; + postRunRecording[client] = false; + runningRunBreatherTimer[client] = INVALID_HANDLE; + + if (recordedRecentData[client] == null) + recordedRecentData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (recordedRunData[client] == null) + recordedRunData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (recordedPostRunData[client] == null) + recordedPostRunData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (runningJumpstatTimers[client] == null) + runningJumpstatTimers[client] = new ArrayList(); + + recordedRecentData[client].Clear(); + recordedRunData[client].Clear(); + recordedPostRunData[client].Clear(); + runningJumpstatTimers[client].Clear(); +} + +static void StartRunRecording(int client) +{ + if (IsFakeClient(client)) + { + return; + } + + QueryClientConVar(client, "sensitivity", SensitivityCheck, client); + QueryClientConVar(client, "m_yaw", MYAWCheck, client); + + DiscardRecording(client); + ResumeRecording(client); + + // Copy pre data + int index; + recordedRunData[client].Resize(preAndPostRunTickCount); + if (recordedRecentData[client].Length < preAndPostRunTickCount) + { + index = recordingIndex[client] - preAndPostRunTickCount; + } + else + { + index = RecordingIndexAdd(client, -preAndPostRunTickCount); + } + for (int i = 0; i < preAndPostRunTickCount; i++) + { + ReplayTickData tickData; + if (index < 0) + { + recordedRecentData[client].GetArray(0, tickData); + recordedRunData[client].SetArray(i, tickData); + index += 1; + } + else + { + recordedRecentData[client].GetArray(index, tickData); + recordedRunData[client].SetArray(i, tickData); + index = RecordingIndexAdd(client, -preAndPostRunTickCount + i + 1); + } + } +} + +static void DiscardRecording(int client) +{ + recordedRunData[client].Clear(); + Call_OnReplayDiscarded(client); +} + +static void PauseRecording(int client) +{ + recordingPaused[client] = true; +} + +static void ResumeRecording(int client) +{ + recordingPaused[client] = false; +} + +static bool SaveRecordingOfRun(char replayPath[PLATFORM_MAX_PATH], int client, int course, float time, int teleportsUsed, bool temp) +{ + // Prepare data + int timeType = GOKZ_GetTimeTypeEx(teleportsUsed); + + // Create and fill General Header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Run, recordedPostRunData[client].Length); + + // Create and fill Run Header + RunReplayHeader runHeader; + runHeader.time = time; + runHeader.course = course; + runHeader.teleportsUsed = teleportsUsed; + + // Build path and create/overwrite associated file + FormatRunReplayPath(replayPath, sizeof(replayPath), course, generalHeader.mode, generalHeader.style, timeType, temp); + if (FileExists(replayPath)) + { + DeleteFile(replayPath); + } + else if (!temp) + { + AddToReplayInfoCache(course, generalHeader.mode, generalHeader.style, timeType); + SortReplayInfoCache(); + } + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + return false; + } + + WriteGeneralHeader(file, generalHeader); + + // Write run header + file.WriteInt32(view_as<int>(runHeader.time)); + file.WriteInt8(runHeader.course); + file.WriteInt32(runHeader.teleportsUsed); + + WriteTickData(file, client, ReplayType_Run); + + delete file; + // If there is no plugin that wants to take over the replay file, we will delete it ourselves. + if (Call_OnReplaySaved(client, ReplayType_Run, gC_CurrentMap, course, timeType, time, replayPath, temp) == Plugin_Continue && temp) + { + DeleteFile(replayPath); + } + + return true; +} + +static bool SaveRecordingOfCheater(int client, ACReason reason) +{ + // Create and fill general header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Cheater, recordedRecentData[client].Length); + + // Create and fill cheater header + CheaterReplayHeader cheaterHeader; + cheaterHeader.ACReason = reason; + + //Build path and create/overwrite associated file + char replayPath[PLATFORM_MAX_PATH]; + FormatCheaterReplayPath(replayPath, sizeof(replayPath), client, generalHeader.mode, generalHeader.style); + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + return false; + } + + WriteGeneralHeader(file, generalHeader); + file.WriteInt8(view_as<int>(cheaterHeader.ACReason)); + WriteTickData(file, client, ReplayType_Cheater); + + delete file; + + return true; +} + +static bool SaveRecordingOfJump(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + // Just cause I know how buggy jumpstats can be + int airtimeTicks = RoundToNearest((float(airtime) / GOKZ_DB_JS_AIRTIME_PRECISION) * tickrate); + if (airtimeTicks + 2 * preAndPostRunTickCount >= maxCheaterReplayTicks) + { + LogError("WARNING: Invalid airtime (this is probably a bugged jump, please report it!)."); + return false; + } + + // Create and fill general header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Jump, 2 * preAndPostRunTickCount + airtimeTicks); + + // Create and fill jump header + JumpReplayHeader jumpHeader; + FillJumpHeader(jumpHeader, jumptype, distance, block, strafes, sync, pre, max, airtime); + + // Make sure the client is authenticated + if (GetSteamAccountID(client) == 0) + { + LogError("Failed to save jump, client is not authenticated."); + return false; + } + + // Build path and create/overwrite associated file + char replayPath[PLATFORM_MAX_PATH]; + if (block > 0) + { + FormatBlockJumpReplayPath(replayPath, sizeof(replayPath), client, block, jumpHeader.jumpType, generalHeader.mode, generalHeader.style); + } + else + { + FormatJumpReplayPath(replayPath, sizeof(replayPath), client, jumpHeader.jumpType, generalHeader.mode, generalHeader.style); + } + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + delete file; + return false; + } + + WriteGeneralHeader(file, generalHeader); + WriteJumpHeader(file, jumpHeader); + WriteTickData(file, client, ReplayType_Jump, airtimeTicks); + + delete file; + + return true; +} + +static void FillGeneralHeader(GeneralReplayHeader generalHeader, int client, int replayType, int tickCount) +{ + // Prepare data + int mode = GOKZ_GetCoreOption(client, Option_Mode); + int style = GOKZ_GetCoreOption(client, Option_Style); + + // Fill general header + generalHeader.magicNumber = RP_MAGIC_NUMBER; + generalHeader.formatVersion = RP_FORMAT_VERSION; + generalHeader.replayType = replayType; + generalHeader.gokzVersion = GOKZ_VERSION; + generalHeader.mapName = gC_CurrentMap; + generalHeader.mapFileSize = gI_CurrentMapFileSize; + generalHeader.serverIP = FindConVar("hostip").IntValue; + generalHeader.timestamp = GetTime(); + GetClientName(client, generalHeader.playerAlias, sizeof(GeneralReplayHeader::playerAlias)); + generalHeader.playerSteamID = GetSteamAccountID(client); + generalHeader.mode = mode; + generalHeader.style = style; + generalHeader.playerSensitivity = playerSensitivity[client]; + generalHeader.playerMYaw = playerMYaw[client]; + generalHeader.tickrate = tickrate; + generalHeader.tickCount = tickCount; + generalHeader.equippedWeapon = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_SECONDARY); + generalHeader.equippedKnife = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_KNIFE); +} + +static void FillJumpHeader(JumpReplayHeader jumpHeader, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + jumpHeader.jumpType = jumptype; + jumpHeader.distance = distance; + jumpHeader.blockDistance = block; + jumpHeader.strafeCount = strafes; + jumpHeader.sync = sync; + jumpHeader.pre = pre; + jumpHeader.max = max; + jumpHeader.airtime = airtime; +} + +static void WriteGeneralHeader(File file, GeneralReplayHeader generalHeader) +{ + file.WriteInt32(generalHeader.magicNumber); + file.WriteInt8(generalHeader.formatVersion); + file.WriteInt8(generalHeader.replayType); + file.WriteInt8(strlen(generalHeader.gokzVersion)); + file.WriteString(generalHeader.gokzVersion, false); + file.WriteInt8(strlen(generalHeader.mapName)); + file.WriteString(generalHeader.mapName, false); + file.WriteInt32(generalHeader.mapFileSize); + file.WriteInt32(generalHeader.serverIP); + file.WriteInt32(generalHeader.timestamp); + file.WriteInt8(strlen(generalHeader.playerAlias)); + file.WriteString(generalHeader.playerAlias, false); + file.WriteInt32(generalHeader.playerSteamID); + file.WriteInt8(generalHeader.mode); + file.WriteInt8(generalHeader.style); + file.WriteInt32(view_as<int>(generalHeader.playerSensitivity)); + file.WriteInt32(view_as<int>(generalHeader.playerMYaw)); + file.WriteInt32(view_as<int>(generalHeader.tickrate)); + file.WriteInt32(generalHeader.tickCount); + file.WriteInt32(generalHeader.equippedWeapon); + file.WriteInt32(generalHeader.equippedKnife); +} + +static void WriteJumpHeader(File file, JumpReplayHeader jumpHeader) +{ + file.WriteInt8(jumpHeader.jumpType); + file.WriteInt32(view_as<int>(jumpHeader.distance)); + file.WriteInt32(jumpHeader.blockDistance); + file.WriteInt8(jumpHeader.strafeCount); + file.WriteInt32(view_as<int>(jumpHeader.sync)); + file.WriteInt32(view_as<int>(jumpHeader.pre)); + file.WriteInt32(view_as<int>(jumpHeader.max)); + file.WriteInt32((jumpHeader.airtime)); +} + +static void WriteTickData(File file, int client, int replayType, int airtime = 0) +{ + ReplayTickData tickData; + ReplayTickData prevTickData; + bool isFirstTick = true; + switch(replayType) + { + case ReplayType_Run: + { + for (int i = 0; i < recordedPostRunData[client].Length; i++) + { + recordedPostRunData[client].GetArray(i, tickData); + recordedPostRunData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + } + case ReplayType_Cheater: + { + for (int i = 0; i < recordedRecentData[client].Length; i++) + { + int rollingI = RecordingIndexAdd(client, i); + recordedRecentData[client].GetArray(rollingI, tickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + + } + case ReplayType_Jump: + { + int replayLength = 2 * preAndPostRunTickCount + airtime; + for (int i = 0; i < replayLength; i++) + { + int rollingI = RecordingIndexAdd(client, i - replayLength); + recordedRecentData[client].GetArray(rollingI, tickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + } + } +} + +static void WriteTickDataToFile(File file, bool isFirstTick, ReplayTickData tickDataStruct, ReplayTickData prevTickDataStruct) +{ + any tickData[RP_V2_TICK_DATA_BLOCKSIZE]; + any prevTickData[RP_V2_TICK_DATA_BLOCKSIZE]; + TickDataToArray(tickDataStruct, tickData); + TickDataToArray(prevTickDataStruct, prevTickData); + + int deltaFlags = (1 << RPDELTA_DELTAFLAGS); + if (isFirstTick) + { + // NOTE: Set every bit to 1 until RP_V2_TICK_DATA_BLOCKSIZE. + deltaFlags = (1 << (RP_V2_TICK_DATA_BLOCKSIZE)) - 1; + } + else + { + // NOTE: Test tickData against prevTickData for differences. + for (int i = 1; i < sizeof(tickData); i++) + { + // If the bits in tickData[i] are different to prevTickData[i], then + // set the corresponding bitflag. + if (tickData[i] ^ prevTickData[i]) + { + deltaFlags |= (1 << i); + } + } + } + + file.WriteInt32(deltaFlags); + // NOTE: write only data that has changed since the previous tick. + for (int i = 1; i < sizeof(tickData); i++) + { + int currentFlag = (1 << i); + if (deltaFlags & currentFlag) + { + file.WriteInt32(tickData[i]); + } + } +} + +static void FormatRunReplayPath(char[] buffer, int maxlength, int course, int mode, int style, int timeType, bool tempPath) +{ + // Use GetEngineTime to prevent accidental replay overrides. + // Technically it would still be possible to override this file by accident, + // if somehow the server restarts to this exact map and course, + // and this function is run at the exact same time, but that is extremely unlikely. + // Also by then this file should have already been deleted. + char tempTimeString[32]; + Format(tempTimeString, sizeof(tempTimeString), "%f_", GetEngineTime()); + BuildPath(Path_SM, buffer, maxlength, + "%s/%s/%s%d_%s_%s_%s.%s", + tempPath ? RP_DIRECTORY_RUNS_TEMP : RP_DIRECTORY_RUNS, + gC_CurrentMap, + tempPath ? tempTimeString : "", + course, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + gC_TimeTypeNames[timeType], + RP_FILE_EXTENSION); +} + +static void FormatCheaterReplayPath(char[] buffer, int maxlength, int client, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d_%s_%d_%s_%s.%s", + RP_DIRECTORY_CHEATERS, + GetSteamAccountID(client), + gC_CurrentMap, + GetTime(), + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static void FormatJumpReplayPath(char[] buffer, int maxlength, int client, int jumpType, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d/%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, + GetSteamAccountID(client), + jumpType, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static void FormatBlockJumpReplayPath(char[] buffer, int maxlength, int client, int block, int jumpType, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d/%s/%d_%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, + GetSteamAccountID(client), + RP_DIRECTORY_BLOCKJUMPS, + jumpType, + block, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static int EncodePlayerFlags(int client, int buttons, int tickCount) +{ + int flags = 0; + MoveType movetype = Movement_GetMovetype(client); + int clientFlags = GetEntityFlags(client); + + flags = view_as<int>(movetype) & RP_MOVETYPE_MASK; + + SetKthBit(flags, 4, IsBitSet(buttons, IN_ATTACK)); + SetKthBit(flags, 5, IsBitSet(buttons, IN_ATTACK2)); + SetKthBit(flags, 6, IsBitSet(buttons, IN_JUMP)); + SetKthBit(flags, 7, IsBitSet(buttons, IN_DUCK)); + SetKthBit(flags, 8, IsBitSet(buttons, IN_FORWARD)); + SetKthBit(flags, 9, IsBitSet(buttons, IN_BACK)); + SetKthBit(flags, 10, IsBitSet(buttons, IN_LEFT)); + SetKthBit(flags, 11, IsBitSet(buttons, IN_RIGHT)); + SetKthBit(flags, 12, IsBitSet(buttons, IN_MOVELEFT)); + SetKthBit(flags, 13, IsBitSet(buttons, IN_MOVERIGHT)); + SetKthBit(flags, 14, IsBitSet(buttons, IN_RELOAD)); + SetKthBit(flags, 15, IsBitSet(buttons, IN_SPEED)); + SetKthBit(flags, 16, IsBitSet(buttons, IN_USE)); + SetKthBit(flags, 17, IsBitSet(buttons, IN_BULLRUSH)); + SetKthBit(flags, 18, IsBitSet(clientFlags, FL_ONGROUND)); + SetKthBit(flags, 19, IsBitSet(clientFlags, FL_DUCKING)); + SetKthBit(flags, 20, IsBitSet(clientFlags, FL_SWIM)); + + SetKthBit(flags, 21, GetEntProp(client, Prop_Data, "m_nWaterLevel") != 0); + + SetKthBit(flags, 22, isTeleportTick[client]); + SetKthBit(flags, 23, Movement_GetTakeoffTick(client) == tickCount); + SetKthBit(flags, 24, GOKZ_GetHitPerf(client)); + SetKthBit(flags, 25, IsCurrentWeaponSecondary(client)); + + return flags; +} + +// Function to set the bitNum bit in integer to value +static void SetKthBit(int &number, int offset, bool value) +{ + int intValue = value ? 1 : 0; + number |= intValue << offset; +} + +static bool IsBitSet(int number, int checkBit) +{ + return (number & checkBit) ? true : false; +} + +static int GetPlayerWeaponSlotDefIndex(int client, int slot) +{ + int ent = GetPlayerWeaponSlot(client, slot); + + // Nothing equipped in the slot + if (ent == -1) + { + return -1; + } + + return GetEntProp(ent, Prop_Send, "m_iItemDefinitionIndex"); +} + +static bool IsCurrentWeaponSecondary(int client) +{ + int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + return activeWeaponEnt == secondaryEnt; +} + +static void CreateReplaysDirectory(const char[] map) +{ + char path[PLATFORM_MAX_PATH]; + + // Create parent replay directory + BuildPath(Path_SM, path, sizeof(path), RP_DIRECTORY); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create maps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + + // Create maps replay directory + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, map); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create maps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS_TEMP); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + + // Create maps replay directory + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS_TEMP, map); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create cheaters replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_CHEATERS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create jumps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_JUMPS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } +} + +public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + playerMYaw[client] = StringToFloat(cvarValue); + } +} + +public void SensitivityCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + playerSensitivity[client] = StringToFloat(cvarValue); + } +} + +static int RecordingIndexAdd(int client, int offset) +{ + int index = recordingIndex[client] + offset; + if (index < 0) + { + index += recordedRecentData[client].Length; + } + return index % recordedRecentData[client].Length; +} + +static void RemoveFromRunningTimers(int client, Handle timerToRemove) +{ + int index = runningJumpstatTimers[client].FindValue(timerToRemove); + if (index != -1) + { + runningJumpstatTimers[client].Erase(index); + } +} diff --git a/sourcemod/scripting/gokz-replays/replay_cache.sp b/sourcemod/scripting/gokz-replays/replay_cache.sp new file mode 100644 index 0000000..83f36d0 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/replay_cache.sp @@ -0,0 +1,176 @@ +/* + Cached info about the map's available replay bots stored in an ArrayList. +*/ + + + +// =====[ PUBLIC ]===== + +// Adds a replay to the cache +void AddToReplayInfoCache(int course, int mode, int style, int timeType) +{ + int index = g_ReplayInfoCache.Length; + g_ReplayInfoCache.Resize(index + 1); + g_ReplayInfoCache.Set(index, course, 0); + g_ReplayInfoCache.Set(index, mode, 1); + g_ReplayInfoCache.Set(index, style, 2); + g_ReplayInfoCache.Set(index, timeType, 3); +} + +// Use this to sort the cache after finished adding to it +void SortReplayInfoCache() +{ + g_ReplayInfoCache.SortCustom(SortFunc_ReplayInfoCache); +} + +public int SortFunc_ReplayInfoCache(int index1, int index2, Handle array, Handle hndl) +{ + // Do not expect any indexes to be 'equal' + int replayInfo1[RP_CACHE_BLOCKSIZE], replayInfo2[RP_CACHE_BLOCKSIZE]; + g_ReplayInfoCache.GetArray(index1, replayInfo1); + g_ReplayInfoCache.GetArray(index2, replayInfo2); + + // Compare courses - lower course number goes first + if (replayInfo1[0] < replayInfo2[0]) + { + return -1; + } + else if (replayInfo1[0] > replayInfo2[0]) + { + return 1; + } + // Same course, so compare mode + else if (replayInfo1[1] < replayInfo2[1]) + { + return -1; + } + else if (replayInfo1[1] > replayInfo2[1]) + { + return 1; + } + // Same course and mode, so compare style + else if (replayInfo1[2] < replayInfo2[2]) + { + return -1; + } + else if (replayInfo1[2] > replayInfo2[2]) + { + return 1; + } + // Same course, mode and style so compare time type, assuming can't be identical + else if (replayInfo1[3] == TimeType_Pro) + { + return 1; + } + return -1; +} + + + +// =====[ EVENTS ]===== + +void OnMapStart_ReplayCache() +{ + if (g_ReplayInfoCache == null) + { + g_ReplayInfoCache = new ArrayList(RP_CACHE_BLOCKSIZE, 0); + } + else + { + g_ReplayInfoCache.Clear(); + } + + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, gC_CurrentMap); + DirectoryListing dir = OpenDirectory(path); + + // We want to find files that look like "0_KZT_NRM_PRO.rec" + char file[PLATFORM_MAX_PATH], pieces[4][16]; + int length, dotpos, course, mode, style, timeType; + + while (dir.GetNext(file, sizeof(file))) + { + // Some credit to Influx Timer - https://github.com/TotallyMehis/Influx-Timer + + // Check file extension + length = strlen(file); + dotpos = 0; + for (int i = 0; i < length; i++) + { + if (file[i] == '.') + { + dotpos = i; + } + } + if (!StrEqual(file[dotpos + 1], RP_FILE_EXTENSION, false)) + { + continue; + } + + // Remove file extension + Format(file, dotpos + 1, file); + + // Break down file name into pieces + if (ExplodeString(file, "_", pieces, sizeof(pieces), sizeof(pieces[])) != sizeof(pieces)) + { + continue; + } + + // Extract info from the pieces + course = StringToInt(pieces[0]); + mode = GetModeIDFromString(pieces[1]); + style = GetStyleIDFromString(pieces[2]); + timeType = GetTimeTypeIDFromString(pieces[3]); + if (!GOKZ_IsValidCourse(course) || mode == -1 || style == -1 || timeType == -1) + { + continue; + } + + // Add it to the cache + AddToReplayInfoCache(course, mode, style, timeType); + } + + SortReplayInfoCache(); + + delete dir; +} + + + +// =====[ PRIVATE ]===== + +static int GetModeIDFromString(const char[] mode) +{ + for (int modeID = 0; modeID < MODE_COUNT; modeID++) + { + if (StrEqual(mode, gC_ModeNamesShort[modeID], false)) + { + return modeID; + } + } + return -1; +} + +static int GetStyleIDFromString(const char[] style) +{ + for (int styleID = 0; styleID < STYLE_COUNT; styleID++) + { + if (StrEqual(style, gC_StyleNamesShort[styleID], false)) + { + return styleID; + } + } + return -1; +} + +static int GetTimeTypeIDFromString(const char[] timeType) +{ + for (int timeTypeID = 0; timeTypeID < TIMETYPE_COUNT; timeTypeID++) + { + if (StrEqual(timeType, gC_TimeTypeNames[timeTypeID], false)) + { + return timeTypeID; + } + } + return -1; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/replay_menu.sp b/sourcemod/scripting/gokz-replays/replay_menu.sp new file mode 100644 index 0000000..94acd66 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/replay_menu.sp @@ -0,0 +1,139 @@ +/* + Lets player select a replay bot to play back. +*/ + + + +static int selectedReplayMode[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void DisplayReplayModeMenu(int client) +{ + if (g_ReplayInfoCache.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Map)"); + GOKZ_PlayErrorSound(client); + return; + } + + Menu menu = new Menu(MenuHandler_ReplayMode); + menu.SetTitle("%T", "Replay Menu (Mode) - Title", client, gC_CurrentMap); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ EVENTS ]===== + +public int MenuHandler_ReplayMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + selectedReplayMode[param1] = param2; + DisplayReplayMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_Replay(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[4]; + menu.GetItem(param2, info, sizeof(info)); + int replayIndex = StringToInt(info); + int replayInfo[RP_CACHE_BLOCKSIZE]; + g_ReplayInfoCache.GetArray(replayIndex, replayInfo); + + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), + "%s/%s/%d_%s_%s_%s.%s", + RP_DIRECTORY_RUNS, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION); + if (!FileExists(path)) + { + BuildPath(Path_SM, path, sizeof(path), + "%s/%d_%s_%s_%s.%s", + RP_DIRECTORY, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION); + if (!FileExists(path)) + { + LogError("Failed to load file: \"%s\".", path); + GOKZ_PrintToChat(param1, true, "%t", "Replay Menu - No File"); + return 0; + } + } + + LoadReplayBot(param1, path); + } + else if (action == MenuAction_Cancel) + { + DisplayReplayModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +static void DisplayReplayMenu(int client) +{ + Menu menu = new Menu(MenuHandler_Replay); + menu.SetTitle("%T", "Replay Menu - Title", client, gC_CurrentMap, gC_ModeNames[selectedReplayMode[client]]); + if (ReplayMenuAddItems(client, menu) > 0) + { + menu.Display(client, MENU_TIME_FOREVER); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Mode)", gC_ModeNames[selectedReplayMode[client]]); + GOKZ_PlayErrorSound(client); + DisplayReplayModeMenu(client); + } +} + +// Returns the number of replay menu items added +static int ReplayMenuAddItems(int client, Menu menu) +{ + int replaysAdded = 0; + int replayCount = g_ReplayInfoCache.Length; + int replayInfo[RP_CACHE_BLOCKSIZE]; + char temp[32], indexString[4]; + + menu.RemoveAllItems(); + + for (int i = 0; i < replayCount; i++) + { + IntToString(i, indexString, sizeof(indexString)); + g_ReplayInfoCache.GetArray(i, replayInfo); + if (replayInfo[1] != selectedReplayMode[client]) // Wrong mode! + { + continue; + } + + if (replayInfo[0] == 0) + { + FormatEx(temp, sizeof(temp), "Main %s", gC_TimeTypeNames[replayInfo[3]]); + } + else + { + FormatEx(temp, sizeof(temp), "Bonus %d %s", replayInfo[0], gC_TimeTypeNames[replayInfo[3]]); + } + menu.AddItem(indexString, temp, ITEMDRAW_DEFAULT); + + replaysAdded++; + } + + return replaysAdded; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-saveloc.sp b/sourcemod/scripting/gokz-saveloc.sp new file mode 100644 index 0000000..98d64c0 --- /dev/null +++ b/sourcemod/scripting/gokz-saveloc.sp @@ -0,0 +1,822 @@ +#include <sourcemod> + +#include <cstrike> +#include <sdktools> + +#include <gokz/core> +#include <gokz/kzplayer> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> +#include <gokz/hud> +#pragma newdecls required +#pragma semicolon 1 + +public Plugin myinfo = +{ + name = "GOKZ SaveLoc", + author = "JWL", + description = "Allows players to save/load locations that preserve position, angles, and velocity", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-saveloc.txt" +#define LOADLOC_INVALIDATE_DURATION 0.12 +#define MAX_LOCATION_NAME_LENGTH 32 + +enum struct Location { + // Location name must be first for FindString to work. + char locationName[MAX_LOCATION_NAME_LENGTH]; + char locationCreator[MAX_NAME_LENGTH]; + + // GOKZ related states + int mode; + int course; + float currentTime; + ArrayList checkpointData; + int checkpointCount; + int teleportCount; + ArrayList undoTeleportData; + + // Movement related states + int groundEnt; + int flags; + float position[3]; + float angles[3]; + float velocity[3]; + float duckAmount; + bool ducking; + bool ducked; + float lastDuckTime; + float duckSpeed; + float stamina; + MoveType movetype; + float ladderNormal[3]; + int collisionGroup; + float waterJumpTime; + bool hasWalkMovedSinceLastJump; + float ignoreLadderJumpTimeOffset; + float lastPositionAtFullCrouchSpeed[2]; + + void Create(int client, int target) + { + GetClientName(client, this.locationCreator, sizeof(Location::locationCreator)); + this.groundEnt = GetEntPropEnt(target, Prop_Data, "m_hGroundEntity"); + this.flags = GetEntityFlags(target); + this.mode = GOKZ_GetCoreOption(target, Option_Mode); + this.course = GOKZ_GetCourse(target); + GetClientAbsOrigin(target, this.position); + GetClientEyeAngles(target, this.angles); + GetEntPropVector(target, Prop_Data, "m_vecVelocity", this.velocity); + this.duckAmount = GetEntPropFloat(target, Prop_Send, "m_flDuckAmount"); + this.ducking = !!GetEntProp(target, Prop_Send, "m_bDucking"); + this.ducked = !!GetEntProp(target, Prop_Send, "m_bDucked"); + this.lastDuckTime = GetEntPropFloat(target, Prop_Send, "m_flLastDuckTime"); + this.duckSpeed = Movement_GetDuckSpeed(target); + this.stamina = GetEntPropFloat(target, Prop_Send, "m_flStamina"); + this.movetype = Movement_GetMovetype(target); + GetEntPropVector(target, Prop_Send, "m_vecLadderNormal", this.ladderNormal); + this.collisionGroup = GetEntProp(target, Prop_Send, "m_CollisionGroup"); + this.waterJumpTime = GetEntPropFloat(target, Prop_Data, "m_flWaterJumpTime"); + this.hasWalkMovedSinceLastJump = !!GetEntProp(target, Prop_Data, "m_bHasWalkMovedSinceLastJump"); + this.ignoreLadderJumpTimeOffset = GetEntPropFloat(target, Prop_Data, "m_ignoreLadderJumpTime") - GetGameTime(); + GetLastPositionAtFullCrouchSpeed(target, this.lastPositionAtFullCrouchSpeed); + + if (GOKZ_GetTimerRunning(target)) + { + this.currentTime = GOKZ_GetTime(target); + } + else + { + this.currentTime = -1.0; + } + this.checkpointData = GOKZ_GetCheckpointData(target); + this.checkpointCount = GOKZ_GetCheckpointCount(target); + this.teleportCount = GOKZ_GetTeleportCount(target); + this.undoTeleportData = GOKZ_GetUndoTeleportData(target); + } + + bool Load(int client) + { + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return false; + } + if (!GOKZ_SetMode(client, this.mode)) + { + GOKZ_PrintToChat(client, true, "%t", "LoadLoc - Mode Not Available"); + } + GOKZ_SetCourse(client, this.course); + if (this.currentTime >= 0.0) + { + GOKZ_SetTime(client, this.currentTime); + } + GOKZ_SetCheckpointData(client, this.checkpointData, GOKZ_CHECKPOINT_VERSION); + GOKZ_SetCheckpointCount(client, this.checkpointCount); + GOKZ_SetTeleportCount(client, this.teleportCount); + GOKZ_SetUndoTeleportData(client, this.undoTeleportData, GOKZ_CHECKPOINT_VERSION); + + SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", this.groundEnt); + SetEntityFlags(client, this.flags); + TeleportEntity(client, this.position, this.angles, this.velocity); + SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", this.duckAmount); + SetEntProp(client, Prop_Send, "m_bDucking", this.ducking); + SetEntProp(client, Prop_Send, "m_bDucked", this.ducked); + SetEntPropFloat(client, Prop_Send, "m_flLastDuckTime", this.lastDuckTime); + Movement_SetDuckSpeed(client, this.duckSpeed); + SetEntPropFloat(client, Prop_Send, "m_flStamina", this.stamina); + Movement_SetMovetype(client, this.movetype); + SetEntPropVector(client, Prop_Send, "m_vecLadderNormal", this.ladderNormal); + SetEntProp(client, Prop_Send, "m_CollisionGroup", this.collisionGroup); + SetEntPropFloat(client, Prop_Data, "m_flWaterJumpTime", this.waterJumpTime); + SetEntProp(client, Prop_Data, "m_bHasWalkMovedSinceLastJump", this.hasWalkMovedSinceLastJump); + SetEntPropFloat(client, Prop_Data, "m_ignoreLadderJumpTime", this.ignoreLadderJumpTimeOffset + GetGameTime()); + SetLastPositionAtFullCrouchSpeed(client, this.lastPositionAtFullCrouchSpeed); + + GOKZ_InvalidateRun(client); + return true; + } +} + +ArrayList gA_Locations; +bool gB_LocMenuOpen[MAXPLAYERS + 1]; +bool gB_UsedLoc[MAXPLAYERS + 1]; +int gI_MostRecentLocation[MAXPLAYERS + 1]; +float gF_LastLoadlocTime[MAXPLAYERS + 1]; + +bool gB_GOKZHUD; + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-saveloc"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-saveloc.phrases"); + + HookEvents(); + RegisterCommands(); + CreateArrays(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZHUD = LibraryExists("gokz-hud"); +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZHUD = gB_GOKZHUD || StrEqual(name, "gokz-hud"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZHUD = gB_GOKZHUD && !StrEqual(name, "gokz-hud"); +} + +public void OnMapStart() +{ + ClearLocations(); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + gF_LastLoadlocTime[client] = 0.0; +} + +public void OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + CloseLocMenu(client); +} + +public void OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + int team = event.GetInt("team"); + + if (team == CS_TEAM_SPECTATOR) + { + CloseLocMenu(client); + } +} + +public Action GOKZ_OnTimerStart(int client, int course) +{ + CloseLocMenu(client); + gB_UsedLoc[client] = false; + if (GetGameTime() < gF_LastLoadlocTime[client] + LOADLOC_INVALIDATE_DURATION) + { + return Plugin_Stop; + } + return Plugin_Continue; +} + +public Action GOKZ_OnTimerEnd(int client, int course, float time) +{ + if (gB_UsedLoc[client]) + { + PrintEndTimeString_SaveLoc(client, course, time); + } + return Plugin_Continue; +} + +// =====[ GENERAL ]===== + +void HookEvents() +{ + HookEvent("player_death", OnPlayerDeath); + HookEvent("player_team", OnPlayerJoinTeam); +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_saveloc", Command_SaveLoc, "[KZ] Save location. Usage: !saveloc <name>"); + RegConsoleCmd("sm_loadloc", Command_LoadLoc, "[KZ] Load location. Usage: !loadloc <#id OR name>"); + RegConsoleCmd("sm_prevloc", Command_PrevLoc, "[KZ] Go back to the previous location."); + RegConsoleCmd("sm_nextloc", Command_NextLoc, "[KZ] Go forward to the next location."); + RegConsoleCmd("sm_locmenu", Command_LocMenu, "[KZ] Open location menu."); + RegConsoleCmd("sm_nameloc", Command_NameLoc, "[KZ] Name location. Usage: !nameloc <#id> <name>"); +} + +public Action Command_SaveLoc(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + int target = -1; + if (!IsPlayerAlive(client)) + { + KZPlayer player = KZPlayer(client); + target = player.ObserverTarget; + if (target == -1) + { + GOKZ_PrintToChat(client, true, "%t", "Must Be Alive"); + GOKZ_PlayErrorSound(client); + return Plugin_Handled; + } + } + + if (args == 0) + { + // save location with empty <name> + SaveLocation(client, "", target); + } + else if (args == 1) + { + // get location <name> + char arg[MAX_LOCATION_NAME_LENGTH]; + GetCmdArg(1, arg, sizeof(arg)); + + if (IsValidLocationName(arg)) + { + // save location with <name> + SaveLocation(client, arg, target); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format"); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "SaveLoc - Usage"); + } + + return Plugin_Handled; +} + +public Action Command_LoadLoc(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + else if (!IsPlayerAlive(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Must Be Alive"); + return Plugin_Handled; + } + else if (gA_Locations.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Locations Found"); + return Plugin_Handled; + } + + if (args == 0) + { + // load most recent location + int id = gI_MostRecentLocation[client]; + LoadLocation(client, id); + } + else if (args == 1) + { + // get location <#id OR name> + char arg[MAX_LOCATION_NAME_LENGTH]; + GetCmdArg(1, arg, sizeof(arg)); + int id; + + if (arg[0] == '#') + { + // load location <#id> + id = StringToInt(arg[1]); + } + else + { + // load location <name> + id = gA_Locations.FindString(arg); + } + + if (IsValidLocationId(id)) + { + if (LoadLocation(client, id)) + { + gF_LastLoadlocTime[client] = GetGameTime(); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Location Not Found"); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "LoadLoc - Usage"); + } + + return Plugin_Handled; +} + +public Action Command_PrevLoc(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + else if (gA_Locations.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Locations Found"); + return Plugin_Handled; + } + else if (gI_MostRecentLocation[client] <= 0) + { + GOKZ_PrintToChat(client, true, "%t", "PrevLoc - Can't Prev Location (No Location Found)"); + return Plugin_Handled; + } + LoadLocation(client, gI_MostRecentLocation[client] - 1); + return Plugin_Handled; +} + +public Action Command_NextLoc(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + else if (gA_Locations.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Locations Found"); + return Plugin_Handled; + } + else if (gI_MostRecentLocation[client] >= gA_Locations.Length - 1) + { + GOKZ_PrintToChat(client, true, "%t", "NextLoc - Can't Next Location (No Location Found)"); + return Plugin_Handled; + } + LoadLocation(client, gI_MostRecentLocation[client] + 1); + return Plugin_Handled; +} + +public Action Command_NameLoc(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + else if (gA_Locations.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Locations Found"); + return Plugin_Handled; + } + + if (args == 0) + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Usage"); + } + else if (args == 1) + { + // name most recent location + char arg[MAX_LOCATION_NAME_LENGTH]; + GetCmdArg(1, arg, sizeof(arg)); + int id = gI_MostRecentLocation[client]; + + if (IsValidLocationName(arg) && IsClientLocationCreator(client, id)) + { + NameLocation(client, id, arg); + } + else if (!IsClientLocationCreator(client, id)) + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Not Creator"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format"); + } + } + else if (args == 2) + { + // name specified location + char arg1[MAX_LOCATION_NAME_LENGTH]; + char arg2[MAX_LOCATION_NAME_LENGTH]; + GetCmdArg(1, arg1, sizeof(arg1)); + GetCmdArg(2, arg2, sizeof(arg2)); + int id = StringToInt(arg1[1]); + + if (IsValidLocationId(id)) + { + + if (IsValidLocationName(arg2) && IsClientLocationCreator(client, id)) + { + NameLocation(client, id, arg2); + } + else if (!IsClientLocationCreator(client, id)) + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Not Creator"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format"); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Location Not Found"); + } + } + else + { + GOKZ_PrintToChat(client, true, "%t", "NameLoc - Usage"); + } + + return Plugin_Handled; +} + +public Action Command_LocMenu(int client, int args) +{ + if (!IsValidClient(client)) + { + return Plugin_Handled; + } + else if (!IsPlayerAlive(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Must Be Alive"); + return Plugin_Handled; + } + else if (gA_Locations.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Locations Found"); + return Plugin_Handled; + } + + ShowLocMenu(client); + + return Plugin_Handled; +} + +// ====[ SAVELOC MENU ]==== + +void ShowLocMenu(int client) +{ + Menu locMenu = new Menu(LocMenuHandler, MENU_ACTIONS_ALL); + locMenu.SetTitle("%t", "LocMenu - Title"); + + // fill the menu with all locations + for (int i = 0; i < gA_Locations.Length; i++) + { + char item[MAX_LOCATION_NAME_LENGTH]; + Format(item, sizeof(item), "%i", i); + locMenu.AddItem(item, item); + } + + // calculate which page of the menu contains client's most recent location + int firstItem; + if (gI_MostRecentLocation[client] > 5) + { + firstItem = gI_MostRecentLocation[client] - (gI_MostRecentLocation[client] % 6); + } + + locMenu.DisplayAt(client, firstItem, MENU_TIME_FOREVER); +} + + + +// ====[ SAVELOC MENU HANDLER ]==== + +public int LocMenuHandler(Menu menu, MenuAction action, int client, int choice) +{ + switch (action) + { + case MenuAction_Display: + { + gB_LocMenuOpen[client] = true; + } + + case MenuAction_DisplayItem: + { + Location loc; + char item[MAX_LOCATION_NAME_LENGTH]; + menu.GetItem(choice, item, sizeof(item)); + + int id = StringToInt(item); + gA_Locations.GetArray(id, loc); + char name[MAX_LOCATION_NAME_LENGTH]; + strcopy(name, sizeof(name), loc.locationName); + + if (id == gI_MostRecentLocation[client]) + { + Format(item, sizeof(item), "> #%i %s", id, name); + } + else + { + Format(item, sizeof(item), "#%i %s", id, name); + } + + return RedrawMenuItem(item); + } + + case MenuAction_Select: + { + char item[MAX_LOCATION_NAME_LENGTH]; + menu.GetItem(choice, item, sizeof(item)); + ReplaceString(item, sizeof(item), "#", ""); + int id = StringToInt(item); + + LoadLocation(client, id); + } + + case MenuAction_Cancel: + { + gB_LocMenuOpen[client] = false; + } + + case MenuAction_End: + { + delete menu; + } + } + + return 0; +} + + + +// ====[ SAVE LOCATION ]==== + +void SaveLocation(int client, char[] name, int target) +{ + Location loc; + if (target == -1) + { + target = client; + } + loc.Create(client, target); + strcopy(loc.locationName, sizeof(Location::locationName), name); + GetClientName(client, loc.locationCreator, sizeof(loc.locationCreator)); + gA_Locations.PushArray(loc); + gI_MostRecentLocation[client] = gA_Locations.Length - 1; + + GOKZ_PrintToChat(client, true, "%t", "SaveLoc - ID Name", gA_Locations.Length - 1, name); + + for (int i = 1; i <= MaxClients; i++) + { + RefreshLocMenu(i); + } +} + + + +// ====[ LOAD LOCATION ]==== + +bool LoadLocation(int client, int id) +{ + if (!IsPlayerAlive(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Must Be Alive"); + return false; + } + char clientName[MAX_NAME_LENGTH]; + + GetClientName(client, clientName, sizeof(clientName)); + Location loc; + gA_Locations.GetArray(id, loc); + if (loc.Load(client)) + { + gB_UsedLoc[client] = true; + if (gB_GOKZHUD) + { + GOKZ_HUD_ForceUpdateTPMenu(client); + } + } + else + { + return false; + } + // print message if loading new location + if (gI_MostRecentLocation[client] != id) + { + gI_MostRecentLocation[client] = id; + + if (StrEqual(clientName, loc.locationCreator)) + { + GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Name", id, loc.locationName); + } + else + { + if (StrEqual(loc.locationName, "")) + { + GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Creator", id, loc.locationCreator); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Name Creator", id, loc.locationName, loc.locationCreator); + } + } + } + + RefreshLocMenu(client); + + return true; +} + + + +// ====[ NAME LOCATION ]==== + +void NameLocation(int client, int id, char[] name) +{ + Location loc; + gA_Locations.GetArray(id, loc); + strcopy(loc.locationName, sizeof(Location::locationName), name); + + GOKZ_PrintToChat(client, true, "%t", "NameLoc - ID Name", id, name); + + for (int i = 1; i <= MaxClients; i++) + { + RefreshLocMenu(i); + } +} + + + +// =====[ HELPER FUNCTIONS ]===== + +void CreateArrays() +{ + gA_Locations = new ArrayList(sizeof(Location)); +} + +void ClearLocations() +{ + Location loc; + for (int i = 0; i < gA_Locations.Length; i++) + { + // Prevent memory leak + gA_Locations.GetArray(i, loc); + delete loc.checkpointData; + } + gA_Locations.Clear(); + for (int i = 1; i <= MaxClients; i++) + { + gI_MostRecentLocation[i] = -1; + gB_LocMenuOpen[i] = false; + } +} + +void RefreshLocMenu(int client) +{ + if (gB_LocMenuOpen[client]) + { + ShowLocMenu(client); + } +} + +void CloseLocMenu(int client) +{ + if (gB_LocMenuOpen[client]) + { + CancelClientMenu(client, true); + gB_LocMenuOpen[client] = false; + } +} + +bool IsValidLocationId(int id) +{ + return !(id < 0) && !(id > gA_Locations.Length - 1); +} + +bool IsValidLocationName(char[] name) +{ + // check if location name starts with letter and is unique + return IsCharAlpha(name[0]) && gA_Locations.FindString(name) == -1; +} + +bool IsClientLocationCreator(int client, int id) +{ + char clientName[MAX_NAME_LENGTH]; + Location loc; + gA_Locations.GetArray(id, loc); + GetClientName(client, clientName, sizeof(clientName)); + + return StrEqual(clientName, loc.locationCreator); +} + +void GetLastPositionAtFullCrouchSpeed(int client, float origin[2]) +{ + // m_vecLastPositionAtFullCrouchSpeed is right after m_flDuckSpeed. + int baseOffset = FindSendPropInfo("CBasePlayer", "m_flDuckSpeed"); + origin[0] = GetEntDataFloat(client, baseOffset + 4); + origin[1] = GetEntDataFloat(client, baseOffset + 8); +} + +void SetLastPositionAtFullCrouchSpeed(int client, float origin[2]) +{ + int baseOffset = FindSendPropInfo("CBasePlayer", "m_flDuckSpeed"); + SetEntDataFloat(client, baseOffset + 4, origin[0]); + SetEntDataFloat(client, baseOffset + 8, origin[1]); +} + +// ====[ PRIVATE ]==== + +static void PrintEndTimeString_SaveLoc(int client, int course, float time) +{ + if (course == 0) + { + switch (GOKZ_GetTimeType(client)) + { + case TimeType_Nub: + { + GOKZ_PrintToChat(client, true, "%t", "Beat Map (NUB)", + client, + GOKZ_FormatTime(time), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + case TimeType_Pro: + { + GOKZ_PrintToChat(client, true, "%t", "Beat Map (PRO)", + client, + GOKZ_FormatTime(time), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + } + } + else + { + switch (GOKZ_GetTimeType(client)) + { + case TimeType_Nub: + { + GOKZ_PrintToChat(client, true, "%t", "Beat Bonus (NUB)", + client, + GOKZ_GetCourse(client), + GOKZ_FormatTime(time), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + case TimeType_Pro: + { + GOKZ_PrintToChat(client, true, "%t", "Beat Bonus (PRO)", + client, + GOKZ_GetCourse(client), + GOKZ_FormatTime(time), + gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + } + } +} diff --git a/sourcemod/scripting/gokz-slayonend.sp b/sourcemod/scripting/gokz-slayonend.sp new file mode 100644 index 0000000..c83c4e6 --- /dev/null +++ b/sourcemod/scripting/gokz-slayonend.sp @@ -0,0 +1,190 @@ +#include <sourcemod> + +#include <gokz/core> +#include <gokz/slayonend> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Slay On End", + author = "DanZay", + description = "Adds option to slay the player upon ending their timer", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-slayonend.txt" + +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemSlayOnEnd; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-slayonend"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-slayonend.phrases"); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) +{ + OnTimerEnd_SlayOnEnd(client); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + OnOptionChanged_Options(client, option, newValue); +} + + + +// =====[ OTHER EVENTS ]===== + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + + + +// =====[ SLAY ON END ]===== + +void OnTimerEnd_SlayOnEnd(int client) +{ + if (GOKZ_GetOption(client, SLAYONEND_OPTION_NAME) == SlayOnEnd_Enabled) + { + CreateTimer(3.0, Timer_SlayPlayer, GetClientUserId(client)); + } +} + +public Action Timer_SlayPlayer(Handle timer, int userid) +{ + int client = GetClientOfUserId(userid); + if (IsValidClient(client)) + { + ForcePlayerSuicide(client); + } + return Plugin_Continue; +} + + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOption(); +} + +void RegisterOption() +{ + GOKZ_RegisterOption(SLAYONEND_OPTION_NAME, SLAYONEND_OPTION_DESCRIPTION, + OptionType_Int, SlayOnEnd_Disabled, 0, SLAYONEND_COUNT - 1); +} + +void OnOptionChanged_Options(int client, const char[] option, any newValue) +{ + if (StrEqual(option, SLAYONEND_OPTION_NAME)) + { + switch (newValue) + { + case SlayOnEnd_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Slay On End - Disable"); + } + case SlayOnEnd_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Slay On End - Enable"); + } + } + } +} + + + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + gTMO_ItemSlayOnEnd = gTM_Options.AddItem(SLAYONEND_OPTION_NAME, TopMenuHandler_SlayOnEnd, gTMO_CatGeneral); +} + +public void TopMenuHandler_SlayOnEnd(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (topobj_id != gTMO_ItemSlayOnEnd) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + if (GOKZ_GetOption(param, SLAYONEND_OPTION_NAME) == SlayOnEnd_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - Slay On End", param, + "Options Menu - Disabled", param); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - Slay On End", param, + "Options Menu - Enabled", param); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, SLAYONEND_OPTION_NAME); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-spec.sp b/sourcemod/scripting/gokz-spec.sp new file mode 100644 index 0000000..29f842f --- /dev/null +++ b/sourcemod/scripting/gokz-spec.sp @@ -0,0 +1,323 @@ +#include <sourcemod> + +#include <cstrike> + +#include <gokz/core> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Spectate Menu", + author = "DanZay", + description = "Provides easy ways to spectate players", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-spec.txt" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-spec"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-spec.phrases"); + + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ SPEC MENU ]===== + +int DisplaySpecMenu(int client, bool useFilter = false, char[] filter = "") +{ + Menu menu = new Menu(MenuHandler_Spec); + menu.SetTitle("%T", "Spec Menu - Title", client); + int menuItems = SpecMenuAddItems(client, menu, useFilter, filter); + if (menuItems == 0 || menuItems == 1) + { + delete menu; + } + else + { + menu.Display(client, MENU_TIME_FOREVER); + } + return menuItems; +} + +bool Spectate(int client) +{ + if (!CanSpectate(client)) + { + return false; + } + + GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR); + + // Put player in free look mode and apply according movetype + SetEntProp(client, Prop_Send, "m_iObserverMode", 6); + SetEntityMoveType(client, MOVETYPE_OBSERVER); + return true; +} + +// Returns whether change to spectating the target was successful +bool SpectatePlayer(int client, int target, bool printMessage = true) +{ + if (!CanSpectate(client)) + { + return false; + } + + if (target == client) + { + Spectate(client); + return true; + } + else if (!IsPlayerAlive(target)) + { + if (printMessage) + { + GOKZ_PrintToChat(client, true, "%t", "Spectate Failure (Dead)"); + GOKZ_PlayErrorSound(client); + } + return false; + } + + GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR); + SetEntProp(client, Prop_Send, "m_iObserverMode", 4); + SetEntPropEnt(client, Prop_Send, "m_hObserverTarget", target); + + return true; +} + +bool CanSpectate(int client) +{ + return !IsPlayerAlive(client) || GOKZ_GetPaused(client) || GOKZ_GetCanPause(client); +} + +public int MenuHandler_Spec(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + int target = GetClientOfUserId(StringToInt(info)); + + if (!IsValidClient(target)) + { + GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid"); + GOKZ_PlayErrorSound(param1); + DisplaySpecMenu(param1); + } + else if (!SpectatePlayer(param1, target)) + { + DisplaySpecMenu(param1); + } + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +// Returns number of items added to the menu +int SpecMenuAddItems(int client, Menu menu, bool useFilter, char[] filter) +{ + char display[MAX_NAME_LENGTH + 4]; + int targetCount = 0; + int latestResult; + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || i == client) + { + continue; + } + if (useFilter) + { + FormatEx(display, sizeof(display), "%N", i); + if (StrContains(display, filter, false) != -1) + { + if (IsFakeClient(i)) + { + FormatEx(display, sizeof(display), "BOT %N", i); + } + } + else // If it doesn't fit the filter, move on + { + continue; + } + } + else + { + if (IsFakeClient(i)) + { + FormatEx(display, sizeof(display), "BOT %N", i); + } + else + { + FormatEx(display, sizeof(display), "%N", i); + } + } + latestResult = i; + menu.AddItem(IntToStringEx(GetClientUserId(i)), display, ITEMDRAW_DEFAULT); + targetCount++; + } + // The only spectate-able player is the latest result, this happens when the player issuing the command also fits in the filter + if (targetCount == 1) + { + SpectatePlayer(client, latestResult); + } + + return targetCount; +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_spec", CommandSpec, "[KZ] Spectate another player. Usage: !spec <player>"); + RegConsoleCmd("sm_specs", CommandSpecs, "[KZ] List currently spectating players in chat."); + RegConsoleCmd("sm_speclist", CommandSpecs, "[KZ] List currently spectating players in chat."); +} + +public Action CommandSpec(int client, int args) +{ + // If no arguments, display the spec menu + if (args < 1) + { + if (DisplaySpecMenu(client) == 0) + { + // No targets, so just join spec + Spectate(client); + } + } + // Otherwise try to spectate the player + else + { + char specifiedPlayer[MAX_NAME_LENGTH]; + GetCmdArg(1, specifiedPlayer, sizeof(specifiedPlayer)); + + char targetName[MAX_TARGET_LENGTH]; + int targetList[1], targetCount; + bool tnIsML; + int flags = COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_ALIVE; + + if ((targetCount = ProcessTargetString( + specifiedPlayer, + client, + targetList, + 1, + flags, + targetName, + sizeof(targetName), + tnIsML)) == 1) + { + SpectatePlayer(client, targetList[0]); + } + else if (targetCount == COMMAND_TARGET_AMBIGUOUS) + { + DisplaySpecMenu(client, true, specifiedPlayer); + } + else + { + ReplyToTargetError(client, targetCount); + } + + } + return Plugin_Handled; +} + +public Action CommandSpecs(int client, int args) +{ + int specs = 0; + char specNames[1024]; + + int target = IsPlayerAlive(client) ? client : GetObserverTarget(client); + int targetSpecs = 0; + char targetSpecNames[1024]; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && IsSpectating(i)) + { + specs++; + if (specs == 1) + { + FormatEx(specNames, sizeof(specNames), "{lime}%N", i); + } + else + { + Format(specNames, sizeof(specNames), "%s{grey}, {lime}%N", specNames, i); + } + + if (target != -1 && GetObserverTarget(i) == target) + { + targetSpecs++; + if (targetSpecs == 1) + { + FormatEx(targetSpecNames, sizeof(targetSpecNames), "{lime}%N", i); + } + else + { + Format(targetSpecNames, sizeof(targetSpecNames), "%s{grey}, {lime}%N", targetSpecNames, i); + } + } + } + } + + if (specs == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Spectator List (None)"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Spectator List", specs, specNames); + if (targetSpecs == 0) + { + GOKZ_PrintToChat(client, false, "%t", "Target Spectator List (None)", target); + } + else + { + GOKZ_PrintToChat(client, false, "%t", "Target Spectator List", target, targetSpecs, targetSpecNames); + } + } + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-tips.sp b/sourcemod/scripting/gokz-tips.sp new file mode 100644 index 0000000..8a84b9e --- /dev/null +++ b/sourcemod/scripting/gokz-tips.sp @@ -0,0 +1,357 @@ +#include <sourcemod> + +#include <gokz/core> +#include <gokz/tips> + +#include <autoexecconfig> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#include <gokz/kzplayer> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Tips", + author = "DanZay", + description = "Prints tips to chat periodically based on loaded plugins", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-tips.txt" + +bool gC_PluginsWithTipsLoaded[TIPS_PLUGINS_COUNT]; +ArrayList g_TipPhrases; +int gI_CurrentTip; +Handle gH_TipTimer; +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemTips; +ConVar gCV_gokz_tips_interval; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-tips"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-tips.phrases"); + LoadTranslations("gokz-tips-tips.phrases"); + LoadTranslations("gokz-tips-core.phrases"); + + // Load translations of tips for other GOKZ plugins + char translation[PLATFORM_MAX_PATH]; + for (int i = 0; i < TIPS_PLUGINS_COUNT; i++) + { + FormatEx(translation, sizeof(translation), "gokz-tips-%s.phrases", gC_PluginsWithTips[i]); + LoadTranslations(translation); + } + + CreateConVars(); + RegisterCommands(); + CreateTipsTimer(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + char gokzPlugin[PLATFORM_MAX_PATH]; + for (int i = 0; i < TIPS_PLUGINS_COUNT; i++) + { + FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]); + gC_PluginsWithTipsLoaded[i] = LibraryExists(gokzPlugin); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + char gokzPlugin[PLATFORM_MAX_PATH]; + for (int i = 0; i < TIPS_PLUGINS_COUNT; i++) + { + FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]); + gC_PluginsWithTipsLoaded[i] = gC_PluginsWithTipsLoaded[i] || StrEqual(name, gokzPlugin); + } +} + +public void OnLibraryRemoved(const char[] name) +{ + char gokzPlugin[PLATFORM_MAX_PATH]; + for (int i = 0; i < TIPS_PLUGINS_COUNT; i++) + { + FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]); + gC_PluginsWithTipsLoaded[i] = gC_PluginsWithTipsLoaded[i] && !StrEqual(name, gokzPlugin); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + OnOptionChanged_Options(client, option, newValue); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + LoadTipPhrases(); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + + + +// =====[ CONVARS ]===== + +void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-tips", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_tips_interval = AutoExecConfig_CreateConVar("gokz_tips_interval", "75", "How often GOKZ tips are printed to chat in seconds.", _, true, 1.0, false); + gCV_gokz_tips_interval.AddChangeHook(OnConVarChanged); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); +} + +public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (convar == gCV_gokz_tips_interval) + { + CreateTipsTimer(); + } +} + + + +// =====[ TIPS ]===== + +void LoadTipPhrases() +{ + if (g_TipPhrases == null) + { + g_TipPhrases = new ArrayList(64, 0); + } + else + { + g_TipPhrases.Clear(); + } + + char tipsPath[PLATFORM_MAX_PATH]; + + BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/%s", TIPS_TIPS); + LoadTipPhrasesFromFile(tipsPath); + + BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/%s", TIPS_CORE); + LoadTipPhrasesFromFile(tipsPath); + + // Load tips for other loaded GOKZ plugins + for (int i = 0; i < TIPS_PLUGINS_COUNT; i++) + { + if (gC_PluginsWithTipsLoaded[i]) + { + BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/gokz-tips-%s.phrases.txt", gC_PluginsWithTips[i]); + LoadTipPhrasesFromFile(tipsPath); + } + } + + ShuffleTipPhrases(); +} + +void LoadTipPhrasesFromFile(const char[] filePath) +{ + KeyValues kv = new KeyValues("Phrases"); + if (!kv.ImportFromFile(filePath)) + { + SetFailState("Failed to load file: \"%s\".", filePath); + } + + char phraseName[64]; + kv.GotoFirstSubKey(true); + do + { + kv.GetSectionName(phraseName, sizeof(phraseName)); + g_TipPhrases.PushString(phraseName); + } while (kv.GotoNextKey(true)); + + delete kv; +} + +void ShuffleTipPhrases() +{ + for (int i = g_TipPhrases.Length - 1; i >= 1; i--) + { + int j = GetRandomInt(0, i); + char tempStringI[64]; + g_TipPhrases.GetString(i, tempStringI, sizeof(tempStringI)); + char tempStringJ[64]; + g_TipPhrases.GetString(j, tempStringJ, sizeof(tempStringJ)); + g_TipPhrases.SetString(i, tempStringJ); + g_TipPhrases.SetString(j, tempStringI); + } +} + +void CreateTipsTimer() +{ + if (gH_TipTimer != null) + { + delete gH_TipTimer; + } + gH_TipTimer = CreateTimer(gCV_gokz_tips_interval.FloatValue, Timer_PrintTip, _, TIMER_REPEAT); +} + +public Action Timer_PrintTip(Handle timer) +{ + char tip[256]; + g_TipPhrases.GetString(gI_CurrentTip, tip, sizeof(tip)); + + for (int client = 1; client <= MaxClients; client++) + { + KZPlayer player = KZPlayer(client); + if (player.InGame && player.Tips != Tips_Disabled) + { + GOKZ_PrintToChat(client, true, "%t", tip); + } + } + + gI_CurrentTip = NextIndex(gI_CurrentTip, g_TipPhrases.Length); + return Plugin_Continue; +} + + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOption(); +} + +void RegisterOption() +{ + GOKZ_RegisterOption(TIPS_OPTION_NAME, TIPS_OPTION_DESCRIPTION, + OptionType_Int, Tips_Enabled, 0, TIPS_COUNT - 1); +} + +void OnOptionChanged_Options(int client, const char[] option, any newValue) +{ + if (StrEqual(option, TIPS_OPTION_NAME)) + { + switch (newValue) + { + case Tips_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Tips - Disable"); + } + case Tips_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - Tips - Enable"); + } + } + } +} + + + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + gTMO_ItemTips = gTM_Options.AddItem(TIPS_OPTION_NAME, TopMenuHandler_Tips, gTMO_CatGeneral); +} + +public void TopMenuHandler_Tips(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (topobj_id != gTMO_ItemTips) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + if (GOKZ_GetOption(param, TIPS_OPTION_NAME) == Tips_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - Tips", param, + "Options Menu - Disabled", param); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - Tips", param, + "Options Menu - Enabled", param); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, TIPS_OPTION_NAME); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +} + + + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_tips", CommandToggleTips, "[KZ] Toggle seeing help and tips."); +} + +public Action CommandToggleTips(int client, int args) +{ + if (GOKZ_GetOption(client, TIPS_OPTION_NAME) == Tips_Disabled) + { + GOKZ_SetOption(client, TIPS_OPTION_NAME, Tips_Enabled); + } + else + { + GOKZ_SetOption(client, TIPS_OPTION_NAME, Tips_Disabled); + } + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-tpanglefix.sp b/sourcemod/scripting/gokz-tpanglefix.sp new file mode 100644 index 0000000..6297795 --- /dev/null +++ b/sourcemod/scripting/gokz-tpanglefix.sp @@ -0,0 +1,277 @@ +#include <sourcemod> + +#include <dhooks> + +#include <gokz/core> +#include <gokz/tpanglefix> + +#include <autoexecconfig> + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include <updater> + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Teleport Angle Fix", + author = "zer0.k", + description = "Fix teleporting not modifying player's view angles due to packet loss", + version = GOKZ_VERSION, + url = "https://github.com/KZGlobalTeam/gokz" +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-tpanglefix.txt" + +TopMenu gTM_Options; +TopMenuObject gTMO_CatGeneral; +TopMenuObject gTMO_ItemTPAngleFix; +Address gA_ViewAnglePatchAddress; +bool gB_EnableFix[MAXPLAYERS + 1]; +DynamicDetour gH_WriteViewAngleUpdate; +int gI_ClientOffset; + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("gokz-tpanglefix"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-tpanglefix.phrases"); + + SetupPatch(); + HookEvents(); + RegisterCommands(); +} + +void SetupPatch() +{ + GameData gamedataConf = LoadGameConfigFile("gokz-tpanglefix.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-tpanglefix gamedata"); + } + // Get the patching address + Address addr = GameConfGetAddress(gamedataConf, "WriteViewAngleUpdate"); + if(addr == Address_Null) + { + SetFailState("Can't find WriteViewAngleUpdate address."); + } + + // Get the offset from the start of the signature to the start of our patch area. + int offset = GameConfGetOffset(gamedataConf, "WriteViewAngleUpdateReliableOffset"); + if(offset == -1) + { + SetFailState("Can't find WriteViewAngleUpdateReliableOffset in gamedata."); + } + gA_ViewAnglePatchAddress = view_as<Address>(addr + view_as<Address>(offset)); +} + +void HookEvents() +{ + GameData gamedataConf = LoadGameConfigFile("gokz-tpanglefix.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-tpanglefix gamedata"); + } + gH_WriteViewAngleUpdate = DynamicDetour.FromConf(gamedataConf, "CGameClient::WriteViewAngleUpdate"); + + if (gH_WriteViewAngleUpdate == INVALID_HANDLE) + { + SetFailState("Failed to find CGameClient::WriteViewAngleUpdate function signature"); + } + + if (!gH_WriteViewAngleUpdate.Enable(Hook_Pre, DHooks_OnWriteViewAngleUpdate_Pre)) + { + SetFailState("Failed to enable detour on CGameClient::WriteViewAngleUpdate"); + } + // Prevent the server from crashing. + FindConVar("sv_parallel_sendsnapshot").SetBool(false); + FindConVar("sv_parallel_sendsnapshot").AddChangeHook(OnParallelSendSnapshotCvarChanged); + + gI_ClientOffset = gamedataConf.GetOffset("ClientIndexOffset"); + if (gI_ClientOffset == -1) + { + SetFailState("Failed to get ClientIndexOffset offset."); + } +} + +void OnParallelSendSnapshotCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (convar.BoolValue) + { + convar.BoolValue = false; + } +} + +public MRESReturn DHooks_OnWriteViewAngleUpdate_Pre(Address pThis) +{ + int client = LoadFromAddress(pThis + view_as<Address>(gI_ClientOffset), NumberType_Int32); + if (gB_EnableFix[client]) + { + PatchAngleFix(); + } + else + { + RestoreAngleFix(); + } + return MRES_Ignored; +} + +void PatchAngleFix() +{ + if (LoadFromAddress(gA_ViewAnglePatchAddress, NumberType_Int8) == 0) + { + StoreToAddress(gA_ViewAnglePatchAddress, 1, NumberType_Int8); + } +} + +void RestoreAngleFix() +{ + if (LoadFromAddress(gA_ViewAnglePatchAddress, NumberType_Int8) == 1) + { + StoreToAddress(gA_ViewAnglePatchAddress, 0, NumberType_Int8); + } +} + +bool ToggleAngleFix(int client) +{ + gB_EnableFix[client] = !gB_EnableFix[client]; + return gB_EnableFix[client]; +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + TopMenu topMenu; + if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null)) + { + GOKZ_OnOptionsMenuReady(topMenu); + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + +} + +// =====[ CLIENT EVENTS ]===== + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + OnOptionChanged_Options(client, option, newValue); +} + +// =====[ OTHER EVENTS ]===== + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_Options(); + OnOptionsMenuReady_OptionsMenu(topMenu); +} + + +// =====[ OPTIONS ]===== + +void OnOptionsMenuReady_Options() +{ + RegisterOption(); +} + +void RegisterOption() +{ + GOKZ_RegisterOption(TPANGLEFIX_OPTION_NAME, TPANGLEFIX_OPTION_DESCRIPTION, + OptionType_Int, TPAngleFix_Disabled, 0, TPANGLEFIX_COUNT - 1); +} + +void OnOptionChanged_Options(int client, const char[] option, any newValue) +{ + if (StrEqual(option, TPANGLEFIX_OPTION_NAME)) + { + gB_EnableFix[client] = newValue; + switch (newValue) + { + case TPAngleFix_Disabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - TP Angle Fix - Disable"); + } + case TPAngleFix_Enabled: + { + GOKZ_PrintToChat(client, true, "%t", "Option - TP Angle Fix - Enable"); + } + } + } +} + +// =====[ OPTIONS MENU ]===== + +void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu) +{ + if (gTM_Options == topMenu) + { + return; + } + + gTM_Options = topMenu; + gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY); + gTMO_ItemTPAngleFix = gTM_Options.AddItem(TPANGLEFIX_OPTION_NAME, TopMenuHandler_TPAngleFix, gTMO_CatGeneral); +} + +public void TopMenuHandler_TPAngleFix(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) +{ + if (topobj_id != gTMO_ItemTPAngleFix) + { + return; + } + + if (action == TopMenuAction_DisplayOption) + { + if (GOKZ_GetOption(param, TPANGLEFIX_OPTION_NAME) == TPAngleFix_Disabled) + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - TP Angle Fix", param, + "Options Menu - Disabled", param); + } + else + { + FormatEx(buffer, maxlength, "%T - %T", + "Options Menu - TP Angle Fix", param, + "Options Menu - Enabled", param); + } + } + else if (action == TopMenuAction_SelectOption) + { + GOKZ_CycleOption(param, TPANGLEFIX_OPTION_NAME); + gTM_Options.Display(param, TopMenuPosition_LastCategory); + } +} + +// =====[ COMMANDS ]===== + +void RegisterCommands() +{ + RegConsoleCmd("sm_tpafix", CommandTPAFix, "[KZ] Toggle teleport angle fix."); +} + +public Action CommandTPAFix(int client, int args) +{ + GOKZ_SetOption(client, TPANGLEFIX_OPTION_NAME, ToggleAngleFix(client)); + return Plugin_Handled; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/GlobalAPI.inc b/sourcemod/scripting/include/GlobalAPI.inc new file mode 100644 index 0000000..8813645 --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI.inc @@ -0,0 +1,822 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_included_ +#endinput +#endif +#define _GlobalAPI_included_ + +// ======================= DEFINITIONS ======================= // + +#define DEFAULT_DATA 0 +#define DEFAULT_INT -1 +#define DEFAULT_STRING "" +#define DEFAULT_FLOAT -1.0 +#define DEFAULT_BOOL view_as<bool>(-1) + +#define GlobalAPI_Plugin_Version "2.0.0" +#define GlobalAPI_Plugin_Desc "Plugin helper for GlobalAPI Production & Staging" +#define GlobalAPI_Plugin_Url "https://bitbucket.org/kztimerglobalteam/GlobalAPI-SMPlugin" +#define GlobalAPI_Plugin_NameVersion "GlobalAPI Plugin " ... GlobalAPI_Plugin_Version + +#define GlobalAPI_Backend_Version "v2.0" +#define GlobalAPI_Backend_Staging_Version "v2.0" +#define GlobalAPI_BaseUrl "https://kztimerglobal.com/api/" ... GlobalAPI_Backend_Version +#define GlobalAPI_Staging_BaseUrl "https://globalapi.ruto.sh/api/" ... GlobalAPI_Backend_Staging_Version + +#define GlobalAPI_Max_BaseUrl_Length 128 +#define GlobalAPI_Max_QueryParam_Num 20 +#define GlobalAPI_Max_QueryParam_Length 64 +#define GlobalAPI_Max_QueryParams_Length (GlobalAPI_Max_QueryParam_Num * GlobalAPI_Max_QueryParam_Length) +#define GlobalAPI_Max_QueryUrl_Length (GlobalAPI_Max_QueryParams_Length + GlobalAPI_Max_BaseUrl_Length) +#define GlobalAPI_Max_QueryParam_Array_Length 64 + +#define GlobalAPI_Max_APIKey_Length 128 +#define GlobalAPI_Max_PluginName_Length 64 +#define GlobalAPI_Max_PluginVersion_Length 32 +#define GlobalAPI_Data_File_Extension "GAPI" + +// ======================= INCLUDES ========================== // + +#include <GlobalAPI/requestdata> +#include <GlobalAPI/responses> +#include <GlobalAPI/stocks> + +// ======================= ENUMS ============================= // + +/** + * Defines what request method is used on requests + */ +enum +{ + GlobalAPIRequestType_GET = 0, /**< Request uses GET HTTP method */ + GlobalAPIRequestType_POST /**< Request uses POST HTTP method */ +}; + +/** + * Defines what accept type is used on requests + */ +enum +{ + GlobalAPIRequestAcceptType_JSON = 0, /**< Request uses application/json HTTP accept type */ + GlobalAPIRequestAcceptType_OctetStream /**< Request uses application/octet-stream HTTP accept type */ +}; + +/** + * Defines what content type is used on requests + */ +enum +{ + GlobalAPIRequestContentType_JSON = 0, /**< Request uses application/json HTTP content type */ + GlobalAPIRequestContentType_OctetStream /**< Request uses application/octet-stream HTTP content type */ +}; + +// ======================= TYPEDEFS ========================== // + +/* + Function types when API call finishes +*/ +typeset OnAPICallFinished +{ + /** + * Called when an API call has finished + * + * @param hResponse JSON_Object handle to the response + * @param hData GlobalAPIRequestData handle for the request + * @noreturn + */ + function void(JSON_Object hResponse, GlobalAPIRequestData hData); + + /** + * Called when an API call has finished + * + * @param hResponse JSON_Object handle to the response + * @param hData GlobalAPIRequestData handle for the request + * @param data Optional data that was passed + * @noreturn + */ + function void(JSON_Object hResponse, GlobalAPIRequestData hData, any data); +}; + +// ======================= FORWARDS ========================== // + +/** + * Called when GlobalAPI plugin is initialized, + * this means API Key is loaded and all the cvars are loaded + * + * @noreturn + */ +forward void GlobalAPI_OnInitialized(); + +/** + * Called when GlobalAPI plugin has failed a request + * + * @param request Handle to the request failed + * @param hData Handle to request's GlobalAPIRequestData + * @noreturn + */ +forward void GlobalAPI_OnRequestFailed(Handle request, GlobalAPIRequestData hData); + +/** + * Called when GlobalAPI plugin has started a request + * + * @param request Handle to the request started + * @param hData Handle to request's GlobalAPIRequestData + * @noreturn + */ +forward void GlobalAPI_OnRequestStarted(Handle request, GlobalAPIRequestData hData); + +/** + * Called when GlobalAPI plugin has finished a request + * + * @param request Handle to the request finished + * @param hData Handle to request's GlobalAPIRequestData + * @noreturn + */ +forward void GlobalAPI_OnRequestFinished(Handle request, GlobalAPIRequestData hData); + +// ======================= NATIVES =========================== // + +/** + * Gets a boolean of whether GlobalAPI plugin is initialized. + * + * @note See GlobalAPI_OnInitialized for the event version. + * @return Whether GlobalAPI plugin is initialized. + */ +native bool GlobalAPI_IsInit(); + +/** + * Gets the API Key used by GlobalAPI plugin + * + * @param buffer Buffer to store result in + * @param maxlength Max length of the buffer + * @noreturn + */ +native void GlobalAPI_GetAPIKey(char[] buffer, int maxlength); + +/** + * Gets whether GlobalAPI is using an API Key + * + * @note This does not mean the API Key is valid! + * @return Whether API Key is used by GlobalAPI plugin + */ +native bool GlobalAPI_HasAPIKey(); + +/** + * Gets whether GlobalAPI is using the staging endpoint + * + * @note It is not safe to call this before GlobalAPI_OnInitialized! + * @return Whether staging endpoint is used by GlobalAPI plugin + */ +native bool GlobalAPI_IsStaging(); + +/** + * Gets whether GlobalAPI is in debug mode + * + * @note It is not safe to call this before GlobalAPI_OnInitialized! + * @return Whether GlobalAPI plugin is in debug mode + */ +native bool GlobalAPI_IsDebugging(); + +/** + * Sends a request in GlobalAPI plugin format + * + * @param hData Handle to GlobalAPIRequestData + * @return Whether the request was sent successfully + */ +native bool GlobalAPI_SendRequest(GlobalAPIRequestData hData); + +/** + * Sends a debug message to GlobalAPI plugin logs if debugging is enabled + * + * @param message Formatting rules + * @param ... Variable number of format parameters + * @note This is not safe to use before convars have loaded + * @return Whether the message was logged + */ +native bool GlobalAPI_DebugMessage(const char[] message, any ...); + +/** + * Starts a GET HTTP Request to /api/{version}/auth/status + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetAuthStatus(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA); + +/** + * Starts a GET HTTP Request to /api/{version}/bans + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param banTypes Ban types to query + * @param banTypesList -Unsupported at the moment- + * @param isExpired Whether to query for isExpired or not + * @param ipAddress IP address to query + * @param steamId64 SteamID64 to query + * @param steamId SteamID2 to query + * @param notesContain Notes to query + * @param statsContain Stats to query + * @param serverId Server ID to query + * @param createdSince Created since date to query + * @param updatedSince Updated since date to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetBans(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] banTypes = DEFAULT_STRING, + const char[] banTypesList = DEFAULT_STRING, bool isExpired = DEFAULT_BOOL, const char[] ipAddress = DEFAULT_STRING, + const char[] steamId64 = DEFAULT_STRING, const char[] steamId = DEFAULT_STRING, const char[] notesContain = DEFAULT_STRING, + const char[] statsContain = DEFAULT_STRING, int serverId = DEFAULT_INT, const char[] createdSince = DEFAULT_STRING, + const char[] updatedSince = DEFAULT_STRING, int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a POST HTTP Request to /api/{version}/bans + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 of the user + * @param banType Type of the ban + * @param stats Stats of the ban + * @param notes Notes of the ban + * @param ipAddress IP address of the user + * @return Whether request was successfully sent + */ +native bool GlobalAPI_CreateBan(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + const char[] steamId, const char[] banType, const char[] stats, + const char[] notes, const char[] ipAddress); + +/** + * Starts a GET HTTP Request to /api/{version}/jumpstats + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Id to query + * @param serverId Server id to query + * @param steamId64 SteamID64 to query + * @param steamId SteamID2 to query + * @param jumpType Jump type to query + * @param steamId64List -Unsupported at the moment- + * @param jumpTypeList -Unsupported at the moment- + * @param greaterThanDistance Greater than distance to query + * @param lessThanDistance Less than distance to query + * @param isMsl Whether to query for isMsl or not + * @param isCrouchBind Whether to query for isCrouchBind or not + * @param isForwardBind Whether to query for isForwardBind or not + * @param isCrouchBoost Whether to query for isCrouchBoost or not + * @param updatedById Updated by id to query + * @param createdSince Created since date to query + * @param updatedSince Updated since date to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetJumpstats(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id = DEFAULT_INT, + int serverId = DEFAULT_INT, const char[] steamId64 = DEFAULT_STRING, const char[] steamId = DEFAULT_STRING, + const char[] jumpType = DEFAULT_STRING, const char[] steamId64List = DEFAULT_STRING, + const char[] jumpTypeList = DEFAULT_STRING, float greaterThanDistance = DEFAULT_FLOAT, + float lessThanDistance = DEFAULT_FLOAT, bool isMsl = DEFAULT_BOOL, bool isCrouchBind = DEFAULT_BOOL, + bool isForwardBind = DEFAULT_BOOL, bool isCrouchBoost = DEFAULT_BOOL, int updatedById = DEFAULT_INT, + const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a POST HTTP Request to /api/{version}/jumpstats + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 of the user + * @param jumpType Type of the jump + * @param distance Distance of the jump + * @param jumpJsonInfo Data of the jump + * @param tickRate Tickrate of the server + * @param mslCount Msl count of the jump + * @param isCrouchBind Whether crouch bind was used + * @param isForwardBind Whether forward bind was used + * @param isCrouchBoost Whether crouch boost was used + * @param strafeCount Strafe count of the jump + * @return Whether request was successfully sent + */ +native bool GlobalAPI_CreateJumpstat(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId, + int jumpType, float distance, const char[] jumpJsonInfo, int tickRate, int mslCount, + bool isCrouchBind, bool isForwardBind, bool isCrouchBoost, int strafeCount); + +/** + * Starts a GET HTTP Request to /api/{version}/jumpstats/{jump_type}/top + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param jumpType Jump type to query + * @param id Id to query + * @param serverId Server Id to query + * @param steamId64 SteamID64 to query + * @param steamId SteamID2 to query + * @param steamId64List -Unsupported at the moment- + * @param jumpTypeList -Unsupported at the moment- + * @param greaterThanDistance Greater than distance to query + * @param lessThanDistance Less than distance to query + * @param isMsl Whether to query for isMsl or not + * @param isCrouchBind Whether to query for isCrouchBind or not + * @param isForwardBind Whether to query for isForwardBind or not + * @param isCrouchBoost Whether to query for isCrouchBoost or not + * @param updatedById Updated by id to query + * @param createdSince Created since date to query + * @param updatedSince Updated since date to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetJumpstatTop(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] jumpType, + int id = DEFAULT_INT, int serverId = DEFAULT_INT, const char[] steamId64 = DEFAULT_STRING, + const char[] steamId = DEFAULT_STRING, const char[] steamId64List = DEFAULT_STRING, + const char[] jumpTypeList = DEFAULT_STRING, float greaterThanDistance = DEFAULT_FLOAT, + float lessThanDistance = DEFAULT_FLOAT, bool isMsl = DEFAULT_BOOL, bool isCrouchBind = DEFAULT_BOOL, + bool isForwardBind = DEFAULT_BOOL, bool isCrouchBoost = DEFAULT_BOOL, int updatedById = DEFAULT_INT, + const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/jumpstats/{jump_type}/top30 + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param jumpType Jump type to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetJumpstatTop30(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] jumpType); + +/** + * Starts a GET HTTP Request to /api/{version}/maps + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param name Map name to query + * @param largerThanFilesize Larger than filesize to query + * @param smallerThanFilesize Smaller than filesize to query + * @param isValidated Whether to query for isValidated or not + * @param difficulty Map difficulty to query + * @param createdSince Created since date to query + * @param updatedSince Updated since date to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetMaps(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name = DEFAULT_STRING, + int largerThanFilesize = DEFAULT_INT, int smallerThanFilesize = DEFAULT_INT, bool isValidated = DEFAULT_BOOL, + int difficulty = DEFAULT_INT, const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/maps/{id} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Map id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetMapById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id); + +/** + * Starts a GET HTTP Request to /api/{version}/maps/name/{map_name} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param name Map name to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetMapByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name); + +/** + * Starts a GET HTTP Request to /api/{version}/modes + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetModes(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA); + +/** + * Starts a GET HTTP Request to /api/{version}/modes/id/{id} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Mode id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetModeById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id); + +/** + * Starts a GET HTTP Request to /api/{version}/modes/name/{mode_name} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param name Mode name to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetModeByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name); + +/** + * Starts a GET HTTP Request to /api/{version}/players + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 to query + * @param isBanned Whether to query for isBanned or not + * @param totalRecords Total records to query + * @param ipAddress IP address to query + * @param steamId64List -Unsupported at the moment- + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetPlayers(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId = DEFAULT_STRING, + bool isBanned = DEFAULT_BOOL, int totalRecords = DEFAULT_INT, const char[] ipAddress = DEFAULT_STRING, + const char[] steamId64List = DEFAULT_STRING); + +/** + * Starts a GET HTTP Request to /api/{version}/players/steamid/{steamid} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetPlayerBySteamId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId); + +/** + * Starts a GET HTTP Request to /api/{version}/players/steamid/{steamid}/ip/{ip} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 to query + * @param ipAddress IP address to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetPlayerBySteamIdAndIp(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + const char[] steamId, const char[] ipAddress); + +/** + * Starts a POST HTTP Request to /api/{version}/records + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 of the user + * @param mapId Map id of the record + * @param mode Mode of the record + * @param stage Stage of the record + * @param tickRate Tickrate of the server + * @param teleports Teleport count of the record + * @param time Elapsed time of the record + * @return Whether request was successfully sent + */ +native bool GlobalAPI_CreateRecord(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId, + int mapId, const char[] mode, int stage, int tickRate, int teleports, float time); + +/** + * Starts a GET HTTP Request to /api/{version}/records/place/{id} + * + * @note This is deprecated! + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordPlaceById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id); + +/** + * Starts a GET HTTP Request to /api/{version}/records/top + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 to query + * @param steamId64 SteamID64 to query + * @param mapId Map id to query + * @param mapName Map name to query + * @param tickRate Tickrate to query + * @param stage Stage to query + * @param modes Mode(s) to query + * @param hasTeleports Whether to query for hasTeleports or not + * @param playerName Player name to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordsTop(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + const char[] steamId = DEFAULT_STRING, const char[] steamId64 = DEFAULT_STRING, int mapId = DEFAULT_INT, + const char[] mapName = DEFAULT_STRING, int tickRate = DEFAULT_INT, int stage = DEFAULT_INT, + const char[] modes = DEFAULT_STRING, bool hasTeleports = DEFAULT_BOOL, + const char[] playerName = DEFAULT_STRING, int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/records/top/recent + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param steamId SteamID2 to query + * @param steamId64 SteamID64 to query + * @param mapId Map id to query + * @param mapName Map name to query + * @param tickRate Tickrate to query + * @param stage Stage to query + * @param modes Mode(s) to query + * @param topAtLeast Place top at least to query + * @param topOverallAtLeast Place top overall at least to query + * @param hasTeleports Whether to query for hasTeleports or not + * @param createdSince Created since date to query + * @param playerName Player name to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordsTopRecent(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + const char[] steamId = DEFAULT_STRING, const char[] steamId64 = DEFAULT_STRING, + int mapId = DEFAULT_INT, const char[] mapName = DEFAULT_STRING, + int tickRate = DEFAULT_INT, int stage = DEFAULT_INT, + const char[] modes = DEFAULT_STRING, int topAtLeast = DEFAULT_INT, + int topOverallAtLeast = DEFAULT_INT, bool hasTeleports = DEFAULT_BOOL, + const char[] createdSince = DEFAULT_STRING, const char[] playerName = DEFAULT_STRING, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/records/top/world_records + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param ids Array of ids to query + * @param idsLength Length of the ids array + * @param mapIds Array of map ids to query + * @param mapIdsLength Length of the map ids array + * @param stages Array of stages to query + * @param stagesLength Length of the stages array + * @param modeIds Array of mode ids to query + * @param modeIdsLength Length of the mode ids array + * @param tickRates Array of tickrates to query + * @param tickRatesLength Length of the tickrates array + * @param hasTeleports Whether to query for hasTeleports or not + * @param mapTag Map tags to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordsTopWorldRecords(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int[] ids = {}, int idsLength = DEFAULT_INT, + int[] mapIds = {}, int mapIdsLength = DEFAULT_INT, + int[] stages = {}, int stagesLength = DEFAULT_INT, + int[] modeIds = {}, int modeIdsLength = DEFAULT_INT, + int[] tickRates = {}, int tickRatesLength = DEFAULT_INT, + bool hasTeleports = DEFAULT_BOOL, char[] mapTag = DEFAULT_STRING, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/servers + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Id to query + * @param port Port to query + * @param ip IP address to query + * @param name Server name to query + * @param ownerSteamId64 Owner's steamid64 to query + * @param approvalStatus Approval status to query + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetServers(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int id = DEFAULT_INT, int port = DEFAULT_INT, const char[] ip = DEFAULT_STRING, + const char[] name = DEFAULT_STRING, const char[] ownerSteamId64 = DEFAULT_STRING, + int approvalStatus = DEFAULT_INT, int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/servers/{id} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param id Id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetServerById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id); + +/** + * Starts a GET HTTP Request to /api/{version}/servers/name/{server_name} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param serverName Server name to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetServersByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] serverName); + +/** + * Starts a GET HTTP Request to /api/{version}/player_ranks + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param pointsGreaterThan Points greater than to query + * @param averageGreaterThan Average greater than to query + * @param ratingGreaterThan Rating greater than to query + * @param finishesGreaterThan Finishes greater than to query + * @param steamId64List Comma-separated stirng of steamid64s to query + * @param recordFilterIds Array of record filter ids to query + * @param recordFilterIdsLength Length of the record filter ids array + * @param mapIds Array of map ids to query + * @param mapIdsLength Length of the map ids array + * @param stages Array of stages to query + * @param stagesLength Length of the stages array + * @param modeIds Array of mode ids to query + * @param modeIdsLength Length of the mode ids array + * @param tickRates Array of tickrates to query + * @param tickRatesLength Length of the tickrates array + * @param hasTeleports Whether to query for hasTeleports or not + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetPlayerRanks(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int pointsGreaterThan = DEFAULT_INT, float averageGreaterThan = DEFAULT_FLOAT, + float ratingGreaterThan = DEFAULT_FLOAT, int finishesGreaterThan = DEFAULT_INT, + const char[] steamId64List = DEFAULT_STRING, + int[] recordFilterIds = {}, int recordFilterIdsLength = DEFAULT_INT, + int[] mapIds = {}, int mapIdsLength = DEFAULT_INT, + int[] stages = {}, int stagesLength = DEFAULT_INT, + int[] modeIds = {}, int modeIdsLength = DEFAULT_INT, + int[] tickRates = {}, int tickRatesLength = DEFAULT_INT, + bool hasTeleports = DEFAULT_BOOL, int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/record_filters + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param ids Array of ids to query + * @param idsLength Length of the ids array + * @param mapIds Array of map ids to query + * @param mapIdsLength Length of the map ids array + * @param stages Array of stages to query + * @param stagesLength Length of the stages array + * @param modeIds Array of mode ids to query + * @param modeIdsLength Length of the mode ids array + * @param tickRates Array of tickrates to query + * @param tickRatesLength Length of the tickrates array + * @param hasTeleports Whether to query for hasTeleports or not + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordFilters(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int[] ids = {}, int idsLength = DEFAULT_INT, + int[] mapIds = {}, int mapIdsLength = DEFAULT_INT, + int[] stages = {}, int stagesLength = DEFAULT_INT, + int[] modeIds = {}, int modeIdsLength = DEFAULT_INT, + int[] tickRates = {}, int tickRatesLength = DEFAULT_INT, + bool hasTeleports = DEFAULT_BOOL, bool isOverall = DEFAULT_BOOL, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/record_filters/distributions + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param ids Array of ids to query + * @param idsLength Length of the ids array + * @param mapIds Array of map ids to query + * @param mapIdsLength Length of the map ids array + * @param stages Array of stages to query + * @param stagesLength Length of the stages array + * @param modeIds Array of mode ids to query + * @param modeIdsLength Length of the mode ids array + * @param tickRates Array of tickrates to query + * @param tickRatesLength Length of the tickrates array + * @param hasTeleports Whether to query for hasTeleports or not + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetRecordFilterDistributions(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int[] ids = {}, int idsLength = DEFAULT_INT, + int[] mapIds = {}, int mapIdsLength = DEFAULT_INT, + int[] stages = {}, int stagesLength = DEFAULT_INT, + int[] modeIds = {}, int modeIdsLength = DEFAULT_INT, + int[] tickRates = {}, int tickRatesLength = DEFAULT_INT, + bool hasTeleports = DEFAULT_BOOL, bool isOverall = DEFAULT_BOOL, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/records/replay/list + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param offset Offset of the dataset to query + * @param limit Amount of items returned for the query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetReplayList(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int offset = DEFAULT_INT, int limit = DEFAULT_INT); + +/** + * Starts a GET HTTP Request to /api/{version}/records/{recordId}/replay + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param recordId Record id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetReplayByRecordId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int recordId); +/** + * Starts a GET HTTP Request to /api/{version}/records/replay/{replayId} + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param replayId Replay id to query + * @return Whether request was successfully sent + */ +native bool GlobalAPI_GetReplayByReplayId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int replayId); + +/** + * Starts a POST HTTP Request to /api/{version}/records/{recordId}/replay + * + * @param callback Callback when request has finished + * @param data Optional data to pass + * @param recordId Id of the record + * @param replayFile Path to the replay file + * @return Whether request was successfully sent + */ +native bool GlobalAPI_CreateReplayForRecordId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, + int recordId, const char[] replayFile); + +// ======================= PLUGIN INFO ======================= // + +public SharedPlugin __pl_GlobalAPI = +{ + name = "GlobalAPI", + file = "GlobalAPI.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_GlobalAPI_SetNTVOptional() +{ + // Plugin + MarkNativeAsOptional("GlobalAPI_IsInit"); + MarkNativeAsOptional("GlobalAPI_GetAPIKey"); + MarkNativeAsOptional("GlobalAPI_HasAPIKey"); + MarkNativeAsOptional("GlobalAPI_IsStaging"); + MarkNativeAsOptional("GlobalAPI_IsDebugging"); + MarkNativeAsOptional("GlobalAPI_SendRequest"); + MarkNativeAsOptional("GlobalAPI_DebugMessage"); + + // Auth + MarkNativeAsOptional("GlobalAPI_GetAuthStatus"); + + // Bans + MarkNativeAsOptional("GlobalAPI_GetBans"); + MarkNativeAsOptional("GlobalAPI_CreateBan"); + + // Jumpstats + MarkNativeAsOptional("GlobalAPI_GetJumpstats"); + MarkNativeAsOptional("GlobalAPI_GetJumpstatTop"); + MarkNativeAsOptional("GlobalAPI_GetJumpstatTop30"); + + // Maps + MarkNativeAsOptional("GlobalAPI_GetMaps"); + MarkNativeAsOptional("GlobalAPI_GetMapById"); + MarkNativeAsOptional("GlobalAPI_GetMapByName"); + + // Modes + MarkNativeAsOptional("GlobalAPI_GetModes"); + MarkNativeAsOptional("GlobalAPI_GetModeById"); + MarkNativeAsOptional("GlobalAPI_GetModeByName"); + + // Players + MarkNativeAsOptional("GlobalAPI_GetPlayers"); + MarkNativeAsOptional("GlobalAPI_GetPlayerBySteamId"); + MarkNativeAsOptional("GlobalAPI_GetPlayerBySteamIdAndIp"); + + // Records + MarkNativeAsOptional("GlobalAPI_CreateRecord"); + MarkNativeAsOptional("GlobalAPI_GetRecordPlaceById"); + MarkNativeAsOptional("GlobalAPI_GetRecordsTop"); + MarkNativeAsOptional("GlobalAPI_GetRecordsTopRecent"); + MarkNativeAsOptional("GlobalAPI_GetRecordsTopWorldRecords"); + + // Servers + MarkNativeAsOptional("GlobalAPI_GetServers"); + MarkNativeAsOptional("GlobalAPI_GetServerById"); + MarkNativeAsOptional("GlobalAPI_GetServersByName"); + + // Ranks + MarkNativeAsOptional("GlobalAPI_GetPlayerRanks"); + + // Record Filters + MarkNativeAsOptional("GlobalAPI_GetRecordFilters"); + MarkNativeAsOptional("GlobalAPI_GetRecordFilterDistributions"); + + // Replays + MarkNativeAsOptional("GlobalAPI_GetReplayList"); + MarkNativeAsOptional("GlobalAPI_GetReplayByRecordId"); + MarkNativeAsOptional("GlobalAPI_GetReplayByReplayId"); + MarkNativeAsOptional("GlobalAPI_CreateReplayForRecordId"); +} +#endif diff --git a/sourcemod/scripting/include/GlobalAPI/iterable.inc b/sourcemod/scripting/include/GlobalAPI/iterable.inc new file mode 100644 index 0000000..585873b --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI/iterable.inc @@ -0,0 +1,55 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_Iterable_included_ +#endinput +#endif +#define _GlobalAPI_Iterable_included_ + +// =========================================================== // + +#include <json> + +// =========================================================== // + +/* + Helper methodmap for JSON_Object arrays +*/ +methodmap APIIterable < JSON_Object +{ + /** + * Creates a new APIIterable + * + * @param hItems JSON_Object array handle + * @return A new APIIterable handle + */ + public APIIterable(JSON_Object hItems) + { + if (hItems.HasKey("result")) + { + return view_as<APIIterable>(hItems.GetObject("result")); + } + return view_as<APIIterable>(hItems); + } + + /* + Gets count of the items in the array + */ + property int Count + { + public get() { return this.Length; } + } + + /** + * Gets an object from the array by index + * + * @note This is an alias to GetObjectIndexed + * @param index Index of the object we want to retrieve + * @return JSON_Object handle to the object retrieved + */ + public JSON_Object GetById(int index) + { + return this.GetObjectIndexed(index); + } +} + +// =========================================================== //
\ No newline at end of file diff --git a/sourcemod/scripting/include/GlobalAPI/request.inc b/sourcemod/scripting/include/GlobalAPI/request.inc new file mode 100644 index 0000000..e683125 --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI/request.inc @@ -0,0 +1,185 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_Request_included_ +#endinput +#endif +#define _GlobalAPI_Request_included_ + +// =========================================================== // + +static char gC_acceptTypePhrases[][] = +{ + "application/json", + "application/octet-stream" +}; + +static char gC_contentTypePhrases[][] = +{ + "application/json", + "application/octet-stream" +}; + +// =========================================================== // + +methodmap GlobalAPIRequest < Handle +{ + /** + * Creates a new GlobalAPIRequest + * + * @param url URL of the request + * @param method SteamWorks k_ETTPMethod of the request + * @return A new GlobalAPIRequest handle + */ + public GlobalAPIRequest(char[] url, EHTTPMethod method) + { + Handle request = SteamWorks_CreateHTTPRequest(method, url); + return view_as<GlobalAPIRequest>(request); + } + + /** + * Sets request timeout + * + * @param seconds Timeout in seconds + * @return Whether the operation was successful + */ + public bool SetTimeout(int seconds) + { + return SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(this, seconds * 1000); + } + + /** + * Sets request body + * + * @param hData GlobalAPIRequestData containing contentType + * @param body Request body to set + * @param maxlength Maxlength of the body + * @return Whether the operation was successful + */ + public bool SetBody(GlobalAPIRequestData hData, char[] body, int maxlength) + { + return SteamWorks_SetHTTPRequestRawPostBody(this, gC_contentTypePhrases[hData.ContentType], body, maxlength); + } + + /** + * Sets request body from a file + * + * @param hData GlobalAPIRequestData containing contentType + * @return Whether the operation was successful + */ + public bool SetBodyFromFile(GlobalAPIRequestData hData, char[] file) + { + return SteamWorks_SetHTTPRequestRawPostBodyFromFile(this, gC_contentTypePhrases[hData.ContentType], file); + } + + /** + * Sets a request context value + * + * @param data Any data to pass + * @return Whether the operation was successful + */ + public bool SetData(any data1, any data2 = 0) + { + return SteamWorks_SetHTTPRequestContextValue(this, data1, data2); + } + + /** + * Sets predefined HTTP callbacks + * + * @note Predefined values respectively: + * @note Global_HTTP_Completed, Global_HTTP_Headers and Global_HTTP_DataReceived + * @noreturn + */ + public void SetCallbacks() + { + SteamWorks_SetHTTPCallbacks(this, Global_HTTP_Completed, Global_HTTP_Headers, Global_HTTP_DataReceived); + } + + /** + * Sets "Accept" header + * + * @param hData GlobalAPIRequestData containing acceptType + * @return Whether the operation was successful + */ + public bool SetAcceptHeaders(GlobalAPIRequestData hData) + { + return SteamWorks_SetHTTPRequestHeaderValue(this, "Accept", gC_acceptTypePhrases[hData.AcceptType]); + } + + /** + * Sets "powered by" header + * + * @return Whether the operation was successful + */ + public bool SetPoweredByHeader() + { + return SteamWorks_SetHTTPRequestHeaderValue(this, "X-Powered-By", GlobalAPI_Plugin_NameVersion); + } + + /** + * Sets authentication header + * + * @return Whether the operation was successful + */ + public bool SetAuthenticationHeader(char[] apiKey) + { + return SteamWorks_SetHTTPRequestHeaderValue(this, "X-ApiKey", apiKey); + } + + /** + * Sets envinroment headers (MetaMod & SourceMod) + * + * @param mmVersion MetaMod version string + * @param smVersion SourceMod version string + * @return Whether the operation was successful + */ + public bool SetEnvironmentHeaders(char[] mmVersion, char[] smVersion) + { + return SteamWorks_SetHTTPRequestHeaderValue(this, "X-MetaMod-Version", mmVersion) + && SteamWorks_SetHTTPRequestHeaderValue(this, "X-SourceMod-Version", smVersion); + } + + /** + * Sets content type header + * + * @param hData GlobalAPIRequestData containing contentType + * @return Whether the operation was successful + */ + public bool SetContentTypeHeader(GlobalAPIRequestData hData) + { + return SteamWorks_SetHTTPRequestHeaderValue(this, "Content-Type", gC_contentTypePhrases[hData.ContentType]); + } + + /** + * Sets request origin header + * + * @param hData GlobalAPIRequestData containing pluginName + * @return Whether the operation was successful + */ + public bool SetRequestOriginHeader(GlobalAPIRequestData hData) + { + char pluginName[GlobalAPI_Max_PluginName_Length]; + hData.GetString("pluginName", pluginName, sizeof(pluginName)); + + char pluginVersion[GlobalAPI_Max_PluginVersion_Length + 2]; + hData.GetString("pluginVersion", pluginVersion, sizeof(pluginVersion)); + + char fullPluginDisplay[sizeof(pluginName) + sizeof(pluginVersion) + 6]; + Format(fullPluginDisplay, sizeof(fullPluginDisplay), "%s (V.%s)", pluginName, pluginVersion); + + return SteamWorks_SetHTTPRequestHeaderValue(this, "X-Request-Origin", fullPluginDisplay); + } + + /** + * Sends our request with all available data + * + * @param hData GlobalAPIRequestData handle with all required keys + * @return Whether the operation was successful + */ + public bool Send(GlobalAPIRequestData hData) + { + Call_Private_OnHTTPStart(this, hData); + return SteamWorks_SendHTTPRequest(this); + } +} + +// =========================================================== //
\ No newline at end of file diff --git a/sourcemod/scripting/include/GlobalAPI/requestdata.inc b/sourcemod/scripting/include/GlobalAPI/requestdata.inc new file mode 100644 index 0000000..f055ee8 --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI/requestdata.inc @@ -0,0 +1,534 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_RequestData_included_ +#endinput +#endif +#define _GlobalAPI_RequestData_included_ + +// =========================================================== // + +#include <json> + +// =========================================================== // + +/* + Helper methodmap for wrapping data related to requests +*/ +methodmap GlobalAPIRequestData < JSON_Object +{ + /** + * Creates a new GlobalAPIRequestData + * + * @note You can pass a plugin handle or name and/or version + * @note Plugin handle is always preferred + * @param plugin Handle to calling plugin + * @param pluginName Name of the calling plugin + * @param pluginVersion Version of the calling plugin + * @return A new GlobalAPIRequestData handle + */ + public GlobalAPIRequestData(Handle plugin = null, char[] pluginName = "Unknown", char[] pluginVersion = "Unknown") + { + JSON_Object requestData = new JSON_Object(); + + if (plugin == null) + { + requestData.SetString("pluginName", pluginName); + requestData.SetString("pluginVersion", pluginVersion); + } + else + { + requestData.SetString("pluginName", GetPluginDisplayName(plugin)); + requestData.SetString("pluginVersion", GetPluginVersion(plugin)); + } + + requestData.SetKeyHidden("pluginName", true); + requestData.SetKeyHidden("pluginVersion", true); + + requestData.SetInt("acceptType", 0); + requestData.SetKeyHidden("acceptType", true); + + requestData.SetInt("contentType", 0); + requestData.SetKeyHidden("contentType", true); + + return view_as<GlobalAPIRequestData>(requestData); + } + + /** + * Sets a key as default + * + * @note This sets them as "Handle" type + * @note - See GlobalAPI.inc for default values + * @param key Key to set as default + * @noreturn + */ + public void SetDefault(char[] key) + { + this.SetHandle(key); + this.SetKeyHidden(key, true); + } + + /** + * Sets url to the request data + * + * @param url Url to set + * @noreturn + */ + public void AddUrl(char[] url) + { + this.SetString("url", url); + this.SetKeyHidden("url", true); + } + + /** + * Sets endpoint to the request data + * + * @param endpoint Endpoint to set + * @noreturn + */ + public void AddEndpoint(char[] endpoint) + { + this.SetString("endpoint", endpoint); + this.SetKeyHidden("endpoint", true); + } + + /** + * Sets body file path to the request data + * + * @note Path to file with data to be posted + * @param path Body file (path) to set + * @noreturn + */ + public void AddBodyFile(char[] path) + { + this.SetString("bodyFile", path); + this.SetKeyHidden("bodyFile", true); + } + + /** + * Sets data file path to the request data + * + * @note Path for downloaded files + * @param path Data path to set + * @noreturn + */ + public void AddDataPath(char[] path) + { + this.SetString("dataFilePath", path); + this.SetKeyHidden("dataFilePath", true); + } + + /* + Get or set the request's "acceptType" + */ + property int AcceptType + { + public get() + { + return this.GetInt("acceptType"); + } + public set(int type) + { + this.SetInt("acceptType", type); + } + } + + /* + Get or set the request's "contentType" + */ + property int ContentType + { + public get() + { + return this.GetInt("contentType"); + } + public set(int type) + { + this.SetInt("contentType", type); + } + } + + /* + Get or set the request's "keyRequired" + */ + property bool KeyRequired + { + public get() + { + return this.GetBool("keyRequired"); + } + public set(bool required) + { + this.SetBool("keyRequired", required); + this.SetKeyHidden("keyRequired", true); + } + } + + /* + Get or set the request's "isRetried" + */ + property bool IsRetried + { + public get() + { + return this.GetBool("isRetried"); + } + public set(bool retried) + { + this.SetBool("isRetried", retried); + this.SetKeyHidden("isRetried", true); + } + } + + /* + Get or set the request's "bodyLength" + */ + property int BodyLength + { + public get() + { + return this.GetInt("bodyLength"); + } + public set(int length) + { + this.SetInt("bodyLength", length); + this.SetKeyHidden("bodyLength", true); + } + } + + /* + Get or set the request's "status" + */ + property int Status + { + public get() + { + return this.GetInt("status"); + } + public set(int status) + { + this.SetInt("status", status); + this.SetKeyHidden("status", true); + } + } + + /* + Get or set the request's "responseTime" + */ + property int ResponseTime + { + public get() + { + return this.GetInt("responseTime"); + } + public set(int responseTime) + { + this.SetInt("responseTime", responseTime); + this.SetKeyHidden("responseTime", true); + } + } + + /* + Get or set the request's "requestType" + */ + property int RequestType + { + public get() + { + return this.GetInt("requestType"); + } + public set(int type) + { + this.SetInt("requestType", type); + this.SetKeyHidden("requestType", true); + } + } + + /* + Get or set the request's "failure" + */ + property bool Failure + { + public get() + { + return this.GetBool("failure"); + } + public set(bool failure) + { + this.SetBool("failure", failure); + this.SetKeyHidden("failure", true); + } + } + + /* + Get or set the request's "callback" + */ + property Handle Callback + { + public get() + { + return view_as<Handle>(this.GetInt("callback")); + } + public set(Handle hFwd) + { + this.SetHandle("callback", hFwd); + this.SetKeyType("callback", Type_Int); + this.SetKeyHidden("callback", true); + } + } + + /* + Get or set the request's "data" + */ + property any Data + { + public get() + { + return this.GetInt("data"); + } + public set(any data) + { + this.SetInt("data", data); + this.SetKeyHidden("data", true); + } + } + + /** + * Adds a number to the request data + * + * @note Default values are added as "defaults" + * @note See GlobalAPI.inc for the default values + * @param key Key name to set + * @param value Value of the key + * @noreturn + */ + public void AddNum(char[] key, int value) + { + if (value == -1) + { + this.SetDefault(key); + } + else + { + this.SetInt(key, value); + } + } + + /** + * Adds a float to the request data + * + * @note Default values are added as "defaults" + * @note See GlobalAPI.inc for the default values + * @param key Key name to set + * @param value Value of the key + * @noreturn + */ + public void AddFloat(char[] key, float value) + { + if (value == -1.000000) + { + this.SetDefault(key); + } + else + { + this.SetFloat(key, value); + } + } + + /** + * Adds a string to the request data + * + * @note Default values are added as "defaults" + * @note See GlobalAPI.inc for the default values + * @param key Key name to set + * @param value Value of the key + * @noreturn + */ + public void AddString(char[] key, char[] value) + { + if (StrEqual(value, "")) + { + this.SetDefault(key); + } + else + { + this.SetString(key, value); + } + } + + /** + * Adds a boolean to the request data + * + * @note Default values are added as "defaults" + * @note See GlobalAPI.inc for the default values + * @param key Key name to set + * @param value Value of the key + * @noreturn + */ + public void AddBool(char[] key, bool value) + { + if (value != true && value != false) + { + this.SetDefault(key); + } + else + { + this.SetBool(key, value); + } + } + + /** + * Adds integer array to the request data + * + * @note Max length <= 0 are added as defaults + * @param key Key name to set + * @param value Values (array) of the key + * @param maxlength Max length of the values array + * @noreturn + */ + public void AddIntArray(char[] key, int[] value, int maxlength) + { + if (maxlength <= 0) + { + this.SetDefault(key); + } + else + { + JSON_Object hArray = new JSON_Object(true); + + for (int i = 0; i < maxlength; i++) + { + hArray.PushInt(value[i]); + } + + this.SetObject(key, hArray); + } + } + + /** + * Adds string array to the request data + * + * @note Item count <= 0 are added as defaults + * @param key Key name to set + * @param itemCount Amount of strings in the array + * @noreturn + */ + public void AddStringArray(char[] key, char[][] value, int itemCount) + { + if (itemCount <= 0) + { + this.SetDefault(key); + } + else + { + JSON_Object hArray = new JSON_Object(true); + + for (int i = 0; i < itemCount; i++) + { + hArray.PushString(value[i]); + } + + this.SetObject(key, hArray); + } + } + + /** + * Converts all of the request data into a query string representation + * + * @note This ignores "hidden" keys + * @param queryString Buffer to store the result in + * @param maxlength Max length of the buffer + * @noreturn + */ + public void ToString(char[] queryString, int maxlength) + { + StringMapSnapshot paramsMap = this.Snapshot(); + + char key[64]; + char value[1024]; + + int paramCount = 0; + + for (int i = 0; i < paramsMap.Length; i++) + { + paramsMap.GetKey(i, key, sizeof(key)); + if (this.GetKeyHidden(key) || json_is_meta_key(key)) + { + continue; + } + + switch(this.GetKeyType(key)) + { + case Type_String: + { + this.GetString(key, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + case Type_Float: + { + float temp = this.GetFloat(key); + FloatToString(temp, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + case Type_Int: + { + int temp = this.GetInt(key); + IntToString(temp, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + case Type_Bool: + { + bool temp = this.GetBool(key); + BoolToString(temp, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + case Type_Object: + { + JSON_Object hObject = this.GetObject(key); + + if (!hObject.IsArray) continue; + + for (int x = 0; x < hObject.Length; x++) + { + switch (hObject.GetKeyTypeIndexed(x)) + { + case Type_Int: + { + int temp = hObject.GetIntIndexed(x); + IntToString(temp, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + case Type_String: + { + hObject.GetStringIndexed(x, value, sizeof(value)); + AppendToQueryString(paramCount, queryString, maxlength, key, value); + } + } + } + } + } + } + + delete paramsMap; + } +} + +// =====[ PRIVATE ]===== + +static void BoolToString(bool value, char[] buffer, int maxlength) +{ + FormatEx(buffer, maxlength, "%s", value ? "true" : "false"); +} + +static void AppendToQueryString(int &index, char[] buffer, int maxlength, char[] key, char[] value) +{ + if (index == 0) + { + index++; + Format(buffer, maxlength, "?%s=%s", key, value); + } + else + { + index++; + Format(buffer, maxlength, "%s&%s=%s", buffer, key, value); + } +} diff --git a/sourcemod/scripting/include/GlobalAPI/responses.inc b/sourcemod/scripting/include/GlobalAPI/responses.inc new file mode 100644 index 0000000..62ebfd2 --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI/responses.inc @@ -0,0 +1,575 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_Responses_included_ +#endinput +#endif +#define _GlobalAPI_Responses_included_ + +// =========================================================== // + +#include <json> +#include <GlobalAPI/iterable> + +// =========================================================== // + +methodmap APIAuth < JSON_Object +{ + public APIAuth(JSON_Object hAuth) + { + return view_as<APIAuth>(hAuth); + } + + public bool GetType(char[] buffer, int maxlength) + { + return this.GetString("Type", buffer, maxlength); + } + + property bool IsValid + { + public get() { return this.GetBool("IsValid"); } + } + + property int Identity + { + public get() { return this.GetInt("Identity"); } + } +} + +// =========================================================== // + +methodmap APIBan < JSON_Object +{ + public APIBan(JSON_Object hBan) + { + return view_as<APIBan>(hBan); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + property int UpdatedById + { + public get() { return this.GetInt("updated_by_id"); } + } + + public void GetStats(char[] buffer, int maxlength) + { + this.GetString("stats", buffer, maxlength); + } + + public void GetBanType(char[] buffer, int maxlength) + { + this.GetString("ban_type", buffer, maxlength); + } + + public void GetExpiresOn(char[] buffer, int maxlength) + { + this.GetString("expires_on", buffer, maxlength); + } + + public void GetSteamId64(char[] buffer, int maxlength) + { + this.GetString("steamid64", buffer, maxlength); + } + + public void GetPlayerName(char[] buffer, int maxlength) + { + this.GetString("player_name", buffer, maxlength); + } + + public void GetNotes(char[] buffer, int maxlength) + { + this.GetString("notes", buffer, maxlength); + } + + public void GetSteamId(char[] buffer, int maxlength) + { + this.GetString("steam_id", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } + + property int ServerId + { + public get() { return this.GetInt("server_id"); } + } + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIJumpstat < JSON_Object +{ + public APIJumpstat(JSON_Object hJump) + { + return view_as<APIJumpstat>(hJump); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + property int ServerId + { + public get() { return this.GetInt("server_id"); } + } + + public void GetSteamId64(char[] buffer, int maxlength) + { + this.GetString("steamid64", buffer, maxlength); + } + + public void GetName(char[] buffer, int maxlength) + { + this.GetString("player_name", buffer, maxlength); + } + + public void GetSteamId(char[] buffer, int maxlength) + { + this.GetString("steam_id", buffer, maxlength); + } + + property int JumpType + { + public get() { return this.GetInt("jump_type"); } + } + + property float Distance + { + public get() { return this.GetFloat("distance"); } + } + + property int TickRate + { + public get() { return this.GetInt("tickrate"); } + } + + property int MslCount + { + public get() { return this.GetInt("msl_count"); } + } + + property int StrafeCount + { + public get() { return this.GetInt("strafe_count"); } + } + + property bool IsCrouchBind + { + public get() { return this.GetBool("is_crouch_bind"); } + } + + property bool IsForwardBind + { + public get() { return this.GetBool("is_forward_bind"); } + } + + property bool IsCrouchBoost + { + public get() { return this.GetBool("is_crouch_boost"); } + } + + property int UpdatedById + { + public get() { return this.GetInt("updated_by_id"); } + } + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIMap < JSON_Object +{ + public APIMap(JSON_Object hMap) + { + return view_as<APIMap>(hMap); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + public void GetName(char[] buffer, int maxlength) + { + this.GetString("name", buffer, maxlength); + } + + property int Filesize + { + public get() { return this.GetInt("filesize"); } + } + + property bool IsValidated + { + public get() { return this.GetBool("validated"); } + } + + property int Difficulty + { + public get() { return this.GetInt("difficulty"); } + } + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } + + public void GetApprovedBySteamId64(char[] buffer, int maxlength) + { + this.GetString("approved_by_steamid64", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIMode < JSON_Object +{ + public APIMode(JSON_Object hMode) + { + return view_as<APIMode>(hMode); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + public void GetName(char[] buffer, int maxlength) + { + this.GetString("name", buffer, maxlength); + } + + public void GetDescription(char[] buffer, int maxlength) + { + this.GetString("description", buffer, maxlength); + } + + property int LatestVersion + { + public get() { return this.GetInt("latest_version"); } + } + + public void GetLatestVersionDesc(char[] buffer, int maxlength) + { + this.GetString("latest_version_description", buffer, maxlength); + } + + public void GetWebsite(char[] buffer, int maxlength) + { + this.GetString("website", buffer, maxlength); + } + + public void GetRepository(char[] buffer, int maxlength) + { + this.GetString("repo", buffer, maxlength); + } + + public void GetContactSteamId64(char[] buffer, int maxlength) + { + this.GetString("contact_steamid64", buffer, maxlength); + } + + // TODO: Add supported_tickrates + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } + + public void GetUpdatedById(char[] buffer, int maxlength) + { + this.GetString("updated_by_id", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIPlayerRank < JSON_Object +{ + public APIPlayerRank(JSON_Object hPlayerRank) + { + return view_as<APIPlayerRank>(hPlayerRank); + } + + property int Points + { + public get() { return this.GetInt("points"); } + } + + property int PointsOverall + { + public get() { return this.GetInt("points_overall"); } + } + + property float Average + { + public get() { return this.GetFloat("average"); } + } + + property float Rating + { + public get() { return this.GetFloat("rating"); } + } + + property int Finishes + { + public get() { return this.GetInt("finishes"); } + } + + public void GetSteamId64(char[] buffer, int maxlength) + { + this.GetString("steamid64", buffer, maxlength); + } + + public void GetSteamId(char[] buffer, int maxlength) + { + this.GetString("steamid", buffer, maxlength); + } + + public void GetPlayerName(char[] buffer, int maxlength) + { + this.GetString("player_name", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIPlayer < JSON_Object +{ + public APIPlayer(JSON_Object hPlayer) + { + return view_as<APIPlayer>(hPlayer); + } + + public void GetSteamId64(char[] buffer, int maxlength) + { + this.GetString("steamid64", buffer, maxlength); + } + + public void GetSteamId(char[] buffer, int maxlength) + { + this.GetString("steam_id", buffer, maxlength); + } + + property bool IsBanned + { + public get() { return this.GetBool("is_banned"); } + } + + property int TotalRecords + { + public get() { return this.GetInt("total_records"); } + } + + public void GetName(char[] buffer, int maxlength) + { + this.GetString("name", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIRecord < JSON_Object +{ + public APIRecord(JSON_Object hRecord) + { + return view_as<APIRecord>(hRecord); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + public void GetSteamId64(char[] buffer, int maxlength) + { + this.GetString("steamid64", buffer, maxlength); + } + + public void GetPlayerName(char[] buffer, int maxlength) + { + this.GetString("player_name", buffer, maxlength); + } + + public void GetSteamId(char[] buffer, int maxlength) + { + this.GetString("steam_id", buffer, maxlength); + } + + property int ServerId + { + public get() { return this.GetInt("server_id"); } + } + + property int MapId + { + public get() { return this.GetInt("map_id"); } + } + + property int Stage + { + public get() { return this.GetInt("stage"); } + } + + public void GetMode(char[] buffer, int maxlength) + { + this.GetString("mode", buffer, maxlength); + } + + property int TickRate + { + public get() { return this.GetInt("tickrate"); } + } + + property float Time + { + public get() { return this.GetFloat("time"); } + } + + property int Teleports + { + public get() { return this.GetInt("teleports"); } + } + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } + + property int UpdatedBy + { + public get() { return this.GetInt("updated_by"); } + } + + public void GetServerName(char[] buffer, int maxlength) + { + this.GetString("server_name", buffer, maxlength); + } + + public void GetMapName(char[] buffer, int maxlength) + { + this.GetString("map_name", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIServer < JSON_Object +{ + public APIServer(JSON_Object hServer) + { + return view_as<APIServer>(hServer); + } + + property int Port + { + public get() { return this.GetInt("port"); } + } + + public void GetIPAddress(char[] buffer, int maxlength) + { + this.GetString("ip", buffer, maxlength); + } + + public void GetName(char[] buffer, int maxlength) + { + this.GetString("name", buffer, maxlength); + } + + public void GetOwnerSteamId64(char[] buffer, int maxlength) + { + this.GetString("owner_steamid64", buffer, maxlength); + } +} + +// =========================================================== // + +methodmap APIRecordFilter < JSON_Object +{ + public APIRecordFilter(JSON_Object hRecordFilter) + { + return view_as<APIRecordFilter>(hRecordFilter); + } + + property int Id + { + public get() { return this.GetInt("id"); } + } + + property int MapId + { + public get() { return this.GetInt("map_id"); } + } + + property int Stage + { + public get() { return this.GetInt("stage"); } + } + + property int ModeId + { + public get() { return this.GetInt("mode_id"); } + } + + property int TickRate + { + public get() { return this.GetInt("tickrate"); } + } + + property bool HasTeleports + { + public get() { return this.GetBool("has_teleports"); } + } + + public void GetCreatedOn(char[] buffer, int maxlength) + { + this.GetString("created_on", buffer, maxlength); + } + + public void GetUpdatedOn(char[] buffer, int maxlength) + { + this.GetString("updated_on", buffer, maxlength); + } + + public void GetUpdatedById(char[] buffer, int maxlength) + { + this.GetString("updated_by_id", buffer, maxlength); + } +} + +// =========================================================== // diff --git a/sourcemod/scripting/include/GlobalAPI/stocks.inc b/sourcemod/scripting/include/GlobalAPI/stocks.inc new file mode 100644 index 0000000..52596c4 --- /dev/null +++ b/sourcemod/scripting/include/GlobalAPI/stocks.inc @@ -0,0 +1,67 @@ +// ================== DOUBLE INCLUDE ========================= // + +#if defined _GlobalAPI_Stocks_included_ +#endinput +#endif +#define _GlobalAPI_Stocks_included_ + +// =========================================================== // + +/** + * Gets plugin's display name from its handle + * + * @param plugin Plugin handle to retrieve name from + * @return String representation of the plugin name + */ +stock char[] GetPluginDisplayName(Handle plugin) +{ + char pluginName[GlobalAPI_Max_PluginName_Length] = "Unknown"; + GetPluginInfo(plugin, PlInfo_Name, pluginName, sizeof(pluginName)); + + return pluginName; +} + +/** + * Gets plugin's version from its handle + * + * @param plugin Plugin handle to retrieve version from + * @return String representation of the plugin version + */ +stock char[] GetPluginVersion(Handle plugin) +{ + char pluginVersion[GlobalAPI_Max_PluginVersion_Length] = "Unknown"; + GetPluginInfo(plugin, PlInfo_Version, pluginVersion, sizeof(pluginVersion)); + + return pluginVersion; +} + +/** + * Gets current map's "display name" + * + * @param buffer Buffer to store the result in + * @param maxlength Max length of the buffer + * @noreturn + */ +stock void GetMapDisplay(char[] buffer, int maxlength) +{ + char map[PLATFORM_MAX_PATH]; + GetCurrentMap(map, sizeof(map)); + GetMapDisplayName(map, map, sizeof(map)); + + FormatEx(buffer, maxlength, map); +} + +/** + * Gets current map's full (game dir) path + * + * @param buffer Buffer to store result in + * @param maxlength Max length of the buffer + * @noreturn + */ +stock void GetMapFullPath(char[] buffer, int maxlength) +{ + char mapPath[PLATFORM_MAX_PATH]; + GetCurrentMap(mapPath, sizeof(mapPath)); + + Format(buffer, maxlength, "maps/%s.bsp", mapPath); +} diff --git a/sourcemod/scripting/include/SteamWorks.inc b/sourcemod/scripting/include/SteamWorks.inc new file mode 100644 index 0000000..0e4aec3 --- /dev/null +++ b/sourcemod/scripting/include/SteamWorks.inc @@ -0,0 +1,413 @@ +#if defined _SteamWorks_Included + #endinput +#endif +#define _SteamWorks_Included + +/* results from UserHasLicenseForApp */ +enum EUserHasLicenseForAppResult +{ + k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app + k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app + k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated +}; + +/* General result codes */ +enum EResult +{ + k_EResultOK = 1, // success + k_EResultFail = 2, // generic failure + k_EResultNoConnection = 3, // no/failed network connection +// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed + k_EResultInvalidPassword = 5, // password/ticket is invalid + k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere + k_EResultInvalidProtocolVer = 7, // protocol version is incorrect + k_EResultInvalidParam = 8, // a parameter is incorrect + k_EResultFileNotFound = 9, // file was not found + k_EResultBusy = 10, // called method busy - action not taken + k_EResultInvalidState = 11, // called object was in an invalid state + k_EResultInvalidName = 12, // name is invalid + k_EResultInvalidEmail = 13, // email is invalid + k_EResultDuplicateName = 14, // name is not unique + k_EResultAccessDenied = 15, // access is denied + k_EResultTimeout = 16, // operation timed out + k_EResultBanned = 17, // VAC2 banned + k_EResultAccountNotFound = 18, // account not found + k_EResultInvalidSteamID = 19, // steamID is invalid + k_EResultServiceUnavailable = 20, // The requested service is currently unavailable + k_EResultNotLoggedOn = 21, // The user is not logged on + k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party) + k_EResultEncryptionFailure = 23, // Encryption or Decryption failed + k_EResultInsufficientPrivilege = 24, // Insufficient privilege + k_EResultLimitExceeded = 25, // Too much of a good thing + k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes) + k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired + k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again + k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time + k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user + k_EResultIPNotFound = 31, // IP address not found + k_EResultPersistFailed = 32, // failed to write change to the data store + k_EResultLockingFailed = 33, // failed to acquire access lock for this operation + k_EResultLogonSessionReplaced = 34, + k_EResultConnectFailed = 35, + k_EResultHandshakeFailed = 36, + k_EResultIOFailure = 37, + k_EResultRemoteDisconnect = 38, + k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested + k_EResultBlocked = 40, // a user didn't allow it + k_EResultIgnored = 41, // target is ignoring sender + k_EResultNoMatch = 42, // nothing matching the request found + k_EResultAccountDisabled = 43, + k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now + k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available + k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin + k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol. + k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another. + k_EResultPasswordRequiredToKickSession = 49,// You are already logged in elsewhere, this cached credential login has failed. + k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait + k_EResultSuspended = 51, // Long running operation (content download) suspended/paused + k_EResultCancelled = 52, // Operation canceled (typically by user: content download) + k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable + k_EResultDiskFull = 54, // Operation canceled - not enough disk space. + k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed + k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side + k_EResultExternalAccountUnlinked = 57, // External account (PSN, Facebook...) is not linked to a Steam account + k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid + k_EResultExternalAccountAlreadyLinked = 59, // External account (PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first + k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files + k_EResultIllegalPassword = 61, // The requested new password is not legal + k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer ) + k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure + k_EResultCannotUseOldPassword = 64, // The requested new password is not legal + k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid + k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent + k_EResultHardwareNotCapableOfIPT = 67, // + k_EResultIPTInitError = 68, // + k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user + k_EResultFacebookQueryError = 70, // Facebook query returned an error + k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired + k_EResultIPLoginRestrictionFailed = 72, + k_EResultAccountLockedDown = 73, + k_EResultAccountLogonDeniedVerifiedEmailRequired = 74, + k_EResultNoMatchingURL = 75, + k_EResultBadResponse = 76, // parse failure, missing field, etc. + k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password + k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range + k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen + k_EResultDisabled = 80, // The requested service has been configured to be unavailable + k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid ! + k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action + k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted + k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent + k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login + k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted + k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker + k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch + k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match + k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners + k_EResultNotModified = 91, // data not modified + k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it + k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance + k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.) + k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource + k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account + k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone + k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet + k_EResultEmailSendFailure = 99, // Cannot send an email + k_EResultNotSettled = 100, // Can't perform operation till payment has settled + k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha + k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned + k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone) + k_EResultInvalidItemType = 104 // the type of thing we were requested to act on is invalid +}; + +/* This enum is used in client API methods, do not re-number existing values. */ +enum EHTTPMethod +{ + k_EHTTPMethodInvalid = 0, + k_EHTTPMethodGET, + k_EHTTPMethodHEAD, + k_EHTTPMethodPOST, + k_EHTTPMethodPUT, + k_EHTTPMethodDELETE, + k_EHTTPMethodOPTIONS, + k_EHTTPMethodPATCH, + + // The remaining HTTP methods are not yet supported, per rfc2616 section 5.1.1 only GET and HEAD are required for + // a compliant general purpose server. We'll likely add more as we find uses for them. + + // k_EHTTPMethodTRACE, + // k_EHTTPMethodCONNECT +}; + + +/* HTTP Status codes that the server can send in response to a request, see rfc2616 section 10.3 for descriptions + of each of these. */ +enum EHTTPStatusCode +{ + // Invalid status code (this isn't defined in HTTP, used to indicate unset in our code) + k_EHTTPStatusCodeInvalid = 0, + + // Informational codes + k_EHTTPStatusCode100Continue = 100, + k_EHTTPStatusCode101SwitchingProtocols = 101, + + // Success codes + k_EHTTPStatusCode200OK = 200, + k_EHTTPStatusCode201Created = 201, + k_EHTTPStatusCode202Accepted = 202, + k_EHTTPStatusCode203NonAuthoritative = 203, + k_EHTTPStatusCode204NoContent = 204, + k_EHTTPStatusCode205ResetContent = 205, + k_EHTTPStatusCode206PartialContent = 206, + + // Redirection codes + k_EHTTPStatusCode300MultipleChoices = 300, + k_EHTTPStatusCode301MovedPermanently = 301, + k_EHTTPStatusCode302Found = 302, + k_EHTTPStatusCode303SeeOther = 303, + k_EHTTPStatusCode304NotModified = 304, + k_EHTTPStatusCode305UseProxy = 305, + //k_EHTTPStatusCode306Unused = 306, (used in old HTTP spec, now unused in 1.1) + k_EHTTPStatusCode307TemporaryRedirect = 307, + + // Error codes + k_EHTTPStatusCode400BadRequest = 400, + k_EHTTPStatusCode401Unauthorized = 401, // You probably want 403 or something else. 401 implies you're sending a WWW-Authenticate header and the client can sent an Authorization header in response. + k_EHTTPStatusCode402PaymentRequired = 402, // This is reserved for future HTTP specs, not really supported by clients + k_EHTTPStatusCode403Forbidden = 403, + k_EHTTPStatusCode404NotFound = 404, + k_EHTTPStatusCode405MethodNotAllowed = 405, + k_EHTTPStatusCode406NotAcceptable = 406, + k_EHTTPStatusCode407ProxyAuthRequired = 407, + k_EHTTPStatusCode408RequestTimeout = 408, + k_EHTTPStatusCode409Conflict = 409, + k_EHTTPStatusCode410Gone = 410, + k_EHTTPStatusCode411LengthRequired = 411, + k_EHTTPStatusCode412PreconditionFailed = 412, + k_EHTTPStatusCode413RequestEntityTooLarge = 413, + k_EHTTPStatusCode414RequestURITooLong = 414, + k_EHTTPStatusCode415UnsupportedMediaType = 415, + k_EHTTPStatusCode416RequestedRangeNotSatisfiable = 416, + k_EHTTPStatusCode417ExpectationFailed = 417, + k_EHTTPStatusCode4xxUnknown = 418, // 418 is reserved, so we'll use it to mean unknown + k_EHTTPStatusCode429TooManyRequests = 429, + + // Server error codes + k_EHTTPStatusCode500InternalServerError = 500, + k_EHTTPStatusCode501NotImplemented = 501, + k_EHTTPStatusCode502BadGateway = 502, + k_EHTTPStatusCode503ServiceUnavailable = 503, + k_EHTTPStatusCode504GatewayTimeout = 504, + k_EHTTPStatusCode505HTTPVersionNotSupported = 505, + k_EHTTPStatusCode5xxUnknown = 599, +}; + +/* list of possible return values from the ISteamGameCoordinator API */ +enum EGCResults +{ + k_EGCResultOK = 0, + k_EGCResultNoMessage = 1, // There is no message in the queue + k_EGCResultBufferTooSmall = 2, // The buffer is too small for the requested message + k_EGCResultNotLoggedOn = 3, // The client is not logged onto Steam + k_EGCResultInvalidMessage = 4, // Something was wrong with the message being sent with SendMessage +}; + +native bool:SteamWorks_IsVACEnabled(); +native bool:SteamWorks_GetPublicIP(ipaddr[4]); +native SteamWorks_GetPublicIPCell(); +native bool:SteamWorks_IsLoaded(); +native bool:SteamWorks_SetGameData(const String:sData[]); +native bool:SteamWorks_SetGameDescription(const String:sDesc[]); +native bool:SteamWorks_SetMapName(const String:sMapName[]); +native bool:SteamWorks_IsConnected(); +native bool:SteamWorks_SetRule(const String:sKey[], const String:sValue[]); +native bool:SteamWorks_ClearRules(); +native bool:SteamWorks_ForceHeartbeat(); +native bool:SteamWorks_GetUserGroupStatus(client, groupid); +native bool:SteamWorks_GetUserGroupStatusAuthID(authid, groupid); + +native EUserHasLicenseForAppResult:SteamWorks_HasLicenseForApp(client, app); +native EUserHasLicenseForAppResult:SteamWorks_HasLicenseForAppId(authid, app); +native SteamWorks_GetClientSteamID(client, String:sSteamID[], length); + +native bool:SteamWorks_RequestStatsAuthID(authid, appid); +native bool:SteamWorks_RequestStats(client, appid); +native bool:SteamWorks_GetStatCell(client, const String:sKey[], &value); +native bool:SteamWorks_GetStatAuthIDCell(authid, const String:sKey[], &value); +native bool:SteamWorks_GetStatFloat(client, const String:sKey[], &Float:value); +native bool:SteamWorks_GetStatAuthIDFloat(authid, const String:sKey[], &Float:value); + +native Handle:SteamWorks_CreateHTTPRequest(EHTTPMethod:method, const String:sURL[]); +native bool:SteamWorks_SetHTTPRequestContextValue(Handle:hHandle, any:data1, any:data2=0); +native bool:SteamWorks_SetHTTPRequestNetworkActivityTimeout(Handle:hHandle, timeout); +native bool:SteamWorks_SetHTTPRequestHeaderValue(Handle:hHandle, const String:sName[], const String:sValue[]); +native bool:SteamWorks_SetHTTPRequestGetOrPostParameter(Handle:hHandle, const String:sName[], const String:sValue[]); +native bool:SteamWorks_SetHTTPRequestUserAgentInfo(Handle:hHandle, const String:sUserAgentInfo[]); +native bool:SteamWorks_SetHTTPRequestRequiresVerifiedCertificate(Handle:hHandle, bool:bRequireVerifiedCertificate); +native bool:SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(Handle:hHandle, unMilliseconds); + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 9 +typeset SteamWorksHTTPRequestCompleted +{ + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1, any data2); +}; + +typeset SteamWorksHTTPHeadersReceived +{ + function void (Handle hRequest, bool bFailure); + function void (Handle hRequest, bool bFailure, any data1); + function void (Handle hRequest, bool bFailure, any data1, any data2); +}; + +typeset SteamWorksHTTPDataReceived +{ + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1, any data2); +}; + +typeset SteamWorksHTTPBodyCallback +{ + function void (const char[] sData); + function void (const char[] sData, any value); + function void (const int[] data, any value, int datalen); +}; + +#else + +funcenum SteamWorksHTTPRequestCompleted +{ + public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode), + public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode, any:data1), + public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode, any:data1, any:data2) +}; + +funcenum SteamWorksHTTPHeadersReceived +{ + public(Handle:hRequest, bool:bFailure), + public(Handle:hRequest, bool:bFailure, any:data1), + public(Handle:hRequest, bool:bFailure, any:data1, any:data2) +}; + +funcenum SteamWorksHTTPDataReceived +{ + public(Handle:hRequest, bool:bFailure, offset, bytesreceived), + public(Handle:hRequest, bool:bFailure, offset, bytesreceived, any:data1), + public(Handle:hRequest, bool:bFailure, offset, bytesreceived, any:data1, any:data2) +}; + +funcenum SteamWorksHTTPBodyCallback +{ + public(const String:sData[]), + public(const String:sData[], any:value), + public(const data[], any:value, datalen) +}; + +#endif + +native bool:SteamWorks_SetHTTPCallbacks(Handle:hHandle, SteamWorksHTTPRequestCompleted:fCompleted = INVALID_FUNCTION, SteamWorksHTTPHeadersReceived:fHeaders = INVALID_FUNCTION, SteamWorksHTTPDataReceived:fData = INVALID_FUNCTION, Handle:hCalling = INVALID_HANDLE); +native bool:SteamWorks_SendHTTPRequest(Handle:hRequest); +native bool:SteamWorks_SendHTTPRequestAndStreamResponse(Handle:hRequest); +native bool:SteamWorks_DeferHTTPRequest(Handle:hRequest); +native bool:SteamWorks_PrioritizeHTTPRequest(Handle:hRequest); +native bool:SteamWorks_GetHTTPResponseHeaderSize(Handle:hRequest, const String:sHeader[], &size); +native bool:SteamWorks_GetHTTPResponseHeaderValue(Handle:hRequest, const String:sHeader[], String:sValue[], size); +native bool:SteamWorks_GetHTTPResponseBodySize(Handle:hRequest, &size); +native bool:SteamWorks_GetHTTPResponseBodyData(Handle:hRequest, String:sBody[], length); +native bool:SteamWorks_GetHTTPStreamingResponseBodyData(Handle:hRequest, cOffset, String:sBody[], length); +native bool:SteamWorks_GetHTTPDownloadProgressPct(Handle:hRequest, &Float:percent); +native bool:SteamWorks_GetHTTPRequestWasTimedOut(Handle:hRequest, &bool:bWasTimedOut); +native bool:SteamWorks_SetHTTPRequestRawPostBody(Handle:hRequest, const String:sContentType[], const String:sBody[], bodylen); +native bool:SteamWorks_SetHTTPRequestRawPostBodyFromFile(Handle:hRequest, const String:sContentType[], const String:sFileName[]); + +native bool:SteamWorks_GetHTTPResponseBodyCallback(Handle:hRequest, SteamWorksHTTPBodyCallback:fCallback, any:data = 0, Handle:hPlugin = INVALID_HANDLE); /* Look up, moved definition for 1.7+ compat. */ +native bool:SteamWorks_WriteHTTPResponseBodyToFile(Handle:hRequest, const String:sFileName[]); + +forward SW_OnValidateClient(ownerauthid, authid); +forward SteamWorks_OnValidateClient(ownerauthid, authid); +forward SteamWorks_SteamServersConnected(); +forward SteamWorks_SteamServersConnectFailure(EResult:result); +forward SteamWorks_SteamServersDisconnected(EResult:result); + +forward Action:SteamWorks_RestartRequested(); +forward SteamWorks_TokenRequested(String:sToken[], maxlen); + +forward SteamWorks_OnClientGroupStatus(authid, groupid, bool:isMember, bool:isOfficer); + +forward EGCResults:SteamWorks_GCSendMessage(unMsgType, const String:pubData[], cubData); +forward SteamWorks_GCMsgAvailable(cubData); +forward EGCResults:SteamWorks_GCRetrieveMessage(punMsgType, const String:pubDest[], cubDest, pcubMsgSize); + +native EGCResults:SteamWorks_SendMessageToGC(unMsgType, const String:pubData[], cubData); + +public Extension:__ext_SteamWorks = +{ + name = "SteamWorks", + file = "SteamWorks.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_EXTENSIONS +public __ext_SteamWorks_SetNTVOptional() +{ + MarkNativeAsOptional("SteamWorks_IsVACEnabled"); + MarkNativeAsOptional("SteamWorks_GetPublicIP"); + MarkNativeAsOptional("SteamWorks_GetPublicIPCell"); + MarkNativeAsOptional("SteamWorks_IsLoaded"); + MarkNativeAsOptional("SteamWorks_SetGameData"); + MarkNativeAsOptional("SteamWorks_SetGameDescription"); + MarkNativeAsOptional("SteamWorks_IsConnected"); + MarkNativeAsOptional("SteamWorks_SetRule"); + MarkNativeAsOptional("SteamWorks_ClearRules"); + MarkNativeAsOptional("SteamWorks_ForceHeartbeat"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatus"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatusAuthID"); + + MarkNativeAsOptional("SteamWorks_HasLicenseForApp"); + MarkNativeAsOptional("SteamWorks_HasLicenseForAppId"); + MarkNativeAsOptional("SteamWorks_GetClientSteamID"); + + MarkNativeAsOptional("SteamWorks_RequestStatsAuthID"); + MarkNativeAsOptional("SteamWorks_RequestStats"); + MarkNativeAsOptional("SteamWorks_GetStatCell"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDCell"); + MarkNativeAsOptional("SteamWorks_GetStatFloat"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDFloat"); + + MarkNativeAsOptional("SteamWorks_SendMessageToGC"); + + MarkNativeAsOptional("SteamWorks_CreateHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestContextValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestNetworkActivityTimeout"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestHeaderValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestGetOrPostParameter"); + + MarkNativeAsOptional("SteamWorks_SetHTTPCallbacks"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequestAndStreamResponse"); + MarkNativeAsOptional("SteamWorks_DeferHTTPRequest"); + MarkNativeAsOptional("SteamWorks_PrioritizeHTTPRequest"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderSize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderValue"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodySize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPStreamingResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPDownloadProgressPct"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBody"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBodyFromFile"); + + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyCallback"); + MarkNativeAsOptional("SteamWorks_WriteHTTPResponseBodyToFile"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/autoexecconfig.inc b/sourcemod/scripting/include/autoexecconfig.inc new file mode 100644 index 0000000..e057b1b --- /dev/null +++ b/sourcemod/scripting/include/autoexecconfig.inc @@ -0,0 +1,765 @@ +/** + * AutoExecConfig + * + * Copyright (C) 2013-2017 Impact + * + * 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 + * 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 <http://www.gnu.org/licenses/> + */ + +#if defined _autoexecconfig_included + #endinput +#endif +#define _autoexecconfig_included + + +#include <sourcemod> + +#define AUTOEXECCONFIG_VERSION "0.1.5" +#define AUTOEXECCONFIG_URL "https://forums.alliedmods.net/showthread.php?t=204254" + +// Append +#define AUTOEXEC_APPEND_BAD_FILENAME 0 +#define AUTOEXEC_APPEND_FILE_NOT_FOUND 1 +#define AUTOEXEC_APPEND_BAD_HANDLE 2 +#define AUTOEXEC_APPEND_SUCCESS 3 + + + +// Find +#define AUTOEXEC_FIND_BAD_FILENAME 10 +#define AUTOEXEC_FIND_FILE_NOT_FOUND 11 +#define AUTOEXEC_FIND_BAD_HANDLE 12 +#define AUTOEXEC_FIND_NOT_FOUND 13 +#define AUTOEXEC_FIND_SUCCESS 14 + + + +// Clean +#define AUTOEXEC_CLEAN_FILE_NOT_FOUND 20 +#define AUTOEXEC_CLEAN_BAD_HANDLE 21 +#define AUTOEXEC_CLEAN_SUCCESS 22 + + + +// General +#define AUTOEXEC_NO_CONFIG 30 + + + +// Formatter +#define AUTOEXEC_FORMAT_BAD_FILENAME 40 +#define AUTOEXEC_FORMAT_SUCCESS 41 + + + +// Global variables +static char g_sConfigFile[PLATFORM_MAX_PATH]; +static char g_sRawFileName[PLATFORM_MAX_PATH]; +static char g_sFolderPath[PLATFORM_MAX_PATH]; + +static bool g_bCreateFile = false; +static Handle g_hPluginHandle = null; + +static bool g_bCreateDirectory = false; +static int g_bCreateDirectoryMode = FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC; + + +// Workaround for now +static int g_iLastFindResult; +static int g_iLastAppendResult; + + + + +/** + * Returns the last result from the parser. + * + * @return Returns one of the AUTOEXEC_FIND values or -1 if not set. +*/ +stock int AutoExecConfig_GetFindResult() +{ + return g_iLastFindResult; +} + + + + + +/** + * Returns the last result from the appender. + * + * @return Returns one of the AUTOEXEC_APPEND values or -1 if not set. +*/ +stock int AutoExecConfig_GetAppendResult() +{ + return g_iLastAppendResult; +} + + +/** + * Set if the config file should be created by the autoexecconfig include itself if it doesn't exist. + * + * @param create True if config file should be created, false otherwise. + * @noreturn + */ +stock void AutoExecConfig_SetCreateFile(bool create) +{ + g_bCreateFile = create; +} + + +/** + * Set if the config file's folder should be created by the autoexecconfig include itself if it doesn't exist. + * Note: Must be used before AutoExecConfig_SetFile as the potential creation of it happens there + * + * @param create True if config file should be created, false otherwise. + * @param mode Folder permission mode, default is u=rwx,g=rx,o=rx. + * @noreturn + */ +stock void AutoExecConfig_SetCreateDirectory(bool create, int mode=FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC) +{ + g_bCreateDirectory = create; + g_bCreateDirectoryMode = mode; +} + + +/** + * Returns if the config file should be created if it doesn't exist. + * + * @return Returns true, if the config file should be created or false if it should not. + */ +stock bool AutoExecConfig_GetCreateFile() +{ + return g_bCreateFile; +} + + +/** + * Set the plugin for which the config file should be created. + * Set to null to use the calling plugin. + * Used to print the correct filename in the top comment when creating the file. + * + * @param plugin The plugin to create convars for or null to use the calling plugin. + * @noreturn + */ +stock void AutoExecConfig_SetPlugin(Handle plugin) +{ + g_hPluginHandle = plugin; +} + + +/** + * Returns the plugin's handle for which the config file is created. + * + * @return The plugin handle + */ +stock Handle AutoExecConfig_GetPlugin() +{ + return g_hPluginHandle; +} + + +/** + * Set the global autoconfigfile used by functions of this file. + * Note: does not support subfolders like folder1/folder2 + * + * @param file Name of the config file, path and .cfg extension is being added if not given. + * @param folder Folder under cfg/ to use. By default this is "sourcemod." + * @return True if formatter returned success, false otherwise. +*/ +stock bool AutoExecConfig_SetFile(char[] file, char[] folder="sourcemod") +{ + Format(g_sConfigFile, sizeof(g_sConfigFile), "%s", file); + + // Global buffers for cfg execution + strcopy(g_sRawFileName, sizeof(g_sRawFileName), file); + strcopy(g_sFolderPath, sizeof(g_sFolderPath), folder); + + + // Format the filename + return AutoExecConfig_FormatFileName(g_sConfigFile, sizeof(g_sConfigFile), folder) == AUTOEXEC_FORMAT_SUCCESS; +} + + + + + + +/** + * Get the formatted autoconfigfile used by functions of this file. + * + * @param buffer String to format. + * @param size Maximum size of buffer + * @return True if filename was set, false otherwise. +*/ +stock bool AutoExecConfig_GetFile(char[] buffer,int size) +{ + if (strlen(g_sConfigFile) > 0) + { + strcopy(buffer, size, g_sConfigFile); + + return true; + } + + // Security for decl users + buffer[0] = '\0'; + + return false; +} + + + + + + +/** + * Creates a convar and appends it to the autoconfigfile if not found. + * FCVAR_DONTRECORD will be skipped. + * + * @param name Name of new convar. + * @param defaultValue String containing the default value of new convar. + * @param description Optional description of the convar. + * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details. + * @param hasMin Optional boolean that determines if the convar has a minimum value. + * @param min Minimum floating point value that the convar can have if hasMin is true. + * @param hasMax Optional boolean that determines if the convar has a maximum value. + * @param max Maximum floating point value that the convar can have if hasMax is true. + * @return A handle to the newly created convar. If the convar already exists, a handle to it will still be returned. + * @error Convar name is blank or is the same as an existing console command. +*/ +stock ConVar AutoExecConfig_CreateConVar(const char[] name, const char[] defaultValue, const char[] description="", int flags=0, bool hasMin=false, float min=0.0, bool hasMax=false, float max=0.0) +{ + // If configfile was set and convar has no dontrecord flag + if (!(flags & FCVAR_DONTRECORD) && strlen(g_sConfigFile) > 0) + { + // Reset the results + g_iLastFindResult = -1; + g_iLastAppendResult = -1; + + + // Add it if not found + char buffer[64]; + + g_iLastFindResult = AutoExecConfig_FindValue(name, buffer, sizeof(buffer), true); + + // We only add this convar if it doesn't exist, or the file doesn't exist and it should be auto-generated + if (g_iLastFindResult == AUTOEXEC_FIND_NOT_FOUND || (g_iLastFindResult == AUTOEXEC_FIND_FILE_NOT_FOUND && g_bCreateFile)) + { + g_iLastAppendResult = AutoExecConfig_AppendValue(name, defaultValue, description, flags, hasMin, min, hasMax, max); + } + } + + + // Create the convar + return CreateConVar(name, defaultValue, description, flags, hasMin, min, hasMax, max); +} + + + + +/** + * Executes the autoconfigfile and adds it to the OnConfigsExecuted forward. + * If we didn't create it ourselves we let SourceMod create it. + * + * @noreturn +*/ +stock void AutoExecConfig_ExecuteFile() +{ + // Only let sourcemod create the file, if we didn't do that already. + AutoExecConfig(!g_bCreateFile, g_sRawFileName, g_sFolderPath); +} + + + + + +/** + * Formats a autoconfigfile, prefixes path and adds .cfg extension if missing. + * + * @param buffer String to format. + * @param size Maximum size of buffer. + * @return Returns one of the AUTOEXEC_FORMAT values.. +*/ +stock static int AutoExecConfig_FormatFileName(char[] buffer, int size, char[] folder="sourcemod") +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + // Can't be an cfgfile + if (StrContains(g_sConfigFile, ".cfg") != -1 && strlen(g_sConfigFile) < 4) + { + return AUTOEXEC_FORMAT_BAD_FILENAME; + } + + + // Pathprefix + char pathprefixbuffer[PLATFORM_MAX_PATH]; + if (strlen(folder) > 0) + { + Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/%s/", folder); + + if (g_bCreateDirectory && !DirExists(pathprefixbuffer)) + { + CreateDirectory(pathprefixbuffer, g_bCreateDirectoryMode); + } + } + else + { + Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/"); + } + + + char filebuffer[PLATFORM_MAX_PATH]; + filebuffer[0] = '\0'; + + // Add path if file doesn't begin with it + if (StrContains(buffer, pathprefixbuffer) != 0) + { + StrCat(filebuffer, sizeof(filebuffer), pathprefixbuffer); + } + + StrCat(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + // Add .cfg extension if file doesn't end with it + if (StrContains(filebuffer[strlen(filebuffer) - 4], ".cfg") != 0) + { + StrCat(filebuffer, sizeof(filebuffer), ".cfg"); + } + + strcopy(buffer, size, filebuffer); + + return AUTOEXEC_FORMAT_SUCCESS; +} + + + + + + +/** + * Appends a convar to the global autoconfigfile + * + * @param name Name of new convar. + * @param defaultValue String containing the default value of new convar. + * @param description Optional description of the convar. + * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details. + * @param hasMin Optional boolean that determines if the convar has a minimum value. + * @param min Minimum floating point value that the convar can have if hasMin is true. + * @param hasMax Optional boolean that determines if the convar has a maximum value. + * @param max Maximum floating point value that the convar can have if hasMax is true. + * @return Returns one of the AUTOEXEC_APPEND values +*/ +stock int AutoExecConfig_AppendValue(const char[] name, const char[] defaultValue, const char[] description, int flags, bool hasMin, float min, bool hasMax, float max) +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char filebuffer[PLATFORM_MAX_PATH]; + strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + //PrintToServer("pathbuffer: %s", filebuffer); + + bool bFileExists = FileExists(filebuffer); + + if (g_bCreateFile || bFileExists) + { + // If the file already exists we open it in append mode, otherwise we use a write mode which creates the file + File fFile = OpenFile(filebuffer, (bFileExists ? "a" : "w")); + char writebuffer[2048]; + + + if (fFile == null) + { + return AUTOEXEC_APPEND_BAD_HANDLE; + } + + // We just created the file, so add some header about version and stuff + if (g_bCreateFile && !bFileExists) + { + fFile.WriteLine( "// This file was auto-generated by AutoExecConfig v%s (%s)", AUTOEXECCONFIG_VERSION, AUTOEXECCONFIG_URL); + + GetPluginFilename(g_hPluginHandle, writebuffer, sizeof(writebuffer)); + Format(writebuffer, sizeof(writebuffer), "// ConVars for plugin \"%s\"", writebuffer); + fFile.WriteLine(writebuffer); + } + + // Spacer + fFile.WriteLine("\n"); + + + // This is used for multiline comments + int newlines = GetCharCountInStr('\n', description); + if (newlines == 0) + { + // We have no newlines, we can write the description to the file as is + Format(writebuffer, sizeof(writebuffer), "// %s", description); + fFile.WriteLine(writebuffer); + } + else + { + char[][] newlineBuf = new char[newlines +1][2048]; + ExplodeString(description, "\n", newlineBuf, newlines +1, 2048, false); + + // Each newline gets a commented newline + for (int i; i <= newlines; i++) + { + if (strlen(newlineBuf[i]) > 0) + { + fFile.WriteLine("// %s", newlineBuf[i]); + } + } + } + + + // Descspacer + fFile.WriteLine("// -"); + + + // Default + Format(writebuffer, sizeof(writebuffer), "// Default: \"%s\"", defaultValue); + fFile.WriteLine(writebuffer); + + + // Minimum + if (hasMin) + { + Format(writebuffer, sizeof(writebuffer), "// Minimum: \"%f\"", min); + fFile.WriteLine(writebuffer); + } + + + // Maximum + if (hasMax) + { + Format(writebuffer, sizeof(writebuffer), "// Maximum: \"%f\"", max); + fFile.WriteLine(writebuffer); + } + + + // Write end and defaultvalue + Format(writebuffer, sizeof(writebuffer), "%s \"%s\"", name, defaultValue); + fFile.WriteLine(writebuffer); + + + fFile.Close(); + + return AUTOEXEC_APPEND_SUCCESS; + } + + return AUTOEXEC_APPEND_FILE_NOT_FOUND; +} + + + + + + +/** + * Returns a convar's value from the global autoconfigfile + * + * @param cvar Cvar to search for. + * @param value Buffer to store result into. + * @param size Maximum size of buffer. + * @param caseSensitive Whether or not the search should be case sensitive. + * @return Returns one of the AUTOEXEC_FIND values +*/ +stock int AutoExecConfig_FindValue(const char[] cvar, char[] value, int size, bool caseSensitive=false) +{ + // Security for decl users + value[0] = '\0'; + + + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char filebuffer[PLATFORM_MAX_PATH]; + strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile); + + + + //PrintToServer("pathbuffer: %s", filebuffer); + + bool bFileExists = FileExists(filebuffer); + + // We want to create the config file and it doesn't exist yet. + if (g_bCreateFile && !bFileExists) + { + return AUTOEXEC_FIND_FILE_NOT_FOUND; + } + + + if (bFileExists) + { + File fFile = OpenFile(filebuffer, "r"); + int valuestart; + int valueend; + int cvarend; + + // Just an reminder to self, leave the values that high + char sConvar[64]; + char sValue[64]; + char readbuffer[2048]; + char copybuffer[2048]; + + if (fFile == null) + { + return AUTOEXEC_FIND_BAD_HANDLE; + } + + + while (!fFile.EndOfFile() && fFile.ReadLine(readbuffer, sizeof(readbuffer))) + { + // Is a comment or not valid + if (IsCharSpace(readbuffer[0]) || readbuffer[0] == '/' || (!IsCharNumeric(readbuffer[0]) && !IsCharAlpha(readbuffer[0])) ) + { + continue; + } + + + // Has not enough spaces, must have at least 1 + if (GetCharCountInStr(' ', readbuffer) < 1) + { + continue; + } + + + // Ignore cvars which aren't quoted + if (GetCharCountInStr('"', readbuffer) != 2) + { + continue; + } + + + + // Get the start of the value + if ( (valuestart = StrContains(readbuffer, "\"")) == -1 ) + { + continue; + } + + + // Get the end of the value + if ( (valueend = StrContains(readbuffer[valuestart+1], "\"")) == -1 ) + { + continue; + } + + + // Get the start of the cvar, + if ( (cvarend = StrContains(readbuffer, " ")) == -1 || cvarend >= valuestart) + { + continue; + } + + + // Skip if cvarendindex is before valuestartindex + if (cvarend >= valuestart) + { + continue; + } + + + // Convar + // Tempcopy for security + strcopy(copybuffer, sizeof(copybuffer), readbuffer); + copybuffer[cvarend] = '\0'; + + strcopy(sConvar, sizeof(sConvar), copybuffer); + + + // Value + // Tempcopy for security + strcopy(copybuffer, sizeof(copybuffer), readbuffer[valuestart+1]); + copybuffer[valueend] = '\0'; + + strcopy(sValue, sizeof(sValue), copybuffer); + + + //PrintToServer("Cvar %s has a value of %s", sConvar, sValue); + + if (StrEqual(sConvar, cvar, caseSensitive)) + { + Format(value, size, "%s", sConvar); + + fFile.Close(); + return AUTOEXEC_FIND_SUCCESS; + } + } + + fFile.Close(); + return AUTOEXEC_FIND_NOT_FOUND; + } + + + return AUTOEXEC_FIND_FILE_NOT_FOUND; +} + + + + + + +/** + * Cleans the global autoconfigfile from too much spaces + * + * @return One of the AUTOEXEC_CLEAN values. +*/ +stock int AutoExecConfig_CleanFile() +{ + // No config set + if (strlen(g_sConfigFile) < 1) + { + return AUTOEXEC_NO_CONFIG; + } + + + char sfile[PLATFORM_MAX_PATH]; + strcopy(sfile, sizeof(sfile), g_sConfigFile); + + + // Security + if (!FileExists(sfile)) + { + return AUTOEXEC_CLEAN_FILE_NOT_FOUND; + } + + + + char sfile2[PLATFORM_MAX_PATH]; + Format(sfile2, sizeof(sfile2), "%s_tempcopy", sfile); + + + char readbuffer[2048]; + int count; + bool firstreached; + + + // Open files + File fFile1 = OpenFile(sfile, "r"); + File fFile2 = OpenFile(sfile2, "w"); + + + + // Check filehandles + if (fFile1 == null || fFile2 == null) + { + if (fFile1 != null) + { + //PrintToServer("Handle1 invalid"); + fFile1.Close(); + } + + if (fFile2 != null) + { + //PrintToServer("Handle2 invalid"); + fFile2.Close(); + } + + return AUTOEXEC_CLEAN_BAD_HANDLE; + } + + + + while (!fFile1.EndOfFile() && fFile1.ReadLine(readbuffer, sizeof(readbuffer))) + { + // Is space + if (IsCharSpace(readbuffer[0])) + { + count++; + } + // No space, count from start + else + { + count = 0; + } + + + // Don't write more than 1 space if seperation after informations have been reached + if (count < 2 || !firstreached) + { + ReplaceString(readbuffer, sizeof(readbuffer), "\n", ""); + fFile2.WriteLine(readbuffer); + } + + + // First bigger seperation after informations has been reached + if (count == 2) + { + firstreached = true; + } + } + + + fFile1.Close(); + fFile2.Close(); + + + // This might be a risk, for now it works + DeleteFile(sfile); + RenameFile(sfile, sfile2); + + return AUTOEXEC_CLEAN_SUCCESS; +} + + + + + + +/** + * Returns how many times the given char occures in the given string. + * + * @param str String to search for in. + * @return Occurences of the given char found in string. +*/ +stock static int GetCharCountInStr(int character, const char[] str) +{ + int len = strlen(str); + int count; + + for (int i; i < len; i++) + { + if (str[i] == character) + { + count++; + } + } + + return count; +} + + + + + + +#pragma deprecated +stock bool AutoExecConfig_CacheConvars() +{ + return false; +} diff --git a/sourcemod/scripting/include/colors.inc b/sourcemod/scripting/include/colors.inc new file mode 100644 index 0000000..3ce8b6a --- /dev/null +++ b/sourcemod/scripting/include/colors.inc @@ -0,0 +1,945 @@ +/************************************************************************** + * * + * Colored Chat Functions * + * Author: exvel, Editor: Popoklopsi, Powerlord, Bara * + * Version: 1.2.3 * + * by modified by 1NutWunDeR * + **************************************************************************/ + +/*Info: purple works only with CPrintToChat and CPrintToChatAll and without {blue} in the same string + (volvo gave them the same color code and saytext2 overrides purple with your current teamcolor.)*/ + +#if defined _colors_included + #endinput +#endif +#define _colors_included + +#define MAX_MESSAGE_LENGTH 320 +#define MAX_COLORS 17 + +#define SERVER_INDEX 0 +#define NO_INDEX -1 +#define NO_PLAYER -2 + +enum Colors +{ + Color_Default = 0, + Color_Darkred, + Color_Green, + Color_Lightgreen, + Color_Red, + Color_Blue, + Color_Olive, + Color_Lime, + Color_Orange, + Color_Purple, + Color_Grey, + Color_Yellow, + Color_Lightblue, + Color_Steelblue, + Color_Darkblue, + Color_Pink, + Color_Lightred, +} + +/* Colors' properties */ +// {"{default}", "{darkred}", "{green}", "{lightgreen}", "{orange}", "{blue}", "{olive}", "{lime}", "{red}", "{purple}", "{grey}", "{yellow}", "{lightblue}", "{steelblue}", "{darkblue}", "{pink}", "{lightred}"}; +new String:CTag[][] = {"{d}", "{dr}", "{gr}", "{lg}", "{o}", "{b}", "{ol}", "{l}", "{r}", "{p}", "{g}", "{y}", "{lb}", "{sb}", "{db}", "{pi}", "{lr}"}; +new String:CTagCode[][] = {"\x01", "\x02", "\x04", "\x03", "\x03", "\x03", "\x05", "\x06", "\x07", "\x03", "\x08", "\x09","\x0A","\x0B","\x0C","\x0E","\x0F"}; +new bool:CTagReqSayText2[] = {false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false}; +new bool:CEventIsHooked = false; +new bool:CSkipList[MAXPLAYERS+1] = {false,...}; + +/* Game default profile */ +new bool:CProfile_Colors[] = {true, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; +new CProfile_TeamIndex[] = {NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX}; +new bool:CProfile_SayText2 = false; + + +static Handle:sm_show_activity = INVALID_HANDLE; + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the client is not connected an error will be thrown. + */ +stock CPrintToChat(client, const String:szMessage[], any:...) +{ + if (client <= 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (!IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + decl String:szCMessage[MAX_MESSAGE_LENGTH]; + + SetGlobalTransTarget(client); + + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); + VFormat(szCMessage, sizeof(szCMessage), szBuffer, 3); + + new index = CFormat(szCMessage, sizeof(szCMessage)); + + if (index == NO_INDEX) + PrintToChat(client, "%s", szCMessage); + else + CSayText2(client, index, szCMessage); +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, CPrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock CReplyToCommand(client, const String:szMessage[], any:...) +{ + decl String:szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 3); + + if (client == 0) + { + CRemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) + { + CRemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); + } + else + { + CPrintToChat(client, "%s", szCMessage); + } +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, CPrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param author Author index whose color will be used for teamcolor tag. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock CReplyToCommandEx(client, author, const String:szMessage[], any:...) +{ + decl String:szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 4); + + if (client == 0) + { + CRemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) + { + CRemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); + } + else + { + CPrintToChatEx(client, author, "%s", szCMessage); + } +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param szMessage Message (formatting rules) + * @return No return + */ +stock CPrintToChatAll(const String:szMessage[], any:...) +{ + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i]) + { + SetGlobalTransTarget(i); + VFormat(szBuffer, sizeof(szBuffer), szMessage, 2); + + CPrintToChat(i, "%s", szBuffer); + } + + CSkipList[i] = false; + } +} + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags and teamcolor tag. + * + * @param client Client index. + * @param author Author index whose color will be used for teamcolor tag. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the client or author are not connected an error will be thrown. + */ +stock CPrintToChatEx(client, author, const String:szMessage[], any:...) +{ + if (client <= 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (!IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + if (author < 0 || author > MaxClients) + ThrowError("Invalid client index %d", author); + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + decl String:szCMessage[MAX_MESSAGE_LENGTH]; + + SetGlobalTransTarget(client); + + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); + VFormat(szCMessage, sizeof(szCMessage), szBuffer, 4); + + new index = CFormat(szCMessage, sizeof(szCMessage), author); + + if (index == NO_INDEX) + PrintToChat(client, "%s", szCMessage); + else + CSayText2(client, author, szCMessage); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags and teamcolor tag. + * + * @param author Author index whos color will be used for teamcolor tag. + * @param szMessage Message (formatting rules). + * @return No return + * + * On error/Errors: If the author is not connected an error will be thrown. + */ +stock CPrintToChatAllEx(author, const String:szMessage[], any:...) +{ + if (author < 0 || author > MaxClients) + ThrowError("Invalid client index %d", author); + + if (!IsClientInGame(author)) + ThrowError("Client %d is not in game", author); + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i]) + { + SetGlobalTransTarget(i); + VFormat(szBuffer, sizeof(szBuffer), szMessage, 3); + + CPrintToChatEx(i, author, "%s", szBuffer); + } + + CSkipList[i] = false; + } +} + +/** + * Removes color tags from the string. + * + * @param szMessage String. + * @return No return + */ +stock CRemoveTags(String:szMessage[], maxlength) +{ + for (new i = 0; i < MAX_COLORS; i++) + ReplaceString(szMessage, maxlength, CTag[i], "", false); + + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); +} + +/** + * Checks whether a color is allowed or not + * + * @param tag Color Tag. + * @return True when color is supported, otherwise false + */ +stock CColorAllowed(Colors:color) +{ + if (!CEventIsHooked) + { + CSetupProfile(); + + CEventIsHooked = true; + } + + return CProfile_Colors[color]; +} + +/** + * Replace the color with another color + * Handle with care! + * + * @param color color to replace. + * @param newColor color to replace with. + * @noreturn + */ +stock CReplaceColor(Colors:color, Colors:newColor) +{ + if (!CEventIsHooked) + { + CSetupProfile(); + + CEventIsHooked = true; + } + + CProfile_Colors[color] = CProfile_Colors[newColor]; + CProfile_TeamIndex[color] = CProfile_TeamIndex[newColor]; + + CTagReqSayText2[color] = CTagReqSayText2[newColor]; + Format(CTagCode[color], sizeof(CTagCode[]), CTagCode[newColor]) +} + +/** + * This function should only be used right in front of + * CPrintToChatAll or CPrintToChatAllEx and it tells + * to those funcions to skip specified client when printing + * message to all clients. After message is printed client will + * no more be skipped. + * + * @param client Client index + * @return No return + */ +stock CSkipNextClient(client) +{ + if (client <= 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + CSkipList[client] = true; +} + +/** + * Replaces color tags in a string with color codes + * + * @param szMessage String. + * @param maxlength Maximum length of the string buffer. + * @return Client index that can be used for SayText2 author index + * + * On error/Errors: If there is more then one team color is used an error will be thrown. + */ +stock CFormat(String:szMessage[], maxlength, author=NO_INDEX) +{ + decl String:szGameName[30]; + + GetGameFolderName(szGameName, sizeof(szGameName)); + + /* Hook event for auto profile setup on map start */ + if (!CEventIsHooked) + { + CSetupProfile(); + HookEvent("server_spawn", CEvent_MapStart, EventHookMode_PostNoCopy); + + CEventIsHooked = true; + } + + new iRandomPlayer = NO_INDEX; + + // On CS:GO set invisible precolor + if (StrEqual(szGameName, "csgo", false)) + Format(szMessage, maxlength, " \x01\x0B\x01%s", szMessage); + + /* If author was specified replace {teamcolor} tag */ + if (author != NO_INDEX) + { + if (CProfile_SayText2) + { + ReplaceString(szMessage, maxlength, "{teamcolor}", "\x03", false); + + iRandomPlayer = author; + } + /* If saytext2 is not supported by game replace {teamcolor} with green tag */ + else + ReplaceString(szMessage, maxlength, "{teamcolor}", CTagCode[Color_Green], false); + } + else + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); + + /* For other color tags we need a loop */ + for (new i = 0; i < MAX_COLORS; i++) + { + /* If tag not found - skip */ + if (StrContains(szMessage, CTag[i], false) == -1) + continue; + + /* If tag is not supported by game replace it with green tag */ + else if (!CProfile_Colors[i]) + ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false); + + /* If tag doesn't need saytext2 simply replace */ + else if (!CTagReqSayText2[i]) + ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i], false); + + /* Tag needs saytext2 */ + else + { + /* If saytext2 is not supported by game replace tag with green tag */ + if (!CProfile_SayText2) + ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false); + + /* Game supports saytext2 */ + else + { + /* If random player for tag wasn't specified replace tag and find player */ + if (iRandomPlayer == NO_INDEX) + { + /* Searching for valid client for tag */ + iRandomPlayer = CFindRandomPlayerByTeam(CProfile_TeamIndex[i]); + + /* If player not found replace tag with green color tag */ + if (iRandomPlayer == NO_PLAYER) + ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false); + + /* If player was found simply replace */ + else + ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i], false); + + } + /* If found another team color tag throw error */ + else + { + //ReplaceString(szMessage, maxlength, CTag[i], ""); + ThrowError("Using two team colors in one message is not allowed"); + } + } + + } + } + + return iRandomPlayer; +} + +/** + * Founds a random player with specified team + * + * @param color_team Client team. + * @return Client index or NO_PLAYER if no player found + */ +stock CFindRandomPlayerByTeam(color_team) +{ + if (color_team == SERVER_INDEX) + return 0; + else + { + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && GetClientTeam(i) == color_team) + return i; + } + } + + return NO_PLAYER; +} + +/** + * Sends a SayText2 usermessage to a client + * + * @param szMessage Client index + * @param maxlength Author index + * @param szMessage Message + * @return No return. + */ +stock CSayText2(client, author, const String:szMessage[]) +{ + new Handle:hBuffer = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + + if(GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) + { + PbSetInt(hBuffer, "ent_idx", author); + PbSetBool(hBuffer, "chat", true); + PbSetString(hBuffer, "msg_name", szMessage); + PbAddString(hBuffer, "params", ""); + PbAddString(hBuffer, "params", ""); + PbAddString(hBuffer, "params", ""); + PbAddString(hBuffer, "params", ""); + } + else + { + BfWriteByte(hBuffer, author); + BfWriteByte(hBuffer, true); + BfWriteString(hBuffer, szMessage); + } + + EndMessage(); +} + +/** + * Creates game color profile + * This function must be edited if you want to add more games support + * + * @return No return. + */ +stock CSetupProfile() +{ + decl String:szGameName[30]; + GetGameFolderName(szGameName, sizeof(szGameName)); + + if (StrEqual(szGameName, "cstrike", false)) + { + CProfile_Colors[Color_Lightgreen] = true; + CProfile_Colors[Color_Orange] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_Colors[Color_Olive] = true; + CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + CProfile_TeamIndex[Color_Orange] = 2; + CProfile_TeamIndex[Color_Blue] = 3; + CProfile_SayText2 = true; + } + else if (StrEqual(szGameName, "csgo", false)) + { + CProfile_Colors[Color_Red] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_Colors[Color_Olive] = true; + CProfile_Colors[Color_Darkred] = true; + CProfile_Colors[Color_Lime] = true; + CProfile_Colors[Color_Purple] = true; + CProfile_Colors[Color_Grey] = true; + CProfile_Colors[Color_Yellow] = true; + CProfile_Colors[Color_Lightblue] = true; + CProfile_Colors[Color_Steelblue] = true; + CProfile_Colors[Color_Darkblue] = true; + CProfile_Colors[Color_Pink] = true; + CProfile_Colors[Color_Lightred] = true; + CProfile_TeamIndex[Color_Orange] = 2; + CProfile_TeamIndex[Color_Blue] = 3; + CProfile_SayText2 = true; + } + else if (StrEqual(szGameName, "tf", false)) + { + CProfile_Colors[Color_Lightgreen] = true; + CProfile_Colors[Color_Orange] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_Colors[Color_Olive] = true; + CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + CProfile_TeamIndex[Color_Orange] = 2; + CProfile_TeamIndex[Color_Blue] = 3; + CProfile_SayText2 = true; + } + else if (StrEqual(szGameName, "left4dead", false) || StrEqual(szGameName, "left4dead2", false)) + { + CProfile_Colors[Color_Lightgreen] = true; + CProfile_Colors[Color_Orange] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_Colors[Color_Olive] = true; + CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + CProfile_TeamIndex[Color_Orange] = 3; + CProfile_TeamIndex[Color_Blue] = 2; + CProfile_SayText2 = true; + } + else if (StrEqual(szGameName, "hl2mp", false)) + { + /* hl2mp profile is based on mp_teamplay convar */ + if (GetConVarBool(FindConVar("mp_teamplay"))) + { + CProfile_Colors[Color_Orange] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_Colors[Color_Olive] = true; + CProfile_TeamIndex[Color_Orange] = 3; + CProfile_TeamIndex[Color_Blue] = 2; + CProfile_SayText2 = true; + } + else + { + CProfile_SayText2 = false; + CProfile_Colors[Color_Olive] = true; + } + } + else if (StrEqual(szGameName, "dod", false)) + { + CProfile_Colors[Color_Olive] = true; + CProfile_SayText2 = false; + } + /* Profile for other games */ + else + { + if (GetUserMessageId("SayText2") == INVALID_MESSAGE_ID) + { + CProfile_SayText2 = false; + } + else + { + CProfile_Colors[Color_Orange] = true; + CProfile_Colors[Color_Blue] = true; + CProfile_TeamIndex[Color_Orange] = 2; + CProfile_TeamIndex[Color_Blue] = 3; + CProfile_SayText2 = true; + } + } +} + +public Action:CEvent_MapStart(Handle:event, const String:name[], bool:dontBroadcast) +{ + CSetupProfile(); + + for (new i = 1; i <= MaxClients; i++) + CSkipList[i] = false; +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * CShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock CShowActivity(client, const String:format[], any:...) +{ + if (sm_show_activity == INVALID_HANDLE) + sm_show_activity = FindConVar("sm_show_activity"); + + new String:tag[] = "[SM] "; + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + //decl String:szCMessage[MAX_MESSAGE_LENGTH]; + new value = GetConVarInt(sm_show_activity); + new ReplySource:replyto = GetCmdReplySource(); + + new String:name[MAX_NAME_LENGTH] = "Console"; + new String:sign[MAX_NAME_LENGTH] = "ADMIN"; + new bool:display_in_chat = false; + if (client != 0) + { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) + ThrowError("Client index %d is invalid", client); + + GetClientName(client, name, sizeof(name)); + new AdminId:id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) + { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + CRemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s\n", tag, szBuffer); + display_in_chat = true; + } + } + else + { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + CRemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) + { + return 1; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) + || IsFakeClient(i) + || (display_in_chat && i == client)) + { + continue; + } + new AdminId:id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) + { + new String:newsign[MAX_NAME_LENGTH]; + newsign = sign; + if ((value & 2) || (i == client)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else + { + /* Treat this as an admin user */ + new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) + { + new String:newsign[MAX_NAME_LENGTH] + newsign = sign; + if ((value & 8) || ((value & 16) && is_root) || (i == client)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Same as CShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock CShowActivityEx(client, const String:tag[], const String:format[], any:...) +{ + if (sm_show_activity == INVALID_HANDLE) + sm_show_activity = FindConVar("sm_show_activity"); + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + //decl String:szCMessage[MAX_MESSAGE_LENGTH]; + new value = GetConVarInt(sm_show_activity); + new ReplySource:replyto = GetCmdReplySource(); + + new String:name[MAX_NAME_LENGTH] = "Console"; + new String:sign[MAX_NAME_LENGTH] = "ADMIN"; + new bool:display_in_chat = false; + if (client != 0) + { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) + ThrowError("Client index %d is invalid", client); + + GetClientName(client, name, sizeof(name)); + new AdminId:id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) + { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CRemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s\n", tag, szBuffer); + display_in_chat = true; + } + } + else + { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CRemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) + { + return 1; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) + || IsFakeClient(i) + || (display_in_chat && i == client)) + { + continue; + } + new AdminId:id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) + { + new String:newsign[MAX_NAME_LENGTH]; + newsign = sign; + if ((value & 2) || (i == client)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else + { + /* Treat this as an admin user */ + new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) + { + new String:newsign[MAX_NAME_LENGTH]; + newsign = sign; + if ((value & 8) || ((value & 16) && is_root) || (i == client)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock CShowActivity2(client, const String:tag[], const String:format[], any:...) +{ + if (sm_show_activity == INVALID_HANDLE) + sm_show_activity = FindConVar("sm_show_activity"); + + decl String:szBuffer[MAX_MESSAGE_LENGTH]; + //decl String:szCMessage[MAX_MESSAGE_LENGTH]; + new value = GetConVarInt(sm_show_activity); + GetCmdReplySource(); + + new String:name[MAX_NAME_LENGTH] = "Console"; + new String:sign[MAX_NAME_LENGTH] = "ADMIN"; + if (client != 0) + { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) + ThrowError("Client index %d is invalid", client); + + GetClientName(client, name, sizeof(name)); + new AdminId:id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + sign = "PLAYER"; + } + + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + /* We don't display directly to the console because the chat text + * simply gets added to the console, so we don't want it to print + * twice. + */ + CPrintToChatEx(client, client, "%s%s", tag, szBuffer); + } + else + { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CRemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s\n", tag, szBuffer); + } + + if (!value) + { + return 1; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) + || IsFakeClient(i) + || i == client) + { + continue; + } + new AdminId:id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) + { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) + { + new String:newsign[MAX_NAME_LENGTH]; + newsign = sign; + if ((value & 2)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else + { + /* Treat this as an admin user */ + new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) + || (value & 8) + || ((value & 16) && is_root)) + { + new String:newsign[MAX_NAME_LENGTH]; + newsign = sign; + if ((value & 8) || ((value & 16) && is_root)) + { + newsign = name; + } + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/distbugfix.inc b/sourcemod/scripting/include/distbugfix.inc new file mode 100644 index 0000000..6a7ed18 --- /dev/null +++ b/sourcemod/scripting/include/distbugfix.inc @@ -0,0 +1,261 @@ + +#if defined _distbug_included + #endinput +#endif +#define _distbug_included + +#define DISTBUG_CONFIG_NAME "distbugfix" + +#define CHAT_PREFIX "{d}[{l}GC{d}]" +#define CONSOLE_PREFIX "[GC]" +#define CHAT_SPACER " {d}|{g} " +#define DISTBUG_VERSION "2.0.0" + +#define MAX_STRAFES 32 +#define MAX_EDGE 32.0 +#define MAX_JUMP_FRAMES 150 // for frame based arrays +#define MAX_BHOP_FRAMES 8 + +#define MAX_COOKIE_SIZE 32 + +enum +{ + SETTINGS_DISTBUG_ENABLED = (1 << 0), + SETTINGS_SHOW_VEER_BEAM = (1 << 1), + SETTINGS_SHOW_JUMP_BEAM = (1 << 2), + SETTINGS_SHOW_HUD_GRAPH = (1 << 3), + SETTINGS_DISABLE_STRAFE_STATS = (1 << 4), + SETTINGS_DISABLE_STRAFE_GRAPH = (1 << 5), + SETTINGS_ADV_CHAT_STATS = (1 << 6), +} + +enum JumpType +{ + // unprintable jumptypes. only for tracking + JUMPTYPE_NONE, + + JUMPTYPE_LJ, // longjump + JUMPTYPE_WJ, // weirdjump + JUMPTYPE_LAJ, // ladderjump + JUMPTYPE_BH, // bunnyhop + JUMPTYPE_CBH, // crouched bunnyhop +}; + +enum JumpDir +{ + JUMPDIR_FORWARDS, + JUMPDIR_BACKWARDS, + JUMPDIR_LEFT, + JUMPDIR_RIGHT, +}; + +enum StrafeType +{ + STRAFETYPE_OVERLAP, // IN_MOVELEFT and IN_MOVERIGHT are overlapping and sidespeed is 0 + STRAFETYPE_NONE, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is 0 + + STRAFETYPE_LEFT, // only IN_MOVELEFT is down and sidespeed isn't 0. + STRAFETYPE_OVERLAP_LEFT, // IN_MOVELEFT and IN_MOVERIGHT are overlapping, but sidespeed is smaller than 0 (not 0) + STRAFETYPE_NONE_LEFT, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is smaller than 0 (not 0) + + STRAFETYPE_RIGHT, // only IN_MOVERIGHT is down and sidespeed isn't 0. + STRAFETYPE_OVERLAP_RIGHT, // IN_MOVELEFT and IN_MOVERIGHT are overlapping, but sidespeed is bigger than 0 (not 0) + STRAFETYPE_NONE_RIGHT, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is bigger than 0 (not 0) +}; + +enum JumpBeamColour +{ + JUMPBEAM_NEUTRAL, // speed stays the same + JUMPBEAM_LOSS, // speed loss + JUMPBEAM_GAIN, // speed gain + JUMPBEAM_DUCK, // duck key down +}; + +enum struct PlayerData +{ + int tickCount; + int buttons; + int lastButtons; + int flags; + int lastFlags; + int framesOnGround; + int framesInAir; + MoveType movetype; + MoveType lastMovetype; + float stamina; + float lastStamina; + float forwardmove; + float lastForwardmove; + float sidemove; + float lastSidemove; + float gravity; + float angles[3]; + float lastAngles[3]; + float position[3]; + float lastPosition[3]; + float velocity[3]; + float lastVelocity[3]; + float lastGroundPos[3]; // last position where the player left the ground. + float ladderNormal[3]; + bool lastGroundPosWalkedOff; + bool landedDucked; + + int prespeedFog; + float prespeedStamina; + + float jumpGroundZ; + float jumpPos[3]; + float jumpAngles[3]; + float landGroundZ; + float landPos[3]; + + int fwdReleaseFrame; + int jumpFrame; + bool trackingJump; + bool failedJump; + bool jumpGotFailstats; + + // jump data + JumpType jumpType; + JumpType lastJumpType; + JumpDir jumpDir; + float jumpDistance; + float jumpPrespeed; + float jumpMaxspeed; + float jumpVeer; + float jumpAirpath; + float jumpSync; + float jumpEdge; + float jumpLandEdge; + float jumpBlockDist; + float jumpHeight; + float jumpJumpoffAngle; + int jumpAirtime; + int jumpFwdRelease; + int jumpOverlap; + int jumpDeadair; + + // strafes! + int strafeCount; + float strafeSync[MAX_STRAFES]; + float strafeGain[MAX_STRAFES]; + float strafeLoss[MAX_STRAFES]; + float strafeMax[MAX_STRAFES]; + int strafeAirtime[MAX_STRAFES]; + int strafeOverlap[MAX_STRAFES]; + int strafeDeadair[MAX_STRAFES]; + float strafeAvgGain[MAX_STRAFES]; + + float strafeAvgEfficiency[MAX_STRAFES]; + int strafeAvgEfficiencyCount[MAX_STRAFES]; // how many samples are in strafeAvgEfficiency + float strafeMaxEfficiency[MAX_STRAFES]; + + StrafeType strafeGraph[MAX_JUMP_FRAMES]; + float mouseGraph[MAX_JUMP_FRAMES]; + float jumpBeamX[MAX_JUMP_FRAMES]; + float jumpBeamY[MAX_JUMP_FRAMES]; + JumpBeamColour jumpBeamColour[MAX_JUMP_FRAMES]; +} + +/** + * Check if player is overlapping their MOVERIGHT and MOVELEFT buttons. + * + * @param x Buttons; + * @return True if overlapping, false otherwise. + */ +stock bool IsOverlapping(int buttons, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS || jumpDir == JUMPDIR_BACKWARDS) + { + return (buttons & IN_MOVERIGHT) && (buttons & IN_MOVELEFT); + } + // else if (jumpDir == JUMPDIR_LEFT || jumpDir == JUMPDIR_RIGHT) + return (buttons & IN_FORWARD) && (buttons & IN_BACK); +} + +/** + * Checks if the player is not holding down their MOVERIGHT and MOVELEFT buttons. + * + * @param x Buttons. + * @return True if they're not holding either, false otherwise. + */ +stock bool IsDeadAirtime(int buttons, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS || jumpDir == JUMPDIR_BACKWARDS) + { + return (!(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT)); + } + // else if (jumpDir == JUMPDIR_LEFT || jumpDir == JUMPDIR_RIGHT) + return (!(buttons & IN_FORWARD) && !(buttons & IN_BACK)); +} + +stock void ToggleCVar(ConVar cvar) +{ + cvar.BoolValue = !cvar.BoolValue; +} + +stock void GetRealLandingOrigin(float landGroundZ, const float origin[3], const float velocity[3], float result[3]) +{ + if ((origin[2] - landGroundZ) == 0.0) + { + result = origin; + return; + } + + // this is like this because it works + float frametime = GetTickInterval(); + float verticalDistance = origin[2] - (origin[2] + velocity[2] * frametime); + float fraction = (origin[2] - landGroundZ) / verticalDistance; + + float addDistance[3]; + addDistance = velocity; + ScaleVector(addDistance, frametime * fraction); + + AddVectors(origin, addDistance, result); +} + +stock int FloatSign(float value) +{ + if (value > 0.0) + { + return 1; + } + else if (value < 0.0) + { + return -1; + } + return 0; +} + +stock int FloatSign2(float value) +{ + if (value >= 0.0) + { + return 1; + } + // else if (value < 0.0) + return -1; +} + +stock void ShowPanel(int client, int duration, const char[] message) +{ + Event show_survival_respawn_status = CreateEvent("show_survival_respawn_status"); + if (show_survival_respawn_status == INVALID_HANDLE) + { + return; + } + + show_survival_respawn_status.SetString("loc_token", message); + show_survival_respawn_status.SetInt("duration", duration); + show_survival_respawn_status.SetInt("userid", -1); + + if (client == -1) + { + show_survival_respawn_status.Fire(); + } + else + { + show_survival_respawn_status.FireToClient(client); + show_survival_respawn_status.Cancel(); + } +} diff --git a/sourcemod/scripting/include/gamechaos.inc b/sourcemod/scripting/include/gamechaos.inc new file mode 100644 index 0000000..eeaf040 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos.inc @@ -0,0 +1,20 @@ + +// GameChaos's includes for various specific stuff + +#if defined _gamechaos_stocks_included + #endinput +#endif +#define _gamechaos_stocks_included + +#define GC_INCLUDES_VERSION 0x01_00_00 +#define GC_INCLUDES_VERSION_STRING "1.0.0" + +#include <gamechaos/arrays> +#include <gamechaos/client> +#include <gamechaos/debug> +#include <gamechaos/maths> +#include <gamechaos/misc> +#include <gamechaos/strings> +#include <gamechaos/tempents> +#include <gamechaos/tracing> +#include <gamechaos/vectors>
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/arrays.inc b/sourcemod/scripting/include/gamechaos/arrays.inc new file mode 100644 index 0000000..eba62bb --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/arrays.inc @@ -0,0 +1,52 @@ + +#if defined _gamechaos_stocks_arrays_included + #endinput +#endif +#define _gamechaos_stocks_arrays_included + +#define GC_ARRAYS_VERSION 0x01_00_00 +#define GC_ARRAYS_VERSION_STRING "1.0.0" + +/** + * Copies an array into an arraylist. + * + * @param array Arraylist Handle. + * @param index Index in the arraylist. + * @param values Array to copy. + * @param size Size of the array to copy. + * @param offset Arraylist offset to set. + * @return Number of cells copied. + * @error Invalid Handle or invalid index. + */ +stock int GCSetArrayArrayIndexOffset(ArrayList array, int index, const any[] values, int size, int offset) +{ + int cells; + for (int i; i < size; i++) + { + array.Set(index, values[i], offset + i); + cells++; + } + return cells; +} + +/** + * Copies an arraylist's specified cells to an array. + * + * @param array Arraylist Handle. + * @param index Index in the arraylist. + * @param result Array to copy to. + * @param size Size of the array to copy to. + * @param offset Arraylist offset. + * @return Number of cells copied. + * @error Invalid Handle or invalid index. + */ +stock int GCCopyArrayArrayIndex(const ArrayList array, int index, any[] result, int size, int offset) +{ + int cells; + for (int i = offset; i < (size + offset); i++) + { + result[i] = array.Get(index, i); + cells++; + } + return cells; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/client.inc b/sourcemod/scripting/include/gamechaos/client.inc new file mode 100644 index 0000000..cb2114a --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/client.inc @@ -0,0 +1,300 @@ + +#if defined _gamechaos_stocks_client_included + #endinput +#endif +#define _gamechaos_stocks_client_included + +#define GC_CLIENT_VERSION 0x01_00_00 +#define GC_CLIENT_VERSION_STRING "1.0.0" + +/** + * Credit: Don't remember. + * Removes a player's weapon from the specified slot. + * + * @param client Client index. + * @param slot Weapon slot. + * @return True if removed, false otherwise. + */ +stock bool GCRemoveWeaponBySlot(int client, int slot) +{ + int entity = GetPlayerWeaponSlot(client, slot); + if (IsValidEdict(entity)) + { + RemovePlayerItem(client, entity); + AcceptEntityInput(entity, "kill"); + return true; + } + return false; +} + +/** + * Checks if a client is valid and not the server and optionally, whether he's alive. + * + * @param client Client index. + * @param alive Whether to check alive. + * @return True if valid, false otherwise. + */ +stock bool GCIsValidClient(int client, bool alive = false) +{ + return (client >= 1 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client) && !IsClientSourceTV(client) && (!alive || IsPlayerAlive(client))); +} + + + +/** + * Gets the value of m_flForwardMove. + * + * @param client Client index. + * @return Value of m_flForwardMove. + */ +stock float GCGetClientForwardMove(int client) +{ + return GetEntPropFloat(client, Prop_Data, "m_flForwardMove"); +} + +/** + * Gets the value of m_flSideMove. + * + * @param client Client index. + * @return Value of m_flSideMove. + */ +stock float GCGetClientSideMove(int client) +{ + return GetEntPropFloat(client, Prop_Data, "m_flSideMove"); +} + +/** + * Gets the client's abs origin. + * + * @param client Client index. + * @return result Player's origin. + */ +stock float[] GCGetClientAbsOriginRet(int client) +{ + float result[3] + GetClientAbsOrigin(client, result); + return result; +} + +/** + * Copies the client's velocity to a vector. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void GCGetClientVelocity(int client, float result[3]) +{ + GetEntPropVector(client, Prop_Data, "m_vecVelocity", result); +} + +/** + * Gets the client's velocity (m_vecVelocity). + * + * @param client Client index. + * @return result m_vecVelocity. + */ +stock float[] GCGetClientVelocityRet(int client) +{ + float result[3] + GetEntPropVector(client, Prop_Data, "m_vecVelocity", result); + return result +} + +/** + * Copies the client's basevelocity to a vector. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void GCGetClientBaseVelocity(int client, float result[3]) +{ + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result); +} + +/** + * Gets the client's basevelocity (m_vecBaseVelocity). + * + * @param client Client index. + * @return result m_vecBaseVelocity. + */ +stock float[] GCGetClientBaseVelocityRet(int client) +{ + float result[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result); + return result; +} + + +/** + * Gets the client's "m_flDuckSpeed" value. + * + * @param client Client index. + * @return "m_flDuckSpeed". + */ +stock float GCGetClientDuckSpeed(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flDuckSpeed"); +} + +/** + * Gets the client's "m_flDuckAmount" value. + * + * @param client Client index. + * @return "m_flDuckAmount". + */ +stock float GCGetClientDuckAmount(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flDuckAmount"); +} + +/** + * Gets the client's "m_bDucking" value. + * + * @param client Client index. + * @return "m_bDucking". + */ +stock int GCGetClientDucking(int client) +{ + return GetEntProp(client, Prop_Data, "m_bDucking"); +} + +/** + * Gets the client's "m_flMaxspeed" value. + * + * @param client Client index. + * @return "m_flMaxspeed". + */ +stock float GCGetClientMaxspeed(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flMaxspeed"); +} + +/** + * Gets the client's "m_afButtonPressed" value. + * + * @param client Client index. + * @return "m_afButtonPressed". + */ +stock int GCGetClientButtonPressed(int client) +{ + return GetEntProp(client, Prop_Data, "m_afButtonPressed"); +} + +/** + * Gets the client's "m_afButtonReleased" value. + * + * @param client Client index. + * @return "m_afButtonReleased". + */ +stock int GCGetClientButtonReleased(int client) +{ + return GetEntProp(client, Prop_Data, "m_afButtonReleased"); +} + +/** + * Gets the client's "m_afButtonLast" value. + * + * @param client Client index. + * @return "m_afButtonLast". + */ +stock int GCGetClientButtonLast(int client) +{ + return GetEntProp(client, Prop_Data, "m_afButtonLast"); +} + +/** + * Gets the client's "m_afButtonForced" value. + * + * @param client Client index. + * @return "m_afButtonForced". + */ +stock int GCGetClientForcedButtons(int client) +{ + return GetEntProp(client, Prop_Data, "m_afButtonForced"); +} + +/** + * Gets the client's "m_flStamina" value. + * + * @param client Client index. + * @return "m_flStamina". + */ +stock float GCGetClientStamina(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flStamina"); +} + + + +/** + * Sets the client's origin. + * + * @param client Client index. + * @param origin New origin. + */ +stock void GCSetClientAbsOrigin(int client, const float origin[3]) +{ + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); +} + +/** + * Sets the client's velocity. + * + * @param client Client index. + * @param velocity New velocity. + */ +stock void GCSetClientVelocity(int client, const float velocity[3]) +{ + SetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity); +} + +/** + * Sets the client's "m_vecAbsVelocity". + * + * @param client Client index. + * @param velocity New "m_vecAbsVelocity". + */ +stock void GCSetClientAbsVelocity(int client, const float velocity[3]) +{ + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", velocity); +} + +/** + * Sets the client's eye angles. + * Ang has to be a 2 member array or more + * + * @param client Client index. + * @param ang New eyeangles. + */ +stock void GCSetClientEyeAngles(int client, const float[] ang) +{ + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[0]", ang[0]); + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[1]", ang[1]); +} + + +/** + * Sets the client's "m_flDuckSpeed". + * + * @param client Client index. + * @param value New "m_flDuckSpeed". + */ +stock void GCSetClientDuckSpeed(int client, float value) +{ + SetEntPropFloat(client, Prop_Send, "m_flDuckSpeed", value); +} + +stock void GCSetClientDuckAmount(int client, float value) +{ + SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", value); +} + +stock void GCSetClientForcedButtons(int client, int buttons) +{ + SetEntProp(client, Prop_Data, "m_afButtonForced", buttons); +} + +stock void GCSetClientStamina(int client, float stamina) +{ + SetEntPropFloat(client, Prop_Send, "m_flStamina", stamina) +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/debug.inc b/sourcemod/scripting/include/gamechaos/debug.inc new file mode 100644 index 0000000..4e5b7e7 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/debug.inc @@ -0,0 +1,19 @@ + +// gamechaos's debug stocks +// useful stocks for debugging + +#if defined _gamechaos_debug_included + #endinput +#endif +#define _gamechaos_debug_included + +#define GC_DEBUG_VERSION 0x1_00_00 +#define GC_DEBUG_VERSION_STRING "1.0.0" + +#if defined GC_DEBUG + #define GC_ASSERT(%1) if (!(%1))SetFailState("Assertion failed: \""...#%1..."\"") + #define GC_DEBUGPRINT(%1) PrintToChatAll(%1) +#else + #define GC_ASSERT(%1)%2; + #define GC_DEBUGPRINT(%1)%2; +#endif diff --git a/sourcemod/scripting/include/gamechaos/isvalidclient.inc b/sourcemod/scripting/include/gamechaos/isvalidclient.inc new file mode 100644 index 0000000..bf80246 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/isvalidclient.inc @@ -0,0 +1,16 @@ + +#if defined _gamechaos_isvalidclient_client_included + #endinput +#endif +#define _gamechaos_isvalidclient_client_included + +/** + * Checks if a client is valid. + * + * @param client Client index. + * @return True if valid, false otherwise. + */ +stock bool IsValidClient(int client) +{ + return (client >= 0 && client <= MaxClients && IsValidEntity(client) && IsClientConnected(client) && IsClientInGame(client)); +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc b/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc new file mode 100644 index 0000000..0cbb828 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc @@ -0,0 +1,226 @@ +// +// Useful things for making plugins for Kreedz Climbing +// + +#if defined _gamechaos_kreedzclimbing_included + #endinput +#endif +#define _gamechaos_kreedzclimbing_included + +#define GC_KREEDZCLIMBING_VERSION 0x01_00_00 +#define GC_KREEDZCLIMBING_VERSION_STRING "1.0.0" + + +#define MAX_COURSE_SIZE 128 // Reasonable maximum characters a course name can have +#define COURSE_CVAR_COUNT 20 // the amount of Course<int> cvars + +// Kreedz Climbing Client Commands: +// These may be executed by a player via the console, with / in chat, or via binds. + +// specmode - Cycles spectator mode (F3 by default). +// kz_pause - Pauses the timer. +// flare - Fires a flare. +// gototimer | start - Returns to the last pressed start timer. +// spectate | spec - Enters spectator mode. +// forcespectator - Becomes a spectator no matter what (force respawns a dead player as well). +// stoptimer - Instantly stops the player's timer. +// climb | ct - Respawns at the map spawnpoint. +// InvalidateTimer - Invalidates the player's timer. An invalid timer can't earn rewards for completing the course. InvalidateTimer 1 displays the message, without the 1 it does not. + +// Kreedz Climbing Constants + +// Timer state (player->m_Local->Timer_Active) +#define TIMER_STATE_INVISIBLE 0 +#define TIMER_STATE_ACTIVE 1 +#define TIMER_STATE_INACTIVE 2 +#define TIMER_STATE_PAUSED 3 + +// Timer flags (player_manager->m_iTimerFlags[32]) +// These are replicated flags for player's timer (most timer data is local to it's own player). +// Note that these flags are mirrors of data local to the player - they are set to the player's +// state every frame and cannot be changed. + +#define TIMER_FLAG_INVALID (1 << 0) +#define TIMER_FLAG_ACTIVE (1 << 1) // We need to broadcast this because Timer_State is local only. +#define TIMER_FLAG_PAUSED (1 << 2) // A paused timer cannot be active and vice versa. + +// Environmental Attributes (player->m_iEnvironmentalAttributes) +#define PLAYER_ENV_ATTRIBUTES_BHOP (1 << 0) +#define PLAYER_ENV_ATTRIBUTES_SURF (1 << 1) +#define PLAYER_ENV_ATTRIBUTES_AUTOBHOP (1 << 2) +#define PLAYER_ENV_ATTRIBUTES_CSGOMOVEMENT (1 << 3) +#define PLAYER_ENV_ATTRIBUTES_CSGODUCKHULL (1 << 4) + +// Movement restriction flags (player->m_iMovementRestrictions) (new version of Environmental Restrictions below) +#define PLAYER_MOVEMENT_RESTRICTION_NOJUMP (1 << 0) +#define PLAYER_MOVEMENT_RESTRICTION_NOBHOP (1 << 1) +#define PLAYER_MOVEMENT_RESTRICTION_NODOUBLEDUCK (1 << 2) + +// OBSOLETE: ONLY IN OLD MAPS: Environmental Restrictions (player->m_iEnvironmentalRestrictions), note not flags, complete integer. +#define PLAYER_ENV_RESTRICTION_NOJUMP 1 +#define PLAYER_ENV_RESTRICTION_NOBHOP 2 +#define PLAYER_ENV_RESTRICTION_BOTH 3 + +// Cooperative status (player->m_Local.m_multiplayercoursedata.Player1Status, Player2Status etc) +#define COOPERATIVE_STATUS_NONE 0 +#define COOPERATIVE_STATUS_WAITING 1 +#define COOPERATIVE_STATUS_READY 2 +#define COOPERATIVE_STATUS_TIMER_ACTIVE 3 +#define COOPERATIVE_STATUS_TIMER_COMPLETE 4 +#define COOPERATIVE_STATUS_PLAYER_DISCONNECTED 5 // Player disconnected from server, waiting for them to reconnect. +#define COOPERATIVE_STATUS_TIMER_PAUSED 6 + +// Kreedz Climbing Button Constants +#define IN_CHECKPOINT (1 << 25) +#define IN_TELEPORT (1 << 26) +#define IN_SPECTATE (1 << 27) +//#define IN_AVAILABLE (1 << 28) // Unused +#define IN_HOOK (1 << 29) + +// converts the course id from the obsolete "player_starttimer" event into the course name +stock void GCCourseidToString(int courseid, char[] course, int size) +{ + char szCourseid[16]; + if (courseid < 1 || courseid > COURSE_CVAR_COUNT) + { + return; + } + FormatEx(szCourseid, sizeof(szCourseid), "Course%i", courseid); + FindConVar(szCourseid).GetString(course, size); +} + +stock void GCGetCurrentMapCourses(ArrayList &array) +{ + if (array == null) + { + // 1 for endurance bool + array = new ArrayList(ByteCountToCells(MAX_COURSE_SIZE) + 1); + } + else + { + array.Clear(); + } + + char course[MAX_COURSE_SIZE]; + + int ent; + while((ent = FindEntityByClassname(ent, "func_stoptimer")) != -1) + { + int courseid = GetEntProp(ent, Prop_Data, "CourseID"); + GCCourseidToString(courseid, course, sizeof(course)); + array.PushString(course); + + bool endurance = GCIsCourseEndurance(course, ent); + array.Set(array.Length - 1, endurance, ByteCountToCells(MAX_COURSE_SIZE)); + } + + int courseStringtableCount; + int courseNamesIdx = FindStringTable("CourseNames"); + courseStringtableCount = GetStringTableNumStrings(courseNamesIdx); + + for (int i; i < courseStringtableCount; i++) + { + ReadStringTable(courseNamesIdx, i, course, sizeof(course)); + array.PushString(course); + + bool endurance = GCIsCourseEndurance(course, ent); + array.Set(array.Length - 1, endurance, ByteCountToCells(MAX_COURSE_SIZE)); + } +} + +stock int GCGetTimerState(int client) +{ + return GetEntProp(client, Prop_Send, "Timer_Active"); +} + +stock void GCSetTimerState(int client, int timerstate) +{ + SetEntProp(client, Prop_Send, "Timer_Active", timerstate); +} + +stock int GCGetPlayerEnvAttributes(int client) +{ + return GetEntProp(client, Prop_Send, "m_iEnvironmentalAttributes"); +} + +stock void GCSetPlayerEnvAttributes(int client, int attributes) +{ + SetEntProp(client, Prop_Send, "m_iEnvironmentalAttributes", attributes); +} + +stock int GCGetPlayerMovementRestrictions(int client) +{ + return GetEntProp(client, Prop_Send, "m_iMovementRestrictions"); +} + +stock void GCSetPlayerMovementRestrictions(int client, int restrictions) +{ + SetEntProp(client, Prop_Send, "m_iMovementRestrictions", restrictions); +} + +stock void GCSetActiveCourse(int client, int course) +{ + int ent = FindEntityByClassname(0, "player_manager"); + int courseOffset = FindSendPropInfo("CPlayerResource", "m_iActiveCourse"); + SetEntData(ent, courseOffset + (client * 4), course); +} + +stock int GCGetTimerFlags(int client) +{ + int ent = FindEntityByClassname(0, "player_manager"); + int courseOffset = FindSendPropInfo("CPlayerResource", "m_iTimerFlags"); + return GetEntData(ent, courseOffset + (client * 4)); +} + +stock bool GCInvalidateTimer(int client) +{ + if (~GCGetTimerFlags(client) & TIMER_FLAG_INVALID) + { + FakeClientCommand(client, "InvalidateTimer 1"); + return true; + } + + return false; +} + +stock bool GCIsCourseEndurance(char[] course, int ent = -1) +{ + if (ent != -1) + { + if (IsValidEntity(ent)) + { + return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse")); + } + } + + while ((ent = FindEntityByClassname(ent, "point_climbtimer")) != -1) + { + if (IsValidEntity(ent)) + { + char buffer[MAX_COURSE_SIZE]; + GetEntPropString(ent, Prop_Data, "m_strCourseName", buffer, sizeof(buffer)); + + if (StrEqual(buffer, course)) + { + return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse")); + } + } + } + + while ((ent = FindEntityByClassname(ent, "func_stoptimer")) != -1) + { + if (IsValidEntity(ent)) + { + char buffer[MAX_COURSE_SIZE]; + int courseid = GetEntProp(ent, Prop_Data, "CourseID"); + GCCourseidToString(courseid, buffer, sizeof(buffer)); + + if (StrEqual(buffer, course)) + { + return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse")); + } + } + } + + return false; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/maths.inc b/sourcemod/scripting/include/gamechaos/maths.inc new file mode 100644 index 0000000..f3c94af --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/maths.inc @@ -0,0 +1,362 @@ + +#if defined _gamechaos_stocks_maths_included + #endinput +#endif +#define _gamechaos_stocks_maths_included + +#define GC_MATHS_VERSION 0x02_00_00 +#define GC_MATHS_VERSION_STRING "2.0.0" + +#include <gamechaos/vectors> + +#define GC_PI 3.14159265359 + +#define GC_DEGREES(%1) ((%1) * 180.0 / GC_PI) // convert radians to degrees +#define GC_RADIANS(%1) ((%1) * GC_PI / 180.0) // convert degrees to radians + +#define GC_FLOAT_NAN view_as<float>(0xffffffff) +#define GC_FLOAT_INFINITY view_as<float>(0x7f800000) +#define GC_FLOAT_NEGATIVE_INFINITY view_as<float>(0xff800000) + +#define GC_FLOAT_LARGEST_POSITIVE view_as<float>(0x7f7fffff) +#define GC_FLOAT_SMALLEST_NEGATIVE view_as<float>(0xff7fffff) + +#define GC_FLOAT_SMALLEST_POSITIVE view_as<float>(0x00000001) +#define GC_FLOAT_LARGEST_NEGATIVE view_as<float>(0x80000001) + +#define GC_INT_MAX 0x7fffffff +#define GC_INT_MIN 0xffffffff + + +/** + * Credit: https://stackoverflow.com/questions/5666222/3d-line-plane-intersection + * Determines the point of intersection between a plane defined by a point and a normal vector and a line defined by a point and a direction vector. + * + * @param planePoint A point on the plane. + * @param planeNormal Normal vector of the plane. + * @param linePoint A point on the line. + * @param lineDirection Direction vector of the line. + * @param result Resultant vector. + */ +stock void GCLineIntersection(const float planePoint[3], const float planeNormal[3], const float linePoint[3], const float lineDirection[3], float result[3]) +{ + if (GetVectorDotProduct(planeNormal, lineDirection) == 0) + { + return; + } + + float t = (GetVectorDotProduct(planeNormal, planePoint) + - GetVectorDotProduct(planeNormal, linePoint)) + / GetVectorDotProduct(planeNormal, lineDirection); + + float lineDir[3]; + lineDir = lineDirection; + NormalizeVector(lineDir, lineDir); + + ScaleVector(lineDir, t); + + AddVectors(linePoint, lineDir, result); +} + +/** + * Calculates a point according to angles supplied that is a certain distance away. + * + * @param client Client index. + * @param result Resultant vector. + * @param distance Maximum distance to trace. + * @return True on success, false otherwise. + */ +stock void GCCalcPointAngleDistance(const float start[3], const float angle[3], float distance, float result[3]) +{ + float zsine = Sine(DegToRad(-angle[0])); + float zcos = Cosine(DegToRad(-angle[0])); + + result[0] = Cosine(DegToRad(angle[1])) * zcos; + result[1] = Sine(DegToRad(angle[1])) * zcos; + result[2] = zsine; + + ScaleVector(result, distance); + AddVectors(start, result, result); +} + +/** + * Compares how close 2 floats are. + * + * @param z1 Float 1 + * @param z2 Float 2 + * @param tolerance How close the floats have to be to return true. + * @return True on success, false otherwise. + */ +stock bool GCIsRoughlyEqual(float z1, float z2, float tolerance) +{ + return FloatAbs(z1 - z2) < tolerance; +} + +/** + * Checks if a float is within a range + * + * @param number Float to check. + * @param min Minimum range. + * @param max Maximum range. + * @return True on success, false otherwise. + */ +stock bool GCIsFloatInRange(float number, float min, float max) +{ + return number >= min && number <= max; +} + +/** + * Keeps the yaw angle within the range of -180 to 180. + * + * @param angle Angle. + * @return Normalised angle. + */ +stock float GCNormaliseYaw(float angle) +{ + if (angle <= -180.0) + { + angle += 360.0; + } + + if (angle > 180.0) + { + angle -= 360.0; + } + + return angle; +} + +/** + * Keeps the yaw angle within the range of -180 to 180. + * + * @param angle Angle. + * @return Normalised angle. + */ +stock float GCNormaliseYawRad(float angle) +{ + if (angle <= -FLOAT_PI) + { + angle += FLOAT_PI * 2; + } + + if (angle > FLOAT_PI) + { + angle -= FLOAT_PI * 2; + } + + return angle; +} + +/** + * Linearly interpolates between 2 values. + * + * @param f1 Float 1. + * @param f2 Float 2. + * @param fraction Amount to interpolate. + * @return Interpolated value. + */ +stock float GCInterpLinear(float f1, float f2, float fraction) +{ + float diff = f2 - f1; + + return diff * fraction + f1; +} + +/** + * Calculates the linear fraction from a value that was interpolated and 2 values it was interpolated from. + * + * @param f1 Float 1. + * @param f2 Float 2. + * @param fraction Interpolated value. + * @return Fraction. + */ +stock float GCCalcLerpFraction(float f1, float f2, float lerped) +{ + float diff = f2 - f1; + + float fraction = lerped - f1 / diff; + return fraction; +} + +/** + * Calculate absolute value of an integer. + * + * @param x Integer. + * @return Absolute value of integer. + */ +stock int GCIntAbs(int x) +{ + return x >= 0 ? x : -x; +} + +/** + * Get the maximum of 2 integers. + * + * @param n1 Integer. + * @param n2 Integer. + * @return The biggest of n1 and n2. + */ +stock int GCIntMax(int n1, int n2) +{ + return n1 > n2 ? n1 : n2; +} + +/** + * Get the minimum of 2 integers. + * + * @param n1 Integer. + * @param n2 Integer. + * @return The smallest of n1 and n2. + */ +stock int GCIntMin(int n1, int n2) +{ + return n1 < n2 ? n1 : n2; +} + +/** + * Checks if an integer is within a range + * + * @param number Integer to check. + * @param min Minimum range. + * @param max Maximum range. + * @return True on success, false otherwise. + */ +stock bool GCIsIntInRange(int number, int min, int max) +{ + return number >= min && number <= max; +} + +/** + * Calculates a float percentage from a common fraction. + * + * @param numerator Numerator. + * @param denominator Denominator. + * @return Float percentage. -1.0 on failure. + */ +stock float GCCalcIntPercentage(int numerator, int denominator) +{ + return float(numerator) / float(denominator) * 100.0; +} + +/** + * Integer power. + * Returns the base raised to the power of the exponent. + * Returns 0 if exponent is negative. + * + * @param base Base to be raised. + * @param exponent Value to raise the base. + * @return Value to the power of exponent. + */ +stock int GCIntPow(int base, int exponent) +{ + if (exponent < 0) + { + return 0; + } + + int result = 1; + for (;;) + { + if (exponent & 1) + { + result *= base + } + + exponent >>= 1; + + if (!exponent) + { + break; + } + + base *= base; + } + return result; +} + +/** + * Swaps the values of 2 variables. + * + * @param cell1 Cell 1. + * @param cell2 Cell 2. + */ +stock void GCSwapCells(any &cell1, any &cell2) +{ + any temp = cell1; + cell1 = cell2; + cell2 = temp; +} + +/** + * Clamps an int between min and max. + * + * @param value Float to clamp. + * @param min Minimum range. + * @param max Maximum range. + * @return Clamped value. + */ +stock int GCIntClamp(int value, int min, int max) +{ + if (value < min) + { + return min; + } + if (value > max) + { + return max; + } + return value; +} + +/** + * Returns the biggest of 2 values. + * + * @param num1 Number 1. + * @param num2 Number 2. + * @return Biggest number. + */ +stock float GCFloatMax(float num1, float num2) +{ + if (num1 > num2) + { + return num1; + } + return num2; +} + +/** + * Returns the smallest of 2 values. + * + * @param num1 Number 1. + * @param num2 Number 2. + * @return Smallest number. + */ +stock float GCFloatMin(float num1, float num2) +{ + if (num1 < num2) + { + return num1; + } + return num2; +} + +/** + * Clamps a float between min and max. + * + * @param value Float to clamp. + * @param min Minimum range. + * @param max Maximum range. + * @return Clamped value. + */ +stock float GCFloatClamp(float value, float min, float max) +{ + if (value < min) + { + return min; + } + if (value > max) + { + return max; + } + return value; +} diff --git a/sourcemod/scripting/include/gamechaos/misc.inc b/sourcemod/scripting/include/gamechaos/misc.inc new file mode 100644 index 0000000..f964862 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/misc.inc @@ -0,0 +1,245 @@ + +#if defined _gamechaos_stocks_misc_included + #endinput +#endif +#define _gamechaos_stocks_misc_included + +#define GC_MISC_VERSION 0x01_00_00 +#define GC_MISC_VERSION_STRING "1.0.0" + +/** + * Check if player is overlapping their MOVERIGHT and MOVELEFT buttons. + * + * @param x Buttons; + * @return True if overlapping, false otherwise. + */ +stock bool GCIsOverlapping(int buttons) +{ + return buttons & IN_MOVERIGHT && buttons & IN_MOVELEFT +} + +/** + * Checks if player gained speed. + * + * @param speed Current player speed. + * @param lastspeed Player speed from previous tick. + * @return True if player gained speed, false otherwise. + */ +stock bool GCIsStrafeSynced(float speed, float lastspeed) +{ + return speed > lastspeed; +} + +/** + * Checks if the player is not holding down their MOVERIGHT and MOVELEFT buttons. + * + * @param x Buttons. + * @return True if they're not holding either, false otherwise. + */ +stock bool GCIsDeadAirtime(int buttons) +{ + return !(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT); +} + +/** +* Source: https://forums.alliedmods.net/showthread.php?p=2535972 +* Runs a single line of vscript code. +* NOTE: Dont use the "script" console command, it startes a new instance and leaks memory. Use this instead! +* +* @param code The code to run. +* @noreturn +*/ +stock void GCRunScriptCode(const char[] code, any ...) +{ + static int scriptLogic = INVALID_ENT_REFERENCE; + + if (scriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(scriptLogic)) + { + scriptLogic = EntIndexToEntRef(CreateEntityByName("logic_script")); + if (scriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(scriptLogic)) + { + SetFailState("Could not create a 'logic_script' entity."); + } + + DispatchSpawn(scriptLogic); + } + + char buffer[512]; + VFormat(buffer, sizeof(buffer), code, 2); + + SetVariantString(buffer); + AcceptEntityInput(scriptLogic, "RunScriptCode"); +} + +stock void GCTE_SendBeamBox(int client, + const float origin[3], + const float mins[3], + const float maxs[3], + int ModelIndex, + int HaloIndex = 0, + float Life = 3.0, + float Width = 2.0, + const int Colour[4] = { 255, 255, 255, 255 }, + float EndWidth = 2.0, + int StartFrame = 0, + int FrameRate = 0, + int FadeLength = 0, + float Amplitude = 0.0, + int Speed = 0) +{ + // credit to some bhop timer by shavit? thanks + int pairs[8][3] = { { 0, 0, 0 }, { 1, 0, 0 }, { 1, 1, 0 }, { 0, 1, 0 }, { 0, 0, 1 }, { 1, 0, 1 }, { 1, 1, 1 }, { 0, 1, 1 } }; + int edges[12][2] = { { 0, 1 }, { 0, 3 }, { 0, 4 }, { 2, 1 }, { 2, 3 }, { 2, 6 }, { 5, 4 }, { 5, 6 }, { 5, 1 }, { 7, 4 }, { 7, 6 }, { 7, 3 } }; + + float corners[8][3]; + float corner[2][3]; + + AddVectors(origin, mins, corner[0]); + AddVectors(origin, maxs, corner[1]); + + for (int i = 0; i < 8; i++) + { + corners[i][0] = corner[pairs[i][0]][0]; + corners[i][1] = corner[pairs[i][1]][1]; + corners[i][2] = corner[pairs[i][2]][2]; + } + + for (int i = 0; i < 12; i++) + { + TE_SetupBeamPoints(corners[edges[i][0]], + corners[edges[i][1]], + ModelIndex, + HaloIndex, + StartFrame, + FrameRate, + Life, + Width, + EndWidth, + FadeLength, + Amplitude, + Colour, + Speed); + TE_SendToClient(client); + } +} + +stock void GCTE_SendBeamCross(int client, + const float origin[3], + int ModelIndex, + int HaloIndex = 0, + float Life = 3.0, + float Width = 2.0, + const int Colour[4] = { 255, 255, 255, 255 }, + float EndWidth = 2.0, + int StartFrame = 0, + int FrameRate = 0, + int FadeLength = 0, + float Amplitude = 0.0, + int Speed = 0) +{ + float points[4][3]; + + for (int i; i < 4; i++) + { + points[i][2] = origin[2]; + } + + // -x; -y + points[0][0] = origin[0] - 8.0; + points[0][1] = origin[1] - 8.0; + + // +x; -y + points[1][0] = origin[0] + 8.0; + points[1][1] = origin[1] - 8.0; + + // +x; +y + points[2][0] = origin[0] + 8.0; + points[2][1] = origin[1] + 8.0; + + // -x; +y + points[3][0] = origin[0] - 8.0; + points[3][1] = origin[1] + 8.0; + + //draw cross + for (int corner; corner < 4; corner++) + { + TE_SetupBeamPoints(origin, points[corner], ModelIndex, HaloIndex, StartFrame, FrameRate, Life, Width, EndWidth, FadeLength, Amplitude, Colour, Speed); + TE_SendToClient(client); + } +} + +stock void GCTE_SendBeamRectangle(int client, + const float origin[3], + const float mins[3], + const float maxs[3], + int modelIndex, + int haloIndex = 0, + float life = 3.0, + float width = 2.0, + const int colour[4] = { 255, 255, 255, 255 }, + float endWidth = 2.0, + int startFrame = 0, + int frameRate = 0, + int fadeLength = 0, + float amplitude = 0.0, + int speed = 0) +{ + float vertices[4][3]; + GCRectangleVerticesFromPoint(vertices, origin, mins, maxs); + + // send the square + for (int i; i < 4; i++) + { + int j = (i == 3) ? (0) : (i + 1); + TE_SetupBeamPoints(vertices[i], + vertices[j], + modelIndex, + haloIndex, + startFrame, + frameRate, + life, + width, + endWidth, + fadeLength, + amplitude, + colour, + speed); + TE_SendToClient(client); + } +} + +/** + * Calculates vertices for a rectangle from a point, mins and maxs. + * + * @param result Vertex array result. + * @param origin Origin to offset mins and maxs by. + * @param mins Minimum size of the rectangle. + * @param maxs Maximum size of the rectangle. + * @return True if overlapping, false otherwise. + */ +stock void GCRectangleVerticesFromPoint(float result[4][3], const float origin[3], const float mins[3], const float maxs[3]) +{ + // Vertices are set clockwise starting from top left (-x; -y) + + // -x; -y + result[0][0] = origin[0] + mins[0]; + result[0][1] = origin[1] + mins[1]; + + // +x; -y + result[1][0] = origin[0] + maxs[0]; + result[1][1] = origin[1] + mins[1]; + + // +x; +y + result[2][0] = origin[0] + maxs[0]; + result[2][1] = origin[1] + maxs[1]; + + // -x; +y + result[3][0] = origin[0] + mins[0]; + result[3][1] = origin[1] + maxs[1]; + + // z is the same for every vertex + for (int vertex; vertex < 4; vertex++) + { + result[vertex][2] = origin[2]; + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/strings.inc b/sourcemod/scripting/include/gamechaos/strings.inc new file mode 100644 index 0000000..8ffcb60 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/strings.inc @@ -0,0 +1,367 @@ + +#if defined _gamechaos_stocks_strings_included + #endinput +#endif +#define _gamechaos_stocks_strings_included + +// these are used for functions that return strings. +// you can change these if they're too small/big. +#define GC_FIXED_BUFFER_SIZE_SMALL 64 +#define GC_FIXED_BUFFER_SIZE_LARGE 4096 + +/** + * Puts the values from a string of integers into an array + * + * @param string + * @param separator + * @param array + * @param arraysize + */ +stock void GCSeparateIntsFromString(const char[] string, const char[] separator, int[] array, int arraysize) +{ + char[][] explodedbuffer = new char[arraysize][32]; + + ExplodeString(string, separator, explodedbuffer, arraysize, 32); + + for (int i; i < arraysize; i++) + { + array[i] = StringToInt(explodedbuffer[i]); + } +} + +/** + * Prints a message to all admins in the chat area. + * + * @param format Formatting rules. + * @param ... Variable number of format parameters. + */ +stock void GCPrintToChatAdmins(const char[] format, any ...) +{ + char buffer[256]; + + for (int i = 1; i <= MaxClients; i++) + { + if (GCIsValidClient(i)) + { + AdminId id = GetUserAdmin(i); + if (!GetAdminFlag(id, Admin_Generic)) + { + continue; + } + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), format, 2); + PrintToChat(i, "%s", buffer); + } + } +} + +/** + * Removes trailings zeroes from a string. Also removes the decimal point if it can. + * + * @param buffer Buffer to trim. + * @return Whether anything was removed. + */ +stock bool GCRemoveTrailing0s(char[] buffer) +{ + bool removed; + int maxlen = strlen(buffer); + + if (maxlen == 0) + { + return removed; + } + + for (int i = maxlen - 1; i > 0 && (buffer[i] == '0' || buffer[i] == '.' || buffer[i] == 0); i--) + { + if (buffer[i] == 0) + { + continue; + } + if (buffer[i] == '.') + { + buffer[i] = 0; + removed = true; + break; + } + buffer[i] = 0; + removed = true; + } + return removed; +} + +/** + * Formats time by HHMMSS. Uses ticks for the time. + * + * @param timeInTicks Time in ticks. + * @param tickRate Tickrate. + * @param formattedTime String to use for formatting. + * @param size String size. + */ +stock void GCFormatTickTimeHHMMSS(int timeInTicks, float tickRate, char[] formattedTime, int size) +{ + if (timeInTicks <= 0) + { + FormatEx(formattedTime, size, "-00:00:00"); + return; + } + + int time = RoundFloat(float(timeInTicks) / tickRate * 100.0); // centiseconds + int iHours = time / 360000; + int iMinutes = time / 6000 - iHours * 6000; + int iSeconds = (time - iHours * 360000 - iMinutes * 6000) / 100; + int iCentiSeconds = time % 100; + + if (iHours != 0) + { + FormatEx(formattedTime, size, "%02i:", iHours); + } + if (iMinutes != 0) + { + Format(formattedTime, size, "%s%02i:", formattedTime, iMinutes); + } + + Format(formattedTime, size, "%s%02i.%02i", formattedTime, iSeconds, iCentiSeconds); +} + +/** + * Formats time by HHMMSS. Uses seconds. + * + * @param seconds Time in seconds. + * @param formattedTime String to use for formatting. + * @param size String size. + * @param decimals Amount of decimals to use for the fractional part. + */ +stock void GCFormatTimeHHMMSS(float seconds, char[] formattedTime, int size, int decimals) +{ + int iFlooredTime = RoundToFloor(seconds); + int iHours = iFlooredTime / 3600; + int iMinutes = iFlooredTime / 60 - iHours * 60; + int iSeconds = iFlooredTime - iHours * 3600 - iMinutes * 60; + int iFraction = RoundToFloor(FloatFraction(seconds) * Pow(10.0, float(decimals))); + + if (iHours != 0) + { + FormatEx(formattedTime, size, "%02i:", iHours); + } + if (iMinutes != 0) + { + Format(formattedTime, size, "%s%02i:", formattedTime, iMinutes); + } + char szFraction[32]; + FormatEx(szFraction, sizeof(szFraction), "%i", iFraction); + + int iTest = strlen(szFraction); + for (int i; i < decimals - iTest; i++) + { + Format(szFraction, sizeof(szFraction), "%s%s", "0", szFraction); + } + + Format(formattedTime, size, "%s%02i.%s", formattedTime, iSeconds, szFraction); +} + +/** + * Encodes and appends a number onto the end of a UTF-8 string. + * + * @param string String to append to. + * @param strsize String size. + * @param number Unicode codepoint to encode. + */ +stock void GCEncodeUtf8(char[] string, char strsize, int number) +{ + // UTF-8 octet sequence (only change digits marked with x) + /* + Char. number range | UTF-8 octet sequence + (hexadecimal) | (binary) + --------------------+--------------------------------------------- + 0000 0000-0000 007F | 0xxxxxxx + 0000 0080-0000 07FF | 110xxxxx 10xxxxxx + 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx + 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // byte 4 | byte 3 | byte 2 | byte 1*/ + + //char encodedChar = 0b_11110000_10000000_10000000_10000000; + + int zeropos = strlen(string); + + if (zeropos >= strsize - 1) // need one byte for null terminator + { + return; + } + + if (number < 0) + { + //PrintToServer("ERROR: Encode() - Can't encode negative numbers"); + return; + } + + if (number >= 0x110_000) + { + //PrintToServer("ERROR: Encode() - Number is too big to encode"); + return; + } + + // 1 byte + if (number < 0x80) + { + string[zeropos] = number; + string[zeropos + 1] = '\0'; + } + // 2 bytes + else if (number < 0x800) + { + // can't encode if we don't have enough room + if (zeropos + 2 >= strsize) + { + return; + } + + string[zeropos] = 0b_1100_0000 | (number >> 6); // don't need to mask out bits over 0x7FF + string[zeropos + 1] = 0b_1000_0000 | (number & 0b_0011_1111); + + string[zeropos + 2] = '\0'; + } + // 3 bytes + else if (number < 0x10_000) + { + // can't encode if we don't have enough room + if (zeropos + 3 >= strsize) + { + return; + } + + string[zeropos] = 0b_1110_0000 | (number >> 12); // don't need to mask out bits over 0xFFFF + string[zeropos + 1] = 0b_1000_0000 | ((number >> 6) & 0b_0011_1111); + string[zeropos + 2] = 0b_1000_0000 | (number & 0b_0011_1111); + + string[zeropos + 3] = '\0'; + } + // 4 bytes + else if (number < 0x110_000) + { + // can't encode if we don't have enough room + if (zeropos + 4 >= strsize) + { + return; + } + + string[zeropos] = 0b_1111_0000 | (number >> 18); // don't need to mask out bits over 0x10FFFF + string[zeropos + 1] = 0b_1000_0000 | ((number >> 12) & 0b_0011_1111); + string[zeropos + 2] = 0b_1000_0000 | ((number >> 6) & 0b_0011_1111); + string[zeropos + 3] = 0b_1000_0000 | (number & 0b_0011_1111); + + string[zeropos + 4] = '\0'; + } +} + +// decode a UTF-8 string into an array of unicode codepoints +/** + * Decodes a UTF-8 string into an array of unicode codepoints. + * + * @param string String to decode. + * @param strsize String size. + * @param codepoints Array to use to store the codepoints. + * @param cplength Array length. + */ +stock void GCDecodeUtf8(char[] string, int strsize, int[] codepoints, int cplength) +{ + int charindex; + int cpindex; + + while (charindex < strsize && cpindex < cplength) + { + if (string[charindex] == '\0') + { + break; + } + + int bytes = GetCharBytes(string[charindex]); + + switch (bytes) + { + case 1: + { + codepoints[cpindex] = string[charindex]; + } + case 2: + { + codepoints[cpindex] = (string[charindex++] & 0b_0001_1111) << 6; // byte 2 + codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1 + } + case 3: + { + codepoints[cpindex] = (string[charindex++] & 0b_0000_1111) << 12; // byte 3 + codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 6; // byte 2 + codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1 + } + case 4: + { + codepoints[cpindex] = (string[charindex++] & 0b_0000_0111) << 18; // byte 4 + codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 12; // byte 3 + codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 6; // byte 2 + codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1 + } + } + + charindex++; + cpindex++; + } +} + +/** + * Converts an integer to a string. + * Same as IntToString, but it returns the string. + * + * @param num Integer to convert. + * @return String of the number. + */ +stock char[] GCIntToStringRet(int num) +{ + char string[GC_FIXED_BUFFER_SIZE_SMALL]; + IntToString(num, string, sizeof string); + return string; +} + + /** + * Converts a floating point number to a string. + * Same as FloatToString, but it returns the string. + * + * @param num Floating point number to convert. + * @return String of the number. + */ +stock char[] GCFloatToStringRet(float num) +{ + char string[GC_FIXED_BUFFER_SIZE_SMALL]; + FloatToString(num, string, sizeof string); + return string; +} + +/** + * Formats a string according to the SourceMod format rules (see documentation). + * Same as Format, except it returns the formatted string. + * + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @return Formatted string. + */ +stock char[] GCFormatReturn(const char[] format, any ...) +{ + char string[GC_FIXED_BUFFER_SIZE_LARGE]; + VFormat(string, sizeof string, format, 2); + return string; +} + +/** + * Removes whitespace characters from the beginning and end of a string. + * Same as TrimString, except it returns the formatted string and + * it doesn't modify the passed string. + * + * @param str The string to trim. + * @return Number of bytes written (UTF-8 safe). + */ +stock char[] GCTrimStringReturn(char[] str) +{ + char string[GC_FIXED_BUFFER_SIZE_LARGE]; + strcopy(string, sizeof string, str); + TrimString(string); + return string; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/tempents.inc b/sourcemod/scripting/include/gamechaos/tempents.inc new file mode 100644 index 0000000..7ec9b5a --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/tempents.inc @@ -0,0 +1,62 @@ + +#if defined _gamechaos_stocks_tempents_included + #endinput +#endif +#define _gamechaos_stocks_tempents_included + +// improved api of some tempents + +#define GC_TEMPENTS_VERSION 0x01_00_00 +#define GC_TEMPENTS_VERSION_STRING "1.0.0" + +#include <sdktools_tempents> + +/** + * Sets up a point to point beam effect. + * + * @param start Start position of the beam. + * @param end End position of the beam. + * @param modelIndex Precached model index. + * @param life Time duration of the beam. + * @param width Initial beam width. + * @param endWidth Final beam width. + * @param colour Color array (r, g, b, a). + * @param haloIndex Precached model index. + * @param amplitude Beam amplitude. + * @param speed Speed of the beam. + * @param fadeLength Beam fade time duration. + * @param frameRate Beam frame rate. + * @param startFrame Initial frame to render. + */ +stock void GCTE_SetupBeamPoints(const float start[3], + const float end[3], + int modelIndex, + float life = 2.0, + float width = 2.0, + float endWidth = 2.0, + const int colour[4] = {255, 255, 255, 255}, + int haloIndex = 0, + float amplitude = 0.0, + int speed = 0, + int fadeLength = 0, + int frameRate = 0, + int startFrame = 0) +{ + TE_Start("BeamPoints"); + TE_WriteVector("m_vecStartPoint", start); + TE_WriteVector("m_vecEndPoint", end); + TE_WriteNum("m_nModelIndex", modelIndex); + TE_WriteNum("m_nHaloIndex", haloIndex); + TE_WriteNum("m_nStartFrame", startFrame); + TE_WriteNum("m_nFrameRate", frameRate); + TE_WriteFloat("m_fLife", life); + TE_WriteFloat("m_fWidth", width); + TE_WriteFloat("m_fEndWidth", endWidth); + TE_WriteFloat("m_fAmplitude", amplitude); + TE_WriteNum("r", colour[0]); + TE_WriteNum("g", colour[1]); + TE_WriteNum("b", colour[2]); + TE_WriteNum("a", colour[3]); + TE_WriteNum("m_nSpeed", speed); + TE_WriteNum("m_nFadeLength", fadeLength); +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/tracing.inc b/sourcemod/scripting/include/gamechaos/tracing.inc new file mode 100644 index 0000000..65d54a8 --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/tracing.inc @@ -0,0 +1,242 @@ + +#if defined _gamechaos_stocks_tracing_included + #endinput +#endif +#define _gamechaos_stocks_tracing_included + +#include <sdktools_trace> + +#define GC_TRACING_VERSION 0x01_00_00 +#define GC_TRACING_VERSION_STRING "1.0.0" + +/** + * Trace ray filter that filters players from being traced. + * + * @param entity Entity. + * @param data Data. + * @return True on success, false otherwise. + */ +stock bool GCTraceEntityFilterPlayer(int entity, any data) +{ + return entity > MAXPLAYERS; +} + +/** + * Traces the player hull beneath the player in the direction of + * the player's velocity. This should be used on the tick when the player lands + * + * @param client Player's index. + * @param pos Player's position vector. + * @param velocity Player's velocity vector. This shuold have the current tick's x and y velocities, but the previous tick's z velocity, since when you're on ground, your z velocity is 0. + * @param result Trace endpoint on success, player's position on failure. + * @param bugged Whether to add gravity to the player's velocity or not. + * @return True on success, false otherwise. + */ +stock bool GCTraceLandPos(int client, const float pos[3], const float velocity[3], float result[3], float fGravity, bool bugged = false) +{ + float newVel[3]; + newVel = velocity; + + if (bugged) + { + // add 0.5 gravity + newVel[2] -= fGravity * GetTickInterval() * 0.5; + } + else + { + // add 1.5 gravity + newVel[2] -= fGravity * GetTickInterval() * 1.5; + } + + ScaleVector(newVel, GetTickInterval() * 2.0); + float pos2[3]; + AddVectors(pos, newVel, pos2); + + float mins[3]; + float maxs[3]; + GetClientMins(client, mins); + GetClientMaxs(client, maxs); + + Handle trace = TR_TraceHullFilterEx(pos, pos2, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer); + + if (!TR_DidHit(trace)) + { + result = pos; + CloseHandle(trace); + return false; + } + + TR_GetEndPosition(result, trace); + CloseHandle(trace); + + return true; +} + +/** + * Traces the player hull 2 units straight down beneath the player. + * + * @param client Player's index. + * @param pos Player's position vector. + * @param result Trace endpoint on success, player's position on failure. + * @return True on success, false otherwise. + */ +stock bool GCTraceGround(int client, const float pos[3], float result[3]) +{ + float mins[3]; + float maxs[3]; + + GetClientMins(client, mins); + GetClientMaxs(client, maxs); + + float startpos[3]; + float endpos[3]; + + startpos = pos; + endpos = pos; + + endpos[2] -= 2.0; + + TR_TraceHullFilter(startpos, endpos, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer); + + if (TR_DidHit()) + { + TR_GetEndPosition(result); + return true; + } + else + { + result = endpos; + return false; + } +} + +/** + * Traces a hull between 2 positions. + * + * @param pos1 Position 1. + * @param pos2 Position 2 + * @param result Trace endpoint on success, player's position on failure. + * @return True on success, false otherwise. + */ +stock bool GCTraceBlock(const float pos1[3], const float pos2[3], float result[3]) +{ + float mins[3] = {-16.0, -16.0, -1.0}; + float maxs[3] = { 16.0, 16.0, 0.0}; + + TR_TraceHullFilter(pos1, pos2, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer); + + if (TR_DidHit()) + { + TR_GetEndPosition(result); + return true; + } + else + { + return false; + } +} + +/** + * Traces from player eye position in the direction of where the player is looking. + * + * @param client Client index. + * @param result Resultant vector. + * @return True on success, false otherwise. + */ +stock bool GCGetEyeRayPosition(int client, float result[3], TraceEntityFilter filter, any data = 0, int flags = MASK_PLAYERSOLID) +{ + float start[3]; + float angle[3]; + + GetClientEyePosition(client, start); + GetClientEyeAngles(client, angle); + + TR_TraceRayFilter(start, angle, flags, RayType_Infinite, filter, data); + + if (TR_DidHit(INVALID_HANDLE)) + { + TR_GetEndPosition(result, INVALID_HANDLE); + return true; + } + return false; +} + +/** + * Traces from player eye position in the direction of where the player is looking, up to a certain distance. + * + * @param client Client index. + * @param result Resultant vector. + * @param distance Maximum distance to trace. + * @return True on success, false otherwise. + */ +stock bool GCTraceEyeRayPositionDistance(int client, float result[3], float distance) +{ + float start[3]; + float angle[3]; + + GetClientEyePosition(client, start); + GetClientEyeAngles(client, angle); + + float endpoint[3]; + float zsine = Sine(DegToRad(-angle[0])); + float zcos = Cosine(DegToRad(-angle[0])); + + endpoint[0] = Cosine(DegToRad(angle[1])) * zcos; + endpoint[1] = Sine(DegToRad(angle[1])) * zcos; + endpoint[2] = zsine; + + ScaleVector(endpoint, distance); + AddVectors(start, endpoint, endpoint); + + TR_TraceRayFilter(start, endpoint, MASK_PLAYERSOLID, RayType_EndPoint, GCTraceEntityFilterPlayer, client); + + if (TR_DidHit()) + { + TR_GetEndPosition(result); + return true; + } + + result = endpoint; + return false; +} + +/** + * Traces a hull in a certain direction and distance. + * + * @param origin Position to trace from. + * @param direction Trace direction. + * @param mins Minimum size of the hull. + * @param maxs Maximum size of the hull. + * @param result Resultant vector. + * @return True on success, false otherwise. + */ +stock bool GCTraceHullDirection(const float origin[3], + const float direction[3], + const float mins[3], + const float maxs[3], + float result[3], + float distance, + TraceEntityFilter filter, + any data = 0, + int flags = MASK_PLAYERSOLID) +{ + float pos2[3]; + float zsine = Sine(DegToRad(-direction[0])); + float zcos = Cosine(DegToRad(-direction[0])); + + pos2[0] = Cosine(DegToRad(direction[1])) * zcos; + pos2[1] = Sine(DegToRad(direction[1])) * zcos; + pos2[2] = zsine; + + ScaleVector(pos2, distance); + AddVectors(origin, pos2, pos2); + + TR_TraceHullFilter(origin, pos2, mins, maxs, flags, filter, data); + if (TR_DidHit()) + { + TR_GetEndPosition(result); + return true; + } + result = pos2; + return false; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gamechaos/vectors.inc b/sourcemod/scripting/include/gamechaos/vectors.inc new file mode 100644 index 0000000..79d5e8f --- /dev/null +++ b/sourcemod/scripting/include/gamechaos/vectors.inc @@ -0,0 +1,66 @@ + +#if defined _gamechaos_stocks_vectors_included + #endinput +#endif +#define _gamechaos_stocks_vectors_included + +#define GC_VECTORS_VERSION 0x01_00_01 +#define GC_VECTORS_VERSION_STRING "1.0.1" + +/** + * Calculates the horizontal (x, y) length of a vector. + * + * @param vec Vector. + * @return Vector length (magnitude). + */ +stock float GCGetVectorLength2D(const float vec[3]) +{ + float tempVec[3]; + tempVec = vec; + tempVec[2] = 0.0; + + return GetVectorLength(tempVec); +} + +/** + * Calculates the horizontal (x, y) distance between 2 vectors. + * + * @param x Vector 1. + * @param y Vector 2. + * @param tolerance How close the floats have to be to return true. + * @return True on success, false otherwise. + */ +stock float GCGetVectorDistance2D(const float x[3], const float y[3]) +{ + float x2[3]; + float y2[3]; + + x2 = x; + y2 = y; + + x2[2] = 0.0; + y2[2] = 0.0; + + return GetVectorDistance(x2, y2); +} + +/** + * Checks if 2 vectors are exactly equal. + * + * @param a Vector 1. + * @param b Vector 2. + * @return True on success, false otherwise. + */ +stock bool GCVectorsEqual(const float a[3], const float b[3]) +{ + bool result = true; + for (int i = 0; i < 3; i++) + { + if (a[i] != b[i]) + { + result = false; + break; + } + } + return result; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/glib/addressutils.inc b/sourcemod/scripting/include/glib/addressutils.inc new file mode 100644 index 0000000..bbe8f14 --- /dev/null +++ b/sourcemod/scripting/include/glib/addressutils.inc @@ -0,0 +1,54 @@ +#if defined _addressutils_included +#endinput +#endif +#define _addressutils_included + +methodmap AddressBase +{ + property Address Address + { + public get() { return view_as<Address>(this); } + } +} + +//-==Operator overloadings +stock Address operator+(Address l, int r) +{ + return l + view_as<Address>(r); +} + +stock Address operator+(int l, Address r) +{ + return view_as<Address>(l) + r; +} + +stock Address operator-(Address l, int r) +{ + return l - view_as<Address>(r); +} + +stock Address operator-(int l, Address r) +{ + return view_as<Address>(l) - r; +} + +stock Address operator*(Address l, int r) +{ + return l * view_as<Address>(r); +} + +stock Address operator*(int l, Address r) +{ + return view_as<Address>(l) * r; +} + +stock Address operator/(Address l, int r) +{ + return l / view_as<Address>(r); +} + +stock Address operator/(int l, Address r) +{ + return view_as<Address>(l) / r; +} +//Operator overloadings==-
\ No newline at end of file diff --git a/sourcemod/scripting/include/glib/assertutils.inc b/sourcemod/scripting/include/glib/assertutils.inc new file mode 100644 index 0000000..83cd90d --- /dev/null +++ b/sourcemod/scripting/include/glib/assertutils.inc @@ -0,0 +1,61 @@ +#if defined _assertutils_included +#endinput +#endif +#define _assertutils_included + +/* Compile time settings for this include. Should be defined before including this file. +* #define ASSERTUTILS_DISABLE //Disables all assertions +* #define ASSERTUTILS_FAILSTATE_FUNC //Define the name of the function that should be called when assertion is hit +*/ + +#if !defined SNAME +#define __SNAME "" +#else +#define __SNAME SNAME +#endif + +#define ASSERT_FMT_STRING_LEN 512 + +#if defined ASSERTUTILS_DISABLE + +#define ASSERT(%1)%2; +#define ASSERT_MSG(%1,%2)%3; +#define ASSERT_FMT(%1,%2)%3; +#define ASSERT_FINAL(%1)%2; +#define ASSERT_FINAL_MSG(%1,%2)%3; + +#elseif defined ASSERTUTILS_FAILSTATE_FUNC + +#define ASSERT(%1) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME..."Assertion failed: \""...#%1..."\"") +#define ASSERT_MSG(%1,%2) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME...%2) +#define ASSERT_FMT(%1,%2) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME...%2) +#define ASSERT_FINAL(%1) if(!(%1)) SetFailState(__SNAME..."Assertion failed: \""...#%1..."\"") +#define ASSERT_FINAL_MSG(%1,%2) if(!(%1)) SetFailState(__SNAME...%2) + +#else + +#define ASSERT(%1) if(!(%1)) SetFailState(__SNAME..."Assertion failed: \""...#%1..."\"") +#define ASSERT_MSG(%1,%2) if(!(%1)) SetFailState(__SNAME...%2) +#define ASSERT_FMT(%1,%2) if(!(%1)) SetFailState(__SNAME...%2) +#define ASSERT_FINAL(%1) ASSERT(%1) +#define ASSERT_FINAL_MSG(%1,%2) ASSERT_MSG(%1,%2) + +#endif + +// Might be redundant as default ASSERT_MSG accept format arguments just fine. +#if 0 +stock void ASSERT_FMT(bool result, char[] fmt, any ...) +{ +#if !defined ASSERTUTILS_DISABLE + if(!result) + { + char buff[ASSERT_FMT_STRING_LEN]; + VFormat(buff, sizeof(buff), fmt, 3); + + SetFailState(__SNAME..."%s", buff); + } +#endif +} +#endif + +#undef ASSERT_FMT_STRING_LEN
\ No newline at end of file diff --git a/sourcemod/scripting/include/glib/memutils.inc b/sourcemod/scripting/include/glib/memutils.inc new file mode 100644 index 0000000..5813d92 --- /dev/null +++ b/sourcemod/scripting/include/glib/memutils.inc @@ -0,0 +1,232 @@ +#if defined _memutils_included +#endinput +#endif +#define _memutils_included + +#include "glib/assertutils" +#include "glib/addressutils" + +/* Compile time settings for this include. Should be defined before including this file. +* #define MEMUTILS_PLUGINENDCALL //This should be defined if main plugin has OnPluginEnd() forward used. +*/ + +#define MEM_LEN_SAFE_THRESHOLD 2000 + +//-==PatchHandling methodmap +static StringMap gPatchStack; + +methodmap PatchHandler < AddressBase +{ + public PatchHandler(Address addr) + { + ASSERT(addr != Address_Null); + + if(!gPatchStack) + gPatchStack = new StringMap(); + + return view_as<PatchHandler>(addr); + } + + property any Any + { + public get() { return view_as<any>(this); } + } + + public void Save(int len) + { + ASSERT(gPatchStack); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + len++; + + int[] arr = new int[len]; + arr[0] = len; + + for(int i = 0; i < len - 1; i++) + arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8); + + char buff[32]; + IntToString(this.Any, buff, sizeof(buff)); + gPatchStack.SetArray(buff, arr, len); + } + + public void PatchNSave(int len, char byte = 0x90) + { + ASSERT(gPatchStack); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + len++; + + int[] arr = new int[len]; + arr[0] = len; + + for(int i = 0; i < len - 1; i++) + { + arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8); + StoreToAddress(this.Address + i, byte, NumberType_Int8); + } + + char buff[32]; + IntToString(this.Any, buff, sizeof(buff)); + gPatchStack.SetArray(buff, arr, len); + } + + public void PatchNSaveSeq(const char[] data, int len) + { + ASSERT(gPatchStack); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + len++; + + int[] arr = new int[len]; + arr[0] = len; + + for(int i = 0; i < len - 1; i++) + { + arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8); + StoreToAddress(this.Address + i, data[i], NumberType_Int8); + } + + char buff[32]; + IntToString(this.Any, buff, sizeof(buff)); + gPatchStack.SetArray(buff, arr, len); + } + + public void Restore() + { + if(!gPatchStack) + return; + + char buff[32]; + IntToString(this.Any, buff, sizeof(buff)); + + int arrSize[1]; + if(!gPatchStack.GetArray(buff, arrSize, sizeof(arrSize))) + return; + + int[] arr = new int[arrSize[0]]; + gPatchStack.GetArray(buff, arr, arrSize[0]); + gPatchStack.Remove(buff); + + for(int i = 0; i < arrSize[0] - 1; i++) + StoreToAddress(this.Address + i, arr[i + 1], NumberType_Int8); + + if(gPatchStack.Size == 0) + delete gPatchStack; + } +} + +public void OnPluginEnd() +{ + if(gPatchStack) + { + StringMapSnapshot sms = gPatchStack.Snapshot(); + char buff[32]; + Address addr; + + for(int i = 0; i < sms.Length; i++) + { + sms.GetKey(i, buff, sizeof(buff)); + addr = view_as<Address>(StringToInt(buff)); + view_as<PatchHandler>(addr).Restore(); + } + } + +#if defined MEMUTILS_PLUGINENDCALL + OnPluginEnd_MemUtilsRedefined(); +#endif +} +#undef OnPluginEnd +#if defined MEMUTILS_PLUGINENDCALL +#define OnPluginEnd OnPluginEnd_MemUtilsRedefined +#else +#define OnPluginEnd OnPluginEnd_Redifined(){}\ +void MEMUTILS_INCLUDE_WARNING_OnPluginEnd_REDIFINITION +#endif +//PatchHandling methodmap==- + +//-==Other util functions +stock int LoadStringFromAddress(Address addr, char[] buff, int length) +{ + int i; + for(i = 0; i < length && (buff[i] = LoadFromAddress(addr + i, NumberType_Int8)) != '\0'; i++) { } + buff[i == length ? i - 1 : i] = '\0'; + return i; +} + +stock void DumpOnAddress(Address addr, int len, int columns = 10) +{ + char buff[128], buff2[128]; + + ASSERT(addr != Address_Null); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + Format(buff, sizeof(buff), "[0x%08X]", addr); + char chr; + for(int i = 0; i < len; i++) + { + chr = LoadFromAddress(addr + i, NumberType_Int8); + Format(buff, sizeof(buff), "%s %02X", buff, chr); + Format(buff2, sizeof(buff2), "%s%c", buff2, (chr > ' ' && chr != 0x7F && chr != 0xFF ? chr : '.')); + if(i % columns == columns - 1) + { + PrintToServer(__SNAME..."%s %s", buff, buff2); + Format(buff, sizeof(buff), "[0x%08X]", addr + i); + buff2[0] = '\0'; + } + } + + if((len - 1) % columns != columns - 1) + PrintToServer(__SNAME..."%s %s", buff, buff2); +} + +//NO OVERLAPPING!! +stock void MoveBytes(Address from, Address to, int len, char replace_with = 0x90) +{ + ASSERT(from != Address_Null); + ASSERT(to != Address_Null); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + ASSERT(to < from || to > from + len); + + if(from == to) + return; + + for(int i = 0; i < len; i++) + { + StoreToAddress(to + i, LoadFromAddress(from + i, NumberType_Int8), NumberType_Int8); + StoreToAddress(from + i, replace_with, NumberType_Int8); + } +} + +stock void CutNCopyBytes(Address from, Address to, int len, char replace_with = 0x90) +{ + ASSERT(from != Address_Null); + ASSERT(to != Address_Null); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + if(from == to) + return; + + int[] arr = new int[len]; + + for(int i = 0; i < len; i++) + { + arr[i] = LoadFromAddress(from + i, NumberType_Int8); + StoreToAddress(from + i, replace_with, NumberType_Int8); + } + + for(int i = 0; i < len; i++) + StoreToAddress(to + i, arr[i], NumberType_Int8); +} + +stock void PatchArea(Address addr, int len, char byte = 0x90) +{ + ASSERT(addr != Address_Null); + ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD); + + for(int i = 0; i < len; i++) + StoreToAddress(addr + i, byte, NumberType_Int8); +} +//Other util functions==- + +#undef MEM_LEN_SAFE_THRESHOLD
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz.inc b/sourcemod/scripting/include/gokz.inc new file mode 100644 index 0000000..edbd896 --- /dev/null +++ b/sourcemod/scripting/include/gokz.inc @@ -0,0 +1,1097 @@ +/* + GOKZ General Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_included_ +#endinput +#endif +#define _gokz_included_ +#include <cstrike> +#include <movement> + +#include <gokz/version> + + + +// =====[ ENUMS ]===== + +enum ObsMode +{ + ObsMode_None = 0, // Not in spectator mode + ObsMode_DeathCam, // Special mode for death cam animation + ObsMode_FreezeCam, // Zooms to a target, and freeze-frames on them + ObsMode_Fixed, // View from a fixed camera position + ObsMode_InEye, // Follow a player in first person view + ObsMode_Chase, // Follow a player in third person view + ObsMode_Roaming // Free roaming +}; + + + +// =====[ CONSTANTS ]===== + +#define GOKZ_SOURCE_URL "https://github.com/KZGlobalTeam/gokz" +#define GOKZ_UPDATER_BASE_URL "http://updater.gokz.org/v2/" +#define GOKZ_COLLISION_GROUP_STANDARD 2 +#define GOKZ_COLLISION_GROUP_NOTRIGGER 1 +#define GOKZ_TP_FREEZE_TICKS 5 +#define EPSILON 0.000001 +#define PI 3.14159265359 +#define SPEED_NORMAL 250.0 +#define SPEED_NO_WEAPON 260.0 +#define FLOAT_MAX view_as<float>(0x7F7FFFFF) +#define SF_BUTTON_USE_ACTIVATES 1024 +#define IGNORE_JUMP_TIME 0.2 +stock float PLAYER_MINS[3] = {-16.0, -16.0, 0.0}; +stock float PLAYER_MAXS[3] = {16.0, 16.0, 72.0}; +stock float PLAYER_MAXS_DUCKED[3] = {16.0, 16.0, 54.0}; + + + +// =====[ STOCKS ]===== + +/** + * Represents a time float as a string e.g. 01:23.45. + * + * @param time Time in seconds. + * @param precise Whether to include fractional seconds. + * @return String representation of time. + */ +stock char[] GOKZ_FormatTime(float time, bool precise = true) +{ + char formattedTime[12]; + + int roundedTime = RoundFloat(time * 100); // Time rounded to number of centiseconds + + int centiseconds = roundedTime % 100; + roundedTime = (roundedTime - centiseconds) / 100; + int seconds = roundedTime % 60; + roundedTime = (roundedTime - seconds) / 60; + int minutes = roundedTime % 60; + roundedTime = (roundedTime - minutes) / 60; + int hours = roundedTime; + + if (hours == 0) + { + if (precise) + { + FormatEx(formattedTime, sizeof(formattedTime), "%02d:%02d.%02d", minutes, seconds, centiseconds); + } + else + { + FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d", minutes, seconds); + } + } + else + { + if (precise) + { + FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds); + } + else + { + FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d", hours, minutes, seconds); + } + } + return formattedTime; +} + +/** + * Checks if the value is a valid client entity index, if they are in-game and not GOTV. + * + * @param client Client index. + * @return Whether client is valid. + */ +stock bool IsValidClient(int client) +{ + return client >= 1 && client <= MaxClients && IsClientInGame(client) && !IsClientSourceTV(client); +} + +/** + * Returns the greater of two float values. + * + * @param value1 First value. + * @param value2 Second value. + * @return Greatest value. + */ +stock float FloatMax(float value1, float value2) +{ + if (value1 >= value2) + { + return value1; + } + return value2; +} + +/** + * Returns the lesser of two float values. + * + * @param value1 First value. + * @param value2 Second value. + * @return Lesser value. + */ +stock float FloatMin(float value1, float value2) +{ + if (value1 <= value2) + { + return value1; + } + return value2; +} + +/** + * Clamp a float value between an upper and lower bound. + * + * @param value Preferred value. + * @param min Minimum value. + * @param max Maximum value. + * @return The closest value to the preferred value. + */ +stock float FloatClamp(float value, float min, float max) +{ + if (value >= max) + { + return max; + } + if (value <= min) + { + return min; + } + return value; +} + + +/** + * Returns the greater of two int values. + * + * @param value1 First value. + * @param value2 Second value. + * @return Greatest value. + */ +stock int IntMax(int value1, int value2) +{ + if (value1 >= value2) + { + return value1; + } + return value2; +} + +/** + * Returns the lesser of two int values. + * + * @param value1 First value. + * @param value2 Second value. + * @return Lesser value. + */ +stock int IntMin(int value1, int value2) +{ + if (value1 <= value2) + { + return value1; + } + return value2; +} + +/** + * Rounds a float to the nearest specified power of 10. + * + * @param value Value to round. + * @param power Power of 10 to round to. + * @return Rounded value. + */ +stock float RoundToPowerOfTen(float value, int power) +{ + float pow = Pow(10.0, float(power)); + return RoundFloat(value / pow) * pow; +} + +/** + * Sets all characters in a string to lower case. + * + * @param input Input string. + * @param output Output buffer. + * @param size Maximum size of output. + */ +stock void String_ToLower(const char[] input, char[] output, int size) +{ + size--; + int i = 0; + while (input[i] != '\0' && i < size) + { + output[i] = CharToLower(input[i]); + i++; + } + output[i] = '\0'; +} + +/** + * Gets the client's observer mode. + * + * @param client Client index. + * @return Current observer mode. + */ +stock ObsMode GetObserverMode(int client) +{ + return view_as<ObsMode>(GetEntProp(client, Prop_Send, "m_iObserverMode")); +} + +/** + * Gets the player a client is spectating. + * + * @param client Client index. + * @return Client index of target, or -1 if not spectating anyone. + */ +stock int GetObserverTarget(int client) +{ + if (!IsValidClient(client)) + { + return -1; + } + ObsMode mode = GetObserverMode(client); + if (mode == ObsMode_InEye || mode == ObsMode_Chase) + { + return GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); + } + return -1; +} + +/** + * Emits a sound to other players that are spectating the client. + * + * @param client Client being spectated. + * @param sound Sound to play. + */ +stock void EmitSoundToClientSpectators(int client, const char[] sound) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidClient(i) && GetObserverTarget(i) == client) + { + EmitSoundToClient(i, sound); + } + } +} + +/** + * Calculates the lowest angle from angle A to angle B. + * Input and result angles are between -180 and 180. + * + * @param angleA Angle A. + * @param angleB Angle B. + * @return Delta angle. + */ +stock float CalcDeltaAngle(float angleA, float angleB) +{ + float difference = angleB - angleA; + + if (difference > 180.0) + { + difference = difference - 360.0; + } + else if (difference <= -180.0) + { + difference = difference + 360.0; + } + + return difference; +} + +/** + * Strips all color control characters in a string. + * The Output buffer can be the same as the input buffer. + * Original code by Psychonic, thanks. + * Source: smlib + * + * @param input Input String. + * @param output Output String. + * @param size Max Size of the Output string + */ +stock void Color_StripFromChatText(const char[] input, char[] output, int size) +{ + int x = 0; + for (int i = 0; input[i] != '\0'; i++) { + + if (x + 1 == size) + { + break; + } + + int character = input[i]; + + if (character > 0x08) + { + output[x++] = character; + } + } + + output[x] = '\0'; +} + +/** + * Returns an integer as a string. + * + * @param num Integer to stringify. + * @return Integer as a string. + */ +stock char[] IntToStringEx(int num) +{ + char string[12]; + IntToString(num, string, sizeof(string)); + return string; +} + +/** + * Returns a float as a string. + * + * @param num Float to stringify. + * @return Float as a string. + */ +stock char[] FloatToStringEx(float num) +{ + char string[32]; + FloatToString(num, string, sizeof(string)); + return string; +} + +/** + * Increment an index, looping back to 0 if the max value is reached. + * + * @param index Current index. + * @param buffer Max value of index. + * @return Current index incremented, or 0 if max value is reached. + */ +stock int NextIndex(int index, int max) +{ + index++; + if (index == max) + { + return 0; + } + return index; +} + +/** + * Reorders an array with current index at the front, and previous + * values after, including looping back to the end after reaching + * the start of the array. + * + * @param input Array to reorder. + * @param inputSize Size of input array. + * @param buffer Output buffer. + * @param bufferSize Size of buffer. + * @param index Index of current/most recent value of input array. + */ +stock void SortByRecent(const int[] input, int inputSize, int[] buffer, int bufferSize, int index) +{ + int reorderedIndex = 0; + for (int i = index; reorderedIndex < bufferSize && i >= 0; i--) + { + buffer[reorderedIndex] = input[i]; + reorderedIndex++; + } + for (int i = inputSize - 1; reorderedIndex < bufferSize && i > index; i--) + { + buffer[reorderedIndex] = input[i]; + reorderedIndex++; + } +} + +/** + * Returns the Steam account ID for a given SteamID2. + * Checks for invalid input are not very extensive. + * + * @param steamID2 SteamID2 to convert. + * @return Steam account ID, or -1 if invalid. + */ +stock int Steam2ToSteamAccountID(const char[] steamID2) +{ + char pieces[3][16]; + if (ExplodeString(steamID2, ":", pieces, sizeof(pieces), sizeof(pieces[])) != 3) + { + return -1; + } + + int IDNumberPart1 = StringToInt(pieces[1]); + int IDNumberPart2 = StringToInt(pieces[2]); + if (pieces[1][0] != '0' && IDNumberPart1 == 0 || IDNumberPart1 != 0 && IDNumberPart1 != 1 || IDNumberPart2 <= 0) + { + return -1; + } + + return IDNumberPart1 + (IDNumberPart2 << 1); +} + +/** + * Teleports a player and removes their velocity and base velocity + * immediately and also every tick for the next 5 ticks. Automatically + * makes the player crouch if there is a ceiling above them. + * + * @param client Client index. + * @param origin Origin to teleport to. + * @param angles Eye angles to set. + */ +stock void TeleportPlayer(int client, const float origin[3], const float angles[3], bool setAngles = true, bool holdStill = true) +{ + // Clear the player's parent before teleporting to fix being + // teleported into seemingly random places if the player has a parent. + AcceptEntityInput(client, "ClearParent"); + + Movement_SetOrigin(client, origin); + Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + if (setAngles) + { + // NOTE: changing angles with TeleportEntity can fail due to packet loss!!! + // (Movement_SetEyeAngles is a thin wrapper of TeleportEntity) + Movement_SetEyeAngles(client, angles); + } + // Duck the player if there is something blocking them from above + Handle trace = TR_TraceHullFilterEx(origin, + origin, + view_as<float>( { -16.0, -16.0, 0.0 } ), // Standing players are 32 x 32 x 72 + view_as<float>( { 16.0, 16.0, 72.0 } ), + MASK_PLAYERSOLID, + TraceEntityFilterPlayers, + client); + bool ducked = TR_DidHit(trace); + + if (holdStill) + { + // Prevent noclip exploit + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + + // Intelligently hold player still to prevent booster and trigger exploits + StartHoldStill(client, ducked); + } + else if (ducked) + { + ForcePlayerDuck(client); + } + + delete trace; +} + +static void StartHoldStill(int client, bool ducked) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(0); // tick counter + data.WriteCell(GOKZ_TP_FREEZE_TICKS); // number of ticks to hold still + data.WriteCell(ducked); + ContinueHoldStill(data); +} + +public void ContinueHoldStill(DataPack data) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int ticks = data.ReadCell(); + int tickCount = data.ReadCell(); + bool ducked = data.ReadCell(); + delete data; + + if (!IsValidClient(client)) + { + return; + } + + if (ticks < tickCount) + { + Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } )); + Movement_SetGravity(client, 1.0); + + // Don't drop the player off of ladders. + // The game will automatically change the movetype back to MOVETYPE_WALK if it can't find a ladder. + // Don't change the movetype if it's currently MOVETYPE_NONE, as that means the player is paused. + if (Movement_GetMovetype(client) != MOVETYPE_NONE) + { + Movement_SetMovetype(client, MOVETYPE_LADDER); + } + + // Prevent noclip exploit + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + + // Force duck on player and make sure that the player can't trigger triggers above them. + // they can still trigger triggers even when we force ducking. + if (ducked) + { + ForcePlayerDuck(client); + + if (ticks < tickCount - 1) + { + // Don't trigger triggers + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER); + } + else + { + // Let the player trigger triggers on the last tick + SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); + } + } + + ++ticks; + data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(ticks); + data.WriteCell(tickCount); + data.WriteCell(ducked); + RequestFrame(ContinueHoldStill, data); + } +} + +/** + * Forces the player to instantly duck. + * + * @param client Client index. + */ +stock void ForcePlayerDuck(int client) +{ + // these are both necessary, because on their own the player will sometimes still be in a state that isn't fully ducked. + SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", 1.0, 0); + SetEntProp(client, Prop_Send, "m_bDucking", false); + SetEntProp(client, Prop_Send, "m_bDucked", true); +} + +/** + * Returns whether the player is stuck e.g. in a wall after noclipping. + * + * @param client Client index. + * @return Whether player is stuck. + */ +stock bool IsPlayerStuck(int client) +{ + float vecMin[3], vecMax[3], vecOrigin[3]; + + GetClientMins(client, vecMin); + GetClientMaxs(client, vecMax); + GetClientAbsOrigin(client, vecOrigin); + + TR_TraceHullFilter(vecOrigin, vecOrigin, vecMin, vecMax, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + return TR_DidHit(); // head in wall ? +} + +/** + * Retrieves the absolute origin of an entity. + * + * @param entity Index of the entity. + * @param result Entity's origin if successful. + * @return Returns true if successful. + */ +stock bool GetEntityAbsOrigin(int entity, float result[3]) +{ + if (!IsValidEntity(entity)) + { + return false; + } + + if (!HasEntProp(entity, Prop_Data, "m_vecAbsOrigin")) + { + return false; + } + + GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", result); + return true; +} + +/** + * Retrieves the name of an entity. + * + * @param entity Index of the entity. + * @param buffer Buffer to store the name. + * @param maxlength Maximum length of the buffer. + * @return Number of non-null bytes written. + */ +stock int GetEntityName(int entity, char[] buffer, int maxlength) +{ + return GetEntPropString(entity, Prop_Data, "m_iName", buffer, maxlength); +} + +/** + * Finds an entity by name or by name and classname. + * Taken from smlib https://github.com/bcserv/smlib + * This can take anywhere from ~0.2% to ~11% of frametime (i5-7600k) in the worst case scenario where + * every entity which has a name (4096 of them) is iterated over. Your mileage may vary. + * + * @param name Name of the entity to find. + * @param className Optional classname to match along with name. + * @param ignorePlayers Ignore player entities. + * @return Entity index if successful, INVALID_ENT_REFERENCE if not. + */ +stock int GOKZFindEntityByName(const char[] name, const char[] className = "", bool ignorePlayers = false) +{ + int result = INVALID_ENT_REFERENCE; + if (className[0] == '\0') + { + // HACK: Double the limit to get non-networked entities too. + // https://developer.valvesoftware.com/wiki/Entity_limit + int realMaxEntities = GetMaxEntities() * 2; + int startEntity = 1; + if (ignorePlayers) + { + startEntity = MaxClients + 1; + } + for (int entity = startEntity; entity < realMaxEntities; entity++) + { + if (!IsValidEntity(entity)) + { + continue; + } + + char entName[65]; + GetEntityName(entity, entName, sizeof(entName)); + if (StrEqual(entName, name)) + { + result = entity; + break; + } + } + } + else + { + int entity = INVALID_ENT_REFERENCE; + while ((entity = FindEntityByClassname(entity, className)) != INVALID_ENT_REFERENCE) + { + char entName[65]; + GetEntityName(entity, entName, sizeof(entName)); + if (StrEqual(entName, name)) + { + result = entity; + break; + } + } + } + return result; +} + +/** + * Gets the current map's display name in lower case. + * + * @param buffer Buffer to store the map name. + * @param maxlength Maximum length of buffer. + */ +stock void GetCurrentMapDisplayName(char[] buffer, int maxlength) +{ + char map[PLATFORM_MAX_PATH]; + GetCurrentMap(map, sizeof(map)); + GetMapDisplayName(map, map, sizeof(map)); + String_ToLower(map, buffer, maxlength); +} + +/** + * Gets the current map's file size. + */ +stock int GetCurrentMapFileSize() +{ + char mapBuffer[PLATFORM_MAX_PATH]; + GetCurrentMap(mapBuffer, sizeof(mapBuffer)); + Format(mapBuffer, sizeof(mapBuffer), "maps/%s.bsp", mapBuffer); + return FileSize(mapBuffer); +} + +/** + * Copies the elements of a source vector to a destination vector. + * + * @param src Source vector. + * @param dest Destination vector. + */ +stock void CopyVector(const any src[3], any dest[3]) +{ + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; +} + +/** + * Returns whether the player is spectating. + * + * @param client Client index. + */ +stock bool IsSpectating(int client) +{ + int team = GetClientTeam(client); + return team == CS_TEAM_SPECTATOR || team == CS_TEAM_NONE; +} + +/** + * Rotate a vector on an axis. + * + * @param vec Vector to rotate. + * @param axis Axis to rotate around. + * @param theta Angle in radians. + * @param result Rotated vector. + */ +stock void RotateVectorAxis(float vec[3], float axis[3], float theta, float result[3]) +{ + float cosTheta = Cosine(theta); + float sinTheta = Sine(theta); + + float axisVecCross[3]; + GetVectorCrossProduct(axis, vec, axisVecCross); + + for (int i = 0; i < 3; i++) + { + result[i] = (vec[i] * cosTheta) + (axisVecCross[i] * sinTheta) + (axis[i] * GetVectorDotProduct(axis, vec)) * (1.0 - cosTheta); + } +} + +/** + * Rotate a vector by pitch and yaw. + * + * @param vec Vector to rotate. + * @param pitch Pitch angle (in degrees). + * @param yaw Yaw angle (in degrees). + * @param result Rotated vector. + */ +stock void RotateVectorPitchYaw(float vec[3], float pitch, float yaw, float result[3]) +{ + if (pitch != 0.0) + { + RotateVectorAxis(vec, view_as<float>({0.0, 1.0, 0.0}), DegToRad(pitch), result); + } + if (yaw != 0.0) + { + RotateVectorAxis(result, view_as<float>({0.0, 0.0, 1.0}), DegToRad(yaw), result); + } +} + +/** + * Attempts to return a valid spawn location. + * + * @param origin Spawn origin if found. + * @param angles Spawn angles if found. + * @return Whether a valid spawn point is found. + */ +stock bool GetValidSpawn(float origin[3], float angles[3]) +{ + // Return true if the spawn found is truly valid (not in the ground or out of bounds) + bool foundValidSpawn; + bool searchCT; + float spawnOrigin[3]; + float spawnAngles[3]; + int spawnEntity = -1; + while (!foundValidSpawn) + { + if (searchCT) + { + spawnEntity = FindEntityByClassname(spawnEntity, "info_player_counterterrorist"); + } + else + { + spawnEntity = FindEntityByClassname(spawnEntity, "info_player_terrorist"); + } + + if (spawnEntity != -1) + { + GetEntPropVector(spawnEntity, Prop_Data, "m_vecOrigin", spawnOrigin); + GetEntPropVector(spawnEntity, Prop_Data, "m_angRotation", spawnAngles); + if (IsSpawnValid(spawnOrigin)) + { + origin = spawnOrigin; + angles = spawnAngles; + foundValidSpawn = true; + } + } + else if (!searchCT) + { + searchCT = true; + } + else + { + break; + } + } + return foundValidSpawn; +} + +/** + * Check whether a position is a valid spawn location. + * A spawn location is considered valid if it is in bounds and not stuck inside the ground. + * + * @param origin Origin vector. + * @return Whether the origin is a valid spawn location. + */ +stock bool IsSpawnValid(float origin[3]) +{ + Handle trace = TR_TraceHullFilterEx(origin, origin, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + if (!TR_StartSolid(trace) && !TR_AllSolid(trace) && TR_GetFraction(trace) == 1.0) + { + delete trace; + return true; + } + delete trace; + return false; +} + +/** + * Get an entity's origin, angles, its bounding box's center and the distance from the center to its bounding box's edges. + * + * @param entity Index of the entity. + * @param origin Entity's origin. + * @param center Center of the entity's bounding box. + * @param angles Entity's angles. + * @param distFromCenter The distance between the center of the entity's bounding box and its edges. + */ +stock void GetEntityPositions(int entity, float origin[3], float center[3], float angles[3], float distFromCenter[3]) +{ + int ent = entity; + float maxs[3], mins[3]; + GetEntPropVector(ent, Prop_Send, "m_vecOrigin", origin); + // Take parent entities into account. + while (GetEntPropEnt(ent, Prop_Send, "moveparent") != -1) + { + ent = GetEntPropEnt(ent, Prop_Send, "moveparent"); + float tempOrigin[3]; + GetEntPropVector(ent, Prop_Send, "m_vecOrigin", tempOrigin); + for (int i = 0; i < 3; i++) + { + origin[i] += tempOrigin[i]; + } + } + + GetEntPropVector(ent, Prop_Data, "m_angRotation", angles); + + GetEntPropVector(ent, Prop_Send, "m_vecMaxs", maxs); + GetEntPropVector(ent, Prop_Send, "m_vecMins", mins); + for (int i = 0; i < 3; i++) + { + center[i] = origin[i] + (maxs[i] + mins[i]) / 2; + distFromCenter[i] = (maxs[i] - mins[i]) / 2; + } +} + +/** + * Find a valid position around a timer. + * + * @param entity Index of the timer entity. + * @param originDest Result origin if a valid position is found. + * @param anglesDest Result angles if a valid position is found. + * @return Whether a valid position is found. + */ +stock bool FindValidPositionAroundTimerEntity(int entity, float originDest[3], float anglesDest[3], bool isButton) +{ + float origin[3], center[3], angles[3], distFromCenter[3]; + GetEntityPositions(entity, origin, center, angles, distFromCenter); + float extraOffset[3]; + if (isButton) // Test several positions within button press range. + { + extraOffset[0] = 32.0; + extraOffset[1] = 32.0; + extraOffset[2] = 32.0; + } + else // Test positions at the inner surface of the zone. + { + extraOffset[0] = -(PLAYER_MAXS[0] - PLAYER_MINS[0]) - 1.03125; + extraOffset[1] = -(PLAYER_MAXS[1] - PLAYER_MINS[1]) - 1.03125; + extraOffset[2] = -(PLAYER_MAXS[2] - PLAYER_MINS[2]) - 1.03125; + } + if (FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest)) + { + return true; + } + // Test the positions right next to the timer button/zones if the tests above fail. + // This can fail when the timer has a cover brush over it. + extraOffset[0] = 0.03125; + extraOffset[1] = 0.03125; + extraOffset[2] = 0.03125; + return FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest); +} + +static bool FindValidPositionAroundCenter(float center[3], float distFromCenter[3], float extraOffset[3], float originDest[3], float anglesDest[3]) +{ + float testOrigin[3]; + int x, y; + + for (int i = 0; i < 3; i++) + { + // The search starts from the center then outwards to opposite directions. + x = i == 2 ? -1 : i; + for (int j = 0; j < 3; j++) + { + y = j == 2 ? -1 : j; + for (int z = -1; z <= 1; z++) + { + testOrigin = center; + testOrigin[0] = testOrigin[0] + (distFromCenter[0] + extraOffset[0]) * x + (PLAYER_MAXS[0] - PLAYER_MINS[0]) * x * 0.5; + testOrigin[1] = testOrigin[1] + (distFromCenter[1] + extraOffset[1]) * y + (PLAYER_MAXS[1] - PLAYER_MINS[1]) * y * 0.5; + testOrigin[2] = testOrigin[2] + (distFromCenter[2] + extraOffset[2]) * z + (PLAYER_MAXS[2] - PLAYER_MINS[2]) * z; + + // Check if there's a line of sight towards the zone as well. + if (IsSpawnValid(testOrigin) && CanSeeBox(testOrigin, center, distFromCenter)) + { + originDest = testOrigin; + // Always look towards the center. + float offsetVector[3]; + offsetVector[0] = -(distFromCenter[0] + extraOffset[0]) * x; + offsetVector[1] = -(distFromCenter[1] + extraOffset[1]) * y; + offsetVector[2] = -(distFromCenter[2] + extraOffset[2]) * z; + GetVectorAngles(offsetVector, anglesDest); + anglesDest[2] = 0.0; // Roll should always be 0.0 + return true; + } + } + } + } + return false; +} + +static bool CanSeeBox(float origin[3], float center[3], float distFromCenter[3]) +{ + float traceOrigin[3], traceDest[3], mins[3], maxs[3]; + + CopyVector(origin, traceOrigin); + + + SubtractVectors(center, distFromCenter, mins); + AddVectors(center, distFromCenter, maxs); + + for (int i = 0; i < 3; i++) + { + mins[i] += 0.03125; + maxs[i] -= 0.03125; + traceDest[i] = FloatClamp(traceOrigin[i], mins[i], maxs[i]); + } + int mask = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE; + Handle trace = TR_TraceRayFilterEx(traceOrigin, traceDest, mask, RayType_EndPoint, TraceEntityFilterPlayers); + if (TR_DidHit(trace)) + { + float end[3]; + TR_GetEndPosition(end, trace); + for (int i = 0; i < 3; i++) + { + if (end[i] != traceDest[i]) + { + delete trace; + return false; + } + } + } + delete trace; + return true; +} + +/** + * Gets entity index from the address to an entity. + * + * @param pEntity Entity address. + * @return Entity index. + * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle. + */ +stock int GOKZGetEntityFromAddress(Address pEntity) +{ + static int offs_RefEHandle; + if (offs_RefEHandle) + { + return EntRefToEntIndex(LoadFromAddress(pEntity + view_as<Address>(offs_RefEHandle), NumberType_Int32) | (1 << 31)); + } + + // if we don't have it already, attempt to lookup offset based on SDK information + // CWorld is derived from CBaseEntity so it should have both offsets + int offs_angRotation = FindDataMapInfo(0, "m_angRotation"), offs_vecViewOffset = FindDataMapInfo(0, "m_vecViewOffset"); + if (offs_angRotation == -1) + { + SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_angRotation"); + } + else if (offs_vecViewOffset == -1) + { + SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_vecViewOffset"); + } + else if ((offs_angRotation + 0x0C) != (offs_vecViewOffset - 0x04)) + { + char game[32]; + GetGameFolderName(game, sizeof(game)); + SetFailState("Could not confirm offset of CBaseEntity::m_RefEHandle (incorrect assumption for game '%s'?)", game); + } + + // offset seems right, cache it for the next call + offs_RefEHandle = offs_angRotation + 0x0C; + return GOKZGetEntityFromAddress(pEntity); +} + +/** + * Gets client index from CGameMovement class. + * + * @param addr Address of CGameMovement class. + * @param offsetCGameMovement_player Offset of CGameMovement::player. + * @return Client index. + * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle. + */ +stock int GOKZGetClientFromGameMovementAddress(Address addr, int offsetCGameMovement_player) +{ + Address playerAddr = view_as<Address>(LoadFromAddress(view_as<Address>(view_as<int>(addr) + offsetCGameMovement_player), NumberType_Int32)); + return GOKZGetEntityFromAddress(playerAddr); +} + +/** + * Gets the nearest point in the oriented bounding box of an entity to a point. + * + * @param entity Entity index. + * @param origin Point's origin. + * @param result Result point. + */ +stock void CalcNearestPoint(int entity, float origin[3], float result[3]) +{ + float entOrigin[3], entMins[3], entMaxs[3], trueMins[3], trueMaxs[3]; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", entOrigin); + GetEntPropVector(entity, Prop_Send, "m_vecMaxs", entMaxs); + GetEntPropVector(entity, Prop_Send, "m_vecMins", entMins); + + AddVectors(entOrigin, entMins, trueMins); + AddVectors(entOrigin, entMaxs, trueMaxs); + + for (int i = 0; i < 3; i++) + { + result[i] = FloatClamp(origin[i], trueMins[i], trueMaxs[i]); + } +} + +/** + * Get the shortest distance from P to the (infinite) line through vLineA and vLineB. + * + * @param P Point's origin. + * @param vLineA Origin of the first point of the line. + * @param vLineB Origin of the first point of the line. + * @return The shortest distance from the point to the line. + */ +stock float CalcDistanceToLine(float P[3], float vLineA[3], float vLineB[3]) +{ + float vClosest[3]; + float vDir[3]; + float t; + float delta[3]; + SubtractVectors(vLineB, vLineA, vDir); + float div = GetVectorDotProduct(vDir, vDir); + if (div < EPSILON) + { + t = 0.0; + } + else + { + t = (GetVectorDotProduct(vDir, P) - GetVectorDotProduct(vDir, vLineA)) / div; + } + for (int i = 0; i < 3; i++) + { + vClosest[i] = vLineA[i] + vDir[i]*t; + } + SubtractVectors(P, vClosest, delta); + return GetVectorLength(delta); +} + +/** + * Gets the ideal amount of time the text should be held for HUD messages. + * + * The message buffer is only 16 slots long, and it is shared between 6 channels maximum. + * Assuming a message is sent every game frame, each channel used should be only taking around 2.5 slots on average. + * This also assumes all channels are used equally (so no other plugin taking all the channel buffer for itself). + * We want to use as much of the message buffer as possible to take into account latency variances. + * + * @param interval HUD message update interval, in tick intervals. + * @return How long the text should be held for. + */ +stock float GetTextHoldTime(int interval) +{ + return 3 * interval * GetTickInterval(); +} diff --git a/sourcemod/scripting/include/gokz/anticheat.inc b/sourcemod/scripting/include/gokz/anticheat.inc new file mode 100644 index 0000000..7fa5409 --- /dev/null +++ b/sourcemod/scripting/include/gokz/anticheat.inc @@ -0,0 +1,168 @@ +/* + gokz-anticheat Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_anticheat_included_ +#endinput +#endif +#define _gokz_anticheat_included_ + + + +// =====[ ENUMS ]===== + +enum ACReason: +{ + ACReason_BhopMacro = 0, + ACReason_BhopHack, + ACREASON_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define AC_MAX_BUTTON_SAMPLES 40 +#define AC_MAX_BHOP_GROUND_TICKS 8 +#define AC_MAX_BHOP_SAMPLES 30 +#define AC_BINDEXCEPTION_SAMPLES 5 +#define AC_LOG_PATH "logs/gokz-anticheat.log" + +stock char gC_ACReasons[ACREASON_COUNT][] = +{ + "BHop Macro", + "BHop Hack" +}; + + + +// =====[ FORWARDS ]===== + +/** + * Called when gokz-anticheat suspects a player of cheating. + * + * @param client Client index. + * @param reason Reason for suspicion. + * @param notes Additional reasoning, description etc. + * @param stats Data supporting the suspicion e.g. scroll pattern. + */ +forward void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats); + + + +// =====[ NATIVES ]===== + +/** + * Gets the number of recent bhop samples available for a player. + * + * @param client Client index. + * @return Number of bhop samples available. + */ +native int GOKZ_AC_GetSampleSize(int client); + +/** + * Gets whether a player hit a perfect bhop for a number of + * recent bhops. Buffer must be large enough to fit the sample + * size. + * + * @param client Client index. + * @param buffer Buffer for perfect bhop booleans, with the first element being the most recent bhop. + * @param sampleSize Maximum recent bhop samples. + * @return Number of bhop samples. + */ +native int GOKZ_AC_GetHitPerf(int client, bool[] buffer, int sampleSize); + +/** + * Gets a player's number of perfect bhops out of a sample + * size of bhops. + * + * @param client Client index. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return Player's number of perfect bhops. + */ +native int GOKZ_AC_GetPerfCount(int client, int sampleSize); + +/** + * Gets a player's ratio of perfect bhops to normal bhops. + * + * @param client Client index. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return Player's ratio of perfect bhops to normal bhops. + */ +native float GOKZ_AC_GetPerfRatio(int client, int sampleSize); + +/** + * Gets a player's jump input counts for a number of recent + * bhops. Buffer must be large enough to fit the sample size. + * + * @param client Client index. + * @param buffer Buffer for jump input counts, with the first element being the most recent bhop. + * @param sampleSize Maximum recent bhop samples. + * @return Number of bhop samples. + */ +native int GOKZ_AC_GetJumpInputs(int client, int[] buffer, int sampleSize); + +/** + * Gets a player's average number of jump inputs for a number + * of recent bhops. + * + * @param client Client index. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return Player's average number of jump inputs. + */ +native float GOKZ_AC_GetAverageJumpInputs(int client, int sampleSize); + +/** + * Gets a player's jump input counts prior to a number of recent + * bhops. Buffer must be large enough to fit the sample size. + * Includes the jump input that resulted in the jump. + * + * @param client Client index. + * @param buffer Buffer for jump input counts, with the first element being the most recent bhop. + * @param sampleSize Maximum recent bhop samples. + * @return Number of bhop samples. + */ +native int GOKZ_AC_GetPreJumpInputs(int client, int[] buffer, int sampleSize); + +/** + * Gets a player's jump input counts after a number of recent + * bhops. Buffer must be large enough to fit the sample size. + * Excludes the jump input that resulted in the jump. + * + * @param client Client index. + * @param buffer Buffer for jump input counts, with the first element being the most recent bhop. + * @param sampleSize Maximum recent bhop samples. + * @return Number of bhop samples. + */ +native int GOKZ_AC_GetPostJumpInputs(int client, int[] buffer, int sampleSize); + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_anticheat = +{ + name = "gokz-anticheat", + file = "gokz-anticheat.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_anticheat_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_AC_GetSampleSize"); + MarkNativeAsOptional("GOKZ_AC_GetHitPerf"); + MarkNativeAsOptional("GOKZ_AC_GetPerfCount"); + MarkNativeAsOptional("GOKZ_AC_GetPerfRatio"); + MarkNativeAsOptional("GOKZ_AC_GetJumpInputs"); + MarkNativeAsOptional("GOKZ_AC_GetAverageJumpInputs"); + MarkNativeAsOptional("GOKZ_AC_GetPreJumpInputs"); + MarkNativeAsOptional("GOKZ_AC_GetPostJumpInputs"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/chat.inc b/sourcemod/scripting/include/gokz/chat.inc new file mode 100644 index 0000000..0264a57 --- /dev/null +++ b/sourcemod/scripting/include/gokz/chat.inc @@ -0,0 +1,45 @@ +/* + gokz-chat Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_chat_included_ +#endinput +#endif +#define _gokz_chat_included_ + + + +// =====[ NATIVES ]===== + +/** + * Gets whether a mode is loaded. + * + * @param client Client. + * @param tag Tag to prepend to the player name in chat. + * @param color Color to use for the tag. + */ +native void GOKZ_CH_SetChatTag(int client, const char[] tag, const char[] color); + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_chat = +{ + name = "gokz-chat", + file = "gokz-chat.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_chat_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_CH_SetChatTag"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/core.inc b/sourcemod/scripting/include/gokz/core.inc new file mode 100644 index 0000000..fb450d1 --- /dev/null +++ b/sourcemod/scripting/include/gokz/core.inc @@ -0,0 +1,1920 @@ +/* + gokz-core Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_core_included_ +#endinput +#endif +#define _gokz_core_included_ + +#include <cstrike> +#include <regex> +#include <topmenus> + +#include <gokz> + + + +// =====[ ENUMS ]===== + +enum +{ + TimeType_Nub = 0, + TimeType_Pro, + TIMETYPE_COUNT +}; + +enum +{ + MapPrefix_Other = 0, + MapPrefix_KZPro, + MAPPREFIX_COUNT +}; + +enum StartPositionType: +{ + StartPositionType_Spawn, + StartPositionType_Custom, + StartPositionType_MapButton, + StartPositionType_MapStart, + STARTPOSITIONTYPE_COUNT +}; + +enum CourseTimerType: +{ + CourseTimerType_None, + CourseTimerType_Default, + CourseTimerType_Button, + CourseTimerType_ZoneLegacy, + CourseTimerType_ZoneNew, + CourseTimerType_COUNT +}; + +enum OptionProp: +{ + OptionProp_Cookie = 0, + OptionProp_Type, + OptionProp_DefaultValue, + OptionProp_MinValue, + OptionProp_MaxValue, + OPTIONPROP_COUNT +}; + +enum OptionType: +{ + OptionType_Int = 0, + OptionType_Float +}; + +enum Option: +{ + OPTION_INVALID = -1, + Option_Mode, + Option_Style, + Option_CheckpointMessages, + Option_CheckpointSounds, + Option_TeleportSounds, + Option_ErrorSounds, + Option_VirtualButtonIndicators, + Option_TimerButtonZoneType, + Option_ButtonThroughPlayers, + Option_Safeguard, + OPTION_COUNT +}; + +enum +{ + Mode_Vanilla = 0, + Mode_SimpleKZ, + Mode_KZTimer, + MODE_COUNT +}; + +enum +{ + Style_Normal = 0, + STYLE_COUNT +}; + +enum +{ + CheckpointMessages_Disabled = 0, + CheckpointMessages_Enabled, + CHECKPOINTMESSAGES_COUNT +}; + +enum +{ + CheckpointSounds_Disabled = 0, + CheckpointSounds_Enabled, + CHECKPOINTSOUNDS_COUNT +}; + +enum +{ + TeleportSounds_Disabled = 0, + TeleportSounds_Enabled, + TELEPORTSOUNDS_COUNT +}; + +enum +{ + ErrorSounds_Disabled = 0, + ErrorSounds_Enabled, + ERRORSOUNDS_COUNT +}; + +enum +{ + VirtualButtonIndicators_Disabled = 0, + VirtualButtonIndicators_Enabled, + VIRTUALBUTTONINDICATORS_COUNT +}; + +enum +{ + TimerButtonZoneType_BothButtons = 0, + TimerButtonZoneType_EndZone, + TimerButtonZoneType_BothZones, + TIMERBUTTONZONETYPE_COUNT +}; + +enum +{ + ButtonThroughPlayers_Disabled = 0, + ButtonThroughPlayers_Enabled, + BUTTONTHROUGHPLAYERS_COUNT +}; + +enum +{ + Safeguard_Disabled = 0, + Safeguard_EnabledNUB, + Safeguard_EnabledPRO, + SAFEGUARD_COUNT +}; + +enum +{ + ModeCVar_Accelerate = 0, + ModeCVar_AccelerateUseWeaponSpeed, + ModeCVar_AirAccelerate, + ModeCVar_AirMaxWishSpeed, + ModeCVar_EnableBunnyhopping, + ModeCVar_Friction, + ModeCVar_Gravity, + ModeCVar_JumpImpulse, + ModeCVar_LadderScaleSpeed, + ModeCVar_LedgeMantleHelper, + ModeCVar_MaxSpeed, + ModeCVar_MaxVelocity, + ModeCVar_StaminaJumpCost, + ModeCVar_StaminaLandCost, + ModeCVar_StaminaMax, + ModeCVar_StaminaRecoveryRate, + ModeCVar_StandableNormal, + ModeCVar_TimeBetweenDucks, + ModeCVar_WalkableNormal, + ModeCVar_WaterAccelerate, + MoveCVar_WaterMoveSpeedMultiplier, + MoveCVar_WaterSwimMode, + MoveCVar_WeaponEncumbrancePerItem, + ModeCVar_WeaponEncumbranceScale, + MODECVAR_COUNT +}; + +// NOTE: gokz-core/map/entlump.sp +enum EntlumpTokenType +{ + EntlumpTokenType_OpenBrace, // { + EntlumpTokenType_CloseBrace, // } + EntlumpTokenType_Identifier, // everything that's inside quotations + EntlumpTokenType_Unknown, + EntlumpTokenType_EndOfStream +}; + +// NOTE: gokz-core/map/triggers.sp +// NOTE: corresponds to climb_teleport_type in kz_mapping_api.fgd +enum TeleportType +{ + TeleportType_Invalid = -1, + TeleportType_Normal, + TeleportType_MultiBhop, + TeleportType_SingleBhop, + TeleportType_SequentialBhop, + TELEPORTTYPE_COUNT +}; + +enum TriggerType +{ + TriggerType_Invalid = 0, + TriggerType_Teleport, + TriggerType_Antibhop +}; + + + +// =====[ CONSTANTS ]===== + +#define GOKZ_CHECKPOINT_VERSION 2 +#define GOKZ_MAX_CHECKPOINTS 2048 +#define GOKZ_MAX_COURSES 100 + +#define GOKZ_BHOP_NO_CHECKPOINT_TIME 0.15 +#define GOKZ_MULT_NO_CHECKPOINT_TIME 0.11 +#define GOKZ_LADDER_NO_CHECKPOINT_TIME 1.5 +#define GOKZ_PAUSE_COOLDOWN 1.0 +#define GOKZ_TIMER_START_NO_TELEPORT_TICKS 4 +#define GOKZ_TIMER_START_GROUND_TICKS 4 +#define GOKZ_TIMER_START_NOCLIP_TICKS 4 +#define GOKZ_JUMPSTATS_NOCLIP_RESET_TICKS 4 +#define GOKZ_TIMER_SOUND_COOLDOWN 0.15 +#define GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME 2.0 +#define GOKZ_TURNBIND_COOLDOWN 0.3 + +#define GOKZ_MAPPING_API_VERSION_NONE 0 // the map doesn't have a mapping api version +#define GOKZ_MAPPING_API_VERSION 1 + +#define GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY 0.2 +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE TeleportType_Normal +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY 0.0 +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES true +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED true +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION false +#define GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER false +#define GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY 0.08 + +#define GOKZ_SOUND_CHECKPOINT "buttons/blip1.wav" +#define GOKZ_SOUND_TELEPORT "buttons/blip1.wav" +#define GOKZ_SOUND_TIMER_STOP "buttons/button18.wav" + +#define GOKZ_START_NAME "climb_start" +#define GOKZ_BONUS_START_NAME_REGEX "^climb_bonus(\\d+)_start$" +#define GOKZ_BONUS_END_NAME_REGEX "^climb_bonus(\\d+)_end$" + +#define GOKZ_START_BUTTON_NAME "climb_startbutton" +#define GOKZ_END_BUTTON_NAME "climb_endbutton" +#define GOKZ_BONUS_START_BUTTON_NAME_REGEX "^climb_bonus(\\d+)_startbutton$" +#define GOKZ_BONUS_END_BUTTON_NAME_REGEX "^climb_bonus(\\d+)_endbutton$" +#define GOKZ_ANTI_BHOP_TRIGGER_NAME "climb_anti_bhop" +#define GOKZ_ANTI_CP_TRIGGER_NAME "climb_anti_checkpoint" +#define GOKZ_ANTI_PAUSE_TRIGGER_NAME "climb_anti_pause" +#define GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME "climb_anti_jumpstat" +#define GOKZ_BHOP_RESET_TRIGGER_NAME "climb_bhop_reset" +#define GOKZ_TELEPORT_TRIGGER_NAME "climb_teleport" + +#define GOKZ_START_ZONE_NAME "climb_startzone" +#define GOKZ_END_ZONE_NAME "climb_endzone" +#define GOKZ_BONUS_START_ZONE_NAME_REGEX "^climb_bonus(\\d+)_startzone$" +#define GOKZ_BONUS_END_ZONE_NAME_REGEX "^climb_bonus(\\d+)_endzone$" + +#define GOKZ_CFG_SERVER "sourcemod/gokz/gokz.cfg" +#define GOKZ_CFG_OPTIONS "cfg/sourcemod/gokz/options.cfg" +#define GOKZ_CFG_OPTIONS_SORTING "cfg/sourcemod/gokz/options_menu_sorting.cfg" +#define GOKZ_CFG_OPTIONS_ROOT "Options" +#define GOKZ_CFG_OPTIONS_DESCRIPTION "description" +#define GOKZ_CFG_OPTIONS_DEFAULT "default" + +#define GOKZ_OPTION_MAX_NAME_LENGTH 30 +#define GOKZ_OPTION_MAX_DESC_LENGTH 255 +#define GENERAL_OPTION_CATEGORY "General" + +// TODO: where do i put the defines? +#define GOKZ_BSP_HEADER_IDENTIFIER (('P' << 24) | ('S' << 16) | ('B' << 8) | 'V') +#define GOKZ_ENTLUMP_MAX_KEY 32 +#define GOKZ_ENTLUMP_MAX_VALUE 1024 + +#define GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH 256 + +#define CHAR_ESCAPE view_as<char>(27) + +#define GOKZ_SAFEGUARD_RESTART_MIN_DELAY 0.6 +#define GOKZ_SAFEGUARD_RESTART_MAX_DELAY 5.0 + +// Prevents the player from retouching a trigger too often. +#define GOKZ_MAX_RETOUCH_TRIGGER_COUNT 4 + +stock char gC_TimeTypeNames[TIMETYPE_COUNT][] = +{ + "NUB", + "PRO" +}; + +stock char gC_ModeNames[MODE_COUNT][] = +{ + "Vanilla", + "SimpleKZ", + "KZTimer" +}; + +stock char gC_ModeNamesShort[MODE_COUNT][] = +{ + "VNL", + "SKZ", + "KZT" +}; + +stock char gC_ModeKeys[MODE_COUNT][] = +{ + "vanilla", + "simplekz", + "kztimer" +}; + +stock float gF_ModeVirtualButtonRanges[MODE_COUNT] = +{ + 0.0, + 32.0, + 70.0 +}; + +stock char gC_ModeStartSounds[MODE_COUNT][] = +{ + "common/wpn_select.wav", + "buttons/button9.wav", + "buttons/button3.wav" +}; + +stock char gC_ModeEndSounds[MODE_COUNT][] = +{ + "common/wpn_select.wav", + "buttons/bell1.wav", + "buttons/button3.wav" +}; + +stock char gC_ModeFalseEndSounds[MODE_COUNT][] = +{ + "common/wpn_select.wav", + "buttons/button11.wav", + "buttons/button2.wav" +}; + +stock char gC_StyleNames[STYLE_COUNT][] = +{ + "Normal" +}; + +stock char gC_StyleNamesShort[STYLE_COUNT][] = +{ + "NRM" +}; + +stock char gC_CoreOptionNames[OPTION_COUNT][] = +{ + "GOKZ - Mode", + "GOKZ - Style", + "GOKZ - Checkpoint Messages", + "GOKZ - Checkpoint Sounds", + "GOKZ - Teleport Sounds", + "GOKZ - Error Sounds", + "GOKZ - VB Indicators", + "GOKZ - Timer Button Zone Type", + "GOKZ - Button Through Players", + "GOKZ - Safeguard" +}; + +stock char gC_CoreOptionDescriptions[OPTION_COUNT][] = +{ + "Movement Mode - 0 = Vanilla, 1 = SimpleKZ, 2 = KZTimer", + "Movement Style - 0 = Normal", + "Checkpoint Messages - 0 = Disabled, 1 = Enabled", + "Checkpoint Sounds - 0 = Disabled, 1 = Enabled", + "Teleport Sounds - 0 = Disabled, 1 = Enabled", + "Error Sounds - 0 = Disabled, 1 = Enabled", + "Virtual Button Indicators - 0 = Disabled, 1 = Enabled", + "Timer Button Zone Type - 0 = Both buttons, 1 = Only end zone, 2 = Both zones", + "Button Through Players - 0 = Disabled, 1 = Enabled", + "Safeguard - 0 = Disabled, 1 = Enabled (NUB), 2 = Enabled (PRO)" +}; + +stock char gC_CoreOptionPhrases[OPTION_COUNT][] = +{ + "Options Menu - Mode", + "Options Menu - Style", + "Options Menu - Checkpoint Messages", + "Options Menu - Checkpoint Sounds", + "Options Menu - Teleport Sounds", + "Options Menu - Error Sounds", + "Options Menu - Virtual Button Indicators", + "Options Menu - Timer Button Zone Type", + "Options Menu - Button Through Players", + "Options Menu - Safeguard" +}; + +stock char gC_TimerButtonZoneTypePhrases[TIMERBUTTONZONETYPE_COUNT][] = +{ + "Timer Button Zone Type - Both Buttons", + "Timer Button Zone Type - Only End Zone", + "Timer Button Zone Type - Both Zones" +}; + +stock char gC_SafeGuardPhrases[SAFEGUARD_COUNT][] = +{ + "Options Menu - Disabled", + "Safeguard - Enabled NUB", + "Safeguard - Enabled PRO" +} + +stock int gI_CoreOptionCounts[OPTION_COUNT] = +{ + MODE_COUNT, + STYLE_COUNT, + CHECKPOINTMESSAGES_COUNT, + CHECKPOINTSOUNDS_COUNT, + TELEPORTSOUNDS_COUNT, + ERRORSOUNDS_COUNT, + VIRTUALBUTTONINDICATORS_COUNT, + TIMERBUTTONZONETYPE_COUNT, + BUTTONTHROUGHPLAYERS_COUNT, + SAFEGUARD_COUNT +}; + +stock int gI_CoreOptionDefaults[OPTION_COUNT] = +{ + Mode_KZTimer, + Style_Normal, + CheckpointMessages_Disabled, + CheckpointSounds_Enabled, + TeleportSounds_Disabled, + ErrorSounds_Enabled, + VirtualButtonIndicators_Disabled, + TimerButtonZoneType_BothButtons, + ButtonThroughPlayers_Enabled, + Safeguard_Disabled +}; + +stock char gC_ModeCVars[MODECVAR_COUNT][] = +{ + "sv_accelerate", + "sv_accelerate_use_weapon_speed", + "sv_airaccelerate", + "sv_air_max_wishspeed", + "sv_enablebunnyhopping", + "sv_friction", + "sv_gravity", + "sv_jump_impulse", + "sv_ladder_scale_speed", + "sv_ledge_mantle_helper", + "sv_maxspeed", + "sv_maxvelocity", + "sv_staminajumpcost", + "sv_staminalandcost", + "sv_staminamax", + "sv_staminarecoveryrate", + "sv_standable_normal", + "sv_timebetweenducks", + "sv_walkable_normal", + "sv_wateraccelerate", + "sv_water_movespeed_multiplier", + "sv_water_swim_mode", + "sv_weapon_encumbrance_per_item", + "sv_weapon_encumbrance_scale" +}; + + +// =====[ STRUCTS ]===== + +enum struct Checkpoint +{ + float origin[3]; + float angles[3]; + float ladderNormal[3]; + bool onLadder; + int groundEnt; + + void Create(int client) + { + Movement_GetOrigin(client, this.origin); + Movement_GetEyeAngles(client, this.angles); + GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", this.ladderNormal); + this.onLadder = Movement_GetMovetype(client) == MOVETYPE_LADDER; + this.groundEnt = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity"); + } +} + +enum struct UndoTeleportData +{ + float tempOrigin[3]; + float tempAngles[3]; + float origin[3]; + float angles[3]; + // Undo TP properties + bool lastTeleportOnGround; + bool lastTeleportInBhopTrigger; + bool lastTeleportInAntiCpTrigger; + + void Init(int client, bool lastTeleportInBhopTrigger, bool lastTeleportOnGround, bool lastTeleportInAntiCpTrigger) + { + Movement_GetOrigin(client, this.tempOrigin); + Movement_GetEyeAngles(client, this.tempAngles); + this.lastTeleportInBhopTrigger = lastTeleportInBhopTrigger; + this.lastTeleportOnGround = lastTeleportOnGround; + this.lastTeleportInAntiCpTrigger = lastTeleportInAntiCpTrigger; + } + + void Update() + { + this.origin = this.tempOrigin; + this.angles = this.tempAngles; + } +} + + +// NOTE: gokz-core/map/entlump.sp +enum struct EntlumpToken +{ + EntlumpTokenType type; + char string[GOKZ_ENTLUMP_MAX_VALUE]; +} + +// NOTE: gokz-core/map/triggers.sp +enum struct AntiBhopTrigger +{ + int entRef; + int hammerID; + float time; +} + +enum struct TeleportTrigger +{ + int hammerID; + TeleportType type; + float delay; + char tpDestination[256]; + bool useDestAngles; + bool resetSpeed; + bool relativeDestination; + bool reorientPlayer; +} + +enum struct TouchedTrigger +{ + TriggerType triggerType; + int entRef; // entref of one of the TeleportTriggers + int startTouchTick; // tick where the player touched the trigger + int groundTouchTick; // tick where the player touched the ground +} + +// Legacy triggers that activate timer buttons. +enum struct TimerButtonTrigger +{ + int hammerID; + int course; + bool isStartTimer; +} + +// =====[ FORWARDS ]===== + +/** + * Called when a player's options values are loaded from clientprefs. + * + * @param client Client index. + */ +forward void GOKZ_OnOptionsLoaded(int client); + +/** + * Called when a player's option's value is changed. + * Only called if client is in game. + * + * @param client Client index. + * @param option Option name. + * @param newValue New value of the option. + */ +forward void GOKZ_OnOptionChanged(int client, const char[] option, any newValue); + +/** + * Called when a player starts their timer. + * + * @param client Client index. + * @param course Course number. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTimerStart(int client, int course); + +/** + * Called when a player has started their timer. + * + * @param client Client index. + * @param course Course number. + */ +forward void GOKZ_OnTimerStart_Post(int client, int course); + +/** + * Called when a player ends their timer. + * + * @param client Client index. + * @param course Course number. + * @param time Player's end time. + * @param teleportsUsed Number of teleports used by player. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTimerEnd(int client, int course, float time, int teleportsUsed); + +/** + * Called when a player has ended their timer. + * + * @param client Client index. + * @param course Course number. + * @param time Player's end time. + * @param teleportsUsed Number of teleports used by player. + */ +forward void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed); + +/** + * Called when the end timer message is printed to chat. + * + * @param client Client index. + * @param course Course number. + * @param time Player's end time. + * @param teleportsUsed Number of teleports used by player. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed); + +/** + * Called when a player's timer has been forcefully stopped. + * + * @param client Client index. + */ +forward void GOKZ_OnTimerStopped(int client); + +/** + * Called when a player pauses. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnPause(int client); + +/** + * Called when a player has paused. + * + * @param client Client index. + */ +forward void GOKZ_OnPause_Post(int client); + +/** + * Called when a player resumes. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnResume(int client); + +/** + * Called when a player has resumed. + * + * @param client Client index. + */ +forward void GOKZ_OnResume_Post(int client); + +/** + * Called when a player makes a checkpoint. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnMakeCheckpoint(int client); + +/** + * Called when a player has made a checkpoint. + * + * @param client Client index. + */ +forward void GOKZ_OnMakeCheckpoint_Post(int client); + +/** + * Called when a player teleports to their checkpoint. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTeleportToCheckpoint(int client); + +/** + * Called when a player has teleported to their checkpoint. + * + * @param client Client index. + */ +forward void GOKZ_OnTeleportToCheckpoint_Post(int client); + +/** + * Called when a player goes to a previous checkpoint. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnPrevCheckpoint(int client); + +/** + * Called when a player has gone to a previous checkpoint. + * + * @param client Client index. + */ +forward void GOKZ_OnPrevCheckpoint_Post(int client); + +/** + * Called when a player goes to a next checkpoint. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnNextCheckpoint(int client); + +/** + * Called when a player has gone to a next checkpoint. + * + * @param client Client index. + */ +forward void GOKZ_OnNextCheckpoint_Post(int client); + +/** + * Called when a player teleports to start. + * + * @param client Client index. + * @param course Course index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTeleportToStart(int client, int course); + +/** + * Called when a player has teleported to start. + * + * @param client Client index. + * @param course Course index. + */ +forward void GOKZ_OnTeleportToStart_Post(int client, int course); + +/** + * Called when a player teleports to end. + * + * @param client Client index. + * @param course Course index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTeleportToEnd(int client, int course); + +/** + * Called when a player has teleported to end. + * + * @param client Client index. + * @param course Course index. + */ +forward void GOKZ_OnTeleportToEnd_Post(int client, int course); + +/** + * Called when a player undoes a teleport. + * + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnUndoTeleport(int client); + +/** + * Called when a player has undone a teleport. + * + * @param client Client index. + */ +forward void GOKZ_OnUndoTeleport_Post(int client); + +/** + * Called when a player has performed a counted teleport (teleport count went up) + * i.e. a catch-all for teleport to checkpoint, teleport to start, undo teleport etc. + * + * @param client Client index. + */ +forward void GOKZ_OnCountedTeleport_Post(int client); + +/** + * Called when a player's start position is set. + * + * @param client Client index. + * @param type Start position type. + * @param origin Start position origin. + * @param angles Start position eye angles. + */ +forward void GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3]); + +/** + * Called when player's begins a jump that is deemed valid. + * A jump is deemed invalid if a player is teleported. + * + * @param client Client index. + * @param jumped Whether player jumped. + * @param ladderJump Whether it was a ladder jump. + * @param jumpbug Whether player performed a jumpbug. + */ +forward void GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug); + +/** + * Called when player's current jump is invalidated. + * A jump is deemed invalid if a player is teleported. + * + * @param client Client index. + */ +forward void GOKZ_OnJumpInvalidated(int client); + +/** + * Called when a player has been switched to a team. + * + * @param client Client index. + */ +forward void GOKZ_OnJoinTeam(int client, int team); + +/** + * Called the first time a player spawns in on a team. + * + * @param client Client index. + */ +forward void GOKZ_OnFirstSpawn(int client); + +/** + * Called when a mode has been loaded. + * + * @param mode Mode loaded. + */ +forward void GOKZ_OnModeLoaded(int mode); + +/** + * Called when a mode has been unloaded. + * + * @param mode Mode unloaded. + */ +forward void GOKZ_OnModeUnloaded(int mode); + +/** + * Called when a plugin other than gokz-core calls a native + * that may affect a player's timer or teleport count in + * their favour e.g. GOKZ_StartTimer, GOKZ_EndTimer, + * GOKZ_SetTime and GOKZ_SetTeleportCount. + * + * @param plugin Handle of the calling plugin. + * @param client Client index. + * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed. + */ +forward Action GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client); + +/** + * Called when the options menu has been created and 3rd + * party plugins can grab the handle or add categories. + * + * @param topMenu Options top menu handle. + */ +forward void GOKZ_OnOptionsMenuCreated(TopMenu topMenu); + +/** + * Called when the options menu is ready to have items added. + * + * @param topMenu Options top menu handle. + */ +forward void GOKZ_OnOptionsMenuReady(TopMenu topMenu); + +/** + * Called when a course is registered. A course is registered if both the + * start and end of it (e.g. timer buttons) have been detected. + * + * @param course Course number. + */ +forward void GOKZ_OnCourseRegistered(int course); + +/** + * Called when a player's run becomes invalidated. + * An invalidated run doesn't necessarily stop the timer. + * + * @param client Client index. + */ +forward void GOKZ_OnRunInvalidated(int client); + +/** + * Called when a sound is emitted to the client via GOKZ Core. + * + * @param client Client index. + * @param sample Sound file name relative to the "sound" folder. + * @param volume Sound volume. + * @param description Optional description. + * @return Plugin_Continue to allow the sound to be played, Plugin_Stop to block it, + * Plugin_Changed when any parameter has been modified. + */ +forward Action GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description); + + +// =====[ NATIVES ]===== + +/** + * Gets whether a mode is loaded. + * + * @param mode Mode. + * @return Whether mode is loaded. + */ +native bool GOKZ_GetModeLoaded(int mode); + +/** + * Gets the version number of a loaded mode. + * + * @param mode Mode. + * @return Version number of the mode, or -1 if not loaded. + */ +native int GOKZ_GetModeVersion(int mode); + +/** + * Sets whether a mode is loaded. To be used by mode plugins. + * + * @param mode Mode. + * @param loaded Whether mode is loaded. + * @param version Version number of the mode. + */ +native void GOKZ_SetModeLoaded(int mode, bool loaded, int version = -1); + +/** + * Gets the total number of loaded modes. + * + * @return Number of loaded modes. + */ +native int GOKZ_GetLoadedModeCount(); + +/** + * Sets the player's current mode. + * If the player's timer is running, it will be stopped. + * + * @param client Client index. + * @param mode Mode. + * @return Whether the operation was successful. + */ +native bool GOKZ_SetMode(int client, int mode); + +/** + * Gets the Handle to the options top menu. + * + * @return Handle to the options top menu, + * or null if not created yet. + */ +native TopMenu GOKZ_GetOptionsTopMenu(); + +/** + * Gets whether a course is registered. A course is registered if both the + * start and end of it (e.g. timer buttons) have been detected. + * + * @param course Course number. + * @return Whether course has been registered. + */ +native bool GOKZ_GetCourseRegistered(int course); + +/** + * Prints a message to a client's chat, formatting colours and optionally + * adding the chat prefix. If using the chat prefix, specify a colour at + * the beginning of the message e.g. "{default}Hello!". + * + * @param client Client index. + * @param addPrefix Whether to add the chat prefix. + * @param format Formatting rules. + * @param any Variable number of format parameters. + */ +native void GOKZ_PrintToChat(int client, bool addPrefix, const char[] format, any...); + +/** + * Prints a message to a client's chat, formatting colours and optionally + * adding the chat prefix. If using the chat prefix, specify a colour at + * the beginning of the message e.g. "{default}Hello!". Also prints the + * message to the server log. + * + * @param client Client index. + * @param addPrefix Whether to add the chat prefix. + * @param format Formatting rules. + * @param any Variable number of format parameters. + */ +native void GOKZ_PrintToChatAndLog(int client, bool addPrefix, const char[] format, any...); + +/** + * Starts a player's timer for a course on the current map. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param course Course number. + * @param allowMidair Whether player is allowed to start timer midair. + * @return Whether player's timer was started. + */ +native bool GOKZ_StartTimer(int client, int course, bool allowOffGround = false); + +/** + * Ends a player's timer for a course on the current map. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param course Course number. + * @return Whether player's timer was ended. + */ +native bool GOKZ_EndTimer(int client, int course); + +/** + * Forces a player's timer to stop. Intended for run invalidation. + * + * @param client Client index. + * @param playSound Whether to play the timer stop sound. + * @return Whether player's timer was stopped. + */ +native bool GOKZ_StopTimer(int client, bool playSound = true); + +/** + * Forces all players' timers to stop. Intended for run invalidation. + * + * @param playSound Whether to play the timer stop sound. + */ +native void GOKZ_StopTimerAll(bool playSound = true); + +/** + * Gets whether or not a player's timer is running i.e. isn't 'stopped'. + * + * @param client Client index. + * @return Whether player's timer is running. + */ +native bool GOKZ_GetTimerRunning(int client); + +/** + * Gets whether or not a player's timer is valid i.e the run is a valid run. + * + * @param client Client index. + * @return Whether player's timer is running. + */ +native bool GOKZ_GetValidTimer(int client); + +/** + * Gets the course a player is currently running. + * + * @param client Client index. + * @return Course number. + */ +native int GOKZ_GetCourse(int client); + +/** + * Set the player's current course. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param course Course number. + * @return Whether native was allowed to proceed. + */ +native bool GOKZ_SetCourse(int client, int course); + +/** + * Gets whether a player is paused. + * + * @param client Client index. + * @return Whether player is paused. + */ +native bool GOKZ_GetPaused(int client); + +/** + * Gets a player's current run time. + * + * @param client Client index. + * @return Player's current run time. + */ +native float GOKZ_GetTime(int client); + +/** + * Gets a player's current run time. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param time Run time to set to. + * @return Whether native was allowed to proceed. + */ +native bool GOKZ_SetTime(int client, float time); + +/** + * Mark a player's run as invalid without stopping the timer. + * + * @param client Client index. + */ +native void GOKZ_InvalidateRun(int client); + +/** + * Gets a player's current checkpoint count. + * + * @param client Client index. + * @return Player's current checkpoint count. + */ +native int GOKZ_GetCheckpointCount(int client); + +/** + * Sets a player's current checkpoint count. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param cpCount Checkpoint count to set to. + * @return Whether native was allowed to proceed. + */ +native int GOKZ_SetCheckpointCount(int client, int cpCount); + +/** + * Gets checkpoint data of a player. + * + * @param client Client index. + * @return Client's checkpoint data. + */ +native ArrayList GOKZ_GetCheckpointData(int client); + +/** + * Sets checkpoint data of a player. The checkpoint data is assumed to be ordered. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param checkpoints Checkpoint data. + * @param version Checkpoint version. + * @return Whether native was allowed to proceed and operation was successful. + */ +native bool GOKZ_SetCheckpointData(int client, ArrayList checkpoints, int version); + +/** + * Get undo teleport data of a player. + * + * @param client Client index. + * @return ArrayList of length 1 containing player's undo teleport data. + */ +native ArrayList GOKZ_GetUndoTeleportData(int client); + +/** + * Set undo teleport data of a player. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param undoTeleportDataArray ArrayList of length 1 containing player's undo teleport data. + * @param version Checkpoint version. + * @return Whether native was allowed to proceed and operation was successful. + */ +native bool GOKZ_SetUndoTeleportData(int client, ArrayList undoTeleportDataArray, int version); + +/** + * Gets a player's current teleport count. + * + * @param client Client index. + * @return Player's current teleport count. + */ +native int GOKZ_GetTeleportCount(int client); + +/** + * Sets a player's current teleport count. + * This can be blocked by OnTimerNativeCalledExternally(). + * + * @param client Client index. + * @param tpCount Teleport count to set to. + * @return Whether native was allowed to proceed. + */ +native bool GOKZ_SetTeleportCount(int client, int tpCount); + +/** + * Teleports a player to start, or respawns them. + * + * @param client Client index. + */ +native void GOKZ_TeleportToStart(int client); + +/** + * Teleports a player to the start zone/button of the specified course. + * + * @param client Client index. + * @param course Course index. + */ +native void GOKZ_TeleportToSearchStart(int client, int course); + +/** + * Gets the virtual button position the player currently has. + * + * @param client Client index. + * @param position Returns the client's virtual button position. + * @param isStart True to get the start button position, false for the end button. + * @return The course the button belongs to. + */ +native int GOKZ_GetVirtualButtonPosition(int client, float position[3], bool isStart); + +/** + * Sets the virtual button position the player currently has. + * + * @param client Client index. + * @param position The client's virtual button position. + * @param course The course the virtual button belongs to. + * @param isStart True to get the start button position, false for the end button. + */ +native void GOKZ_SetVirtualButtonPosition(int client, const float position[3], int course, bool isStart); + +/** + * Resets the player's virtual button. + * + * @param client Client index. + * @param isStart True to get the start button position, false for the end button. + */ +native void GOKZ_ResetVirtualButtonPosition(int client, bool isStart); + +/** + * Locks the virtual button position of a player. + * + * @param client Client index. + */ +native void GOKZ_LockVirtualButtons(int client); + +/** + * Gets the start position the player currently has. + * + * @param client Client index. + * @param position Returns the client's start position. + * @param angles Returns the client's start angles. + * @return Player's current start position type. + */ +native StartPositionType GOKZ_GetStartPosition(int client, float position[3], float angles[3]); + +/** + * Sets the start position the player currently has. + * + * @param client Client index. + * @param type The start position type. + * @param position The client's start position. + * @param angles The client's start angles. + */ +native void GOKZ_SetStartPosition(int client, StartPositionType type, const float position[3], const float angles[3]); + +/** + * Gets the type of start position the player currently has. + * The "Spawn" type means teleport to start will respawn the player. + * + * @param client Client index. + * @return Player's current start position type. + */ +native StartPositionType GOKZ_GetStartPositionType(int client); + +/** + * Set the start position of the player to the start of a course. + * + * @param client Client index. + * @param course Course index. + * + * @return False if the course start was not found. + */ +native bool GOKZ_SetStartPositionToMapStart(int client, int course); + +/** + * Teleports a player to end. + * + * @param client Client index. + * @param course Course index. + */ +native void GOKZ_TeleportToEnd(int client, int course); + +/** + * Set a new checkpoint at a player's current position. + * + * @param client Client index. + */ +native void GOKZ_MakeCheckpoint(int client); + +/** + * Gets whether a player can make a new checkpoint. + * @param client Client index. + * @return Whether player can set a checkpoint. + */ +native bool GOKZ_GetCanMakeCheckpoint(int client); + +/** + * Teleports a player to their last checkpoint. + * + * @param client Client index. + */ +native void GOKZ_TeleportToCheckpoint(int client); + +/** + * Gets whether a player can teleport to their checkpoint + * e.g. will return false if player has no checkpoints. + * + * @param client Client index. + * @return Whether player can teleport to checkpoint. + */ +native bool GOKZ_GetCanTeleportToCheckpoint(int client); + +/** + * Teleport a player back to a previous checkpoint. + * + * @param client Client index. + */ +native void GOKZ_PrevCheckpoint(int client); + +/** + * Gets whether a player can go to their previous checkpoint + * e.g. will return false if player has no checkpoints. + * + * @param client Client index. + * @return Whether player can go to previous checkpoint. + */ +native bool GOKZ_GetCanPrevCheckpoint(int client); + +/** + * Teleport a player to a more recent checkpoint. + * + * @param client Client index. + */ +native void GOKZ_NextCheckpoint(int client); + +/** + * Gets whether a player can go to their next checkpoint + * e.g. will return false if player has no checkpoints. + * + * @param client Client index. + * @return Whether player can go to next checkpoint. + */ +native bool GOKZ_GetCanNextCheckpoint(int client); + +/** + * Teleport a player to where they last teleported from. + * + * @param client Client index. + */ +native void GOKZ_UndoTeleport(int client); + +/** + * Gets whether a player can undo their teleport + * e.g. will return false if teleport was from midair. + * + * @param client Client index. + * @return Whether player can undo teleport. + */ +native bool GOKZ_GetCanUndoTeleport(int client); + +/** + * Pause a player's timer and freeze them. + * + * @param client Client index. + */ +native void GOKZ_Pause(int client); + +/** + * Gets whether a player can pause. Pausing is not allowed + * under some circumstance when the timer is running. + * + * @param client Client index. + */ +native bool GOKZ_GetCanPause(int client); + +/** + * Resumes a player's timer and unfreezes them. + * + * @param client Client index. + */ +native void GOKZ_Resume(int client); + +/** + * Gets whether a player can resume. Resuming is not allowed + * under some circumstance when the timer is running. + * + * @param client Client index. + */ +native bool GOKZ_GetCanResume(int client); + +/** + * Toggles the paused state of a player. + * + * @param client Client index. + */ +native void GOKZ_TogglePause(int client); + +/** + * Gets whether a player can teleport to start. + * + * @param client Client index. + * @return Whether player can teleport to start. + */ +native bool GOKZ_GetCanTeleportToStartOrEnd(int client); + +/** + * Plays the error sound to a player if they have the option enabled. + * + * @param client Client index. + */ +native void GOKZ_PlayErrorSound(int client); + +/** + * Set the origin of a player without invalidating any jumpstats. + * + * Only use this in plugins that create a new mode! + * + * @param client Client index. + * @param origin The new origin. + */ +native void GOKZ_SetValidJumpOrigin(int client, const float origin[3]); + +/** + * Registers an option with gokz-core, which uses clientprefs to + * keep track of the option's value and to save it to a database. + * This also effectively provides natives and forwards for other + * plugins to access any options that have been registered. + * + * @param name Option name. + * @param description Option description. + * @param type Type to treat the option value as. + * @param defaultValue Default value of option. + * @param minValue Minimum value of option. + * @param maxValue Maximum value of option. + * @return Whether registration was successful. + */ +native bool GOKZ_RegisterOption(const char[] name, const char[] description, OptionType type, any defaultValue, any minValue, any maxValue); + +/** + * Gets a property of a registered option. If used outside of + * gokz-core to get the cookie, a clone of its Handle is returned. + * + * @param option Option name. + * @param prop Option property to get. + * @return Value of property, or -1 (int) if option isn't registered. + */ +native any GOKZ_GetOptionProp(const char[] option, OptionProp prop); + +/** + * Sets a property of a registered option. For safety and simplicity, + * the cookie property is read-only and will fail to be set. + * + * @param option Option name. + * @param prop Option property to set. + * @param value Value to set the property to. + * @return Whether option property was successfully set. + */ +native bool GOKZ_SetOptionProp(const char[] option, OptionProp prop, any value); + +/** + * Gets the current value of a player's option. + * + * @param client Client index. + * @param option Option name. + * @return Current value of option, or -1 (int) if option isn't registered. + */ +native any GOKZ_GetOption(int client, const char[] option); + +/** + * Sets a player's option's value. Fails if option doesn't exist, + * or if desired value is outside the registered value range. + * + * @param client Client index. + * @param option Option name. + * @param value New option value. + * @return Whether option was successfully set. + */ +native bool GOKZ_SetOption(int client, const char[] option, any value); + +/** + * Gets whether player's last takeoff was a perfect bunnyhop as adjusted by GOKZ. + * + * @param client Client index. + * @return Whether player's last takeoff was a GOKZ perfect b-hop. + */ +native bool GOKZ_GetHitPerf(int client); + +/** + * Sets whether player's last takeoff was a perfect bunnyhop as adjusted by GOKZ. + * Intended to be called by GOKZ mode plugins only. + * + * @param client Client index. + * @param hitPerf Whether player's last takeoff was a GOKZ perfect b-hop. + */ +native void GOKZ_SetHitPerf(int client, bool hitPerf); + +/** + * Gets a player's horizontal speed at the time of their last takeoff as recorded by GOKZ. + * + * @param client Client index. + * @return Player's last takeoff speed as recorded by GOKZ. + */ +native float GOKZ_GetTakeoffSpeed(int client); + +/** + * Sets a player's recorded horizontal speed at the time of their last takeoff. + * Intended to be called by GOKZ mode plugins only. + * + * @param client Client index. + * @param takeoffSpeed Player's last takeoff speed as recorded by GOKZ. + */ +native void GOKZ_SetTakeoffSpeed(int client, float takeoffSpeed); + +/** + * Gets whether a player's current or last jump/airtime is valid. + * A jump is deemed invalid if the player is teleported. + * + * @param client Client index. + * @return Validity of player's current or last jump. + */ +native bool GOKZ_GetValidJump(int client); + +/** + * Has a player switch to a team via GOKZ Core. + * + * @param client Client index. + * @param team Which team to switch to. + * @param restorePos Whether to restore saved position if leaving spectators. + * @param forceBroadcast Force JoinTeam forward calling even if client's team did not change. + */ +native void GOKZ_JoinTeam(int client, int team, bool restorePos = true, bool forceBroadcast = false); + +/** + * Emit a sound to a player via GOKZ Core. + * Sounds emitted by this native will call GOKZ_OnEmitSoundToClient forward. + * + * @param client Client index. + * @param sample Sound file name relative to the "sound" folder. + * @param volume Sound volume. + * @param description Optional description. + */ +native void GOKZ_EmitSoundToClient(int client, const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = ""); + + +// =====[ STOCKS ]===== + +/** + * Makes a player join a team if they aren't on one and respawns them. + * + * @param client Client index. + * @param team Which team to switch to if not on one. + * @param restorePos Whether to restore saved position if leaving spectators. + */ +stock void GOKZ_RespawnPlayer(int client, int team = CS_TEAM_T, bool restorePos = true) +{ + if (IsSpectating(client)) + { + GOKZ_JoinTeam(client, team, restorePos); + } + else + { + CS_RespawnPlayer(client); + } +} + +/** + * Prints a message to all client's chat, formatting colours and optionally + * adding the chat prefix. If using the chat prefix, specify a colour at + * the beginning of the message e.g. "{default}Hello!". + * + * @param addPrefix Whether to add the chat prefix. + * @param format Formatting rules. + * @param any Variable number of format parameters. + */ +stock void GOKZ_PrintToChatAll(bool addPrefix, const char[] format, any...) +{ + char buffer[1024]; + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), format, 3); + GOKZ_PrintToChat(client, addPrefix, buffer); + } + } +} + +/** + * Prints a chat message to those spectating the client, formatting colours + * and optionally adding the chat prefix. If using the chat prefix, specify + * a colour at the beginning of the message e.g. "{default}Hello!". + * + * @param client Client index. + * @param addPrefix Whether to add the chat prefix. + * @param format Formatting rules. + * @param any Variable number of format parameters. + */ +stock void GOKZ_PrintToChatSpectators(int client, bool addPrefix, const char[] format, any...) +{ + char buffer[1024]; + for (int target = 1; target <= MaxClients; target++) + { + if (IsClientInGame(target) && GetObserverTarget(target) == client) + { + SetGlobalTransTarget(target); + VFormat(buffer, sizeof(buffer), format, 4); + GOKZ_PrintToChat(target, addPrefix, buffer); + } + } +} + +/** + * Gets the player's current time type. + * + * @param client Client index. + * @return Player's current time type. + */ +stock int GOKZ_GetTimeType(int client) +{ + return GOKZ_GetTimeTypeEx(GOKZ_GetTeleportCount(client)); +} + +/** + * Gets the time type given a teleport count. + * + * @param teleports Teleport count. + * @return Time type. + */ +stock int GOKZ_GetTimeTypeEx(int teleportCount) +{ + if (teleportCount == 0) + { + return TimeType_Pro; + } + return TimeType_Nub; +} + +/** + * Clears and populates a menu with an item for each mode + * in order of the mode enumeration. Highlights the client's + * selected mode with an asterisk. + * + * @param client Client index to check selected mode. + * @param menu Menu to populate items with. + * @param disableUnloadedModes Draw items for unloaded modes as disabled. + */ +stock void GOKZ_MenuAddModeItems(int client, Menu menu, bool disableUnloadedModes) +{ + int selectedMode = GOKZ_GetCoreOption(client, Option_Mode); + char display[32]; + + menu.RemoveAllItems(); + + for (int mode = 0; mode < MODE_COUNT; mode++) + { + FormatEx(display, sizeof(display), "%s", gC_ModeNames[mode]); + // Add asterisk to selected mode + if (mode == selectedMode) + { + Format(display, sizeof(display), "%s*", display); + } + + if (GOKZ_GetModeLoaded(mode)) + { + menu.AddItem("", display, ITEMDRAW_DEFAULT); + } + else + { + menu.AddItem("", display, ITEMDRAW_DISABLED); + } + } +} + +/** + * Increment an (integer-type) option's value. + * Loops back to min. value if max. value is exceeded. + * + * @param client Client index. + * @param option Option name. + * @return Whether option was successfully set. + */ +stock bool GOKZ_CycleOption(int client, const char[] option) +{ + int maxValue = GOKZ_GetOptionProp(option, OptionProp_MaxValue); + if (maxValue == -1) + { + return false; + } + + int newValue = GOKZ_GetOption(client, option) + 1; + if (newValue > GOKZ_GetOptionProp(option, OptionProp_MaxValue)) + { + newValue = GOKZ_GetOptionProp(option, OptionProp_MinValue); + } + return GOKZ_SetOption(client, option, newValue); +} + +/** + * Returns whether an option is a gokz-core option. + * + * @param option Option name. + * @param optionEnum Variable to store enumerated gokz-core option (if it is one). + * @return Whether option is a gokz-core option. + */ +stock bool GOKZ_IsCoreOption(const char[] option, Option &optionEnum = OPTION_INVALID) +{ + for (Option i; i < OPTION_COUNT; i++) + { + if (StrEqual(option, gC_CoreOptionNames[i])) + { + optionEnum = i; + return true; + } + } + return false; +} + +/** + * Gets a property of a gokz-core option. + * + * @param coreOption gokz-core option. + * @param prop Option property to get. + * @return Value of property, or -1 if option isn't registered. + */ +stock any GOKZ_GetCoreOptionProp(Option option, OptionProp prop) +{ + return GOKZ_GetOptionProp(gC_CoreOptionNames[option], prop); +} + +/** + * Gets the current value of a player's gokz-core option. + * + * @param client Client index. + * @param option gokz-core option. + * @return Current value of option. + */ +stock any GOKZ_GetCoreOption(int client, Option option) +{ + return GOKZ_GetOption(client, gC_CoreOptionNames[option]); +} + +/** + * Sets the player's gokz-core option's value. + * + * @param client Client index. + * @param option gokz-core option. + * @param value New option value. + * @return Whether option was successfully set. + */ +stock bool GOKZ_SetCoreOption(int client, Option option, any value) +{ + return GOKZ_SetOption(client, gC_CoreOptionNames[option], value); +} + +/** + * Increment an integer-type gokz-core option's value. + * Loops back to '0' if max value is exceeded. + * + * @param client Client index. + * @param option gokz-core option. + * @return Whether option was successfully set. + */ +stock bool GOKZ_CycleCoreOption(int client, Option option) +{ + return GOKZ_CycleOption(client, gC_CoreOptionNames[option]); +} + +/** + * Gets the current default mode. + * + * @return Default mode. + */ +stock int GOKZ_GetDefaultMode() +{ + return GOKZ_GetCoreOptionProp(Option_Mode, OptionProp_DefaultValue); +} + +/** + * Returns whether a course number is a valid (within valid range). + * + * @param course Course number. + * @param bonus Whether to only consider bonus course numbers as valid. + * @return Whether course number is valid. + */ +stock bool GOKZ_IsValidCourse(int course, bool bonus = false) +{ + return (!bonus && course == 0) || (0 < course && course < GOKZ_MAX_COURSES); +} + +/** + * Returns an integer from an entity's name as matched using a regular expression. + * + * @param entity Entity index. + * @param re Regular expression to match the integer with. + * @param substringID ID of the substring that will contain the integer. + * @returns Integer found in the entity's name, or -1 if not found. + */ +stock int GOKZ_MatchIntFromEntityName(int entity, Regex re, int substringID) +{ + int num = -1; + char buffer[32]; + GetEntityName(entity, buffer, sizeof(buffer)); + + if (re.Match(buffer) > 0) + { + re.GetSubString(1, buffer, sizeof(buffer)); + num = StringToInt(buffer); + } + + return num; +} + +/** + * Emits a sound to other players that are spectating the client. + * Sounds emitted by this function will call GOKZ_OnEmitSoundToClient forward. + * + * @param sample Sound file name relative to the "sound" folder. + * @param volume Sound volume. + * @param description Optional description. + */ +stock void GOKZ_EmitSoundToAll(const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = "") +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + GOKZ_EmitSoundToClient(client, sample, volume, description); + } + } +} + +/** + * Emits a sound to other players that are spectating the client. + * Sounds emitted by this function will call GOKZ_OnEmitSoundToClient forward. + * + * @param client Client being spectated. + * @param sample Sound file name relative to the "sound" folder. + * @param volume Sound volume. + * @param description Optional description. + */ +stock void GOKZ_EmitSoundToClientSpectators(int client, const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = "") +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidClient(i) && GetObserverTarget(i) == client) + { + GOKZ_EmitSoundToClient(i, sample, volume); + } + } +} + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_core = +{ + name = "gokz-core", + file = "gokz-core.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_core_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_GetModeLoaded"); + MarkNativeAsOptional("GOKZ_GetModeVersion"); + MarkNativeAsOptional("GOKZ_SetModeLoaded"); + MarkNativeAsOptional("GOKZ_GetLoadedModeCount"); + MarkNativeAsOptional("GOKZ_SetMode"); + MarkNativeAsOptional("GOKZ_GetOptionsTopMenu"); + MarkNativeAsOptional("GOKZ_GetCourseRegistered"); + MarkNativeAsOptional("GOKZ_PrintToChat"); + MarkNativeAsOptional("GOKZ_PrintToChatAndLog"); + MarkNativeAsOptional("GOKZ_StartTimer"); + MarkNativeAsOptional("GOKZ_EndTimer"); + MarkNativeAsOptional("GOKZ_StopTimer"); + MarkNativeAsOptional("GOKZ_StopTimerAll"); + MarkNativeAsOptional("GOKZ_TeleportToStart"); + MarkNativeAsOptional("GOKZ_TeleportToSearchStart"); + MarkNativeAsOptional("GOKZ_GetVirtualButtonPosition"); + MarkNativeAsOptional("GOKZ_SetVirtualButtonPosition"); + MarkNativeAsOptional("GOKZ_ResetVirtualButtonPosition"); + MarkNativeAsOptional("GOKZ_LockVirtualButtons"); + MarkNativeAsOptional("GOKZ_GetStartPosition"); + MarkNativeAsOptional("GOKZ_SetStartPosition"); + MarkNativeAsOptional("GOKZ_GetStartPositionType"); + MarkNativeAsOptional("GOKZ_SetStartPositionToMapStart"); + MarkNativeAsOptional("GOKZ_TeleportToEnd"); + MarkNativeAsOptional("GOKZ_MakeCheckpoint"); + MarkNativeAsOptional("GOKZ_GetCanMakeCheckpoint"); + MarkNativeAsOptional("GOKZ_TeleportToCheckpoint"); + MarkNativeAsOptional("GOKZ_GetCanTeleportToCheckpoint"); + MarkNativeAsOptional("GOKZ_PrevCheckpoint"); + MarkNativeAsOptional("GOKZ_GetCanPrevCheckpoint"); + MarkNativeAsOptional("GOKZ_NextCheckpoint"); + MarkNativeAsOptional("GOKZ_GetCanNextCheckpoint"); + MarkNativeAsOptional("GOKZ_UndoTeleport"); + MarkNativeAsOptional("GOKZ_GetCanUndoTeleport"); + MarkNativeAsOptional("GOKZ_Pause"); + MarkNativeAsOptional("GOKZ_GetCanPause"); + MarkNativeAsOptional("GOKZ_Resume"); + MarkNativeAsOptional("GOKZ_GetCanResume"); + MarkNativeAsOptional("GOKZ_TogglePause"); + MarkNativeAsOptional("GOKZ_GetCanTeleportToStartOrEnd"); + MarkNativeAsOptional("GOKZ_PlayErrorSound"); + MarkNativeAsOptional("GOKZ_SetValidJumpOrigin"); + MarkNativeAsOptional("GOKZ_GetTimerRunning"); + MarkNativeAsOptional("GOKZ_GetCourse"); + MarkNativeAsOptional("GOKZ_SetCourse"); + MarkNativeAsOptional("GOKZ_GetPaused"); + MarkNativeAsOptional("GOKZ_GetTime"); + MarkNativeAsOptional("GOKZ_SetTime"); + MarkNativeAsOptional("GOKZ_InvalidateRun"); + MarkNativeAsOptional("GOKZ_GetCheckpointCount"); + MarkNativeAsOptional("GOKZ_SetCheckpointCount"); + MarkNativeAsOptional("GOKZ_GetCheckpointData"); + MarkNativeAsOptional("GOKZ_SetCheckpointData"); + MarkNativeAsOptional("GOKZ_GetUndoTeleportData"); + MarkNativeAsOptional("GOKZ_SetUndoTeleportData"); + MarkNativeAsOptional("GOKZ_GetTeleportCount"); + MarkNativeAsOptional("GOKZ_SetTeleportCount"); + MarkNativeAsOptional("GOKZ_RegisterOption"); + MarkNativeAsOptional("GOKZ_GetOptionProp"); + MarkNativeAsOptional("GOKZ_SetOptionProp"); + MarkNativeAsOptional("GOKZ_GetOption"); + MarkNativeAsOptional("GOKZ_SetOption"); + MarkNativeAsOptional("GOKZ_GetHitPerf"); + MarkNativeAsOptional("GOKZ_SetHitPerf"); + MarkNativeAsOptional("GOKZ_GetTakeoffSpeed"); + MarkNativeAsOptional("GOKZ_SetTakeoffSpeed"); + MarkNativeAsOptional("GOKZ_GetValidJump"); + MarkNativeAsOptional("GOKZ_JoinTeam"); + MarkNativeAsOptional("GOKZ_EmitSoundToClient"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/global.inc b/sourcemod/scripting/include/gokz/global.inc new file mode 100644 index 0000000..0f23a0c --- /dev/null +++ b/sourcemod/scripting/include/gokz/global.inc @@ -0,0 +1,317 @@ +/* + gokz-global Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_global_included_ +#endinput +#endif +#define _gokz_global_included_ + +#include <GlobalAPI> + + + +// =====[ ENUMS ]===== + +enum +{ + EnforcedCVar_Cheats = 0, + EnforcedCVar_ClampUnsafeVelocities, + EnforcedCVar_DropKnifeEnable, + EnforcedCVar_AutoBunnyhopping, + EnforcedCVar_MinUpdateRate, + EnforcedCVar_MaxUpdateRate, + EnforcedCVar_MinCmdRate, + EnforcedCVar_MaxCmdRate, + EnforcedCVar_ClientCmdrateDifference, + EnforcedCVar_Turbophysics, + ENFORCEDCVAR_COUNT +}; + +enum +{ + BannedPluginCommand_Funcommands = 0, + BannedPluginCommand_Playercommands, + BANNEDPLUGINCOMMAND_COUNT +}; + +enum +{ + BannedPlugin_Funcommands = 0, + BannedPlugin_Playercommands, + BANNEDPLUGIN_COUNT +}; + +enum GlobalMode +{ + GlobalMode_Invalid = -1, + GlobalMode_KZTimer = 200, + GlobalMode_KZSimple, + GlobalMode_Vanilla +} + + + +// =====[ CONSTANTS ]===== + +#define GL_SOUND_NEW_RECORD "gokz/holyshit.mp3" +#define GL_FPS_MAX_CHECK_INTERVAL 1.0 +#define GL_FPS_MAX_KICK_TIMEOUT 10.0 +#define GL_FPS_MAX_MIN_VALUE 120 +#define GL_MYAW_MAX_VALUE 0.3 + +stock char gC_EnforcedCVars[ENFORCEDCVAR_COUNT][] = +{ + "sv_cheats", + "sv_clamp_unsafe_velocities", + "mp_drop_knife_enable", + "sv_autobunnyhopping", + "sv_minupdaterate", + "sv_maxupdaterate", + "sv_mincmdrate", + "sv_maxcmdrate", + "sv_client_cmdrate_difference", + "sv_turbophysics" +}; + +stock char gC_BannedPluginCommands[BANNEDPLUGINCOMMAND_COUNT][] = +{ + "sm_beacon", + "sm_slap" +}; + +stock char gC_BannedPlugins[BANNEDPLUGIN_COUNT][] = +{ + "Fun Commands", + "Player Commands" +}; + +stock float gF_EnforcedCVarValues[ENFORCEDCVAR_COUNT] = +{ + 0.0, + 0.0, + 0.0, + 0.0, + 128.0, + 128.0, + 128.0, + 128.0, + 0.0, + 0.0 +}; + + + +// =====[ FORWARDS ]===== + +/** + * Called when a player sets a new global top time. + * + * @param client Client index. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param timeType Time type i.e. NUB or PRO. + * @param rank Ranking within the same time type. + * @param rankOverall Overall (NUB and PRO) ranking (0 if not ranked high enough). + * @param runTime Player's end time. + */ +forward void GOKZ_GL_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float runTime); + + + +// =====[ NATIVES ]===== + +/** + * Prints to chat the global records for a map, course and mode. + * + * @param client Client index. + * @param map Map name or "" for current map. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode GOKZ mode. + */ +native void GOKZ_GL_PrintRecords(int client, const char[] map = "", int course, int mode, const char[] steamid = DEFAULT_STRING); + +/** + * Opens up the global map top menu for a map, course and mode. + * + * @param client Client index. + * @param map Map name or "" for current map. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode GOKZ mode. + * @param timeType Type of time i.e. NUB or PRO. + */ +native void GOKZ_GL_DisplayMapTopMenu(int client, const char[] map = "", int course, int mode, int timeType); + +/** + * Get the total global points of a player. + * + * @param client Client index. + * @param mode GOKZ mode. + * @param timeType Type of time i.e. NUB or PRO. + */ +native void GOKZ_GL_GetPoints(int client, int mode, int timeType); + +/** + * Get the global points on the main coruse of the current map. + * + * @param client Client index. + * @param mode GOKZ mode. + * @param timeType Type of time i.e. NUB or PRO. + */ +native void GOKZ_GL_GetMapPoints(int client, int mode, int timeType); + +/** + * Get the total global ranking points of a player. + * + * @param client Client index. + * @param mode GOKZ mode. + * @return The points. + */ +native int GOKZ_GL_GetRankPoints(int client, int mode); + +/** + * Get the amount of maps a player finished. + * + * @param client Client index. + * @param mode GOKZ mode. + * @param timeType Type of time i.e. NUB or PRO. + */ +native void GOKZ_GL_GetFinishes(int client, int mode, int timeType); + +/** + * Fetch the points a player got from the Global API. + * + * @param client Client index. -1 to update all indices. + * @param mode GOKZ mode. -1 to update all modes. + */ +native void GOKZ_GL_UpdatePoints(int client = -1, int mode = -1); + +/** + * Gets whether the Global API key is valid or not for global status. + * + * @return True if the API key is valid, false otherwise or if there is no connection to the Global API. + */ +native bool GOKZ_GL_GetAPIKeyValid(); + +/** + * Gets whether the running plugins are valid or not for global status. + * + * @return True if the plugins are valid, false otherwise. + */ +native bool GOKZ_GL_GetPluginsValid(); + +/** + * Gets whether the setting enforcer is valid or not for global status. + * + * @return True if the setting enforcer is valid, false otherwise. + */ +native bool GOKZ_GL_GetSettingsEnforcerValid(); + +/** + * Gets whether the current map is valid or not for global status. + * + * @return True if the map is valid, false otherwise or if there is no connection to the Global API. + */ +native bool GOKZ_GL_GetMapValid(); + +/** + * Gets whether the current player is valid or not for global status. + * + * @param client Client index. + * @return True if the player is valid, false otherwise or if there is no connection to the Global API. + */ +native bool GOKZ_GL_GetPlayerValid(int client); + + + +// =====[ STOCKS ]===== + +/** + * Gets the global mode enumeration equivalent for the GOKZ mode. + * + * @param mode GOKZ mode. + * @return Global mode enumeration equivalent. + */ +stock GlobalMode GOKZ_GL_GetGlobalMode(int mode) +{ + switch (mode) + { + case Mode_Vanilla:return GlobalMode_Vanilla; + case Mode_SimpleKZ:return GlobalMode_KZSimple; + case Mode_KZTimer:return GlobalMode_KZTimer; + } + return GlobalMode_Invalid; +} + +/** + * Gets the global mode enumeration equivalent for the GOKZ mode. + * + * @param mode GOKZ mode. + * @return Global mode enumeration equivalent. + */ +stock int GOKZ_GL_FromGlobalMode(GlobalMode mode) +{ + switch (mode) + { + case GlobalMode_Vanilla:return Mode_Vanilla; + case GlobalMode_KZSimple:return Mode_SimpleKZ; + case GlobalMode_KZTimer:return Mode_KZTimer; + } + return -1; +} + +/** + * Gets the string representation of a mode. + * + * @param mode GOKZ mode. + * @param mode_str String version of the mode. + * @param size Max length of mode_str. + * @return True if the conversion was successful. + */ +stock bool GOKZ_GL_GetModeString(int mode, char[] mode_str, int size) +{ + switch (mode) + { + case Mode_Vanilla:strcopy(mode_str, size, "kz_vanilla"); + case Mode_SimpleKZ:strcopy(mode_str, size, "kz_simple"); + case Mode_KZTimer:strcopy(mode_str, size, "kz_timer"); + default:return false; + } + return true; +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_global = +{ + name = "gokz-global", + file = "gokz-global.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_global_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_GL_PrintRecords"); + MarkNativeAsOptional("GOKZ_GL_DisplayMapTopMenu"); + MarkNativeAsOptional("GOKZ_GL_UpdatePoints"); + MarkNativeAsOptional("GOKZ_GL_GetAPIKeyValid"); + MarkNativeAsOptional("GOKZ_GL_GetPluginsValid"); + MarkNativeAsOptional("GOKZ_GL_GetSettingsEnforcerValid"); + MarkNativeAsOptional("GOKZ_GL_GetMapValid"); + MarkNativeAsOptional("GOKZ_GL_GetPlayerValid"); + MarkNativeAsOptional("GOKZ_GL_GetPoints"); + MarkNativeAsOptional("GOKZ_GL_GetMapPoints"); + MarkNativeAsOptional("GOKZ_GL_GetRankPoints"); + MarkNativeAsOptional("GOKZ_GL_GetFinishes"); + MarkNativeAsOptional("GOKZ_GL_UpdatePoints"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/hud.inc b/sourcemod/scripting/include/gokz/hud.inc new file mode 100644 index 0000000..5d658ff --- /dev/null +++ b/sourcemod/scripting/include/gokz/hud.inc @@ -0,0 +1,468 @@ +/* + gokz-hud Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_hud_included_ +#endinput +#endif +#define _gokz_hud_included_ + + + +// =====[ ENUMS ]===== + +enum HUDOption: +{ + HUDOPTION_INVALID = -1, + HUDOption_TPMenu, + HUDOption_InfoPanel, + HUDOption_ShowKeys, + HUDOption_TimerText, + HUDOption_TimerStyle, + HUDOption_TimerType, + HUDOption_SpeedText, + HUDOption_ShowWeapon, + HUDOption_ShowControls, + HUDOption_DeadstrafeColor, + HUDOption_ShowSpectators, + HUDOption_SpecListPosition, + HUDOption_UpdateRate, + HUDOption_DynamicMenu, + HUDOPTION_COUNT +}; + +enum +{ + TPMenu_Disabled = 0, + TPMenu_Simple, + TPMenu_Advanced, + TPMENU_COUNT +}; + +enum +{ + InfoPanel_Disabled = 0, + InfoPanel_Enabled, + INFOPANEL_COUNT +}; + +enum +{ + ShowKeys_Spectating = 0, + ShowKeys_Always, + ShowKeys_Disabled, + SHOWKEYS_COUNT +}; + +enum +{ + TimerText_Disabled = 0, + TimerText_InfoPanel, + TimerText_TPMenu, + TimerText_Bottom, + TimerText_Top, + TIMERTEXT_COUNT +}; + +enum +{ + TimerStyle_Standard = 0, + TimerStyle_Precise, + TIMERSTYLE_COUNT +}; + +enum +{ + TimerType_Disabled = 0, + TimerType_Enabled, + TIMERTYPE_COUNT +}; + +enum +{ + SpeedText_Disabled = 0, + SpeedText_InfoPanel, + SpeedText_Bottom, + SPEEDTEXT_COUNT +}; + +enum +{ + ShowWeapon_Disabled = 0, + ShowWeapon_Enabled, + SHOWWEAPON_COUNT +}; + +enum +{ + ReplayControls_Disabled = 0, + ReplayControls_Enabled, + REPLAYCONTROLS_COUNT +}; + +enum +{ + DeadstrafeColor_Disabled = 0, + DeadstrafeColor_Enabled, + DEADSTRAFECOLOR_COUNT +}; + +enum +{ + ShowSpecs_Disabled = 0, + ShowSpecs_Number, + ShowSpecs_Full, + SHOWSPECS_COUNT +}; + +enum +{ + SpecListPosition_TPMenu = 0, + SpecListPosition_InfoPanel, + SPECLISTPOSITION_COUNT +} + +enum +{ + UpdateRate_Slow = 0, + UpdateRate_Fast, + UPDATERATE_COUNT, +}; + +enum +{ + DynamicMenu_Legacy = 0, + DynamicMenu_Disabled, + DynamicMenu_Enabled, + DYNAMICMENU_COUNT +}; + +// =====[ STRUCTS ]====== + +enum struct HUDInfo +{ + bool TimerRunning; + int TimeType; + float Time; + bool Paused; + bool OnGround; + bool OnLadder; + bool Noclipping; + bool Ducking; + bool HitBhop; + bool IsTakeoff; + float Speed; + int ID; + bool Jumped; + bool HitPerf; + bool HitJB; + float TakeoffSpeed; + int Buttons; + int CurrentTeleport; +} + + + +// =====[ CONSTANTS ]===== + +#define HUD_OPTION_CATEGORY "HUD" +#define HUD_MAX_BHOP_GROUND_TICKS 5 +#define HUD_MAX_HINT_SIZE 227 + +stock char gC_HUDOptionNames[HUDOPTION_COUNT][] = +{ + "GOKZ HUD - Teleport Menu", + "GOKZ HUD - Centre Panel", + "GOKZ HUD - Show Keys", + "GOKZ HUD - Timer Text", + "GOKZ HUD - Timer Style", + "GOKZ HUD - Show Time Type", + "GOKZ HUD - Speed Text", + "GOKZ HUD - Show Weapon", + "GOKZ HUD - Show Controls", + "GOKZ HUD - Dead Strafe", + "GOKZ HUD - Show Spectators", + "GOKZ HUD - Spec List Pos", + "GOKZ HUD - Update Rate", + "GOKZ HUD - Dynamic Menu" +}; + +stock char gC_HUDOptionDescriptions[HUDOPTION_COUNT][] = +{ + "Teleport Menu - 0 = Disabled, 1 = Simple, 2 = Advanced", + "Centre Information Panel - 0 = Disabled, 1 = Enabled", + "Key Press Display - 0 = Spectating, 1 = Always, 2 = Disabled", + "Timer Display - 0 = Disabled, 1 = Centre Panel, 2 = Teleport Menu, 3 = Bottom, 4 = Top", + "Timer Style - 0 = Standard, 1 = Precise", + "Timer Type - 0 = Disabled, 1 = Enabled", + "Speed Display - 0 = Disabled, 1 = Centre Panel, 2 = Bottom", + "Weapon Viewmodel - 0 = Disabled, 1 = Enabled", + "Replay Controls Display - 0 = Disbled, 1 = Enabled", + "Dead Strafe Indicator - 0 = Disabled, 1 = Enabled", + "Show Spectators - 0 = Disabled, 1 = Number Only, 2 = Number and Names", + "Spectator List Position - 0 = Teleport Menu, 2 = Center Panel", + "HUD Update Rate - 0 = Slow, 1 = Fast", + "Dynamic Menu - 0 = Legacy, 1 = Disabled, 2 = Enabled" +}; + +stock char gC_HUDOptionPhrases[HUDOPTION_COUNT][] = +{ + "Options Menu - Teleport Menu", + "Options Menu - Info Panel", + "Options Menu - Show Keys", + "Options Menu - Timer Text", + "Options Menu - Timer Style", + "Options Menu - Timer Type", + "Options Menu - Speed Text", + "Options Menu - Show Weapon", + "Options Menu - Show Controls", + "Options Menu - Dead Strafe Indicator", + "Options Menu - Show Spectators", + "Options Menu - Spectator List Position", + "Options Menu - Update Rate", + "Options Menu - Dynamic Menu" +}; + +stock int gI_HUDOptionCounts[HUDOPTION_COUNT] = +{ + TPMENU_COUNT, + INFOPANEL_COUNT, + SHOWKEYS_COUNT, + TIMERTEXT_COUNT, + TIMERSTYLE_COUNT, + TIMERTYPE_COUNT, + SPEEDTEXT_COUNT, + SHOWWEAPON_COUNT, + REPLAYCONTROLS_COUNT, + DEADSTRAFECOLOR_COUNT, + SHOWSPECS_COUNT, + SPECLISTPOSITION_COUNT, + UPDATERATE_COUNT, + DYNAMICMENU_COUNT +}; + +stock int gI_HUDOptionDefaults[HUDOPTION_COUNT] = +{ + TPMenu_Advanced, + InfoPanel_Enabled, + ShowKeys_Spectating, + TimerText_InfoPanel, + TimerStyle_Standard, + TimerType_Enabled, + SpeedText_InfoPanel, + ShowWeapon_Enabled, + ReplayControls_Enabled, + DeadstrafeColor_Disabled, + ShowSpecs_Disabled, + SpecListPosition_TPMenu, + UpdateRate_Slow, + DynamicMenu_Legacy +}; + +stock char gC_TPMenuPhrases[TPMENU_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Simple", + "Options Menu - Advanced" +}; + +stock char gC_ShowKeysPhrases[SHOWKEYS_COUNT][] = +{ + "Options Menu - Spectating", + "Options Menu - Always", + "Options Menu - Disabled" +}; + +stock char gC_TimerTextPhrases[TIMERTEXT_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Info Panel", + "Options Menu - Teleport Menu", + "Options Menu - Bottom", + "Options Menu - Top" +}; + +stock char gC_TimerTypePhrases[TIMERTYPE_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Enabled" +}; + +stock char gC_SpeedTextPhrases[SPEEDTEXT_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Info Panel", + "Options Menu - Bottom" +}; + +stock char gC_ShowControlsPhrases[REPLAYCONTROLS_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Enabled" +}; + +stock char gC_DeadstrafeColorPhrases[DEADSTRAFECOLOR_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Enabled" +}; + +stock char gC_ShowSpecsPhrases[SHOWSPECS_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Number", + "Options Menu - Number and Names" +}; + +stock char gC_SpecListPositionPhrases[SPECLISTPOSITION_COUNT][] = +{ + "Options Menu - Teleport Menu", + "Options Menu - Info Panel" +}; + +stock char gC_HUDUpdateRatePhrases[UPDATERATE_COUNT][]= +{ + "Options Menu - Slow", + "Options Menu - Fast" +}; + +stock char gC_DynamicMenuPhrases[DYNAMICMENU_COUNT][]= +{ + "Options Menu - Legacy", + "Options Menu - Disabled", + "Options Menu - Enabled" +}; + +// =====[ NATIVES ]===== + +/** + * Returns whether the GOKZ HUD menu is showing for a client. + * + * @param client Client index. + * @return Whether the GOKZ HUD menu is showing. + */ +native bool GOKZ_HUD_GetMenuShowing(int client); + +/** + * Sets whether the GOKZ HUD menu would be showing for a client. + * + * @param client Client index. + * @param value Whether the GOKZ HUD menu would be showing for a client. + */ +native void GOKZ_HUD_SetMenuShowing(int client, bool value); + +/** + * Gets the spectator text for the menu. Used by GOKZ-replays. + * + * @param client Client index. + * @param value Whether the GOKZ HUD menu would be showing for a client. + */ +native void GOKZ_HUD_GetMenuSpectatorText(int client, any[] info, char[] buffer, int size); + +/** + * Forces the client's TP menu to update. + * + * @param client Client index. + */ +native void GOKZ_HUD_ForceUpdateTPMenu(int client); + +// =====[ STOCKS ]===== + +/** + * Returns whether an option is a gokz-hud option. + * + * @param option Option name. + * @param optionEnum Variable to store enumerated gokz-hud option (if it is one). + * @return Whether option is a gokz-hud option. + */ +stock bool GOKZ_HUD_IsHUDOption(const char[] option, HUDOption &optionEnum = HUDOPTION_INVALID) +{ + for (HUDOption i; i < HUDOPTION_COUNT; i++) + { + if (StrEqual(option, gC_HUDOptionNames[i])) + { + optionEnum = i; + return true; + } + } + return false; +} + +/** + * Gets the current value of a player's gokz-hud option. + * + * @param client Client index. + * @param option gokz-hud option. + * @return Current value of option. + */ +stock any GOKZ_HUD_GetOption(int client, HUDOption option) +{ + return GOKZ_GetOption(client, gC_HUDOptionNames[option]); +} + +/** + * Sets a player's gokz-hud option's value. + * + * @param client Client index. + * @param option gokz-hud option. + * @param value New option value. + * @return Whether option was successfully set. + */ +stock bool GOKZ_HUD_SetOption(int client, HUDOption option, any value) +{ + return GOKZ_SetOption(client, gC_HUDOptionNames[option], value); +} + +/** + * Increment an integer-type gokz-hud option's value. + * Loops back to '0' if max value is exceeded. + * + * @param client Client index. + * @param option gokz-hud option. + * @return Whether option was successfully set. + */ +stock bool GOKZ_HUD_CycleOption(int client, HUDOption option) +{ + return GOKZ_CycleOption(client, gC_HUDOptionNames[option]); +} + +/** + * Represents a time float as a string e.g. 01:23.45 + * and according to the client's HUD options. + * + * @param client Client index. + * @param time Time in seconds. + * @return String representation of time. + */ +stock char[] GOKZ_HUD_FormatTime(int client, float time) +{ + bool precise = GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Precise; + return GOKZ_FormatTime(time, precise); +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_hud = +{ + name = "gokz-hud", + file = "gokz-hud.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_hud_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_HUD_GetMenuShowing"); + MarkNativeAsOptional("GOKZ_HUD_SetMenuShowing"); + MarkNativeAsOptional("GOKZ_HUD_GetMenuSpectatorText"); + MarkNativeAsOptional("GOKZ_HUD_ForceUpdateTPMenu"); +} +#endif diff --git a/sourcemod/scripting/include/gokz/jumpbeam.inc b/sourcemod/scripting/include/gokz/jumpbeam.inc new file mode 100644 index 0000000..1b92479 --- /dev/null +++ b/sourcemod/scripting/include/gokz/jumpbeam.inc @@ -0,0 +1,148 @@ +/* + gokz-jumpbeam Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_jumpbeam_included_ +#endinput +#endif +#define _gokz_jumpbeam_included_ + + + +// =====[ ENUMS ]===== + +enum JBOption: +{ + JBOPTION_INVALID = -1, + JBOption_Type, + JBOPTION_COUNT +}; + +enum +{ + JBType_Disabled = 0, + JBType_Feet, + JBType_Head, + JBType_FeetAndHead, + JBType_Ground, + JBTYPE_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define JB_BEAM_LIFETIME 4.0 + +stock char gC_JBOptionNames[JBOPTION_COUNT][] = +{ + "GOKZ JB - Jump Beam Type" +}; + +stock char gC_JBOptionDescriptions[JBOPTION_COUNT][] = +{ + "Jump Beam Type - 0 = Disabled, 1 = Feet, 2 = Head, 3 = Feet & Head, 4 = Ground" +}; + +stock int gI_JBOptionDefaultValues[JBOPTION_COUNT] = +{ + JBType_Disabled +}; + +stock int gI_JBOptionCounts[JBOPTION_COUNT] = +{ + JBTYPE_COUNT +}; + +stock char gC_JBOptionPhrases[JBOPTION_COUNT][] = +{ + "Options Menu - Jump Beam" +}; + +stock char gC_JBTypePhrases[JBTYPE_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Feet", + "Options Menu - Head", + "Options Menu - Feet and Head", + "Options Menu - Ground" +}; + + + +// =====[ STOCKS ]===== + +/** + * Returns whether an option is a gokz-jumpbeam option. + * + * @param option Option name. + * @param optionEnum Variable to store enumerated gokz-jumpbeam option (if it is one). + * @return Whether option is a gokz-jumpbeam option. + */ +stock bool GOKZ_JB_IsJBOption(const char[] option, JBOption &optionEnum = JBOPTION_INVALID) +{ + for (JBOption i; i < JBOPTION_COUNT; i++) + { + if (StrEqual(option, gC_JBOptionNames[i])) + { + optionEnum = i; + return true; + } + } + return false; +} + +/** + * Gets the current value of a player's gokz-jumpbeam option. + * + * @param client Client index. + * @param option gokz-jumpbeam option. + * @return Current value of option. + */ +stock any GOKZ_JB_GetOption(int client, JBOption option) +{ + return GOKZ_GetOption(client, gC_JBOptionNames[option]); +} + +/** + * Sets a player's gokz-jumpbeam option's value. + * + * @param client Client index. + * @param option gokz-jumpbeam option. + * @param value New option value. + * @return Whether option was successfully set. + */ +stock bool GOKZ_JB_SetOption(int client, JBOption option, any value) +{ + return GOKZ_SetOption(client, gC_JBOptionNames[option], value); +} + +/** + * Increment an integer-type gokz-jumpbeam option's value. + * Loops back to '0' if max value is exceeded. + * + * @param client Client index. + * @param option gokz-jumpbeam option. + * @return Whether option was successfully set. + */ +stock bool GOKZ_JB_CycleOption(int client, JBOption option) +{ + return GOKZ_CycleOption(client, gC_JBOptionNames[option]); +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_jumpbeam = +{ + name = "gokz-jumpbeam", + file = "gokz-jumpbeam.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/jumpstats.inc b/sourcemod/scripting/include/gokz/jumpstats.inc new file mode 100644 index 0000000..452ae28 --- /dev/null +++ b/sourcemod/scripting/include/gokz/jumpstats.inc @@ -0,0 +1,442 @@ +/* + gokz-jumpstats Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_jumpstats_included_ +#endinput +#endif +#define _gokz_jumpstats_included_ + + + +// =====[ ENUMS ]===== + +enum +{ + JumpType_FullInvalid = -1, + JumpType_LongJump, + JumpType_Bhop, + JumpType_MultiBhop, + JumpType_WeirdJump, + JumpType_LadderJump, + JumpType_Ladderhop, + JumpType_Jumpbug, + JumpType_LowpreBhop, + JumpType_LowpreWeirdJump, + JumpType_Fall, + JumpType_Other, + JumpType_Invalid, + JUMPTYPE_COUNT +}; + +enum +{ + StrafeDirection_None, + StrafeDirection_Left, + StrafeDirection_Right +}; + +enum +{ + DistanceTier_None = 0, + DistanceTier_Meh, + DistanceTier_Impressive, + DistanceTier_Perfect, + DistanceTier_Godlike, + DistanceTier_Ownage, + DistanceTier_Wrecker, + DISTANCETIER_COUNT +}; + +enum JSOption: +{ + JSOPTION_INVALID = -1, + JSOption_JumpstatsMaster, + JSOption_MinChatTier, + JSOption_MinConsoleTier, + JSOption_MinSoundTier, + JSOption_FailstatsConsole, + JSOption_FailstatsChat, + JSOption_JumpstatsAlways, + JSOption_ExtendedChatReport, + JSOption_MinChatBroadcastTier, + JSOption_MinSoundBroadcastTier, + JSOPTION_COUNT +}; + +enum +{ + JSToggleOption_Disabled = 0, + JSToggleOption_Enabled, + JSTOGGLEOPTION_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define JS_CFG_TIERS "cfg/sourcemod/gokz/gokz-jumpstats-tiers.cfg" +#define JS_CFG_SOUNDS "cfg/sourcemod/gokz/gokz-jumpstats-sounds.cfg" +#define JS_CFG_BROADCAST "cfg/sourcemod/gokz/gokz-jumpstats-broadcast.cfg" +#define JS_OPTION_CATEGORY "Jumpstats" +#define JS_MAX_LADDERJUMP_OFFSET 2.0 +#define JS_MAX_BHOP_GROUND_TICKS 5 +#define JS_MAX_DUCKBUG_RESET_TICKS 6 +#define JS_MAX_WEIRDJUMP_FALL_OFFSET 64.0 +#define JS_TOUCH_GRACE_TICKS 3 +#define JS_MAX_TRACKED_STRAFES 48 +#define JS_MIN_BLOCK_DISTANCE 186 +#define JS_MIN_LAJ_BLOCK_DISTANCE 50 +#define JS_MAX_LAJ_FAILSTAT_DISTANCE 250 +#define JS_TOP_RECORD_COUNT 20 +#define JS_MAX_JUMP_DISTANCE 500 +#define JS_FAILSTATS_MAX_TRACKED_TICKS 128 +#define JS_MIN_TELEPORT_DELAY 5 +#define JS_SPEED_MODIFICATION_TOLERANCE 0.1 +#define JS_OFFSET_EPSILON 0.03125 + +stock char gC_JumpTypes[JUMPTYPE_COUNT][] = +{ + "Long Jump", + "Bunnyhop", + "Multi Bunnyhop", + "Weird Jump", + "Ladder Jump", + "Ladderhop", + "Jumpbug", + "Lowpre Bunnyhop", + "Lowpre Weird Jump", + "Fall", + "Unknown Jump", + "Invalid Jump" +}; + +stock char gC_JumpTypesShort[JUMPTYPE_COUNT][] = +{ + "LJ", + "BH", + "MBH", + "WJ", + "LAJ", + "LAH", + "JB", + "LBH", + "LWJ", + "FL", + "UNK", + "INV" +}; + +stock char gC_JumpTypeKeys[JUMPTYPE_COUNT][] = +{ + "longjump", + "bhop", + "multibhop", + "weirdjump", + "ladderjump", + "ladderhop", + "jumpbug", + "lowprebhop", + "lowpreweirdjump", + "fall", + "unknown", + "invalid" +}; + +stock char gC_DistanceTiers[DISTANCETIER_COUNT][] = +{ + "None", + "Meh", + "Impressive", + "Perfect", + "Godlike", + "Ownage", + "Wrecker" +}; + +stock char gC_DistanceTierKeys[DISTANCETIER_COUNT][] = +{ + "none", + "meh", + "impressive", + "perfect", + "godlike", + "ownage", + "wrecker" +}; + +stock char gC_DistanceTierChatColours[DISTANCETIER_COUNT][] = +{ + "{grey}", + "{grey}", + "{blue}", + "{green}", + "{darkred}", + "{gold}", + "{orchid}" +}; + +stock char gC_JSOptionNames[JSOPTION_COUNT][] = +{ + "GOKZ JS - Master Switch", + "GOKZ JS - Chat Report", + "GOKZ JS - Console Report", + "GOKZ JS - Sounds", + "GOKZ JS - Failstats Console", + "GOKZ JS - Failstats Chat", + "GOKZ JS - Jumpstats Always", + "GOKZ JS - Ext Chat Report", + "GOKZ JS - Min Chat Broadcast", + "GOKZ JS - Min Sound Broadcast" +}; + +stock char gC_JSOptionDescriptions[JSOPTION_COUNT][] = +{ + "Master Switch for All Jumpstats Functionality - 0 = Disabled, 1 = Enabled", + "Minimum Tier for Jumpstats Chat Report - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker", + "Minimum Tier for Jumpstats Console report - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker", + "Minimum Tier for Jumpstats Sounds - 0 = Disabled, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker", + "Print Failstats To Console - 0 = Disabled, 1 = Enabled", + "Print Failstats To Chat - 0 = Disabled, 1 = Enabled", + "Always show jumpstats, even for invalid jumps - 0 = Disabled, 1 = Enabled", + "Extended Chat Report - 0 = Disabled, 1 = Enabled", + "Minimum Jump Tier for Jumpstat Chat Broadcast - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker", + "Minimum Jump Tier for Jumpstat Sound Broadcast - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker" +}; + +stock char gI_JSOptionPhrases[JSOPTION_COUNT][] = +{ + "Options Menu - Jumpstats Master Switch", + "Options Menu - Jumpstats Chat Report", + "Options Menu - Jumpstats Console Report", + "Options Menu - Jumpstats Sounds", + "Options Menu - Failstats Console Report", + "Options Menu - Failstats Chat Report", + "Options Menu - Jumpstats Always", + "Options Menu - Extended Jump Chat Report", + "Options Menu - Minimal Jump Chat Broadcast Tier", + "Options Menu - Minimal Jump Sound Broadcast Tier" +}; + +stock int gI_JSOptionDefaults[JSOPTION_COUNT] = +{ + JSToggleOption_Enabled, + DistanceTier_Meh, + DistanceTier_Meh, + DistanceTier_Impressive, + JSToggleOption_Enabled, + JSToggleOption_Disabled, + JSToggleOption_Disabled, + JSToggleOption_Disabled, + DistanceTier_Ownage, + DistanceTier_None +}; + +stock int gI_JSOptionCounts[JSOPTION_COUNT] = +{ + JSTOGGLEOPTION_COUNT, + DISTANCETIER_COUNT, + DISTANCETIER_COUNT, + DISTANCETIER_COUNT, + JSTOGGLEOPTION_COUNT, + JSTOGGLEOPTION_COUNT, + JSTOGGLEOPTION_COUNT, + JSTOGGLEOPTION_COUNT, + DISTANCETIER_COUNT, + DISTANCETIER_COUNT +}; + + + +// =====[ STRUCTS ]===== + +enum struct Jump +{ + int jumper; + int block; + int crouchRelease; + int crouchTicks; + int deadair; + int duration; + int originalType; + int overlap; + int releaseW; + int strafes; + int type; + float deviation; + float distance; + float edge; + float height; + float maxSpeed; + float offset; + float preSpeed; + float sync; + float width; + + // For the 'always' stats + float miss; + + // We can't make a separate enum struct for that cause it won't let us + // index an array of enum structs. + int strafes_gainTicks[JS_MAX_TRACKED_STRAFES]; + int strafes_deadair[JS_MAX_TRACKED_STRAFES]; + int strafes_overlap[JS_MAX_TRACKED_STRAFES]; + int strafes_ticks[JS_MAX_TRACKED_STRAFES]; + float strafes_gain[JS_MAX_TRACKED_STRAFES]; + float strafes_loss[JS_MAX_TRACKED_STRAFES]; + float strafes_sync[JS_MAX_TRACKED_STRAFES]; + float strafes_width[JS_MAX_TRACKED_STRAFES]; +} + + + +// =====[ FORWARDS ]===== + +/** + * Called when a player begins their jump. + * + * @param client Client index. + * @param jumpType Type of jump. + */ +forward void GOKZ_JS_OnTakeoff(int client, int jumpType); + +/** + * Called when a player lands their jump. + * + * @param jump The jumpstats. + */ +forward void GOKZ_JS_OnLanding(Jump jump); + +/** + * Called when player's current jump has been declared an invalid jumpstat. + * + * @param client Client index. + */ +forward void GOKZ_JS_OnJumpInvalidated(int client); + +/** + * Called when a player fails a blockjump. + * + * @param jump The jumpstats. + */ +forward void GOKZ_JS_OnFailstat(Jump jump); + +/** + * Called when a player lands a jump and has always-on jumpstats enabled. + * + * @param jump The jumpstats. + */ +forward void GOKZ_JS_OnJumpstatAlways(Jump jump); + +/** + * Called when a player fails a jump and has always-on failstats enabled. + * + * @param jump The failstats. + */ +forward void GOKZ_JS_OnFailstatAlways(Jump jump); + + + +// =====[ NATIVES ]===== + +/** + * Gets the default jumpstats option value as set by a config file. + * + * @param option GOKZ Jumpstats option. + * @return Default option value. + */ +native int GOKZ_JS_GetDefaultOption(JSOption option); + +/** + * Declare a player's current jump an invalid jumpstat. + * + * @param client Client index. + */ +native void GOKZ_JS_InvalidateJump(int client); + + + +// =====[ STOCKS ]===== + +/** + * Returns whether an option is a gokz-jumpstats option. + * + * @param option Option name. + * @param optionEnum Variable to store enumerated gokz-jumpstats option (if it is one). + * @return Whether option is a gokz-jumpstats option. + */ +stock bool GOKZ_JS_IsJSOption(const char[] option, JSOption &optionEnum = JSOPTION_INVALID) +{ + for (JSOption i; i < JSOPTION_COUNT; i++) + { + if (StrEqual(option, gC_JSOptionNames[i])) + { + optionEnum = i; + return true; + } + } + return false; +} + +/** + * Gets the current value of a player's gokz-jumpstats option. + * + * @param client Client index. + * @param option gokz-jumpstats option. + * @return Current value of option. + */ +stock any GOKZ_JS_GetOption(int client, JSOption option) +{ + return GOKZ_GetOption(client, gC_JSOptionNames[option]); +} + +/** + * Sets a player's gokz-jumpstats option's value. + * + * @param client Client index. + * @param option gokz-jumpstats option. + * @param value New option value. + * @return Whether option was successfully set. + */ +stock bool GOKZ_JS_SetOption(int client, JSOption option, any value) +{ + return GOKZ_SetOption(client, gC_JSOptionNames[option], value); +} + +/** + * Increment an integer-type gokz-jumpstats option's value. + * Loops back to '0' if max value is exceeded. + * + * @param client Client index. + * @param option gokz-jumpstats option. + * @return Whether option was successfully set. + */ +stock bool GOKZ_JS_CycleOption(int client, JSOption option) +{ + return GOKZ_CycleOption(client, gC_JSOptionNames[option]); +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_jumpstats = +{ + name = "gokz-jumpstats", + file = "gokz-jumpstats.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_jumpstats_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_JS_GetDefaultOption"); + MarkNativeAsOptional("GOKZ_JS_InvalidateJump"); +} +#endif diff --git a/sourcemod/scripting/include/gokz/kzplayer.inc b/sourcemod/scripting/include/gokz/kzplayer.inc new file mode 100644 index 0000000..8176d39 --- /dev/null +++ b/sourcemod/scripting/include/gokz/kzplayer.inc @@ -0,0 +1,584 @@ +/* + GOKZ KZPlayer Methodmap Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_kzplayer_included_ +#endinput +#endif +#define _gokz_kzplayer_included_ + +#include <movementapi> + +#include <gokz> + + + +methodmap KZPlayer < MovementAPIPlayer { + + public KZPlayer(int client) { + return view_as<KZPlayer>(MovementAPIPlayer(client)); + } + + + + // =====[ GENERAL ]===== + + property bool Valid { + public get() { + return IsValidClient(this.ID); + } + } + + property bool InGame { + public get() { + return IsClientInGame(this.ID); + } + } + + property bool Authorized { + public get() { + return IsClientAuthorized(this.ID); + } + } + + property bool Fake { + public get() { + return IsFakeClient(this.ID); + } + } + + property bool Alive { + public get() { + return IsPlayerAlive(this.ID); + } + } + + property ObsMode ObserverMode { + public get() { + return GetObserverMode(this.ID); + } + } + + property int ObserverTarget { + public get() { + return GetObserverTarget(this.ID); + } + } + + + + // =====[ CORE ]===== + #if defined _gokz_core_included_ + + public void StartTimer(int course) { + GOKZ_StartTimer(this.ID, course); + } + + public void EndTimer(int course) { + GOKZ_EndTimer(this.ID, course); + } + + public bool StopTimer() { + return GOKZ_StopTimer(this.ID); + } + + public void TeleportToStart() { + GOKZ_TeleportToStart(this.ID); + } + + public void TeleportToSearchStart(int course) { + GOKZ_TeleportToSearchStart(this.ID, course); + } + + public void TeleportToEnd(int course) { + GOKZ_TeleportToEnd(this.ID, course); + } + + property StartPositionType StartPositionType { + public get() { + return GOKZ_GetStartPositionType(this.ID); + } + } + + public void MakeCheckpoint() { + GOKZ_MakeCheckpoint(this.ID); + } + + property bool CanMakeCheckpoint { + public get() { + return GOKZ_GetCanMakeCheckpoint(this.ID); + } + } + + public void TeleportToCheckpoint() { + GOKZ_TeleportToCheckpoint(this.ID); + } + + property bool CanTeleportToCheckpoint { + public get() { + return GOKZ_GetCanTeleportToCheckpoint(this.ID); + } + } + + public void PrevCheckpoint() { + GOKZ_PrevCheckpoint(this.ID); + } + + property bool CanPrevCheckpoint { + public get() { + return GOKZ_GetCanPrevCheckpoint(this.ID); + } + } + + public void NextCheckpoint() { + GOKZ_NextCheckpoint(this.ID); + } + + property bool CanNextCheckpoint { + public get() { + return GOKZ_GetCanNextCheckpoint(this.ID); + } + } + + public void UndoTeleport() { + GOKZ_UndoTeleport(this.ID); + } + + property bool CanUndoTeleport { + public get() { + return GOKZ_GetCanUndoTeleport(this.ID); + } + } + + public void Pause() { + GOKZ_Pause(this.ID); + } + + property bool CanPause { + public get() { + return GOKZ_GetCanPause(this.ID); + } + } + + public void Resume() { + GOKZ_Resume(this.ID); + } + + property bool CanResume { + public get() { + return GOKZ_GetCanResume(this.ID); + } + } + + public void TogglePause() { + GOKZ_TogglePause(this.ID); + } + + public void PlayErrorSound() { + GOKZ_PlayErrorSound(this.ID); + } + + property bool TimerRunning { + public get() { + return GOKZ_GetTimerRunning(this.ID); + } + } + + property int Course { + public get() { + return GOKZ_GetCourse(this.ID); + } + } + + property bool Paused { + public get() { + return GOKZ_GetPaused(this.ID); + } + public set(bool pause) { + if (pause) { + this.Pause(); + } + else { + this.Resume(); + } + } + } + + property bool CanTeleportToStart { + public get() { + return GOKZ_GetCanTeleportToStartOrEnd(this.ID); + } + } + + property float Time { + public get() { + return GOKZ_GetTime(this.ID); + } + public set(float value) { + GOKZ_SetTime(this.ID, value); + } + } + + property int CheckpointCount { + public get() { + return GOKZ_GetCheckpointCount(this.ID); + } + public set(int cpCount) { + GOKZ_SetCheckpointCount(this.ID, cpCount); + } + } + + property ArrayList CheckpointData { + public get() { + return GOKZ_GetCheckpointData(this.ID); + } + public set(ArrayList checkpoints) { + GOKZ_SetCheckpointData(this.ID, checkpoints, GOKZ_CHECKPOINT_VERSION); + } + } + + property int TeleportCount { + public get() { + return GOKZ_GetTeleportCount(this.ID); + } + public set(int value) { + GOKZ_SetTeleportCount(this.ID, value); + } + } + + property int TimeType { + public get() { + return GOKZ_GetTimeType(this.ID); + } + } + + property bool GOKZHitPerf { + public get() { + return GOKZ_GetHitPerf(this.ID); + } + public set(bool value) { + GOKZ_SetHitPerf(this.ID, value); + } + } + + property float GOKZTakeoffSpeed { + public get() { + return GOKZ_GetTakeoffSpeed(this.ID); + } + public set(float value) { + GOKZ_SetTakeoffSpeed(this.ID, value); + } + } + + property bool ValidJump { + public get() { + return GOKZ_GetValidJump(this.ID); + } + } + + public any GetOption(const char[] option) { + return GOKZ_GetOption(this.ID, option); + } + + public bool SetOption(const char[] option, any value) { + return GOKZ_SetOption(this.ID, option, value); + } + + public bool CycleOption(const char[] option) { + return GOKZ_CycleOption(this.ID, option); + } + + public any GetCoreOption(Option option) { + return GOKZ_GetCoreOption(this.ID, option); + } + + public bool SetCoreOption(Option option, int value) { + return GOKZ_SetCoreOption(this.ID, option, value); + } + + public bool CycleCoreOption(Option option) { + return GOKZ_CycleCoreOption(this.ID, option); + } + + property int Mode { + public get() { + return this.GetCoreOption(Option_Mode); + } + public set(int value) { + this.SetCoreOption(Option_Mode, value); + } + } + + property int Style { + public get() { + return this.GetCoreOption(Option_Style); + } + public set(int value) { + this.SetCoreOption(Option_Style, value); + } + } + + property int CheckpointMessages { + public get() { + return this.GetCoreOption(Option_CheckpointMessages); + } + public set(int value) { + this.SetCoreOption(Option_CheckpointMessages, value); + } + } + + property int CheckpointSounds { + public get() { + return this.GetCoreOption(Option_CheckpointSounds); + } + public set(int value) { + this.SetCoreOption(Option_CheckpointSounds, value); + } + } + + property int TeleportSounds { + public get() { + return this.GetCoreOption(Option_TeleportSounds); + } + public set(int value) { + this.SetCoreOption(Option_TeleportSounds, value); + } + } + + property int ErrorSounds { + public get() { + return this.GetCoreOption(Option_ErrorSounds); + } + public set(int value) { + this.SetCoreOption(Option_ErrorSounds, value); + } + } + + #endif + // =====[ END CORE ]===== + + + + // =====[ HUD ]===== + #if defined _gokz_hud_included_ + + public any GetHUDOption(HUDOption option) { + return GOKZ_HUD_GetOption(this.ID, option); + } + + public bool SetHUDOption(HUDOption option, any value) { + return GOKZ_HUD_SetOption(this.ID, option, value); + } + + public bool CycleHUDOption(HUDOption option) { + return GOKZ_HUD_CycleOption(this.ID, option); + } + + property int TPMenu { + public get() { + return this.GetHUDOption(HUDOption_TPMenu); + } + public set(int value) { + this.SetHUDOption(HUDOption_TPMenu, value); + } + } + + property int InfoPanel { + public get() { + return this.GetHUDOption(HUDOption_InfoPanel); + } + public set(int value) { + this.SetHUDOption(HUDOption_InfoPanel, value); + } + } + + property int ShowKeys { + public get() { + return this.GetHUDOption(HUDOption_ShowKeys); + } + public set(int value) { + this.SetHUDOption(HUDOption_ShowKeys, value); + } + } + + property int TimerText { + public get() { + return this.GetHUDOption(HUDOption_TimerText); + } + public set(int value) { + this.SetHUDOption(HUDOption_TimerText, value); + } + } + + property int TimerStyle { + public get() { + return this.GetHUDOption(HUDOption_TimerStyle); + } + public set(int value) { + this.SetHUDOption(HUDOption_TimerStyle, value); + } + } + + property int SpeedText { + public get() { + return this.GetHUDOption(HUDOption_SpeedText); + } + public set(int value) { + this.SetHUDOption(HUDOption_SpeedText, value); + } + } + + property int ShowWeapon { + public get() { + return this.GetHUDOption(HUDOption_ShowWeapon); + } + public set(int value) { + this.SetHUDOption(HUDOption_ShowWeapon, value); + } + } + + property int ReplayControls { + public get() { + return this.GetHUDOption(HUDOption_ShowControls); + } + public set(int value) { + this.SetHUDOption(HUDOption_ShowControls, value); + } + } + + property int ShowSpectators { + public get() { + return this.GetHUDOption(HUDOption_ShowSpectators); + } + public set(int value) { + this.SetHUDOption(HUDOption_ShowSpectators, value); + } + } + + property int SpecListPosition { + public get() { + return this.GetHUDOption(HUDOption_SpecListPosition); + } + public set(int value){ + this.SetHUDOption(HUDOption_SpecListPosition, value); + } + } + + property bool MenuShowing { + public get() { + return GOKZ_HUD_GetMenuShowing(this.ID); + } + public set(bool value) { + GOKZ_HUD_SetMenuShowing(this.ID, value); + } + } + property int DynamicMenu { + public get() { + return this.GetHUDOption(HUDOption_DynamicMenu); + } + public set(int value) { + this.SetHUDOption(HUDOption_DynamicMenu, value); + } + } + #endif + // =====[ END HUD ]===== + + + + // =====[ PISTOL ]===== + #if defined _gokz_pistol_included_ + + property int Pistol { + public get() { + return this.GetOption(PISTOL_OPTION_NAME); + } + public set(int value) { + this.SetOption(PISTOL_OPTION_NAME, value); + } + } + + #endif + // =====[ END PISTOL ]===== + + + + // =====[ JUMP BEAM ]===== + #if defined _gokz_jumpbeam_included_ + + public any GetJBOption(JBOption option) { + return GOKZ_JB_GetOption(this.ID, option); + } + + public bool SetJBOption(JBOption option, any value) { + return GOKZ_JB_SetOption(this.ID, option, value); + } + + public bool CycleJBOption(JBOption option) { + return GOKZ_JB_CycleOption(this.ID, option); + } + + property int JBType { + public get() { + return this.GetJBOption(JBOption_Type); + } + public set(int value) { + this.SetJBOption(JBOption_Type, value); + } + } + + #endif + // =====[ END JUMP BEAM ]===== + + + + // =====[ TIPS ]===== + #if defined _gokz_tips_included_ + + property int Tips { + public get() { + return this.GetOption(TIPS_OPTION_NAME); + } + public set(int value) { + this.SetOption(TIPS_OPTION_NAME, value); + } + } + + #endif + // =====[ END TIPS ]===== + + + + // =====[ QUIET ]===== + #if defined _gokz_quiet_included_ + + property int ShowPlayers { + public get() { + return this.GetOption(gC_QTOptionNames[QTOption_ShowPlayers]); + } + public set(int value) { + this.SetOption(gC_QTOptionNames[QTOption_ShowPlayers], value); + } + } + + #endif + // =====[ END QUIET ]===== + + + + // =====[ SLAY ON END ]===== + #if defined _gokz_slayonend_included_ + + property int SlayOnEnd { + public get() { + return this.GetOption(SLAYONEND_OPTION_NAME); + } + public set(int value) { + this.SetOption(SLAYONEND_OPTION_NAME, value); + } + } + + #endif + // =====[ END SLAY ON END ]===== +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/localdb.inc b/sourcemod/scripting/include/gokz/localdb.inc new file mode 100644 index 0000000..472a120 --- /dev/null +++ b/sourcemod/scripting/include/gokz/localdb.inc @@ -0,0 +1,353 @@ +/* + gokz-localdb Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_localdb_included_ +#endinput +#endif +#define _gokz_localdb_included_ + + + +// =====[ ENUMS ]===== + +enum DatabaseType +{ + DatabaseType_None = -1, + DatabaseType_MySQL, + DatabaseType_SQLite +}; + +enum +{ + JumpstatDB_Lookup_JumpID = 0, + JumpstatDB_Lookup_Distance, + JumpstatDB_Lookup_Block +}; + +enum +{ + JumpstatDB_FindPlayer_SteamID32 = 0, + JumpstatDB_FindPlayer_Alias +}; + +enum +{ + JumpstatDB_Top20_JumpID = 0, + JumpstatDB_Top20_SteamID, + JumpstatDB_Top20_Alias, + JumpstatDB_Top20_Block, + JumpstatDB_Top20_Distance, + JumpstatDB_Top20_Strafes, + JumpstatDB_Top20_Sync, + JumpstatDB_Top20_Pre, + JumpstatDB_Top20_Max, + JumpstatDB_Top20_Air +}; + +enum +{ + JumpstatDB_PBMenu_JumpID = 0, + JumpstatDB_PBMenu_JumpType, + JumpstatDB_PBMenu_Distance, + JumpstatDB_PBMenu_Strafes, + JumpstatDB_PBMenu_Sync, + JumpstatDB_PBMenu_Pre, + JumpstatDB_PBMenu_Max, + JumpstatDB_PBMenu_Air +}; + +enum +{ + JumpstatDB_BlockPBMenu_JumpID = 0, + JumpstatDB_BlockPBMenu_JumpType, + JumpstatDB_BlockPBMenu_Block, + JumpstatDB_BlockPBMenu_Distance, + JumpstatDB_BlockPBMenu_Strafes, + JumpstatDB_BlockPBMenu_Sync, + JumpstatDB_BlockPBMenu_Pre, + JumpstatDB_BlockPBMenu_Max, + JumpstatDB_BlockPBMenu_Air +}; + +enum +{ + JumpstatDB_Cache_Distance = 0, + JumpstatDB_Cache_Block, + JumpstatDB_Cache_BlockDistance, + JUMPSTATDB_CACHE_COUNT +}; + +enum +{ + TimerSetupDB_GetVBPos_SteamID = 0, + TimerSetupDB_GetVBPos_MapID, + TimerSetupDB_GetVBPos_Course, + TimerSetupDB_GetVBPos_IsStart, + TimerSetupDB_GetVBPos_PositionX, + TimerSetupDB_GetVBPos_PositionY, + TimerSetupDB_GetVBPos_PositionZ +}; + +enum +{ + TimerSetupDB_GetStartPos_SteamID = 0, + TimerSetupDB_GetStartPos_MapID, + TimerSetupDB_GetStartPos_PositionX, + TimerSetupDB_GetStartPos_PositionY, + TimerSetupDB_GetStartPos_PositionZ, + TimerSetupDB_GetStartPos_Angle0, + TimerSetupDB_GetStartPos_Angle1 +}; + +enum DBOption: +{ + DBOption_AutoLoadTimerSetup = 0, + DBOPTION_COUNT +}; + +enum +{ + DBOption_Disabled = 0, + DBOption_Enabled, + DBOPTIONBOOL_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define GOKZ_DB_JS_DISTANCE_PRECISION 10000 +#define GOKZ_DB_JS_SYNC_PRECISION 100 +#define GOKZ_DB_JS_PRE_PRECISION 100 +#define GOKZ_DB_JS_MAX_PRECISION 100 +#define GOKZ_DB_JS_AIRTIME_PRECISION 10000 +#define GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER 100 + +stock char gC_DBOptionNames[DBOPTION_COUNT][] = +{ + "GOKZ DB - Auto Load Setup" +}; + +stock char gC_DBOptionDescriptions[DBOPTION_COUNT][] = +{ + "Automatically load timer setup on map start - 0 = Disabled, 1 = Enabled" +} + +stock int gI_DBOptionDefaultValues[DBOPTION_COUNT] = +{ + DBOption_Disabled +}; + +stock int gI_DBOptionCounts[DBOPTION_COUNT] = +{ + DBOPTIONBOOL_COUNT +}; + +stock char gC_DBOptionPhrases[DBOPTION_COUNT][] = +{ + "Options Menu - Auto Load Timer Setup" +}; + + + +// =====[ TYPES ]===== + +typeset GetVBPositionCallback +{ + function Action(int client, const float position[3], int course, bool isStart); +}; + +typeset GetStartPositionCallback +{ + function Action(int client, const float position[3], const float angles[3]); +}; + + + +// =====[ FORWARDS ]===== + +/** + * Called when gokz-localdb has connected to the database. + * Use GOKZ_DB_GetDatabase to get a clone of the database handle. + * + * @param DBType Database type. + */ +forward void GOKZ_DB_OnDatabaseConnect(DatabaseType DBType); + +/** + * Called when a player is ready for database interaction. + * At this point, the player is present and updated in the "Players" table. + * + * @param client Client index. + * @param steamID SteamID32 of the player (from GetSteamAccountID()). + * @param cheater Whether player is marked as a cheater in the database. + */ +forward void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater); + +/** + * Called when the current map is ready for database interaction. + * At this point, the map is present and updated in the "Maps" table. + * + * @param mapID MapID from the "Maps" table. + */ +forward void GOKZ_DB_OnMapSetup(int mapID); + +/** + * Called when a time has been inserted into the database. + * + * @param client Client index. + * @param steamID SteamID32 of the player (from GetSteamAccountID()). + * @param mapID MapID from the "Maps" table. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param style Player's movement style. + * @param runTimeMS Player's end time in milliseconds. + * @param teleportsUsed Number of teleports used by player. + */ +forward void GOKZ_DB_OnTimeInserted( + int client, + int steamID, + int mapID, + int course, + int mode, + int style, + int runTimeMS, + int teleportsUsed); + +/** + * Called when jumpstat PB has been achieved. + * + * @param client Client index. + * @param jumptype Type of the jump. + * @param mode Mode the jump was performed in. + * @param distance Distance jumped. + * @param block The size of the block jumped across. + * @param strafes The amount of strafes used. + * @param sync Keyboard/mouse synchronisation of the jump. + * @param pre Speed at takeoff. + * @param max Maximum speed during the jump. + * @param airtime Amount of time spend airborne. + */ +forward void GOKZ_DB_OnJumpstatPB( + int client, + int jumptype, + int mode, + float distance, + int block, + int strafes, + float sync, + float pre, + float max, + int airtime); + + +// =====[ NATIVES ]===== + +/** + * Gets a clone of the GOKZ local database handle. + * + * @param database Database handle, or null if connection hasn't been made. + */ +native Database GOKZ_DB_GetDatabase(); + +/** + * Gets the GOKZ local database type. + * + * @return Database type. + */ +native DatabaseType GOKZ_DB_GetDatabaseType(); + +/** + * Gets whether client has been set up for GOKZ Local DB. + * + * @param client Client index. + * @return Whether GOKZ Local DB has set up the client. + */ +native bool GOKZ_DB_IsClientSetUp(int client); + +/** + * Gets whether GOKZ Local DB is set up for the current map. + * + * @return Whether GOKZ Local DB has set up the current map. + */ +native bool GOKZ_DB_IsMapSetUp(); + +/** + * Gets the current map's MapID as in the "Maps" table. + * + * @return MapID from the "Maps" table. + */ +native int GOKZ_DB_GetCurrentMapID(); + +/** + * Gets whether player is marked as a cheater in the database. + * + * @param client Client index. + * @return Whether player is marked as a cheater in the database. + */ +native bool GOKZ_DB_IsCheater(int client); + +/** + * Sets wheter player is marked as a cheater in the database. + * + * @param client Client index. + * @param cheater Whether to mark the player as a cheater. + */ +native void GOKZ_DB_SetCheater(int client, bool cheater); + + + +// =====[ STOCKS ]===== + +/** + * Converts a time float (seconds) to an integer (milliseconds). + * + * @param time Time in seconds. + * @return Time in milliseconds. + */ +stock int GOKZ_DB_TimeFloatToInt(float time) +{ + return RoundFloat(time * 1000.0); +} + +/** + * Converts a time integer (milliseconds) to a float (seconds). + * + * @param time Time in milliseconds. + * @return Time in seconds. + */ +stock float GOKZ_DB_TimeIntToFloat(int time) +{ + return time / 1000.0; +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_localdb = +{ + name = "gokz-localdb", + file = "gokz-localdb.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_localdb_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_DB_GetDatabase"); + MarkNativeAsOptional("GOKZ_DB_GetDatabaseType"); + MarkNativeAsOptional("GOKZ_DB_IsClientSetUp"); + MarkNativeAsOptional("GOKZ_DB_IsMapSetUp"); + MarkNativeAsOptional("GOKZ_DB_GetCurrentMapID"); + MarkNativeAsOptional("GOKZ_DB_IsCheater"); + MarkNativeAsOptional("GOKZ_DB_SetCheater"); +} +#endif diff --git a/sourcemod/scripting/include/gokz/localranks.inc b/sourcemod/scripting/include/gokz/localranks.inc new file mode 100644 index 0000000..914c6cb --- /dev/null +++ b/sourcemod/scripting/include/gokz/localranks.inc @@ -0,0 +1,176 @@ +/* + gokz-localranks Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_localranks_included_ +#endinput +#endif +#define _gokz_localranks_included_ + + + +// =====[ ENUMS ]===== + +enum +{ + RecordType_Nub = 0, + RecordType_Pro, + RecordType_NubAndPro, + RECORDTYPE_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define LR_CFG_MAP_POOL "cfg/sourcemod/gokz/gokz-localranks-mappool.cfg" +#define LR_CFG_SOUNDS "cfg/sourcemod/gokz/gokz-localranks-sounds.cfg" +#define LR_COMMAND_COOLDOWN 2.5 +#define LR_MAP_TOP_CUTOFF 20 +#define LR_PLAYER_TOP_CUTOFF 20 + + + +// =====[ FORWARDS ]===== + +/** + * Called when a player's time has been processed by GOKZ Local Ranks. + * + * @param client Client index. + * @param steamID SteamID32 of the player (from GetSteamAccountID()). + * @param mapID MapID from the "Maps" database table. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param style Player's movement style. + * @param runTime Player's end time. + * @param teleportsUsed Number of teleportsUsed used by player. + * @param firstTime Whether this is player's first time on this course. + * @param pbDiff Difference between new time and PB in seconds (-'ve means beat PB). + * @param rank New rank of the player's PB time. + * @param maxRank New total number of players with times. + * @param firstTimePro Whether this is player's first PRO time on this course. + * @param pbDiffPro Difference between new time and PRO PB in seconds (-'ve means beat PB). + * @param rankPro New rank of the player's PB PRO time. + * @param maxRankPro New total number of players with PRO times. + */ +forward void GOKZ_LR_OnTimeProcessed( + int client, + int steamID, + int mapID, + int course, + int mode, + int style, + float runTime, + int teleportsUsed, + bool firstTime, + float pbDiff, + int rank, + int maxRank, + bool firstTimePro, + float pbDiffPro, + int rankPro, + int maxRankPro); + +/** + * Called when a player sets a new local record. + * + * @param client Client index. + * @param steamID SteamID32 of the player (from GetSteamAccountID()). + * @param mapID MapID from the "Maps" table. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param style Player's movement style. + * @param recordType Type of record. + */ +forward void GOKZ_LR_OnNewRecord( + int client, + int steamID, + int mapID, + int course, + int mode, + int style, + int recordType, + float pbDiff, + int teleportsUsed); + +/** + * Called when a player misses the server record time. + * Is called regardless of player's current run type. + * + * @param client Client index. + * @param recordTime Record time missed. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param style Player's movement style. + * @param recordType Type of record. + */ +forward void GOKZ_LR_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType); + +/** + * Called when a player misses their personal best time. + * Is called regardless of player's current run type. + * + * @param client Client index. + * @param pbTime Personal best time missed. + * @param course Course number e.g. 0=main, 1='bonus1' etc. + * @param mode Player's movement mode. + * @param style Player's movement style. + * @param recordType Type of record. + */ +forward void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType); + + + +// =====[ NATIVES ]===== + +/** + * Gets whether player has missed the server record time. + * + * @param client Client index. + * @param timeType Which record time i.e. NUB or PRO. + * @return Whether player has missed the server record time. + */ +native bool GOKZ_LR_GetRecordMissed(int client, int timeType); + +/** + * Gets whether player has missed their personal best time. + * + * @param client Client index. + * @param timeType Which PB time i.e. NUB or PRO. + * @return Whether player has missed their PB time. + */ +native bool GOKZ_LR_GetPBMissed(int client, int timeType); + +/** + * Reopens the map top menu with the already selected parameters. + * Don't use if the client hasn't opened the map top menu before. + * + * @param client Client index. + */ +native void GOKZ_LR_ReopenMapTopMenu(int client); + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_localranks = +{ + name = "gokz-localranks", + file = "gokz-localranks.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_localranks_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_LR_GetRecordMissed"); + MarkNativeAsOptional("GOKZ_LR_GetPBMissed"); + MarkNativeAsOptional("GOKZ_LR_ReopenMapTopMenu"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/momsurffix.inc b/sourcemod/scripting/include/gokz/momsurffix.inc new file mode 100644 index 0000000..65f603e --- /dev/null +++ b/sourcemod/scripting/include/gokz/momsurffix.inc @@ -0,0 +1,23 @@ +/* + gokz-momsurffix Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_momsurffix_included_ +#endinput +#endif +#define _gokz_momsurffix_included_ + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_momsurffix = +{ + name = "gokz-momsurffix", + file = "gokz-momsurffix.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/paint.inc b/sourcemod/scripting/include/gokz/paint.inc new file mode 100644 index 0000000..19f4fb5 --- /dev/null +++ b/sourcemod/scripting/include/gokz/paint.inc @@ -0,0 +1,114 @@ +/* + gokz-paint Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_paint_included_ +#endinput +#endif +#define _gokz_paint_included_ + + + +// =====[ ENUMS ]===== + +enum PaintOption: +{ + PAINTOPTION_INVALID = -1, + PaintOption_Color, + PaintOption_Size, + PAINTOPTION_COUNT +}; + +enum +{ + PaintColor_Red = 0, + PaintColor_White, + PaintColor_Black, + PaintColor_Blue, + PaintColor_Brown, + PaintColor_Green, + PaintColor_Yellow, + PaintColor_Purple, + PAINTCOLOR_COUNT +}; + +enum +{ + PaintSize_Small = 0, + PaintSize_Medium, + PaintSize_Big, + PAINTSIZE_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define PAINT_OPTION_CATEGORY "Paint" +#define MIN_PAINT_SPACING 1.0 + +stock char gC_PaintOptionNames[PAINTOPTION_COUNT][] = +{ + "GOKZ Paint - Color", + "GOKZ Paint - Size" +}; + +stock char gC_PaintOptionDescriptions[PAINTOPTION_COUNT][] = +{ + "Paint Color", + "Paint Size - 0 = Small, 1 = Medium, 2 = Big" +}; + +stock char gC_PaintOptionPhrases[PAINTOPTION_COUNT][] = +{ + "Options Menu - Paint Color", + "Options Menu - Paint Size" +}; + +stock int gI_PaintOptionCounts[PAINTOPTION_COUNT] = +{ + PAINTCOLOR_COUNT, + PAINTSIZE_COUNT +}; + +stock int gI_PaintOptionDefaults[PAINTOPTION_COUNT] = +{ + PaintColor_Red, + PaintSize_Medium +}; + +stock char gC_PaintColorPhrases[PAINTCOLOR_COUNT][] = +{ + "Options Menu - Red", + "Options Menu - White", + "Options Menu - Black", + "Options Menu - Blue", + "Options Menu - Brown", + "Options Menu - Green", + "Options Menu - Yellow", + "Options Menu - Purple" +}; + +stock char gC_PaintSizePhrases[PAINTSIZE_COUNT][] = +{ + "Options Menu - Small", + "Options Menu - Medium", + "Options Menu - Big" +}; + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_paint = +{ + name = "gokz-paint", + file = "gokz-paint.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; diff --git a/sourcemod/scripting/include/gokz/pistol.inc b/sourcemod/scripting/include/gokz/pistol.inc new file mode 100644 index 0000000..1edd5f9 --- /dev/null +++ b/sourcemod/scripting/include/gokz/pistol.inc @@ -0,0 +1,93 @@ +/* + gokz-pistol Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_pistol_included_ +#endinput +#endif +#define _gokz_pistol_included_ + + + +// =====[ ENUMS ]===== + +enum +{ + Pistol_Disabled = 0, + Pistol_USPS, + Pistol_Glock18, + Pistol_DualBerettas, + Pistol_P250, + Pistol_FiveSeveN, + Pistol_Tec9, + Pistol_CZ75Auto, + Pistol_DesertEagle, + Pistol_R8Revolver, + PISTOL_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define PISTOL_OPTION_NAME "GOKZ - Pistol" +#define PISTOL_OPTION_DESCRIPTION "Pistol - 0 = Disabled, 1 = USP-S / P2000, 2 = Glock-18, 3 = Dual Berettas, 4 = P250, 5 = Five-SeveN, 6 = Tec-9, 7 = CZ75-Auto, 8 = Desert Eagle, 9 = R8 Revolver" + +stock char gC_PistolNames[PISTOL_COUNT][] = +{ + "", // Disabled + "USP-S / P2000", + "Glock-18", + "Dual Berettas", + "P250", + "Five-SeveN", + "Tec-9", + "CZ75-Auto", + "Desert Eagle", + "R8 Revolver" +}; + +stock char gC_PistolClassNames[PISTOL_COUNT][] = +{ + "", // Disabled + "weapon_hkp2000", + "weapon_glock", + "weapon_elite", + "weapon_p250", + "weapon_fiveseven", + "weapon_tec9", + "weapon_cz75a", + "weapon_deagle", + "weapon_revolver" +}; + +stock int gI_PistolTeams[PISTOL_COUNT] = +{ + CS_TEAM_NONE, // Disabled + CS_TEAM_CT, + CS_TEAM_T, + CS_TEAM_NONE, + CS_TEAM_NONE, + CS_TEAM_CT, + CS_TEAM_T, + CS_TEAM_NONE, + CS_TEAM_NONE, + CS_TEAM_NONE +}; + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_pistol = +{ + name = "gokz-pistol", + file = "gokz-pistol.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/profile.inc b/sourcemod/scripting/include/gokz/profile.inc new file mode 100644 index 0000000..70d314a --- /dev/null +++ b/sourcemod/scripting/include/gokz/profile.inc @@ -0,0 +1,291 @@ +/* + gokz-profile Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_profile_included_ +#endinput +#endif +#define _gokz_profile_included_ + + +// =====[ RANKS ]===== + +#define RANK_COUNT 23 + +stock int gI_rankThreshold[MODE_COUNT][RANK_COUNT] = { + { + 0, + 1, + 500, + 1000, + + 2000, + 5000, + 10000, + + 20000, + 30000, + 40000, + + 60000, + 70000, + 80000, + + 100000, + 120000, + 140000, + + 160000, + 180000, + 200000, + + 250000, + 300000, + 400000, + 600000 + }, + { + 0, + 1, + 500, + 1000, + + 2000, + 5000, + 10000, + + 20000, + 30000, + 40000, + + 60000, + 70000, + 80000, + + 100000, + 120000, + 150000, + + 200000, + 230000, + 250000, + + 300000, + 400000, + 500000, + 800000 + }, + { + 0, + 1, + 500, + 1000, + + 2000, + 5000, + 10000, + + 20000, + 30000, + 40000, + + 60000, + 70000, + 80000, + + 100000, + 120000, + 150000, + + 200000, + 230000, + 250000, + + 400000, + 600000, + 800000, + 1000000 + }, +}; + +stock char gC_rankName[RANK_COUNT][] = { + "New", + "Beginner-", + "Beginner", + "Beginner+", + "Amateur-", + "Amateur", + "Amateur+", + "Casual-", + "Casual", + "Casual+", + "Regular-", + "Regular", + "Regular+", + "Skilled-", + "Skilled", + "Skilled+", + "Expert-", + "Expert", + "Expert+", + "Semipro", + "Pro", + "Master", + "Legend" +}; + +stock char gC_rankColor[RANK_COUNT][] = { + "{grey}", + "{default}", + "{default}", + "{default}", + "{blue}", + "{blue}", + "{blue}", + "{lightgreen}", + "{lightgreen}", + "{lightgreen}", + "{green}", + "{green}", + "{green}", + "{purple}", + "{purple}", + "{purple}", + "{orchid}", + "{orchid}", + "{orchid}", + "{lightred}", + "{lightred}", + "{red}", + "{gold}" +}; + + +// =====[ ENUMS ]===== + +enum ProfileOption: +{ + PROFILEOPTION_INVALID = -1, + ProfileOption_ShowRankChat, + ProfileOption_ShowRankClanTag, + ProfileOption_TagType, + PROFILEOPTION_COUNT +}; + +enum +{ + ProfileOptionBool_Disabled = 0, + ProfileOptionBool_Enabled, + PROFILEOPTIONBOOL_COUNT +}; + +enum +{ + ProfileTagType_Rank = 0, + ProfileTagType_Admin, + ProfileTagType_VIP, + PROFILETAGTYPE_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +stock char gC_ProfileOptionNames[PROFILEOPTION_COUNT][] = +{ + "GOKZ Profile - Show Rank Chat", + "GOKZ Profile - Show Rank Clan", + "GOKZ Profile - Tag Type" +}; + +stock char gC_ProfileOptionDescriptions[PROFILEOPTION_COUNT][] = +{ + "Show Rank Tag in Chat - 0 = Disabled, 1 = Enabled", + "Show Rank in Clan - 0 = Disabled, 1 = Enabled", + "Type of Tag to Show - 0 = Rank, 1 = Admin, 2 = VIP" +}; + +stock char gC_ProfileOptionPhrases[PROFILEOPTION_COUNT][] = +{ + "Options Menu - Show Rank Chat", + "Options Menu - Show Rank Clan", + "Options Menu - Tag Type", +}; + +stock char gC_ProfileBoolPhrases[PROFILEOPTIONBOOL_COUNT][] = +{ + "Options Menu - Disabled", + "Options Menu - Enabled" +}; + +stock char gC_ProfileTagTypePhrases[PROFILETAGTYPE_COUNT][] = +{ + "Options Menu - Tag Rank", + "Options Menu - Tag Admin", + "Options Menu - Tag VIP" +}; + +stock int gI_ProfileOptionCounts[PROFILEOPTION_COUNT] = +{ + PROFILEOPTIONBOOL_COUNT, + PROFILEOPTIONBOOL_COUNT, + PROFILETAGTYPE_COUNT +}; + +stock int gI_ProfileOptionDefaults[PROFILEOPTION_COUNT] = +{ + ProfileOptionBool_Enabled, + ProfileOptionBool_Enabled, + ProfileTagType_Rank +}; + +#define PROFILE_OPTION_CATEGORY "Profile" +#define TAG_COLOR_ADMIN "{red}" +#define TAG_COLOR_VIP "{purple}" + + +// =====[ FORWARDS ]===== + + +/** + * Called when the rank of a player is updated. + * + * @param client Client index. + * @param mode Game mode. + * @param rank The new rank. + */ +forward void GOKZ_PF_OnRankUpdated(int client, int mode, int rank); + +// =====[ NATIVES ]===== + +/** + * Gets whether a mode is loaded. + * + * @param client Client. + * @param tag Mode. + * @returns Integer representing the player rank. + */ +native int GOKZ_PF_GetRank(int client, int mode); + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_profile = +{ + name = "gokz-profile", + file = "gokz-profile.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_profile_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_PF_GetRank"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/quiet.inc b/sourcemod/scripting/include/gokz/quiet.inc new file mode 100644 index 0000000..a328b7e --- /dev/null +++ b/sourcemod/scripting/include/gokz/quiet.inc @@ -0,0 +1,205 @@ +/* + gokz-quiet Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_quiet_included_ +#endinput +#endif +#define _gokz_quiet_included_ + + + +// =====[ ENUMS ]===== + +enum QTOption: +{ + QTOPTION_INVALID = -1, + QTOption_ShowPlayers, + QTOption_Soundscapes, + QTOption_FallDamageSound, + QTOption_AmbientSounds, + QTOption_CheckpointVolume, + QTOption_TeleportVolume, + QTOption_TimerVolume, + QTOption_ErrorVolume, + QTOption_ServerRecordVolume, + QTOption_WorldRecordVolume, + QTOption_JumpstatsVolume, + QTOPTION_COUNT +}; + +enum +{ + ShowPlayers_Disabled = 0, + ShowPlayers_Enabled, + SHOWPLAYERS_COUNT +}; + +enum +{ + Soundscapes_Disabled = 0, + Soundscapes_Enabled, + SOUNDSCAPES_COUNT +}; + +// =====[ CONSTANTS ]===== + +#define QUIET_OPTION_CATEGORY "Quiet" +#define DEFAULT_VOLUME 10 +#define VOLUME_COUNT 21 // Maximum of 200% + +#define EFFECT_IMPACT 8 +#define EFFECT_KNIFESLASH 2 +#define BLANK_SOUNDSCAPEINDEX 482 // Search for "coopcementplant.missionselect_blank" id with sv_soundscape_printdebuginfo. + +stock char gC_QTOptionNames[QTOPTION_COUNT][] = +{ + "GOKZ QT - Show Players", + "GOKZ QT - Soundscapes", + "GOKZ QT - Fall Damage Sound", + "GOKZ QT - Ambient Sounds", + "GOKZ QT - Checkpoint Volume", + "GOKZ QT - Teleport Volume", + "GOKZ QT - Timer Volume", + "GOKZ QT - Error Volume", + "GOKZ QT - Server Record Volume", + "GOKZ QT - World Record Volume", + "GOKZ QT - Jumpstats Volume" +}; + +stock char gC_QTOptionDescriptions[QTOPTION_COUNT][] = +{ + "Visibility of Other Players - 0 = Disabled, 1 = Enabled", + "Play Soundscapes - 0 = Disabled, 1 = Enabled", + "Play Fall Damage Sound - 0 to 20 = 0% to 200%", + "Play Ambient Sounds - 0 to 20 = 0% to 200%", + "Checkpoint Volume - 0 to 20 = 0% to 200%", + "Teleport Volume - 0 to 20 = 0% to 200%", + "Timer Volume - 0 to 20 = 0% to 200%", + "Error Volume - 0 to 20 = 0% to 200%", + "Server Record Volume - 0 to 20 = 0% to 200%", + "World Record Volume - 0 to 20 = 0% to 200%", + "Jumpstats Volume - 0 to 20 = 0% to 200%" +}; + +stock int gI_QTOptionDefaultValues[QTOPTION_COUNT] = +{ + ShowPlayers_Enabled, + Soundscapes_Enabled, + DEFAULT_VOLUME, // Fall damage volume + DEFAULT_VOLUME, // Ambient volume + DEFAULT_VOLUME, // Checkpoint volume + DEFAULT_VOLUME, // Teleport volume + DEFAULT_VOLUME, // Timer volume + DEFAULT_VOLUME, // Error volume + DEFAULT_VOLUME, // Server Record Volume + DEFAULT_VOLUME, // World Record Volume + DEFAULT_VOLUME // Jumpstats Volume +}; + +stock int gI_QTOptionCounts[QTOPTION_COUNT] = +{ + SHOWPLAYERS_COUNT, + SOUNDSCAPES_COUNT, + VOLUME_COUNT, // Fall damage volume + VOLUME_COUNT, // Ambient volume + VOLUME_COUNT, // Checkpoint volume + VOLUME_COUNT, // Teleport volume + VOLUME_COUNT, // Timer volume + VOLUME_COUNT, // Error volume + VOLUME_COUNT, // Server Record volume + VOLUME_COUNT, // World Record volume + VOLUME_COUNT // Jumpstats volume +}; + +stock char gC_QTOptionPhrases[QTOPTION_COUNT][] = +{ + "Options Menu - Show Players", + "Options Menu - Soundscapes", + "Options Menu - Fall Damage Sounds", + "Options Menu - Ambient Sounds", + "Options Menu - Checkpoint Volume", + "Options Menu - Teleport Volume", + "Options Menu - Timer Volume", + "Options Menu - Error Volume", + "Options Menu - Server Record Volume", + "Options Menu - World Record Volume", + "Options Menu - Jumpstats Volume" +}; + +// =====[ STOCKS ]===== + +/** + * Returns whether an option is a gokz-quiet option. + * + * @param option Option name. + * @param optionEnum Variable to store enumerated gokz-quiet option (if it is one). + * @return Whether option is a gokz-quiet option. + */ +stock bool GOKZ_QT_IsQTOption(const char[] option, QTOption &optionEnum = QTOPTION_INVALID) +{ + for (QTOption i; i < QTOPTION_COUNT; i++) + { + if (StrEqual(option, gC_QTOptionNames[i])) + { + optionEnum = i; + return true; + } + } + return false; +} + +/** + * Gets the current value of a player's gokz-quiet option. + * + * @param client Client index. + * @param option gokz-quiet option. + * @return Current value of option. + */ +stock any GOKZ_QT_GetOption(int client, QTOption option) +{ + return GOKZ_GetOption(client, gC_QTOptionNames[option]); +} + +/** + * Sets a player's gokz-quiet option's value. + * + * @param client Client index. + * @param option gokz-quiet option. + * @param value New option value. + * @return Whether option was successfully set. + */ +stock bool GOKZ_QT_SetOption(int client, QTOption option, any value) +{ + return GOKZ_SetOption(client, gC_QTOptionNames[option], value); +} + +/** + * Increment an integer-type gokz-quiet option's value. + * Loops back to '0' if max value is exceeded. + * + * @param client Client index. + * @param option gokz-quiet option. + * @return Whether option was successfully set. + */ +stock bool GOKZ_QT_CycleOption(int client, QTOption option) +{ + return GOKZ_CycleOption(client, gC_QTOptionNames[option]); +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_quiet = +{ + name = "gokz-quiet", + file = "gokz-quiet.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/racing.inc b/sourcemod/scripting/include/gokz/racing.inc new file mode 100644 index 0000000..d6819ea --- /dev/null +++ b/sourcemod/scripting/include/gokz/racing.inc @@ -0,0 +1,189 @@ +/* + gokz-racing Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_racing_included_ +#endinput +#endif +#define _gokz_racing_included_ + + + +// =====[ ENUMS ]===== + +enum RaceInfo: +{ + RaceInfo_ID, + RaceInfo_Status, + RaceInfo_HostUserID, + RaceInfo_FinishedRacerCount, + RaceInfo_Type, + RaceInfo_Course, + RaceInfo_Mode, + RaceInfo_CheckpointRule, + RaceInfo_CooldownRule, + RACEINFO_COUNT +}; + +enum +{ + RaceType_Normal, + RaceType_Duel, + RACETYPE_COUNT +}; + +enum +{ + RaceStatus_Pending, + RaceStatus_Countdown, + RaceStatus_Started, + RaceStatus_Aborting, + RACESTATUS_COUNT +}; + +enum +{ + RacerStatus_Available, + RacerStatus_Pending, + RacerStatus_Accepted, + RacerStatus_Racing, + RacerStatus_Finished, + RacerStatus_Surrendered, + RACERSTATUS_COUNT +}; + +enum +{ + CheckpointRule_None, + CheckpointRule_Limit, + CheckpointRule_Cooldown, + CheckpointRule_Unlimited, + CHECKPOINTRULE_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define RC_COUNTDOWN_TIME 10.0 +#define RC_REQUEST_TIMEOUT_TIME 15.0 + + + +// =====[ FORWARDS ]===== + +/** + * Called when a player has finished their race. + * + * @param client Client index. + * @param raceID ID of the race. + * @param place Final placement in the race. + */ +forward void GOKZ_RC_OnFinish(int client, int raceID, int place); + +/** + * Called when a player has surrendered their race. + * + * @param client Client index. + * @param raceID ID of the race. + */ +forward void GOKZ_RC_OnSurrender(int client, int raceID); + +/** + * Called when a player receives a race request. + * + * @param client Client index. + * @param raceID ID of the race. + */ +forward void GOKZ_RC_OnRequestReceived(int client, int raceID) + +/** + * Called when a player accepts a race request. + * + * @param client Client index. + * @param raceID ID of the race. + */ +forward void GOKZ_RC_OnRequestAccepted(int client, int raceID) + +/** + * Called when a player declines a race request. + * + * @param client Client index. + * @param raceID ID of the race. + * @param timeout Whether the client was too late to respond. + */ +forward void GOKZ_RC_OnRequestDeclined(int client, int raceID, bool timeout) + +/** + * Called when a race has been registered. + * The initial status of a race is RaceStatus_Pending. + * + * @param raceID ID of the race. + */ +forward void GOKZ_RC_OnRaceRegistered(int raceID); + +/** + * Called when a race's info property has changed. + * + * @param raceID ID of the race. + * @param prop Info property that was changed. + * @param oldValue Previous value. + * @param newValue New value. + */ +forward void GOKZ_RC_OnRaceInfoChanged(int raceID, RaceInfo prop, int oldValue, int newValue); + + + +// =====[ NATIVES ]===== + +/** + * Gets the value of a race info property. + * + * @param raceID Race index. + * @param prop Info property to get. + * @return Value of the info property. + */ +native int GOKZ_RC_GetRaceInfo(int raceID, RaceInfo prop); + +/** + * Gets a player's racer status. + * Refer to the RacerStatus enumeration. + * + * @param client Client index. + * @return Racer status of the client. + */ +native int GOKZ_RC_GetStatus(int client); + +/** + * Gets the ID of the race a player is in. + * + * @param client Client index. + * @return ID of the race the player is in, or -1 if not in a race. + */ +native int GOKZ_RC_GetRaceID(int client); + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_racing = +{ + name = "gokz-racing", + file = "gokz-racing.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_racing_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_RC_GetRaceInfo"); + MarkNativeAsOptional("GOKZ_RC_GetStatus"); + MarkNativeAsOptional("GOKZ_RC_GetRaceID"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/replays.inc b/sourcemod/scripting/include/gokz/replays.inc new file mode 100644 index 0000000..6aabdbd --- /dev/null +++ b/sourcemod/scripting/include/gokz/replays.inc @@ -0,0 +1,275 @@ +/* + gokz-replays Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_replays_included_ +#endinput +#endif +#define _gokz_replays_included_ + +// Bit of a hack, but need it for other plugins that depend on replays to compile +#if defined REQUIRE_PLUGIN +#undef REQUIRE_PLUGIN +#include <gokz/anticheat> +#define REQUIRE_PLUGIN +#else +#include <gokz/anticheat> +#endif + + + +// =====[ ENUMS ]===== +enum +{ + ReplayType_Run = 0, + ReplayType_Cheater, + ReplayType_Jump, + REPLAYTYPE_COUNT +}; + +enum ReplaySaveState +{ + ReplaySave_Local = 0, + ReplaySave_Temp, + ReplaySave_Disabled +}; + +// NOTE: Replays use delta compression for storage. +// This enum is the indices of the ReplayTickData enum struct. +// NOTE: This has to match the ReplayTickData enum struct!!! +enum +{ + RPDELTA_DELTAFLAGS = 0, + RPDELTA_DELTAFLAGS2, + RPDELTA_VEL_X, + RPDELTA_VEL_Y, + RPDELTA_VEL_Z, + RPDELTA_MOUSE_X, + RPDELTA_MOUSE_Y, + RPDELTA_ORIGIN_X, + RPDELTA_ORIGIN_Y, + RPDELTA_ORIGIN_Z, + RPDELTA_ANGLES_X, + RPDELTA_ANGLES_Y, + RPDELTA_ANGLES_Z, + RPDELTA_VELOCITY_X, + RPDELTA_VELOCITY_Y, + RPDELTA_VELOCITY_Z, + RPDELTA_FLAGS, + RPDELTA_PACKETSPERSECOND, + RPDELTA_LAGGEDMOVEMENTVALUE, + RPDELTA_BUTTONSFORCED, + + RP_V2_TICK_DATA_BLOCKSIZE +}; + + + +// =====[ STRUCTS ] ===== + +enum struct GeneralReplayHeader +{ + int magicNumber; + int formatVersion; + int replayType; + char gokzVersion[32]; + char mapName[64]; + int mapFileSize; + int serverIP; + int timestamp; + char playerAlias[MAX_NAME_LENGTH]; + int playerSteamID; + int mode; + int style; + float playerSensitivity; + float playerMYaw; + float tickrate; + int tickCount; + int equippedWeapon; + int equippedKnife; +} + +enum struct JumpReplayHeader +{ + int jumpType; + float distance; + int blockDistance; + int strafeCount; + float sync; + float pre; + float max; + int airtime; +} + +enum struct CheaterReplayHeader +{ + ACReason ACReason; +} + +enum struct RunReplayHeader +{ + float time; + int course; + int teleportsUsed; +} + +// NOTE: Make sure to change the RPDELTA_* enum, TickDataToArray() and TickDataFromArray() when adding/removing stuff from this!!! +enum struct ReplayTickData +{ + int deltaFlags; + int deltaFlags2; + float vel[3]; + int mouse[2]; + float origin[3]; + float angles[3]; + float velocity[3]; + int flags; + float packetsPerSecond; + float laggedMovementValue; + int buttonsForced; +} + + + +// =====[ CONSTANTS ]===== + +#define RP_DIRECTORY "data/gokz-replays" // In Path_SM +#define RP_DIRECTORY_RUNS "data/gokz-replays/_runs" // In Path_SM +#define RP_DIRECTORY_RUNS_TEMP "data/gokz-replays/_tempRuns" // In Path_SM +#define RP_DIRECTORY_CHEATERS "data/gokz-replays/_cheaters" // In Path_SM +#define RP_DIRECTORY_JUMPS "data/gokz-replays/_jumps" // In Path_SM +#define RP_DIRECTORY_BLOCKJUMPS "blocks" +#define RP_FILE_EXTENSION "replay" +#define RP_MAGIC_NUMBER 0x676F6B7A +#define RP_FORMAT_VERSION 0x02 +#define RP_NAV_FILE "maps/gokz-replays.nav" +#define RP_V1_TICK_DATA_BLOCKSIZE 7 +#define RP_CACHE_BLOCKSIZE 4 +#define RP_MAX_BOTS 4 +#define RP_PLAYBACK_BREATHER_TIME 2.0 +#define RP_MIN_CHEATER_REPLAY_LENGTH 30 // 30 seconds +#define RP_MAX_CHEATER_REPLAY_LENGTH 120 // 2 minutes +#define RP_MAX_BHOP_GROUND_TICKS 5 +#define RP_SKIP_TIME 10 // 10 seconds +#define RP_MAX_DURATION 6451200 // 14 hours on 128 tick +#define RP_JUMP_STEP_SOUND_THRESHOLD 140.0 +#define RP_PLAYER_ACCELSPEED 450.0 + +#define RP_MOVETYPE_MASK (0xF) +#define RP_IN_ATTACK (1 << 4) +#define RP_IN_ATTACK2 (1 << 5) +#define RP_IN_JUMP (1 << 6) +#define RP_IN_DUCK (1 << 7) +#define RP_IN_FORWARD (1 << 8) +#define RP_IN_BACK (1 << 9) +#define RP_IN_LEFT (1 << 10) +#define RP_IN_RIGHT (1 << 11) +#define RP_IN_MOVELEFT (1 << 12) +#define RP_IN_MOVERIGHT (1 << 13) +#define RP_IN_RELOAD (1 << 14) +#define RP_IN_SPEED (1 << 15) +#define RP_IN_USE (1 << 16) +#define RP_IN_BULLRUSH (1 << 17) +#define RP_FL_ONGROUND (1 << 18) +#define RP_FL_DUCKING (1 << 19) +#define RP_FL_SWIM (1 << 20) +#define RP_UNDER_WATER (1 << 21) +#define RP_TELEPORT_TICK (1 << 22) +#define RP_TAKEOFF_TICK (1 << 23) +#define RP_HIT_PERF (1 << 24) +#define RP_SECONDARY_EQUIPPED (1 << 25) + + + +// =====[ FORWARDS ]===== + +/** + * Called when a replay of a player is written to disk. + * This includes replays of cheaters which are saved if + * the player is marked as a cheater by gokz-localdb. + * + * @param client The client ID of the player who completed the run. + * @param replayType The type of the replay (Run/Jump/Cheater). + * @param map The name of the map the run was completed on. + * @param course The specific course on the map the run was completed on. + * @param timeType The type of time (Pro/Nub). + * @param time The time the run was completed in. + * @param filePath Replay file path. + * @param tempReplay Whether the replay file should only be temporaily stored. + * @return Plugin_Handled to take over the temporary replay deletion, Plugin_Continue to allow temporary replay deletion by the replay plugin. + */ +forward Action GOKZ_RP_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay); + +/** + * Called when a currently being recorded replay is discarded from + * memory and recording has been stopped (without writing it to disk). + * + * @param client Client index. + */ +forward void GOKZ_RP_OnReplayDiscarded(int client); + +/** + * Called when a player has ended their timer, and gokz-replays has + * processed the time and has possibly written a replay to disk. + * + * @param client Client index. + * @param filePath Replay file path, or "" if no replay saved. + * @param course Course number. + * @param time Player's end time. + * @param teleportsUsed Number of teleports used by player. + */ +forward void GOKZ_RP_OnTimerEnd_Post(int client, const char[] filePath, int course, float time, int teleportsUsed); + + + +// =====[ NATIVES ]==== + +/** + * Called by the HUD to get the state of the current replay. + * + * @param client Client index. + * @param info Struct to pass the values into. + * @return If successful + */ +native int GOKZ_RP_GetPlaybackInfo(int client, any[] info); + +/** + * Called by the LocalDB to initiate a replay of a jump + * + * @param client Client index. + * @param path Path to the replay file. + * @return The client ID of the bot performing the replay. + */ +native int GOKZ_RP_LoadJumpReplay(int client, char[] path); + +/** + * Called by the HUD to show the replay control menu. + * + * @param client Client index. + */ +native bool GOKZ_RP_UpdateReplayControlMenu(int client); + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_replays = +{ + name = "gokz-replays", + file = "gokz-replays.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_gokz_replays_SetNTVOptional() +{ + MarkNativeAsOptional("GOKZ_RP_GetPlaybackInfo"); + MarkNativeAsOptional("GOKZ_RP_LoadJumpReplay"); + MarkNativeAsOptional("GOKZ_RP_UpdateReplayControlMenu"); +} +#endif diff --git a/sourcemod/scripting/include/gokz/slayonend.inc b/sourcemod/scripting/include/gokz/slayonend.inc new file mode 100644 index 0000000..2ed01e5 --- /dev/null +++ b/sourcemod/scripting/include/gokz/slayonend.inc @@ -0,0 +1,43 @@ +/* + gokz-slayonend Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_slayonend_included_ +#endinput +#endif +#define _gokz_slayonend_included_ + + + +// =====[ ENUMS ]===== + +enum +{ + SlayOnEnd_Disabled = 0, + SlayOnEnd_Enabled, + SLAYONEND_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define SLAYONEND_OPTION_NAME "GOKZ - Slay On End" +#define SLAYONEND_OPTION_DESCRIPTION "Automatic Slaying Upon Ending Timer - 0 = Disabled, 1 = Enabled" + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_slayonend = +{ + name = "gokz-slayonend", + file = "gokz-slayonend.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/tips.inc b/sourcemod/scripting/include/gokz/tips.inc new file mode 100644 index 0000000..b5e2d3e --- /dev/null +++ b/sourcemod/scripting/include/gokz/tips.inc @@ -0,0 +1,59 @@ +/* + gokz-tips Plugin Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz +*/ + +#if defined _gokz_tips_included_ +#endinput +#endif +#define _gokz_tips_included_ + + + +// =====[ ENUMS ]===== + +enum +{ + Tips_Disabled = 0, + Tips_Enabled, + TIPS_COUNT +}; + + + +// =====[ CONSTANTS ]===== + +#define TIPS_PLUGINS_COUNT 9 +#define TIPS_CORE "gokz-tips-core.phrases.txt" +#define TIPS_TIPS "gokz-tips-tips.phrases.txt" +#define TIPS_OPTION_NAME "GOKZ - Tips" +#define TIPS_OPTION_DESCRIPTION "Random Tips Periodically in Chat - 0 = Disabled, 1 = Enabled" + +stock char gC_PluginsWithTips[TIPS_PLUGINS_COUNT][] = +{ + "goto", + "hud", + "jumpstats", + "localranks", + "measure", + "pistol", + "quiet", + "replays", + "spec" +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_tips = +{ + name = "gokz-tips", + file = "gokz-tips.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/tpanglefix.inc b/sourcemod/scripting/include/gokz/tpanglefix.inc new file mode 100644 index 0000000..fc8faa2 --- /dev/null +++ b/sourcemod/scripting/include/gokz/tpanglefix.inc @@ -0,0 +1,40 @@ +/* + gokz-tpanglefix Plugin Include + + Website: https://github.com/KZGlobalTeam/gokz +*/ + +#if defined _gokz_tpanglefix_included_ +#endinput +#endif +#define _gokz_tpanglefix_included_ + + +// =====[ ENUMS ]===== + +enum +{ + TPAngleFix_Disabled = 0, + TPAngleFix_Enabled, + TPANGLEFIX_COUNT +}; + + +// =====[ CONSTANTS ]===== + +#define TPANGLEFIX_OPTION_NAME "GOKZ - TPAngleFix" +#define TPANGLEFIX_OPTION_DESCRIPTION "TPAngleFix - 0 = Disabled, 1 = Enabled" + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_gokz_tpanglefix = +{ + name = "gokz-tpanglefix", + file = "gokz-tpanglefix.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +};
\ No newline at end of file diff --git a/sourcemod/scripting/include/gokz/version.inc b/sourcemod/scripting/include/gokz/version.inc new file mode 100644 index 0000000..34cdb19 --- /dev/null +++ b/sourcemod/scripting/include/gokz/version.inc @@ -0,0 +1,12 @@ +/* + GOKZ Version Include + + Website: https://bitbucket.org/kztimerglobalteam/gokz + + You should not need to edit this file. + This file is overwritten in the build pipeline for versioning. +*/ + + + +#define GOKZ_VERSION ""
\ No newline at end of file diff --git a/sourcemod/scripting/include/json.inc b/sourcemod/scripting/include/json.inc new file mode 100644 index 0000000..ebc46e4 --- /dev/null +++ b/sourcemod/scripting/include/json.inc @@ -0,0 +1,473 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_included + #endinput +#endif +#define _json_included + +#include <string> +#include <json/definitions> +#include <json/helpers/decode> +#include <json/helpers/encode> +#include <json/helpers/string> +#include <json/object> + +/** + * Encodes a JSON instance into its string representation. + * + * @param obj Object to encode. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param pretty_print Should the output be pretty printed (newlines and spaces)? [default: false] + * @param depth The current depth of the encoder. [default: 0] + */ +stock void json_encode( + JSON_Object obj, + char[] output, + int max_size, + bool pretty_print = false, + int depth = 0 +) +{ + bool is_array = obj.IsArray; + bool is_empty = true; + int builder_size; + + // used in key iterator + int str_length = 1; + int int_value; + int cell_length = 0; + + StringMapSnapshot snap = null; + int json_size = 0; + if (is_array) { + json_size = obj.CurrentIndex; + + strcopy(output, max_size, "["); + } else { + snap = obj.Snapshot(); + json_size = snap.Length; + + strcopy(output, max_size, "{"); + } + + int key_length = 0; + for (int i = 0; i < json_size; ++i) { + key_length = (is_array) ? JSON_INDEX_BUFFER_SIZE : snap.KeyBufferSize(i); + char[] key = new char[key_length]; + + if (is_array) { + obj.GetIndexString(key, key_length, i); + } else { + snap.GetKey(i, key, key_length); + } + + // skip meta-keys + if (json_is_meta_key(key)) { + continue; + } + + // skip keys that are marked as hidden + if (obj.GetKeyHidden(key)) { + continue; + } + + JSON_CELL_TYPE type = obj.GetKeyType(key); + // skip keys of unknown type + if (type == Type_Invalid) { + continue; + } + + // if we are dealing with a string, prepare the str_value variable for fetching + if (type == Type_String) { + str_length = obj.GetKeyLength(key); + } + char[] str_value = new char[str_length + 1]; + + // determine the length of the char[] needed to represent our cell data + cell_length = 0; + switch (type) { + case Type_String: { + // get the string value early, as its cell_length is determined by its contents + obj.GetString(key, str_value, str_length + 1); + cell_length = json_cell_string_size(str_length); + } + case Type_Int: { + // get the int value early, as its cell_length is determined by its contents + int_value = obj.GetInt(key); + cell_length = json_cell_int_size(int_value); + } + case Type_Float: { + cell_length = json_cell_float_size(); + } + case Type_Bool: { + cell_length = json_cell_bool_size(); + } + case Type_Null: { + cell_length = json_cell_null_size(); + } + case Type_Object: { + cell_length = max_size; + } + } + + // fit the contents into the cell + char[] cell = new char[cell_length]; + switch (type) { + case Type_String: { + json_cell_string(str_value, cell, cell_length); + } + case Type_Int: { + json_cell_int(int_value, cell, cell_length); + } + case Type_Float: { + float value = obj.GetFloat(key); + json_cell_float(value, cell, cell_length); + } + case Type_Bool: { + bool value = obj.GetBool(key); + json_cell_bool(value, cell, cell_length); + } + case Type_Null: { + json_cell_null(cell, cell_length); + } + case Type_Object: { + JSON_Object child = obj.GetObject(key); + json_encode(child, cell, cell_length, pretty_print, depth + 1); + } + } + + // make the builder fit our key:value + // use previously determined cell length and + 1 for , + builder_size = cell_length + 1; + if (! is_array) { + // get the length of the key and + 1 for : + builder_size += json_cell_string_size(strlen(key)) + 1; + + if (pretty_print) { + builder_size += strlen(JSON_PP_AFTER_COLON); + } + } + + char[] builder = new char[builder_size]; + strcopy(builder, builder_size, ""); + + // add the key if we're working with an object + if (! is_array) { + json_cell_string(key, builder, builder_size); + StrCat(builder, builder_size, ":"); + + if (pretty_print) { + StrCat(builder, builder_size, JSON_PP_AFTER_COLON); + } + } + + // add the value and a trailing comma + StrCat(builder, builder_size, cell); + StrCat(builder, builder_size, ","); + + // prepare pretty printing then send builder to output afterwards + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth + 1; ++j) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + + StrCat(output, max_size, builder); + + is_empty = false; + } + + if (snap != null) { + delete snap; + } + + if (! is_empty) { + // remove the final comma + output[strlen(output) - 1] = '\0'; + + if (pretty_print) { + StrCat(output, max_size, JSON_PP_NEWLINE); + + for (int j = 0; j < depth; ++j) { + StrCat(output, max_size, JSON_PP_INDENT); + } + } + } + + // append closing bracket + StrCat(output, max_size, (is_array) ? "]" : "}"); +} + +/** + * Decodes a JSON string into a JSON instance. + * + * @param buffer Buffer to decode. + * @param result Object to store output in. Setting this allows loading over + * an existing JSON instance, 'refreshing' it as opposed to + * creating a new one. [default: null] + * @param pos Current position of the decoder as a bytes offset into the buffer. + * @param depth Current depth of the decoder as child elements in the object. + * @returns JSON instance or null if decoding failed (buffer didn't contain valid JSON). + */ +stock JSON_Object json_decode( + const char[] buffer, + JSON_Object result = null, + int &pos = 0, + int depth = 0 +) +{ + int length = strlen(buffer); + bool is_array = false; + + // skip preceding whitespace + if (! json_skip_whitespace(buffer, length, pos)) { + LogError("json_decode: buffer ended early at position %d", pos); + + return null; + } + + if (json_is_object(buffer[pos])) { + is_array = false; + } else if (json_is_array(buffer[pos])) { + is_array = true; + } else { + LogError("json_decode: character not identified as object or array at position %d", pos); + + return null; + } + + if (result == null) { + result = new JSON_Object(is_array); + } + + bool empty_checked = false; + char[] key = new char[length]; + char[] cell = new char[length]; + + // while we haven't reached the end of our structure + while (! is_array && ! json_is_object_end(buffer[pos]) + || is_array && ! json_is_array_end(buffer[pos])) { + // pos is either an opening structure or comma, so increment past it + ++pos; + + // skip any whitespace preceding the element + if (! json_skip_whitespace(buffer, length, pos)) { + LogError("json_decode: buffer ended early at position %d", pos); + + return null; + } + + // if we are at the end of an object or array + // and haven't checked for empty yet, we can stop here (empty structure) + if ((! is_array && json_is_object_end(buffer[pos]) + || is_array && json_is_array_end(buffer[pos])) + && ! empty_checked) { + break; + } + + empty_checked = true; + + // if dealing with an object, look for the key + if (! is_array) { + if (! json_is_string(buffer[pos])) { + LogError("json_decode: expected key string at position %d", pos); + + return null; + } + + // extract the key from the buffer + json_extract_string(buffer, length, pos, key, length, is_array); + + // skip any whitespace following the key + if (! json_skip_whitespace(buffer, length, pos)) { + LogError("json_decode: buffer ended early at position %d", pos); + + return null; + } + + // ensure that we find a colon + if (buffer[pos++] != ':') { + LogError("json_decode: expected colon after key at position %d", pos); + + return null; + } + + // skip any whitespace following the colon + if (! json_skip_whitespace(buffer, length, pos)) { + LogError("json_decode: buffer ended early at position %d", pos); + + return null; + } + } + + if (json_is_object(buffer[pos]) || json_is_array(buffer[pos])) { + // if we are dealing with an object or array + // fetch the existing object if one exists at the key + JSON_Object current = (! is_array) ? result.GetObject(key) : null; + + // decode recursively + JSON_Object value = json_decode(buffer, current, pos, depth + 1); + + // decoding failed, error will be logged in json_decode + if (value == null) { + return null; + } + + if (is_array) { + result.PushObject(value); + } else { + result.SetObject(key, value); + } + } else if (json_is_string(buffer[pos])) { + // if we are dealing with a string, attempt to extract it + if (! json_extract_string(buffer, length, pos, cell, length, is_array)) { + LogError("json_decode: couldn't extract string at position %d", pos); + + return null; + } + + if (is_array) { + result.PushString(cell); + } else { + result.SetString(key, cell); + } + } else { + if (! json_extract_until_end(buffer, length, pos, cell, length, is_array)) { + LogError("json_decode: couldn't extract until end at position %d", pos); + + return null; + } + + if (strlen(cell) == 0) { + LogError("json_decode: empty cell encountered at position %d", pos); + + return null; + } + + if (json_is_int(cell)) { + int value = json_extract_int(cell); + if (is_array) { + result.PushInt(value); + } else { + result.SetInt(key, value); + } + } else if (json_is_float(cell)) { + float value = json_extract_float(cell); + if (is_array) { + result.PushFloat(value); + } else { + result.SetFloat(key, value); + } + } else if (json_is_bool(cell)) { + bool value = json_extract_bool(cell); + if (is_array) { + result.PushBool(value); + } else { + result.SetBool(key, value); + } + } else if (json_is_null(cell)) { + if (is_array) { + result.PushHandle(null); + } else { + result.SetHandle(key, null); + } + } else { + LogError("json_decode: unknown type encountered at position %d: %s", pos, cell); + + return null; + } + } + + if (! json_skip_whitespace(buffer, length, pos)) { + LogError("json_decode: buffer ended early at position %d", pos); + + return null; + } + } + + // skip remaining whitespace and ensure we're at the end of the buffer + ++pos; + if (json_skip_whitespace(buffer, length, pos) && depth == 0) { + LogError("json_decode: unexpected data after end of structure at position %d", pos); + + return null; + } + + return result; +} + +/** + * Recursively cleans up a JSON instance and any JSON instances stored within. + * + * @param obj JSON instance to clean up. + */ +stock void json_cleanup(JSON_Object obj) +{ + bool is_array = obj.IsArray; + + int key_length = 0; + StringMapSnapshot snap = obj.Snapshot(); + for (int i = 0; i < snap.Length; ++i) { + key_length = snap.KeyBufferSize(i); + char[] key = new char[key_length]; + + // ignore meta keys + snap.GetKey(i, key, key_length); + if (json_is_meta_key(key)) { + continue; + } + + // only clean up objects + JSON_CELL_TYPE type = obj.GetKeyType(key); + if (type != Type_Object) { + continue; + } + + JSON_Object nested_obj = obj.GetObject(key); + if (nested_obj != null) { + nested_obj.Cleanup(); + delete nested_obj; + } + } + + obj.Clear(); + delete snap; + + if (is_array) { + obj.SetValue(JSON_ARRAY_INDEX_KEY, 0); + } +} diff --git a/sourcemod/scripting/include/json/decode_helpers.inc b/sourcemod/scripting/include/json/decode_helpers.inc new file mode 100644 index 0000000..0032cc3 --- /dev/null +++ b/sourcemod/scripting/include/json/decode_helpers.inc @@ -0,0 +1,312 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2018 James D. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_decode_helpers_included + #endinput +#endif +#define _json_decode_helpers_included + +#include <string> + +/** + * @section Analysing format of incoming JSON cells. + */ + +/** + * Checks whether the character at the given + * position in the buffer is whitespace. + * + * @param buffer String buffer of data. + * @param pos Position to check in buffer. + * @return True if buffer[pos] is whitespace, false otherwise. + */ +stock bool json_is_whitespace(const char[] buffer, int &pos) { + return buffer[pos] == ' ' || buffer[pos] == '\t' || + buffer[pos] == '\r' || buffer[pos] == '\n'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of a string. + * + * @param buffer String buffer of data. + * @return True if buffer[0] is the start of a string, false otherwise. + */ +stock bool json_is_string(const char[] buffer) { + return buffer[0] == '"'; +} + +/** + * Checks whether the buffer provided contains an int. + * + * @param buffer String buffer of data. + * @return True if buffer contains an int, false otherwise. + */ +stock bool json_is_int(const char[] buffer) { + int length = strlen(buffer); + if (buffer[0] != '+' && buffer[0] != '-' && !IsCharNumeric(buffer[0])) { + return false; + } + + for (int i = 0; i < length; ++i) { + if (!IsCharNumeric(buffer[i])) return false; + } + + return true; +} + +/** + * Checks whether the buffer provided contains a float. + * + * @param buffer String buffer of data. + * @return True if buffer contains a float, false otherwise. + */ +stock bool json_is_float(const char[] buffer) { + bool has_decimal = false; + int length = strlen(buffer); + if (buffer[0] != '+' && buffer[0] != '-' && buffer[0] != '.' && !IsCharNumeric(buffer[0])) { + return false; + } + + for (int i = 0; i < length; ++i) { + if (buffer[i] == '.') { + if (has_decimal) { + return false; + } + + has_decimal = true; + } else if (!IsCharNumeric(buffer[i])) { + return false; + } + } + + return true; +} + +/** + * Checks whether the buffer provided contains a bool. + * + * @param buffer String buffer of data. + * @return True if buffer contains a bool, false otherwise. + */ +stock bool json_is_bool(const char[] buffer) { + return StrEqual(buffer, "true") || + StrEqual(buffer, "false"); +} + +/** + * Checks whether the buffer provided contains null. + * + * @param buffer String buffer of data. + * @return True if buffer contains null, false otherwise. + */ +stock bool json_is_null(const char[] buffer) { + return StrEqual(buffer, "null"); +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of an object. + * + * @param buffer String buffer of data. + * @return True if buffer[0] is the start of an object, false otherwise. + */ +stock bool json_is_object(const char[] buffer) { + return buffer[0] == '{'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the end of an object. + * + * @param buffer String buffer of data. + * @return True if buffer[0] is the end of an object, false otherwise. + */ +stock bool json_is_object_end(const char[] buffer) { + return buffer[0] == '}'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of an array. + * + * @param buffer String buffer of data. + * @return True if buffer[0] is the start of an array, false otherwise. + */ +stock bool json_is_array(const char[] buffer) { + return buffer[0] == '['; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of an array. + * + * @param buffer String buffer of data. + * @return True if buffer[0] is the start of an array, false otherwise. + */ +stock bool json_is_array_end(const char[] buffer) { + return buffer[0] == ']'; +} + +/** + * Checks whether the character at the given position in the buffer + * is considered a valid 'end point' for some data, such as a + * colon (indicating a key), a comma (indicating a new element), + * or the end of an object or array. + * + * @param buffer String buffer of data. + * @param pos Position to check in buffer. + * @return True if buffer[pos] is a valid data end point, false otherwise. + */ +stock bool json_is_at_end(const char[] buffer, int &pos, bool is_array) { + return buffer[pos] == ',' || + (!is_array && buffer[pos] == ':') || + json_is_object_end(buffer[pos]) || + json_is_array_end(buffer[pos]); +} + +/** + * Moves the position until it reaches a non-whitespace + * character or the end of the buffer's maximum size. + * + * @param buffer String buffer of data. + * @param maxlen Maximum size of string buffer. + * @param pos Position to increment. + * @return True if pos is not at the end of the buffer, false otherwise. + */ +stock bool json_skip_whitespace(const char[] buffer, int maxlen, int &pos) { + while (json_is_whitespace(buffer, pos) && pos < maxlen) { + ++pos; + } + + return pos < maxlen; +} + +/** + * Extracts a JSON cell from the buffer until + * a valid end point is reached. + * + * @param buffer String buffer of data. + * @param maxlen Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_maxlen Maximum size of output string buffer. + * @param is_array Whether the decoder is currently processing an array. + * @return True if pos is not at the end of the buffer, false otherwise. + */ +stock bool json_extract_until_end(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) { + // extracts a string from current pos until a valid 'end point' + strcopy(output, output_maxlen, ""); + + int start = pos; + while (!json_is_whitespace(buffer, pos) && !json_is_at_end(buffer, pos, is_array) && pos < maxlen) { + ++pos; + } + int end = pos - 1; + + // skip trailing whitespace + json_skip_whitespace(buffer, maxlen, pos); + + if (!json_is_at_end(buffer, pos, is_array)) return false; + strcopy(output, end - start + 2, buffer[start]); + + return pos < maxlen; +} + + +/** + * Extracts a JSON string from the buffer until + * a valid end point is reached. + * + * @param buffer String buffer of data. + * @param maxlen Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_maxlen Maximum size of output string buffer. + * @param is_array Whether the decoder is currently processing an array. + * @return True if pos is not at the end of the buffer, false otherwise. + */ +stock bool json_extract_string(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) { + // extracts a string which needs to be quote-escaped + strcopy(output, output_maxlen, ""); + + ++pos; + int start = pos; + while (!(buffer[pos] == '"' && buffer[pos - 1] != '\\') && pos < maxlen) { + ++pos; + } + int end = pos - 1; + + // jump 1 ahead since we ended on " instead of an ending char + ++pos; + + // skip trailing whitespace + json_skip_whitespace(buffer, maxlen, pos); + + if (!json_is_at_end(buffer, pos, is_array)) return false; + // copy only from start with length end - start + 2 (+2 for NULL terminator and something else) + strcopy(output, end - start + 2, buffer[start]); + json_unescape_string(output, maxlen); + + return pos < maxlen; +} + +/** + * Extracts an int from the buffer. + * + * @param buffer String buffer of data. + * @return Int value of the buffer. + */ +stock int json_extract_int(const char[] buffer) { + return StringToInt(buffer); +} + +/** + * Extracts a float from the buffer. + * + * @param buffer String buffer of data. + * @return Float value of the buffer. + */ +stock float json_extract_float(const char[] buffer) { + return StringToFloat(buffer); +} + +/** + * Extracts a bool from the buffer. + * + * @param buffer String buffer of data. + * @return Bool value of the buffer. + */ +stock bool json_extract_bool(const char[] buffer) { + return StrEqual(buffer, "true"); +} diff --git a/sourcemod/scripting/include/json/definitions.inc b/sourcemod/scripting/include/json/definitions.inc new file mode 100644 index 0000000..63063d3 --- /dev/null +++ b/sourcemod/scripting/include/json/definitions.inc @@ -0,0 +1,103 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_definitions_included + #endinput +#endif +#define _json_definitions_included + +#include <string> +#include <json/helpers/string> + +/** + * @section Pretty Print Constants + * + * Used to determine how pretty printed JSON should be formatted when encoded. + * You can modify these if you prefer your JSON formatted differently. + */ + +#define JSON_PP_AFTER_COLON " " +#define JSON_PP_INDENT " " +#define JSON_PP_NEWLINE "\n" + +/** + * @section Buffer Size Constants + * + * You may need to change these if you are working with very large arrays or floating point numbers. + */ + +#define JSON_FLOAT_BUFFER_SIZE 32 +#define JSON_INDEX_BUFFER_SIZE 16 + +/** + * @section Meta-key Constants + * + * Used to store metadata for each key in an object. + * You shouldn't need to change these unless working with keys that may clash with them. + */ + +#define JSON_ARRAY_INDEX_KEY "__array_index" +#define JSON_META_TYPE_KEY ":type" +#define JSON_META_LENGTH_KEY ":length" +#define JSON_META_HIDDEN_KEY ":hidden" + +/** + * @section General + */ + +/** + * Types of cells within a JSON object + */ +enum JSON_CELL_TYPE { + Type_Invalid = -1, + Type_String = 0, + Type_Int, + Type_Float, + Type_Bool, + Type_Null, + Type_Object +}; + +/** + * Checks whether the key provided is a meta-key that should only be used internally. + * + * @param key Key to check. + * @returns True when it is a meta-key, false otherwise. + */ +stock bool json_is_meta_key(char[] key) +{ + return json_string_endswith(key, JSON_META_TYPE_KEY) + || json_string_endswith(key, JSON_META_LENGTH_KEY) + || json_string_endswith(key, JSON_META_HIDDEN_KEY) + || StrEqual(key, JSON_ARRAY_INDEX_KEY); +} diff --git a/sourcemod/scripting/include/json/encode_helpers.inc b/sourcemod/scripting/include/json/encode_helpers.inc new file mode 100644 index 0000000..37cb83d --- /dev/null +++ b/sourcemod/scripting/include/json/encode_helpers.inc @@ -0,0 +1,164 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2018 James D. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_encode_helpers_included + #endinput +#endif +#define _json_encode_helpers_included + +#include <string> + +/** + * @section Calculating buffer sizes for JSON cells. + */ + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a string. + * + * @param maxlen The string's current length or buffer size. + * @return Maximum buffer length. + */ +stock int json_cell_string_size(int maxlen) { + return (maxlen * 2) + 3; // * 2 for potential escaping, + 2 for surrounding quotes + NULL +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of an int. + * + * @param input The int. + * @return Maximum buffer length. + */ +stock int json_cell_int_size(int input) { + if (input == 0) { + return 2; // "0" + NULL + } + + return ((input < 0) ? 1 : 0) + RoundToFloor(Logarithm(FloatAbs(float(input)), 10.0)) + 2; // neg sign + number of digits + NULL +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a float. + * + * @return Maximum buffer length. + */ +stock int json_cell_float_size() { + return JSON_FLOAT_BUFFER_SIZE; // fixed-length +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a bool. + * + * @return Maximum buffer length. + */ +stock int json_cell_bool_size() { + return 6; // "true|false" + NULL +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of null. + * + * @return Maximum buffer length. + */ +stock int json_cell_null_size() { + return 5; // "null" + NULL +} + +/** + * @section Generating JSON cells. + */ + +/** + * Generates the JSON cell representation of a string. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param maxlen Maximum size of string buffer. + */ +stock void json_cell_string(const char[] input, char[] output, int maxlen) { + strcopy(output, maxlen, "_"); // add dummy char at start so first quotation isn't escaped + StrCat(output, maxlen, input); // add input string to output + // escape everything according to JSON spec + json_escape_string(output, maxlen); + + // surround string with quotations + output[0] = '"'; + StrCat(output, maxlen, "\""); +} + +/** + * Generates the JSON cell representation of an int. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param maxlen Maximum size of string buffer. + */ +stock void json_cell_int(int input, char[] output, int maxlen) { + IntToString(input, output, maxlen); +} + +/** + * Generates the JSON cell representation of a float. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param maxlen Maximum size of string buffer. + */ +stock void json_cell_float(float input, char[] output, int maxlen) { + FloatToString(input, output, maxlen); +} + +/** + * Generates the JSON cell representation of a bool. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param maxlen Maximum size of string buffer. + */ +stock void json_cell_bool(bool input, char[] output, int maxlen) { + strcopy(output, maxlen, (input) ? "true" : "false"); +} + +/** + * Generates the JSON cell representation of null. + * + * @param output String buffer to store output. + * @param maxlen Maximum size of string buffer. + */ +stock void json_cell_null(char[] output, int maxlen) { + strcopy(output, maxlen, "null"); +} diff --git a/sourcemod/scripting/include/json/helpers/decode.inc b/sourcemod/scripting/include/json/helpers/decode.inc new file mode 100644 index 0000000..f420222 --- /dev/null +++ b/sourcemod/scripting/include/json/helpers/decode.inc @@ -0,0 +1,502 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_helpers_decode_included + #endinput +#endif +#define _json_helpers_decode_included + +#include <string> + +/** + * @section Determine Buffer Contents + */ + +/** + * Checks whether the character at the beginning + * of the buffer is whitespace. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer + * is whitespace, false otherwise. + */ +stock bool json_is_whitespace(const char[] buffer) +{ + return buffer[0] == ' ' + || buffer[0] == '\t' + || buffer[0] == '\r' + || buffer[0] == '\n'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of a string. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer + * is the start of a string, false otherwise. + */ +stock bool json_is_string(const char[] buffer) +{ + return buffer[0] == '"'; +} + +/** + * Checks whether the buffer provided contains an int. + * + * @param buffer String buffer of data. + * @returns True if buffer contains an int, false otherwise. + */ +stock bool json_is_int(const char[] buffer) +{ + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + + int length = strlen(buffer); + for (int i = 0; i < length; ++i) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + if (IsCharNumeric(buffer[i])) { + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else { + has_digit_gt_zero = true; + } + } else { + return false; + } + } + + // buffer must start with zero and have no other numerics before decimal + // OR not start with zero and have other numerics + return (starts_with_zero && ! has_digit_gt_zero) + || (! starts_with_zero && has_digit_gt_zero); +} + +/** + * Checks whether the buffer provided contains a float. + * + * @param buffer String buffer of data. + * @returns True if buffer contains a float, false otherwise. + */ +stock bool json_is_float(const char[] buffer) +{ + bool starts_with_zero = false; + bool has_digit_gt_zero = false; + bool after_decimal = false; + bool has_digit_after_decimal = false; + bool after_exponent = false; + bool has_digit_after_exponent = false; + + int length = strlen(buffer); + for (int i = 0; i < length; ++i) { + // allow minus as first character only + if (i == 0 && buffer[i] == '-') { + continue; + } + + // if we haven't encountered a decimal or exponent yet + if (! after_decimal && ! after_exponent) { + if (buffer[i] == '.') { + // if we encounter a decimal before any digits + if (! starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + // if we encounter an exponent before any non-zero digits + if (starts_with_zero && ! has_digit_gt_zero) { + return false; + } + + after_exponent = true; + } else if (IsCharNumeric(buffer[i])) { + if (buffer[i] == '0') { + if (starts_with_zero) { + // detect repeating leading zeros + return false; + } else if (! has_digit_gt_zero) { + starts_with_zero = true; + } + } else { + has_digit_gt_zero = true; + } + } else { + return false; + } + } else if (after_decimal && ! after_exponent) { + // after decimal has been encountered, allow any numerics + if (IsCharNumeric(buffer[i])) { + has_digit_after_decimal = true; + } else if (buffer[i] == 'e' || buffer[i] == 'E') { + if (! has_digit_after_decimal) { + // detect exponents directly after decimal + return false; + } + + after_exponent = true; + } else { + return false; + } + } else if (after_exponent) { + if ( + (buffer[i] == '+' || buffer[i] == '-') + && (buffer[i - 1] == 'e' || buffer[i - 1] == 'E') + ) { + // allow + or - directly after exponent + continue; + } else if (IsCharNumeric(buffer[i])) { + has_digit_after_exponent = true; + } else { + return false; + } + } + } + + if (starts_with_zero && has_digit_gt_zero) { + /* if buffer starts with zero, there should + be no other digits before the decimal */ + return false; + } + + // if we have a decimal, there should be digit(s) after it + if (after_decimal) { + if (! has_digit_after_decimal) { + return false; + } + } + + // if we have an exponent, there should be digit(s) after it + if (after_exponent) { + if (! has_digit_after_exponent) { + return false; + } + } + + /* we should have reached an exponent, decimal, or both. + otherwise, this number can be handled by the int parser */ + return after_decimal || after_exponent; +} + +/** + * Checks whether the buffer provided contains a bool. + * + * @param buffer String buffer of data. + * @returns True if buffer contains a bool, false otherwise. + */ +stock bool json_is_bool(const char[] buffer) +{ + return StrEqual(buffer, "true") || StrEqual(buffer, "false"); +} + +/** + * Checks whether the buffer provided contains null. + * + * @param buffer String buffer of data. + * @returns True if buffer contains null, false otherwise. + */ +stock bool json_is_null(const char[] buffer) +{ + return StrEqual(buffer, "null"); +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of an object. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer is + * the start of an object, false otherwise. + */ +stock bool json_is_object(const char[] buffer) +{ + return buffer[0] == '{'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the end of an object. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer is + * the end of an object, false otherwise. + */ +stock bool json_is_object_end(const char[] buffer) +{ + return buffer[0] == '}'; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the start of an array. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer is + * the start of an array, false otherwise. + */ +stock bool json_is_array(const char[] buffer) +{ + return buffer[0] == '['; +} + +/** + * Checks whether the character at the beginning + * of the buffer is the end of an array. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer is + * the end of an array, false otherwise. + */ +stock bool json_is_array_end(const char[] buffer) +{ + return buffer[0] == ']'; +} + +/** + * Checks whether the character at the beginning of the buffer + * is considered a valid 'end point' for some data, such as a + * colon (indicating a key), a comma (indicating a new element), + * or the end of an object or array. + * + * @param buffer String buffer of data. + * @returns True if the first character in the buffer + * is a valid data end point, false otherwise. + */ +stock bool json_is_at_end(const char[] buffer, bool is_array) +{ + return buffer[0] == ',' + || (! is_array && buffer[0] == ':') + || json_is_object_end(buffer[0]) + || json_is_array_end(buffer[0]); +} + +/** + * @section Extract Contents from Buffer + */ + +/** + * Moves the position until it reaches a non-whitespace + * character or the end of the buffer's maximum size. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @returns True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_skip_whitespace(const char[] buffer, int max_size, int &pos) +{ + while (json_is_whitespace(buffer[pos]) && pos < max_size) { + ++pos; + } + + return pos < max_size; +} + +/** + * Extracts a JSON cell from the buffer until + * a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @returns True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_until_end( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // set start to position of first character in cell + int start = pos; + + // while we haven't hit whitespace, an end point or the end of the buffer + while ( + ! json_is_whitespace(buffer[pos]) + && ! json_is_at_end(buffer[pos], is_array) + && pos < max_size + ) { + ++pos; + } + + // set end to the current position + int end = pos; + + // skip any following whitespace + json_skip_whitespace(buffer, max_size, pos); + + // if we aren't at a valid endpoint, extraction has failed + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + strcopy(output, end - start + 1, buffer[start]); + + return pos < max_size; +} + +/** + * Extracts a JSON string from the buffer until + * a valid end point is reached. + * + * @param buffer String buffer of data. + * @param max_size Maximum size of string buffer. + * @param pos Position to increment. + * @param output String buffer to store output. + * @param output_max_size Maximum size of output string buffer. + * @param is_array Whether the decoder is processing an array. + * @returns True if pos has not reached the end + * of the buffer, false otherwise. + */ +stock bool json_extract_string( + const char[] buffer, + int max_size, + int &pos, + char[] output, + int output_max_size, + bool is_array +) { + strcopy(output, output_max_size, ""); + + // increment past opening quote + ++pos; + + // set start to position of first character in string + int start = pos; + + // while we haven't hit the end of the buffer + while (pos < max_size) { + // check for unescaped control characters + if ( + buffer[pos] == '\b' + || buffer[pos] == '\f' + || buffer[pos] == '\n' + || buffer[pos] == '\r' + || buffer[pos] == '\t' + ) { + return false; + } + + if (buffer[pos] == '"') { + // count preceding backslashes to check if quote is escaped + int search_pos = pos; + int preceding_backslashes = 0; + while (search_pos > 0 && buffer[--search_pos] == '\\') { + ++preceding_backslashes; + } + + // if we have an even number of backslashes, the quote is not escaped + if (preceding_backslashes % 2 == 0) { + break; + } + } + + // pass over the character as it is part of the string + ++pos; + } + + // set end to the current position + int end = pos; + + // increment past closing quote + ++pos; + + // skip trailing whitespace + if (! json_skip_whitespace(buffer, max_size, pos)) { + return false; + } + + // if we haven't reached an ending character at the end of the cell, + // there is likely junk data not encapsulated by a string + if (! json_is_at_end(buffer[pos], is_array)) { + return false; + } + + // copy only from start with length end - start + NULL terminator + strcopy(output, end - start + 1, buffer[start]); + json_unescape_string(output, max_size); + + return pos < max_size; +} + +/** + * Extracts an int from the buffer. + * + * @param buffer String buffer of data. + * @returns Int value of the buffer. + */ +stock int json_extract_int(const char[] buffer) +{ + return StringToInt(buffer); +} + +/** + * Extracts a float from the buffer. + * + * @param buffer String buffer of data. + * @returns Float value of the buffer. + */ +stock float json_extract_float(const char[] buffer) +{ + return StringToFloat(buffer); +} + +/** + * Extracts a bool from the buffer. + * + * @param buffer String buffer of data. + * @returns Bool value of the buffer. + */ +stock bool json_extract_bool(const char[] buffer) +{ + return StrEqual(buffer, "true"); +} diff --git a/sourcemod/scripting/include/json/helpers/encode.inc b/sourcemod/scripting/include/json/helpers/encode.inc new file mode 100644 index 0000000..ceae54d --- /dev/null +++ b/sourcemod/scripting/include/json/helpers/encode.inc @@ -0,0 +1,200 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_helpers_encode_included + #endinput +#endif +#define _json_helpers_encode_included + +#include <string> + +/** + * @section Calculate Buffer Size for Value + */ + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a string. + * + * @param length The length of the string. + * @returns Maximum buffer length. + */ +stock int json_cell_string_size(int length) +{ + // double for potential escaping, + 2 for outside quotes + NULL terminator + return (length * 2) + 3; +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of an int. + * + * @param input Value to calculate maximum buffer length for. + * @returns Maximum buffer length. + */ +stock int json_cell_int_size(int input) +{ + if (input == 0) { + // "0" + NULL terminator + return 2; + } + + int result = 0; + if (input < 0) { + // negative sign + result += 1; + } + + // calculate number of digits in number + result += RoundToFloor(Logarithm(FloatAbs(float(input)), 10.0)) + 1; + + // NULL terminator + result += 1; + + return result; +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a float. + * + * @returns Maximum buffer length. + */ +stock int json_cell_float_size() +{ + return JSON_FLOAT_BUFFER_SIZE; +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of a bool. + * + * @returns Maximum buffer length. + */ +stock int json_cell_bool_size() +{ + // "true"|"false" + NULL terminator + return 6; +} + +/** + * Calculates the maximum buffer length required to + * store the JSON cell representation of null. + * + * @returns Maximum buffer length. + */ +stock int json_cell_null_size() +{ + // "null" + NULL terminator + return 5; +} + +/** + * @section Convert Values to JSON Cells + */ + +/** + * Generates the JSON cell representation of a string. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_string(const char[] input, char[] output, int max_size) +{ + // add dummy char that won't be escaped to replace with a quote later + strcopy(output, max_size, "?"); + + // add input string to output + StrCat(output, max_size, input); + + // escape the output string + json_escape_string(output, max_size); + + // surround string with quotations + output[0] = '"'; + StrCat(output, max_size, "\""); +} + +/** + * Generates the JSON cell representation of an int. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_int(int input, char[] output, int max_size) +{ + IntToString(input, output, max_size); +} + +/** + * Generates the JSON cell representation of a float. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_float(float input, char[] output, int max_size) +{ + FloatToString(input, output, max_size); + + // trim trailing 0s from float output up until decimal point + int last_char = strlen(output) - 1; + while (output[last_char] == '0' && output[last_char - 1] != '.') { + output[last_char--] = '\0'; + } +} + +/** + * Generates the JSON cell representation of a bool. + * + * @param input Value to generate output for. + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_bool(bool input, char[] output, int max_size) +{ + strcopy(output, max_size, (input) ? "true" : "false"); +} + +/** + * Generates the JSON cell representation of null. + * + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + */ +stock void json_cell_null(char[] output, int max_size) +{ + strcopy(output, max_size, "null"); +} diff --git a/sourcemod/scripting/include/json/helpers/string.inc b/sourcemod/scripting/include/json/helpers/string.inc new file mode 100644 index 0000000..14fe38a --- /dev/null +++ b/sourcemod/scripting/include/json/helpers/string.inc @@ -0,0 +1,133 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_helpers_string_included + #endinput +#endif +#define _json_helpers_string_included + +/** + * Mapping characters to their escaped form. + */ +char JSON_STRING_NORMAL[][] = { + "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" +}; +char JSON_STRING_ESCAPED[][] = { + "\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", "\\r", "\\t" +}; + +/** + * Escapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_escape_string(char[] buffer, int max_size) +{ + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); ++i) { + ReplaceString( + buffer, + max_size, + JSON_STRING_NORMAL[i], + JSON_STRING_ESCAPED[i] + ); + } +} + +/** + * Unescapes a string in-place in a buffer. + * + * @param buffer String buffer. + * @param max_size Maximum size of string buffer. + */ +stock void json_unescape_string(char[] buffer, int max_size) +{ + for (int i = 0; i < sizeof(JSON_STRING_NORMAL); ++i) { + ReplaceString( + buffer, + max_size, + JSON_STRING_ESCAPED[i], + JSON_STRING_NORMAL[i] + ); + } +} + +/** + * Checks if a string starts with another string. + * + * @param haystack String to check that starts with needle. + * @param max_size Maximum size of string buffer. + * @param needle String to check that haystack starts with. + * @returns True if haystack begins with needle, false otherwise. + */ +stock bool json_string_startswith(const char[] haystack, const char[] needle) +{ + int haystack_length = strlen(haystack); + int needle_length = strlen(needle); + if (needle_length > haystack_length) { + return false; + } + + for (int i = 0; i < needle_length; ++i) { + if (haystack[i] != needle[i]) { + return false; + } + } + + return true; +} + +/** + * Checks if a string ends with another string. + * + * @param haystack String to check that ends with needle. + * @param max_size Maximum size of string buffer. + * @param needle String to check that haystack ends with. + * @returns True if haystack ends with needle, false otherwise. + */ +stock bool json_string_endswith(const char[] haystack, const char[] needle) +{ + int haystack_length = strlen(haystack); + int needle_length = strlen(needle); + if (needle_length > haystack_length) { + return false; + } + + for (int i = 0; i < needle_length; ++i) { + if (haystack[haystack_length - needle_length + i] != needle[i]) { + return false; + } + } + + return true; +} diff --git a/sourcemod/scripting/include/json/object.inc b/sourcemod/scripting/include/json/object.inc new file mode 100644 index 0000000..8d17568 --- /dev/null +++ b/sourcemod/scripting/include/json/object.inc @@ -0,0 +1,1014 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2019 James Dickens. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_object_included + #endinput +#endif +#define _json_object_included + +#include <string> +#include <json/definitions> +#include <json/helpers/encode> +#include <json> + +methodmap JSON_Object < StringMap +{ + /** + * Creates a new JSON_Object. + * + * @param is_array Should the object created be an array? [default: false] + * @returns A new JSON_Object. + */ + public JSON_Object(bool is_array = false) + { + StringMap self = CreateTrie(); + if (is_array) { + self.SetValue(JSON_ARRAY_INDEX_KEY, 0); + } + + return view_as<JSON_Object>(self); + } + + /** + * Checks whether the object has a key. + * + * @param key Key to check existence of. + * @returns True if the key exists, false otherwise. + */ + public bool HasKey(const char[] key) + { + int dummy_int; + char dummy_str[1]; + + return this.GetValue(key, dummy_int) + || this.GetString(key, dummy_str, sizeof(dummy_str)); + } + + /** + * @section Array helpers. + */ + + /** + * Whether the current object is an array. + */ + property bool IsArray { + public get() + { + return this.HasKey(JSON_ARRAY_INDEX_KEY); + } + } + + /** + * The current index of the object if it is an array, or -1 otherwise. + */ + property int CurrentIndex { + public get() + { + if (! this.IsArray) { + return -1; + } + + int result; + return (this.GetValue(JSON_ARRAY_INDEX_KEY, result)) ? result : -1; + } + + public set(int value) + { + this.SetValue(JSON_ARRAY_INDEX_KEY, value); + } + } + + /** + * The number of items in the object if it is an array, + * or the number of keys (including meta-keys) otherwise. + */ + property int Length { + public get() + { + StringMapSnapshot snap = this.Snapshot(); + int length = (this.IsArray) ? this.CurrentIndex : snap.Length; + delete snap; + + return length; + } + } + + /** + * Increments the current index of the object. + * + * @returns True on success, false if the current object is not an array. + */ + public bool IncrementIndex() + { + if (! this.HasKey(JSON_ARRAY_INDEX_KEY)) { + return false; + } + + this.CurrentIndex += 1; + + return true; + } + + /** + * Checks whether the object has an index. + * + * @param index Index to check existence of. + * @returns True if the index exists, false otherwise. + */ + public bool HasIndex(int index) + { + return index >= 0 && index < this.Length; + } + + /** + * Gets the string representation of an array index. + * + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param key Key to get string for. [default: current index] + * @returns True on success, false otherwise. + */ + public int GetIndexString(char[] output, int max_size, int key = -1) + { + key = (key == -1) ? this.CurrentIndex : key; + if (key == -1) { + return false; + } + + return IntToString(key, output, max_size); + } + + /** + * @section Internal Getters + */ + + /** + * Gets the cell type stored at a key. + * + * @param key Key to get value type for. + * @returns Value type for key provided, + * or Type_Invalid if it does not exist. + */ + public JSON_CELL_TYPE GetKeyType(const char[] key) + { + int max_size = strlen(key) + strlen(JSON_META_TYPE_KEY) + 1; + char[] type_key = new char[max_size]; + Format(type_key, max_size, "%s%s", key, JSON_META_TYPE_KEY); + + JSON_CELL_TYPE type; + return (this.GetValue(type_key, type)) ? type : Type_Invalid; + } + + /** + * Gets the cell type stored at an index. + * + * @param index Index to get value type for. + * @returns Value type for index provided, or Type_Invalid if it does not exist. + */ + public JSON_CELL_TYPE GetKeyTypeIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return Type_Invalid; + } + + return this.GetKeyType(key); + } + + /** + * Gets the length of the string stored at a key. + * + * @param key Key to get string length for. + * @returns Length of string at key provided, + * or -1 if it is not a string/does not exist. + */ + public int GetKeyLength(const char[] key) + { + int max_size = strlen(key) + strlen(JSON_META_LENGTH_KEY) + 1; + char[] length_key = new char[max_size]; + Format(length_key, max_size, "%s%s", key, JSON_META_LENGTH_KEY); + + int length; + return (this.GetValue(length_key, length)) ? length : -1; + } + + /** + * Gets the length of the string stored at an index. + * + * @param index Index to get string length for. + * @returns Length of string at index provided, or -1 if it is not a string/does not exist. + */ + public int GetKeyLengthIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return -1; + } + + return this.GetKeyLength(key); + } + + /** + * Gets whether the key should be hidden from encoding. + * + * @param key Key to get hidden state for. + * @returns Whether or not the key should be hidden. + */ + public bool GetKeyHidden(const char[] key) + { + int max_size = strlen(key) + strlen(JSON_META_HIDDEN_KEY) + 1; + char[] length_key = new char[max_size]; + Format(length_key, max_size, "%s%s", key, JSON_META_HIDDEN_KEY); + + bool hidden; + return (this.GetValue(length_key, hidden)) ? hidden : false; + } + + /** + * Gets whether the index should be hidden from encoding. + * + * @param index Index to get hidden state for. + * @returns Whether or not the index should be hidden. + */ + public bool GetKeyHiddenIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.GetKeyHidden(key); + } + + /** + * @section Internal Setters + */ + + /** + * Sets the cell type stored at a key. + * + * @param key Key to set value type for. + * @param type Type to set key to. + * @returns True on success, false otherwise. + */ + public bool SetKeyType(const char[] key, JSON_CELL_TYPE type) + { + int max_size = strlen(key) + strlen(JSON_META_TYPE_KEY) + 1; + char[] type_key = new char[max_size]; + Format(type_key, max_size, "%s%s", key, JSON_META_TYPE_KEY); + + return this.SetValue(type_key, type); + } + + /** + * Sets the cell type stored at an index. + * + * @param index Index to set value type for. + * @param type Type to set index to. + * @returns True on success, false otherwise. + */ + public bool SetKeyTypeIndexed(int index, JSON_CELL_TYPE value) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetKeyType(key, value); + } + + /** + * Sets the length of the string stored at a key. + * + * @param key Key to set string length for. + * @param length Length to set string to. + * @returns True on success, false otherwise. + */ + public bool SetKeyLength(const char[] key, int length) + { + int max_size = strlen(key) + strlen(JSON_META_LENGTH_KEY) + 1; + char[] length_key = new char[max_size]; + Format(length_key, max_size, "%s%s", key, JSON_META_LENGTH_KEY); + + return this.SetValue(length_key, length); + } + + /** + * Sets the length of the string stored at an index. + * + * @param index Index to set string length for. + * @param length Length to set string to. + * @returns True on success, false otherwise. + */ + public bool SetKeyLengthIndexed(int index, int length) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetKeyLength(key, length); + } + + /** + * Sets whether the key should be hidden from encoding. + * + * @param key Key to set hidden state for. + * @param hidden Wheter or not the key should be hidden. + * @returns True on success, false otherwise. + */ + public bool SetKeyHidden(const char[] key, bool hidden) + { + int max_size = strlen(key) + strlen(JSON_META_HIDDEN_KEY) + 1; + char[] hidden_key = new char[max_size]; + Format(hidden_key, max_size, "%s%s", key, JSON_META_HIDDEN_KEY); + + return this.SetValue(hidden_key, hidden); + } + + /** + * Sets whether the index should be hidden from encoding. + * + * @param index Index to set hidden state for. + * @param hidden Wheter or not the index should be hidden. + * @returns True on success, false otherwise. + */ + public bool SetKeyHiddenIndexed(int index, bool hidden) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetKeyHidden(key, hidden); + } + + /** + * @section Getters + */ + + // GetValue is implemented natively by StringMap + + /** + * Retrieves the value stored at an index. + * + * @param index Index to retrieve value for. + * @param value Variable to store value. + * @returns Value stored at index. + */ + public bool GetValueIndexed(int index, any &value) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.GetValue(key, value); + } + + // GetString is implemented natively by StringMap + + /** + * Retrieves the string stored at an index. + * + * @param index Index to retrieve string value for. + * @param value String buffer to store output. + * @param max_size Maximum size of string buffer. + * @returns True on success. False if the key is not set, or the key is set as a value or array (not a string). + */ + public bool GetStringIndexed(int index, char[] value, int max_size) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.GetString(key, value, max_size); + } + + /** + * Retrieves the int stored at a key. + * + * @param key Key to retrieve int value for. + * @returns Value stored at key. + */ + public int GetInt(const char[] key) + { + int value; + return (this.GetValue(key, value)) ? value : -1; + } + + /** + * Retrieves the int stored at an index. + * + * @param index Index to retrieve int value for. + * @returns Value stored at index. + */ + public int GetIntIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return -1; + } + + return this.GetInt(key); + } + + /** + * Retrieves the float stored at a key. + * + * @param key Key to retrieve float value for. + * @returns Value stored at key. + */ + public float GetFloat(const char[] key) + { + float value; + return (this.GetValue(key, value)) ? value : -1.0; + } + + /** + * Retrieves the float stored at an index. + * + * @param index Index to retrieve float value for. + * @returns Value stored at index. + */ + public float GetFloatIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return -1.0; + } + + return this.GetFloat(key); + } + + /** + * Retrieves the bool stored at a key. + * + * @param key Key to retrieve bool value for. + * @returns Value stored at key. + */ + public bool GetBool(const char[] key) + { + bool value; + return (this.GetValue(key, value)) ? value : false; + } + + /** + * Retrieves the bool stored at an index. + * + * @param index Index to retrieve bool value for. + * @returns Value stored at index. + */ + public bool GetBoolIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.GetBool(key); + } + + /** + * Retrieves the handle stored at a key. + * + * @param key Key to retrieve handle value for. + * @returns Value stored at key. + */ + public Handle GetHandle(const char[] key) + { + Handle value; + return (this.GetValue(key, value)) ? value : null; + } + + /** + * Retrieves the handle stored at an index. + * + * @param index Index to retrieve handle value for. + * @returns Value stored at index. + */ + public Handle GetHandleIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return null; + } + + return this.GetHandle(key); + } + + /** + * Retrieves the JSON object stored at a key. + * + * @param key Key to retrieve object value for. + * @returns Value stored at key. + */ + public JSON_Object GetObject(const char[] key) + { + return view_as<JSON_Object>(this.GetHandle(key)); + } + + /** + * Retrieves the object stored at an index. + * + * @param index Index to retrieve object value for. + * @returns Value stored at index. + */ + public JSON_Object GetObjectIndexed(int index) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return null; + } + + return this.GetObject(key); + } + + /** + * @section Setters + */ + + /** + * Sets the string stored at a key. + * + * @param key Key to set to string value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetString(const char[] key, const char[] value, bool replace = true) + { + return this.SetString(key, value, replace) + && this.SetKeyType(key, Type_String) + && this.SetKeyLength(key, strlen(value)); + } + + /** + * Sets the string stored at an index. + * + * @param index Index to set to string value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetStringIndexed(int index, const char[] value, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetString(key, value, replace); + } + + /** + * Sets the int stored at a key. + * + * @param key Key to set to int value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetInt(const char[] key, int value, bool replace = true) + { + return this.SetValue(key, value, replace) + && this.SetKeyType(key, Type_Int); + } + + /** + * Sets the int stored at an index. + * + * @param index Index to set to int value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetIntIndexed(int index, int value, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetInt(key, value, replace); + } + + /** + * Sets the float stored at a key. + * + * @param key Key to set to float value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetFloat(const char[] key, float value, bool replace = true) + { + return this.SetValue(key, value, replace) + && this.SetKeyType(key, Type_Float); + } + + /** + * Sets the float stored at an index. + * + * @param index Index to set to float value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetFloatIndexed(int index, float value, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetFloat(key, value, replace); + } + + /** + * Sets the bool stored at a key. + * + * @param key Key to set to bool value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetBool(const char[] key, bool value, bool replace = true) + { + return this.SetValue(key, value, replace) + && this.SetKeyType(key, Type_Bool); + } + + /** + * Sets the bool stored at an index. + * + * @param index Index to set to bool value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetBoolIndexed(int index, bool value, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetBool(key, value, replace); + } + + /** + * Sets the handle stored at a key. + * + * @param key Key to set to handle value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetHandle(const char[] key, Handle value = null, bool replace = true) + { + return this.SetValue(key, value, replace) + && this.SetKeyType(key, Type_Null); + } + + /** + * Sets the handle stored at an index. + * + * @param index Index to set to handle value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetHandleIndexed(int index, Handle value = null, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetHandle(key, value, replace); + } + + /** + * Sets the object stored at a key. + * + * @param key Key to set to object value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetObject(const char[] key, JSON_Object value, bool replace = true) + { + return this.SetValue(key, value, replace) + && this.SetKeyType(key, Type_Object); + } + + /** + * Sets the object stored at an index. + * + * @param index Index to set to object value. + * @param value Value to set. + * @returns True on success, false otherwise. + */ + public bool SetObjectIndexed(int index, JSON_Object value, bool replace = true) + { + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + return this.SetObject(key, value, replace); + } + + /** + * @section Array setters. + */ + + /** + * Pushes a string to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushString(const char[] value) + { + return this.SetStringIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * Pushes an int to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushInt(int value) + { + return this.SetIntIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * Pushes a float to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushFloat(float value) + { + return this.SetFloatIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * Pushes a bool to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushBool(bool value) + { + return this.SetBoolIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * Pushes a handle to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushHandle(Handle value = null) + { + return this.SetHandleIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * Pushes an object to the end of the array. + * + * @param value Value to push. + * @returns True on success, false otherwise. + */ + public bool PushObject(JSON_Object value) + { + return this.SetObjectIndexed(this.CurrentIndex, value) + && this.IncrementIndex(); + } + + /** + * @section Generic. + */ + + /** + * Finds the index of a value in the array. + * + * @param value Value to search for. + * @returns The index of the value if it is found, -1 otherwise. + */ + public int IndexOf(any value) + { + any current; + for (int i = 0; i < this.CurrentIndex; ++i) { + if (this.GetValueIndexed(i, current) && value == current) { + return i; + } + } + + return -1; + } + + /** + * Finds the index of a string in the array. + * + * @param value String to search for. + * @returns The index of the string if it is found, -1 otherwise. + */ + public int IndexOfString(const char[] value) + { + for (int i = 0; i < this.CurrentIndex; ++i) { + if (this.GetKeyTypeIndexed(i) != Type_String) { + continue; + } + + int current_size = this.GetKeyLengthIndexed(i) + 1; + char[] current = new char[current_size]; + this.GetStringIndexed(i, current, current_size); + if (StrEqual(value, current)) { + return i; + } + } + + return -1; + } + + /** + * Determines whether the array contains a value. + * + * @param value Value to search for. + * @returns True if the value is found, false otherwise. + */ + public bool Contains(any value) + { + return this.IndexOf(value) != -1; + } + + /** + * Determines whether the array contains a string. + * + * @param value String to search for. + * @returns True if the string is found, false otherwise. + */ + public bool ContainsString(const char[] value) + { + return this.IndexOfString(value) != -1; + } + + /** + * Removes an item from the object by key. + * + * @param key Key of object to remove. + * @returns True on success, false if the value was never set. + */ + public bool Remove(const char[] key) { + static char meta_keys[][] = { + JSON_META_TYPE_KEY, JSON_META_LENGTH_KEY, JSON_META_HIDDEN_KEY + }; + + // create a new char[] which will fit the longest meta-key + int meta_key_size = strlen(key) + 8; + char[] meta_key = new char[meta_key_size]; + + // view ourselves as a StringMap so we can call underlying Remove() method + StringMap self = view_as<StringMap>(this); + + bool success = true; + for (int i = 0; i < sizeof(meta_keys); ++i) { + Format(meta_key, meta_key_size, "%s%s", key, meta_keys[i]); + + if (this.HasKey(meta_key)) { + success = success && self.Remove(meta_key); + } + } + + return success && self.Remove(key); + } + + /** + * Removes a key and its related meta-keys from the object. + * + * @param key Key to remove. + * @returns True on success, false if the value was never set. + */ + public bool RemoveIndexed(int index) + { + if (! this.HasIndex(index)) { + return false; + } + + char key[JSON_INDEX_BUFFER_SIZE]; + if (! this.GetIndexString(key, sizeof(key), index)) { + return false; + } + + if (! this.Remove(key)) { + return false; + } + + for (int i = index + 1; i < this.CurrentIndex; ++i) { + if (! this.GetIndexString(key, sizeof(key), i)) { + return false; + } + + int target = i - 1; + + JSON_CELL_TYPE type = this.GetKeyTypeIndexed(i); + + switch (type) { + case Type_String: { + int str_length = this.GetKeyLengthIndexed(i); + char[] str_value = new char[str_length]; + + this.GetStringIndexed(i, str_value, str_length + 1); + this.SetStringIndexed(target, str_value); + } + case Type_Int: { + this.SetIntIndexed(target, this.GetIntIndexed(i)); + } + case Type_Float: { + this.SetFloatIndexed(target, this.GetFloatIndexed(i)); + } + case Type_Bool: { + this.SetBoolIndexed(target, this.GetBoolIndexed(i)); + } + case Type_Null: { + this.SetHandleIndexed(target, this.GetHandleIndexed(i)); + } + case Type_Object: { + this.SetObjectIndexed(target, this.GetObjectIndexed(i)); + } + } + + if (this.GetKeyHiddenIndexed(i)) { + this.SetKeyHiddenIndexed(target, true); + } + + this.Remove(key); + } + + this.CurrentIndex -= 1; + + return true; + } + + /** + * Encodes the instance into its string representation. + * + * @param output String buffer to store output. + * @param max_size Maximum size of string buffer. + * @param pretty_print Should the output be pretty printed (newlines and spaces)? [default: false] + * @param depth The current depth of the encoder. [default: 0] + */ + public void Encode(char[] output, int max_size, bool pretty_print = false, int depth = 0) + { + json_encode(this, output, max_size, pretty_print, depth); + } + + /** + * Decodes a JSON string into this object. + * + * @param buffer Buffer to decode. + */ + public void Decode(const char[] buffer) + { + json_decode(buffer, this); + } + + /** + * Recursively cleans up the object and any objects referenced within. + */ + public void Cleanup() + { + json_cleanup(this); + } +}; diff --git a/sourcemod/scripting/include/json/string_helpers.inc b/sourcemod/scripting/include/json/string_helpers.inc new file mode 100644 index 0000000..6063d47 --- /dev/null +++ b/sourcemod/scripting/include/json/string_helpers.inc @@ -0,0 +1,77 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * sm-json + * Provides a pure SourcePawn implementation of JSON encoding and decoding. + * https://github.com/clugg/sm-json + * + * sm-json (C)2018 James D. (clug) + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <http://www.sourcemod.net/license.php>. + */ + +#if defined _json_string_helpers_included + #endinput +#endif +#define _json_string_helpers_included + +/** + * Checks if a string starts with another string. + * + * @param haystack String to check that starts with needle. + * @param maxlen Maximum size of string buffer. + * @param needle String to check that haystack starts with. + * @return True if haystack begins with needle, false otherwise. + */ +stock bool json_string_startswith(const char[] haystack, const char[] needle) { + int haystack_length = strlen(haystack); + int needle_length = strlen(needle); + if (needle_length > haystack_length) return false; + + for (int i = 0; i < needle_length; ++i) { + if (haystack[i] != needle[i]) return false; + } + + return true; +} + +/** + * Checks if a string ends with another string. + * + * @param haystack String to check that ends with needle. + * @param maxlen Maximum size of string buffer. + * @param needle String to check that haystack ends with. + * @return True if haystack ends with needle, false otherwise. + */ +stock bool json_string_endswith(const char[] haystack, const char[] needle) { + int haystack_length = strlen(haystack); + int needle_length = strlen(needle); + if (needle_length > haystack_length) return false; + + for (int i = 0; i < needle_length; ++i) { + if (haystack[haystack_length - needle_length + i] != needle[i]) return false; + } + + return true; +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/movement.inc b/sourcemod/scripting/include/movement.inc new file mode 100644 index 0000000..7cc5b29 --- /dev/null +++ b/sourcemod/scripting/include/movement.inc @@ -0,0 +1,530 @@ +/* + MovementAPI Function Stock Library + + Website: https://github.com/danzayau/MovementAPI +*/ + +#if defined _movement_included_ + #endinput +#endif +#define _movement_included_ + +#include <sdktools> + + + +// =====[ STOCKS ]===== + +/** + * Calculates the horizontal distance between two vectors. + * + * @param vec1 First vector. + * @param vec2 Second vector. + * @return Vector horizontal distance. + */ +stock float GetVectorHorizontalDistance(const float vec1[3], const float vec2[3]) +{ + return SquareRoot(Pow(vec2[0] - vec1[0], 2.0) + Pow(vec2[1] - vec1[1], 2.0)); +} + +/** + * Calculates a vector's horizontal length. + * + * @param vec Vector. + * @return Vector horizontal length (magnitude). + */ +stock float GetVectorHorizontalLength(const float vec[3]) +{ + return SquareRoot(Pow(vec[0], 2.0) + Pow(vec[1], 2.0)); +} + +/** + * Scales a vector to a certain horizontal length. + * + * @param vec Vector. + * @param length New horizontal length. + */ +stock void SetVectorHorizontalLength(float vec[3], float length) +{ + float newVec[3]; + newVec = vec; + newVec[2] = 0.0; + NormalizeVector(newVec, newVec); + ScaleVector(newVec, length); + newVec[2] = vec[2]; + vec = newVec; +} + +/** + * Gets a player's currently pressed buttons. + * + * @param client Client index. + * @return Bitsum of buttons. + */ +stock int Movement_GetButtons(int client) +{ + return GetClientButtons(client); +} + +/** + * Gets a player's origin. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void Movement_GetOrigin(int client, float result[3]) +{ + GetClientAbsOrigin(client, result); +} + +/** + * Gets a player's origin. + * If the player is on the ground, a trace hull is used to find the + * exact height of the ground the player is standing on. This is thus + * more accurate than Movement_GetOrigin when player is on ground. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void Movement_GetOriginEx(int client, float result[3]) +{ + if (!Movement_GetOnGround(client)) + { + GetClientAbsOrigin(client, result); + return; + } + + // Get the coordinate of the solid beneath the player's origin + // More accurate than GetClientAbsOrigin when on ground + float startPosition[3], endPosition[3]; + GetClientAbsOrigin(client, startPosition); + endPosition = startPosition; + endPosition[2] = startPosition[2] - 2.0; // Should be less than 2.0 units away + Handle trace = TR_TraceHullFilterEx( + startPosition, + endPosition, + view_as<float>( { -16.0, -16.0, 0.0 } ), // Players are 32 x 32 x 72 + view_as<float>( { 16.0, 16.0, 72.0 } ), + MASK_PLAYERSOLID, + TraceEntityFilterPlayers, + client); + if (TR_DidHit(trace)) + { + TR_GetEndPosition(result, trace); + // Do not get rid of the offset. The offset is correct, as the player must be + // at least 0.03125 units away from the ground. + } + else + { + result = startPosition; // Fallback to GetClientAbsOrigin + } + delete trace; +} + +public bool TraceEntityFilterPlayers(int entity, int contentsMask) +{ + return entity > MaxClients; +} + +/** + * Sets a player's origin by teleporting them. + * + * @param client Client index. + * @param origin New origin. + */ +stock void Movement_SetOrigin(int client, const float origin[3]) +{ + TeleportEntity(client, origin, NULL_VECTOR, NULL_VECTOR); +} + +/** + * Gets a player's velocity. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void Movement_GetVelocity(int client, float result[3]) +{ + GetEntPropVector(client, Prop_Data, "m_vecVelocity", result); +} + +/** + * Sets a player's velocity by teleporting them. + * + * @param client Client index. + * @param velocity New velocity. + */ +stock void Movement_SetVelocity(int client, const float velocity[3]) +{ + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, velocity); +} + +/** + * Gets a player's horizontal speed. + * + * @param client Client index. + * @return Player's horizontal speed. + */ +stock float Movement_GetSpeed(int client) +{ + float velocity[3]; + Movement_GetVelocity(client, velocity); + return GetVectorHorizontalLength(velocity); +} + +/** + * Sets a player's horizontal speed. + * + * @param client Client index. + * @param value New horizontal speed. + * @param applyBaseVel Whether to apply base velocity as well. + */ +stock void Movement_SetSpeed(int client, float value, bool applyBaseVel = false) +{ + float velocity[3]; + Movement_GetVelocity(client, velocity); + SetVectorHorizontalLength(velocity, value) + if (applyBaseVel) + { + float baseVelocity[3]; + Movement_GetBaseVelocity(client, baseVelocity); + AddVectors(velocity, baseVelocity, velocity); + } + Movement_SetVelocity(client, velocity); +} + +/** + * Gets a player's vertical velocity. + * + * @param client Client index. + * @return Player's vertical velocity. + */ +stock float Movement_GetVerticalVelocity(int client) +{ + float velocity[3]; + Movement_GetVelocity(client, velocity); + return velocity[2]; +} + +/** + * Sets a player's vertical velocity. + * + * @param client Client index. + * @param value New vertical velocity. + */ +stock void Movement_SetVerticalVelocity(int client, float value) +{ + float velocity[3]; + Movement_GetVelocity(client, velocity); + velocity[2] = value; + Movement_SetVelocity(client, velocity); +} + +/** + * Gets a player's base velocity. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void Movement_GetBaseVelocity(int client, float result[3]) +{ + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result); +} + +/** + * Sets a player's base velocity. + * + * @param client Client index. + * @param baseVelocity New base velocity. + */ +stock void Movement_SetBaseVelocity(int client, const float baseVelocity[3]) +{ + SetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); +} + +/** + * Gets a player's eye angles. + * + * @param client Client index. + * @param result Resultant vector. + */ +stock void Movement_GetEyeAngles(int client, float result[3]) +{ + GetClientEyeAngles(client, result); +} + +/** + * Sets a player's eye angles by teleporting them. + * + * @param client Client index. + * @param eyeAngles New eye angles. + */ +stock void Movement_SetEyeAngles(int client, const float eyeAngles[3]) +{ + TeleportEntity(client, NULL_VECTOR, eyeAngles, NULL_VECTOR); +} + +/** + * Gets whether a player is on the ground. + * + * @param client Client index. + * @return Whether player is on the ground. + */ +stock bool Movement_GetOnGround(int client) +{ + return view_as<bool>(GetEntityFlags(client) & FL_ONGROUND); +} + +/** + * Gets whether a player is ducking or ducked. + * + * @param client Client index. + * @return Whether player is ducking or ducked. + */ +stock bool Movement_GetDucking(int client) +{ + return GetEntProp(client, Prop_Send, "m_bDucked") || GetEntProp(client, Prop_Send, "m_bDucking"); +} + +/** + * Gets a player's "m_flDuckSpeed" value. + * + * @param client Client index. + * @return Value of "m_flDuckSpeed". + */ +stock float Movement_GetDuckSpeed(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flDuckSpeed"); +} + +/** + * Sets a player's "m_flDuckSpeed" value. + * + * @param client Client index. + * @param value New "m_flDuckSpeed" value. + */ +stock void Movement_SetDuckSpeed(int client, float value) +{ + SetEntPropFloat(client, Prop_Send, "m_flDuckSpeed", value); +} + +/** + * Gets a player's "m_flVelocityModifier" value. + * + * @param client Client index. + * @return Value of "m_flVelocityModifier". + */ +stock float Movement_GetVelocityModifier(int client) +{ + return GetEntPropFloat(client, Prop_Send, "m_flVelocityModifier"); +} + +/** + * Sets a player's "m_flVelocityModifier" value. + * + * @param client Client index. + * @param value New "m_flVelocityModifier" value. + */ +stock void Movement_SetVelocityModifier(int client, float value) +{ + SetEntPropFloat(client, Prop_Send, "m_flVelocityModifier", value); +} + +/** + * Gets a player's gravity scale factor. + * + * @param client Client index. + * @return Gravity scale factor. + */ +stock float Movement_GetGravity(int client) +{ + return GetEntityGravity(client); +} + +/** + * Sets a player's gravity scale factor. + * + * @param client Client index. + * @param value Desired gravity scale factor. + */ +stock void Movement_SetGravity(int client, float value) +{ + SetEntityGravity(client, value); +} + +/** + * Gets a player's movetype. + * + * @param client Client index. + * @return Player's movetype. + */ +stock MoveType Movement_GetMovetype(int client) +{ + return GetEntityMoveType(client); +} + +/** + * Sets a player's movetype. + * + * @param client Client index. + * @param movetype New movetype. + */ +stock void Movement_SetMovetype(int client, MoveType movetype) +{ + SetEntityMoveType(client, movetype); +} + +/** + * Gets whether a player is on a ladder. + * + * @param client Client index. + * @return Whether player is on a ladder. + */ +stock bool Movement_GetOnLadder(int client) +{ + return GetEntityMoveType(client) == MOVETYPE_LADDER; +} + +/** + * Gets whether a player is noclipping. + * + * @param client Client index. + * @return Whether player is noclipping. + */ +stock bool Movement_GetNoclipping(int client) +{ + return GetEntityMoveType(client) == MOVETYPE_NOCLIP; +} + + + +// =====[ METHODMAP ]===== + +methodmap MovementPlayer { + + public MovementPlayer(int client) { + return view_as<MovementPlayer>(client); + } + + property int ID { + public get() { + return view_as<int>(this); + } + } + + property int Buttons { + public get() { + return Movement_GetButtons(this.ID); + } + } + + public void GetOrigin(float result[3]) { + Movement_GetOrigin(this.ID, result); + } + + public void SetOrigin(const float origin[3]) { + Movement_SetOrigin(this.ID, origin); + } + + public void GetVelocity(float result[3]) { + Movement_GetVelocity(this.ID, result); + } + + public void SetVelocity(const float velocity[3]) { + Movement_SetVelocity(this.ID, velocity); + } + + property float Speed { + public get() { + return Movement_GetSpeed(this.ID); + } + public set(float value) { + Movement_SetSpeed(this.ID, value); + } + } + + property float VerticalVelocity { + public get() { + return Movement_GetVerticalVelocity(this.ID); + } + public set(float value) { + Movement_SetVerticalVelocity(this.ID, value); + } + } + + public void GetBaseVelocity(float result[3]) { + Movement_GetBaseVelocity(this.ID, result); + } + + public void SetBaseVelocity(const float baseVelocity[3]) { + Movement_SetBaseVelocity(this.ID, baseVelocity); + } + + public void GetEyeAngles(float result[3]) { + Movement_GetEyeAngles(this.ID, result); + } + + public void SetEyeAngles(const float eyeAngles[3]) { + Movement_SetEyeAngles(this.ID, eyeAngles); + } + + property bool OnGround { + public get() { + return Movement_GetOnGround(this.ID); + } + } + + property bool Ducking { + public get() { + return Movement_GetDucking(this.ID); + } + } + + property float DuckSpeed { + public get() { + return Movement_GetDuckSpeed(this.ID); + } + public set(float value) { + Movement_SetDuckSpeed(this.ID, value); + } + } + + property float VelocityModifier { + public get() { + return Movement_GetVelocityModifier(this.ID); + } + public set(float value) { + Movement_SetVelocityModifier(this.ID, value); + } + } + + property float Gravity { + public get() { + return Movement_GetGravity(this.ID); + } + public set(float value) { + Movement_SetGravity(this.ID, value); + } + } + + property MoveType Movetype { + public get() { + return Movement_GetMovetype(this.ID); + } + public set(MoveType movetype) { + Movement_SetMovetype(this.ID, movetype); + } + } + + property bool OnLadder { + public get() { + return Movement_GetOnLadder(this.ID); + } + } + + property bool Noclipping { + public get() { + return Movement_GetNoclipping(this.ID); + } + } +} diff --git a/sourcemod/scripting/include/movementapi.inc b/sourcemod/scripting/include/movementapi.inc new file mode 100644 index 0000000..290c3f2 --- /dev/null +++ b/sourcemod/scripting/include/movementapi.inc @@ -0,0 +1,663 @@ +/* + MovementAPI Plugin Include + + Website: https://github.com/danzayau/MovementAPI +*/ + +#if defined _movementapi_included_ + #endinput +#endif +#define _movementapi_included_ + +#include <movement> + + + +/* + Terminology + + Takeoff + Becoming airborne, including jumping, falling, getting off a ladder and leaving noclip. + + Landing + Leaving the air, including landing on the ground, grabbing a ladder and entering noclip. + + Perfect Bunnyhop (Perf) + When the player has jumped in the tick after landing and keeps their speed. + + Duckbug/Crouchbug + When the player sucessfully lands due to uncrouching from mid air and not by falling + down. This causes no stamina loss or fall damage upon landing. + + Jumpbug + This is achieved by duckbugging and jumping at the same time. The player is never seen + as 'on ground' when bunnyhopping from a tick by tick perspective. A jumpbug inherits + the same behavior as a duckbug/crouchbug, along with its effects such as maintaining + speed due to no stamina loss. + + Distbug + Landing behavior varies depending on whether the player lands close to the edge of a + block or not: + + 1. If the player lands close to the edge of a block, this causes the jump duration to + be one tick longer and the player can "slide" on the ground during the landing tick, + using the position post-tick as landing position becomes inaccurate. + + 2. On the other hand, if the player does not land close to the edge, the player will + be considered on the ground one tick earlier, using this position as landing position + is not accurate as the player has yet to be fully on the ground. + + In scenario 1, GetNobugLandingOrigin calculates the correct landing position of the + player before the sliding effect takes effect. + + In scenario 2, GetNobugLandingOrigin attempts to extrapolate the player's fully on + ground position to make landing positions consistent across scenarios. +*/ + + + +// =====[ FORWARDS ]===== + +/** + * Called when a player's movetype changes. + * + * @param client Client index. + * @param oldMovetype Player's old movetype. + * @param newMovetype Player's new movetype. + */ +forward void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype); + +/** + * Called when a player touches the ground. + * + * @param client Client index. + */ +forward void Movement_OnStartTouchGround(int client); + +/** + * Called when a player leaves the ground. + * + * @param client Client index. + * @param jumped Whether player jumped to leave ground. + * @param ladderJump Whether player jumped from a ladder. + * @param jumpbug Whether player performed a jumpbug. + */ +forward void Movement_OnStopTouchGround(int client, bool jumped, bool ladderJump, bool jumpbug); + +/** + * Called when a player starts ducking. + * + * @param client Client index. + */ +forward void Movement_OnStartDucking(int client); + +/** + * Called when a player stops ducking. + * + * @param client Client index. + */ +forward void Movement_OnStopDucking(int client); + +/** + * Called when a player jumps (player_jump event), including 'jumpbugs'. + * Setting velocity when this is called may not be effective. + * + * @param client Client index. + * @param jumpbug Whether player 'jumpbugged'. + */ +forward void Movement_OnPlayerJump(int client, bool jumpbug); + +/** + * Called before PlayerMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnPlayerMovePre(int client, float origin[3], float velocity[3]); + +/** + * Called after PlayerMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnPlayerMovePost(int client, float origin[3], float velocity[3]); + +/** + * Called before Duck movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnDuckPre(int client, float origin[3], float velocity[3]); + +/** + * Called after Duck movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnDuckPost(int client, float origin[3], float velocity[3]); + +/** + * Called before LadderMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnLadderMovePre(int client, float origin[3], float velocity[3]); + +/** + * Called after LadderMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnLadderMovePost(int client, float origin[3], float velocity[3]); + +/** + * Called before FullLadderMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnFullLadderMovePre(int client, float origin[3], float velocity[3]); + +/** + * Called after FullLadderMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnFullLadderMovePost(int client, float origin[3], float velocity[3]); + +/** + * Called after the player jumps, but before jumping stamina is applied and takeoff variables are not set yet. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnJumpPre(int client, float origin[3], float velocity[3]); + +/** + * Called after the player jumps and after jumping stamina is applied and takeoff variables are already set here. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnJumpPost(int client, float origin[3], float velocity[3]); + +/** + * Called before AirAccelerate movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnAirAcceleratePre(int client, float origin[3], float velocity[3]); + +/** + * Called after AirAccelerate movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnAirAcceleratePost(int client, float origin[3], float velocity[3]); + +/** + * Called before WalkMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnWalkMovePre(int client, float origin[3], float velocity[3]); + +/** + * Called after WalkMove movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnWalkMovePost(int client, float origin[3], float velocity[3]); + +/** + * Called before CategorizePosition movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnCategorizePositionPre(int client, float origin[3], float velocity[3]); + +/** + * Called after CategorizePosition movement function is called. + * Modifying origin or velocity parameters will change player's origin and velocity accordingly. + * + * @param client Client index. + * @param origin Player origin. + * @param velocity Player velocity. + * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise. + */ +forward Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3]); + +// =====[ NATIVES ]===== + +/** + * Gets whether a player's last takeoff was a jump. + * + * @param client Client index. + * @return Whether player's last takeoff was a jump. + */ +native bool Movement_GetJumped(int client); + +/** + * Gets whether a player's last takeoff was a perfect bunnyhop. + * + * @param client Client index. + * @return Whether player's last takeoff was a perfect bunnyhop. + */ +native bool Movement_GetHitPerf(int client); + +/** + * Gets a player's origin at the time of their last takeoff. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetTakeoffOrigin(int client, float result[3]); + +/** + * Gets a player's velocity at the time of their last takeoff. + * + * If sv_enablebunnyhopping is 0, CS:GO may adjust the player's + * velocity after the takeoff velocity has already been measured. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetTakeoffVelocity(int client, float result[3]); + +/** + * Gets a player's horizontal speed at the time of their last takeoff. + * + * If sv_enablebunnyhopping is 0, CS:GO may adjust the player's + * velocity after the takeoff velocity has already been measured. + * + * @param client Client index. + * @return Player's last takeoff speed. + */ +native float Movement_GetTakeoffSpeed(int client); + +/** + * Gets a player's 'tickcount' at the time of their last takeoff. + * + * @param client Client index. + * @return Player's last takeoff 'tickcount'. + */ +native int Movement_GetTakeoffTick(int client); + +/** + * Gets a player's 'cmdnum' at the time of their last takeoff. + * + * @param client Client index. + * @return Player's last takeoff 'cmdnum'. + */ +native int Movement_GetTakeoffCmdNum(int client); + +/** + * Gets a player's origin at the time of their last landing with the distbug fixed. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetNobugLandingOrigin(int client, float result[3]); + +/** + * Gets a player's origin at the time of their last landing. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetLandingOrigin(int client, float result[3]); + +/** + * Gets a player's velocity at the time of their last landing. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetLandingVelocity(int client, float result[3]); + +/** + * Gets a player's horizontal speed at the time of their last landing. + * + * @param client Client index. + * @return Last landing speed of the player (horizontal). + */ +native float Movement_GetLandingSpeed(int client); + +/** + * Gets a player's 'tickcount' at the time of their last landing. + * + * @param client Client index. + * @return Player's last landing 'tickcount'. + */ +native int Movement_GetLandingTick(int client); + +/** + * Gets a player's 'cmdnum' at the time of their last landing. + * + * @param client Client index. + * @return Player's last landing 'cmdnum'. + */ +native int Movement_GetLandingCmdNum(int client); + +/** + * Gets whether a player is turning their aim horizontally. + * + * @param client Client index. + * @return Whether player is turning their aim horizontally. + */ +native bool Movement_GetTurning(int client); + +/** + * Gets whether a player is turning their aim left. + * + * @param client Client index. + * @return Whether player is turning their aim left. + */ +native bool Movement_GetTurningLeft(int client); + +/** + * Gets whether a player is turning their aim right. + * + * @param client Client index. + * @return Whether player is turning their aim right. + */ +native bool Movement_GetTurningRight(int client); + +/** + * Gets result of CCSPlayer::GetPlayerMaxSpeed(client), which + * is the player's max speed as limited by their weapon. + * + * @param client Client index. + * @return Player's max speed as limited by their weapon. + */ +native float Movement_GetMaxSpeed(int client); + +/** + * Gets whether a player duckbugged on this tick. + * + * @param client Client index. + * @return Whether a player duckbugged on this tick. + */ +native bool Movement_GetDuckbugged(int client); + +/** + * Gets whether a player jumpbugged on this tick. + * + * @param client Client index. + * @return Whether a player jumpbugged on this tick. + */ +native bool Movement_GetJumpbugged(int client); + +/** + * Get the player's origin during movement processing. + * + * @param client Client index. + * @param result Resultant vector. + */ +native void Movement_GetProcessingOrigin(int client, float result[3]); + +/** + * Get the player's velocity during movement processing. + * + * @param client Param description + * @param result Resultant vector. + */ +native void Movement_GetProcessingVelocity(int client, float result[3]); + +/** + * Set the player's takeoff origin. + * + * @param client Client index. + * @param origin Desired origin. + */ +native void Movement_SetTakeoffOrigin(int client, float origin[3]); + +/** + * Set the player's takeoff velocity. + * + * @param client Client index. + * @param origin Desired velocity. + */ +native void Movement_SetTakeoffVelocity(int client, float velocity[3]); + +/** + * Set the player's landing origin. + * + * @param client Client index. + * @param origin Desired origin. + */ +native void Movement_SetLandingOrigin(int client, float origin[3]); + +/** + * Set the player's landing velocity. + * + * @param client Client index. + * @param origin Desired velocity. + */ +native void Movement_SetLandingVelocity(int client, float velocity[3]); + +// =====[ METHODMAP ]===== + +methodmap MovementAPIPlayer < MovementPlayer { + + public MovementAPIPlayer(int client) { + return view_as<MovementAPIPlayer>(MovementPlayer(client)); + } + + property bool Jumped { + public get() { + return Movement_GetJumped(this.ID); + } + } + + property bool HitPerf { + public get() { + return Movement_GetHitPerf(this.ID); + } + } + + public void GetTakeoffOrigin(float buffer[3]) { + Movement_GetTakeoffOrigin(this.ID, buffer); + } + + public void GetTakeoffVelocity(float buffer[3]) { + Movement_GetTakeoffVelocity(this.ID, buffer); + } + + public void SetTakeoffOrigin(float buffer[3]) + { + Movement_SetTakeoffOrigin(this.ID, buffer); + } + + public void SetTakeoffVelocity(float buffer[3]) + { + Movement_SetTakeoffVelocity(this.ID, buffer); + } + + property float TakeoffSpeed { + public get() { + return Movement_GetTakeoffSpeed(this.ID); + } + } + + property int TakeoffTick { + public get() { + return Movement_GetTakeoffTick(this.ID); + } + } + + property int TakeoffCmdNum { + public get() { + return Movement_GetTakeoffCmdNum(this.ID); + } + } + + public void GetLandingOrigin(float buffer[3]) { + Movement_GetLandingOrigin(this.ID, buffer); + } + + public void GetLandingVelocity(float buffer[3]) { + Movement_GetLandingVelocity(this.ID, buffer); + } + + public void SetLandingOrigin(float buffer[3]) + { + Movement_SetLandingOrigin(this.ID, buffer); + } + + public void SetLandingVelocity(float buffer[3]) + { + Movement_SetLandingVelocity(this.ID, buffer); + } + + property float LandingSpeed { + public get() { + return Movement_GetLandingSpeed(this.ID); + } + } + + property int LandingTick { + public get() { + return Movement_GetLandingTick(this.ID); + } + } + + property int LandingCmdNum { + public get() { + return Movement_GetLandingCmdNum(this.ID); + } + } + + property bool Turning { + public get() { + return Movement_GetTurning(this.ID); + } + } + + property bool TurningLeft { + public get() { + return Movement_GetTurningLeft(this.ID); + } + } + + property bool TurningRight { + public get() { + return Movement_GetTurningRight(this.ID); + } + } + + property float MaxSpeed { + public get() { + return Movement_GetMaxSpeed(this.ID); + } + } + + public void GetProcessingVelocity(float buffer[3]) + { + Movement_GetProcessingVelocity(this.ID, buffer); + } + + public void GetProcessingOrigin(float buffer[3]) + { + Movement_GetProcessingOrigin(this.ID, buffer); + } +} + + + +// =====[ DEPENDENCY ]===== + +public SharedPlugin __pl_movementapi = +{ + name = "movementapi", + file = "movementapi.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_movementapi_SetNTVOptional() +{ + MarkNativeAsOptional("Movement_GetJumped"); + MarkNativeAsOptional("Movement_GetHitPerf"); + MarkNativeAsOptional("Movement_GetTakeoffOrigin"); + MarkNativeAsOptional("Movement_GetTakeoffVelocity"); + MarkNativeAsOptional("Movement_GetTakeoffSpeed"); + MarkNativeAsOptional("Movement_GetTakeoffTick"); + MarkNativeAsOptional("Movement_GetTakeoffCmdNum"); + MarkNativeAsOptional("Movement_GetLandingOrigin"); + MarkNativeAsOptional("Movement_GetLandingVelocity"); + MarkNativeAsOptional("Movement_GetLandingSpeed"); + MarkNativeAsOptional("Movement_GetLandingTick"); + MarkNativeAsOptional("Movement_GetLandingCmdNum"); + MarkNativeAsOptional("Movement_GetTurning"); + MarkNativeAsOptional("Movement_GetTurningLeft"); + MarkNativeAsOptional("Movement_GetTurningRight"); + MarkNativeAsOptional("Movement_GetMaxSpeed"); + MarkNativeAsOptional("Movement_GetProcessingOrigin"); + MarkNativeAsOptional("Movement_GetProcessingVelocity"); + MarkNativeAsOptional("Movement_SetTakeoffOrigin"); + MarkNativeAsOptional("Movement_SetTakeoffVelocity"); + MarkNativeAsOptional("Movement_SetLandingOrigin"); + MarkNativeAsOptional("Movement_SetLandingVelocity"); +} +#endif
\ No newline at end of file diff --git a/sourcemod/scripting/include/smjansson.inc b/sourcemod/scripting/include/smjansson.inc new file mode 100644 index 0000000..029a492 --- /dev/null +++ b/sourcemod/scripting/include/smjansson.inc @@ -0,0 +1,1328 @@ +#if defined _jansson_included_ + #endinput +#endif +#define _jansson_included_ + + +/** + * --- Type + * + * The JSON specification (RFC 4627) defines the following data types: + * object, array, string, number, boolean, and null. + * JSON types are used dynamically; arrays and objects can hold any + * other data type, including themselves. For this reason, Jansson�s + * type system is also dynamic in nature. There�s one Handle type to + * represent all JSON values, and the referenced structure knows the + * type of the JSON value it holds. + * + */ +enum json_type { + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_INTEGER, + JSON_REAL, + JSON_TRUE, + JSON_FALSE, + JSON_NULL +} + +/** + * Return the type of the JSON value. + * + * @param hObj Handle to the JSON value + * + * @return json_type of the value. + */ +native json_type:json_typeof(Handle:hObj); + +/** + * The type of a JSON value is queried and tested using these macros + * + * @param %1 Handle to the JSON value + * + * @return True if the value has the correct type. + */ +#define json_is_object(%1) ( json_typeof(%1) == JSON_OBJECT ) +#define json_is_array(%1) ( json_typeof(%1) == JSON_ARRAY ) +#define json_is_string(%1) ( json_typeof(%1) == JSON_STRING ) +#define json_is_integer(%1) ( json_typeof(%1) == JSON_INTEGER ) +#define json_is_real(%1) ( json_typeof(%1) == JSON_REAL ) +#define json_is_true(%1) ( json_typeof(%1) == JSON_TRUE ) +#define json_is_false(%1) ( json_typeof(%1) == JSON_FALSE ) +#define json_is_null(%1) ( json_typeof(%1) == JSON_NULL ) +#define json_is_number(%1) ( json_typeof(%1) == JSON_INTEGER || json_typeof(%1) == JSON_REAL ) +#define json_is_boolean(%1) ( json_typeof(%1) == JSON_TRUE || json_typeof(%1) == JSON_FALSE ) + +/** + * Saves json_type as a String in output + * + * @param input json_type value to convert to string + * @param output Buffer to store the json_type value + * @param maxlength Maximum length of string buffer. + * + * @return False if the type does not exist. + */ +stock bool:Stringify_json_type(json_type:input, String:output[], maxlength) { + switch(input) { + case JSON_OBJECT: strcopy(output, maxlength, "Object"); + case JSON_ARRAY: strcopy(output, maxlength, "Array"); + case JSON_STRING: strcopy(output, maxlength, "String"); + case JSON_INTEGER: strcopy(output, maxlength, "Integer"); + case JSON_REAL: strcopy(output, maxlength, "Real"); + case JSON_TRUE: strcopy(output, maxlength, "True"); + case JSON_FALSE: strcopy(output, maxlength, "False"); + case JSON_NULL: strcopy(output, maxlength, "Null"); + default: return false; + } + + return true; +} + + + +/** + * --- Equality + * + * - Two integer or real values are equal if their contained numeric + * values are equal. An integer value is never equal to a real value, + * though. + * - Two strings are equal if their contained UTF-8 strings are equal, + * byte by byte. Unicode comparison algorithms are not implemented. + * - Two arrays are equal if they have the same number of elements and + * each element in the first array is equal to the corresponding + * element in the second array. + * - Two objects are equal if they have exactly the same keys and the + * value for each key in the first object is equal to the value of + * the corresponding key in the second object. + * - Two true, false or null values have no "contents", so they are + * equal if their types are equal. + * + */ + +/** + * Test whether two JSON values are equal. + * + * @param hObj Handle to the first JSON object + * @param hOther Handle to the second JSON object + * + * @return Returns false if they are inequal or one + * or both of the pointers are NULL. + */ +native bool:json_equal(Handle:hObj, Handle:hOther); + + + + +/** + * --- Copying + * + * Jansson supports two kinds of copying: shallow and deep. There is + * a difference between these methods only for arrays and objects. + * + * Shallow copying only copies the first level value (array or object) + * and uses the same child values in the copied value. + * + * Deep copying makes a fresh copy of the child values, too. Moreover, + * all the child values are deep copied in a recursive fashion. + * + */ + +/** + * Get a shallow copy of the passed object + * + * @param hObj Handle to JSON object to be copied + * + * @return Returns a shallow copy of the object, + * or INVALID_HANDLE on error. + */ +native Handle:json_copy(Handle:hObj); + +/** + * Get a deep copy of the passed object + * + * @param hObj Handle to JSON object to be copied + * + * @return Returns a deep copy of the object, + * or INVALID_HANDLE on error. + */ +native Handle:json_deep_copy(Handle:hObj); + + + + +/** + * --- Objects + * + * A JSON object is a dictionary of key-value pairs, where the + * key is a Unicode string and the value is any JSON value. + * + */ + +/** + * Returns a handle to a new JSON object, or INVALID_HANDLE on error. + * Initially, the object is empty. + * + * @return Handle to a new JSON object. + */ +native Handle:json_object(); + +/** + * Returns the number of elements in hObj + * + * @param hObj Handle to JSON object + * + * @return Number of elements in hObj, + * or 0 if hObj is not a JSON object. + */ +native json_object_size(Handle:hObj); + +/** + * Get a value corresponding to sKey from hObj + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Key to retrieve + * + * @return Handle to a the JSON object or + * INVALID_HANDLE on error. + */ +native Handle:json_object_get(Handle:hObj, const String:sKey[]); + +/** + * Set the value of sKey to hValue in hObj. + * If there already is a value for key, it is replaced by the new value. + * + * @param hObj Handle to JSON object to set a value on + * @param sKey Key to store in the object + * Must be a valid null terminated UTF-8 encoded + * Unicode string. + * @param hValue Value to store in the object + * + * @return True on success. + */ +native bool:json_object_set(Handle:hObj, const String:sKey[], Handle:hValue); + +/** + * Set the value of sKey to hValue in hObj. + * If there already is a value for key, it is replaced by the new value. + * This function automatically closes the Handle to the value object. + * + * @param hObj Handle to JSON object to set a value on + * @param sKey Key to store in the object + * Must be a valid null terminated UTF-8 encoded + * Unicode string. + * @param hValue Value to store in the object + * + * @return True on success. + */ +native bool:json_object_set_new(Handle:hObj, const String:sKey[], Handle:hValue); + +/** + * Delete sKey from hObj if it exists. + * + * @param hObj Handle to JSON object to delete a key from + * @param sKey Key to delete + * + * @return True on success. + */ +native bool:json_object_del(Handle:hObj, const String:sKey[]); + +/** + * Remove all elements from hObj. + * + * @param hObj Handle to JSON object to remove all + * elements from. + * + * @return True on success. + */ +native bool:json_object_clear(Handle:hObj); + +/** + * Update hObj with the key-value pairs from hOther, overwriting + * existing keys. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update(Handle:hObj, Handle:hOther); + +/** + * Like json_object_update(), but only the values of existing keys + * are updated. No new keys are created. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update_existing(Handle:hObj, Handle:hOther); + +/** + * Like json_object_update(), but only new keys are created. + * The value of any existing key is not changed. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update_missing(Handle:hObj, Handle:hOther); + + + + +/** + * Object iteration + * + * Example code: + * - We assume hObj is a Handle to a valid JSON object. + * + * + * new Handle:hIterator = json_object_iter(hObj); + * while(hIterator != INVALID_HANDLE) + * { + * new String:sKey[128]; + * json_object_iter_key(hIterator, sKey, sizeof(sKey)); + * + * new Handle:hValue = json_object_iter_value(hIterator); + * + * // Do something with sKey and hValue + * + * CloseHandle(hValue); + * + * hIterator = json_object_iter_next(hObj, hIterator); + * } + * + */ + +/** + * Returns a handle to an iterator which can be used to iterate over + * all key-value pairs in hObj. + * If you are not iterating to the end of hObj make sure to close the + * handle to the iterator manually. + * + * @param hObj Handle to JSON object to get an iterator + * for. + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error. + */ +native Handle:json_object_iter(Handle:hObj); + +/** + * Like json_object_iter(), but returns an iterator to the key-value + * pair in object whose key is equal to key. + * Iterating forward to the end of object only yields all key-value + * pairs of the object if key happens to be the first key in the + * underlying hash table. + * + * @param hObj Handle to JSON object to get an iterator + * for. + * @param sKey Start key for the iterator + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error. + */ +native Handle:json_object_iter_at(Handle:hObj, const String:key[]); + +/** + * Returns an iterator pointing to the next key-value pair in object. + * This automatically closes the Handle to the iterator hIter. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error, or if the + * whole object has been iterated through. + */ +native Handle:json_object_iter_next(Handle:hObj, Handle:hIter); + +/** + * Extracts the associated key of hIter as a null terminated UTF-8 + * encoded string in the passed buffer. + * + * @param hIter Handle to the JSON String object + * @param sKeyBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * @error Invalid JSON Object Iterator. + * @return Length of the returned string or -1 on error. + */ +native json_object_iter_key(Handle:hIter, String:sKeyBuffer[], maxlength); + +/** + * Returns a handle to the value hIter is pointing at. + * + * @param hIter Handle to JSON object iterator. + * + * @return Handle to value or INVALID_HANDLE on error. + */ +native Handle:json_object_iter_value(Handle:hIter); + +/** + * Set the value of the key-value pair in hObj, that is pointed to + * by hIter, to hValue. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * @param hValue Handle to JSON value. + * + * @return True on success. + */ +native bool:json_object_iter_set(Handle:hObj, Handle:hIter, Handle:hValue); + +/** + * Set the value of the key-value pair in hObj, that is pointed to + * by hIter, to hValue. + * This function automatically closes the Handle to the value object. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * @param hValue Handle to JSON value. + * + * @return True on success. + */ +native bool:json_object_iter_set_new(Handle:hObj, Handle:hIter, Handle:hValue); + + + + +/** + * Arrays + * + * A JSON array is an ordered collection of other JSON values. + * + */ + +/** + * Returns a handle to a new JSON array, or INVALID_HANDLE on error. + * + * @return Handle to the new JSON array + */ +native Handle:json_array(); + +/** + * Returns the number of elements in hArray + * + * @param hObj Handle to JSON array + * + * @return Number of elements in hArray, + * or 0 if hObj is not a JSON array. + */ +native json_array_size(Handle:hArray); + +/** + * Returns the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Handle to a the JSON object or + * INVALID_HANDLE on error. + */ +native Handle:json_array_get(Handle:hArray, iIndex); + +/** + * Replaces the element in array at position iIndex with hValue. + * The valid range for iIndex is from 0 to the return value of + * json_array_size() minus 1. + * + * @param hArray Handle to JSON array + * @param iIndex Position to replace + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_set(Handle:hArray, iIndex, Handle:hValue); + +/** + * Replaces the element in array at position iIndex with hValue. + * The valid range for iIndex is from 0 to the return value of + * json_array_size() minus 1. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param iIndex Position to replace + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_set_new(Handle:hArray, iIndex, Handle:hValue); + +/** + * Appends value to the end of array, growing the size of array by 1. + * + * @param hArray Handle to JSON array + * @param hValue Value to append to the array + * + * @return True on success. + */ +native bool:json_array_append(Handle:hArray, Handle:hValue); + +/** + * Appends value to the end of array, growing the size of array by 1. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param hValue Value to append to the array + * + * @return True on success. + */ +native bool:json_array_append_new(Handle:hArray, Handle:hValue); + +/** + * Inserts value to hArray at position iIndex, shifting the elements at + * iIndex and after it one position towards the end of the array. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_insert(Handle:hArray, iIndex, Handle:hValue); + +/** + * Inserts value to hArray at position iIndex, shifting the elements at + * iIndex and after it one position towards the end of the array. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_insert_new(Handle:hArray, iIndex, Handle:hValue); + +/** + * Removes the element in hArray at position iIndex, shifting the + * elements after iIndex one position towards the start of the array. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * + * @return True on success. + */ +native bool:json_array_remove(Handle:hArray, iIndex); + +/** + * Removes all elements from hArray. + * + * @param hArray Handle to JSON array + * + * @return True on success. + */ +native bool:json_array_clear(Handle:hArray); + +/** + * Appends all elements in hOther to the end of hArray. + * + * @param hArray Handle to JSON array to be extended + * @param hOther Handle to JSON array, source to copy from + * + * @return True on success. + */ +native bool:json_array_extend(Handle:hArray, Handle:hOther); + + + + +/** + * Booleans & NULL + * + */ + +/** + * Returns a handle to a new JSON Boolean with value true, + * or INVALID_HANDLE on error. + * + * @return Handle to the new Boolean object + */ +native Handle:json_true(); + +/** + * Returns a handle to a new JSON Boolean with value false, + * or INVALID_HANDLE on error. + * + * @return Handle to the new Boolean object + */ +native Handle:json_false(); + +/** + * Returns a handle to a new JSON Boolean with the value passed + * in bState or INVALID_HANDLE on error. + * + * @param bState Value for the new Boolean object + * @return Handle to the new Boolean object + */ +native Handle:json_boolean(bool:bState); + +/** + * Returns a handle to a new JSON NULL or INVALID_HANDLE on error. + * + * @return Handle to the new NULL object + */ +native Handle:json_null(); + + + + +/** + * Strings + * + * Jansson uses UTF-8 as the character encoding. All JSON strings must + * be valid UTF-8 (or ASCII, as it�s a subset of UTF-8). Normal null + * terminated C strings are used, so JSON strings may not contain + * embedded null characters. + * + */ + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * + * @param sValue Value for the new String object + * Must be a valid UTF-8 encoded Unicode string. + * @return Handle to the new String object + */ +native Handle:json_string(const String:sValue[]); + +/** + * Saves the associated value of hString as a null terminated UTF-8 + * encoded string in the passed buffer. + * + * @param hString Handle to the JSON String object + * @param sValueBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * @error Invalid JSON String Object. + * @return Length of the returned string or -1 on error. + */ +native json_string_value(Handle:hString, String:sValueBuffer[], maxlength); + +/** + * Sets the associated value of JSON String object to value. + * + * @param hString Handle to the JSON String object + * @param sValue Value to set the object to. + * Must be a valid UTF-8 encoded Unicode string. + * @error Invalid JSON String Object. + * @return True on success. + */ +native bool:json_string_set(Handle:hString, String:sValue[]); + + + + +/** + * Numbers + * + * The JSON specification only contains one numeric type, 'number'. + * The C (and Pawn) programming language has distinct types for integer + * and floating-point numbers, so for practical reasons Jansson also has + * distinct types for the two. They are called 'integer' and 'real', + * respectively. (Whereas 'real' is a 'Float' for Pawn). + * Therefore a number is represented by either a value of the type + * JSON_INTEGER or of the type JSON_REAL. + * + */ + +/** + * Returns a handle to a new JSON integer, or INVALID_HANDLE on error. + * + * @param iValue Value for the new Integer object + * @return Handle to the new Integer object + */ +native Handle:json_integer(iValue); + +/** + * Returns the associated value of a JSON Integer Object. + * + * @param hInteger Handle to the JSON Integer object + * @error Invalid JSON Integer Object. + * @return Value of the hInteger, + * or 0 if hInteger is not a JSON integer. + */ +native json_integer_value(Handle:hInteger); + +/** + * Sets the associated value of JSON Integer to value. + * + * @param hInteger Handle to the JSON Integer object + * @param iValue Value to set the object to. + * @error Invalid JSON Integer Object. + * @return True on success. + */ +native bool:json_integer_set(Handle:hInteger, iValue); + +/** + * Returns a handle to a new JSON real, or INVALID_HANDLE on error. + * + * @param fValue Value for the new Real object + * @return Handle to the new String object + */ +native Handle:json_real(Float:fValue); + +/** + * Returns the associated value of a JSON Real. + * + * @param hReal Handle to the JSON Real object + * @error Invalid JSON Real Object. + * @return Float value of hReal, + * or 0.0 if hReal is not a JSON Real. + */ +native Float:json_real_value(Handle:hReal); + +/** + * Sets the associated value of JSON Real to fValue. + * + * @param hReal Handle to the JSON Integer object + * @param fValue Value to set the object to. + * @error Invalid JSON Real handle. + * @return True on success. + */ +native bool:json_real_set(Handle:hReal, Float:value); + +/** + * Returns the associated value of a JSON integer or a + * JSON Real, cast to Float regardless of the actual type. + * + * @param hNumber Handle to the JSON Number + * @error Not a JSON Real or JSON Integer + * @return Float value of hNumber, + * or 0.0 on error. + */ +native Float:json_number_value(Handle:hNumber); + + + + +/** + * Decoding + * + * This sections describes the functions that can be used to decode JSON text + * to the Jansson representation of JSON data. The JSON specification requires + * that a JSON text is either a serialized array or object, and this + * requirement is also enforced with the following functions. In other words, + * the top level value in the JSON text being decoded must be either array or + * object. + * + */ + +/** + * Decodes the JSON string sJSON and returns the array or object it contains. + * Errors while decoding can be found in the sourcemod error log. + * + * @param sJSON String containing valid JSON + + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load(const String:sJSON[]); + +/** + * Decodes the JSON string sJSON and returns the array or object it contains. + * This function provides additional error feedback and does not log errors + * to the sourcemod error log. + * + * @param sJSON String containing valid JSON + * @param sErrorText This buffer will be filled with the error + * message. + * @param maxlen Size of the buffer + * @param iLine This int will contain the line of the error + * @param iColumn This int will contain the column of the error + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_ex(const String:sJSON[], String:sErrorText[], maxlen, &iLine, &iColumn); + +/** + * Decodes the JSON text in file sFilePath and returns the array or object + * it contains. + * Errors while decoding can be found in the sourcemod error log. + * + * @param sFilePath Path to a file containing pure JSON + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_file(const String:sFilePath[PLATFORM_MAX_PATH]); + +/** + * Decodes the JSON text in file sFilePath and returns the array or object + * it contains. + * This function provides additional error feedback and does not log errors + * to the sourcemod error log. + * + * @param sFilePath Path to a file containing pure JSON + * @param sErrorText This buffer will be filled with the error + * message. + * @param maxlen Size of the buffer + * @param iLine This int will contain the line of the error + * @param iColumn This int will contain the column of the error + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_file_ex(const String:sFilePath[PLATFORM_MAX_PATH], String:sErrorText[], maxlen, &iLine, &iColumn); + + + +/** + * Encoding + * + * This sections describes the functions that can be used to encode values + * to JSON. By default, only objects and arrays can be encoded directly, + * since they are the only valid root values of a JSON text. + * + */ + +/** + * Saves the JSON representation of hObject in sJSON. + * + * @param hObject String containing valid JSON + * @param sJSON Buffer to store the created JSON string. + * @param maxlength Maximum length of string buffer. + * @param iIndentWidth Indenting with iIndentWidth spaces. + * The valid range for this is between 0 and 31 (inclusive), + * other values result in an undefined output. If this is set + * to 0, no newlines are inserted between array and object items. + * @param bEnsureAscii If this is set, the output is guaranteed + * to consist only of ASCII characters. This is achieved + * by escaping all Unicode characters outside the ASCII range. + * @param bSortKeys If this flag is used, all the objects in output are sorted + * by key. This is useful e.g. if two JSON texts are diffed + * or visually compared. + * @param bPreserveOrder If this flag is used, object keys in the output are sorted + * into the same order in which they were first inserted to + * the object. For example, decoding a JSON text and then + * encoding with this flag preserves the order of object keys. + * @return Length of the returned string or -1 on error. + */ +native json_dump(Handle:hObject, String:sJSON[], maxlength, iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false); + +/** + * Write the JSON representation of hObject to the file sFilePath. + * If sFilePath already exists, it is overwritten. + * + * @param hObject String containing valid JSON + * @param sFilePath Buffer to store the created JSON string. + * @param iIndentWidth Indenting with iIndentWidth spaces. + * The valid range for this is between 0 and 31 (inclusive), + * other values result in an undefined output. If this is set + * to 0, no newlines are inserted between array and object items. + * @param bEnsureAscii If this is set, the output is guaranteed + * to consist only of ASCII characters. This is achieved + * by escaping all Unicode characters outside the ASCII range. + * @param bSortKeys If this flag is used, all the objects in output are sorted + * by key. This is useful e.g. if two JSON texts are diffed + * or visually compared. + * @param bPreserveOrder If this flag is used, object keys in the output are sorted + * into the same order in which they were first inserted to + * the object. For example, decoding a JSON text and then + * encoding with this flag preserves the order of object keys. + * @return Length of the returned string or -1 on error. + */ +native bool:json_dump_file(Handle:hObject, const String:sFilePath[], iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false); + + + +/** + * Convenience stocks + * + * These are some custom functions to ease the development using this + * extension. + * + */ + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * Formats the string according to the SourceMod format rules. + * The result must be a valid UTF-8 encoded Unicode string. + * + * @param sFormat Formatting rules. + * @param ... Variable number of format parameters. + * @return Handle to the new String object + */ +stock Handle:json_string_format(const String:sFormat[], any:...) { + new String:sTmp[4096]; + VFormat(sTmp, sizeof(sTmp), sFormat, 2); + + return json_string(sTmp); +} + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * This stock allows to specify the size of the temporary buffer used + * to create the string. Use this if the default of 4096 is not enough + * for your string. + * Formats the string according to the SourceMod format rules. + * The result must be a valid UTF-8 encoded Unicode string. + * + * @param tmpBufferLength Size of the temporary buffer + * @param sFormat Formatting rules. + * @param ... Variable number of format parameters. + * @return Handle to the new String object + */ +stock Handle:json_string_format_ex(tmpBufferLength, const String:sFormat[], any:...) { + new String:sTmp[tmpBufferLength]; + VFormat(sTmp, sizeof(sTmp), sFormat, 3); + + return json_string(sTmp); +} + + +/** + * Returns the boolean value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return True if it's a boolean and TRUE, + * false otherwise. + */ +stock bool:json_array_get_bool(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new bool:bResult = (json_is_true(hElement) ? true : false); + + CloseHandle(hElement); + return bResult; +} + +/** + * Returns the float value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Float value, + * or 0.0 if element is not a JSON Real. + */ +stock Float:json_array_get_float(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0); + + CloseHandle(hElement); + return fResult; +} + +/** + * Returns the integer value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Integer value, + * or 0 if element is not a JSON Integer. + */ +stock json_array_get_int(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0); + + CloseHandle(hElement); + return iResult; +} + +/** + * Saves the associated value of the element in hArray at position iIndex + * as a null terminated UTF-8 encoded string in the passed buffer. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * @param sBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * + * @error Element is not a JSON String. + * @return Length of the returned string or -1 on error. + */ +stock json_array_get_string(Handle:hArray, iIndex, String:sBuffer[], maxlength) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new iResult = -1; + if(json_is_string(hElement)) { + iResult = json_string_value(hElement, sBuffer, maxlength); + } + CloseHandle(hElement); + + return iResult; +} + +/** + * Returns the boolean value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Entry to retrieve + * + * @return True if it's a boolean and TRUE, + * false otherwise. + */ +stock bool:json_object_get_bool(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new bool:bResult = (json_is_true(hElement) ? true : false); + + CloseHandle(hElement); + return bResult; +} + +/** + * Returns the float value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Position to retrieve + * + * @return Float value, + * or 0.0 if element is not a JSON Real. + */ +stock Float:json_object_get_float(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0); + + CloseHandle(hElement); + return fResult; +} + +/** + * Returns the integer value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Position to retrieve + * + * @return Integer value, + * or 0 if element is not a JSON Integer. + */ +stock json_object_get_int(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0); + + CloseHandle(hElement); + return iResult; +} + +/** + * Saves the associated value of the element in hObj at entry sKey + * as a null terminated UTF-8 encoded string in the passed buffer. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Entry to retrieve + * @param sBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * + * @error Element is not a JSON String. + * @return Length of the returned string or -1 on error. + */ +stock json_object_get_string(Handle:hObj, const String:sKey[], String:sBuffer[], maxlength) { + new Handle:hElement = json_object_get(hObj, sKey); + + new iResult = -1; + if(json_is_string(hElement)) { + iResult = json_string_value(hElement, sBuffer, maxlength); + } + CloseHandle(hElement); + + return iResult; +} + + + +/** + * Pack String Rules + * + * Here�s the full list of format characters: + * n Output a JSON null value. No argument is consumed. + * s Output a JSON string, consuming one argument. + * b Output a JSON bool value, consuming one argument. + * i Output a JSON integer value, consuming one argument. + * f Output a JSON real value, consuming one argument. + * r Output a JSON real value, consuming one argument. + * [] Build an array with contents from the inner format string, + * recursive value building is supported. + * No argument is consumed. + * {} Build an array with contents from the inner format string. + * The first, third, etc. format character represent a key, + * and must be s (as object keys are always strings). The + * second, fourth, etc. format character represent a value. + * Recursive value building is supported. + * No argument is consumed. + * + */ + +/** + * This method can be used to create json objects/arrays directly + * without having to create the structure. + * See 'Pack String Rules' for more details. + * + * @param sPackString Pack string similiar to Format()s fmt. + * See 'Pack String Rules'. + * @param hParams ADT Array containing all keys and values + * in the order they appear in the pack string. + * + * @error Invalid pack string or pack string and + * ADT Array don't match up regarding type + * or size. + * @return Handle to JSON element. + */ +stock Handle:json_pack(const String:sPackString[], Handle:hParams) { + new iPos = 0; + return json_pack_element_(sPackString, iPos, hParams); +} + + + + + +/** +* Internal stocks used by json_pack(). Don't use these directly! +* +*/ +stock Handle:json_pack_array_(const String:sFormat[], &iPos, Handle:hParams) { + new Handle:hObj = json_array(); + new iStrLen = strlen(sFormat); + for(; iPos < iStrLen;) { + new this_char = sFormat[iPos]; + + if(this_char == 32 || this_char == 58 || this_char == 44) { + // Skip whitespace, ',' and ':' + iPos++; + continue; + } + + if(this_char == 93) { + // array end + iPos++; + break; + } + + // Get the next entry as value + // This automatically increments the position! + new Handle:hValue = json_pack_element_(sFormat, iPos, hParams); + + // Append the value to the array. + json_array_append_new(hObj, hValue); + } + + return hObj; +} + +stock Handle:json_pack_object_(const String:sFormat[], &iPos, Handle:hParams) { + new Handle:hObj = json_object(); + new iStrLen = strlen(sFormat); + for(; iPos < iStrLen;) { + new this_char = sFormat[iPos]; + + if(this_char == 32 || this_char == 58 || this_char == 44) { + // Skip whitespace, ',' and ':' + iPos++; + continue; + } + + if(this_char == 125) { + // } --> object end + iPos++; + break; + } + + if(this_char != 115) { + LogError("Object keys must be strings at %d.", iPos); + return INVALID_HANDLE; + } + + // Get the key string for this object from + // the hParams array. + decl String:sKey[255]; + GetArrayString(hParams, 0, sKey, sizeof(sKey)); + RemoveFromArray(hParams, 0); + + // Advance one character in the pack string, + // because we've just read the Key string for + // this object. + iPos++; + + // Get the next entry as value + // This automatically increments the position! + new Handle:hValue = json_pack_element_(sFormat, iPos, hParams); + + // Insert into object + json_object_set_new(hObj, sKey, hValue); + } + + return hObj; +} + +stock Handle:json_pack_element_(const String:sFormat[], &iPos, Handle:hParams) { + new this_char = sFormat[iPos]; + while(this_char == 32 || this_char == 58 || this_char == 44) { + iPos++; + this_char = sFormat[iPos]; + } + + // Advance one character in the pack string + iPos++; + + switch(this_char) { + case 91: { + // { --> Array + return json_pack_array_(sFormat, iPos, hParams); + } + + case 123: { + // { --> Object + return json_pack_object_(sFormat, iPos, hParams); + + } + + case 98: { + // b --> Boolean + new iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_boolean(bool:iValue); + } + + case 102, 114: { + // r,f --> Real (Float) + new Float:iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_real(iValue); + } + + case 110: { + // n --> NULL + return json_null(); + } + + case 115: { + // s --> String + decl String:sKey[255]; + GetArrayString(hParams, 0, sKey, sizeof(sKey)); + RemoveFromArray(hParams, 0); + + return json_string(sKey); + } + + case 105: { + // i --> Integer + new iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_integer(iValue); + } + } + + SetFailState("Invalid pack String '%s'. Type '%s' not supported at %i", sFormat, this_char, iPos); + return json_null(); +} + + + + + +/** + * Not yet implemented + * + * native json_object_foreach(Handle:hObj, ForEachCallback:cb); + * native Handle:json_unpack(const String:sFormat[], ...); + * + */ + + + + + + +/** + * Do not edit below this line! + */ +public Extension:__ext_smjansson = +{ + name = "SMJansson", + file = "smjansson.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_EXTENSIONS +public __ext_smjansson_SetNTVOptional() +{ + MarkNativeAsOptional("json_typeof"); + MarkNativeAsOptional("json_equal"); + + MarkNativeAsOptional("json_copy"); + MarkNativeAsOptional("json_deep_copy"); + + MarkNativeAsOptional("json_object"); + MarkNativeAsOptional("json_object_size"); + MarkNativeAsOptional("json_object_get"); + MarkNativeAsOptional("json_object_set"); + MarkNativeAsOptional("json_object_set_new"); + MarkNativeAsOptional("json_object_del"); + MarkNativeAsOptional("json_object_clear"); + MarkNativeAsOptional("json_object_update"); + MarkNativeAsOptional("json_object_update_existing"); + MarkNativeAsOptional("json_object_update_missing"); + + MarkNativeAsOptional("json_object_iter"); + MarkNativeAsOptional("json_object_iter_at"); + MarkNativeAsOptional("json_object_iter_next"); + MarkNativeAsOptional("json_object_iter_key"); + MarkNativeAsOptional("json_object_iter_value"); + MarkNativeAsOptional("json_object_iter_set"); + MarkNativeAsOptional("json_object_iter_set_new"); + + MarkNativeAsOptional("json_array"); + MarkNativeAsOptional("json_array_size"); + MarkNativeAsOptional("json_array_get"); + MarkNativeAsOptional("json_array_set"); + MarkNativeAsOptional("json_array_set_new"); + MarkNativeAsOptional("json_array_append"); + MarkNativeAsOptional("json_array_append_new"); + MarkNativeAsOptional("json_array_insert"); + MarkNativeAsOptional("json_array_insert_new"); + MarkNativeAsOptional("json_array_remove"); + MarkNativeAsOptional("json_array_clear"); + MarkNativeAsOptional("json_array_extend"); + + MarkNativeAsOptional("json_string"); + MarkNativeAsOptional("json_string_value"); + MarkNativeAsOptional("json_string_set"); + + MarkNativeAsOptional("json_integer"); + MarkNativeAsOptional("json_integer_value"); + MarkNativeAsOptional("json_integer_set"); + + MarkNativeAsOptional("json_real"); + MarkNativeAsOptional("json_real_value"); + MarkNativeAsOptional("json_real_set"); + MarkNativeAsOptional("json_number_value"); + + MarkNativeAsOptional("json_boolean"); + MarkNativeAsOptional("json_true"); + MarkNativeAsOptional("json_false"); + MarkNativeAsOptional("json_null"); + + MarkNativeAsOptional("json_load"); + MarkNativeAsOptional("json_load_file"); + + MarkNativeAsOptional("json_dump"); + MarkNativeAsOptional("json_dump_file"); +} +#endif diff --git a/sourcemod/scripting/include/sourcebanspp.inc b/sourcemod/scripting/include/sourcebanspp.inc new file mode 100644 index 0000000..c984bff --- /dev/null +++ b/sourcemod/scripting/include/sourcebanspp.inc @@ -0,0 +1,106 @@ +// ************************************************************************* +// This file is part of SourceBans++. +// +// Copyright (C) 2014-2019 SourceBans++ Dev Team <https://github.com/sbpp> +// +// SourceBans++ 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, per version 3 of the License. +// +// SourceBans++ 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 SourceBans++. If not, see <http://www.gnu.org/licenses/>. +// +// This file based off work(s) covered by the following copyright(s): +// +// SourceBans 1.4.11 +// Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect +// Licensed under GNU GPL version 3, or later. +// Page: <http://www.sourcebans.net/> - <https://github.com/GameConnect/sourcebansv1> +// +// ************************************************************************* + +#if defined _sourcebanspp_included +#endinput +#endif +#define _sourcebanspp_included + +public SharedPlugin __pl_sourcebanspp = +{ + name = "sourcebans++", + file = "sbpp_main.smx", + #if defined REQUIRE_PLUGIN + required = 1 + #else + required = 0 + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_sourcebanspp_SetNTVOptional() +{ + MarkNativeAsOptional("SBBanPlayer"); + MarkNativeAsOptional("SBPP_BanPlayer"); + MarkNativeAsOptional("SBPP_ReportPlayer"); +} +#endif + + +/********************************************************* + * Ban Player from server + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + * @noreturn + *********************************************************/ +#pragma deprecated Use SBPP_BanPlayer() instead. +native void SBBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Ban Player from server + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + * @noreturn + *********************************************************/ +native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Reports a player + * + * @param iReporter The client index of the reporter + * @param iTarget The client index of the player to report + * @param sReason The reason to report the player + * @noreturn + *********************************************************/ +native void SBPP_ReportPlayer(int iReporter, int iTarget, const char[] sReason); + +/********************************************************* + * Called when the admin banning the player. + * + * @param iAdmin The client index of the admin who is banning the client + * @param iTarget The client index of the player to ban + * @param iTime The time to ban the player for (in minutes, 0 = permanent) + * @param sReason The reason to ban the player from the server + *********************************************************/ +forward void SBPP_OnBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); + +/********************************************************* + * Called when a new report is inserted + * + * @param iReporter The client index of the reporter + * @param iTarget The client index of the player to report + * @param sReason The reason to report the player + * @noreturn + *********************************************************/ +forward void SBPP_OnReportPlayer(int iReporter, int iTarget, const char[] sReason); + +//Yarr! diff --git a/sourcemod/scripting/include/sourcemod-colors.inc b/sourcemod/scripting/include/sourcemod-colors.inc new file mode 100644 index 0000000..66bc97b --- /dev/null +++ b/sourcemod/scripting/include/sourcemod-colors.inc @@ -0,0 +1,921 @@ +#if defined _sourcemod_colors_included + #endinput +#endif +#define _sourcemod_colors_included "1.0" + +/* +* _____ _ _____ _ +* / ____| | | / ____| | | +* | (___ ___ _ _ _ __ ___ ___ _ __ ___ ___ __| | | | ___ | | ___ _ __ ___ +* \___ \ / _ \| | | | '__/ __/ _ \ '_ ` _ \ / _ \ / _` | | | / _ \| |/ _ \| '__/ __| +* ____) | (_) | |_| | | | (_| __/ | | | | | (_) | (_| | | |___| (_) | | (_) | | \__ \ +* |_____/ \___/ \__,_|_| \___\___|_| |_| |_|\___/ \__,_| \_____\___/|_|\___/|_| |___/ +* +* +* - Author: Keith Warren (Drixevel) +* - Original By: Raska aka KissLick (ColorVariables) +* +* This is meant to be a drop-in replacement for every Source Engine game to add colors to chat and more cheat features. +*/ + +// ---------------------------------------------------------------------------------------- +#define MAX_BUFFER_SIZE 1024 + +static bool g_bInit; +static StringMap g_hColors; +static char g_sChatPrefix[64]; + +static bool g_bIgnorePrefix; +static int g_iAuthor; +static bool g_bSkipPlayers[MAXPLAYERS + 1]; +// ---------------------------------------------------------------------------------------- + +/* +* Sets the prefix for all chat prints to use. +* +* prefix - String to use. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CSetPrefix(const char[] prefix, any ...) +{ + VFormat(g_sChatPrefix, sizeof(g_sChatPrefix), prefix, 2); +} + +/* +* Setup the next print to skip using the prefix. +* +* +* Return - N/A +*/ +stock void CSkipNextPrefix() +{ + g_bIgnorePrefix = true; +} + +/* +* Sets the author for the next print. (Mostly applies colors) +* +* client - Author index. +* +* Return - N/A +*/ +stock void CSetNextAuthor(int client) +{ + if (client < 1 || client > MaxClients || !IsClientInGame(client)) + ThrowError("Invalid client index %i", client); + + g_iAuthor = client; +} + +/* +* Setup the next chat print to not be sent to this client. +* +* client - Client index. +* +* Return - N/A +*/ +stock void CSkipNextClient(int client) +{ + if (client < 1 || client > MaxClients) + ThrowError("Invalid client index %i", client); + + g_bSkipPlayers[client] = true; +} + +/* +* Sends a chat print to the client. +* +* client - Client index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChat(int client, const char[] message, any ...) +{ + if ((client < 1 || client > MaxClients || !IsClientInGame(client) || IsFakeClient(client)) && !IsClientSourceTV(client)) + return; + + SetGlobalTransTarget(client); + + char buffer[MAX_BUFFER_SIZE]; + VFormat(buffer, sizeof(buffer), message, 3); + + AddPrefixAndDefaultColor(buffer, sizeof(buffer)); + g_bIgnorePrefix = false; + + CProcessVariables(buffer, sizeof(buffer)); + CAddWhiteSpace(buffer, sizeof(buffer)); + + SendPlayerMessage(client, buffer, g_iAuthor); + g_iAuthor = 0; +} + +/* +* Sends a chat print to the client with a specified author. +* +* client - Client index. +* author - Author index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatEx(int client, int author, const char[] message, any ...) +{ + CSetNextAuthor(author); + char buffer[MAX_BUFFER_SIZE]; + VFormat(buffer, sizeof(buffer), message, 4); + CPrintToChat(client, buffer); +} + +/* +* Sends a chat print to all clients. +* +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatAll(const char[] message, any ...) +{ + char buffer[MAX_BUFFER_SIZE]; + + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || g_bSkipPlayers[client]) + { + g_bSkipPlayers[client] = false; + continue; + } + + SetGlobalTransTarget(client); + + VFormat(buffer, sizeof(buffer), message, 2); + + AddPrefixAndDefaultColor(buffer, sizeof(buffer)); + g_bIgnorePrefix = false; + + CProcessVariables(buffer, sizeof(buffer)); + CAddWhiteSpace(buffer, sizeof(buffer)); + + SendPlayerMessage(client, buffer, g_iAuthor); + } + + g_iAuthor = 0; +} + +/* +* Sends a chat print to all clients with a specified author. +* +* author - Author index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatAllEx(int author, const char[] message, any ...) +{ + CSetNextAuthor(author); + char buffer[MAX_BUFFER_SIZE]; + VFormat(buffer, sizeof(buffer), message, 3); + CPrintToChatAll(buffer); +} + +/* +* Sends a chat print to a specified team. +* +* team - Team index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatTeam(int team, const char[] message, any ...) +{ + char buffer[MAX_BUFFER_SIZE]; + + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || GetClientTeam(client) != team || g_bSkipPlayers[client]) + { + g_bSkipPlayers[client] = false; + continue; + } + + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + AddPrefixAndDefaultColor(buffer, sizeof(buffer)); + g_bIgnorePrefix = false; + + CProcessVariables(buffer, sizeof(buffer)); + CAddWhiteSpace(buffer, sizeof(buffer)); + + SendPlayerMessage(client, buffer, g_iAuthor); + } + + g_iAuthor = 0; +} + +/* +* Sends a chat print to a specified team with a specified author. +* +* team - Team index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatTeamEx(int team, int author, const char[] message, any ...) +{ + CSetNextAuthor(author); + char buffer[MAX_BUFFER_SIZE]; + VFormat(buffer, sizeof(buffer), message, 4); + CPrintToChatTeam(team, buffer); +} + +/* +* Sends a chat print to available admins. +* Example for bitflags: (ADMFLAG_RESERVATION | ADMFLAG_GENERIC) +* +* bitflags - Bit Flags. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatAdmins(int bitflags, const char[] message, any ...) +{ + char buffer[MAX_BUFFER_SIZE]; + AdminId iAdminID; + + for (int client = 1; client <= MaxClients; client++) + { + if (!IsClientInGame(client) || g_bSkipPlayers[client]) + { + g_bSkipPlayers[client] = false; + continue; + } + + iAdminID = GetUserAdmin(client); + + if (iAdminID == INVALID_ADMIN_ID || !(GetAdminFlags(iAdminID, Access_Effective) & bitflags)) + continue; + + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + AddPrefixAndDefaultColor(buffer, sizeof(buffer)); + g_bIgnorePrefix = false; + + CProcessVariables(buffer, sizeof(buffer)); + CAddWhiteSpace(buffer, sizeof(buffer)); + + SendPlayerMessage(client, buffer, g_iAuthor); + } + + g_iAuthor = 0; +} + +/* +* Sends a chat print to available admins with a specified author. +* Example for bitflags: (ADMFLAG_RESERVATION | ADMFLAG_GENERIC) +* +* bitflags - Bit Flags. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CPrintToChatAdminsEx(int bitflags, int author, const char[] message, any ...) +{ + CSetNextAuthor(author); + char buffer[MAX_BUFFER_SIZE]; + VFormat(buffer, sizeof(buffer), message, 4); + CPrintToChatTeam(bitflags, buffer); +} + +/* +* Sends a reply message to the client. (This is useful because it works for console as well) +* +* client - Client index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CReplyToCommand(int client, const char[] message, any ...) +{ + if (client < 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (client != 0 && !IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + char buffer[MAX_BUFFER_SIZE]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + AddPrefixAndDefaultColor(buffer, sizeof(buffer), "engine 1"); + g_bIgnorePrefix = false; + + if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) + { + CRemoveColors(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } + else + CPrintToChat(client, "%s", buffer); +} + +/* +* Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. +* This version does not display a message to the originating client if used from chat triggers or menus. +* If manual replies are used for these cases, then this function will suffice. +* Otherwise, ShowActivity2() is slightly more useful. +* +* client - Client index. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CShowActivity(int client, const char[] message, any ...) +{ + if (client < 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (client != 0 && !IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + char buffer[MAX_BUFFER_SIZE]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + Format(buffer, sizeof(buffer), "{engine 1}%s", buffer); + CProcessVariables(buffer, sizeof(buffer)); + CAddWhiteSpace(buffer, sizeof(buffer)); + + ShowActivity(client, "%s", buffer); +} + +/* +* Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. +* All users receive a message in their chat text, except for the originating client, who receives the message based on the current ReplySource. +* +* client - Client index. +* tag - Tag to show. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CShowActivityEx(int client, const char[] tag, const char[] message, any ...) +{ + if (client < 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (client != 0 && !IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + char buffer[MAX_BUFFER_SIZE]; char sBufferTag[MAX_BUFFER_SIZE]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + Format(buffer, sizeof(buffer), "{engine 1}%s", buffer); + CProcessVariables(buffer, sizeof(buffer)); + Format(sBufferTag, sizeof(sBufferTag), "{prefix}%s", tag); + CProcessVariables(sBufferTag, sizeof(sBufferTag)); + CAddWhiteSpace(buffer, sizeof(buffer)); + CAddWhiteSpace(sBufferTag, sizeof(sBufferTag)); + + ShowActivityEx(client, sBufferTag, " %s", buffer); +} + +/* +* Same as ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). +* +* client - Client index. +* tag - Tag to show. +* message - Message string. +* any - Extra Parameters +* +* Return - N/A +*/ +stock void CShowActivity2(int client, const char[] tag, const char[] message, any ...) +{ + if (client < 0 || client > MaxClients) + ThrowError("Invalid client index %d", client); + + if (client != 0 && !IsClientInGame(client)) + ThrowError("Client %d is not in game", client); + + char buffer[MAX_BUFFER_SIZE]; char sBufferTag[MAX_BUFFER_SIZE]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + Format(buffer, sizeof(buffer), "{engine 2}%s", buffer); + CProcessVariables(buffer, sizeof(buffer)); + Format(sBufferTag, sizeof(sBufferTag), "{prefix}%s", tag); + CProcessVariables(sBufferTag, sizeof(sBufferTag)); + CAddWhiteSpace(buffer, sizeof(buffer)); + CAddWhiteSpace(sBufferTag, sizeof(sBufferTag)); + + ShowActivityEx(client, sBufferTag, " %s", buffer); +} + +/* +* Strips all colors from the specified string. +* +* msg - String buffer. +* size - Size of the string. +* +* Return - N/A +*/ +stock void CRemoveColors(char[] msg, int size) +{ + CProcessVariables(msg, size, true); +} + +/* +* Processes colors in a string by replacing found tags with color/hex codes. +* +* msg - String buffer. +* size - Size of the string. +* removecolors - Whether to remove colors or keep them. (same as CRemoveColors) +* +* Return - N/A +*/ +stock void CProcessVariables(char[] msg, int size, bool removecolors = false) +{ + Init(); + + char[] sOut = new char[size]; char[] sCode = new char[size]; char[] color = new char[size]; + int iOutPos = 0; int iCodePos = -1; + int iMsgLen = strlen(msg); + + for (int i = 0; i < iMsgLen; i++) + { + if (msg[i] == '{') + iCodePos = 0; + + if (iCodePos > -1) + { + sCode[iCodePos] = msg[i]; + sCode[iCodePos + 1] = '\0'; + + if (msg[i] == '}' || i == iMsgLen - 1) + { + strcopy(sCode, strlen(sCode) - 1, sCode[1]); + StringToLower(sCode); + + if (CGetColor(sCode, color, size)) + { + if (!removecolors) + { + StrCat(sOut, size, color); + iOutPos += strlen(color); + } + } + else + { + Format(sOut, size, "%s{%s}", sOut, sCode); + iOutPos += strlen(sCode) + 2; + } + + iCodePos = -1; + strcopy(sCode, size, ""); + strcopy(color, size, ""); + } + else + iCodePos++; + + continue; + } + + sOut[iOutPos] = msg[i]; + iOutPos++; + sOut[iOutPos] = '\0'; + } + + strcopy(msg, size, sOut); +} + +/* +* Retrieves the color/hex code for a specified color name. +* +* name - Color to search for. +* color - String buffer. +* size - Size of the string. +* +* Return - True if found, false otherwise. +*/ +stock bool CGetColor(const char[] name, char[] color, int size) +{ + if (name[0] == '\0') + return false; + + if (name[0] == '@') + { + int iSpace; + char sData[64]; char m_sName[64]; + strcopy(m_sName, sizeof(m_sName), name[1]); + + if ((iSpace = FindCharInString(m_sName, ' ')) != -1 && (iSpace + 1 < strlen(m_sName))) + { + strcopy(m_sName, iSpace + 1, m_sName); + strcopy(sData, sizeof(sData), m_sName[iSpace + 1]); + } + + if (color[0] != '\0') + return true; + } + else if (name[0] == '#') + { + if (strlen(name) == 7) + { + Format(color, size, "\x07%s", name[1]); + return true; + } + + if (strlen(name) == 9) + { + Format(color, size, "\x08%s", name[1]); + return true; + } + } + else if (StrContains(name, "player ", false) == 0 && strlen(name) > 7) + { + int client = StringToInt(name[7]); + + if (client < 1 || client > MaxClients || !IsClientInGame(client)) + { + strcopy(color, size, "\x01"); + LogError("Invalid client index %d", client); + return false; + } + + strcopy(color, size, "\x01"); + + switch (GetClientTeam(client)) + { + case 1: g_hColors.GetString("engine 8", color, size); + case 2: g_hColors.GetString("engine 9", color, size); + case 3: g_hColors.GetString("engine 11", color, size); + } + + return true; + } + else + return g_hColors.GetString(name, color, size); + + return false; +} + +/* +* Checks if the specified color exists. +* +* name - Color to search for. +* +* Return - True if found, false otherwise. +*/ +stock bool CExistColor(const char[] name) +{ + if (name[0] == '\0' || name[0] == '@' || name[0] == '#') + return false; + + char color[64]; + return g_hColors.GetString(name, color, sizeof(color)); +} + +/* +* Sends a raw SayText2 usermsg to the specified client with settings. +* +* client - Client index. +* message - Message string. +* author - Author index. +* chat - "0 - raw text, 1 - sets CHAT_FILTER_PUBLICCHAT " +* +* Return - N/A +*/ +stock void CSayText2(int client, const char[] message, int author, bool chat = true) +{ + if (client < 1 || client > MaxClients) + return; + + Handle hMsg = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) + { + PbSetInt(hMsg, "ent_idx", author); + PbSetBool(hMsg, "chat", chat); + PbSetString(hMsg, "msg_name", message); + PbAddString(hMsg, "params", ""); + PbAddString(hMsg, "params", ""); + PbAddString(hMsg, "params", ""); + PbAddString(hMsg, "params", ""); + } + else + { + BfWriteByte(hMsg, author); + BfWriteByte(hMsg, true); + BfWriteString(hMsg, message); + } + + EndMessage(); +} + +/* +* Adds a space to the start a string buffer. +* +* buffer - String buffer. +* size - Size of the string. +* +* Return - N/A +*/ +stock void CAddWhiteSpace(char[] buffer, int size) +{ + if (!IsSource2009()) + Format(buffer, size, " %s", buffer); +} + +// ---------------------------------------------------------------------------------------- +// Private stuff +// ---------------------------------------------------------------------------------------- + +stock bool Init() +{ + if (g_bInit) + { + LoadColors(); + return true; + } + + for (int i = 1; i <= MaxClients; i++) + g_bSkipPlayers[i] = false; + + LoadColors(); + g_bInit = true; + + return true; +} + +stock void LoadColors() +{ + if (g_hColors == null) + g_hColors = new StringMap(); + else + g_hColors.Clear(); + + g_hColors.SetString("default", "\x01"); + g_hColors.SetString("teamcolor", "\x03"); + + if (IsSource2009()) + { + g_hColors.SetString("aliceblue", "\x07F0F8FF"); + g_hColors.SetString("allies", "\x074D7942"); + g_hColors.SetString("ancient", "\x07EB4B4B"); + g_hColors.SetString("antiquewhite", "\x07FAEBD7"); + g_hColors.SetString("aqua", "\x0700FFFF"); + g_hColors.SetString("aquamarine", "\x077FFFD4"); + g_hColors.SetString("arcana", "\x07ADE55C"); + g_hColors.SetString("axis", "\x07FF4040"); + g_hColors.SetString("azure", "\x07007FFF"); + g_hColors.SetString("beige", "\x07F5F5DC"); + g_hColors.SetString("bisque", "\x07FFE4C4"); + g_hColors.SetString("black", "\x07000000"); + g_hColors.SetString("blanchedalmond", "\x07FFEBCD"); + g_hColors.SetString("blue", "\x0799CCFF"); + g_hColors.SetString("blueviolet", "\x078A2BE2"); + g_hColors.SetString("brown", "\x07A52A2A"); + g_hColors.SetString("burlywood", "\x07DEB887"); + g_hColors.SetString("cadetblue", "\x075F9EA0"); + g_hColors.SetString("chartreuse", "\x077FFF00"); + g_hColors.SetString("chocolate", "\x07D2691E"); + g_hColors.SetString("collectors", "\x07AA0000"); + g_hColors.SetString("common", "\x07B0C3D9"); + g_hColors.SetString("community", "\x0770B04A"); + g_hColors.SetString("coral", "\x07FF7F50"); + g_hColors.SetString("cornflowerblue", "\x076495ED"); + g_hColors.SetString("cornsilk", "\x07FFF8DC"); + g_hColors.SetString("corrupted", "\x07A32C2E"); + g_hColors.SetString("crimson", "\x07DC143C"); + g_hColors.SetString("cyan", "\x0700FFFF"); + g_hColors.SetString("darkblue", "\x0700008B"); + g_hColors.SetString("darkcyan", "\x07008B8B"); + g_hColors.SetString("darkgoldenrod", "\x07B8860B"); + g_hColors.SetString("darkgray", "\x07A9A9A9"); + g_hColors.SetString("darkgrey", "\x07A9A9A9"); + g_hColors.SetString("darkgreen", "\x07006400"); + g_hColors.SetString("darkkhaki", "\x07BDB76B"); + g_hColors.SetString("darkmagenta", "\x078B008B"); + g_hColors.SetString("darkolivegreen", "\x07556B2F"); + g_hColors.SetString("darkorange", "\x07FF8C00"); + g_hColors.SetString("darkorchid", "\x079932CC"); + g_hColors.SetString("darkred", "\x078B0000"); + g_hColors.SetString("darksalmon", "\x07E9967A"); + g_hColors.SetString("darkseagreen", "\x078FBC8F"); + g_hColors.SetString("darkslateblue", "\x07483D8B"); + g_hColors.SetString("darkslategray", "\x072F4F4F"); + g_hColors.SetString("darkslategrey", "\x072F4F4F"); + g_hColors.SetString("darkturquoise", "\x0700CED1"); + g_hColors.SetString("darkviolet", "\x079400D3"); + g_hColors.SetString("deeppink", "\x07FF1493"); + g_hColors.SetString("deepskyblue", "\x0700BFFF"); + g_hColors.SetString("dimgray", "\x07696969"); + g_hColors.SetString("dimgrey", "\x07696969"); + g_hColors.SetString("dodgerblue", "\x071E90FF"); + g_hColors.SetString("exalted", "\x07CCCCCD"); + g_hColors.SetString("firebrick", "\x07B22222"); + g_hColors.SetString("floralwhite", "\x07FFFAF0"); + g_hColors.SetString("forestgreen", "\x07228B22"); + g_hColors.SetString("frozen", "\x074983B3"); + g_hColors.SetString("fuchsia", "\x07FF00FF"); + g_hColors.SetString("fullblue", "\x070000FF"); + g_hColors.SetString("fullred", "\x07FF0000"); + g_hColors.SetString("gainsboro", "\x07DCDCDC"); + g_hColors.SetString("genuine", "\x074D7455"); + g_hColors.SetString("ghostwhite", "\x07F8F8FF"); + g_hColors.SetString("gold", "\x07FFD700"); + g_hColors.SetString("goldenrod", "\x07DAA520"); + g_hColors.SetString("gray", "\x07CCCCCC"); + g_hColors.SetString("grey", "\x07CCCCCC"); + g_hColors.SetString("green", "\x073EFF3E"); + g_hColors.SetString("greenyellow", "\x07ADFF2F"); + g_hColors.SetString("haunted", "\x0738F3AB"); + g_hColors.SetString("honeydew", "\x07F0FFF0"); + g_hColors.SetString("hotpink", "\x07FF69B4"); + g_hColors.SetString("immortal", "\x07E4AE33"); + g_hColors.SetString("indianred", "\x07CD5C5C"); + g_hColors.SetString("indigo", "\x074B0082"); + g_hColors.SetString("ivory", "\x07FFFFF0"); + g_hColors.SetString("khaki", "\x07F0E68C"); + g_hColors.SetString("lavender", "\x07E6E6FA"); + g_hColors.SetString("lavenderblush", "\x07FFF0F5"); + g_hColors.SetString("lawngreen", "\x077CFC00"); + g_hColors.SetString("legendary", "\x07D32CE6"); + g_hColors.SetString("lemonchiffon", "\x07FFFACD"); + g_hColors.SetString("lightblue", "\x07ADD8E6"); + g_hColors.SetString("lightcoral", "\x07F08080"); + g_hColors.SetString("lightcyan", "\x07E0FFFF"); + g_hColors.SetString("lightgoldenrodyellow", "\x07FAFAD2"); + g_hColors.SetString("lightgray", "\x07D3D3D3"); + g_hColors.SetString("lightgrey", "\x07D3D3D3"); + g_hColors.SetString("lightgreen", "\x0799FF99"); + g_hColors.SetString("lightpink", "\x07FFB6C1"); + g_hColors.SetString("lightsalmon", "\x07FFA07A"); + g_hColors.SetString("lightseagreen", "\x0720B2AA"); + g_hColors.SetString("lightskyblue", "\x0787CEFA"); + g_hColors.SetString("lightslategray", "\x07778899"); + g_hColors.SetString("lightslategrey", "\x07778899"); + g_hColors.SetString("lightsteelblue", "\x07B0C4DE"); + g_hColors.SetString("lightyellow", "\x07FFFFE0"); + g_hColors.SetString("lime", "\x0700FF00"); + g_hColors.SetString("limegreen", "\x0732CD32"); + g_hColors.SetString("linen", "\x07FAF0E6"); + g_hColors.SetString("magenta", "\x07FF00FF"); + g_hColors.SetString("maroon", "\x07800000"); + g_hColors.SetString("mediumaquamarine", "\x0766CDAA"); + g_hColors.SetString("mediumblue", "\x070000CD"); + g_hColors.SetString("mediumorchid", "\x07BA55D3"); + g_hColors.SetString("mediumpurple", "\x079370D8"); + g_hColors.SetString("mediumseagreen", "\x073CB371"); + g_hColors.SetString("mediumslateblue", "\x077B68EE"); + g_hColors.SetString("mediumspringgreen", "\x0700FA9A"); + g_hColors.SetString("mediumturquoise", "\x0748D1CC"); + g_hColors.SetString("mediumvioletred", "\x07C71585"); + g_hColors.SetString("midnightblue", "\x07191970"); + g_hColors.SetString("mintcream", "\x07F5FFFA"); + g_hColors.SetString("mistyrose", "\x07FFE4E1"); + g_hColors.SetString("moccasin", "\x07FFE4B5"); + g_hColors.SetString("mythical", "\x078847FF"); + g_hColors.SetString("navajowhite", "\x07FFDEAD"); + g_hColors.SetString("navy", "\x07000080"); + g_hColors.SetString("normal", "\x07B2B2B2"); + g_hColors.SetString("oldlace", "\x07FDF5E6"); + g_hColors.SetString("olive", "\x079EC34F"); + g_hColors.SetString("olivedrab", "\x076B8E23"); + g_hColors.SetString("orange", "\x07FFA500"); + g_hColors.SetString("orangered", "\x07FF4500"); + g_hColors.SetString("orchid", "\x07DA70D6"); + g_hColors.SetString("palegoldenrod", "\x07EEE8AA"); + g_hColors.SetString("palegreen", "\x0798FB98"); + g_hColors.SetString("paleturquoise", "\x07AFEEEE"); + g_hColors.SetString("palevioletred", "\x07D87093"); + g_hColors.SetString("papayawhip", "\x07FFEFD5"); + g_hColors.SetString("peachpuff", "\x07FFDAB9"); + g_hColors.SetString("peru", "\x07CD853F"); + g_hColors.SetString("pink", "\x07FFC0CB"); + g_hColors.SetString("plum", "\x07DDA0DD"); + g_hColors.SetString("powderblue", "\x07B0E0E6"); + g_hColors.SetString("purple", "\x07800080"); + g_hColors.SetString("rare", "\x074B69FF"); + g_hColors.SetString("red", "\x07FF4040"); + g_hColors.SetString("rosybrown", "\x07BC8F8F"); + g_hColors.SetString("royalblue", "\x074169E1"); + g_hColors.SetString("saddlebrown", "\x078B4513"); + g_hColors.SetString("salmon", "\x07FA8072"); + g_hColors.SetString("sandybrown", "\x07F4A460"); + g_hColors.SetString("seagreen", "\x072E8B57"); + g_hColors.SetString("seashell", "\x07FFF5EE"); + g_hColors.SetString("selfmade", "\x0770B04A"); + g_hColors.SetString("sienna", "\x07A0522D"); + g_hColors.SetString("silver", "\x07C0C0C0"); + g_hColors.SetString("skyblue", "\x0787CEEB"); + g_hColors.SetString("slateblue", "\x076A5ACD"); + g_hColors.SetString("slategray", "\x07708090"); + g_hColors.SetString("slategrey", "\x07708090"); + g_hColors.SetString("snow", "\x07FFFAFA"); + g_hColors.SetString("springgreen", "\x0700FF7F"); + g_hColors.SetString("steelblue", "\x074682B4"); + g_hColors.SetString("strange", "\x07CF6A32"); + g_hColors.SetString("tan", "\x07D2B48C"); + g_hColors.SetString("teal", "\x07008080"); + g_hColors.SetString("thistle", "\x07D8BFD8"); + g_hColors.SetString("tomato", "\x07FF6347"); + g_hColors.SetString("turquoise", "\x0740E0D0"); + g_hColors.SetString("uncommon", "\x07B0C3D9"); + g_hColors.SetString("unique", "\x07FFD700"); + g_hColors.SetString("unusual", "\x078650AC"); + g_hColors.SetString("valve", "\x07A50F79"); + g_hColors.SetString("vintage", "\x07476291"); + g_hColors.SetString("violet", "\x07EE82EE"); + g_hColors.SetString("wheat", "\x07F5DEB3"); + g_hColors.SetString("white", "\x07FFFFFF"); + g_hColors.SetString("whitesmoke", "\x07F5F5F5"); + g_hColors.SetString("yellow", "\x07FFFF00"); + g_hColors.SetString("yellowgreen", "\x079ACD32"); + } + else + { + g_hColors.SetString("red", "\x07"); + g_hColors.SetString("lightred", "\x0F"); + g_hColors.SetString("darkred", "\x02"); + g_hColors.SetString("bluegrey", "\x0A"); + g_hColors.SetString("blue", "\x0B"); + g_hColors.SetString("darkblue", "\x0C"); + g_hColors.SetString("purple", "\x03"); + g_hColors.SetString("orchid", "\x0E"); + g_hColors.SetString("yellow", "\x09"); + g_hColors.SetString("gold", "\x10"); + g_hColors.SetString("lightgreen", "\x05"); + g_hColors.SetString("green", "\x04"); + g_hColors.SetString("lime", "\x06"); + g_hColors.SetString("grey", "\x08"); + g_hColors.SetString("grey2", "\x0D"); + } + + g_hColors.SetString("engine 1", "\x01"); + g_hColors.SetString("engine 2", "\x02"); + g_hColors.SetString("engine 3", "\x03"); + g_hColors.SetString("engine 4", "\x04"); + g_hColors.SetString("engine 5", "\x05"); + g_hColors.SetString("engine 6", "\x06"); + g_hColors.SetString("engine 7", "\x07"); + g_hColors.SetString("engine 8", "\x08"); + g_hColors.SetString("engine 9", "\x09"); + g_hColors.SetString("engine 10", "\x0A"); + g_hColors.SetString("engine 11", "\x0B"); + g_hColors.SetString("engine 12", "\x0C"); + g_hColors.SetString("engine 13", "\x0D"); + g_hColors.SetString("engine 14", "\x0E"); + g_hColors.SetString("engine 15", "\x0F"); + g_hColors.SetString("engine 16", "\x10"); +} + +stock bool HasBrackets(const char[] sSource) +{ + return (sSource[0] == '{' && sSource[strlen(sSource) - 1] == '}'); +} + +stock void StringToLower(char[] sSource) +{ + for (int i = 0; i < strlen(sSource); i++) + { + if (sSource[i] == '\0') + break; + + sSource[i] = CharToLower(sSource[i]); + } +} + +stock bool IsSource2009() +{ + EngineVersion iEngineVersion = GetEngineVersion(); + return (iEngineVersion == Engine_CSS || iEngineVersion == Engine_TF2 || iEngineVersion == Engine_HL2DM || iEngineVersion == Engine_DODS); +} + +stock void AddPrefixAndDefaultColor(char[] message, int size, char[] sDefaultColor = "engine 1", char[] sPrefixColor = "engine 2") +{ + if (g_sChatPrefix[0] != '\0' && !g_bIgnorePrefix) + Format(message, size, "{%s}[%s]{%s} %s", sPrefixColor, g_sChatPrefix, sDefaultColor, message); + else + Format(message, size, "{%s}%s", sDefaultColor, message); +} + +stock void SendPlayerMessage(int client, char[] message, int author = 0) +{ + if (author > 0 && author <= MaxClients && IsClientInGame(author)) + CSayText2(client, message, author); + else + PrintToChat(client, message); +}
\ No newline at end of file diff --git a/sourcemod/scripting/include/updater.inc b/sourcemod/scripting/include/updater.inc new file mode 100644 index 0000000..f37bdf2 --- /dev/null +++ b/sourcemod/scripting/include/updater.inc @@ -0,0 +1,97 @@ +#if defined _updater_included + #endinput +#endif +#define _updater_included + +/** + * Adds your plugin to the updater. The URL will be updated if + * your plugin was previously added. + * + * @param url URL to your plugin's update file. + * @noreturn + */ +native Updater_AddPlugin(const String:url[]); + +/** + * Removes your plugin from the updater. This does not need to + * be called during OnPluginEnd. + * + * @noreturn + */ +native Updater_RemovePlugin(); + +/** + * Forces your plugin to be checked for updates. The behaviour + * of the update is dependant on the server's configuration. + * + * @return True if an update was triggered. False otherwise. + * @error Plugin not found in updater. + */ +native bool:Updater_ForceUpdate(); + +/** + * Called when your plugin is about to be checked for updates. + * + * @return Plugin_Handled to prevent checking, Plugin_Continue to allow it. + */ +forward Action:Updater_OnPluginChecking(); + +/** + * Called when your plugin is about to begin downloading an available update. + * + * @return Plugin_Handled to prevent downloading, Plugin_Continue to allow it. + */ +forward Action:Updater_OnPluginDownloading(); + +/** + * Called when your plugin's update files have been fully downloaded + * and are about to write to their proper location. This should be used + * to free read-only resources that require write access for your update. + * + * @note OnPluginUpdated will be called later during the same frame. + * + * @noreturn + */ +forward Updater_OnPluginUpdating(); + +/** + * Called when your plugin's update has been completed. It is safe + * to reload your plugin at this time. + * + * @noreturn + */ +forward Updater_OnPluginUpdated(); + +/** + * @brief Reloads a plugin. + * + * @param plugin Plugin Handle (INVALID_HANDLE uses the calling plugin). + * @noreturn + */ +stock ReloadPlugin(Handle:plugin=INVALID_HANDLE) +{ + decl String:filename[64]; + GetPluginFilename(plugin, filename, sizeof(filename)); + ServerCommand("sm plugins reload %s", filename); +} + + +public SharedPlugin:__pl_updater = +{ + name = "updater", + file = "updater.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_PLUGIN +public __pl_updater_SetNTVOptional() +{ + MarkNativeAsOptional("Updater_AddPlugin"); + MarkNativeAsOptional("Updater_RemovePlugin"); + MarkNativeAsOptional("Updater_ForceUpdate"); +} +#endif diff --git a/sourcemod/scripting/momsurffix/baseplayer.sp b/sourcemod/scripting/momsurffix/baseplayer.sp new file mode 100644 index 0000000..c638773 --- /dev/null +++ b/sourcemod/scripting/momsurffix/baseplayer.sp @@ -0,0 +1,189 @@ +#define MAX_EDICT_BITS 11 +#define NUM_ENT_ENTRY_BITS (MAX_EDICT_BITS + 1) +#define NUM_ENT_ENTRIES (1 << NUM_ENT_ENTRY_BITS) +#define ENT_ENTRY_MASK (NUM_ENT_ENTRIES - 1) +#define INVALID_EHANDLE_INDEX 0xFFFFFFFF + +enum struct CBasePlayerOffsets +{ + //... + int m_surfaceFriction; + //... + int m_hGroundEntity; + //... + int m_MoveType; + //... +} + +enum struct CBaseHandleOffsets +{ + int m_Index; +} + +enum struct CEntInfoOffsets +{ + int m_pEntity; + int m_SerialNumber; + //... + int size; +} + +enum struct CBaseEntityListOffsets +{ + int m_EntPtrArray; +} + +enum struct BasePlayerOffsets +{ + CBasePlayerOffsets cbpoffsets; + CBaseHandleOffsets cbhoffsets; + CEntInfoOffsets ceioffsets; + CBaseEntityListOffsets cbeloffsets; +} +static BasePlayerOffsets offsets; + +methodmap CBasePlayer < AddressBase +{ + property float m_surfaceFriction + { + public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_surfaceFriction, NumberType_Int32)); } + } + + //... + + property Address m_hGroundEntity + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_hGroundEntity, NumberType_Int32)); } + } + + //... + + property MoveType m_MoveType + { + public get() { return view_as<MoveType>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_MoveType, NumberType_Int8)); } + } +} + +methodmap CBaseEntityList < AddressBase +{ + property PseudoStackArray m_EntPtrArray + { + public get() { return view_as<PseudoStackArray>(LoadFromAddress(this.Address + offsets.cbeloffsets.m_EntPtrArray, NumberType_Int32)); } + } +} + +static CBaseEntityList g_pEntityList; + +methodmap CBaseHandle < AddressBase +{ + property int m_Index + { + public get() { return LoadFromAddress(this.Address + offsets.cbhoffsets.m_Index, NumberType_Int32); } + } + + public CBaseHandle Get() + { + return LookupEntity(this); + } + + public int GetEntryIndex() + { + return + } +} + +methodmap CEntInfo < AddressBase +{ + public static int Size() + { + return offsets.ceioffsets.size; + } + + property CBaseHandle m_pEntity + { + public get() { return view_as<CBaseHandle>(LoadFromAddress(this.Address + offsets.ceioffsets.m_pEntity, NumberType_Int32)); } + } + + property int m_SerialNumber + { + public get() { return LoadFromAddress(this.Address + offsets.ceioffsets.m_SerialNumber, NumberType_Int32); } + } +} + +stock bool InitBasePlayer(GameData gd) +{ + char buff[128]; + bool early = false; + + if(gEngineVersion == Engine_CSS) + { + //g_pEntityList + g_pEntityList = view_as<CBaseEntityList>(gd.GetAddress("g_pEntityList")); + ASSERT_MSG(g_pEntityList.Address != Address_Null, "Can't get \"g_pEntityList\" address from gamedata. Gamedata needs an update."); + + //CBaseEntityList + ASSERT_FMT(gd.GetKeyValue("CBaseEntityList::m_EntPtrArray", buff, sizeof(buff)), "Can't get \"CBaseEntityList::m_EntPtrArray\" offset from gamedata."); + offsets.cbeloffsets.m_EntPtrArray = StringToInt(buff); + + //CEntInfo + ASSERT_FMT(gd.GetKeyValue("CEntInfo::m_pEntity", buff, sizeof(buff)), "Can't get \"CEntInfo::m_pEntity\" offset from gamedata."); + offsets.ceioffsets.m_pEntity = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CEntInfo::m_SerialNumber", buff, sizeof(buff)), "Can't get \"CEntInfo::m_SerialNumber\" offset from gamedata."); + offsets.ceioffsets.m_SerialNumber = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CEntInfo::size", buff, sizeof(buff)), "Can't get \"CEntInfo::size\" offset from gamedata."); + offsets.ceioffsets.size = StringToInt(buff); + + //CBaseHandle + ASSERT_FMT(gd.GetKeyValue("CBaseHandle::m_Index", buff, sizeof(buff)), "Can't get \"CBaseHandle::m_Index\" offset from gamedata."); + offsets.cbhoffsets.m_Index = StringToInt(buff); + + //CBasePlayer + ASSERT_FMT(gd.GetKeyValue("CBasePlayer::m_surfaceFriction", buff, sizeof(buff)), "Can't get \"CBasePlayer::m_surfaceFriction\" offset from gamedata."); + int offs = StringToInt(buff); + int prop_offs = FindSendPropInfo("CBasePlayer", "m_szLastPlaceName"); + ASSERT_FMT(prop_offs > 0, "Can't get \"CBasePlayer::m_szLastPlaceName\" offset from FindSendPropInfo()."); + offsets.cbpoffsets.m_surfaceFriction = prop_offs + offs; + } + else if(gEngineVersion == Engine_CSGO) + { + //CBasePlayer + ASSERT_FMT(gd.GetKeyValue("CBasePlayer::m_surfaceFriction", buff, sizeof(buff)), "Can't get \"CBasePlayer::m_surfaceFriction\" offset from gamedata."); + int offs = StringToInt(buff); + int prop_offs = FindSendPropInfo("CBasePlayer", "m_ubEFNoInterpParity"); + ASSERT_FMT(prop_offs > 0, "Can't get \"CBasePlayer::m_ubEFNoInterpParity\" offset from FindSendPropInfo()."); + offsets.cbpoffsets.m_surfaceFriction = prop_offs - offs; + } + + offsets.cbpoffsets.m_hGroundEntity = FindSendPropInfo("CBasePlayer", "m_hGroundEntity"); + ASSERT_FMT(offsets.cbpoffsets.m_hGroundEntity > 0, "Can't get \"CBasePlayer::m_hGroundEntity\" offset from FindSendPropInfo()."); + + if(IsValidEntity(0)) + { + offsets.cbpoffsets.m_MoveType = FindDataMapInfo(0, "m_MoveType"); + ASSERT_FMT(offsets.cbpoffsets.m_MoveType != -1, "Can't get \"CBasePlayer::m_MoveType\" offset from FindDataMapInfo()."); + } + else + early = true; + + return early; +} + +stock void LateInitBasePlayer(GameData gd) +{ + ASSERT(IsValidEntity(0)); + offsets.cbpoffsets.m_MoveType = FindDataMapInfo(0, "m_MoveType"); + ASSERT_FMT(offsets.cbpoffsets.m_MoveType != -1, "Can't get \"CBasePlayer::m_MoveType\" offset from FindDataMapInfo()."); +} + +stock CBaseHandle LookupEntity(CBaseHandle handle) +{ + if(handle.m_Index == INVALID_EHANDLE_INDEX) + return view_as<CBaseHandle>(0); + + CEntInfo pInfo = view_as<CEntInfo>(g_pEntityList.m_EntPtrArray.Get32(handle.m_Index & ENT_ENTRY_MASK, CEntInfo.Size())); + + if(pInfo.m_SerialNumber == (handle.m_Index >> NUM_ENT_ENTRY_BITS)) + return pInfo.m_pEntity; + else + return view_as<CBaseHandle>(0); +}
\ No newline at end of file diff --git a/sourcemod/scripting/momsurffix/gamemovement.sp b/sourcemod/scripting/momsurffix/gamemovement.sp new file mode 100644 index 0000000..a51beeb --- /dev/null +++ b/sourcemod/scripting/momsurffix/gamemovement.sp @@ -0,0 +1,411 @@ +enum struct CGameMovementOffsets +{ + int player; + int mv; + //... + int m_pTraceListData; + int m_nTraceCount; +} + +enum struct CMoveDataOffsets +{ + int m_nPlayerHandle; + //... + int m_vecVelocity; + //... + int m_vecAbsOrigin; +} + +enum struct GameMoventOffsets +{ + CGameMovementOffsets cgmoffsets; + CMoveDataOffsets cmdoffsets; +} +static GameMoventOffsets offsets; + +methodmap CMoveData < AddressBase +{ + property CBaseHandle m_nPlayerHandle + { + public get() { return view_as<CBaseHandle>(this.Address + offsets.cmdoffsets.m_nPlayerHandle); } + } + + //... + + property Vector m_vecVelocity + { + public get() { return view_as<Vector>(this.Address + offsets.cmdoffsets.m_vecVelocity); } + } + + //... + + property Vector m_vecAbsOrigin + { + public get() { return view_as<Vector>(this.Address + offsets.cmdoffsets.m_vecAbsOrigin); } + } +} + +methodmap CGameMovement < AddressBase +{ + property CBasePlayer player + { + public get() { return view_as<CBasePlayer>(LoadFromAddress(this.Address + offsets.cgmoffsets.player, NumberType_Int32)); } + } + + property CMoveData mv + { + public get() { return view_as<CMoveData>(LoadFromAddress(this.Address + offsets.cgmoffsets.mv, NumberType_Int32)); } + } + + //... + + property ITraceListData m_pTraceListData + { + public get() { return view_as<ITraceListData>(LoadFromAddress(this.Address + offsets.cgmoffsets.m_pTraceListData, NumberType_Int32)); } + } + + property int m_nTraceCount + { + public get() { return LoadFromAddress(this.Address + offsets.cgmoffsets.m_nTraceCount, NumberType_Int32); } + public set(int _tracecount) { StoreToAddress(this.Address + offsets.cgmoffsets.m_nTraceCount, _tracecount, NumberType_Int32, false); } + } +} + +static Handle gAddToTouched; + +methodmap IMoveHelper < AddressBase +{ + public bool AddToTouched(CGameTrace trace, Vector vec) + { + return SDKCall(gAddToTouched, this.Address, trace, vec); + } +} + +enum //Collision_Group_t +{ + COLLISION_GROUP_NONE = 0, + COLLISION_GROUP_DEBRIS, // Collides with nothing but world and static stuff + COLLISION_GROUP_DEBRIS_TRIGGER, // Same as debris, but hits triggers + COLLISION_GROUP_INTERACTIVE_DEBRIS, // Collides with everything except other interactive debris or debris + COLLISION_GROUP_INTERACTIVE, // Collides with everything except interactive debris or debris + COLLISION_GROUP_PLAYER, + COLLISION_GROUP_BREAKABLE_GLASS, + COLLISION_GROUP_VEHICLE, + COLLISION_GROUP_PLAYER_MOVEMENT, // For HL2, same as Collision_Group_Player, for + // TF2, this filters out other players and CBaseObjects + COLLISION_GROUP_NPC, // Generic NPC group + COLLISION_GROUP_IN_VEHICLE, // for any entity inside a vehicle + COLLISION_GROUP_WEAPON, // for any weapons that need collision detection + COLLISION_GROUP_VEHICLE_CLIP, // vehicle clip brush to restrict vehicle movement + COLLISION_GROUP_PROJECTILE, // Projectiles! + COLLISION_GROUP_DOOR_BLOCKER, // Blocks entities not permitted to get near moving doors + COLLISION_GROUP_PASSABLE_DOOR, // Doors that the player shouldn't collide with + COLLISION_GROUP_DISSOLVING, // Things that are dissolving are in this group + COLLISION_GROUP_PUSHAWAY, // Nonsolid on client and server, pushaway in player code + + COLLISION_GROUP_NPC_ACTOR, // Used so NPCs in scripts ignore the player. + COLLISION_GROUP_NPC_SCRIPTED, // USed for NPCs in scripts that should not collide with each other + + LAST_SHARED_COLLISION_GROUP +}; + +static Handle gClipVelocity, gLockTraceFilter, gUnlockTraceFilter, gGetPlayerMins, gGetPlayerMaxs, gTracePlayerBBox; +static IMoveHelper sm_pSingleton; + +stock void InitGameMovement(GameData gd) +{ + char buff[128]; + + //CGameMovement + ASSERT_FMT(gd.GetKeyValue("CGameMovement::player", buff, sizeof(buff)), "Can't get \"CGameMovement::player\" offset from gamedata."); + offsets.cgmoffsets.player = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameMovement::mv", buff, sizeof(buff)), "Can't get \"CGameMovement::mv\" offset from gamedata."); + offsets.cgmoffsets.mv = StringToInt(buff); + + if(gEngineVersion == Engine_CSGO) + { + ASSERT_FMT(gd.GetKeyValue("CGameMovement::m_pTraceListData", buff, sizeof(buff)), "Can't get \"CGameMovement::m_pTraceListData\" offset from gamedata."); + offsets.cgmoffsets.m_pTraceListData = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameMovement::m_nTraceCount", buff, sizeof(buff)), "Can't get \"CGameMovement::m_nTraceCount\" offset from gamedata."); + offsets.cgmoffsets.m_nTraceCount = StringToInt(buff); + } + + //CMoveData + if(gEngineVersion == Engine_CSS) + { + ASSERT_FMT(gd.GetKeyValue("CMoveData::m_nPlayerHandle", buff, sizeof(buff)), "Can't get \"CMoveData::m_nPlayerHandle\" offset from gamedata."); + offsets.cmdoffsets.m_nPlayerHandle = StringToInt(buff); + } + + ASSERT_FMT(gd.GetKeyValue("CMoveData::m_vecVelocity", buff, sizeof(buff)), "Can't get \"CMoveData::m_vecVelocity\" offset from gamedata."); + offsets.cmdoffsets.m_vecVelocity = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CMoveData::m_vecAbsOrigin", buff, sizeof(buff)), "Can't get \"CMoveData::m_vecAbsOrigin\" offset from gamedata."); + offsets.cmdoffsets.m_vecAbsOrigin = StringToInt(buff); + + if(gEngineVersion == Engine_CSGO) + { + //sm_pSingleton + sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton")); + ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from gamedata. Gamedata needs an update."); + } + else + { + //sm_pSingleton for late loading + sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton")); + + //CMoveHelperServer::CMoveHelperServer + Handle dhook = DHookCreateDetour(Address_Null, CallConv_CDECL, ReturnType_Int, ThisPointer_Ignore); + ASSERT_MSG(DHookSetFromConf(dhook, gd, SDKConf_Signature, "CMoveHelperServer::CMoveHelperServer"), "Failed to get \"CMoveHelperServer::CMoveHelperServer\" signature. Gamedata needs an update."); + DHookAddParam(dhook, HookParamType_Int); + DHookEnableDetour(dhook, true, CMoveHelperServer_Dhook); + } + + //AddToTouched + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("AddToTouched")); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + + gAddToTouched = EndPrepSDKCall(); + ASSERT(gAddToTouched); + + if(gEngineVersion == Engine_CSGO) + { + //ClipVelocity + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("ClipVelocity")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gClipVelocity = EndPrepSDKCall(); + ASSERT(gClipVelocity); + + //LockTraceFilter + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("LockTraceFilter")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gLockTraceFilter = EndPrepSDKCall(); + ASSERT(gLockTraceFilter); + + //UnlockTraceFilter + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("UnlockTraceFilter")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer); + + gUnlockTraceFilter = EndPrepSDKCall(); + ASSERT(gUnlockTraceFilter); + } + else if(gEngineVersion == Engine_CSS && gOSType == OSLinux) + { + //ClipVelocity + StartPrepSDKCall(SDKCall_Static); + + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::ClipVelocity"), "Failed to get \"CGameMovement::ClipVelocity\" signature. Gamedata needs an update."); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gClipVelocity = EndPrepSDKCall(); + ASSERT(gClipVelocity); + } + + if(gEngineVersion == Engine_CSGO || gOSType == OSWindows) + { + //GetPlayerMins + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("GetPlayerMins")); + + if(gEngineVersion == Engine_CSS) + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gGetPlayerMins = EndPrepSDKCall(); + ASSERT(gGetPlayerMins); + + //GetPlayerMaxs + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("GetPlayerMaxs")); + + if(gEngineVersion == Engine_CSS) + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gGetPlayerMaxs = EndPrepSDKCall(); + ASSERT(gGetPlayerMaxs); + } + else + { + //GetPlayerMins + StartPrepSDKCall(SDKCall_Static); + + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::GetPlayerMins"), "Failed to get \"CGameMovement::GetPlayerMins\" signature. Gamedata needs an update."); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gGetPlayerMins = EndPrepSDKCall(); + ASSERT(gGetPlayerMins); + + //GetPlayerMaxs + StartPrepSDKCall(SDKCall_Static); + + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::GetPlayerMaxs"), "Failed to get \"CGameMovement::GetPlayerMaxs\" signature. Gamedata needs an update."); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gGetPlayerMaxs = EndPrepSDKCall(); + ASSERT(gGetPlayerMaxs); + } + + //TracePlayerBBox + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("TracePlayerBBox")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gTracePlayerBBox = EndPrepSDKCall(); + ASSERT(gTracePlayerBBox); +} + +public MRESReturn CMoveHelperServer_Dhook(Handle hReturn, Handle hParams) +{ + if(sm_pSingleton.Address == Address_Null) + { + if(gOSType == OSLinux) + { + GameData gd = new GameData(GAME_DATA_FILE); + + sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton")); + ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from gamedata. Gamedata needs an update."); + + delete gd; + } + else + { + sm_pSingleton = view_as<IMoveHelper>(DHookGetReturn(hReturn)); + ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from \"CMoveHelperServer::CMoveHelperServer\" dhook."); + } + } + + return MRES_Ignored; +} + +stock void TracePlayerBBox(CGameMovement pThis, Vector start, Vector end, int mask, int collisionGroup, CGameTrace trace) +{ + SDKCall(gTracePlayerBBox, pThis, start, end, mask, collisionGroup, trace); +} + +stock CTraceFilterSimple LockTraceFilter(CGameMovement pThis, int collisionGroup) +{ + ASSERT(pThis.Address != Address_Null); + return SDKCall(gLockTraceFilter, pThis.Address, collisionGroup); +} + +stock void UnlockTraceFilter(CGameMovement pThis, CTraceFilterSimple filter) +{ + ASSERT(pThis.Address != Address_Null); + SDKCall(gUnlockTraceFilter, pThis.Address, filter.Address); +} + +stock int ClipVelocity(CGameMovement pThis, Vector invec, Vector normal, Vector out, float overbounce) +{ + if(gEngineVersion == Engine_CSGO) + { + ASSERT(pThis.Address != Address_Null); + return SDKCall(gClipVelocity, pThis.Address, invec.Address, normal.Address, out.Address, overbounce); + } + else if (gEngineVersion == Engine_CSS && gOSType == OSLinux) + { + return SDKCall(gClipVelocity, pThis.Address, invec.Address, normal.Address, out.Address, overbounce); + } + else + { + float backoff, angle, adjust; + int blocked; + + angle = normal.z; + + if(angle > 0.0) + blocked |= 0x01; + if(CloseEnoughFloat(angle, 0.0)) + blocked |= 0x02; + + backoff = invec.Dot(VectorToArray(normal)) * overbounce; + + out.x = invec.x - (normal.x * backoff); + out.y = invec.y - (normal.y * backoff); + out.z = invec.z - (normal.z * backoff); + + adjust = out.Dot(VectorToArray(normal)); + if(adjust < 0.0) + { + out.x -= (normal.x * adjust); + out.y -= (normal.y * adjust); + out.z -= (normal.z * adjust); + } + + return blocked; + } +} + +stock Vector GetPlayerMinsCSS(CGameMovement pThis, Vector vec) +{ + if(gOSType == OSLinux) + { + SDKCall(gGetPlayerMins, vec.Address, pThis.Address); + return vec; + } + else + return SDKCall(gGetPlayerMins, pThis.Address, vec.Address); +} + +stock Vector GetPlayerMaxsCSS(CGameMovement pThis, Vector vec) +{ + if(gOSType == OSLinux) + { + SDKCall(gGetPlayerMaxs, vec.Address, pThis.Address); + return vec; + } + else + return SDKCall(gGetPlayerMaxs, pThis.Address, vec.Address); +} + +stock Vector GetPlayerMins(CGameMovement pThis) +{ + return SDKCall(gGetPlayerMins, pThis.Address); +} + +stock Vector GetPlayerMaxs(CGameMovement pThis) +{ + return SDKCall(gGetPlayerMaxs, pThis.Address); +} + +stock IMoveHelper MoveHelper() +{ + return sm_pSingleton; +}
\ No newline at end of file diff --git a/sourcemod/scripting/momsurffix/gametrace.sp b/sourcemod/scripting/momsurffix/gametrace.sp new file mode 100644 index 0000000..e8db1ad --- /dev/null +++ b/sourcemod/scripting/momsurffix/gametrace.sp @@ -0,0 +1,480 @@ +enum struct cplane_tOffsets +{ + int normal; + int dist; + int type; + int signbits; +} + +enum struct csurface_tOffsets +{ + int name; + int surfaceProps; + int flags; +} + +enum struct CGameTraceOffsets +{ + //CBaseTrace + int startpos; + int endpos; + int plane; + int fraction; + int contents; + int dispFlags; + int allsolid; + int startsolid; + //CGameTrace + int fractionleftsolid; + int surface; + int hitgroup; + int physicsbone; + int m_pEnt; + int hitbox; + int size; +} + +enum struct Ray_tOffsets +{ + int m_Start; + int m_Delta; + int m_StartOffset; + int m_Extents; + int m_pWorldAxisTransform; + int m_IsRay; + int m_IsSwept; + int size; +} + +enum struct CTraceFilterSimpleOffsets +{ + int vptr; + int m_pPassEnt; + int m_collisionGroup; + int m_pExtraShouldHitCheckFunction; + int size; + Address vtable; +} + +enum struct GameTraceOffsets +{ + cplane_tOffsets cptoffsets; + csurface_tOffsets cstoffsets; + CGameTraceOffsets cgtoffsets; + Ray_tOffsets rtoffsets; + CTraceFilterSimpleOffsets ctfsoffsets; +} +static GameTraceOffsets offsets; + +methodmap Cplane_t < AddressBase +{ + property Vector normal + { + public get() { return view_as<Vector>(this.Address + offsets.cptoffsets.normal); } + } + + property float dist + { + public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cptoffsets.dist, NumberType_Int32)); } + } + + property char type + { + public get() { return view_as<char>(LoadFromAddress(this.Address + offsets.cptoffsets.type, NumberType_Int8)); } + } + + property char signbits + { + public get() { return view_as<char>(LoadFromAddress(this.Address + offsets.cptoffsets.signbits, NumberType_Int8)); } + } +} + +methodmap Csurface_t < AddressBase +{ + property Address name + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cstoffsets.name, NumberType_Int32)); } + } + + property int surfaceProps + { + public get() { return LoadFromAddress(this.Address + offsets.cstoffsets.surfaceProps, NumberType_Int16); } + } + + property int flags + { + public get() { return LoadFromAddress(this.Address + offsets.cstoffsets.flags, NumberType_Int16); } + } +} + +methodmap CGameTrace < AllocatableBase +{ + public static int Size() + { + return offsets.cgtoffsets.size; + } + + property Vector startpos + { + public get() { return view_as<Vector>(this.Address + offsets.cgtoffsets.startpos); } + } + + property Vector endpos + { + public get() { return view_as<Vector>(this.Address + offsets.cgtoffsets.endpos); } + } + + property Cplane_t plane + { + public get() { return view_as<Cplane_t>(this.Address + offsets.cgtoffsets.plane); } + } + + property float fraction + { + public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cgtoffsets.fraction, NumberType_Int32)); } + } + + property int contents + { + public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.contents, NumberType_Int32); } + } + + property int dispFlags + { + public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.dispFlags, NumberType_Int16); } + } + + property bool allsolid + { + public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.cgtoffsets.allsolid, NumberType_Int8)); } + } + + property bool startsolid + { + public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.cgtoffsets.startsolid, NumberType_Int8)); } + } + + property float fractionleftsolid + { + public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cgtoffsets.fractionleftsolid, NumberType_Int32)); } + } + + property Csurface_t surface + { + public get() { return view_as<Csurface_t>(this.Address + offsets.cgtoffsets.surface); } + } + + property int hitgroup + { + public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.hitgroup, NumberType_Int32); } + } + + property int physicsbone + { + public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.physicsbone, NumberType_Int16); } + } + + property Address m_pEnt + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cgtoffsets.m_pEnt, NumberType_Int32)); } + } + + property int hitbox + { + public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.hitbox, NumberType_Int32); } + } + + public CGameTrace() + { + return MALLOC(CGameTrace); + } +} + +methodmap Ray_t < AllocatableBase +{ + public static int Size() + { + return offsets.rtoffsets.size; + } + + property Vector m_Start + { + public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Start); } + } + + property Vector m_Delta + { + public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Delta); } + } + + property Vector m_StartOffset + { + public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_StartOffset); } + } + + property Vector m_Extents + { + public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Extents); } + } + + property Address m_pWorldAxisTransform + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.rtoffsets.m_pWorldAxisTransform, NumberType_Int32)); } + public set(Address _worldaxistransform) { StoreToAddress(this.Address + offsets.rtoffsets.m_pWorldAxisTransform, view_as<int>(_worldaxistransform), NumberType_Int32, false); } + } + + property bool m_IsRay + { + public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.rtoffsets.m_IsRay, NumberType_Int8)); } + public set(bool _isray) { StoreToAddress(this.Address + offsets.rtoffsets.m_IsRay, _isray, NumberType_Int8, false); } + } + + property bool m_IsSwept + { + public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.rtoffsets.m_IsSwept, NumberType_Int8)); } + public set(bool _isswept) { StoreToAddress(this.Address + offsets.rtoffsets.m_IsSwept, _isswept, NumberType_Int8, false); } + } + + public Ray_t() + { + return MALLOC(Ray_t); + } + + //That function is quite heavy, linux builds have it inlined, so can't use it! + //Replacing this function with lighter alternative may increase speed by ~4 times! + //From my testings the main performance killer here is StoreToAddress().... + public void Init(float start[3], float end[3], float mins[3], float maxs[3]) + { + float buff[3], buff2[3]; + + SubtractVectors(end, start, buff); + this.m_Delta.FromArray(buff); + + if(gEngineVersion == Engine_CSGO) + this.m_pWorldAxisTransform = Address_Null; + this.m_IsSwept = (this.m_Delta.LengthSqr() != 0.0); + + SubtractVectors(maxs, mins, buff); + ScaleVector(buff, 0.5); + this.m_Extents.FromArray(buff); + + this.m_IsRay = (this.m_Extents.LengthSqr() < 1.0e-6); + + AddVectors(mins, maxs, buff); + ScaleVector(buff, 0.5); + AddVectors(start, buff, buff2); + this.m_Start.FromArray(buff2); + NegateVector(buff); + this.m_StartOffset.FromArray(buff); + } +} + +methodmap CTraceFilterSimple < AllocatableBase +{ + public static int Size() + { + return offsets.ctfsoffsets.size; + } + + property Address vptr + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.ctfsoffsets.vptr, NumberType_Int32)); } + public set(Address _vtbladdr) { StoreToAddress(this.Address + offsets.ctfsoffsets.vptr, view_as<int>(_vtbladdr), NumberType_Int32, false); } + } + + property CBaseHandle m_pPassEnt + { + public get() { return view_as<CBaseHandle>(LoadFromAddress(this.Address + offsets.ctfsoffsets.m_pPassEnt, NumberType_Int32)); } + public set(CBaseHandle _passent) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_pPassEnt, view_as<int>(_passent), NumberType_Int32, false); } + } + + property int m_collisionGroup + { + public get() { return LoadFromAddress(this.Address + offsets.ctfsoffsets.m_collisionGroup, NumberType_Int32); } + public set(int _collisiongroup) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_collisionGroup, _collisiongroup, NumberType_Int32, false); } + } + + property Address m_pExtraShouldHitCheckFunction + { + public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction, NumberType_Int32)); } + public set(Address _checkfnc) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction, view_as<int>(_checkfnc), NumberType_Int32, false); } + } + + public CTraceFilterSimple() + { + CTraceFilterSimple addr = MALLOC(CTraceFilterSimple); + addr.vptr = offsets.ctfsoffsets.vtable; + return addr; + } + + public void Init(CBaseHandle passentity, int collisionGroup, Address pExtraShouldHitCheckFn = Address_Null) + { + this.m_pPassEnt = passentity; + this.m_collisionGroup = collisionGroup; + this.m_pExtraShouldHitCheckFunction = pExtraShouldHitCheckFn; + } +} + +static Handle gCanTraceRay; + +methodmap ITraceListData < AddressBase +{ + public bool CanTraceRay(Ray_t ray) + { + return SDKCall(gCanTraceRay, this.Address, ray.Address); + } +} + +static Handle gTraceRay, gTraceRayAgainstLeafAndEntityList; +static Address gEngineTrace; + +stock void InitGameTrace(GameData gd) +{ + char buff[128]; + + //cplane_t + ASSERT_FMT(gd.GetKeyValue("cplane_t::normal", buff, sizeof(buff)), "Can't get \"cplane_t::normal\" offset from gamedata."); + offsets.cptoffsets.normal = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("cplane_t::dist", buff, sizeof(buff)), "Can't get \"cplane_t::dist\" offset from gamedata."); + offsets.cptoffsets.dist = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("cplane_t::type", buff, sizeof(buff)), "Can't get \"cplane_t::type\" offset from gamedata."); + offsets.cptoffsets.type = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("cplane_t::signbits", buff, sizeof(buff)), "Can't get \"cplane_t::signbits\" offset from gamedata."); + offsets.cptoffsets.signbits = StringToInt(buff); + + //csurface_t + ASSERT_FMT(gd.GetKeyValue("csurface_t::name", buff, sizeof(buff)), "Can't get \"csurface_t::name\" offset from gamedata."); + offsets.cstoffsets.name = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("csurface_t::surfaceProps", buff, sizeof(buff)), "Can't get \"csurface_t::surfaceProps\" offset from gamedata."); + offsets.cstoffsets.surfaceProps = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("csurface_t::flags", buff, sizeof(buff)), "Can't get \"csurface_t::flags\" offset from gamedata."); + offsets.cstoffsets.flags = StringToInt(buff); + + //CGameTrace + ASSERT_FMT(gd.GetKeyValue("CGameTrace::startpos", buff, sizeof(buff)), "Can't get \"CGameTrace::startpos\" offset from gamedata."); + offsets.cgtoffsets.startpos = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::endpos", buff, sizeof(buff)), "Can't get \"CGameTrace::endpos\" offset from gamedata."); + offsets.cgtoffsets.endpos = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::plane", buff, sizeof(buff)), "Can't get \"CGameTrace::plane\" offset from gamedata."); + offsets.cgtoffsets.plane = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::fraction", buff, sizeof(buff)), "Can't get \"CGameTrace::fraction\" offset from gamedata."); + offsets.cgtoffsets.fraction = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::contents", buff, sizeof(buff)), "Can't get \"CGameTrace::contents\" offset from gamedata."); + offsets.cgtoffsets.contents = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::dispFlags", buff, sizeof(buff)), "Can't get \"CGameTrace::dispFlags\" offset from gamedata."); + offsets.cgtoffsets.dispFlags = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::allsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::allsolid\" offset from gamedata."); + offsets.cgtoffsets.allsolid = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::startsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::startsolid\" offset from gamedata."); + offsets.cgtoffsets.startsolid = StringToInt(buff); + + ASSERT_FMT(gd.GetKeyValue("CGameTrace::fractionleftsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::fractionleftsolid\" offset from gamedata."); + offsets.cgtoffsets.fractionleftsolid = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::surface", buff, sizeof(buff)), "Can't get \"CGameTrace::surface\" offset from gamedata."); + offsets.cgtoffsets.surface = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::hitgroup", buff, sizeof(buff)), "Can't get \"CGameTrace::hitgroup\" offset from gamedata."); + offsets.cgtoffsets.hitgroup = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::physicsbone", buff, sizeof(buff)), "Can't get \"CGameTrace::physicsbone\" offset from gamedata."); + offsets.cgtoffsets.physicsbone = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::m_pEnt", buff, sizeof(buff)), "Can't get \"CGameTrace::m_pEnt\" offset from gamedata."); + offsets.cgtoffsets.m_pEnt = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::hitbox", buff, sizeof(buff)), "Can't get \"CGameTrace::hitbox\" offset from gamedata."); + offsets.cgtoffsets.hitbox = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CGameTrace::size", buff, sizeof(buff)), "Can't get \"CGameTrace::size\" offset from gamedata."); + offsets.cgtoffsets.size = StringToInt(buff); + + //Ray_t + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Start", buff, sizeof(buff)), "Can't get \"Ray_t::m_Start\" offset from gamedata."); + offsets.rtoffsets.m_Start = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Delta", buff, sizeof(buff)), "Can't get \"Ray_t::m_Delta\" offset from gamedata."); + offsets.rtoffsets.m_Delta = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_StartOffset", buff, sizeof(buff)), "Can't get \"Ray_t::m_StartOffset\" offset from gamedata."); + offsets.rtoffsets.m_StartOffset = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Extents", buff, sizeof(buff)), "Can't get \"Ray_t::m_Extents\" offset from gamedata."); + offsets.rtoffsets.m_Extents = StringToInt(buff); + + if(gEngineVersion == Engine_CSGO) + { + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_pWorldAxisTransform", buff, sizeof(buff)), "Can't get \"Ray_t::m_pWorldAxisTransform\" offset from gamedata."); + offsets.rtoffsets.m_pWorldAxisTransform = StringToInt(buff); + } + + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_IsRay", buff, sizeof(buff)), "Can't get \"Ray_t::m_IsRay\" offset from gamedata."); + offsets.rtoffsets.m_IsRay = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("Ray_t::m_IsSwept", buff, sizeof(buff)), "Can't get \"Ray_t::m_IsSwept\" offset from gamedata."); + offsets.rtoffsets.m_IsSwept = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("Ray_t::size", buff, sizeof(buff)), "Can't get \"Ray_t::size\" offset from gamedata."); + offsets.rtoffsets.size = StringToInt(buff); + + //CTraceFilterSimple + ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::vptr", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::vptr\" offset from gamedata."); + offsets.ctfsoffsets.vptr = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_pPassEnt", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_pPassEnt\" offset from gamedata."); + offsets.ctfsoffsets.m_pPassEnt = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_collisionGroup", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_collisionGroup\" offset from gamedata."); + offsets.ctfsoffsets.m_collisionGroup = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_pExtraShouldHitCheckFunction", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_pExtraShouldHitCheckFunction\" offset from gamedata."); + offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction = StringToInt(buff); + ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::size", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::size\" offset from gamedata."); + offsets.ctfsoffsets.size = StringToInt(buff); + + if(gEngineVersion == Engine_CSS) + { + offsets.ctfsoffsets.vtable = gd.GetAddress("CTraceFilterSimple::vtable"); + ASSERT_MSG(offsets.ctfsoffsets.vtable != Address_Null, "Can't get \"CTraceFilterSimple::vtable\" address from gamedata. Gamedata needs an update."); + } + + //enginetrace + gd.GetKeyValue("CEngineTrace", buff, sizeof(buff)); + gEngineTrace = CreateInterface(buff); + ASSERT_MSG(gEngineTrace != Address_Null, "Can't create \"enginetrace\" from CreateInterface()."); + + //RayTrace + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("TraceRay")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gTraceRay = EndPrepSDKCall(); + ASSERT(gTraceRay); + + if(gEngineVersion == Engine_CSGO) + { + //TraceRayAgainstLeafAndEntityList + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("TraceRayAgainstLeafAndEntityList")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gTraceRayAgainstLeafAndEntityList = EndPrepSDKCall(); + ASSERT(gTraceRayAgainstLeafAndEntityList); + + //CanTraceRay + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("CanTraceRay")); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + + gCanTraceRay = EndPrepSDKCall(); + ASSERT(gCanTraceRay); + } +} + +stock void TraceRayAgainstLeafAndEntityList(Ray_t ray, ITraceListData traceData, int mask, CTraceFilterSimple filter, CGameTrace trace) +{ + SDKCall(gTraceRayAgainstLeafAndEntityList, gEngineTrace, ray.Address, traceData.Address, mask, filter.Address, trace.Address); +} + +stock void TraceRay(Ray_t ray, int mask, CTraceFilterSimple filter, CGameTrace trace) +{ + SDKCall(gTraceRay, gEngineTrace, ray.Address, mask, filter.Address, trace.Address); +} diff --git a/sourcemod/scripting/momsurffix/utils.sp b/sourcemod/scripting/momsurffix/utils.sp new file mode 100644 index 0000000..e68e342 --- /dev/null +++ b/sourcemod/scripting/momsurffix/utils.sp @@ -0,0 +1,279 @@ +#define MALLOC(%1) view_as<%1>(AllocatableBase._malloc(%1.Size(), #%1)) +#define MEMORYPOOL_NAME_MAX 128 + +enum struct MemoryPoolEntry +{ + Address addr; + char name[MEMORYPOOL_NAME_MAX]; +} + +methodmap AllocatableBase < AddressBase +{ + public static Address _malloc(int size, const char[] name) + { + Address addr = Malloc(size, name); + + return addr; + } + + public void Free() + { + Free(this.Address); + } +} + +methodmap PseudoStackArray < AddressBase +{ + public Address Get8(int idx, int size = 4) + { + ASSERT(idx >= 0); + ASSERT(size > 0); + + return view_as<Address>(LoadFromAddress(this.Address + (idx * size), NumberType_Int8)); + } + + public Address Get16(int idx, int size = 4) + { + ASSERT(idx >= 0); + ASSERT(size > 0); + + return view_as<Address>(LoadFromAddress(this.Address + (idx * size), NumberType_Int16)); + } + + public Address Get32(int idx, int size = 4) + { + ASSERT(idx >= 0); + ASSERT(size > 0); + + return this.Address + (idx * size); + } +} + +methodmap Vector < AllocatableBase +{ + public static int Size() + { + return 12; + } + + public Vector() + { + return MALLOC(Vector); + } + + property float x + { + public set(float _x) { StoreToAddress(this.Address, view_as<int>(_x), NumberType_Int32, false); } + public get() { return view_as<float>(LoadFromAddress(this.Address, NumberType_Int32)); } + } + + property float y + { + public set(float _y) { StoreToAddress(this.Address + 4, view_as<int>(_y), NumberType_Int32, false); } + public get() { return view_as<float>(LoadFromAddress(this.Address + 4, NumberType_Int32)); } + } + + property float z + { + public set(float _z) { StoreToAddress(this.Address + 8, view_as<int>(_z), NumberType_Int32, false); } + public get() { return view_as<float>(LoadFromAddress(this.Address + 8, NumberType_Int32)); } + } + + public void ToArray(float buff[3]) + { + buff[0] = this.x; + buff[1] = this.y; + buff[2] = this.z; + } + + public void FromArray(float buff[3]) + { + this.x = buff[0]; + this.y = buff[1]; + this.z = buff[2]; + } + + public void CopyTo(Vector dst) + { + dst.x = this.x; + dst.y = this.y; + dst.z = this.z; + } + + public float LengthSqr() + { + return this.x*this.x + this.y*this.y + this.z*this.z; + } + + public float Length() + { + return SquareRoot(this.LengthSqr()); + } + + public float Dot(float vec[3]) + { + return this.x*vec[0] + this.y*vec[1] + this.z*vec[2]; + } +} + +static Address g_pMemAlloc; +static Handle gMalloc, gFree, gCreateInterface; +static ArrayList gMemoryPool; + +stock void InitUtils(GameData gd) +{ + gMemoryPool = new ArrayList(sizeof(MemoryPoolEntry)); + + //CreateInterface + StartPrepSDKCall(SDKCall_Static); + + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CreateInterface"), "Failed to get \"CreateInterface\" signature. Gamedata needs an update."); + + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gCreateInterface = EndPrepSDKCall(); + ASSERT(gCreateInterface); + + if(gEngineVersion == Engine_CSGO || gOSType == OSWindows) + { + //g_pMemAlloc + g_pMemAlloc = gd.GetAddress("g_pMemAlloc"); + ASSERT_MSG(g_pMemAlloc != Address_Null, "Can't get \"g_pMemAlloc\" address from gamedata. Gamedata needs an update."); + + //Malloc + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("Malloc")); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gMalloc = EndPrepSDKCall(); + ASSERT(gMalloc); + + //Free + StartPrepSDKCall(SDKCall_Raw); + + PrepSDKCall_SetVirtual(gd.GetOffset("Free")); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gFree = EndPrepSDKCall(); + ASSERT(gFree); + } + else + { + //Malloc + StartPrepSDKCall(SDKCall_Static); + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "malloc"), "Failed to get \"malloc\" signature. Gamedata needs an update."); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + gMalloc = EndPrepSDKCall(); + ASSERT(gMalloc); + + //Free + StartPrepSDKCall(SDKCall_Static); + ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "free"), "Failed to get \"free\" signature. Gamedata needs an update."); + + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + + gFree = EndPrepSDKCall(); + ASSERT(gFree); + } +} + +stock Address CreateInterface(const char[] name) +{ + return SDKCall(gCreateInterface, name, 0); +} + +stock Address Malloc(int size, const char[] name) +{ + ASSERT(gMemoryPool); + ASSERT(size > 0); + + MemoryPoolEntry entry; + strcopy(entry.name, sizeof(MemoryPoolEntry::name), name); + + if(gEngineVersion == Engine_CSS && gOSType == OSLinux) + entry.addr = SDKCall(gMalloc, 0, size); + else + entry.addr = SDKCall(gMalloc, g_pMemAlloc, size); + + ASSERT_FMT(entry.addr != Address_Null, "Failed to allocate memory (size: %i)!", size); + gMemoryPool.PushArray(entry); + + return entry.addr; +} + +stock void Free(Address addr) +{ + ASSERT(addr != Address_Null); + ASSERT(gMemoryPool); + int idx = gMemoryPool.FindValue(addr, MemoryPoolEntry::addr); + + //Memory wasn't allocated by this plugin, return. + if(idx == -1) + return; + + gMemoryPool.Erase(idx); + + if(gEngineVersion == Engine_CSS && gOSType == OSLinux) + SDKCall(gFree, 0, addr); + else + SDKCall(gFree, g_pMemAlloc, addr); +} + +stock void AddToMemoryPool(Address addr, const char[] name) +{ + ASSERT(addr != Address_Null); + ASSERT(gMemoryPool); + + MemoryPoolEntry entry; + strcopy(entry.name, sizeof(MemoryPoolEntry::name), name); + entry.addr = addr; + + gMemoryPool.PushArray(entry); +} + +stock void CleanUpUtils() +{ + if(!gMemoryPool) + return; + + MemoryPoolEntry entry; + + for(int i = 0; i < gMemoryPool.Length; i++) + { + gMemoryPool.GetArray(i, entry, sizeof(MemoryPoolEntry)); + view_as<AllocatableBase>(entry.addr).Free(); + } + + delete gMemoryPool; +} + +stock void DumpMemoryUsage() +{ + if(!gMemoryPool || (gMemoryPool && gMemoryPool.Length == 0)) + { + PrintToServer(SNAME..."Theres's currently no active pool or it's empty!"); + return; + } + + MemoryPoolEntry entry; + + PrintToServer(SNAME..."Active memory pool (%i):", gMemoryPool.Length); + for(int i = 0; i < gMemoryPool.Length; i++) + { + gMemoryPool.GetArray(i, entry, sizeof(MemoryPoolEntry)); + PrintToServer(SNAME..."[%i]: 0x%08X \"%s\"", i, entry.addr, entry.name); + } +}
\ No newline at end of file |
