From 2381943f434737262f7083e87950f079276137b9 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:41:13 -0700 Subject: [PATCH 01/12] Move the SDK step volume table into GetSurfaceStepVolume Refactor with no behaviour change, to make the footstep commits that follow easier to read. UpdateStepSound keeps a CNEO_Player pointer instead of casting inline, the per-surface soft and loud volumes move into CNEO_Player::GetSurfaceStepVolume with the loud flag passed in, and the SDK's crouched volume scale gets a name, NEO_FS_CROUCH_VOLUME_SCALE. The SDK switch stays under #else for non-NEO builds. --- src/game/client/neo/c_neo_player.h | 1 + src/game/server/neo/neo_player.h | 1 + src/game/shared/baseplayer_shared.cpp | 11 ++++++++++- src/game/shared/neo/neo_player_shared.cpp | 15 +++++++++++++++ src/game/shared/neo/neo_player_shared.h | 3 +++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index ce5374de23..51ac0edc6b 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -94,6 +94,7 @@ class C_NEO_Player : public C_HL2MP_Player virtual void StartWalking(void); virtual void StopWalking(void); bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void Lean(void); diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 3475e8b0e8..88a4a32a9d 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -201,6 +201,7 @@ class CNEO_Player : public CHL2MP_Player virtual void StartWalking(void) OVERRIDE; virtual void StopWalking(void) OVERRIDE; bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; // Cloak Power Interface float CloakPower_Get(void) const ; diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 3d0b0927dc..459cae9809 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -551,7 +551,8 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri int fLadder; #ifdef NEO - const bool bSilent = !assert_cast(this)->IsMakingFootstepSounds(&velrun, &speed, &fLadder); + CNEO_Player *pNeoPlayer = assert_cast(this); + const bool bSilent = !pNeoPlayer->IsMakingFootstepSounds(&velrun, &speed, &fLadder); #endif @@ -662,6 +663,9 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri SetStepSoundTime( STEPSOUNDTIME_NORMAL, bWalking ); +#ifdef NEO + fvol = pNeoPlayer->GetSurfaceStepVolume(psurface, !bWalking); +#else switch ( psurface->game.material ) { default: @@ -693,6 +697,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri fvol = bWalking ? 0.2 : 0.5; break; } +#endif } #ifdef NEO @@ -704,7 +709,11 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri // 65% volume if ducking if ( GetFlags() & FL_DUCKING ) { +#ifdef NEO + fvol *= NEO_FS_CROUCH_VOLUME_SCALE; +#else fvol *= 0.65; +#endif } PlayStepSound( feet, psurface, fvol, false ); } diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 50754f8c07..ad3a3090fa 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -2,6 +2,7 @@ #include "neo_player_shared.h" #include "in_buttons.h" +#include "decals.h" #include "neo_gamerules.h" #include "tier0/valve_minmax_off.h" @@ -411,6 +412,20 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa return true; } +// The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE +float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const +{ + switch (psurface->game.material) + { + case CHAR_TEX_DIRT: + return bLoud ? 0.55f : 0.25f; + case CHAR_TEX_VENT: + return bLoud ? 0.7f : 0.4f; + default: // Concrete, metal, grate, tile and slosh + return bLoud ? 0.5f : 0.2f; + } +} + int GetRank(const int xp) { int iRank = NEO_RANK_RANKLESS_DOG; diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 181f3b85bb..57214267e8 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -44,6 +44,9 @@ extern ConVar sv_neo_spec_replace_player_min_exp; // Crouch/Walk Modifier #define NEO_CROUCH_WALK_MODIFIER 0.75 +// Footsteps: the SDK plays crouched steps at 65% volume +#define NEO_FS_CROUCH_VOLUME_SCALE 0.65f + // Movement Calculations // Recon From 531d733d9ed94a3d8fe4954cd298d9fccf91337a Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:41:59 -0700 Subject: [PATCH 02/12] Name master's walk-or-aim silent rule Refactor with no behaviour change, to make the footstep commits that follow easier to read. IsInSilentMoveMode names the condition master used to decide a step could be silent, the two class speed branches collapse into one check, and the minimum speed for any step gets a name, FS_MIN_STEP_SPEED. --- src/game/client/neo/c_neo_player.h | 1 + src/game/server/neo/neo_player.h | 1 + src/game/shared/neo/neo_player_shared.cpp | 22 ++++++++++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 51ac0edc6b..4ac38cfb1e 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -94,6 +94,7 @@ class C_NEO_Player : public C_HL2MP_Player virtual void StartWalking(void); virtual void StopWalking(void); bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + bool IsInSilentMoveMode() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void Lean(void); diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 88a4a32a9d..ed1e52a047 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -201,6 +201,7 @@ class CNEO_Player : public CHL2MP_Player virtual void StartWalking(void) OVERRIDE; virtual void StopWalking(void) OVERRIDE; bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + bool IsInSilentMoveMode() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; // Cloak Power Interface diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index ad3a3090fa..f1327f9438 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -386,8 +386,11 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa bool onground = (GetFlags() & FL_ONGROUND); bool movingalongground = (groundspeed > 0.0001f); - // Support crouched with a supa7 aimed in has a speed of 55.f - const bool moving_fast_enough = speed >= 50.f; + // Below this nothing plays, on a ladder or off. It catches drift that is not movement at all, such as being + // nudged, and sits just under the 55 u/s floor on max speed so no movement mode is ever filtered here. + // Whether crouching, walking or aiming is silent is decided below + constexpr float FS_MIN_STEP_SPEED = 50.f; + const bool moving_fast_enough = speed >= FS_MIN_STEP_SPEED; // To hear step sounds you must be either on a ladder or moving along the ground AND // You must be moving fast enough @@ -397,21 +400,24 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa // Changing movement direction, looking around, wall-running accelerate the player. Threshold should be lower than regular speed, but higher than walk/aim speed constexpr float SILENT_THRESHOLD_GRACE = 0.7f; - if (GetFlags() & FL_DUCKING) + if (IsInSilentMoveMode()) { - if ((IsInAim() || IsWalking()) && speed <= (GetCrouchSpeed() * SILENT_THRESHOLD_GRACE)) + const float flClassSpeed = (GetFlags() & FL_DUCKING) ? GetCrouchSpeed() : GetNormSpeed(); + if (speed <= (flClassSpeed * SILENT_THRESHOLD_GRACE)) { return false; } } - else if ((IsInAim() || IsWalking()) && speed <= (GetNormSpeed() * SILENT_THRESHOLD_GRACE)) - { - return false; - } return true; } +// Walking and aiming are the silent movement modes, measured against the silent limit +bool CNEO_Player::IsInSilentMoveMode() const +{ + return IsWalking() || IsInAim(); +} + // The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const { From d07bb5a7510ef1740c842930664f95d25d33be84 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:42:10 -0700 Subject: [PATCH 03/12] Name the slowest class sprint modifier Refactor with no behaviour change, to make the footstep commits that follow easier to read. NEO_MIN_SPRINT_MODIFIER is the smallest of the class sprint modifiers, counting Support's even though Support cannot sprint today, and NEO_Min is the constexpr helper that picks it. A static_assert states the one thing the movement design assumes about it: sprinting is faster than running. Nothing reads it yet. A later footstep commit asserts against it that a step at full sprint speed is never forgiven. Co-Authored-By: Claude Fable 5.1 --- src/game/shared/neo/neo_player_shared.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 57214267e8..3c9b5f3e5e 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -39,6 +39,14 @@ extern ConVar sv_neo_spec_replace_player_min_exp; #define NEO_ASSAULT_SPRINT_MODIFIER 1.6 #define NEO_SUPPORT_SPRINT_MODIFIER 1.35 // Redundant, but for future usecases +constexpr float NEO_Min(float a, float b) { return a < b ? a : b; } + +// The slowest any class sprints. Support cannot sprint today, but its modifier is defined for future use, so it +// is counted. Anything that must stay under every sprint, such as a footstep rule, compares against this +static constexpr float NEO_MIN_SPRINT_MODIFIER = + NEO_Min(NEO_Min(NEO_RECON_SPRINT_MODIFIER, NEO_ASSAULT_SPRINT_MODIFIER), NEO_SUPPORT_SPRINT_MODIFIER); +static_assert(NEO_MIN_SPRINT_MODIFIER > 1.f, "Sprinting must be faster than running"); + // Aim Modifier #define NEO_AIM_MODIFIER 0.6 // Crouch/Walk Modifier From 4ca09599e6a5f82f54a72aa95120a867559c8a3b Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:42:11 -0700 Subject: [PATCH 04/12] Let every client see a crouch from the key press Groundwork for the footstep commits that follow. Nothing players hear changes here, and movement is untouched. The game only counts a player as crouched once the crouch animation ends, so for the first part of a crouch nothing a client can see says the player has pressed the key. The engine defines FL_ANIMDUCKING for exactly this, a crouch in progress, but nothing in this codebase set it. CGameMovement::Duck now sets it while the crouch key is down and clears it on release. It travels in the networked flags, so every client can tell a crouch has started before the crouched state arrives. Nothing reads it yet; footsteps use it in a later commit. --- src/game/shared/gamemovement.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/game/shared/gamemovement.cpp b/src/game/shared/gamemovement.cpp index e2f751263d..70299022f9 100644 --- a/src/game/shared/gamemovement.cpp +++ b/src/game/shared/gamemovement.cpp @@ -4717,6 +4717,18 @@ void CGameMovement::Duck( void ) mv->m_nOldButtons &= ~IN_DUCK; } +#ifdef NEO + // Networked to every client, so other players' footsteps can tell a crouch that is starting + if ( mv->m_nButtons & IN_DUCK ) + { + player->AddFlag( FL_ANIMDUCKING ); + } + else + { + player->RemoveFlag( FL_ANIMDUCKING ); + } +#endif + // Handle death. if ( IsDead() ) return; From a403c7b4f914ac45ce93bbf651c9e989b87e1827 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:42:11 -0700 Subject: [PATCH 05/12] Play the landing step on the tick you land Since #2151 the first footstep after a landing came late by a random amount, up to half a step interval, because the step timer restarted every tick while the player was in the air. The silent check could no longer return early, so UpdateStepSound reached SetStepSoundTime while airborne. Return before the timer is reset when off the ground and out of water, as the SDK does. The timer waits at zero in the air and the step lands on the same tick the player does. Ladders are unaffected. --- src/game/shared/baseplayer_shared.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 459cae9809..4788a39b01 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -567,6 +567,11 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri if ( m_flStepSoundTime > 0 ) return; +#ifdef NEO + // Off the ground the step timer waits at zero, as in the SDK, so the step lands on the tick the player does + if ( !fLadder && !( GetFlags() & FL_ONGROUND ) && GetWaterLevel() == WL_NotInWater ) + return; +#endif #ifndef NEO if ( GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) From a56f891f688e55a4a78636aa43869c8a4660e2c9 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:42:33 -0700 Subject: [PATCH 06/12] Make crouching silent Crouching alone was never silent. Only crouching while also aiming or walking could be, and only well under crouching speed, so plain crouching always made footsteps. Worse, those steps played at the loud volume, so crouching was louder than running; a later commit fixes the volume. Crouched steps are now silent while ground speed stays at or under FS_SILENT_SPEED_FRACTION, a fraction of the player's run speed with the weapon's weight speed modifier applied. The limit sits a little above crouching speed. That gap is the headroom: how much faster than crouching speed a player can go before a step plays. The modifier is on both sides of the comparison, so every gun gets the same headroom. Crouch sprinting, slides and pushes are faster than that and stay audible. Ladders are always audible: climb speed ignores posture and ground speed is near zero while climbing. Ground speed is used rather than full speed because other clients estimate vertical speed from origin changes on stairs, and full speed would make them disagree with the local player. This takes a side on parity: the original game plays crouched steps at any realistic crouching speed. --- src/game/shared/neo/neo_player_shared.cpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index f1327f9438..33d7651b2f 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -358,6 +358,16 @@ bool GetClNeoDisplayName(wchar_t (&pWszDisplayName)[NEO_MAX_DISPLAYNAME], return GetClNeoDisplayName(pWszDisplayName, wszNeoName, wszNeoClantag, flags); } +namespace +{ + // Footstep tuning. Fractions are of the player's run speed with the weapon's weight speed modifier applied + + // Crouching is silent until moving faster than this. Anything above it is a sprint, a slide or a push + constexpr float FS_SILENT_HEADROOM = 0.10f; + constexpr float FS_SILENT_SPEED_FRACTION = float(NEO_CROUCH_WALK_MODIFIER) * (1.f + FS_SILENT_HEADROOM); // 0.825 + static_assert(FS_SILENT_SPEED_FRACTION < 1.f, "Running must never be silent"); +} + bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLadder) const { if (GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) @@ -400,14 +410,21 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa // Changing movement direction, looking around, wall-running accelerate the player. Threshold should be lower than regular speed, but higher than walk/aim speed constexpr float SILENT_THRESHOLD_GRACE = 0.7f; - if (IsInSilentMoveMode()) + // A ladder has no ground entity, so crouching takes effect at once, but ground speed is near zero while climbing + if ((GetFlags() & FL_DUCKING) && !isOnLadder) { - const float flClassSpeed = (GetFlags() & FL_DUCKING) ? GetCrouchSpeed() : GetNormSpeed(); - if (speed <= (flClassSpeed * SILENT_THRESHOLD_GRACE)) + // Ground speed is the horizontal part of velocity. Full speed would differ by client: other clients rebuild a + // remote player's velocity from origin changes, and stairs move the origin up a step at a time, so they see a + // vertical spike the local player never does. Ground speed is the same everywhere, so every client agrees + if (groundspeed <= (GetNormSpeed_WithActiveWepEncumberment() * FS_SILENT_SPEED_FRACTION)) { return false; } } + else if (IsInSilentMoveMode() && speed <= (GetNormSpeed() * SILENT_THRESHOLD_GRACE)) + { + return false; + } return true; } From d7f8e336367aa667a32b3e1cfc02ef04d4ae0b6c Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:42:52 -0700 Subject: [PATCH 07/12] Make walking silent with every weapon Standing walk and aim were silent under a fraction of the class run speed that ignored the weapon's weight speed modifier. Heavy weapons slow you down, so the headroom was wide with a light gun and gone with a heavy one. Walking with the BALC was always audible. Walking speed is equal to crouching speed, so both now use the crouching rule: silent at or under FS_SILENT_SPEED_FRACTION of run speed with the weapon's weight speed modifier applied. Every gun now has the same headroom above walking speed. Lighter weapons keep the limit they had, heavier ones gain the headroom crouching has, and the BALC becomes silent while walking. Aim is slower than walking and keeps wide headroom, as before. --- src/game/shared/neo/neo_player_shared.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 33d7651b2f..534f154498 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -362,10 +362,11 @@ namespace { // Footstep tuning. Fractions are of the player's run speed with the weapon's weight speed modifier applied - // Crouching is silent until moving faster than this. Anything above it is a sprint, a slide or a push + // Crouching, walking and aiming are silent until moving faster than this. Anything above it is a sprint, a slide or a push constexpr float FS_SILENT_HEADROOM = 0.10f; constexpr float FS_SILENT_SPEED_FRACTION = float(NEO_CROUCH_WALK_MODIFIER) * (1.f + FS_SILENT_HEADROOM); // 0.825 static_assert(FS_SILENT_SPEED_FRACTION < 1.f, "Running must never be silent"); + static_assert(FS_SILENT_SPEED_FRACTION > NEO_AIM_MODIFIER, "Aiming must be silent"); } bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLadder) const @@ -407,11 +408,8 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa if (!moving_fast_enough || !(isOnLadder || (onground && movingalongground))) return false; - // Changing movement direction, looking around, wall-running accelerate the player. Threshold should be lower than regular speed, but higher than walk/aim speed - constexpr float SILENT_THRESHOLD_GRACE = 0.7f; - // A ladder has no ground entity, so crouching takes effect at once, but ground speed is near zero while climbing - if ((GetFlags() & FL_DUCKING) && !isOnLadder) + if (IsInSilentMoveMode() && !isOnLadder) { // Ground speed is the horizontal part of velocity. Full speed would differ by client: other clients rebuild a // remote player's velocity from origin changes, and stairs move the origin up a step at a time, so they see a @@ -421,18 +419,15 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa return false; } } - else if (IsInSilentMoveMode() && speed <= (GetNormSpeed() * SILENT_THRESHOLD_GRACE)) - { - return false; - } return true; } -// Walking and aiming are the silent movement modes, measured against the silent limit +// Crouching speed and walking speed are the same, so crouching, walking and aiming share one silent limit. +// Crouch counts once the crouch animation ends bool CNEO_Player::IsInSilentMoveMode() const { - return IsWalking() || IsInAim(); + return (GetFlags() & FL_DUCKING) || IsWalking() || IsInAim(); } // The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE From b07b08634e3b6e1c7811a37f3a059ccc713cf342 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:43:44 -0700 Subject: [PATCH 08/12] Fix rapid look and movement changes causing an accidental footstep Two things a crouching player does all the time, using up the available headroom: turning the view, and switching movement keys. Acceleration builds toward the new direction faster than friction removes the old one, so a player who looked around or sidestepped made a step they never intended. Turning the view. Track the view turn rate from the eye yaw, which every client receives, holding its peak and decaying over friction's time constant so the extra headroom lasts exactly as long as the speed bump it covers. The headroom grows with the turn rate up to FS_TURN_HEADROOM, reaching it at FS_TURN_RATE_FOR_FULL_HEADROOM. Sprinting gets no extra headroom, so crouch sprinting stays audible mid-turn. A yaw change over FS_TURN_SNAP_DEGREES in one sample is a snap, such as a respawn or a remote player leaving dormancy, and is ignored. Switching movement keys. A switch from W+D to W+A raises speed the same way without moving the view, so the turn tracking cannot see it. One switch is a correction. Repeating it is wiggle-walking, which gains speed and should be heard. Once a player has been crouched, walking or aiming on the ground long enough to have settled, and is not sprinting, the first step that is over the headroom but only slightly too fast is forgiven. The next one plays. The forgiveness comes back after a short stretch under the limit, so a brief wiggle stays silent and a repeated one does not. The settle time keeps slides from a run and landings audible. The speed cap keeps sprints and slides from being forgiven. --- src/game/client/neo/c_neo_player.cpp | 6 ++ src/game/client/neo/c_neo_player.h | 13 +++ src/game/server/neo/neo_player.h | 12 +++ src/game/shared/baseplayer_shared.cpp | 6 ++ src/game/shared/neo/neo_player_shared.cpp | 97 ++++++++++++++++++++++- 5 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index 2010f66563..9cae6ac9cd 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -123,6 +123,12 @@ BEGIN_PREDICTION_DATA(C_NEO_Player) DEFINE_PRED_FIELD_TOL(m_flVisionLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flJumpLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flNextPingTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), + DEFINE_FIELD(m_flFootstepTurnRate, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepPrevYaw, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepTurnTime, FIELD_FLOAT), + DEFINE_FIELD(m_bFootstepForgivenessReady, FIELD_BOOLEAN), + DEFINE_FIELD(m_flSilentModeOnGroundTime, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepQuietTime, FIELD_FLOAT), END_PREDICTION_DATA() static void __MsgFunc_IdleRespawnShowMenu(bf_read &) diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 4ac38cfb1e..4a8e36f281 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -96,6 +96,9 @@ class C_NEO_Player : public C_HL2MP_Player bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; + void UpdateFootstepTurnRate(); + void UpdateFootstepForgiveness(bool bLoud); + bool ForgiveStep(); void Lean(void); @@ -280,6 +283,16 @@ class C_NEO_Player : public C_HL2MP_Player private: C_NEO_Player(const C_NEO_Player &); + + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate + float m_flFootstepTurnRate = 0.f; + float m_flFootstepPrevYaw = 0.f; + float m_flFootstepTurnTime = 0.f; + + // First step forgiveness, see ForgiveStep + bool m_bFootstepForgivenessReady = true; + float m_flSilentModeOnGroundTime = -1.f; + float m_flFootstepQuietTime = -1.f; }; inline C_NEO_Player *ToNEOPlayer(C_BaseEntity *pEntity) diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index ed1e52a047..4c3cbea072 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -203,6 +203,9 @@ class CNEO_Player : public CHL2MP_Player bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; + void UpdateFootstepTurnRate(); + void UpdateFootstepForgiveness(bool bLoud); + bool ForgiveStep(); // Cloak Power Interface float CloakPower_Get(void) const ; @@ -379,6 +382,15 @@ class CNEO_Player : public CHL2MP_Player CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerTarget); CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerImpersonatingMe); + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate + float m_flFootstepTurnRate = 0.f; + float m_flFootstepPrevYaw = 0.f; + float m_flFootstepTurnTime = 0.f; + + // First step forgiveness, see ForgiveStep + bool m_bFootstepForgivenessReady = true; + float m_flSilentModeOnGroundTime = -1.f; + float m_flFootstepQuietTime = -1.f; }; inline CNEO_Player *ToNEOPlayer(CBaseEntity *pEntity) diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 4788a39b01..18286a7bc5 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -552,7 +552,9 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri #ifdef NEO CNEO_Player *pNeoPlayer = assert_cast(this); + pNeoPlayer->UpdateFootstepTurnRate(); const bool bSilent = !pNeoPlayer->IsMakingFootstepSounds(&velrun, &speed, &fLadder); + pNeoPlayer->UpdateFootstepForgiveness(!bSilent); #endif @@ -720,6 +722,10 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri fvol *= 0.65; #endif } +#ifdef NEO + if (pNeoPlayer->ForgiveStep()) + return; +#endif PlayStepSound( feet, psurface, fvol, false ); } diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 534f154498..517c634585 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -367,6 +367,16 @@ namespace constexpr float FS_SILENT_SPEED_FRACTION = float(NEO_CROUCH_WALK_MODIFIER) * (1.f + FS_SILENT_HEADROOM); // 0.825 static_assert(FS_SILENT_SPEED_FRACTION < 1.f, "Running must never be silent"); static_assert(FS_SILENT_SPEED_FRACTION > NEO_AIM_MODIFIER, "Aiming must be silent"); + + // Turning while moving raises ground speed by up to about this much of crouching speed at a fast turn, so the + // headroom grows with the view turn rate until it covers that. Full headroom arrives at FS_TURN_RATE_FOR_FULL_HEADROOM + constexpr float FS_TURN_HEADROOM = float(NEO_CROUCH_WALK_MODIFIER) * 0.45f; // 0.3375 + constexpr float FS_TURN_RATE_FOR_FULL_HEADROOM = 180.f; + + // Forgiveness only applies to a small overshoot. A direction change adds at most as much speed as a fast turn, + // so the cap is the silent limit plus the full turn headroom. Anything faster is a sprint or a slide + constexpr float FS_FORGIVENESS_MAX_SPEED_FRACTION = FS_SILENT_SPEED_FRACTION + FS_TURN_HEADROOM; // 1.1625 + static_assert(FS_FORGIVENESS_MAX_SPEED_FRACTION < NEO_MIN_SPRINT_MODIFIER, "A sprint at full speed must never be forgiven"); } bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLadder) const @@ -411,10 +421,16 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa // A ladder has no ground entity, so crouching takes effect at once, but ground speed is near zero while climbing if (IsInSilentMoveMode() && !isOnLadder) { + float silentThreshold = FS_SILENT_SPEED_FRACTION; + // Sprinting stays audible while turning + if (!m_fIsSprinting) + { + silentThreshold += FS_TURN_HEADROOM * Min(m_flFootstepTurnRate / FS_TURN_RATE_FOR_FULL_HEADROOM, 1.f); + } // Ground speed is the horizontal part of velocity. Full speed would differ by client: other clients rebuild a // remote player's velocity from origin changes, and stairs move the origin up a step at a time, so they see a // vertical spike the local player never does. Ground speed is the same everywhere, so every client agrees - if (groundspeed <= (GetNormSpeed_WithActiveWepEncumberment() * FS_SILENT_SPEED_FRACTION)) + if (groundspeed <= (GetNormSpeed_WithActiveWepEncumberment() * silentThreshold)) { return false; } @@ -430,6 +446,19 @@ bool CNEO_Player::IsInSilentMoveMode() const return (GetFlags() & FL_DUCKING) || IsWalking() || IsInAim(); } +// Starts a timestamp the first time a condition holds and clears it when it stops +static void TrackSince(float &flSince, bool bActive, float flNow) +{ + if (!bActive) + { + flSince = -1.f; + } + else if (flSince < 0.f) + { + flSince = flNow; + } +} + // The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const { @@ -444,6 +473,72 @@ float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLou } } +// Tracks what ForgiveStep needs: how long the player has been settled into a silent mode on the ground, and +// when a used forgiveness is earned back +void CNEO_Player::UpdateFootstepForgiveness(bool bLoud) +{ + // Time under the silent limit that earns the forgiveness back + constexpr float FS_FORGIVENESS_RECHARGE_TIME = 1.f; + + const float flNow = gpGlobals->curtime; + TrackSince(m_flSilentModeOnGroundTime, IsInSilentMoveMode() && (GetFlags() & FL_ONGROUND), flNow); + TrackSince(m_flFootstepQuietTime, !bLoud, flNow); + if (!bLoud && !m_bFootstepForgivenessReady && flNow - m_flFootstepQuietTime >= FS_FORGIVENESS_RECHARGE_TIME) + { + m_bFootstepForgivenessReady = true; + } +} + +// Forgiveness: one step that is only slightly over the silent limit is skipped instead of played, for a player +// settled into crouching, walking or aiming. It covers a direction change, which raises ground speed the same +// way turning does but without moving the view, so the turn headroom cannot see it. The forgiveness is spent +// on use and earned back after a short stretch under the limit, so one correction stays silent and +// wiggle-walking does not +bool CNEO_Player::ForgiveStep() +{ + // Settled into the crouch, walk or aim first, so slides from a run or a landing stay audible + constexpr float FS_FORGIVENESS_SETTLE_TIME = 0.5f; + + if (!m_bFootstepForgivenessReady || m_fIsSprinting || m_flSilentModeOnGroundTime < 0.f || + gpGlobals->curtime - m_flSilentModeOnGroundTime < FS_FORGIVENESS_SETTLE_TIME) + { + return false; + } + + if (GetAbsVelocity().Length2D() > GetNormSpeed_WithActiveWepEncumberment() * FS_FORGIVENESS_MAX_SPEED_FRACTION) + { + return false; + } + + m_bFootstepForgivenessReady = false; + return true; +} + +// Acceleration builds toward a new view direction faster than friction removes the old one, +// so turning while moving raises ground speed until friction catches up +void CNEO_Player::UpdateFootstepTurnRate() +{ + // Friction's time constant, 1 / sv_friction, so the extra headroom fades as fast as the speed bump it covers + constexpr float FS_TURN_RATE_DECAY_TIME = 0.25f; + + const float flNow = gpGlobals->curtime; + const float flDt = flNow - m_flFootstepTurnTime; + if (flDt <= 0.f) + { + return; + } + + // A larger change in one sample is a snap, such as a respawn, a spectator takeover or a remote player leaving dormancy + constexpr float FS_TURN_SNAP_DEGREES = 90.f; + + const float flYaw = EyeAngles()[YAW]; + const float flYawDelta = fabsf(AngleDiff(flYaw, m_flFootstepPrevYaw)); + const float flRate = (flDt < 0.5f && flYawDelta <= FS_TURN_SNAP_DEGREES) ? flYawDelta / flDt : 0.f; + m_flFootstepTurnRate = Max(flRate, m_flFootstepTurnRate * expf(-flDt / FS_TURN_RATE_DECAY_TIME)); + m_flFootstepPrevYaw = flYaw; + m_flFootstepTurnTime = flNow; +} + int GetRank(const int xp) { int iRank = NEO_RANK_RANKLESS_DOG; From aebd574e7e3900c8757b6e1302e3b0f63f173048 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:44:08 -0700 Subject: [PATCH 09/12] Stop crouched footsteps being louder than running Crouching under the silent limit makes no step since an earlier commit. This is about the crouched steps that still play: crouch sprints, slides, pushes, and crouching that goes over its headroom. Each step plays at one of two volumes, soft or loud, and the SDK picks between them by speed, using limits tuned for its own slow crouch. Every NEO crouch is faster than the SDK's crouched limit, so every crouched step that played picked loud, and even after the SDK's crouched volume reduction it was louder than a standing run. A heavy weapon could also drop a standing sprint under the SDK's standing limit and make it soft. Speed no longer picks the volume. Sprinting picks loud and everything else picks soft, then the SDK's crouched reduction applies as before. The end state, on a surface where soft is 0.2 and loud is 0.5: run soft 0.2 unchanged sprint loud 0.5 unchanged crouch sprint loud, crouched 0.325 unchanged other audible crouched step soft, crouched 0.13 was 0.325 crouching, walking, aiming silent unchanged The step interval still follows speed. --- src/game/client/neo/c_neo_player.h | 1 + src/game/server/neo/neo_player.h | 1 + src/game/shared/baseplayer_shared.cpp | 9 +++++++-- src/game/shared/neo/neo_player_shared.cpp | 7 +++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 4a8e36f281..284d9e1802 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -95,6 +95,7 @@ class C_NEO_Player : public C_HL2MP_Player virtual void StopWalking(void); bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; + bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); void UpdateFootstepForgiveness(bool bLoud); diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 4c3cbea072..70f7445111 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -202,6 +202,7 @@ class CNEO_Player : public CHL2MP_Player virtual void StopWalking(void) OVERRIDE; bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; + bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); void UpdateFootstepForgiveness(bool bLoud); diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 18286a7bc5..21b78fa4f4 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -614,6 +614,11 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri #endif bWalking = speed < velrun; +#ifdef NEO + const bool bLoudStep = pNeoPlayer->IsLoudFootstep(); +#else + const bool bLoudStep = !bWalking; +#endif #ifndef NEO VectorCopy( vecOrigin, knee ); @@ -659,7 +664,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri else if ( GetWaterLevel() == WL_Feet ) { psurface = physprops->GetSurfaceData( physprops->GetSurfaceIndex( "water" ) ); - fvol = bWalking ? 0.2 : 0.5; + fvol = bLoudStep ? 0.5 : 0.2; SetStepSoundTime( STEPSOUNDTIME_WATER_FOOT, bWalking ); } @@ -671,7 +676,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri SetStepSoundTime( STEPSOUNDTIME_NORMAL, bWalking ); #ifdef NEO - fvol = pNeoPlayer->GetSurfaceStepVolume(psurface, !bWalking); + fvol = pNeoPlayer->GetSurfaceStepVolume(psurface, bLoudStep); #else switch ( psurface->game.material ) { diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 517c634585..8d27f54b8a 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -459,6 +459,13 @@ static void TrackSince(float &flSince, bool bActive, float flNow) } } +// The SDK picks the loud step volume by speed, with limits tuned for its slow crouch that every NEO crouch exceeds. +// Pick it by intent instead: only sprinting is loud. The SDK's crouched volume reduction still applies on top +bool CNEO_Player::IsLoudFootstep() const +{ + return m_fIsSprinting; +} + // The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const { From d7b44f47852747301178f147f1785c5db0a33b23 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:44:40 -0700 Subject: [PATCH 10/12] Hold the first footstep after pressing crouch, walk or aim The game only counts a player as crouched once the crouch animation ends, and they keep run speed until then, so a running step almost always played after pressing crouch. Walk and aim take effect at once but can catch a step the same way. The player did the right thing and got punished for it, and it reads as lag. Pressing any of the three now arms the next audible step. Instead of playing, it is held. It is dropped once the player has settled into the silent mode, and played if they are still loud a moment after the mode took effect, as in a slide, a push or a crouch sprint. Letting go of the arming key while the step is held plays it. Each key arms from its own press, so crouch pressed while still aiming counts. Leaving the ground or grabbing a ladder forgets the held step. A held step plays at the volume for how the player is moving when it plays, and keeps the next step a short gap away without restarting the cadence. Crouch is read from the key press flag so other clients see the same thing. On its own this opens a speed exploit: a player can tap crouch, settle and release in a rhythm that drops one step per cycle while moving at run speed for most of it, which is silent movement at near run speed. The next commit closes it. --- src/game/client/neo/c_neo_player.cpp | 4 + src/game/client/neo/c_neo_player.h | 12 ++ src/game/server/neo/neo_player.h | 12 ++ src/game/shared/baseplayer_shared.cpp | 3 + src/game/shared/neo/neo_player_shared.cpp | 148 +++++++++++++++++++++- src/game/shared/neo/neo_player_shared.h | 16 +++ 6 files changed, 194 insertions(+), 1 deletion(-) diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index 9cae6ac9cd..a5c81f6f96 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -123,6 +123,10 @@ BEGIN_PREDICTION_DATA(C_NEO_Player) DEFINE_PRED_FIELD_TOL(m_flVisionLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flJumpLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flNextPingTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), + DEFINE_FIELD(m_iHeldStep, FIELD_INTEGER), + DEFINE_FIELD(m_nHeldStepKeysDown, FIELD_INTEGER), + DEFINE_FIELD(m_nHeldStepArmedKeys, FIELD_INTEGER), + DEFINE_FIELD(m_flHeldStepQuietTime, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepTurnRate, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepPrevYaw, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepTurnTime, FIELD_FLOAT), diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 284d9e1802..0ac493cf66 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -95,11 +95,15 @@ class C_NEO_Player : public C_HL2MP_Player virtual void StopWalking(void); bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; + int GetSilentMoveKeys(bool bCrouchFromPress) const; + bool IsOnFootstepGround() const; bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); + void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); void UpdateFootstepForgiveness(bool bLoud); bool ForgiveStep(); + bool HoldStep(surfacedata_t *psurface, const Vector &vecOrigin); void Lean(void); @@ -285,6 +289,14 @@ class C_NEO_Player : public C_HL2MP_Player private: C_NEO_Player(const C_NEO_Player &); + // Held step: the first audible step after pressing crouch, walk or aim, see UpdateHeldStep + bool IsHeldStepPending() const; + void PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin); + int m_iHeldStep = NEO_FS_HELD_NONE; + int m_nHeldStepKeysDown = 0; + int m_nHeldStepArmedKeys = 0; + float m_flHeldStepQuietTime = -1.f; + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; float m_flFootstepPrevYaw = 0.f; diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 70f7445111..85106b7104 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -202,11 +202,15 @@ class CNEO_Player : public CHL2MP_Player virtual void StopWalking(void) OVERRIDE; bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; bool IsInSilentMoveMode() const; + int GetSilentMoveKeys(bool bCrouchFromPress) const; + bool IsOnFootstepGround() const; bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); + void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); void UpdateFootstepForgiveness(bool bLoud); bool ForgiveStep(); + bool HoldStep(surfacedata_t *psurface, const Vector &vecOrigin); // Cloak Power Interface float CloakPower_Get(void) const ; @@ -383,6 +387,14 @@ class CNEO_Player : public CHL2MP_Player CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerTarget); CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerImpersonatingMe); + // Held step: the first audible step after pressing crouch, walk or aim, see UpdateHeldStep + bool IsHeldStepPending() const; + void PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin); + int m_iHeldStep = NEO_FS_HELD_NONE; + int m_nHeldStepKeysDown = 0; + int m_nHeldStepArmedKeys = 0; + float m_flHeldStepQuietTime = -1.f; + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; float m_flFootstepPrevYaw = 0.f; diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 21b78fa4f4..81de734596 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -554,6 +554,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri CNEO_Player *pNeoPlayer = assert_cast(this); pNeoPlayer->UpdateFootstepTurnRate(); const bool bSilent = !pNeoPlayer->IsMakingFootstepSounds(&velrun, &speed, &fLadder); + pNeoPlayer->UpdateHeldStep(!bSilent, psurface, vecOrigin); pNeoPlayer->UpdateFootstepForgiveness(!bSilent); #endif @@ -728,6 +729,8 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri #endif } #ifdef NEO + if (pNeoPlayer->HoldStep(psurface, feet)) + return; if (pNeoPlayer->ForgiveStep()) return; #endif diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 8d27f54b8a..b4f3e9bd98 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -443,7 +443,16 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa // Crouch counts once the crouch animation ends bool CNEO_Player::IsInSilentMoveMode() const { - return (GetFlags() & FL_DUCKING) || IsWalking() || IsInAim(); + return GetSilentMoveKeys(false) != 0; +} + +// The same modes as a key mask. Crouch counts from the key press (FL_ANIMDUCKING, see CGameMovement::Duck) or once the +// crouch animation ends (FL_DUCKING); walking and aiming take effect at once either way +int CNEO_Player::GetSilentMoveKeys(bool bCrouchFromPress) const +{ + return ((GetFlags() & (bCrouchFromPress ? FL_ANIMDUCKING : FL_DUCKING)) ? NEO_FS_SILENT_MOVE_KEY_CROUCH : 0) | + (IsWalking() ? NEO_FS_SILENT_MOVE_KEY_WALK : 0) | + (IsInAim() ? NEO_FS_SILENT_MOVE_KEY_AIM : 0); } // Starts a timestamp the first time a condition holds and clears it when it stops @@ -546,6 +555,143 @@ void CNEO_Player::UpdateFootstepTurnRate() m_flFootstepTurnTime = flNow; } +// On the ground and not climbing. A ladder has no ground entity, and its steps are always audible +bool CNEO_Player::IsOnFootstepGround() const +{ + return (GetFlags() & FL_ONGROUND) && GetMoveType() != MOVETYPE_LADDER; +} + +// A step is armed or already held back +bool CNEO_Player::IsHeldStepPending() const +{ + return m_iHeldStep == NEO_FS_HELD_ARMED || m_iHeldStep == NEO_FS_HELD_HELD; +} + +// Crouch only counts once the crouch animation ends, so without this one running step plays after pressing crouch. +// The step is held until the player settles into the crouch, walk or aim, or lets go of the key +void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin) +{ + // Time to slow down under the silent limit after crouching, walking or aiming takes effect + constexpr float FS_HELD_STEP_SETTLE_TIME = 0.1f; + + // Each key arms from its own press, as pressing crouch while still aiming keeps run speed through the crouch animation + const int nKeysDown = GetSilentMoveKeys(true); + const int nPressed = nKeysDown & ~m_nHeldStepKeysDown; + m_nHeldStepKeysDown = nKeysDown; + + // The held step only settles or plays on the ground. A jump lands with its own sound and restarts the step cadence, + // and a ladder has every step audible, so a step held across either would swallow the steps that follow + if (!IsAlive() || !IsOnFootstepGround()) + { + m_iHeldStep = NEO_FS_HELD_NONE; + m_nHeldStepArmedKeys = 0; + return; + } + + const float flNow = gpGlobals->curtime; + // Letting go of a key that armed the step is the release. A step that is still held plays now: holding it back + // delayed it, releasing early means the player never settled, so it was never earned + if (m_iHeldStep != NEO_FS_HELD_NONE && (m_nHeldStepArmedKeys & ~nKeysDown)) + { + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + PlayHeldStep(psurface, vecOrigin); + } + m_iHeldStep = NEO_FS_HELD_NONE; + m_nHeldStepArmedKeys = 0; + } + + // Already crouched: walking or aiming does not raise the threshold, so there is no step to hold + if (nPressed && !(GetFlags() & FL_DUCKING)) + { + if (IsHeldStepPending()) + { + m_nHeldStepArmedKeys |= nPressed; + } + else + { + m_iHeldStep = NEO_FS_HELD_ARMED; + m_nHeldStepArmedKeys = nPressed; + m_flHeldStepQuietTime = -1.f; + } + } + + if (IsHeldStepPending()) + { + // Has a mode the player pressed taken effect yet. It must be one that armed the step, not any silent mode the + // player was already in: pressing crouch while aiming, the aim is already silent but the crouch only counts + // once its animation ends. Until then the player is still at run speed, and the step that would play is the + // accidental running step this hold exists to stop + const bool bArmedModeActive = (m_nHeldStepArmedKeys & GetSilentMoveKeys(false)) != 0; + TrackSince(m_flHeldStepQuietTime, bArmedModeActive, flNow); + + if (bArmedModeActive) + { + if (!bLoud) + { + // Settled into a silent crouch, walk or aim: the held step is dropped + m_iHeldStep = NEO_FS_HELD_NONE; + } + else if (flNow - m_flHeldStepQuietTime >= FS_HELD_STEP_SETTLE_TIME) + { + // Still loud after crouching, walking or aiming took effect, such as a slide, a push or a crouch sprint + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + PlayHeldStep(psurface, vecOrigin); + } + m_iHeldStep = NEO_FS_HELD_NONE; + } + } + } +} + +// Returns true if this audible step is held back instead of played +bool CNEO_Player::HoldStep(surfacedata_t *psurface, const Vector &vecOrigin) +{ + if (!IsHeldStepPending()) + { + return false; + } + + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + // Crouching or walking took effect and speed is still dropping, so keep waiting on the same step + if (m_flHeldStepQuietTime >= 0.f) + { + return true; + } + + // Another step before crouching takes effect: the earlier one plays and this one waits, so no step is skipped + PlayHeldStep(psurface, vecOrigin); + } + + m_iHeldStep = NEO_FS_HELD_HELD; + return true; +} + +void CNEO_Player::PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin) +{ + // Only reached on the ground, where the surface was just traced; without one there is no step to play + if (!psurface) + { + return; + } + // The volume for how the player moves now, not when the step was held: crouching, sprinting or releasing aim since + // then changes it, and the listener hears the step with the player as they are + float flVolume = GetSurfaceStepVolume(psurface, IsLoudFootstep()); + if (GetFlags() & FL_DUCKING) + { + flVolume *= NEO_FS_CROUCH_VOLUME_SCALE; + } + Vector vecFeet = vecOrigin; + PlayStepSound(vecFeet, psurface, flVolume, false); + + // Keep the next step off the top of this one without restarting the step cadence, + // as a full interval here lets tapping crouch push every step back and skip half of them + constexpr float FS_HELD_STEP_MIN_GAP_MS = 100.f; + m_flStepSoundTime = Max(m_flStepSoundTime, FS_HELD_STEP_MIN_GAP_MS); +} + int GetRank(const int xp) { int iRank = NEO_RANK_RANKLESS_DOG; diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 3c9b5f3e5e..29d86fb573 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -55,6 +55,22 @@ static_assert(NEO_MIN_SPRINT_MODIFIER > 1.f, "Sprinting must be faster than runn // Footsteps: the SDK plays crouched steps at 65% volume #define NEO_FS_CROUCH_VOLUME_SCALE 0.65f +// Held step: the first audible step after pressing crouch, walk or aim waits until the player settles or lets go +enum NeoFootstepHeldState +{ + NEO_FS_HELD_NONE = 0, + NEO_FS_HELD_ARMED, // Crouch, walk or aim pressed, the next audible step is held + NEO_FS_HELD_HELD, // A step is held back +}; + +// Keys that start a silent movement mode, see CNEO_Player::GetSilentMoveKeys +enum NeoFootstepSilentMoveKey +{ + NEO_FS_SILENT_MOVE_KEY_CROUCH = (1 << 0), + NEO_FS_SILENT_MOVE_KEY_WALK = (1 << 1), + NEO_FS_SILENT_MOVE_KEY_AIM = (1 << 2), +}; + // Movement Calculations // Recon From 1c1507021dd02889ea9cee591e89a385d2c76433 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:45:00 -0700 Subject: [PATCH 11/12] Tapping crouch, walk or aim delays a step, never skips one Closes the speed exploit the previous commit opened: dropping a step per crouch tap while moving at run speed. A held step that was dropped on settling now plays if the arming key is released within one step interval of the drop, unless another silent mode is still held, so going from crouch to walk is not a tap. A dropped step then always costs a real step's worth of silent movement, and there is no rhythm of taps that moves at run speed for free. --- src/game/client/neo/c_neo_player.cpp | 2 ++ src/game/client/neo/c_neo_player.h | 2 ++ src/game/server/neo/neo_player.h | 2 ++ src/game/shared/neo/neo_player_shared.cpp | 28 +++++++++++++++++++---- src/game/shared/neo/neo_player_shared.h | 1 + 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index a5c81f6f96..b803f42afc 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -127,6 +127,8 @@ BEGIN_PREDICTION_DATA(C_NEO_Player) DEFINE_FIELD(m_nHeldStepKeysDown, FIELD_INTEGER), DEFINE_FIELD(m_nHeldStepArmedKeys, FIELD_INTEGER), DEFINE_FIELD(m_flHeldStepQuietTime, FIELD_FLOAT), + DEFINE_FIELD(m_flHeldStepDropTime, FIELD_FLOAT), + DEFINE_FIELD(m_flHeldStepWindow, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepTurnRate, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepPrevYaw, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepTurnTime, FIELD_FLOAT), diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 0ac493cf66..01305f1750 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -296,6 +296,8 @@ class C_NEO_Player : public C_HL2MP_Player int m_nHeldStepKeysDown = 0; int m_nHeldStepArmedKeys = 0; float m_flHeldStepQuietTime = -1.f; + float m_flHeldStepDropTime = 0.f; + float m_flHeldStepWindow = 0.f; // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 85106b7104..96484558e4 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -394,6 +394,8 @@ class CNEO_Player : public CHL2MP_Player int m_nHeldStepKeysDown = 0; int m_nHeldStepArmedKeys = 0; float m_flHeldStepQuietTime = -1.f; + float m_flHeldStepDropTime = 0.f; + float m_flHeldStepWindow = 0.f; // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index b4f3e9bd98..76d1f67022 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -568,7 +568,7 @@ bool CNEO_Player::IsHeldStepPending() const } // Crouch only counts once the crouch animation ends, so without this one running step plays after pressing crouch. -// The step is held until the player settles into the crouch, walk or aim, or lets go of the key +// Releasing early gives the step back, so tapping crouch, walk or aim can delay a step but never skip one. void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin) { // Time to slow down under the silent limit after crouching, walking or aiming takes effect @@ -590,10 +590,14 @@ void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vect const float flNow = gpGlobals->curtime; // Letting go of a key that armed the step is the release. A step that is still held plays now: holding it back - // delayed it, releasing early means the player never settled, so it was never earned + // delayed it, releasing early means the player never settled, so it was never earned. A step that was already + // dropped plays only if the release came soon enough and no other silent mode is still held, so going from + // crouch to walk is not a tap if (m_iHeldStep != NEO_FS_HELD_NONE && (m_nHeldStepArmedKeys & ~nKeysDown)) { - if (m_iHeldStep == NEO_FS_HELD_HELD) + const bool bStillQuiet = (GetSilentMoveKeys(false) & nKeysDown) != 0; + if (m_iHeldStep == NEO_FS_HELD_HELD || + (m_iHeldStep == NEO_FS_HELD_DROPPED && !bStillQuiet && flNow - m_flHeldStepDropTime <= m_flHeldStepWindow)) { PlayHeldStep(psurface, vecOrigin); } @@ -629,8 +633,16 @@ void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vect { if (!bLoud) { - // Settled into a silent crouch, walk or aim: the held step is dropped - m_iHeldStep = NEO_FS_HELD_NONE; + // Settled into a silent crouch, walk or aim + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + m_iHeldStep = NEO_FS_HELD_DROPPED; + m_flHeldStepDropTime = flNow; + } + else + { + m_iHeldStep = NEO_FS_HELD_NONE; + } } else if (flNow - m_flHeldStepQuietTime >= FS_HELD_STEP_SETTLE_TIME) { @@ -643,6 +655,10 @@ void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vect } } } + else if (m_iHeldStep == NEO_FS_HELD_DROPPED && flNow - m_flHeldStepDropTime > m_flHeldStepWindow) + { + m_iHeldStep = NEO_FS_HELD_NONE; + } } // Returns true if this audible step is held back instead of played @@ -666,6 +682,8 @@ bool CNEO_Player::HoldStep(surfacedata_t *psurface, const Vector &vecOrigin) } m_iHeldStep = NEO_FS_HELD_HELD; + // Releasing within one step interval of dropping the step still plays it + m_flHeldStepWindow = m_flStepSoundTime * 0.001f; return true; } diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 29d86fb573..35d5a29f27 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -61,6 +61,7 @@ enum NeoFootstepHeldState NEO_FS_HELD_NONE = 0, NEO_FS_HELD_ARMED, // Crouch, walk or aim pressed, the next audible step is held NEO_FS_HELD_HELD, // A step is held back + NEO_FS_HELD_DROPPED, // The held step was dropped, but plays if the key is released soon after }; // Keys that start a silent movement mode, see CNEO_Player::GetSilentMoveKeys From 2699d6fbf2bedf568d3d2f7dfd07d5e7569b9ba2 Mon Sep 17 00:00:00 2001 From: Dan Peavey Date: Tue, 27 Oct 2026 18:45:18 -0700 Subject: [PATCH 12/12] Keep the step on landing audible in every posture Closes a speed exploit: a silent bunny hop. A flat jump falls too slowly for the engine's landing sound, so the footstep players hear on landing is the regular step whose timer ran out in the air. With crouching now a silent mode, a crouched landing at crouching speed made no sound, so a player could cover ground at jump speed with no step at all. After at least FS_LANDING_MIN_AIR_TIME off the ground, the next step decision is never silent, whatever the posture. Shorter than that is a stair edge or a bump. A standing jump in place still makes nothing, since it is under the minimum step speed. --- src/game/client/neo/c_neo_player.cpp | 2 ++ src/game/client/neo/c_neo_player.h | 6 ++++++ src/game/server/neo/neo_player.h | 6 ++++++ src/game/shared/baseplayer_shared.cpp | 2 ++ src/game/shared/neo/neo_player_shared.cpp | 25 +++++++++++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index b803f42afc..6f81470c63 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -129,6 +129,8 @@ BEGIN_PREDICTION_DATA(C_NEO_Player) DEFINE_FIELD(m_flHeldStepQuietTime, FIELD_FLOAT), DEFINE_FIELD(m_flHeldStepDropTime, FIELD_FLOAT), DEFINE_FIELD(m_flHeldStepWindow, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepAirTime, FIELD_FLOAT), + DEFINE_FIELD(m_bFootstepLanding, FIELD_BOOLEAN), DEFINE_FIELD(m_flFootstepTurnRate, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepPrevYaw, FIELD_FLOAT), DEFINE_FIELD(m_flFootstepTurnTime, FIELD_FLOAT), diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 01305f1750..9090ff475a 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -100,6 +100,8 @@ class C_NEO_Player : public C_HL2MP_Player bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); + void UpdateFootstepLanding(); + void OnFootstepDecided(); void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); void UpdateFootstepForgiveness(bool bLoud); bool ForgiveStep(); @@ -299,6 +301,10 @@ class C_NEO_Player : public C_HL2MP_Player float m_flHeldStepDropTime = 0.f; float m_flHeldStepWindow = 0.f; + // Landing, see UpdateFootstepLanding + float m_flFootstepAirTime = -1.f; + bool m_bFootstepLanding = false; + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; float m_flFootstepPrevYaw = 0.f; diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 96484558e4..ea92489d87 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -207,6 +207,8 @@ class CNEO_Player : public CHL2MP_Player bool IsLoudFootstep() const; float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; void UpdateFootstepTurnRate(); + void UpdateFootstepLanding(); + void OnFootstepDecided(); void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); void UpdateFootstepForgiveness(bool bLoud); bool ForgiveStep(); @@ -397,6 +399,10 @@ class CNEO_Player : public CHL2MP_Player float m_flHeldStepDropTime = 0.f; float m_flHeldStepWindow = 0.f; + // Landing, see UpdateFootstepLanding + float m_flFootstepAirTime = -1.f; + bool m_bFootstepLanding = false; + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate float m_flFootstepTurnRate = 0.f; float m_flFootstepPrevYaw = 0.f; diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 81de734596..5d1dbdd7d9 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -553,6 +553,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri #ifdef NEO CNEO_Player *pNeoPlayer = assert_cast(this); pNeoPlayer->UpdateFootstepTurnRate(); + pNeoPlayer->UpdateFootstepLanding(); const bool bSilent = !pNeoPlayer->IsMakingFootstepSounds(&velrun, &speed, &fLadder); pNeoPlayer->UpdateHeldStep(!bSilent, psurface, vecOrigin); pNeoPlayer->UpdateFootstepForgiveness(!bSilent); @@ -571,6 +572,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri if ( m_flStepSoundTime > 0 ) return; #ifdef NEO + pNeoPlayer->OnFootstepDecided(); // Off the ground the step timer waits at zero, as in the SDK, so the step lands on the tick the player does if ( !fLadder && !( GetFlags() & FL_ONGROUND ) && GetWaterLevel() == WL_NotInWater ) return; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 76d1f67022..1cd2928ef5 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -418,6 +418,10 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa if (!moving_fast_enough || !(isOnLadder || (onground && movingalongground))) return false; + // Landing from a jump or a drop makes a footstep whatever the posture, see UpdateFootstepLanding + if (m_bFootstepLanding) + return true; + // A ladder has no ground entity, so crouching takes effect at once, but ground speed is near zero while climbing if (IsInSilentMoveMode() && !isOnLadder) { @@ -530,6 +534,27 @@ bool CNEO_Player::ForgiveStep() return true; } +// Marks a landing after a jump or a drop. The mark lasts until the next step decision: after a jump the step timer +// has run out in the air, so that is the landing tick; after a short drop it is the first step due +void CNEO_Player::UpdateFootstepLanding() +{ + // Shorter than this is a stair edge or a bump, not a drop + constexpr float FS_LANDING_MIN_AIR_TIME = 0.1f; + + const float flNow = gpGlobals->curtime; + const bool bInAir = !IsOnFootstepGround(); + if (!bInAir && m_flFootstepAirTime >= 0.f && flNow - m_flFootstepAirTime >= FS_LANDING_MIN_AIR_TIME) + { + m_bFootstepLanding = true; + } + TrackSince(m_flFootstepAirTime, bInAir, flNow); +} + +void CNEO_Player::OnFootstepDecided() +{ + m_bFootstepLanding = false; +} + // Acceleration builds toward a new view direction faster than friction removes the old one, // so turning while moving raises ground speed until friction catches up void CNEO_Player::UpdateFootstepTurnRate()