#include #include #include #include #include #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 #include 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[][] = { "|", // overlap "|", // none "|", // left "|", // overlap_left "|", // none_left "|", // right "|", // overlap_right "|", // 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[][] = { "|", "|", "|" }; 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), "L: "); hsrIndex += strcopy(hudStrafeRight, sizeof(hudStrafeRight), "R: "); hmIndex += strcopy(hudMouse, sizeof(hudMouse), "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), "%s
%s
%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); } }