summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/distbugfix.sp
diff options
context:
space:
mode:
Diffstat (limited to 'sourcemod/scripting/distbugfix.sp')
-rw-r--r--sourcemod/scripting/distbugfix.sp1592
1 files changed, 1592 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);
+ }
+}