diff --git a/spt.vcxproj b/spt.vcxproj
index 1ee0305f2..62f80503b 100644
--- a/spt.vcxproj
+++ b/spt.vcxproj
@@ -1223,6 +1223,7 @@ del "$(OutDir)spt-version.obj"
+
diff --git a/spt.vcxproj.filters b/spt.vcxproj.filters
index 595b293a9..a992be5d0 100644
--- a/spt.vcxproj.filters
+++ b/spt.vcxproj.filters
@@ -391,6 +391,9 @@
spt\features
+
+ spt\features
+
diff --git a/spt/features/aim.cpp b/spt/features/aim.cpp
index d84a61b48..12edbc6ef 100644
--- a/spt/features/aim.cpp
+++ b/spt/features/aim.cpp
@@ -5,6 +5,7 @@
#include "ent_utils.hpp"
#include "math.hpp"
#include "playerio.hpp"
+#include "signals.hpp"
#undef min
#undef max
@@ -330,7 +331,7 @@ CON_COMMAND(_y_spt_setangle,
void AimFeature::LoadFeature()
{
- if (spt_generic.ORIG_ControllerMove && spt_playerio.ORIG_CreateMove)
+ if (spt_generic.ORIG_ControllerMove && CreateMoveSignal.Works)
{
InitCommand(tas_aim_reset);
InitCommand(tas_aim);
diff --git a/spt/features/ihud.cpp b/spt/features/ihud.cpp
index f6f4f3847..7813703c7 100644
--- a/spt/features/ihud.cpp
+++ b/spt/features/ihud.cpp
@@ -70,13 +70,10 @@ class InputHud : public FeatureWrapper
virtual void LoadFeature() override;
- virtual void PreHook() override;
-
virtual void UnloadFeature() override;
private:
- DECL_HOOK_THISCALL(void, DecodeUserCmdFromBuffer, void*, bf_read& buf, int sequence_number);
- void CreateMove(uintptr_t pCmd);
+ void SetData(uintptr_t pCmd);
void DrawRectAndCenterTxt(Color buttonColor,
int x0,
int y0,
@@ -101,8 +98,6 @@ class InputHud : public FeatureWrapper
int buttonBits = 0;
bool awaitingFrameDraw = false;
-
- bool loadingSuccessful = false;
};
InputHud spt_ihud;
@@ -564,22 +559,7 @@ CON_COMMAND(y_spt_ihud_add_key, "Add custom key to ihud.")
}
#endif
-namespace patterns
-{
- PATTERNS(
- DecodeUserCmdFromBuffer,
- "5135",
- "83 EC 54 33 C0 D9 EE 89 44 24 ?? D9 54 24 ?? 89 44 24 ??",
- "7197370",
- "55 8B EC 83 EC 54 56 8B F1 C7 45 ?? ?? ?? ?? ?? 8D 4D ?? C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 E8 ?? ?? ?? ?? 8B 4D ??",
- "4044",
- "83 EC 54 53 57 8D 44 24 ?? 50 8B 44 24 ?? 99");
-}
-
-void InputHud::InitHooks()
-{
- HOOK_FUNCTION(client, DecodeUserCmdFromBuffer);
-}
+void InputHud::InitHooks() {}
bool InputHud::ShouldLoadFeature()
{
@@ -588,10 +568,11 @@ bool InputHud::ShouldLoadFeature()
void InputHud::LoadFeature()
{
- if (!loadingSuccessful)
+ if (!(CreateMoveSignal.Works && DecodeUserCmdFromBufferSignal.Works))
return;
- if (CreateMoveSignal.Works)
- CreateMoveSignal.Connect(this, &InputHud::CreateMove);
+
+ CreateMoveSignal.Connect(this, &InputHud::SetData);
+ DecodeUserCmdFromBufferSignal.Connect(this, &InputHud::SetData);
bool result = spt_hud_feat.AddHudDefaultGroup(HudCallback(
std::bind(&InputHud::DrawInputHud, this), []() { return y_spt_ihud.GetBool(); }, false));
@@ -664,27 +645,8 @@ void InputHud::LoadFeature()
anglesSetting = {true, false, L"", font, 4, 1, 0, 0, background, highlight, textcolor, texthighlight, 0};
}
-void InputHud::PreHook()
-{
- loadingSuccessful = !!(ORIG_DecodeUserCmdFromBuffer);
-}
-
void InputHud::UnloadFeature() {}
-void InputHud::SetInputInfo(int button, Vector movement)
-{
- if (awaitingFrameDraw)
- {
- buttonBits |= button;
- }
- else
- {
- buttonBits = button;
- }
- inputMovement = movement;
- awaitingFrameDraw = true;
-}
-
bool InputHud::ModifySetting(const char* element, const char* param, const char* value)
{
InputHud::Button* target;
@@ -1057,21 +1019,20 @@ void InputHud::DrawButton(Button button)
button.text.c_str());
}
-IMPL_HOOK_THISCALL(InputHud, void, DecodeUserCmdFromBuffer, void*, bf_read& buf, int sequence_number)
+void InputHud::SetData(uintptr_t pCmd)
{
- spt_ihud.ORIG_DecodeUserCmdFromBuffer(thisptr, buf, sequence_number);
-
- auto m_pCommands =
- *reinterpret_cast(reinterpret_cast(thisptr) + spt_playerio.offM_pCommands);
- auto pCmd = m_pCommands + spt_playerio.sizeofCUserCmd * (sequence_number % 90);
auto cmd = reinterpret_cast(pCmd);
- spt_ihud.SetInputInfo(cmd->buttons, Vector(cmd->sidemove, cmd->forwardmove, cmd->upmove));
-}
-void InputHud::CreateMove(uintptr_t pCmd)
-{
- auto cmd = reinterpret_cast(pCmd);
- spt_ihud.SetInputInfo(cmd->buttons, Vector(cmd->sidemove, cmd->forwardmove, cmd->upmove));
+ if (awaitingFrameDraw)
+ {
+ buttonBits |= cmd->buttons;
+ }
+ else
+ {
+ buttonBits = cmd->buttons;
+ }
+ inputMovement = Vector(cmd->sidemove, cmd->forwardmove, cmd->upmove);
+ awaitingFrameDraw = true;
}
std::string InputHud::Button::ToString() const
diff --git a/spt/features/playerio.cpp b/spt/features/playerio.cpp
index f5974a0d1..113e1cdf3 100644
--- a/spt/features/playerio.cpp
+++ b/spt/features/playerio.cpp
@@ -97,6 +97,14 @@ namespace patterns
"8B 44 24 ?? 83 EC 1C 53 55 56 57",
"dmomm",
"83 EC 10 53 8B 5C 24 ?? 55 56 8B F1");
+ PATTERNS(
+ DecodeUserCmdFromBuffer,
+ "5135",
+ "83 EC 54 33 C0 D9 EE 89 44 24 ?? D9 54 24 ?? 89 44 24 ??",
+ "7197370",
+ "55 8B EC 83 EC 54 56 8B F1 C7 45 ?? ?? ?? ?? ?? 8D 4D ?? C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 C7 45 ?? 00 00 00 00 E8 ?? ?? ?? ?? 8B 4D ??",
+ "4044",
+ "83 EC 54 53 57 8D 44 24 ?? 50 8B 44 24 ?? 99");
PATTERNS(
GetGroundEntity,
"5135",
@@ -114,6 +122,7 @@ namespace patterns
void PlayerIOFeature::InitHooks()
{
HOOK_FUNCTION(client, CreateMove);
+ HOOK_FUNCTION(client, DecodeUserCmdFromBuffer);
HOOK_FUNCTION(client, GetButtonBits);
FIND_PATTERN(client, GetGroundEntity);
}
@@ -171,6 +180,11 @@ void PlayerIOFeature::PreHook()
offSidemove = 28;
}
+ if (ORIG_DecodeUserCmdFromBuffer)
+ {
+ DecodeUserCmdFromBufferSignal.Works = true;
+ }
+
GetPlayerFields();
}
@@ -284,38 +298,48 @@ Strafe::MovementVars PlayerIOFeature::GetMovementVars()
return vars;
}
-void __fastcall PlayerIOFeature::HOOKED_CreateMove_Func(void* thisptr,
- int edx,
- int sequence_number,
- float input_sample_frametime,
- bool active)
+IMPL_HOOK_THISCALL(PlayerIOFeature,
+ void,
+ CreateMove,
+ void*,
+ int sequence_number,
+ float input_sample_frametime,
+ bool active)
{
- auto m_pCommands = *reinterpret_cast(reinterpret_cast(thisptr) + offM_pCommands);
- pCmd = m_pCommands + sizeofCUserCmd * (sequence_number % 90);
+ auto m_pCommands =
+ *reinterpret_cast(reinterpret_cast(thisptr) + spt_playerio.offM_pCommands);
+ auto pCmd = m_pCommands + spt_playerio.sizeofCUserCmd * (sequence_number % 90);
+ spt_playerio.pCmd = pCmd;
- ORIG_CreateMove(thisptr, edx, sequence_number, input_sample_frametime, active);
+ spt_playerio.ORIG_CreateMove(thisptr, sequence_number, input_sample_frametime, active);
CreateMoveSignal(pCmd);
- pCmd = 0;
+ spt_playerio.pCmd = 0;
}
-void __fastcall PlayerIOFeature::HOOKED_CreateMove(void* thisptr,
- int edx,
- int sequence_number,
- float input_sample_frametime,
- bool active)
+IMPL_HOOK_THISCALL(PlayerIOFeature, void, DecodeUserCmdFromBuffer, void*, bf_read& buf, int sequence_number)
{
- spt_playerio.HOOKED_CreateMove_Func(thisptr, edx, sequence_number, input_sample_frametime, active);
+ spt_playerio.ORIG_DecodeUserCmdFromBuffer(thisptr, buf, sequence_number);
+
+ auto m_pCommands =
+ *reinterpret_cast(reinterpret_cast(thisptr) + spt_playerio.offM_pCommands);
+ auto pCmd = m_pCommands + spt_playerio.sizeofCUserCmd * (sequence_number % 90);
+
+ DecodeUserCmdFromBufferSignal(pCmd);
}
-int __fastcall PlayerIOFeature::HOOKED_GetButtonBits_Func(void* thisptr, int edx, int bResetState)
+IMPL_HOOK_THISCALL(PlayerIOFeature, int, GetButtonBits, void*, int bResetState)
{
- int rv = ORIG_GetButtonBits(thisptr, edx, bResetState);
+ int rv = spt_playerio.ORIG_GetButtonBits(thisptr, bResetState);
if (bResetState == 1)
{
static int keyPressed = 0;
+ int spamButtons = spt_playerio.spamButtons;
+ bool& forceJump = spt_playerio.forceJump;
+ bool& forceUnduck = spt_playerio.forceUnduck;
+
keyPressed = (keyPressed ^ spamButtons) & spamButtons;
rv |= keyPressed;
@@ -335,11 +359,6 @@ int __fastcall PlayerIOFeature::HOOKED_GetButtonBits_Func(void* thisptr, int edx
return rv;
}
-int __fastcall PlayerIOFeature::HOOKED_GetButtonBits(void* thisptr, int edx, int bResetState)
-{
- return spt_playerio.HOOKED_GetButtonBits_Func(thisptr, edx, bResetState);
-}
-
bool PlayerIOFeature::GetFlagsDucking()
{
return m_fFlags.GetValue() & FL_DUCKING;
@@ -354,7 +373,7 @@ Strafe::PlayerData PlayerIOFeature::GetPlayerData()
const int IN_DUCK = 1 << 2;
data.Ducking = GetFlagsDucking();
- data.DuckPressed = (ORIG_GetButtonBits(cinput_thisptr, 0, 0) & IN_DUCK);
+ data.DuckPressed = (ORIG_GetButtonBits(cinput_thisptr, 0) & IN_DUCK);
data.UnduckedOrigin = m_vecAbsOrigin.GetValue();
data.Velocity = GetPlayerVelocity();
data.Basevelocity = Vector();
@@ -454,7 +473,7 @@ bool PlayerIOFeature::IsGroundEntitySet()
bool PlayerIOFeature::TryJump()
{
const int IN_JUMP = (1 << 1);
- return ORIG_GetButtonBits(cinput_thisptr, 0, 0) & IN_JUMP;
+ return ORIG_GetButtonBits(cinput_thisptr, 0) & IN_JUMP;
}
bool PlayerIOFeature::PlayerIOAddressesFound()
diff --git a/spt/features/playerio.hpp b/spt/features/playerio.hpp
index 11d48b80d..a7d387d7f 100644
--- a/spt/features/playerio.hpp
+++ b/spt/features/playerio.hpp
@@ -23,18 +23,9 @@ typedef void*(__cdecl* _GetLocalPlayer)();
class PlayerIOFeature : public FeatureWrapper
{
private:
- void __fastcall HOOKED_CreateMove_Func(void* thisptr,
- int edx,
- int sequence_number,
- float input_sample_frametime,
- bool active);
- static void __fastcall HOOKED_CreateMove(void* thisptr,
- int edx,
- int sequence_number,
- float input_sample_frametime,
- bool active);
- int __fastcall HOOKED_GetButtonBits_Func(void* thisptr, int edx, int bResetState);
- static int __fastcall HOOKED_GetButtonBits(void* thisptr, int edx, int bResetState);
+ DECL_HOOK_THISCALL(void, CreateMove, void*, int sequence_number, float input_sample_frametime, bool active);
+ DECL_HOOK_THISCALL(void, DecodeUserCmdFromBuffer, void*, bf_read& buf, int sequence_number);
+ DECL_HOOK_THISCALL(int, GetButtonBits, void*, int bResetState);
public:
virtual bool ShouldLoadFeature() override;
@@ -60,10 +51,8 @@ class PlayerIOFeature : public FeatureWrapper
int spamButtons = 0;
ptrdiff_t offServerAbsOrigin = 0;
uintptr_t pCmd = 0;
- _CreateMove ORIG_CreateMove = nullptr;
_GetGroundEntity ORIG_GetGroundEntity = nullptr;
- _GetButtonBits ORIG_GetButtonBits = nullptr;
Vector currentVelocity;
Vector previousVelocity;
diff --git a/spt/features/strafehud.cpp b/spt/features/strafehud.cpp
new file mode 100644
index 000000000..c11a14d10
--- /dev/null
+++ b/spt/features/strafehud.cpp
@@ -0,0 +1,561 @@
+#include "stdafx.hpp"
+#include "..\feature.hpp"
+#include "hud.hpp"
+
+#ifdef SPT_HUD_ENABLED
+
+#include
+
+#include "interfaces.hpp"
+#include "playerio.hpp"
+#include "game_detection.hpp"
+#include "math.hpp"
+#include "signals.hpp"
+#include "visualizations\imgui\imgui_interface.hpp"
+
+#ifdef OE
+#include "..\game_shared\usercmd.h"
+#else
+#include "usercmd.h"
+#endif
+
+#undef min
+#undef max
+
+// Draw strafe graph
+class StrafeHUD : public FeatureWrapper
+{
+public:
+ void SetData(uintptr_t pCmd);
+ void DrawHUD();
+
+protected:
+ virtual bool ShouldLoadFeature() override;
+
+ virtual void InitHooks() override;
+
+ virtual void LoadFeature() override;
+
+ virtual void UnloadFeature() override;
+
+private:
+ Vector wishDir;
+ std::vector accels;
+
+ struct Line
+ {
+ Color color;
+ int start;
+ int end;
+ };
+
+ struct Point
+ {
+ int x, y;
+ };
+
+ bool shouldUpdateLines = false;
+ std::vector lines;
+ std::vector points;
+};
+
+static StrafeHUD feat_strafehud;
+
+ConVar spt_strafehud("spt_strafehud", "0", FCVAR_CHEAT, "Draws the strafe HUD.");
+ConVar spt_strafehud_x("spt_strafehud_x", "-10", FCVAR_CHEAT, "The X position for the strafe HUD.");
+ConVar spt_strafehud_y("spt_strafehud_y", "-10", FCVAR_CHEAT, "The Y position for the strafe HUD.");
+ConVar spt_strafehud_size("spt_strafehud_size",
+ "256",
+ FCVAR_CHEAT,
+ "The width and height of the strafe HUD.",
+ true,
+ 1,
+ false,
+ 0);
+ConVar spt_strafehud_detail_scale("spt_strafehud_detail_scale",
+ "4",
+ FCVAR_CHEAT,
+ "The detail scale for the lines of the strafe HUD.",
+ true,
+ 0,
+ true,
+ 645);
+ConVar spt_strafehud_lock_mode("spt_strafehud_lock_mode",
+ "1",
+ FCVAR_CHEAT,
+ "Lock mode used by the strafe HUD:\n"
+ "0 - view direction\n"
+ "1 - velocity direction\n"
+ "2 - absolute angles",
+ true,
+ 0,
+ true,
+ 2);
+
+// Strafe stuff
+
+static Vector GetGroundFrictionVelocity(const Strafe::PlayerData player, const Strafe::MovementVars& vars)
+{
+ const float friction = vars.Friction * vars.EntFriction;
+ Vector vel = player.Velocity;
+
+ const float velLen = vel.Length2D();
+ if (vars.OnGround)
+ {
+ if (velLen >= vars.Stopspeed)
+ {
+ vel *= (1.0f - vars.Frametime * friction);
+ }
+ else if (velLen >= std::max(0.1f, vars.Frametime * vars.Stopspeed * friction))
+ {
+ vel -= (vel / velLen) * vars.Frametime * vars.Stopspeed * friction;
+ }
+ else
+ {
+ vel = Vector(0.0f, 0.0f, 0.0f);
+ }
+
+ if (vel.Length2D() < 1.0f)
+ {
+ vel = Vector(0.0f, 0.0f, 0.0f);
+ }
+ }
+
+ return vel;
+}
+
+static void CreateWishDirs(const Strafe::PlayerData player,
+ const Strafe::MovementVars& vars,
+ float sinPlayerYaw,
+ float cosPlayerYaw,
+ __m128 forwardmoves,
+ __m128 sidemoves,
+ __m128& outWishDirX,
+ __m128& outWishDirY)
+{
+ __m128 wishDirX = sidemoves;
+ __m128 wishDirY = forwardmoves;
+
+ const __m128 lengths = _mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(wishDirX, wishDirX), _mm_mul_ps(wishDirY, wishDirY)));
+ const __m128 mask = _mm_cmpgt_ps(lengths, _mm_set1_ps(1.0f));
+ wishDirX = _mm_blendv_ps(wishDirX, _mm_div_ps(wishDirX, lengths), mask);
+ wishDirY = _mm_blendv_ps(wishDirY, _mm_div_ps(wishDirY, lengths), mask);
+
+ const __m128 sinYaw = _mm_set1_ps(sinPlayerYaw);
+ const __m128 cosYaw = _mm_set1_ps(cosPlayerYaw);
+
+ outWishDirX = _mm_add_ps(_mm_mul_ps(sinYaw, wishDirX), _mm_mul_ps(cosYaw, wishDirY));
+ outWishDirY = _mm_sub_ps(_mm_mul_ps(sinYaw, wishDirY), _mm_mul_ps(cosYaw, wishDirX));
+
+ if (utils::DoesGameLookLikePortal())
+ {
+ if (!vars.OnGround && player.Velocity.Length2DSqr() > 300 * 300)
+ {
+ if (std::fabs(player.Velocity.x) > 150)
+ {
+ __m128 velX = _mm_set1_ps(player.Velocity.x);
+ outWishDirX =
+ _mm_blendv_ps(outWishDirX,
+ _mm_set1_ps(0.0f),
+ _mm_cmplt_ps(_mm_mul_ps(velX, outWishDirX), _mm_set1_ps(0.0f)));
+ }
+ if (std::fabs(player.Velocity.y) > 150)
+ {
+ __m128 velY = _mm_set1_ps(player.Velocity.y);
+ outWishDirY =
+ _mm_blendv_ps(outWishDirY,
+ _mm_set1_ps(0.0f),
+ _mm_cmplt_ps(_mm_mul_ps(velY, outWishDirY), _mm_set1_ps(0.0f)));
+ }
+ }
+ }
+}
+
+static __m128 GetMaxSpeeds(const Strafe::PlayerData player,
+ const Strafe::MovementVars& vars,
+ __m128 wishDirX,
+ __m128 wishDirY,
+ bool forceOnGround)
+{
+ const __m128 maxSpeed = _mm_set1_ps(vars.Maxspeed);
+ const __m128 wishSpeedCap = _mm_set1_ps(vars.WishspeedCap);
+
+ const __m128 duckMultiplier = _mm_set1_ps((vars.OnGround && player.Ducking) ? 0.33333333f : 1.0f);
+
+ wishDirX = _mm_mul_ps(wishDirX, maxSpeed);
+ wishDirY = _mm_mul_ps(wishDirY, maxSpeed);
+
+ const __m128 wishDirLen =
+ _mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(wishDirX, wishDirX), _mm_mul_ps(wishDirY, wishDirY)));
+
+ __m128 clampedMaxSpeed = _mm_min_ps(maxSpeed, wishDirLen);
+ clampedMaxSpeed = _mm_mul_ps(clampedMaxSpeed, duckMultiplier);
+
+ if (forceOnGround || vars.OnGround)
+ return clampedMaxSpeed;
+
+ return _mm_min_ps(wishSpeedCap, clampedMaxSpeed);
+}
+
+static inline __m128 GetMaxAccels(const Strafe::PlayerData player,
+ const Strafe::MovementVars& vars,
+ __m128 wishDirX,
+ __m128 wishDirY)
+{
+ const float accel = vars.OnGround ? vars.Accelerate : vars.Airaccelerate;
+ return _mm_mul_ps(_mm_set1_ps(vars.EntFriction * vars.Frametime * accel),
+ GetMaxSpeeds(player, vars, wishDirX, wishDirY, true));
+}
+
+static void GetAccelAfterMove(const Strafe::PlayerData player,
+ const Strafe::MovementVars& vars,
+ float sinPlayerYaw,
+ float cosPlayerYaw,
+ const Vector& oldVel,
+ float oldVelLength2D,
+ const float* yaws,
+ float* outAccel)
+{
+ const __m128 vecYaws = _mm_loadu_ps(yaws);
+ const __m128 forwardmoves = _mm_cos_ps(vecYaws);
+ const __m128 sidemoves = _mm_sin_ps(vecYaws);
+
+ __m128 wishDirX, wishDirY;
+ CreateWishDirs(player, vars, sinPlayerYaw, cosPlayerYaw, forwardmoves, sidemoves, wishDirX, wishDirY);
+
+ const __m128 wishDirLengths =
+ _mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(wishDirX, wishDirX), _mm_mul_ps(wishDirY, wishDirY)));
+ const __m128 zeroMask = _mm_cmpeq_ps(wishDirLengths, _mm_set1_ps(0.0f));
+
+ const __m128 maxSpeeds = GetMaxSpeeds(player, vars, wishDirX, wishDirY, false);
+ const __m128 maxAccels = GetMaxAccels(player, vars, wishDirX, wishDirY);
+
+ const __m128 velX = _mm_set1_ps(oldVel.x);
+ const __m128 velY = _mm_set1_ps(oldVel.y);
+
+ wishDirX = _mm_div_ps(wishDirX, wishDirLengths);
+ wishDirY = _mm_div_ps(wishDirY, wishDirLengths);
+
+ const __m128 dotProducts = _mm_add_ps(_mm_mul_ps(velX, wishDirX), _mm_mul_ps(velY, wishDirY));
+ const __m128 accelDiff = _mm_sub_ps(maxSpeeds, dotProducts);
+ const __m128 accelMask = _mm_cmple_ps(accelDiff, _mm_set1_ps(0.0f));
+
+ const __m128 accelForce = _mm_min_ps(maxAccels, accelDiff);
+
+ const __m128 newVelX = _mm_add_ps(velX, _mm_mul_ps(wishDirX, accelForce));
+ const __m128 newVelY = _mm_add_ps(velY, _mm_mul_ps(wishDirY, accelForce));
+ const __m128 newVelLen = _mm_sqrt_ps(_mm_add_ps(_mm_mul_ps(newVelX, newVelX), _mm_mul_ps(newVelY, newVelY)));
+
+ const __m128 accel = _mm_sub_ps(newVelLen, _mm_set1_ps(oldVelLength2D));
+
+ const __m128 result = _mm_blendv_ps(accel, _mm_set1_ps(0.0f), _mm_or_ps(zeroMask, accelMask));
+
+ _mm_storeu_ps(outAccel, result);
+}
+
+void StrafeHUD::SetData(uintptr_t pCmd)
+{
+ if (!spt_strafehud.GetBool())
+ return;
+
+ const CUserCmd* cmd = reinterpret_cast(pCmd);
+ float forwardmove = cmd->forwardmove;
+ float sidemove = cmd->sidemove;
+ float upmove = cmd->upmove;
+
+ accels.clear();
+ lines.clear();
+
+ const auto player = spt_playerio.GetPlayerData();
+ const auto vars = spt_playerio.GetMovementVars();
+ const float playerYaw = utils::GetPlayerEyeAngles().y;
+
+ float relAng = 0.0f;
+ const int lockMode = spt_strafehud_lock_mode.GetInt();
+ if (lockMode > 0)
+ {
+ relAng = playerYaw;
+ if (lockMode == 1)
+ {
+ QAngle angles;
+ VectorAngles(player.Velocity, Vector(0, 0, 1), angles);
+ relAng -= angles.y;
+ }
+ relAng *= utils::M_DEG2RAD;
+ }
+
+ const float speed = forwardmove * forwardmove + sidemove * sidemove + upmove * upmove;
+ if (speed > vars.Maxspeed * vars.Maxspeed)
+ {
+ const float ratio = vars.Maxspeed / std::sqrtf(speed);
+ forwardmove *= ratio;
+ sidemove *= ratio;
+ upmove *= ratio;
+ }
+
+ wishDir = Vector(std::cosf(relAng) * sidemove - std::sinf(relAng) * forwardmove,
+ std::sinf(relAng) * sidemove + std::cosf(relAng) * forwardmove,
+ 0.0f);
+ const float wishDirLen = wishDir.Length2D();
+ if (wishDirLen > 1.0f)
+ {
+ wishDir /= wishDirLen;
+ }
+
+ int detail = spt_strafehud_size.GetInt() * spt_strafehud_detail_scale.GetFloat();
+ detail = ((detail + 3) / 4) * 4; // round up to multiple of 4
+ accels.reserve(detail);
+
+ const Vector oldVel = GetGroundFrictionVelocity(player, vars);
+
+ const float sinPlayerYaw = std::sinf(playerYaw * utils::M_DEG2RAD);
+ const float cosPlayerYaw = std::cosf(playerYaw * utils::M_DEG2RAD);
+
+ for (int i = 0; i < detail; i += 4)
+ {
+ std::array ang;
+ for (int j = 0; j < 4; j++)
+ {
+ ang[j] = ((i + j) / (float)detail) * 2.0f * M_PI + relAng;
+ }
+
+ std::array accel{};
+ GetAccelAfterMove(player,
+ vars,
+ sinPlayerYaw,
+ cosPlayerYaw,
+ oldVel,
+ oldVel.Length2D(),
+ ang.data(),
+ accel.data());
+ accels.insert(accels.end(), accel.begin(), accel.end());
+ }
+
+ auto [minIt, maxIt] = std::minmax_element(accels.begin(), accels.end());
+ float biggestAccel = *maxIt;
+ float smallestAccel = *minIt;
+ for (float& accel : accels)
+ {
+ if (accel > 0.0f && biggestAccel > 0.0f)
+ {
+ accel /= biggestAccel;
+ }
+ else if (accel < 0.0f && smallestAccel < 0.0f)
+ {
+ accel /= -smallestAccel;
+ }
+ }
+
+ shouldUpdateLines = true;
+}
+
+void StrafeHUD::DrawHUD()
+{
+ const int pad = 5;
+ int size = spt_strafehud_size.GetInt();
+ int x = spt_strafehud_x.GetInt();
+ int y = spt_strafehud_y.GetInt();
+
+ if (x < 0)
+ x += spt_hud_feat.renderView->width - size;
+ if (y < 0)
+ y += spt_hud_feat.renderView->height - size;
+
+ const Color bgColor(0, 0, 0, 192);
+ const Color lineColor(64, 64, 64, 255);
+ const Color wishDirColor(0, 0, 255, 255);
+
+ auto surface = interfaces::surface;
+
+ // Draw background
+ surface->DrawSetColor(bgColor);
+ surface->DrawFilledRect(x, y, x + size, y + size);
+ x += pad;
+ y += pad;
+ size -= pad * 2;
+
+ const int mid_x = x + size / 2;
+ const int mid_y = y + size / 2;
+
+ const float dx = size * 0.5f;
+ const float dy = size * 0.5f;
+
+ // Containing rect
+ surface->DrawOutlinedRect(x, y, x + size, y + size);
+
+ // Circles
+ surface->DrawOutlinedCircle(mid_x, mid_y, size / 2, 32);
+ surface->DrawOutlinedCircle(mid_x, mid_y, size / 4, 32);
+
+ // Half-lines and diagonals
+ surface->DrawLine(mid_x, y, mid_x, y + size);
+ surface->DrawLine(x, mid_y, x + size, mid_y);
+ surface->DrawLine(x, y, x + size, y + size);
+ surface->DrawLine(x, y + size, x + size, y);
+
+ // Acceleration line
+ if (shouldUpdateLines)
+ {
+ shouldUpdateLines = false;
+
+ lines.clear();
+ points.clear();
+
+ const Color accelColor(0, 255, 0, 255);
+ const Color decelColor(255, 0, 0, 255);
+ const Color nocelColor(255, 255, 0, 255);
+
+ Color prevColor(0, 0, 0);
+ Line currentLine = Line{
+ .color = nocelColor,
+ .start = 0,
+ .end = 0,
+ };
+
+ const int detail = accels.size();
+ for (int i = 0; i < detail; i++)
+ {
+ const float ang1 = (i / (float)detail) * 2.0f * M_PI;
+ const int i2 = (i + 1) % detail;
+ const float ang2 = (i2 / (float)detail) * 2.0f * M_PI;
+
+ const float a1 = std::min(std::max(accels[i], -1.0f), 1.0f);
+ const float a2 = std::min(std::max(accels[i2], -1.0f), 1.0f);
+
+ const float ad1 = (a1 + 1.0f) * 0.5f;
+ const float ad2 = (a2 + 1.0f) * 0.5f;
+
+ Color currentColor = nocelColor;
+ if ((ad1 != 0.0f && ad2 != 0.0f) && a1 * a2 > 0.0f)
+ {
+ currentColor = (a1 >= 0.0f) ? accelColor : decelColor;
+ }
+
+ if (i == 0)
+ {
+ // Add first point
+ points.push_back(Point{mid_x + (int)(std::sinf(ang1) * dx * ad1),
+ mid_y - (int)(std::cosf(ang1) * dy * ad1)});
+ }
+
+ if (currentColor != prevColor)
+ {
+ if (currentLine.start != currentLine.end)
+ {
+ lines.push_back(currentLine);
+ }
+ currentLine = Line{currentColor, (int)points.size() - 1, 0};
+ }
+
+ points.push_back(Point{
+ mid_x + (int)(std::sinf(ang2) * dx * ad2),
+ mid_y - (int)(std::cosf(ang2) * dy * ad2),
+ });
+
+ currentLine.end = (int)points.size() - 1;
+ prevColor = currentColor;
+ }
+
+ points.push_back(points[0]);
+ currentLine.end = (int)points.size() - 1;
+ lines.push_back(currentLine);
+ }
+
+ // Draw lines
+ for (const auto& line : lines)
+ {
+ assert(line.end > line.start);
+
+ surface->DrawSetColor(line.color);
+
+ for (int i = line.start; i < line.end; i++)
+ {
+ const auto& point1 = points[i];
+ const auto& point2 = points[i + 1];
+ surface->DrawLine(point1.x, point1.y, point2.x, point2.y);
+ }
+ }
+
+ // Wish dir
+ surface->DrawSetColor(wishDirColor);
+ const int x0 = mid_x;
+ const int y0 = mid_y;
+ const int x1 = mid_x + wishDir.x * dx;
+ const int y1 = mid_y - wishDir.y * dy;
+
+ const int halfThickness = 1;
+
+ const float slope = (y1 - y0) / (float)(x1 - x0);
+ if (std::fabs(slope) <= 1)
+ {
+ for (int i = -halfThickness; i <= halfThickness; i++)
+ {
+ surface->DrawLine(x0, y0 + i, x1, y1 + i);
+ }
+ }
+ else
+ {
+ for (int i = -halfThickness; i <= halfThickness; i++)
+ {
+ surface->DrawLine(x0 + i, y0, x1 + i, y1);
+ }
+ }
+}
+
+bool StrafeHUD::ShouldLoadFeature()
+{
+ return spt_hud_feat.ShouldLoadFeature();
+}
+
+void StrafeHUD::InitHooks() {}
+
+void StrafeHUD::LoadFeature()
+{
+ if (!(CreateMoveSignal.Works && DecodeUserCmdFromBufferSignal.Works))
+ return;
+
+ CreateMoveSignal.Connect(this, &StrafeHUD::SetData);
+ DecodeUserCmdFromBufferSignal.Connect(this, &StrafeHUD::SetData);
+
+ bool result = spt_hud_feat.AddHudDefaultGroup(HudCallback(
+ std::bind(&StrafeHUD::DrawHUD, this), []() { return spt_strafehud.GetBool(); }, false));
+ if (result)
+ {
+ InitConcommandBase(spt_strafehud);
+ InitConcommandBase(spt_strafehud_x);
+ InitConcommandBase(spt_strafehud_y);
+ InitConcommandBase(spt_strafehud_size);
+ InitConcommandBase(spt_strafehud_detail_scale);
+ InitConcommandBase(spt_strafehud_lock_mode);
+ }
+
+ SptImGuiGroup::Hud_StrafeHud.RegisterUserCallback(
+ []()
+ {
+ ImGui::BeginDisabled(!SptImGui::CvarCheckbox(spt_strafehud, "##enabled"));
+
+ const char* opts[] = {"View direction", "Velocity direction", "Absolute angles"};
+ SptImGui::CvarCombo(spt_strafehud_lock_mode, "lock mode", opts, ARRAYSIZE(opts));
+
+ ConVar* var = &spt_strafehud_detail_scale;
+ long long varVal;
+ SptImGui::CvarsDragScalar(&var, &varVal, 1, false, "detail scale");
+ ImGui::SameLine();
+ SptImGui::CmdHelpMarkerWithName(*var);
+ var = &spt_strafehud_size;
+ SptImGui::CvarsDragScalar(&var, &varVal, 1, false, "HUD size");
+ ImGui::SameLine();
+ SptImGui::CmdHelpMarkerWithName(*var);
+
+ ConVar* cvars[] = {&spt_strafehud_x, &spt_strafehud_y};
+ float f[ARRAYSIZE(cvars)];
+ SptImGui::CvarsDragScalar(cvars, f, ARRAYSIZE(cvars), true, "HUD pos");
+ ImGui::SameLine();
+ SptImGui::HelpMarker("Can be set with spt_strafehud_x/y");
+ ImGui::EndDisabled();
+ });
+}
+
+void StrafeHUD::UnloadFeature() {}
+
+#endif
diff --git a/spt/features/visualizations/imgui/imgui_interface.hpp b/spt/features/visualizations/imgui/imgui_interface.hpp
index ea534969c..4ff640a9f 100644
--- a/spt/features/visualizations/imgui/imgui_interface.hpp
+++ b/spt/features/visualizations/imgui/imgui_interface.hpp
@@ -214,6 +214,7 @@ namespace SptImGuiGroup
inline Tab Hud_TextHud{"Text HUD", &Hud}; // use the RegisterHudCvarXXX functions below to add cvars here
inline Tab Hud_IHud{"Input HUD", &Hud};
inline Tab Hud_JHud{"Jump HUD", &Hud};
+ inline Tab Hud_StrafeHud{"Strafe HUD", &Hud};
// development/debugging features
inline Tab Dev{"DEV", &Root};
diff --git a/spt/utils/signals.cpp b/spt/utils/signals.cpp
index fa4b5988e..4fa7962c5 100644
--- a/spt/utils/signals.cpp
+++ b/spt/utils/signals.cpp
@@ -10,6 +10,7 @@ Gallant::Signal1 OngroundSignal;
Gallant::Signal2 SetPausedSignal;
Gallant::Signal1 SV_ActivateServerSignal;
Gallant::Signal1 CreateMoveSignal;
+Gallant::Signal1 DecodeUserCmdFromBufferSignal;
Gallant::Signal0 VagCrashSignal;
Gallant::Signal0 DemoStartPlaybackSignal;
Gallant::Signal1 SV_FrameSignal;
@@ -18,7 +19,6 @@ Gallant::Signal2 ProcessMovementPre_Signal;
Gallant::Signal2 RenderViewPre_Signal;
Gallant::Signal2 SetSignonStateSignal;
-
// Plugin callbacks
Gallant::Signal0 TickSignal;
Gallant::Signal1 LevelInitSignal;
diff --git a/spt/utils/signals.hpp b/spt/utils/signals.hpp
index 6a99f1794..744757990 100644
--- a/spt/utils/signals.hpp
+++ b/spt/utils/signals.hpp
@@ -14,6 +14,7 @@ extern Gallant::Signal1 OngroundSignal;
extern Gallant::Signal2 SetPausedSignal;
extern Gallant::Signal1 SV_ActivateServerSignal;
extern Gallant::Signal1 CreateMoveSignal;
+extern Gallant::Signal1 DecodeUserCmdFromBufferSignal;
extern Gallant::Signal0 VagCrashSignal;
extern Gallant::Signal0 DemoStartPlaybackSignal;
extern Gallant::Signal1 SV_FrameSignal;