diff --git a/spt.vcxproj b/spt.vcxproj index 1ee0305f..62f80503 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 595b293a..a992be5d 100644 --- a/spt.vcxproj.filters +++ b/spt.vcxproj.filters @@ -391,6 +391,9 @@ spt\features + + spt\features + diff --git a/spt/features/strafehud.cpp b/spt/features/strafehud.cpp new file mode 100644 index 00000000..f583c985 --- /dev/null +++ b/spt/features/strafehud.cpp @@ -0,0 +1,533 @@ +#include "stdafx.hpp" +#include "..\feature.hpp" +#include "hud.hpp" + +#if !defined(BMS) && defined(SPT_HUD_ENABLED) + +#include + +#include "interfaces.hpp" +#include "playerio.hpp" +#include "game_detection.hpp" +#include "math.hpp" +#include "signals.hpp" + +#ifdef OE +#include "..\game_shared\usercmd.h" +#else +#include "usercmd.h" +#endif + +// 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() +{ + interfaces::surface; + + 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 true; +} + +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); + } +} + +void StrafeHUD::UnloadFeature() {} + +#endif