From 14c348682fa382ec92a7f28574225bcaed2d2ad8 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 22:55:00 -0600 Subject: [PATCH 01/60] Null-check the objective carrier in ghost boundary CNEORules::Think() refreshes the ghoster index only after every entity has thought, so in the frame after the ghost carrier disconnects (a kick, neo_bot_quota 0) the boundary's Think() still reads the old index. UTIL_PlayerByIndex() then returns null for the freed edict and GetFlags() crashes the server. Skip the position update when the player is gone, the same way GetGhostMarkerPos() already does. The juggernaut branch reads its index the same way and gets the same check. --- src/game/server/neo/neo_ghost_boundary.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/neo_ghost_boundary.cpp b/src/game/server/neo/neo_ghost_boundary.cpp index 9f38e5e589..31f5e0b96e 100644 --- a/src/game/server/neo/neo_ghost_boundary.cpp +++ b/src/game/server/neo/neo_ghost_boundary.cpp @@ -102,7 +102,7 @@ void CNEO_GhostBoundary::Think() } CBasePlayer* pPlayer = UTIL_PlayerByIndex(iJuggernautIndex); - if (pPlayer->GetFlags() & FL_ONGROUND) + if (pPlayer && (pPlayer->GetFlags() & FL_ONGROUND)) { m_vecLastObjectivePos = pPlayer->GetAbsOrigin(); } @@ -120,8 +120,9 @@ void CNEO_GhostBoundary::Think() return; } + // The ghoster index is only refreshed after entities think, so it can still name a player who just disconnected CBasePlayer* pPlayer = UTIL_PlayerByIndex(iGhosterIndex); - if (pPlayer->GetFlags() & FL_ONGROUND) + if (pPlayer && (pPlayer->GetFlags() & FL_ONGROUND)) { m_vecLastObjectivePos = pPlayer->GetAbsOrigin(); } From e07fe414fa360322bbb131eeece23516810ce5cf Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:10:00 -0600 Subject: [PATCH 02/60] Keep the muzzle flash viewmodel on map cleanup CleanUpMap removed each player's neo_predicted_viewmodel_muzzleflash but kept the neo_predicted_viewmodel it is parented to. A bot with a pending class change calls RequestSetClass from CNEOBot::Spawn before CBasePlayer::Spawn recreates the viewmodels, so GiveDefaultItems -> Weapon_Switch dereferenced the removed muzzle flash viewmodel and the server crashed (bot_changeclass, then mp_restartgame). Preserve it like the viewmodel, so the player keeps one for life. --- src/game/shared/neo/neo_gamerules.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 85605812cf..38e7155f74 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -569,6 +569,7 @@ static const char *s_NeoPreserveEnts[] = "info_player_defender", "info_player_start", "neo_predicted_viewmodel", + "neo_predicted_viewmodel_muzzleflash", "neo_ghost_retrieval_point", "neo_ghostspawnpoint", "neo_juggernautspawnpoint", From 25edc86fec069e14f8325c9cee3b2c86d06485d5 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:20:00 -0600 Subject: [PATCH 03/60] Time a reload by its own sequence, not a transition The Tachi and Milso viewmodels carry a node graph from HL2's pistol, so a reload started from the lowered pose (during a sprint) played the lowtoidle transition first. DefaultReload took that 0.37-0.40 s transition as the reload length, and the server refilled the magazine after about 0.6 s with no reload animation. Skip transitions for ACT_VM_RELOAD as SetIdealActivity already does for the draw, so the reload plays at once and lasts its own 2.1 s. --- src/game/shared/basecombatweapon_shared.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game/shared/basecombatweapon_shared.cpp b/src/game/shared/basecombatweapon_shared.cpp index e72e024be8..f9c993e6d8 100644 --- a/src/game/shared/basecombatweapon_shared.cpp +++ b/src/game/shared/basecombatweapon_shared.cpp @@ -2443,7 +2443,13 @@ bool CBaseCombatWeapon::SetIdealActivity( Activity ideal ) int nextSequence = FindTransitionSequence( GetSequence(), m_nIdealSequence, NULL ); // Don't use transitions when we're deploying +#ifdef NEO + // or reloading: the reload ends after the sequence's duration, + // so a transition played first (such as the Tachi's lowtoidle after a sprint) would cut the reload short + if ( ideal != ACT_VM_DRAW && ideal != ACT_VM_RELOAD && IsWeaponVisible() && nextSequence != m_nIdealSequence ) +#else if ( ideal != ACT_VM_DRAW && IsWeaponVisible() && nextSequence != m_nIdealSequence ) +#endif { //Set our activity to the next transitional animation SetActivity( ACT_TRANSITION ); From 362eb29123db527ff3fb2363591bcffcf0aef04f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:30:00 -0600 Subject: [PATCH 04/60] Hold an empty weapon on its empty idle The Tachi and Milso viewmodels have an idle01empty sequence with the slide held back, which neo never played: an emptied pistol held its last fire frame for 2.0 s and then idled with the slide forward. The original NEOTOKYO holds the slide back from the end of the last shot until the reload. Send ACT_VM_IDLE_EMPTY for ACT_VM_IDLE while the clip is empty, as the SDK crossbow does, and go to it as the emptying shot's sequence ends. Models without the sequence are unchanged. --- .../weapons/weapon_neobasecombatweapon.cpp | 22 +++++++++++++++++++ .../neo/weapons/weapon_neobasecombatweapon.h | 2 ++ 2 files changed, 24 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 4d75d319dc..4808916e9c 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -941,6 +941,22 @@ void CNEOBaseCombatWeapon::DryFire() m_flNextPrimaryAttack = gpGlobals->curtime + GetFastestDryRefireTime(); // SequenceDuration(); } +// An empty weapon idles on its empty sequence where the model has one, such as a pistol with its slide held back +bool CNEOBaseCombatWeapon::SendWeaponAnim(int iActivity) +{ + if (iActivity == ACT_VM_IDLE && UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle()) + { + iActivity = ACT_VM_IDLE_EMPTY; + } + + return BaseClass::SendWeaponAnim(iActivity); +} + +bool CNEOBaseCombatWeapon::HasEmptyIdle() +{ + return SelectWeightedSequence(ACT_VM_IDLE_EMPTY) != ACTIVITY_NOT_AVAILABLE; +} + void CNEOBaseCombatWeapon::PrimaryAttack(void) { if (ShootingIsPrevented()) @@ -1064,6 +1080,12 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) m_iPrimaryAmmoCount -= info.m_iShots; } + // the last round's shot goes straight on to the empty idle when it ends + if (UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle()) + { + SetWeaponIdleTime(gpGlobals->curtime + SequenceDuration()); + } + info.m_flDistance = MAX_TRACE_LENGTH; info.m_iAmmoType = m_iPrimaryAmmoType; info.m_iTracerFreq = UsesTracers() ? 1 : 0; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index b8a0afbefa..c3899d4ddc 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -191,6 +191,7 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon #endif virtual void DryFire(void); + virtual bool SendWeaponAnim(int iActivity) override; virtual Activity GetPrimaryAttackActivity(void) override; #ifdef CLIENT_DLL @@ -292,6 +293,7 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); + bool HasEmptyIdle(); }; From 3ec8492d6f4b1212e574b90379c4bb3168a75287 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:35:00 -0600 Subject: [PATCH 05/60] Draw an empty pistol with its slide held back The Tachi and Milso viewmodels ship a drawempty sequence, the normal draw with the slide held back, but it carries no activity, so an empty pistol drew with the slide forward and only pulled it back as the empty idle faded in. Pick drawempty by name in Deploy when the clip is empty. Models without it, and loaded draws, are unchanged. --- .../shared/neo/weapons/weapon_neobasecombatweapon.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 4808916e9c..5fec7bd26f 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -498,6 +498,16 @@ bool CNEOBaseCombatWeapon::Deploy(void) AddEffects(EF_BONEMERGE); m_bTriggerReset = true; + // An empty weapon draws with its slide held back where the model has that draw, + // which carries no activity, so it is picked by name + static constexpr const char *VM_SEQ_DRAW_EMPTY = "drawempty"; + const int iDrawEmpty = (UsesClipsForAmmo1() && m_iClip1 <= 0) ? LookupSequence(VM_SEQ_DRAW_EMPTY) : ACTIVITY_NOT_AVAILABLE; + if (iDrawEmpty != ACTIVITY_NOT_AVAILABLE) + { + SetSequence(iDrawEmpty); + SendViewModelAnim(iDrawEmpty); + } + #ifdef DEBUG CNEO_Player* pOwner = NULL; if (GetOwner()) From 09473ff6dd6447f2d3156dba5f300552577bf077 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:50:00 -0600 Subject: [PATCH 06/60] Finish reloads at the last reload click A reload credited the magazine only when the whole reload animation ended, 0.2 to 0.7 s after the last click of the reload sound. Switching weapons in that window dropped the reload, though the gun sounded done. Each gun script now names its ReloadReadyTime: the last loud click of its reload sound, rounded up to the animation frame. Where the charging handle is home later (Jitte, SRM, MPN, AA13, PZ), the script comment names that time as the alternative value. The reload completes there and the rest of the animation plays on. Guns without the key (Supa7) keep the animation length. --- game/neo/scripts/weapon_aa13.txt | 2 ++ game/neo/scripts/weapon_jitte.txt | 2 ++ game/neo/scripts/weapon_jittescoped.txt | 2 ++ game/neo/scripts/weapon_kyla.txt | 1 + game/neo/scripts/weapon_m41.txt | 1 + game/neo/scripts/weapon_m41l.txt | 1 + game/neo/scripts/weapon_m41s.txt | 1 + game/neo/scripts/weapon_milso.txt | 1 + game/neo/scripts/weapon_mpn.txt | 2 ++ game/neo/scripts/weapon_mpn_unsilenced.txt | 2 ++ game/neo/scripts/weapon_mx.txt | 1 + game/neo/scripts/weapon_mx_silenced.txt | 1 + game/neo/scripts/weapon_pz.txt | 2 ++ game/neo/scripts/weapon_srm.txt | 2 ++ game/neo/scripts/weapon_srm_s.txt | 2 ++ game/neo/scripts/weapon_srs.txt | 1 + game/neo/scripts/weapon_tachi.txt | 1 + game/neo/scripts/weapon_zr68c.txt | 1 + game/neo/scripts/weapon_zr68l.txt | 1 + game/neo/scripts/weapon_zr68s.txt | 1 + .../shared/neo/weapons/neo_weapon_parse.cpp | 1 + src/game/shared/neo/weapons/neo_weapon_parse.h | 1 + .../neo/weapons/weapon_neobasecombatweapon.cpp | 17 ++++++++++++++++- 23 files changed, 46 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/weapon_aa13.txt b/game/neo/scripts/weapon_aa13.txt index def63c9983..72faf2faca 100644 --- a/game/neo/scripts/weapon_aa13.txt +++ b/game/neo/scripts/weapon_aa13.txt @@ -6,6 +6,8 @@ WeaponData "Damage" "25" "Bullets" "5" "CycleTime" "0.333" + "ReloadReadyTime" "4.200" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 4.333, the alternative value // Weapon data is loaded by both the Game and Client DLLs. "printname" "AA13" diff --git a/game/neo/scripts/weapon_jitte.txt b/game/neo/scripts/weapon_jitte.txt index c450c6f29a..ff6676127f 100644 --- a/game/neo/scripts/weapon_jitte.txt +++ b/game/neo/scripts/weapon_jitte.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "15" "Penetration" "12.0" "CycleTime" "0.090909" // time between shots + "ReloadReadyTime" "2.633" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.900, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_jittescoped.txt b/game/neo/scripts/weapon_jittescoped.txt index a7c32e13d1..a84887e09a 100644 --- a/game/neo/scripts/weapon_jittescoped.txt +++ b/game/neo/scripts/weapon_jittescoped.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "15" "Penetration" "20.0" "CycleTime" "0.090909" // time between shots + "ReloadReadyTime" "2.633" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.900, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_kyla.txt b/game/neo/scripts/weapon_kyla.txt index ab9152032c..c668090a11 100644 --- a/game/neo/scripts/weapon_kyla.txt +++ b/game/neo/scripts/weapon_kyla.txt @@ -13,6 +13,7 @@ WeaponData "Damage" "45" //"11" "Penetration" "10.0" "CycleTime" "0.363636" // time between shoots + "ReloadReadyTime" "1.767" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.6" diff --git a/game/neo/scripts/weapon_m41.txt b/game/neo/scripts/weapon_m41.txt index 29acafbfcf..071e254a1c 100644 --- a/game/neo/scripts/weapon_m41.txt +++ b/game/neo/scripts/weapon_m41.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "29" "Penetration" "32.0" "CycleTime" "0.166666" // time between shots + "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_m41l.txt b/game/neo/scripts/weapon_m41l.txt index 303ba2c82d..56a48fa531 100644 --- a/game/neo/scripts/weapon_m41l.txt +++ b/game/neo/scripts/weapon_m41l.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "45" "Penetration" "32.0" "CycleTime" "0.5" // time between shots + "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_m41s.txt b/game/neo/scripts/weapon_m41s.txt index af971caae6..fda35bb328 100644 --- a/game/neo/scripts/weapon_m41s.txt +++ b/game/neo/scripts/weapon_m41s.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "26" "Penetration" "20.0" "CycleTime" "0.166666" // time between shots + "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_milso.txt b/game/neo/scripts/weapon_milso.txt index 8e05550834..7fad6a3130 100644 --- a/game/neo/scripts/weapon_milso.txt +++ b/game/neo/scripts/weapon_milso.txt @@ -13,6 +13,7 @@ WeaponData "Damage" "17" "Penetration" "12.0" "CycleTime" "0.212121" + "ReloadReadyTime" "1.633" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn.txt b/game/neo/scripts/weapon_mpn.txt index c85eb6a752..20bd77b8e9 100644 --- a/game/neo/scripts/weapon_mpn.txt +++ b/game/neo/scripts/weapon_mpn.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots + "ReloadReadyTime" "2.700" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.800, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn_unsilenced.txt b/game/neo/scripts/weapon_mpn_unsilenced.txt index 8bd6e69fcc..fcc71f1ebf 100644 --- a/game/neo/scripts/weapon_mpn_unsilenced.txt +++ b/game/neo/scripts/weapon_mpn_unsilenced.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots + "ReloadReadyTime" "2.700" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.800, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mx.txt b/game/neo/scripts/weapon_mx.txt index 1eaabb29e8..2589f69fe7 100644 --- a/game/neo/scripts/weapon_mx.txt +++ b/game/neo/scripts/weapon_mx.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "28" "Penetration" "38.0" "CycleTime" "0.106060" // time between shots + "ReloadReadyTime" "2.500" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_mx_silenced.txt b/game/neo/scripts/weapon_mx_silenced.txt index b8b7c86556..ea661d8b14 100644 --- a/game/neo/scripts/weapon_mx_silenced.txt +++ b/game/neo/scripts/weapon_mx_silenced.txt @@ -13,6 +13,7 @@ WeaponData "Damage" "23" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots + "ReloadReadyTime" "2.500" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_pz.txt b/game/neo/scripts/weapon_pz.txt index c67473d642..72bac4dd3d 100644 --- a/game/neo/scripts/weapon_pz.txt +++ b/game/neo/scripts/weapon_pz.txt @@ -15,6 +15,8 @@ WeaponData "Damage" "20" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots + "ReloadReadyTime" "5.133" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 5.367, the alternative value "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_srm.txt b/game/neo/scripts/weapon_srm.txt index e697644a27..c38f49ce6a 100644 --- a/game/neo/scripts/weapon_srm.txt +++ b/game/neo/scripts/weapon_srm.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "12" "Penetration" "20.0" "CycleTime" "0.060606" // time between shots + "ReloadReadyTime" "2.833" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.900, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_srm_s.txt b/game/neo/scripts/weapon_srm_s.txt index 8d190812a7..2555d18694 100644 --- a/game/neo/scripts/weapon_srm_s.txt +++ b/game/neo/scripts/weapon_srm_s.txt @@ -13,6 +13,8 @@ WeaponData "Damage" "12" "Penetration" "10.0" "CycleTime" "0.090909" // time between shots + "ReloadReadyTime" "2.833" // reload is done at the last reload click, the animation plays on to its end; + // the charging handle is home at 2.900, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_srs.txt b/game/neo/scripts/weapon_srs.txt index 4d7ba65f34..edfc3f5dc6 100644 --- a/game/neo/scripts/weapon_srs.txt +++ b/game/neo/scripts/weapon_srs.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "100" "Penetration" "65.0" "CycleTime" "1.4" + "ReloadReadyTime" "3.067" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_tachi.txt b/game/neo/scripts/weapon_tachi.txt index 21c7909c8b..d409d0f4a4 100644 --- a/game/neo/scripts/weapon_tachi.txt +++ b/game/neo/scripts/weapon_tachi.txt @@ -32,6 +32,7 @@ WeaponData "Damage" "17" "Penetration" "19.0" "CycleTime" "0.121212" + "ReloadReadyTime" "1.700" // reload is done here, the animation plays on to its end "ViewModelOffset" { diff --git a/game/neo/scripts/weapon_zr68c.txt b/game/neo/scripts/weapon_zr68c.txt index 4ce959e1c1..3c4945526a 100644 --- a/game/neo/scripts/weapon_zr68c.txt +++ b/game/neo/scripts/weapon_zr68c.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "17" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots + "ReloadReadyTime" "2.267" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68l.txt b/game/neo/scripts/weapon_zr68l.txt index 7a32a52377..ce27a1f96e 100644 --- a/game/neo/scripts/weapon_zr68l.txt +++ b/game/neo/scripts/weapon_zr68l.txt @@ -15,6 +15,7 @@ WeaponData "Damage" "37" "Penetration" "32.0" "CycleTime" "0.5" // time between shots + "ReloadReadyTime" "2.533" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index f38e687f26..6d8127dd26 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -17,6 +17,7 @@ WeaponData "Damage" "22" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots + "ReloadReadyTime" "2.267" // reload is done here, the animation plays on to its end "TPMuzzleFlashScale" "0.5" diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index 5a7dbd3214..8bad569c25 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -38,6 +38,7 @@ void CNEOWeaponInfo::Parse( KeyValues *pKeyValuesData, const char *szWeaponName m_iPlayerDamage = pKeyValuesData->GetInt( "Damage", 42 ); // Douglas Adams 1952 - 2001 m_iBullets = pKeyValuesData->GetInt( "Bullets", 1 ); m_flCycleTime = pKeyValuesData->GetFloat( "CycleTime", 0.15 ); + m_flReloadReadyTime = pKeyValuesData->GetFloat( "ReloadReadyTime", 0.0f ); const char *notFoundStr = "notfound"; Q_strncpy(szViewModel2, pKeyValuesData->GetString("team2viewmodel", notFoundStr), MAX_WEAPON_STRING); diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index c36e610af5..5ef4b51c80 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -24,6 +24,7 @@ class CNEOWeaponInfo : public CHL2MPSWeaponInfo int m_iBullets; float m_flCycleTime; + float m_flReloadReadyTime; char szViewModel2[MAX_WEAPON_STRING]; // Team 2 (NSF) wep vm char szBulletCharacter[MAX_BULLET_CHARACTER];// character used to display ammunition in current clip char szDeathIcon[MAX_BULLET_CHARACTER]; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 4d75d319dc..841544fe84 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -416,7 +416,22 @@ void CNEOBaseCombatWeapon::Equip(CBaseCombatCharacter* pOwner) bool CNEOBaseCombatWeapon::Reload( void ) { - return BaseClass::Reload(); + if (!BaseClass::Reload()) + { + return false; + } + + // The magazine is loaded at the gun's last reload click, + // and the rest of the reload animation plays on as before + const float flReadyTime = GetNEOWpnData().m_flReloadReadyTime; + if (flReadyTime > 0.0f) + { + const float flReady = gpGlobals->curtime + flReadyTime; + GetOwner()->SetNextAttack(flReady); + m_flNextPrimaryAttack = m_flNextSecondaryAttack = flReady; + } + + return true; #if(0) CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); From 680983fa2c24a2785bae1790b364e9ef1beed9e8 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:54:00 -0600 Subject: [PATCH 07/60] Animate players on the SDK base player anim state Move the player animation back onto CBasePlayerAnimState, the 2006 SDK base classic NEOTOKYO is built on, which neo compiles but did not use. CNEOPlayerAnimState replaces CHL2MPPlayerAnimState and takes CMultiPlayerAnimState out of the build (the files stay on disk). The base gives classic's torso blend by speed, constant leg ground speeds, the 3 s face-front timer and its feet rules. The new class adds the run legs only while sprinting, the jump held until a drawn landing, one gesture layer for fire, reload, grenade and detpack events, the lean rule, the #2108 transitioner reset, and a face-front timer counted from the last squared frame so server and client agree. Client-side animation and the network excludes from #653 are kept. --- src/game/client/CMakeLists.txt | 6 +- src/game/client/hl2mp/c_hl2mp_player.cpp | 2 +- src/game/client/hl2mp/c_hl2mp_player.h | 4 +- src/game/server/CMakeLists.txt | 6 +- src/game/server/hl2mp/hl2mp_player.cpp | 2 +- src/game/server/hl2mp/hl2mp_player.h | 4 +- src/game/shared/base_playeranimstate.cpp | 11 + src/game/shared/base_playeranimstate.h | 5 + .../shared/hl2mp/hl2mp_playeranimstate.cpp | 1015 ----------------- src/game/shared/hl2mp/hl2mp_playeranimstate.h | 80 -- src/game/shared/neo/neo_playeranimstate.cpp | 453 ++++++++ src/game/shared/neo/neo_playeranimstate.h | 76 ++ 12 files changed, 555 insertions(+), 1109 deletions(-) delete mode 100644 src/game/shared/hl2mp/hl2mp_playeranimstate.cpp delete mode 100644 src/game/shared/hl2mp/hl2mp_playeranimstate.h create mode 100644 src/game/shared/neo/neo_playeranimstate.cpp create mode 100644 src/game/shared/neo/neo_playeranimstate.h diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt index cd2703666e..c20ddfac2a 100644 --- a/src/game/client/CMakeLists.txt +++ b/src/game/client/CMakeLists.txt @@ -1757,6 +1757,7 @@ set(UNITY_SOURCE_NEO_SHARED ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_penetration_resistance.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_shared.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_spawnpoint.cpp + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_playeranimstate.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel_muzzleflash.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_shot_manipulator.cpp @@ -1787,6 +1788,7 @@ target_sources_grouped( ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_penetration_resistance.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_shared.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_spawnpoint.h + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_playeranimstate.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel_muzzleflash.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_shot_manipulator.h @@ -1896,14 +1898,11 @@ target_sources_grouped( TARGET client NAME "Source Files\\Shared\\Multiplayer" FILES - ${CMAKE_SOURCE_DIR}/game/shared/Multiplayer/multiplayer_animstate.cpp - ${CMAKE_SOURCE_DIR}/game/shared/Multiplayer/multiplayer_animstate.h ) set(UNITY_SOURCE_HL2MP ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_gamerules.cpp ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_player_shared.cpp - ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_playeranimstate.cpp ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_weapon_parse.cpp hl2mp/c_hl2mp_player.cpp hl2mp/c_te_hl2mp_shotgun_shot.cpp @@ -1921,7 +1920,6 @@ target_sources_grouped( FILES ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_gamerules.h ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_player_shared.h - ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_playeranimstate.h ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_weapon_parse.h hl2mp/c_hl2mp_player.h hl2mp/clientmode_hl2mpnormal.h diff --git a/src/game/client/hl2mp/c_hl2mp_player.cpp b/src/game/client/hl2mp/c_hl2mp_player.cpp index 0b267d9fb0..8b760b2d37 100644 --- a/src/game/client/hl2mp/c_hl2mp_player.cpp +++ b/src/game/client/hl2mp/c_hl2mp_player.cpp @@ -188,7 +188,7 @@ C_HL2MP_Player::C_HL2MP_Player() : m_PlayerAnimState( this ), m_iv_angEyeAngles( m_EntClientFlags |= ENTCLIENTFLAG_DONTUSEIK; #endif #ifdef NEO - m_PlayerAnimState = CreateHL2MPPlayerAnimState( this ); + m_PlayerAnimState = CreateNEOPlayerAnimState( this ); #endif m_blinkTimer.Invalidate(); diff --git a/src/game/client/hl2mp/c_hl2mp_player.h b/src/game/client/hl2mp/c_hl2mp_player.h index aaa99399c0..9e15e066dd 100644 --- a/src/game/client/hl2mp/c_hl2mp_player.h +++ b/src/game/client/hl2mp/c_hl2mp_player.h @@ -9,7 +9,7 @@ #define HL2MP_PLAYER_H #pragma once -#include "hl2mp_playeranimstate.h" +#include "neo_playeranimstate.h" #include "c_basehlplayer.h" #include "hl2mp_player_shared.h" #include "beamdraw.h" @@ -145,7 +145,7 @@ class C_HL2MP_Player : public C_BaseHLPlayer C_HL2MP_Player( const C_HL2MP_Player & ); #ifdef NEO - CHL2MPPlayerAnimState *m_PlayerAnimState; + CNEOPlayerAnimState *m_PlayerAnimState; #else CPlayerAnimState m_PlayerAnimState; #endif diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index fec1fd9998..20980013ca 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1423,6 +1423,7 @@ set(UNITY_SOURCE_NEO_SHARED ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_penetration_resistance.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_shared.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_spawnpoint.cpp + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_playeranimstate.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel_muzzleflash.cpp ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_shot_manipulator.cpp @@ -1480,6 +1481,7 @@ target_sources_grouped( ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_penetration_resistance.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_shared.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_player_spawnpoint.h + ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_playeranimstate.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_predicted_viewmodel_muzzleflash.h ${CMAKE_SOURCE_DIR}/game/shared/neo/neo_shot_manipulator.h @@ -1740,14 +1742,11 @@ target_sources_grouped( TARGET server NAME "Source Files\\Shared\\Multiplayer" FILES - ${CMAKE_SOURCE_DIR}/game/shared/Multiplayer/multiplayer_animstate.cpp - ${CMAKE_SOURCE_DIR}/game/shared/Multiplayer/multiplayer_animstate.h ) set(UNITY_SOURCE_HL2MP ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_gamerules.cpp ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_player_shared.cpp - ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_playeranimstate.cpp ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_weapon_parse.cpp hl2mp/hl2mp_client.cpp hl2mp/hl2mp_cvars.cpp @@ -1766,7 +1765,6 @@ target_sources_grouped( FILES ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_gamerules.h ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_player_shared.h - ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_playeranimstate.h ${CMAKE_SOURCE_DIR}/game/shared/hl2mp/hl2mp_weapon_parse.h hl2mp/hl2mp_gameinterface.h hl2mp/hl2mp_player.h diff --git a/src/game/server/hl2mp/hl2mp_player.cpp b/src/game/server/hl2mp/hl2mp_player.cpp index d9e8c9bcc5..d73f33a6af 100644 --- a/src/game/server/hl2mp/hl2mp_player.cpp +++ b/src/game/server/hl2mp/hl2mp_player.cpp @@ -193,7 +193,7 @@ CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this ) { #ifdef NEO //Tony; create our player animation state. - m_PlayerAnimState = CreateHL2MPPlayerAnimState( this ); + m_PlayerAnimState = CreateNEOPlayerAnimState( this ); #endif UseClientSideAnimation(); diff --git a/src/game/server/hl2mp/hl2mp_player.h b/src/game/server/hl2mp/hl2mp_player.h index e5e0b60a14..463d9d2374 100644 --- a/src/game/server/hl2mp/hl2mp_player.h +++ b/src/game/server/hl2mp/hl2mp_player.h @@ -16,7 +16,7 @@ class CHL2MP_Player; #include "hl2_player.h" #include "simtimer.h" #include "soundenvelope.h" -#include "hl2mp_playeranimstate.h" +#include "neo_playeranimstate.h" #include "hl2mp_player_shared.h" #include "hl2mp_gamerules.h" #include "utldict.h" @@ -177,7 +177,7 @@ class CHL2MP_Player : public CHL2_Player private: #ifdef NEO - CHL2MPPlayerAnimState *m_PlayerAnimState; + CNEOPlayerAnimState *m_PlayerAnimState; #else CPlayerAnimState m_PlayerAnimState; #endif diff --git a/src/game/shared/base_playeranimstate.cpp b/src/game/shared/base_playeranimstate.cpp index 0126f016ae..7f4950894c 100644 --- a/src/game/shared/base_playeranimstate.cpp +++ b/src/game/shared/base_playeranimstate.cpp @@ -234,6 +234,17 @@ void CBasePlayerAnimState::RestartMainSequence() } +#ifdef NEO +void CBasePlayerAnimState::OnNewModel() +{ + // the aim layer transitioners keep the previous sequences to blend out of them, + // and after a model swap those indices address a foreign sequence table + m_IdleSequenceTransitioner.RemoveAll(); + m_SequenceTransitioner.RemoveAll(); + ClearAnimationState(); +} +#endif + void CBasePlayerAnimState::ComputeSequences( CStudioHdr *pStudioHdr ) { VPROF( "CBasePlayerAnimState::ComputeSequences" ); diff --git a/src/game/shared/base_playeranimstate.h b/src/game/shared/base_playeranimstate.h index 95e2c73a4f..de0a68be97 100644 --- a/src/game/shared/base_playeranimstate.h +++ b/src/game/shared/base_playeranimstate.h @@ -162,6 +162,11 @@ abstract_class CBasePlayerAnimState : virtual public IPlayerAnimState void RestartMainSequence(); +#ifdef NEO + // A sequence index is only meaningful for the model it was computed against + void OnNewModel(); +#endif + // Helpers for the derived classes to use. protected: diff --git a/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp b/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp deleted file mode 100644 index 9e441acd87..0000000000 --- a/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp +++ /dev/null @@ -1,1015 +0,0 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// -#include "cbase.h" -#include "tier0/vprof.h" -#include "animation.h" -#include "studio.h" -#include "apparent_velocity_helper.h" -#include "utldict.h" - -#include "hl2mp_playeranimstate.h" -#include "base_playeranimstate.h" -#include "datacache/imdlcache.h" - -#ifdef CLIENT_DLL -#include "c_hl2mp_player.h" -#ifdef NEO -#include "c_neo_player.h" -#endif -#else -#include "hl2mp_player.h" -#ifdef NEO -#include "neo_player.h" -#endif -#endif - -#ifdef NEO -#include "shareddefs.h" - -#define DEFAULT_IDLE_NAME "Idle_Upper_" -#define DEFAULT_CROUCH_IDLE_NAME "Crouch_Idle_Upper_" -#define DEFAULT_CROUCH_WALK_NAME "Crouch_Walk_Upper_" -#define DEFAULT_WALK_NAME "Walk_Upper_" -#define DEFAULT_RUN_NAME "Run_Upper_" - -#define DEFAULT_FIRE_IDLE_NAME "Idle_Shoot_" -#define DEFAULT_FIRE_CROUCH_NAME "Crouch_Idle_Shoot_" -#define DEFAULT_FIRE_CROUCH_WALK_NAME "Crouch_Walk_Shoot_" -#define DEFAULT_FIRE_WALK_NAME "Walk_Shoot_" -#define DEFAULT_FIRE_RUN_NAME "Run_Shoot_" -#endif - -#include "filesystem.h" -#include "engine/ivdebugoverlay.h" - -#define HL2MP_RUN_SPEED 320.0f -#define HL2MP_WALK_SPEED 75.0f -#define HL2MP_CROUCHWALK_SPEED 110.0f - -extern ConVar sv_showanimstate; -extern ConVar showanimstate_log; - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *pPlayer - -// Output : CMultiPlayerAnimState* -//----------------------------------------------------------------------------- -CHL2MPPlayerAnimState* CreateHL2MPPlayerAnimState( CHL2MP_Player *pPlayer ) -{ - MDLCACHE_CRITICAL_SECTION(); - - // Setup the movement data. - MultiPlayerMovementData_t movementData; - movementData.m_flBodyYawRate = 720.0f; - movementData.m_flRunSpeed = HL2MP_RUN_SPEED; - movementData.m_flWalkSpeed = HL2MP_WALK_SPEED; - movementData.m_flSprintSpeed = -1.0f; - - // Create animation state for this player. - CHL2MPPlayerAnimState *pRet = new CHL2MPPlayerAnimState( pPlayer, movementData ); - - // Specific HL2MP player initialization. - pRet->InitHL2MPAnimState( pPlayer ); - - return pRet; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : - -//----------------------------------------------------------------------------- -CHL2MPPlayerAnimState::CHL2MPPlayerAnimState() -{ - m_pHL2MPPlayer = NULL; - - // Don't initialize HL2MP specific variables here. Init them in InitHL2MPAnimState() -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *pPlayer - -// &movementData - -//----------------------------------------------------------------------------- -CHL2MPPlayerAnimState::CHL2MPPlayerAnimState( CBasePlayer *pPlayer, MultiPlayerMovementData_t &movementData ) -: CMultiPlayerAnimState( pPlayer, movementData ) -{ - m_pHL2MPPlayer = NULL; - - // Don't initialize HL2MP specific variables here. Init them in InitHL2MPAnimState() -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : - -//----------------------------------------------------------------------------- -CHL2MPPlayerAnimState::~CHL2MPPlayerAnimState() -{ -} - -//----------------------------------------------------------------------------- -// Purpose: Initialize HL2MP specific animation state. -// Input : *pPlayer - -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::InitHL2MPAnimState( CHL2MP_Player *pPlayer ) -{ - m_pHL2MPPlayer = pPlayer; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::ClearAnimationState( void ) -{ - BaseClass::ClearAnimationState(); -} - -int CHL2MPPlayerAnimState::CalcSequenceIndex(const char* pBaseName, ...) -{ - char szFullName[512]; - va_list marker; - va_start(marker, pBaseName); - Q_vsnprintf(szFullName, sizeof(szFullName), pBaseName, marker); - va_end(marker); - CBaseAnimatingOverlay* pPlayer = GetBasePlayer(); - int iSequence = pPlayer->LookupSequence(szFullName); - - // Show warnings if we can't find anything here. - if (iSequence == -1) - { - static CUtlDict dict; - if (dict.Find(szFullName) == -1) - { - dict.Insert(szFullName, 0); - Warning("CalcSequenceIndex: can't find '%s'.\n", szFullName); - } - - iSequence = 0; - } - - //DevMsg("CalcSeqIdx: \"%s\": %d\n", szFullName, iSequence); - - return iSequence; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : actDesired - -// Output : Activity -//----------------------------------------------------------------------------- -Activity CHL2MPPlayerAnimState::TranslateActivity( Activity actDesired ) -{ - // Hook into baseclass when / if hl2mp player models get swim animations. - Activity translateActivity = actDesired; //BaseClass::TranslateActivity( actDesired ); - if ( GetHL2MPPlayer()->GetActiveWeapon() ) - { - bool required = false; - translateActivity = GetHL2MPPlayer()->GetActiveWeapon()->ActivityOverride( translateActivity, &required); - } - return translateActivity; -} - -#ifdef NEO -//----------------------------------------------------------------------------- -// Purpose: -// Input : *pStudioHdr - -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::ComputeSequences(CStudioHdr* pStudioHdr) -{ - VPROF("CBasePlayerAnimState::ComputeSequences"); - - // Lower body (walk/run/idle). - ComputeMainSequence(); - - // The groundspeed interpolator uses the main sequence info. - UpdateInterpolators(); - ComputeGestureSequence(pStudioHdr); -} - -#endif //NEO -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::Update( float eyeYaw, float eyePitch ) -{ - // Profile the animation update. - VPROF( "CHL2MPPlayerAnimState::Update" ); - - // Get the HL2MP player. - CHL2MP_Player *pHL2MPPlayer = GetHL2MPPlayer(); - if ( !pHL2MPPlayer ) - return; - - // Get the studio header for the player. - CStudioHdr *pStudioHdr = pHL2MPPlayer->GetModelPtr(); - if ( !pStudioHdr ) - return; - - // Check to see if we should be updating the animation state - dead, ragdolled? - if ( !ShouldUpdateAnimState() ) - { - ClearAnimationState(); - return; - } - - // Store the eye angles. - m_flEyeYaw = AngleNormalize( eyeYaw ); - m_flEyePitch = AngleNormalize( eyePitch ); - - // Compute the player sequences. - ComputeSequences( pStudioHdr ); - - if ( SetupPoseParameters( pStudioHdr ) ) - { - // Pose parameter - what direction are the player's legs running in. - ComputePoseParam_MoveYaw( pStudioHdr ); - - // Pose parameter - Torso aiming (up/down). - ComputePoseParam_AimPitch( pStudioHdr ); - - // Pose parameter - Torso aiming (rotation). - ComputePoseParam_AimYaw( pStudioHdr ); - } - -#ifdef CLIENT_DLL - if ( C_BasePlayer::ShouldDrawLocalPlayer() ) - { - m_pHL2MPPlayer->SetPlaybackRate( 1.0f ); - } -#endif - -#ifdef CLIENT_DLL - if (cl_showanimstate.GetInt() == m_pHL2MPPlayer->entindex()) - { - DebugShowAnimStateFull(5); - } - else if (cl_showanimstate.GetInt() == -2) - { - C_BasePlayer* targetPlayer = C_BasePlayer::GetLocalPlayer(); - - if (targetPlayer && (targetPlayer->GetObserverMode() == OBS_MODE_IN_EYE || targetPlayer->GetObserverMode() == OBS_MODE_CHASE)) - { - C_BaseEntity* target = targetPlayer->GetObserverTarget(); - - if (target && target->IsPlayer()) - { - targetPlayer = ToBasePlayer(target); - } - } - - if (m_pHL2MPPlayer == targetPlayer) - { - DebugShowAnimStateFull(6); - } - } -#else - if (sv_showanimstate.GetInt() == m_pHL2MPPlayer->entindex()) - { - DebugShowAnimState(20); - } -#endif -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : event - -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) -{ - Activity iGestureActivity = ACT_INVALID; - - switch( event ) - { - case PLAYERANIMEVENT_ATTACK_PRIMARY: - { -#ifdef NEO - AddToGestureSlot(GESTURE_SLOT_ATTACK_AND_RELOAD, CalcFireSequence(), true); -#else - // Weapon primary fire. - if ( m_pHL2MPPlayer->GetFlags() & FL_DUCKING ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_ATTACK_CROUCH_PRIMARYFIRE ); - else - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_ATTACK_STAND_PRIMARYFIRE ); - - iGestureActivity = ACT_VM_PRIMARYATTACK; -#endif - break; - } - - case PLAYERANIMEVENT_VOICE_COMMAND_GESTURE: - { - if ( !IsGestureSlotActive( GESTURE_SLOT_ATTACK_AND_RELOAD ) ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, (Activity)nData ); - - break; - } - case PLAYERANIMEVENT_ATTACK_SECONDARY: - { -#ifdef NEO - AddToGestureSlot(GESTURE_SLOT_ATTACK_AND_RELOAD, CalcFireSequence(), true); -#else - // Weapon secondary fire. - if ( m_pHL2MPPlayer->GetFlags() & FL_DUCKING ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_ATTACK_CROUCH_SECONDARYFIRE ); - else - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_ATTACK_STAND_SECONDARYFIRE ); - - iGestureActivity = ACT_VM_PRIMARYATTACK; -#endif - break; - } - case PLAYERANIMEVENT_ATTACK_PRE: - { - if ( m_pHL2MPPlayer->GetFlags() & FL_DUCKING ) - { - // Weapon pre-fire. Used for minigun windup, sniper aiming start, etc in crouch. - iGestureActivity = ACT_MP_ATTACK_CROUCH_PREFIRE; - } - else - { - // Weapon pre-fire. Used for minigun windup, sniper aiming start, etc. - iGestureActivity = ACT_MP_ATTACK_STAND_PREFIRE; - } - - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, iGestureActivity, false ); - - break; - } - case PLAYERANIMEVENT_ATTACK_POST: - { - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_ATTACK_STAND_POSTFIRE ); - break; - } - - case PLAYERANIMEVENT_RELOAD: - { -#ifdef NEO - AddToGestureSlot(GESTURE_SLOT_ATTACK_AND_RELOAD, CalcReloadSequence(), true); -#else - // Weapon reload. - if ( GetBasePlayer()->GetFlags() & FL_DUCKING ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_CROUCH ); - else - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_STAND ); -#endif - break; - } - case PLAYERANIMEVENT_RELOAD_LOOP: - { - // Weapon reload. - if ( GetBasePlayer()->GetFlags() & FL_DUCKING ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_CROUCH_LOOP ); - else - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_STAND_LOOP ); - break; - } - case PLAYERANIMEVENT_RELOAD_END: - { - // Weapon reload. - if ( GetBasePlayer()->GetFlags() & FL_DUCKING ) - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_CROUCH_END ); - else - RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_STAND_END ); - break; - } - default: - { - BaseClass::DoAnimationEvent( event, nData ); - break; - } - } - -#ifdef CLIENT_DLL - // Make the weapon play the animation as well - if ( iGestureActivity != ACT_INVALID ) - { - CBaseCombatWeapon *pWeapon = GetHL2MPPlayer()->GetActiveWeapon(); - if ( pWeapon ) - { -// pWeapon->EnsureCorrectRenderingModel(); - pWeapon->SendWeaponAnim( iGestureActivity ); -// // Force animation events! -// pWeapon->ResetEventsParity(); // reset event parity so the animation events will occur on the weapon. - pWeapon->DoAnimationEvents( pWeapon->GetModelPtr() ); - } - } -#endif -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *idealActivity - -//----------------------------------------------------------------------------- -bool CHL2MPPlayerAnimState::HandleSwimming( Activity &idealActivity ) -{ - bool bInWater = BaseClass::HandleSwimming( idealActivity ); - - return bInWater; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *idealActivity - -// Output : Returns true on success, false on failure. -//----------------------------------------------------------------------------- -bool CHL2MPPlayerAnimState::HandleMoving( Activity &idealActivity ) -{ -#ifdef NEO - if (m_pHL2MPPlayer->IsSprinting()) - { - idealActivity = ACT_RUN; - } - else - { - Vector vecVelocity; - GetOuterAbsVelocity(vecVelocity); - if (vecVelocity.Length() > 0.5) - { - idealActivity = ACT_WALK; - } - } - - return true; -#else - return BaseClass::HandleMoving( idealActivity ); -#endif -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *idealActivity - -// Output : Returns true on success, false on failure. -//----------------------------------------------------------------------------- -bool CHL2MPPlayerAnimState::HandleDucking( Activity &idealActivity ) -{ - if ( m_pHL2MPPlayer->GetFlags() & FL_DUCKING ) - { - if ( GetOuterXYSpeed() < MOVING_MINIMUM_SPEED ) - { -#ifdef NEO - idealActivity = ACT_CROUCHIDLE; -#else - idealActivity = ACT_MP_CROUCH_IDLE; -#endif - } - else - { -#ifdef NEO - idealActivity = ACT_RUN_CROUCH; -#else - idealActivity = ACT_MP_CROUCHWALK; -#endif - } - - return true; - } - - return false; -} - -//----------------------------------------------------------------------------- -// Purpose: -bool CHL2MPPlayerAnimState::HandleJumping( Activity &idealActivity ) -{ - Vector vecVelocity; - GetOuterAbsVelocity( vecVelocity ); - - if ( m_bJumping ) - { - static bool bNewJump = false; //Tony; hl2mp players only have a 'hop' - - if ( m_bFirstJumpFrame ) - { - m_bFirstJumpFrame = false; - RestartMainSequence(); // Reset the animation. - } - - // Reset if we hit water and start swimming. - if ( m_pHL2MPPlayer->GetWaterLevel() >= WL_Waist ) - { - m_bJumping = false; - RestartMainSequence(); - } - // Don't check if he's on the ground for a sec.. sometimes the client still has the - // on-ground flag set right when the message comes in. - else if ( gpGlobals->curtime - m_flJumpStartTime > 0.2f ) - { - if ( m_pHL2MPPlayer->GetFlags() & FL_ONGROUND ) - { - m_bJumping = false; - RestartMainSequence(); - - if ( bNewJump ) - { -#ifdef NEO - RestartGesture(GESTURE_SLOT_JUMP, ACT_HOP); -#else - RestartGesture( GESTURE_SLOT_JUMP, ACT_MP_JUMP_LAND ); -#endif - } - } - } - - // if we're still jumping - if ( m_bJumping ) - { -#ifdef NEO - idealActivity = ACT_HOP; -#else - if ( bNewJump ) - { - if ( gpGlobals->curtime - m_flJumpStartTime > 0.5 ) - { - idealActivity = ACT_MP_JUMP_FLOAT; - } - else - { - idealActivity = ACT_MP_JUMP_START; - } - } - else - { - idealActivity = ACT_MP_JUMP; - } -#endif - } - } - - if ( m_bJumping ) - return true; - - return false; -} - -bool CHL2MPPlayerAnimState::SetupPoseParameters( CStudioHdr *pStudioHdr ) -{ -#ifdef NEO - BaseClass::SetupPoseParameters(pStudioHdr); -#endif - // Check to see if this has already been done. - if ( m_bPoseParameterInit ) - return true; - - // Save off the pose parameter indices. - if ( !pStudioHdr ) - return false; - - // Tony; just set them both to the same for now. - m_PoseParameterData.m_iMoveX = GetBasePlayer()->LookupPoseParameter( pStudioHdr, "move_yaw" ); - m_PoseParameterData.m_iMoveY = GetBasePlayer()->LookupPoseParameter( pStudioHdr, "move_yaw" ); - if ( ( m_PoseParameterData.m_iMoveX < 0 ) || ( m_PoseParameterData.m_iMoveY < 0 ) ) - return false; - - // Look for the aim pitch blender. - m_PoseParameterData.m_iAimPitch = GetBasePlayer()->LookupPoseParameter( pStudioHdr, "body_pitch" ); - if ( m_PoseParameterData.m_iAimPitch < 0 ) - return false; - - // Look for aim yaw blender. - m_PoseParameterData.m_iAimYaw = GetBasePlayer()->LookupPoseParameter( pStudioHdr, "body_yaw" ); - if ( m_PoseParameterData.m_iAimYaw < 0 ) - return false; - - m_bPoseParameterInit = true; - - return true; -} -float SnapYawTo( float flValue ); -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::EstimateYaw( void ) -{ - // Get the frame time. - float flDeltaTime = gpGlobals->frametime; - if ( flDeltaTime == 0.0f ) - return; - -#if 0 // 9way - // Get the player's velocity and angles. - Vector vecEstVelocity; - GetOuterAbsVelocity( vecEstVelocity ); - QAngle angles = GetBasePlayer()->GetLocalAngles(); - - // If we are not moving, sync up the feet and eyes slowly. - if ( vecEstVelocity.x == 0.0f && vecEstVelocity.y == 0.0f ) - { - float flYawDelta = angles[YAW] - m_PoseParameterData.m_flEstimateYaw; - flYawDelta = AngleNormalize( flYawDelta ); - - if ( flDeltaTime < 0.25f ) - { - flYawDelta *= ( flDeltaTime * 4.0f ); - } - else - { - flYawDelta *= flDeltaTime; - } - - m_PoseParameterData.m_flEstimateYaw += flYawDelta; - AngleNormalize( m_PoseParameterData.m_flEstimateYaw ); - } - else - { - m_PoseParameterData.m_flEstimateYaw = ( atan2( vecEstVelocity.y, vecEstVelocity.x ) * 180.0f / M_PI ); - m_PoseParameterData.m_flEstimateYaw = clamp( m_PoseParameterData.m_flEstimateYaw, -180.0f, 180.0f ); - } -#else - float dt = gpGlobals->frametime; - - // Get the player's velocity and angles. - Vector vecEstVelocity; - GetOuterAbsVelocity( vecEstVelocity ); - QAngle angles = GetBasePlayer()->GetLocalAngles(); - - if ( vecEstVelocity.y == 0 && vecEstVelocity.x == 0 ) - { - float flYawDiff = angles[YAW] - m_PoseParameterData.m_flEstimateYaw; - flYawDiff = flYawDiff - (int)(flYawDiff / 360) * 360; - if (flYawDiff > 180) - flYawDiff -= 360; - if (flYawDiff < -180) - flYawDiff += 360; - - if (dt < 0.25) - flYawDiff *= dt * 4; - else - flYawDiff *= dt; - - m_PoseParameterData.m_flEstimateYaw += flYawDiff; - m_PoseParameterData.m_flEstimateYaw = m_PoseParameterData.m_flEstimateYaw - (int)(m_PoseParameterData.m_flEstimateYaw / 360) * 360; - } - else - { - m_PoseParameterData.m_flEstimateYaw = (atan2(vecEstVelocity.y, vecEstVelocity.x) * 180 / M_PI); - - if (m_PoseParameterData.m_flEstimateYaw > 180) - m_PoseParameterData.m_flEstimateYaw = 180; - else if (m_PoseParameterData.m_flEstimateYaw < -180) - m_PoseParameterData.m_flEstimateYaw = -180; - } -#endif -} -//----------------------------------------------------------------------------- -// Purpose: Override for backpeddling -// Input : dt - -//----------------------------------------------------------------------------- -float SnapYawTo( float flValue ); -void CHL2MPPlayerAnimState::ComputePoseParam_MoveYaw( CStudioHdr *pStudioHdr ) -{ -#ifdef NEO - BaseClass::ComputePoseParam_MoveYaw(pStudioHdr); -#endif - // Get the estimated movement yaw. - EstimateYaw(); - -#if 1 // 9way - ConVarRef mp_slammoveyaw("mp_slammoveyaw"); - - // Get the view yaw. - float flAngle = AngleNormalize( m_flEyeYaw ); - - // Calc side to side turning - the view vs. movement yaw. - float flYaw = flAngle - m_PoseParameterData.m_flEstimateYaw; - flYaw = AngleNormalize( -flYaw ); - - // Get the current speed the character is running. - bool bIsMoving; - float flPlaybackRate = CalcMovementPlaybackRate( &bIsMoving ); - - // Setup the 9-way blend parameters based on our speed and direction. - Vector2D vecCurrentMoveYaw( 0.0f, 0.0f ); - if ( bIsMoving ) - { - if ( mp_slammoveyaw.GetBool() ) - flYaw = SnapYawTo( flYaw ); - - vecCurrentMoveYaw.x = cos( DEG2RAD( flYaw ) ) * flPlaybackRate; - vecCurrentMoveYaw.y = -sin( DEG2RAD( flYaw ) ) * flPlaybackRate; - } - - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveX, vecCurrentMoveYaw.x ); - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveY, vecCurrentMoveYaw.y ); - - m_DebugAnimData.m_vecMoveYaw = vecCurrentMoveYaw; -#else - // view direction relative to movement - float flYaw; - - QAngle angles = GetBasePlayer()->GetLocalAngles(); - float ang = angles[ YAW ]; - if ( ang > 180.0f ) - { - ang -= 360.0f; - } - else if ( ang < -180.0f ) - { - ang += 360.0f; - } - - // calc side to side turning - flYaw = ang - m_PoseParameterData.m_flEstimateYaw; - // Invert for mapping into 8way blend - flYaw = -flYaw; - flYaw = flYaw - (int)(flYaw / 360) * 360; - - if (flYaw < -180) - { - flYaw = flYaw + 360; - } - else if (flYaw > 180) - { - flYaw = flYaw - 360; - } - - //Tony; oops, i inverted this previously above. - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveY, flYaw ); - -#endif - -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::ComputePoseParam_AimPitch( CStudioHdr *pStudioHdr ) -{ - // Get the view pitch. - float flAimPitch = m_flEyePitch; - - // Set the aim pitch pose parameter and save. - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iAimPitch, flAimPitch ); - m_DebugAnimData.m_flAimPitch = flAimPitch; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::ComputePoseParam_AimYaw( CStudioHdr *pStudioHdr ) -{ - // Get the movement velocity. - Vector vecVelocity; - GetOuterAbsVelocity( vecVelocity ); - - // Check to see if we are moving. - bool bMoving = ( vecVelocity.Length() > 1.0f ) ? true : false; - -#ifdef NEO - // If we are moving or leaning or are prone and undeployed. - if ( bMoving || m_bForceAimYaw || IsLeaning(pStudioHdr) ) -#else - // If we are moving or are prone and undeployed. - if ( bMoving || m_bForceAimYaw ) -#endif // NEO - { - // The feet match the eye direction when moving - the move yaw takes care of the rest. - m_flGoalFeetYaw = m_flEyeYaw; - } - // Else if we are not moving. - else - { - // Initialize the feet. - if ( m_PoseParameterData.m_flLastAimTurnTime <= 0.0f ) - { - m_flGoalFeetYaw = m_flEyeYaw; - m_flCurrentFeetYaw = m_flEyeYaw; - m_PoseParameterData.m_flLastAimTurnTime = gpGlobals->curtime; - } - // Make sure the feet yaw isn't too far out of sync with the eye yaw. - // TODO: Do something better here! - else - { - float flYawDelta = AngleNormalize( m_flGoalFeetYaw - m_flEyeYaw ); - -#ifdef NEO - if (fabs(flYawDelta) > 90.f /*NEO_ANIMSTATE_MAX_BODY_YAW_DEGREES*/) -#else - if ( fabs( flYawDelta ) > 45.0f ) -#endif - { - float flSide = ( flYawDelta > 0.0f ) ? -1.0f : 1.0f; - -#ifdef NEO - m_flGoalFeetYaw += (90.f/*NEO_ANIMSTATE_MAX_BODY_YAW_DEGREES*/ * flSide); -#else - m_flGoalFeetYaw += ( 45.0f * flSide ); -#endif - } - } - } - - // Fix up the feet yaw. - m_flGoalFeetYaw = AngleNormalize( m_flGoalFeetYaw ); - if ( m_flGoalFeetYaw != m_flCurrentFeetYaw ) - { - if ( m_bForceAimYaw ) - { - m_flCurrentFeetYaw = m_flGoalFeetYaw; - } - else - { - ConvergeYawAngles( m_flGoalFeetYaw, 720.0f, gpGlobals->frametime, m_flCurrentFeetYaw ); - m_flLastAimTurnTime = gpGlobals->curtime; - } - } - - // Rotate the body into position. - m_angRender[YAW] = m_flCurrentFeetYaw; - - // Find the aim(torso) yaw base on the eye and feet yaws. - float flAimYaw = m_flEyeYaw - m_flCurrentFeetYaw; - flAimYaw = AngleNormalize( flAimYaw ); - - // Set the aim yaw and save. - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iAimYaw, flAimYaw ); - m_DebugAnimData.m_flAimYaw = flAimYaw; - - // Turn off a force aim yaw - either we have already updated or we don't need to. - m_bForceAimYaw = false; - -#ifndef CLIENT_DLL - QAngle angle = GetBasePlayer()->GetAbsAngles(); - angle[YAW] = m_flCurrentFeetYaw; - - GetBasePlayer()->SetAbsAngles( angle ); -#endif -} - -//----------------------------------------------------------------------------- -// Purpose: Override the default, because hl2mp models don't use moveX -// Input : - -// Output : float -//----------------------------------------------------------------------------- -float CHL2MPPlayerAnimState::GetCurrentMaxGroundSpeed() -{ -#ifdef NEO - return BaseClass::GetCurrentMaxGroundSpeed(); -#endif - CStudioHdr *pStudioHdr = GetBasePlayer()->GetModelPtr(); - - if ( pStudioHdr == NULL ) - return 1.0f; - -// float prevX = GetBasePlayer()->GetPoseParameter( m_PoseParameterData.m_iMoveX ); - float prevY = GetBasePlayer()->GetPoseParameter( m_PoseParameterData.m_iMoveY ); - - float d = sqrt( /*prevX * prevX + */prevY * prevY ); - float newY;//, newX; - if ( d == 0.0 ) - { -// newX = 1.0; - newY = 0.0; - } - else - { -// newX = prevX / d; - newY = prevY / d; - } - -// GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveX, newX ); - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveY, newY ); - - float speed = GetBasePlayer()->GetSequenceGroundSpeed( GetBasePlayer()->GetSequence() ); - -// GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveX, prevX ); - GetBasePlayer()->SetPoseParameter( pStudioHdr, m_PoseParameterData.m_iMoveY, prevY ); - - return speed; -} - -#ifdef NEO -bool CHL2MPPlayerAnimState::IsLeaning(CStudioHdr *pStudioHdr) -{ - if (CBaseAnimating* pBaseAnimating = GetBasePlayer()->GetBaseAnimating()) - { - constexpr int LEAN_BONE_CONTROLLER = 0; -#ifdef CLIENT_DLL - constexpr float BONE_CONTROLLER_VALUE_NOT_LEANING = 0.5f; -#else - constexpr int BONE_CONTROLLER_VALUE_NOT_LEANING = 0; -#endif // CLIENT_DLL - return pBaseAnimating->GetBoneController(LEAN_BONE_CONTROLLER) != BONE_CONTROLLER_VALUE_NOT_LEANING; - } - return false; -} -#endif // NEO - -// ----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::AnimStateLog(const char* pMsg, ...) -{ - // Format the string. - char str[4096]; - va_list marker; - va_start(marker, pMsg); - Q_vsnprintf(str, sizeof(str), pMsg, marker); - va_end(marker); - - // Log it? - if (showanimstate_log.GetInt() == 1 || showanimstate_log.GetInt() == 3) - { - Msg("%s", str); - } - - if (showanimstate_log.GetInt() > 1) - { -#ifdef CLIENT_DLL - const char* fname = "AnimStateClient.log"; -#else - const char* fname = "AnimStateServer.log"; -#endif - static FileHandle_t hFile = filesystem->Open(fname, "wt"); - filesystem->FPrintf(hFile, "%s", str); - filesystem->Flush(hFile); - } -} - -// ----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::AnimStatePrintf(int iLine, const char* pMsg, ...) -{ - // Format the string. - char str[4096]; - va_list marker; - va_start(marker, pMsg); - Q_vsnprintf(str, sizeof(str), pMsg, marker); - va_end(marker); - - // Show it with Con_NPrintf. - engine->Con_NPrintf(iLine, "%s", str); - - // Log it. - AnimStateLog("%s\n", str); -} - - -// ----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::DebugShowAnimState(int iStartLine) -{ - Vector vOuterVel; - GetOuterAbsVelocity(vOuterVel); - - int iLine = iStartLine; - AnimStatePrintf(iLine++, "main: %s(%d), cycle: %.2f cyclerate: %.2f playbackrate: %.2f\n", - GetSequenceName(m_pHL2MPPlayer->GetModelPtr(), m_pHL2MPPlayer->GetSequence()), - m_pHL2MPPlayer->GetSequence(), - m_pHL2MPPlayer->GetCycle(), - m_pHL2MPPlayer->GetSequenceCycleRate(m_pHL2MPPlayer->GetModelPtr(), m_pHL2MPPlayer->GetSequence()), - m_pHL2MPPlayer->GetPlaybackRate() - ); - - for (int i = 0; i < m_pHL2MPPlayer->GetNumAnimOverlays() - 1; i++) - { - CAnimationLayer* pLayer = m_pHL2MPPlayer->GetAnimOverlay(AIMSEQUENCE_LAYER + i); -#ifdef CLIENT_DLL - AnimStatePrintf(iLine++, "%s(%d), weight: %.2f, cycle: %.2f, order (%d), aim (%d)", - !pLayer->IsActive() ? "-- " : (pLayer->m_nSequence == 0 ? "-- " : GetSequenceName(m_pHL2MPPlayer->GetModelPtr(), pLayer->m_nSequence)), - !pLayer->IsActive() ? 0 : (int)pLayer->m_nSequence, - !pLayer->IsActive() ? 0 : (float)pLayer->m_flWeight, - !pLayer->IsActive() ? 0 : (float)pLayer->m_flCycle, - !pLayer->IsActive() ? 0 : (int)pLayer->m_nOrder, - i - ); -#else - AnimStatePrintf(iLine++, "%s(%d), flags (%d), weight: %.2f, cycle: %.2f, order (%d), aim (%d)", - !pLayer->IsActive() ? "-- " : (pLayer->m_nSequence == 0 ? "-- " : GetSequenceName(m_pHL2MPPlayer->GetModelPtr(), pLayer->m_nSequence)), - !pLayer->IsActive() ? 0 : (int)pLayer->m_nSequence, - !pLayer->IsActive() ? 0 : (int)pLayer->m_fFlags,// Doesn't exist on client - !pLayer->IsActive() ? 0 : (float)pLayer->m_flWeight, - !pLayer->IsActive() ? 0 : (float)pLayer->m_flCycle, - !pLayer->IsActive() ? 0 : (int)pLayer->m_nOrder, - i - ); -#endif - } - - AnimStatePrintf(iLine++, "vel: %.2f, time: %.2f, max: %.2f, animspeed: %.2f", - vOuterVel.Length2D(), gpGlobals->curtime, GetInterpolatedGroundSpeed(), m_pHL2MPPlayer->GetSequenceGroundSpeed(m_pHL2MPPlayer->GetSequence())); - - { - AnimStatePrintf(iLine++, "ent yaw: %.2f, body_yaw: %.2f, body_pitch: %.2f, move_x: %.2f, move_y: %.2f", - m_angRender[YAW], g_flLastBodyYaw, g_flLastBodyPitch, m_DebugAnimData.m_vecMoveYaw.x, m_DebugAnimData.m_vecMoveYaw.y); - } - - // Draw a red triangle on the ground for the eye yaw. - float flBaseSize = 10; - float flHeight = 80; - Vector vBasePos = m_pHL2MPPlayer->GetAbsOrigin() + Vector(0, 0, 3); - QAngle angles(0, 0, 0); - angles[YAW] = m_flEyeYaw; - Vector vForward, vRight, vUp; - AngleVectors(angles, &vForward, &vRight, &vUp); - debugoverlay->AddTriangleOverlay(vBasePos + vRight * flBaseSize / 2, vBasePos - vRight * flBaseSize / 2, vBasePos + vForward * flHeight, 255, 0, 0, 255, false, 0.01); - - // Draw a blue triangle on the ground for the body yaw. - angles[YAW] = m_angRender[YAW]; - AngleVectors(angles, &vForward, &vRight, &vUp); - debugoverlay->AddTriangleOverlay(vBasePos + vRight * flBaseSize / 2, vBasePos - vRight * flBaseSize / 2, vBasePos + vForward * flHeight, 0, 0, 255, 255, false, 0.01); - -} - -// ----------------------------------------------------------------------------- -void CHL2MPPlayerAnimState::DebugShowAnimStateFull(int iStartLine) -{ - AnimStateLog("----------------- frame %d -----------------\n", gpGlobals->framecount); - - DebugShowAnimState(iStartLine); - - AnimStateLog("--------------------------------------------\n\n"); -} diff --git a/src/game/shared/hl2mp/hl2mp_playeranimstate.h b/src/game/shared/hl2mp/hl2mp_playeranimstate.h deleted file mode 100644 index 9a05125539..0000000000 --- a/src/game/shared/hl2mp/hl2mp_playeranimstate.h +++ /dev/null @@ -1,80 +0,0 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// -#ifndef HL2MP_PLAYERANIMSTATE_H -#define HL2MP_PLAYERANIMSTATE_H -#ifdef _WIN32 -#pragma once -#endif - - -#include "convar.h" -#include "multiplayer_animstate.h" - -#if defined( CLIENT_DLL ) -class C_HL2MP_Player; -#define CHL2MP_Player C_HL2MP_Player -#else -class CHL2MP_Player; -#endif - -// ------------------------------------------------------------------------------------------------ // -// CPlayerAnimState declaration. -// ------------------------------------------------------------------------------------------------ // -class CHL2MPPlayerAnimState : public CMultiPlayerAnimState -{ -public: - - DECLARE_CLASS( CHL2MPPlayerAnimState, CMultiPlayerAnimState ); - - CHL2MPPlayerAnimState(); - CHL2MPPlayerAnimState( CBasePlayer *pPlayer, MultiPlayerMovementData_t &movementData ); - ~CHL2MPPlayerAnimState(); - - void InitHL2MPAnimState( CHL2MP_Player *pPlayer ); - CHL2MP_Player *GetHL2MPPlayer( void ) { return m_pHL2MPPlayer; } - - virtual void ClearAnimationState(); - int CalcSequenceIndex(const char* pBaseName, ...); - virtual Activity TranslateActivity( Activity actDesired ); - virtual void Update( float eyeYaw, float eyePitch ); - - void DoAnimationEvent( PlayerAnimEvent_t event, int nData = 0 ); - - bool HandleMoving( Activity &idealActivity ); - bool HandleJumping( Activity &idealActivity ); - bool HandleDucking( Activity &idealActivity ); - bool HandleSwimming( Activity &idealActivity ); - - virtual float GetCurrentMaxGroundSpeed(); -#ifdef NEO - virtual void ComputeSequences(CStudioHdr* pStudioHdr); -#endif - - void DebugShowAnimStateFull(int iStartLine); - virtual void DebugShowAnimState(int iStartLine); - void AnimStatePrintf(int iLine, PRINTF_FORMAT_STRING const char* pMsg, ...); - void AnimStateLog(PRINTF_FORMAT_STRING const char* pMsg, ...); - -private: - - bool SetupPoseParameters( CStudioHdr *pStudioHdr ); - virtual void EstimateYaw( void ); - virtual void ComputePoseParam_MoveYaw( CStudioHdr *pStudioHdr ); - virtual void ComputePoseParam_AimPitch( CStudioHdr *pStudioHdr ); - virtual void ComputePoseParam_AimYaw( CStudioHdr *pStudioHdr ); - -#ifdef NEO - bool IsLeaning(CStudioHdr *pStudioHdr); -#endif // NEO - - CHL2MP_Player *m_pHL2MPPlayer; -#ifndef NEO - bool m_bInAirWalk; - float m_flHoldDeployedPoseUntilTime; -#endif -}; - -CHL2MPPlayerAnimState *CreateHL2MPPlayerAnimState( CHL2MP_Player *pPlayer ); - - - -#endif // HL2MP_PLAYERANIMSTATE_H diff --git a/src/game/shared/neo/neo_playeranimstate.cpp b/src/game/shared/neo/neo_playeranimstate.cpp new file mode 100644 index 0000000000..7a16ca7139 --- /dev/null +++ b/src/game/shared/neo/neo_playeranimstate.cpp @@ -0,0 +1,453 @@ +#include "cbase.h" +#include "neo_playeranimstate.h" +#include "datacache/imdlcache.h" +#include "weapon_detpack.h" + +#ifdef CLIENT_DLL +#include "c_neo_player.h" +#else +#include "neo_player.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Upper body sequence names, each followed by the weapon's animation suffix +static constexpr const char *AIM_IDLE_NAME = "idle_upper_"; +static constexpr const char *AIM_CROUCH_IDLE_NAME = "crouch_idle_upper_"; +static constexpr const char *AIM_CROUCH_WALK_NAME = "crouch_walk_upper_"; +static constexpr const char *AIM_WALK_NAME = "walk_upper_"; +static constexpr const char *AIM_RUN_NAME = "run_upper_"; + +static constexpr const char *FIRE_IDLE_NAME = "idle_shoot_"; +static constexpr const char *FIRE_CROUCH_NAME = "crouch_idle_shoot_"; +static constexpr const char *FIRE_CROUCH_WALK_NAME = "crouch_walk_shoot_"; +static constexpr const char *FIRE_WALK_NAME = "walk_shoot_"; +static constexpr const char *FIRE_RUN_NAME = "run_shoot_"; + +// The fire, reload and throw sequences play on the layer above the base's aim layers +static constexpr int GESTURE_LAYER = AIMSEQUENCE_LAYER + NUM_AIMSEQUENCE_LAYERS; +static constexpr int NUM_LAYERS = GESTURE_LAYER + 1; + +// The legs play at the rate of the player's speed over the speed the walk, run and crouch walk sequences cover, +// which is constant as in classic NEOTOKYO: two of the original models' forward walk and run animations carry no movement +static constexpr float ANIM_TOPSPEED_WALK = 100.0f; +static constexpr float ANIM_TOPSPEED_RUN = 250.0f; +static constexpr float ANIM_TOPSPEED_RUN_CROUCH = 85.0f; + +// A sprinting player plays the run legs only above this speed: neo's slowest sprint is 185, +// and bots sprint through turns at 100-180 +static constexpr float SPRINT_LEGS_MIN_SPEED = 100.0f; + +// The feet face the eyes when they are this close (degrees) +static constexpr float FEET_FACE_EYES_TOLERANCE = 1.0f; + +// Don't check if a jumping player is on the ground for this long: +// sometimes the client still has the on-ground flag set right when the jump event comes in +static constexpr float JUMP_MIN_TIME = 0.2f; + +// The remote of a thrown detpack fires the remote's sequence only this long after the detpack is ready to trigger +static constexpr float DET_REMOTE_FIRE_DELAY = 0.005f; + +#ifdef CLIENT_DLL +// A jump has landed when the drawn body is this close above the ground (the ground check of player movement), +static constexpr float JUMP_LAND_GROUND_DIST = 2.0f; +// or when the ground flag has been set this long and the trace still finds no ground under the drawn body +static constexpr float JUMP_LAND_MAX_FLAG_LEAD = 0.5f; +#endif + +CNEOPlayerAnimState *CreateNEOPlayerAnimState( CHL2MP_Player *pPlayer ) +{ + MDLCACHE_CRITICAL_SECTION(); + return new CNEOPlayerAnimState( pPlayer ); +} + +CNEOPlayerAnimState::CNEOPlayerAnimState( CHL2MP_Player *pPlayer ) +{ + m_pPlayer = pPlayer; + m_bJumping = false; + m_bFirstJumpFrame = false; + m_flJumpStartTime = 0.0f; +#ifdef CLIENT_DLL + m_flJumpGroundFlagTime = 0.0f; +#endif + m_iGestureSequence = 0; + m_flGestureCycle = 0.0f; + + CModAnimConfig config; + config.m_flMaxBodyYawDegrees = NEO_ANIMSTATE_MAX_BODY_YAW_DEGREES; + config.m_LegAnimType = LEGANIM_9WAY; + config.m_bUseAimSequences = true; + Init( pPlayer, config ); +} + +void CNEOPlayerAnimState::ClearAnimationState() +{ + m_bJumping = false; + m_iGestureSequence = 0; + BaseClass::ClearAnimationState(); +} + +void CNEOPlayerAnimState::ClearAnimationLayers() +{ + if ( !m_pOuter ) + { + return; + } + + m_pOuter->SetNumAnimOverlays( NUM_LAYERS ); + for ( int i = 0; i < m_pOuter->GetNumAnimOverlays(); i++ ) + { + m_pOuter->GetAnimOverlay( i )->SetOrder( CBaseAnimatingOverlay::MAX_OVERLAYS ); +#ifndef CLIENT_DLL + m_pOuter->GetAnimOverlay( i )->m_fFlags = 0; +#endif + } +} + +void CNEOPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) +{ + switch ( event ) + { + case PLAYERANIMEVENT_ATTACK_PRIMARY: + StartGesture( CalcFireSequence() ); + break; + + case PLAYERANIMEVENT_RELOAD: + StartGesture( CalcReloadSequence() ); + break; + + case PLAYERANIMEVENT_JUMP: + m_bJumping = true; + m_bFirstJumpFrame = true; + m_flJumpStartTime = gpGlobals->curtime; + break; + + case PLAYERANIMEVENT_SPAWN: + ClearAnimationState(); + break; + + default: + break; + } +} + +void CNEOPlayerAnimState::ComputeSequences( CStudioHdr *pStudioHdr ) +{ + BaseClass::ComputeSequences( pStudioHdr ); + ComputeGestureSequence( pStudioHdr ); +} + +void CNEOPlayerAnimState::StartGesture( int iSequence ) +{ + if ( iSequence <= 0 ) + { + return; + } + + m_iGestureSequence = iSequence; + m_flGestureCycle = 0.0f; + m_hGestureWeapon = m_pPlayer->GetActiveWeapon(); +} + +void CNEOPlayerAnimState::ComputeGestureSequence( CStudioHdr *pStudioHdr ) +{ + if ( m_iGestureSequence <= 0 ) + { + return; + } + + // a weapon switch ends the gesture of the previous weapon + if ( m_pPlayer->GetActiveWeapon() != m_hGestureWeapon.Get() ) + { + m_iGestureSequence = 0; + return; + } + + m_flGestureCycle += m_pOuter->GetSequenceCycleRate( pStudioHdr, m_iGestureSequence ) * gpGlobals->frametime; + if ( m_flGestureCycle >= 1.0f ) + { + m_iGestureSequence = 0; + return; + } + + CAnimationLayer *pLayer = m_pOuter->GetAnimOverlay( GESTURE_LAYER ); + pLayer->m_nSequence = m_iGestureSequence; + pLayer->m_flCycle = m_flGestureCycle; + pLayer->m_flPlaybackRate = 1.0f; + pLayer->m_flWeight = 1.0f; + pLayer->m_nOrder = GESTURE_LAYER; +#ifndef CLIENT_DLL + pLayer->m_fFlags |= ANIM_LAYER_ACTIVE; +#endif +} + +Activity CNEOPlayerAnimState::CalcMainActivity() +{ + if ( HandleJumping() ) + { + return ACT_HOP; + } + + if ( m_pPlayer->GetWaterLevel() >= WL_Waist ) + { + return ACT_SWIM; + } + + const float flSpeed = GetOuterXYSpeed(); + if ( m_pPlayer->GetFlags() & FL_DUCKING ) + { + return ( flSpeed > MOVING_MINIMUM_SPEED ) ? ACT_RUN_CROUCH : ACT_CROUCHIDLE; + } + + if ( flSpeed <= MOVING_MINIMUM_SPEED ) + { + return ACT_IDLE; + } + + return ( m_pPlayer->IsSprinting() && flSpeed > SPRINT_LEGS_MIN_SPEED ) ? ACT_RUN : ACT_WALK; +} + +bool CNEOPlayerAnimState::HandleJumping() +{ + if ( !m_bJumping ) + { + return false; + } + + if ( m_bFirstJumpFrame ) + { + m_bFirstJumpFrame = false; + RestartMainSequence(); +#ifdef CLIENT_DLL + m_flJumpGroundFlagTime = 0.0f; +#endif + } + + const bool bInWater = m_pPlayer->GetWaterLevel() >= WL_Waist; + if ( bInWater || ( gpGlobals->curtime - m_flJumpStartTime > JUMP_MIN_TIME && HasLandedFromJump() ) ) + { + m_bJumping = false; + RestartMainSequence(); + } + + return m_bJumping; +} + +//----------------------------------------------------------------------------- +// Purpose: Whether a jumping player has landed. The client draws other players a few tenths of a second behind +// their networked ground flag, so there the jump lasts until the drawn body reaches the ground. +//----------------------------------------------------------------------------- +bool CNEOPlayerAnimState::HasLandedFromJump() +{ + if ( !( m_pPlayer->GetFlags() & FL_ONGROUND ) ) + { +#ifdef CLIENT_DLL + m_flJumpGroundFlagTime = 0.0f; +#endif + return false; + } + +#ifdef CLIENT_DLL + if ( m_flJumpGroundFlagTime <= 0.0f ) + { + m_flJumpGroundFlagTime = gpGlobals->curtime; + } + + if ( gpGlobals->curtime - m_flJumpGroundFlagTime > JUMP_LAND_MAX_FLAG_LEAD ) + { + return true; + } + + const Vector &vecOrigin = m_pPlayer->GetAbsOrigin(); + trace_t tr; + UTIL_TraceHull( vecOrigin, vecOrigin - Vector( 0, 0, JUMP_LAND_GROUND_DIST ), + m_pPlayer->GetPlayerMins(), m_pPlayer->GetPlayerMaxs(), + MASK_PLAYERSOLID, m_pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + + return tr.fraction < 1.0f || tr.startsolid; +#else + return true; +#endif // CLIENT_DLL +} + +float CNEOPlayerAnimState::GetCurrentMaxGroundSpeed() +{ + switch ( m_pOuter->GetSequenceActivity( m_pOuter->GetSequence() ) ) + { + case ACT_IDLE: + case ACT_WALK: + return ANIM_TOPSPEED_WALK; + case ACT_RUN: + return ANIM_TOPSPEED_RUN; + case ACT_RUN_CROUCH: + return ANIM_TOPSPEED_RUN_CROUCH; + default: + return 0.0f; + } +} + +void CNEOPlayerAnimState::ComputePoseParam_BodyYaw() +{ + // a leaning player's feet turn to the eyes, as the base turns a new player's (no turn yet), + const bool bLeaning = IsLeaning(); + if ( bLeaning ) + { + m_flLastTurnTime = 0.0f; + } + + BaseClass::ComputePoseParam_BodyYaw(); + + // and the face-front time counts from the lean or the last frame the feet faced the eyes: + // the base counts from the feet's last turn, so after standing still it squared the feet up on the next turn at once + if ( bLeaning || fabsf( AngleDiff( m_flEyeYaw, m_flCurrentFeetYaw ) ) < FEET_FACE_EYES_TOLERANCE ) + { + m_flLastTurnTime = gpGlobals->curtime; + } +} + +bool CNEOPlayerAnimState::IsLeaning() const +{ + constexpr int LEAN_BONE_CONTROLLER = 0; + // the client reads the lean controller encoded 0-1 (0.5 is upright), the server reads it in degrees +#ifdef CLIENT_DLL + constexpr float BONE_CONTROLLER_VALUE_NOT_LEANING = 0.5f; +#else + constexpr float BONE_CONTROLLER_VALUE_NOT_LEANING = 0.0f; +#endif // CLIENT_DLL + return m_pPlayer->GetBoneController( LEAN_BONE_CONTROLLER ) != BONE_CONTROLLER_VALUE_NOT_LEANING; +} + +//----------------------------------------------------------------------------- +// Purpose: Whether a remote detpack shows its remote: triggered, or thrown and ready to trigger +//----------------------------------------------------------------------------- +static bool IsDetRemoteOut( CBaseCombatWeapon *pWeapon, float flReadyDelay ) +{ + auto pDetpack = static_cast( pWeapon ); + return pDetpack->m_bRemoteHasBeenTriggered || + ( pDetpack->m_bThisDetpackHasBeenThrown && gpGlobals->curtime > pDetpack->m_flNextPrimaryAttack + flReadyDelay ); +} + +static bool IsRemoteDet( CBaseCombatWeapon *pWeapon ) +{ + return !Q_strcmp( pWeapon->GetClassname(), "weapon_remotedet" ); +} + +//----------------------------------------------------------------------------- +// Purpose: The weapon's animation name suffix, with the substitutions for the classes that lack the weapon's animations +//----------------------------------------------------------------------------- +const char *CNEOPlayerAnimState::GetAimSuffix() const +{ + auto pWeapon = m_pPlayer->GetActiveWeapon(); + if ( !pWeapon ) + { + return nullptr; + } + + const int iClass = ToNEOPlayer( m_pPlayer )->m_iNeoClass; + if ( IsRemoteDet( pWeapon ) ) + { + // only recon has the detpack animations, the other classes hold it like a grenade + if ( iClass != NEO_CLASS_RECON ) + { + return "Grenade"; + } + + return IsDetRemoteOut( pWeapon, 0.0f ) ? "detremote" : "detpack"; + } + + const char *pSuffix = pWeapon->GetWpnData().szAnimationPrefix; + if ( iClass != NEO_CLASS_VIP && !Q_strcmp( pSuffix, "smac" ) ) + { + return "srm"; + } + + if ( iClass == NEO_CLASS_VIP && !Q_strcmp( pSuffix, "ZR68C" ) ) + { + return "m4"; + } + + return pSuffix; +} + +int CNEOPlayerAnimState::CalcAimLayerSequence( float *flCycle, float *flAimSequenceWeight, bool bForceIdle ) +{ + const char *pSuffix = GetAimSuffix(); + if ( !pSuffix ) + { + return 0; + } + + const Activity act = GetCurrentMainSequenceActivity(); + if ( bForceIdle ) + { + return CalcSequenceIndex( "%s%s", ( act == ACT_CROUCHIDLE ) ? AIM_CROUCH_IDLE_NAME : AIM_IDLE_NAME, pSuffix ); + } + + switch ( act ) + { + case ACT_RUN: + return CalcSequenceIndex( "%s%s", AIM_RUN_NAME, pSuffix ); + case ACT_WALK: + return CalcSequenceIndex( "%s%s", AIM_WALK_NAME, pSuffix ); + case ACT_CROUCHIDLE: + return CalcSequenceIndex( "%s%s", AIM_CROUCH_IDLE_NAME, pSuffix ); + case ACT_RUN_CROUCH: + return CalcSequenceIndex( "%s%s", AIM_CROUCH_WALK_NAME, pSuffix ); + default: + return CalcSequenceIndex( "%s%s", AIM_IDLE_NAME, pSuffix ); + } +} + +int CNEOPlayerAnimState::CalcFireSequence() +{ + const char *pSuffix = GetAimSuffix(); + if ( !pSuffix ) + { + return 0; + } + + auto pWeapon = m_pPlayer->GetActiveWeapon(); + if ( IsRemoteDet( pWeapon ) ) + { + // a class without the detpack animations throws it like a grenade and does not swing the remote + const bool bRemote = IsDetRemoteOut( pWeapon, DET_REMOTE_FIRE_DELAY ); + if ( ToNEOPlayer( m_pPlayer )->m_iNeoClass == NEO_CLASS_RECON ) + { + pSuffix = bRemote ? "detremote" : "detpack"; + } + else + { + pSuffix = bRemote ? "SRM" : "Gren1"; + } + } + else if ( !Q_strcmp( pSuffix, "Grenade" ) ) + { + pSuffix = "Gren1"; + } + + switch ( GetCurrentMainSequenceActivity() ) + { + case ACT_RUN: + return CalcSequenceIndex( "%s%s", FIRE_RUN_NAME, pSuffix ); + case ACT_WALK: + return CalcSequenceIndex( "%s%s", FIRE_WALK_NAME, pSuffix ); + case ACT_CROUCHIDLE: + return CalcSequenceIndex( "%s%s", FIRE_CROUCH_NAME, pSuffix ); + case ACT_RUN_CROUCH: + return CalcSequenceIndex( "%s%s", FIRE_CROUCH_WALK_NAME, pSuffix ); + default: + return CalcSequenceIndex( "%s%s", FIRE_IDLE_NAME, pSuffix ); + } +} + +int CNEOPlayerAnimState::CalcReloadSequence() +{ + auto pWeapon = m_pPlayer->GetActiveWeapon(); + if ( !pWeapon ) + { + return 0; + } + + return CalcSequenceIndex( "Reload_%s", pWeapon->GetWpnData().szAnimationPrefix ); +} diff --git a/src/game/shared/neo/neo_playeranimstate.h b/src/game/shared/neo/neo_playeranimstate.h new file mode 100644 index 0000000000..4c03be195d --- /dev/null +++ b/src/game/shared/neo/neo_playeranimstate.h @@ -0,0 +1,76 @@ +#ifndef NEO_PLAYERANIMSTATE_H +#define NEO_PLAYERANIMSTATE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "base_playeranimstate.h" + +#ifdef CLIENT_DLL +class C_HL2MP_Player; +#define CHL2MP_Player C_HL2MP_Player +#else +class CHL2MP_Player; +#endif + +enum PlayerAnimEvent_t +{ + PLAYERANIMEVENT_ATTACK_PRIMARY, + PLAYERANIMEVENT_RELOAD, + PLAYERANIMEVENT_JUMP, + PLAYERANIMEVENT_SPAWN, + + PLAYERANIMEVENT_COUNT +}; + +// The player animation of classic NEOTOKYO: the 2006 SDK base (9-way legs, upper body aim layers blended by speed) +// plus one gesture layer for firing, reloading and throwing. +class CNEOPlayerAnimState : public CBasePlayerAnimState +{ +public: + DECLARE_CLASS( CNEOPlayerAnimState, CBasePlayerAnimState ); + + CNEOPlayerAnimState( CHL2MP_Player *pPlayer ); + + void DoAnimationEvent( PlayerAnimEvent_t event, int nData = 0 ); + + virtual void ClearAnimationState() override; + virtual void ClearAnimationLayers() override; + virtual void ComputeSequences( CStudioHdr *pStudioHdr ) override; + virtual Activity CalcMainActivity() override; + virtual int CalcAimLayerSequence( float *flCycle, float *flAimSequenceWeight, bool bForceIdle ) override; + virtual float GetCurrentMaxGroundSpeed() override; + +protected: + virtual void ComputePoseParam_BodyYaw() override; + +private: + const char *GetAimSuffix() const; + int CalcFireSequence(); + int CalcReloadSequence(); + void StartGesture( int iSequence ); + void ComputeGestureSequence( CStudioHdr *pStudioHdr ); + + bool HandleJumping(); + bool HasLandedFromJump(); + bool IsLeaning() const; + + CHL2MP_Player *m_pPlayer; + + // the jump pose plays from the jump event until the player lands + bool m_bJumping; + bool m_bFirstJumpFrame; + float m_flJumpStartTime; +#ifdef CLIENT_DLL + float m_flJumpGroundFlagTime; // when the client first saw the ground flag during a jump, 0 while in the air +#endif + + // fire, reload and throw sequences share one layer above the aim layers, and a new one restarts it + int m_iGestureSequence; + float m_flGestureCycle; + EHANDLE m_hGestureWeapon; +}; + +CNEOPlayerAnimState *CreateNEOPlayerAnimState( CHL2MP_Player *pPlayer ); + +#endif // NEO_PLAYERANIMSTATE_H From 69d69c9fa220aec8c4c9d117945b59460568b6b4 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:55:00 -0600 Subject: [PATCH 08/60] Keep the fire time, load the magazine at the click Keep the end of the reload animation as the time the gun can fire again, as before this branch, and only load the magazine early: the owner stays busy until the animation ends, and the weapon loads the magazine from ItemBusyFrame once the reload's ready time has passed. A switch after the last click keeps the ammo; firing, aiming and the sprint lower still wait for the animation to end. The script key is now ReloadReadyBeforeEnd, the time from the last reload click to the end of the reload animation, and the ready time is that much before the end of the reload sequence actually played, clamped to the reload start. The ready time is networked and predicted so the client loads the magazine on the same frame. --- game/neo/scripts/weapon_aa13.txt | 4 +-- game/neo/scripts/weapon_jitte.txt | 4 +-- game/neo/scripts/weapon_jittescoped.txt | 4 +-- game/neo/scripts/weapon_kyla.txt | 2 +- game/neo/scripts/weapon_m41.txt | 2 +- game/neo/scripts/weapon_m41l.txt | 2 +- game/neo/scripts/weapon_m41s.txt | 2 +- game/neo/scripts/weapon_milso.txt | 2 +- game/neo/scripts/weapon_mpn.txt | 4 +-- game/neo/scripts/weapon_mpn_unsilenced.txt | 4 +-- game/neo/scripts/weapon_mx.txt | 2 +- game/neo/scripts/weapon_mx_silenced.txt | 2 +- game/neo/scripts/weapon_pz.txt | 4 +-- game/neo/scripts/weapon_srm.txt | 4 +-- game/neo/scripts/weapon_srm_s.txt | 4 +-- game/neo/scripts/weapon_srs.txt | 2 +- game/neo/scripts/weapon_tachi.txt | 2 +- game/neo/scripts/weapon_zr68c.txt | 2 +- game/neo/scripts/weapon_zr68l.txt | 2 +- game/neo/scripts/weapon_zr68s.txt | 2 +- .../shared/neo/weapons/neo_weapon_parse.cpp | 2 +- .../shared/neo/weapons/neo_weapon_parse.h | 2 +- .../weapons/weapon_neobasecombatweapon.cpp | 29 +++++++++++++------ .../neo/weapons/weapon_neobasecombatweapon.h | 3 ++ 24 files changed, 53 insertions(+), 39 deletions(-) diff --git a/game/neo/scripts/weapon_aa13.txt b/game/neo/scripts/weapon_aa13.txt index 72faf2faca..f8fe920d0a 100644 --- a/game/neo/scripts/weapon_aa13.txt +++ b/game/neo/scripts/weapon_aa13.txt @@ -6,8 +6,8 @@ WeaponData "Damage" "25" "Bullets" "5" "CycleTime" "0.333" - "ReloadReadyTime" "4.200" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 4.333, the alternative value + "ReloadReadyBeforeEnd" "0.467" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.333 before the end, the alternative value // Weapon data is loaded by both the Game and Client DLLs. "printname" "AA13" diff --git a/game/neo/scripts/weapon_jitte.txt b/game/neo/scripts/weapon_jitte.txt index ff6676127f..2ab8600053 100644 --- a/game/neo/scripts/weapon_jitte.txt +++ b/game/neo/scripts/weapon_jitte.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "15" "Penetration" "12.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyTime" "2.633" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.900, the alternative value + "ReloadReadyBeforeEnd" "0.667" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.400 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_jittescoped.txt b/game/neo/scripts/weapon_jittescoped.txt index a84887e09a..217946dee2 100644 --- a/game/neo/scripts/weapon_jittescoped.txt +++ b/game/neo/scripts/weapon_jittescoped.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "15" "Penetration" "20.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyTime" "2.633" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.900, the alternative value + "ReloadReadyBeforeEnd" "0.667" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.400 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_kyla.txt b/game/neo/scripts/weapon_kyla.txt index c668090a11..8d56a57f53 100644 --- a/game/neo/scripts/weapon_kyla.txt +++ b/game/neo/scripts/weapon_kyla.txt @@ -13,7 +13,7 @@ WeaponData "Damage" "45" //"11" "Penetration" "10.0" "CycleTime" "0.363636" // time between shoots - "ReloadReadyTime" "1.767" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.600" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.6" diff --git a/game/neo/scripts/weapon_m41.txt b/game/neo/scripts/weapon_m41.txt index 071e254a1c..a7727d8038 100644 --- a/game/neo/scripts/weapon_m41.txt +++ b/game/neo/scripts/weapon_m41.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "29" "Penetration" "32.0" "CycleTime" "0.166666" // time between shots - "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_m41l.txt b/game/neo/scripts/weapon_m41l.txt index 56a48fa531..b98a05166c 100644 --- a/game/neo/scripts/weapon_m41l.txt +++ b/game/neo/scripts/weapon_m41l.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "45" "Penetration" "32.0" "CycleTime" "0.5" // time between shots - "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_m41s.txt b/game/neo/scripts/weapon_m41s.txt index fda35bb328..08ae66d288 100644 --- a/game/neo/scripts/weapon_m41s.txt +++ b/game/neo/scripts/weapon_m41s.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "26" "Penetration" "20.0" "CycleTime" "0.166666" // time between shots - "ReloadReadyTime" "3.367" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_milso.txt b/game/neo/scripts/weapon_milso.txt index 7fad6a3130..5e220ac1f1 100644 --- a/game/neo/scripts/weapon_milso.txt +++ b/game/neo/scripts/weapon_milso.txt @@ -13,7 +13,7 @@ WeaponData "Damage" "17" "Penetration" "12.0" "CycleTime" "0.212121" - "ReloadReadyTime" "1.633" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.467" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn.txt b/game/neo/scripts/weapon_mpn.txt index 20bd77b8e9..e61bce9774 100644 --- a/game/neo/scripts/weapon_mpn.txt +++ b/game/neo/scripts/weapon_mpn.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots - "ReloadReadyTime" "2.700" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.800, the alternative value + "ReloadReadyBeforeEnd" "0.500" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.400 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn_unsilenced.txt b/game/neo/scripts/weapon_mpn_unsilenced.txt index fcc71f1ebf..4cc79578cb 100644 --- a/game/neo/scripts/weapon_mpn_unsilenced.txt +++ b/game/neo/scripts/weapon_mpn_unsilenced.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots - "ReloadReadyTime" "2.700" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.800, the alternative value + "ReloadReadyBeforeEnd" "0.500" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.400 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mx.txt b/game/neo/scripts/weapon_mx.txt index 2589f69fe7..2c587ffa45 100644 --- a/game/neo/scripts/weapon_mx.txt +++ b/game/neo/scripts/weapon_mx.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "28" "Penetration" "38.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyTime" "2.500" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.367" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_mx_silenced.txt b/game/neo/scripts/weapon_mx_silenced.txt index ea661d8b14..2f80fc7f50 100644 --- a/game/neo/scripts/weapon_mx_silenced.txt +++ b/game/neo/scripts/weapon_mx_silenced.txt @@ -13,7 +13,7 @@ WeaponData "Damage" "23" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyTime" "2.500" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.367" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_pz.txt b/game/neo/scripts/weapon_pz.txt index 72bac4dd3d..4b9f268184 100644 --- a/game/neo/scripts/weapon_pz.txt +++ b/game/neo/scripts/weapon_pz.txt @@ -15,8 +15,8 @@ WeaponData "Damage" "20" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyTime" "5.133" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 5.367, the alternative value + "ReloadReadyBeforeEnd" "0.633" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.400 before the end, the alternative value "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_srm.txt b/game/neo/scripts/weapon_srm.txt index c38f49ce6a..2d2170338f 100644 --- a/game/neo/scripts/weapon_srm.txt +++ b/game/neo/scripts/weapon_srm.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "12" "Penetration" "20.0" "CycleTime" "0.060606" // time between shots - "ReloadReadyTime" "2.833" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.900, the alternative value + "ReloadReadyBeforeEnd" "0.233" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.167 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_srm_s.txt b/game/neo/scripts/weapon_srm_s.txt index 2555d18694..e8fb6146d3 100644 --- a/game/neo/scripts/weapon_srm_s.txt +++ b/game/neo/scripts/weapon_srm_s.txt @@ -13,8 +13,8 @@ WeaponData "Damage" "12" "Penetration" "10.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyTime" "2.833" // reload is done at the last reload click, the animation plays on to its end; - // the charging handle is home at 2.900, the alternative value + "ReloadReadyBeforeEnd" "0.233" // the reload is done at the last reload click, this long before its animation ends + // the charging handle is home 0.167 before the end, the alternative value "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_srs.txt b/game/neo/scripts/weapon_srs.txt index edfc3f5dc6..ab9e1de751 100644 --- a/game/neo/scripts/weapon_srs.txt +++ b/game/neo/scripts/weapon_srs.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "100" "Penetration" "65.0" "CycleTime" "1.4" - "ReloadReadyTime" "3.067" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.633" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_tachi.txt b/game/neo/scripts/weapon_tachi.txt index d409d0f4a4..89d0bf13c1 100644 --- a/game/neo/scripts/weapon_tachi.txt +++ b/game/neo/scripts/weapon_tachi.txt @@ -32,7 +32,7 @@ WeaponData "Damage" "17" "Penetration" "19.0" "CycleTime" "0.121212" - "ReloadReadyTime" "1.700" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.400" // the reload is done at the last reload click, this long before its animation ends "ViewModelOffset" { diff --git a/game/neo/scripts/weapon_zr68c.txt b/game/neo/scripts/weapon_zr68c.txt index 3c4945526a..f11a4f6aef 100644 --- a/game/neo/scripts/weapon_zr68c.txt +++ b/game/neo/scripts/weapon_zr68c.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "17" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyTime" "2.267" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.433" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68l.txt b/game/neo/scripts/weapon_zr68l.txt index ce27a1f96e..aea02e630a 100644 --- a/game/neo/scripts/weapon_zr68l.txt +++ b/game/neo/scripts/weapon_zr68l.txt @@ -15,7 +15,7 @@ WeaponData "Damage" "37" "Penetration" "32.0" "CycleTime" "0.5" // time between shots - "ReloadReadyTime" "2.533" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.167" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index 6d8127dd26..f5c6574194 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -17,7 +17,7 @@ WeaponData "Damage" "22" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyTime" "2.267" // reload is done here, the animation plays on to its end + "ReloadReadyBeforeEnd" "0.433" // the reload is done at the last reload click, this long before its animation ends "TPMuzzleFlashScale" "0.5" diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index 8bad569c25..bafe4597b6 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -38,7 +38,7 @@ void CNEOWeaponInfo::Parse( KeyValues *pKeyValuesData, const char *szWeaponName m_iPlayerDamage = pKeyValuesData->GetInt( "Damage", 42 ); // Douglas Adams 1952 - 2001 m_iBullets = pKeyValuesData->GetInt( "Bullets", 1 ); m_flCycleTime = pKeyValuesData->GetFloat( "CycleTime", 0.15 ); - m_flReloadReadyTime = pKeyValuesData->GetFloat( "ReloadReadyTime", 0.0f ); + m_flReloadReadyBeforeEnd = pKeyValuesData->GetFloat( "ReloadReadyBeforeEnd", 0.0f ); const char *notFoundStr = "notfound"; Q_strncpy(szViewModel2, pKeyValuesData->GetString("team2viewmodel", notFoundStr), MAX_WEAPON_STRING); diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index 5ef4b51c80..040dc00649 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -24,7 +24,7 @@ class CNEOWeaponInfo : public CHL2MPSWeaponInfo int m_iBullets; float m_flCycleTime; - float m_flReloadReadyTime; + float m_flReloadReadyBeforeEnd; char szViewModel2[MAX_WEAPON_STRING]; // Team 2 (NSF) wep vm char szBulletCharacter[MAX_BULLET_CHARACTER];// character used to display ammunition in current clip char szDeathIcon[MAX_BULLET_CHARACTER]; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 841544fe84..c40f1f9041 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -41,6 +41,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) RecvPropFloat(RECVINFO(m_flAccuracyPenalty)), RecvPropInt(RECVINFO(m_nNumShotsFired)), RecvPropBool(RECVINFO(m_bTriggerReset)), + RecvPropTime(RECVINFO(m_flReloadReadyTime)), RecvPropInt(RECVINFO(m_spawnflags)), #else SendPropTime(SENDINFO(m_flSoonestAttack)), @@ -48,6 +49,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropFloat(SENDINFO(m_flAccuracyPenalty)), SendPropInt(SENDINFO(m_nNumShotsFired)), SendPropBool(SENDINFO(m_bTriggerReset)), + SendPropTime(SENDINFO(m_flReloadReadyTime)), SendPropInt(SENDINFO(m_spawnflags)), SendPropExclude("DT_BaseAnimating", "m_nSequence"), #endif @@ -60,6 +62,7 @@ BEGIN_PREDICTION_DATA(CNEOBaseCombatWeapon) DEFINE_PRED_FIELD(m_flAccuracyPenalty, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_nNumShotsFired, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_bTriggerReset, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flReloadReadyTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -72,6 +75,7 @@ BEGIN_DATADESC( CNEOBaseCombatWeapon ) DEFINE_FIELD(m_flAccuracyPenalty, FIELD_FLOAT), DEFINE_FIELD(m_nNumShotsFired, FIELD_INTEGER), DEFINE_FIELD(m_bTriggerReset, FIELD_BOOLEAN), + DEFINE_FIELD(m_flReloadReadyTime, FIELD_TIME), END_DATADESC() #endif @@ -421,15 +425,9 @@ bool CNEOBaseCombatWeapon::Reload( void ) return false; } - // The magazine is loaded at the gun's last reload click, - // and the rest of the reload animation plays on as before - const float flReadyTime = GetNEOWpnData().m_flReloadReadyTime; - if (flReadyTime > 0.0f) - { - const float flReady = gpGlobals->curtime + flReadyTime; - GetOwner()->SetNextAttack(flReady); - m_flNextPrimaryAttack = m_flNextSecondaryAttack = flReady; - } + // The magazine loads at the gun's last reload click, which the script gives as a time before the reload animation ends; + // the animation and the wait to fire still run to the end + m_flReloadReadyTime = Max(gpGlobals->curtime, m_flNextPrimaryAttack - GetNEOWpnData().m_flReloadReadyBeforeEnd); return true; @@ -467,6 +465,13 @@ bool CNEOBaseCombatWeapon::Reload( void ) //----------------------------------------------------------------------------- void CNEOBaseCombatWeapon::FinishReload(void) { + // The magazine was already loaded at the reload's ready time + if (m_flReloadReadyTime == 0.0f) + { + return; + } + m_flReloadReadyTime = 0.0f; + CBaseCombatCharacter* pOwner = GetOwner(); if (pOwner) @@ -627,6 +632,12 @@ void CNEOBaseCombatWeapon::ItemBusyFrame(void) { BaseClass::ItemBusyFrame(); UpdateInaccuracy(); + + // The owner stays busy until the reload animation ends, but the magazine loads at the reload's ready time + if (m_bInReload && m_flReloadReadyTime > 0.0f && gpGlobals->curtime >= m_flReloadReadyTime) + { + FinishReload(); + } } void CNEOBaseCombatWeapon::ItemHolsterFrame(void) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index b8a0afbefa..1e8caa7a0b 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -290,6 +290,9 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon CNetworkVar(int, m_nNumShotsFired); CNetworkVar(bool, m_bTriggerReset); + // When the reload in progress loads the magazine, 0 once it is loaded + CNetworkVar(float, m_flReloadReadyTime); + private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); From a7a5ac2d14f001877abbf6bb122d20c5416bf9b4 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:08:00 -0600 Subject: [PATCH 09/60] Add the ZR68 reload sound pieces to the sound script Four entries, weapon_zr68.reload_{drop,seat,slap,bolt}, for the per-phase reload pieces sliced from zr68_reload.wav (neoAssets research-reload-phases 86ebd7f+). CHAN_ITEM, so a later piece from the same weapon replaces an unfinished one. No code plays them yet. --- game/neo/scripts/game_sounds_weapons.txt | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index ad9ac75c4f..b48815b6c8 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -770,6 +770,44 @@ "wave" ")weapons/zr68/zr68_reload.wav" } +// ZR68 reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_zr68.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68_reload_drop.wav" +} + +"weapon_zr68.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68_reload_seat.wav" +} + +"weapon_zr68.reload_slap" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68_reload_slap.wav" +} + +"weapon_zr68.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68_reload_bolt.wav" +} + //------------------- "weapon_zr68l.reload" From 505894071bacadb2b127f09f6f0df3368f67b556 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:09:00 -0600 Subject: [PATCH 10/60] Parse the ReloadPhases weapon-script block A weapon script may list, per reload sequence, the phases the sequence carries (drop, seat, slap, bolt), each with the time it counts and the sound piece that goes with it, and whether a partial reload keeps a chambered round. Data only: nothing reads it yet. The ZR68S gets its block, generated from ntre's segments.csv for the sequences in neoAssets research-reload-phases 86ebd7f. --- game/neo/scripts/weapon_zr68s.txt | 44 +++++++++++++ .../shared/neo/weapons/neo_weapon_parse.cpp | 63 +++++++++++++++++++ .../shared/neo/weapons/neo_weapon_parse.h | 42 +++++++++++++ 3 files changed, 149 insertions(+) diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index f5c6574194..c0f49a3bf7 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -19,6 +19,50 @@ WeaponData "CycleTime" "0.106060" // time between shots "ReloadReadyBeforeEnd" "0.433" // the reload is done at the last reload click, this long before its animation ends + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.306" } + "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } + "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } + "bolt" { "credit" "2.267" "sound" "weapon_zr68.reload_bolt" "at" "2.029" } + } + "reload_partial" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.306" } + "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } + "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "0.733" "sound" "weapon_zr68.reload_seat" "at" "0.694" } + "slap" { "credit" "1.100" "sound" "weapon_zr68.reload_slap" "at" "1.129" } + "bolt" { "credit" "1.667" "sound" "weapon_zr68.reload_bolt" "at" "1.429" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "0.733" "sound" "weapon_zr68.reload_seat" "at" "0.694" } + "slap" { "credit" "1.100" "sound" "weapon_zr68.reload_slap" "at" "1.129" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.300" "sound" "weapon_zr68.reload_slap" "at" "0.329" } + "bolt" { "credit" "0.867" "sound" "weapon_zr68.reload_bolt" "at" "0.629" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.300" "sound" "weapon_zr68.reload_slap" "at" "0.329" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "0.767" "sound" "weapon_zr68.reload_bolt" "at" "0.529" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "30" diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index bafe4597b6..59bfc81349 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -24,6 +24,9 @@ CNEOWeaponInfo::CNEOWeaponInfo() m_flPenetration = 0.f; m_bDropOnDeath = true; iAimFOV = 0; + m_flReloadReadyBeforeEnd = 0.0f; + m_bChamberedRound = false; + m_iNumReloadTables = 0; m_flVMFov = m_flVMAimFov = 0.f; m_vecVMPosOffset = m_vecVMAimPosOffset = vec3_origin; @@ -31,6 +34,65 @@ CNEOWeaponInfo::CNEOWeaponInfo() } +static ReloadPhase_t ReloadPhaseFromName(const char *pszName) +{ + static constexpr const char *PHASE_NAMES[RELOAD_PHASE_COUNT] = { "", "drop", "seat", "slap", "bolt" }; + for (int i = RELOAD_PHASE_DROPPED; i < RELOAD_PHASE_COUNT; ++i) + { + if (V_stricmp(pszName, PHASE_NAMES[i]) == 0) + { + return static_cast(i); + } + } + return RELOAD_PHASE_NONE; +} + +// "ReloadPhases" { "ChamberedRound" "1" "" { "" { "credit" "s" "sound" "name" "at" "s" } ... } ... } +void CNEOWeaponInfo::ParseReloadPhases(KeyValues *pBlock) +{ + m_iNumReloadTables = 0; + if (!pBlock) + { + return; + } + + m_bChamberedRound = pBlock->GetBool("ChamberedRound", true); + for (KeyValues *pSeq = pBlock->GetFirstTrueSubKey(); pSeq && m_iNumReloadTables < RELOAD_PHASE_TABLES_MAX; pSeq = pSeq->GetNextTrueSubKey()) + { + ReloadPhaseTable_t &table = m_reloadTables[m_iNumReloadTables++]; + V_strncpy(table.szSequence, pSeq->GetName(), sizeof(table.szSequence)); + table.iNumRows = 0; + + for (KeyValues *pRow = pSeq->GetFirstTrueSubKey(); pRow && table.iNumRows < RELOAD_PHASE_ROWS_MAX; pRow = pRow->GetNextTrueSubKey()) + { + const ReloadPhase_t phase = ReloadPhaseFromName(pRow->GetName()); + if (phase == RELOAD_PHASE_NONE) + { + Warning("%s: unknown reload phase \"%s\" in \"%s\"\n", szClassName, pRow->GetName(), table.szSequence); + continue; + } + + ReloadPhaseRow_t &row = table.rows[table.iNumRows++]; + row.phase = phase; + row.flCredit = pRow->GetFloat("credit"); + row.flSoundAt = pRow->GetFloat("at"); + V_strncpy(row.szSound, pRow->GetString("sound"), sizeof(row.szSound)); + } + } +} + +int CNEOWeaponInfo::FindReloadTable(const char *pszSequence) const +{ + for (int i = 0; i < m_iNumReloadTables; ++i) + { + if (V_stricmp(m_reloadTables[i].szSequence, pszSequence) == 0) + { + return i; + } + } + return -1; +} + void CNEOWeaponInfo::Parse( KeyValues *pKeyValuesData, const char *szWeaponName ) { BaseClass::Parse( pKeyValuesData, szWeaponName ); @@ -39,6 +101,7 @@ void CNEOWeaponInfo::Parse( KeyValues *pKeyValuesData, const char *szWeaponName m_iBullets = pKeyValuesData->GetInt( "Bullets", 1 ); m_flCycleTime = pKeyValuesData->GetFloat( "CycleTime", 0.15 ); m_flReloadReadyBeforeEnd = pKeyValuesData->GetFloat( "ReloadReadyBeforeEnd", 0.0f ); + ParseReloadPhases(pKeyValuesData->FindKey("ReloadPhases")); const char *notFoundStr = "notfound"; Q_strncpy(szViewModel2, pKeyValuesData->GetString("team2viewmodel", notFoundStr), MAX_WEAPON_STRING); diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index 040dc00649..d9ef6fb52e 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -12,6 +12,40 @@ #include "hl2mp_weapon_parse.h" +// A reload is a linear sequence of phases; a reload sequence credits each phase at a point of its animation +enum ReloadPhase_t +{ + RELOAD_PHASE_NONE = 0, // no reload in progress, or none pending + RELOAD_PHASE_DROPPED, // the old magazine is out + RELOAD_PHASE_SEATED, // the fresh magazine is in + RELOAD_PHASE_SLAPPED, // the magazine was tapped or slapped home + RELOAD_PHASE_BOLTED, // the bolt or charging handle chambered a round + + RELOAD_PHASE_COUNT +}; + +static constexpr int RELOAD_PHASE_TABLES_MAX = 8; +static constexpr int RELOAD_PHASE_ROWS_MAX = RELOAD_PHASE_COUNT - 1; +static constexpr int RELOAD_SOUND_NAME_MAX = 64; +static constexpr int RELOAD_SEQUENCE_NAME_MAX = 32; + +// One phase of a reload sequence: when it is credited, and the sound piece that goes with it +struct ReloadPhaseRow_t +{ + ReloadPhase_t phase; + float flCredit; // seconds from the sequence start + float flSoundAt; // when the sound piece's first transient plays, seconds from the sequence start + char szSound[RELOAD_SOUND_NAME_MAX]; +}; + +// The phases one reload sequence carries, in order +struct ReloadPhaseTable_t +{ + char szSequence[RELOAD_SEQUENCE_NAME_MAX]; + int iNumRows; + ReloadPhaseRow_t rows[RELOAD_PHASE_ROWS_MAX]; +}; + //-------------------------------------------------------------------------------------------------------- class CNEOWeaponInfo : public CHL2MPSWeaponInfo { @@ -25,6 +59,11 @@ class CNEOWeaponInfo : public CHL2MPSWeaponInfo int m_iBullets; float m_flCycleTime; float m_flReloadReadyBeforeEnd; + bool m_bChamberedRound; // a partial reload keeps the round in the chamber + int m_iNumReloadTables; // 0: the weapon reloads without phases + ReloadPhaseTable_t m_reloadTables[RELOAD_PHASE_TABLES_MAX]; + + int FindReloadTable(const char *pszSequence) const; char szViewModel2[MAX_WEAPON_STRING]; // Team 2 (NSF) wep vm char szBulletCharacter[MAX_BULLET_CHARACTER];// character used to display ammunition in current clip char szDeathIcon[MAX_BULLET_CHARACTER]; @@ -39,6 +78,9 @@ class CNEOWeaponInfo : public CHL2MPSWeaponInfo float m_flVMAimFov; Vector m_vecVMAimPosOffset; QAngle m_angVMAimAngOffset; + +private: + void ParseReloadPhases(::KeyValues *pBlock); }; From 5f5fc36e4cda31edb874a283527ee78371839e61 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:10:00 -0600 Subject: [PATCH 11/60] Track reload phases and credit ammo per phase A weapon whose script lists ReloadPhases reloads as a linear sequence of phases instead of one refill at the end. The drop leaves only a chambered round in the gun (the old magazine is gone), the seat loads one magazine from the reserve, the bolt chambers a round on a dry reload. A partial reload of a 30-round gun ends at 31. The phases credited so far survive a weapon switch; once the weapon is drawn again the reload resumes and credits only the phases left. Until the resume sequences are selected, the shipped reload plays and the earlier phases are skipped. The wait to fire stays the length of the animation that plays. Weapons without the block (Supa7, SMAC, the rest for now) take the previous path unchanged. The ZR68S drops ReloadReadyBeforeEnd: its seat credit replaces it. --- game/neo/scripts/weapon_zr68s.txt | 1 - .../weapons/weapon_neobasecombatweapon.cpp | 260 ++++++++++++++++++ .../neo/weapons/weapon_neobasecombatweapon.h | 20 ++ 3 files changed, 280 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index c0f49a3bf7..047af749a4 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -17,7 +17,6 @@ WeaponData "Damage" "22" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyBeforeEnd" "0.433" // the reload is done at the last reload click, this long before its animation ends // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index aea8431488..5c738b7c59 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -42,6 +42,11 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) RecvPropInt(RECVINFO(m_nNumShotsFired)), RecvPropBool(RECVINFO(m_bTriggerReset)), RecvPropTime(RECVINFO(m_flReloadReadyTime)), + RecvPropInt(RECVINFO(m_nReloadPhase)), + RecvPropBool(RECVINFO(m_bChamberEmpty)), + RecvPropBool(RECVINFO(m_bReloadDry)), + RecvPropInt(RECVINFO(m_nReloadTable)), + RecvPropTime(RECVINFO(m_flReloadStart)), RecvPropInt(RECVINFO(m_spawnflags)), #else SendPropTime(SENDINFO(m_flSoonestAttack)), @@ -50,6 +55,11 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropInt(SENDINFO(m_nNumShotsFired)), SendPropBool(SENDINFO(m_bTriggerReset)), SendPropTime(SENDINFO(m_flReloadReadyTime)), + SendPropInt(SENDINFO(m_nReloadPhase), 3, SPROP_UNSIGNED), + SendPropBool(SENDINFO(m_bChamberEmpty)), + SendPropBool(SENDINFO(m_bReloadDry)), + SendPropInt(SENDINFO(m_nReloadTable), 4), + SendPropTime(SENDINFO(m_flReloadStart)), SendPropInt(SENDINFO(m_spawnflags)), SendPropExclude("DT_BaseAnimating", "m_nSequence"), #endif @@ -63,6 +73,11 @@ BEGIN_PREDICTION_DATA(CNEOBaseCombatWeapon) DEFINE_PRED_FIELD(m_nNumShotsFired, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_bTriggerReset, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flReloadReadyTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nReloadPhase, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_bChamberEmpty, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_bReloadDry, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nReloadTable, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flReloadStart, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -76,6 +91,11 @@ BEGIN_DATADESC( CNEOBaseCombatWeapon ) DEFINE_FIELD(m_nNumShotsFired, FIELD_INTEGER), DEFINE_FIELD(m_bTriggerReset, FIELD_BOOLEAN), DEFINE_FIELD(m_flReloadReadyTime, FIELD_TIME), + DEFINE_FIELD(m_nReloadPhase, FIELD_INTEGER), + DEFINE_FIELD(m_bChamberEmpty, FIELD_BOOLEAN), + DEFINE_FIELD(m_bReloadDry, FIELD_BOOLEAN), + DEFINE_FIELD(m_nReloadTable, FIELD_INTEGER), + DEFINE_FIELD(m_flReloadStart, FIELD_TIME), END_DATADESC() #endif @@ -233,6 +253,11 @@ static const WeaponHandlingInfo_t handlingTable[] = { CNEOBaseCombatWeapon::CNEOBaseCombatWeapon( void ) { m_bTriggerReset = true; + m_nReloadPhase = RELOAD_PHASE_NONE; + m_bChamberEmpty = false; + m_bReloadDry = false; + m_nReloadTable = -1; + m_flReloadStart = 0.0f; } const CNEOWeaponInfo &CNEOBaseCombatWeapon::GetNEOWpnData() const @@ -420,6 +445,11 @@ void CNEOBaseCombatWeapon::Equip(CBaseCombatCharacter* pOwner) bool CNEOBaseCombatWeapon::Reload( void ) { + if (HasReloadPhases()) + { + return ReloadWithPhases(); + } + if (!BaseClass::Reload()) { return false; @@ -459,12 +489,200 @@ bool CNEOBaseCombatWeapon::Reload( void ) #endif } +// The shipped reload sequence, the dry reload with every phase +static constexpr const char *VM_SEQ_RELOAD = "reload"; + +bool CNEOBaseCombatWeapon::HasReloadPhases() const +{ + return GetNEOWpnData().m_iNumReloadTables > 0; +} + +// A reload is dry when it has to chamber a round with the bolt +bool CNEOBaseCombatWeapon::IsReloadDry() const +{ + return !GetNEOWpnData().m_bChamberedRound || m_bChamberEmpty; +} + +// The phase that completes a reload: the bolt on a dry one, the last magazine phase otherwise +ReloadPhase_t CNEOBaseCombatWeapon::LastReloadPhase(bool bDry) const +{ + const CNEOWeaponInfo &info = GetNEOWpnData(); + const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); + if (iTable < 0) + { + return RELOAD_PHASE_NONE; + } + + ReloadPhase_t last = RELOAD_PHASE_NONE; + const ReloadPhaseTable_t &table = info.m_reloadTables[iTable]; + for (int i = 0; i < table.iNumRows; ++i) + { + if (table.rows[i].phase == RELOAD_PHASE_BOLTED && !bDry) + { + continue; + } + last = Max(last, table.rows[i].phase); + } + return last; +} + +// An interrupted reload has phases credited and phases left to do +bool CNEOBaseCombatWeapon::IsReloadPending() const +{ + return m_nReloadPhase != RELOAD_PHASE_NONE && m_nReloadPhase < LastReloadPhase(IsReloadDry()); +} + +// Start, or resume after a switch, a reload whose phases the weapon script lists +bool CNEOBaseCombatWeapon::ReloadWithPhases() +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (!pOwner || m_bInReload || pOwner->GetNextAttack() > gpGlobals->curtime) + { + return false; + } + + const CNEOWeaponInfo &info = GetNEOWpnData(); + if (!IsReloadPending()) + { + m_nReloadPhase = RELOAD_PHASE_NONE; + if (m_iPrimaryAmmoCount <= 0) + { + return false; + } + + if (m_iClip1 == 0) + { + CBasePlayer *pPlayer = ToBasePlayer(pOwner); + if (pPlayer && !(pPlayer->m_nButtons & IN_RELOAD) && !ClientWantsAutoReload(pOwner)) + { + return false; + } + m_bChamberEmpty = info.m_bChamberedRound; + } + + // a full magazine with a round in the chamber has nothing to gain + const int iFull = GetMaxClip1() + (IsReloadDry() ? 0 : 1); + if (m_iClip1 >= iFull) + { + return false; + } + } + + const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); + if (iTable < 0) + { + Warning("%s: ReloadPhases has no \"%s\" sequence\n", GetClassname(), VM_SEQ_RELOAD); + return false; + } + + // The shipped reload plays whatever phases are left; the phases credited before a switch are not credited again + m_bReloadDry = IsReloadDry(); + m_nReloadTable = iTable; + WeaponSound(RELOAD_NPC); + SendWeaponAnim(ACT_VM_RELOAD); + if (pOwner->IsPlayer()) + { + static_cast(pOwner)->SetAnimation(PLAYER_RELOAD); + } + + // the wait to fire is never shorter than the animation that plays + const float flEnd = gpGlobals->curtime + SequenceDuration(); + pOwner->SetNextAttack(flEnd); + m_flNextPrimaryAttack = m_flNextSecondaryAttack = flEnd; + m_flReloadStart = gpGlobals->curtime; + m_bInReload = true; + return true; +} + +// Credit every phase of the playing reload sequence whose time has come +void CNEOBaseCombatWeapon::RunReloadPhases() +{ + if (!m_bInReload || m_nReloadTable < 0) + { + return; + } + + const ReloadPhaseTable_t &table = GetNEOWpnData().m_reloadTables[m_nReloadTable]; + const float flElapsed = gpGlobals->curtime - m_flReloadStart; + for (int i = 0; i < table.iNumRows; ++i) + { + const ReloadPhaseRow_t &row = table.rows[i]; + if (row.phase <= m_nReloadPhase || (row.phase == RELOAD_PHASE_BOLTED && !m_bReloadDry)) + { + continue; + } + if (flElapsed < row.flCredit) + { + break; + } + CreditReloadPhase(row.phase); + } +} + +void CNEOBaseCombatWeapon::CreditReloadPhase(ReloadPhase_t phase) +{ + switch (phase) + { + case RELOAD_PHASE_DROPPED: + // the old magazine and its rounds are gone, a chambered round stays + m_iClip1 = (m_iClip1 > 0 && !IsReloadDry()) ? 1 : 0; + break; + case RELOAD_PHASE_SEATED: + { + const int iLoad = Min(GetMaxClip1(), m_iPrimaryAmmoCount.Get()); + m_iClip1 += iLoad; + m_iPrimaryAmmoCount -= iLoad; + break; + } + case RELOAD_PHASE_BOLTED: + m_bChamberEmpty = false; + break; + default: + break; + } + + m_nReloadPhase = phase; +} + +void CNEOBaseCombatWeapon::CheckReloadWithPhases() +{ + if (m_bInReload) + { + RunReloadPhases(); + if (m_flNextPrimaryAttack > gpGlobals->curtime) + { + return; + } + + // the sequence has ended + m_bInReload = false; + m_nReloadTable = -1; + if (!IsReloadPending()) + { + m_nReloadPhase = RELOAD_PHASE_NONE; + } + return; + } + + // an interrupted reload resumes once the weapon is drawn again + if (IsReloadPending()) + { + Reload(); + } +} + //----------------------------------------------------------------------------- // Purpose: Reload has finished. //----------------------------------------------------------------------------- void CNEOBaseCombatWeapon::FinishReload(void) { + // A reload with phases loads at its seat credit + if (HasReloadPhases()) + { + return; + } + // The magazine was already loaded at the reload's ready time if (m_flReloadReadyTime == 0.0f) { @@ -588,11 +806,27 @@ float CNEOBaseCombatWeapon::GetPenetration() const bool CNEOBaseCombatWeapon::Holster(CBaseCombatWeapon* pSwitchingTo) { + // The phases credited so far stay; a reload with nothing left to do is over + if (HasReloadPhases()) + { + if (!IsReloadPending()) + { + m_nReloadPhase = RELOAD_PHASE_NONE; + } + m_nReloadTable = -1; + } + return BaseClass::Holster(pSwitchingTo); } void CNEOBaseCombatWeapon::CheckReload(void) { + if (HasReloadPhases()) + { + CheckReloadWithPhases(); + return; + } + if (!m_bInReload && UsesClipsForAmmo1() && m_iClip1 == 0 && GetOwner() && !ClientWantsAutoReload(GetOwner())) { return; @@ -643,6 +877,12 @@ void CNEOBaseCombatWeapon::ItemBusyFrame(void) BaseClass::ItemBusyFrame(); UpdateInaccuracy(); + if (HasReloadPhases()) + { + RunReloadPhases(); + return; + } + // The owner stays busy until the reload animation ends, but the magazine loads at the reload's ready time if (m_bInReload && m_flReloadReadyTime > 0.0f && gpGlobals->curtime >= m_flReloadReadyTime) { @@ -1005,6 +1245,13 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) return; } + // a dry reload seated the magazine but has not chambered a round yet + else if (HasReloadPhases() && m_bChamberEmpty && m_iClip1 > 0) + { + CheckReload(); + return; + } + else if (m_iClip1 == 0) { if (!m_bFireOnEmpty) @@ -1116,6 +1363,19 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) m_iPrimaryAmmoCount -= info.m_iShots; } + if (HasReloadPhases()) + { + // the last round leaves the chamber empty, and a shot shows the seated magazine feeds without its slap + if (GetNEOWpnData().m_bChamberedRound && m_iClip1 <= 0) + { + m_bChamberEmpty = true; + } + if (m_nReloadPhase == RELOAD_PHASE_SEATED) + { + m_nReloadPhase = RELOAD_PHASE_NONE; + } + } + // the last round's shot goes straight on to the empty idle when it ends if (UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle()) { diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index a772edda17..e7ae04a0b8 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -294,10 +294,30 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon // When the reload in progress loads the magazine, 0 once it is loaded CNetworkVar(float, m_flReloadReadyTime); + // Reload phases (weapons whose script has a ReloadPhases block): the last phase credited, kept across a switch + CNetworkVar(int, m_nReloadPhase); + // no round in the chamber: the gun cannot fire, and the next reload needs the bolt + CNetworkVar(bool, m_bChamberEmpty); + // the reload in progress needs the bolt + CNetworkVar(bool, m_bReloadDry); + // the phase table of the sequence playing, -1 none + CNetworkVar(int, m_nReloadTable); + // when the reload sequence started; phase credits are offsets from it + CNetworkVar(float, m_flReloadStart); + private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); bool HasEmptyIdle(); + bool HasReloadPhases() const; + bool IsReloadDry() const; + ReloadPhase_t LastReloadPhase(bool bDry) const; + bool IsReloadPending() const; + bool ReloadWithPhases(); + void RunReloadPhases(); + void CreditReloadPhase(ReloadPhase_t phase); + void CheckReloadWithPhases(); + }; #endif // WEAPON_NEO_BASECOMBATWEAPON_SHARED_H From 9d3c2afa49a7bfdde17ae6ae3bc3f33d8e718ca5 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:11:00 -0600 Subject: [PATCH 12/60] Time the ZR68S resume phases for the resized reaches The resume sequences' reaches now keep the hand at the shipped reach pace (neoAssets research-reload-phases b863e63), so each resume credits its phases 0.5 s later than before. --- game/neo/scripts/weapon_zr68s.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index 047af749a4..98b52a92e9 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -38,27 +38,27 @@ WeaponData } "reload_from_drop_dry" { - "seat" { "credit" "0.733" "sound" "weapon_zr68.reload_seat" "at" "0.694" } - "slap" { "credit" "1.100" "sound" "weapon_zr68.reload_slap" "at" "1.129" } - "bolt" { "credit" "1.667" "sound" "weapon_zr68.reload_bolt" "at" "1.429" } + "seat" { "credit" "1.233" "sound" "weapon_zr68.reload_seat" "at" "1.194" } + "slap" { "credit" "1.600" "sound" "weapon_zr68.reload_slap" "at" "1.629" } + "bolt" { "credit" "2.167" "sound" "weapon_zr68.reload_bolt" "at" "1.929" } } "reload_from_drop_partial" { - "seat" { "credit" "0.733" "sound" "weapon_zr68.reload_seat" "at" "0.694" } - "slap" { "credit" "1.100" "sound" "weapon_zr68.reload_slap" "at" "1.129" } + "seat" { "credit" "1.233" "sound" "weapon_zr68.reload_seat" "at" "1.194" } + "slap" { "credit" "1.600" "sound" "weapon_zr68.reload_slap" "at" "1.629" } } "reload_from_seat_dry" { - "slap" { "credit" "0.300" "sound" "weapon_zr68.reload_slap" "at" "0.329" } - "bolt" { "credit" "0.867" "sound" "weapon_zr68.reload_bolt" "at" "0.629" } + "slap" { "credit" "0.767" "sound" "weapon_zr68.reload_slap" "at" "0.796" } + "bolt" { "credit" "1.333" "sound" "weapon_zr68.reload_bolt" "at" "1.096" } } "reload_from_seat_partial" { - "slap" { "credit" "0.300" "sound" "weapon_zr68.reload_slap" "at" "0.329" } + "slap" { "credit" "0.767" "sound" "weapon_zr68.reload_slap" "at" "0.796" } } "reload_from_slap_dry" { - "bolt" { "credit" "0.767" "sound" "weapon_zr68.reload_bolt" "at" "0.529" } + "bolt" { "credit" "1.233" "sound" "weapon_zr68.reload_bolt" "at" "0.996" } } } From 8d7daa223797d772fb00a6f11aad1f6fa83fa508 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:12:00 -0600 Subject: [PATCH 13/60] Play the reload sound in per-phase pieces A reload whose phases name sound pieces plays each piece when its time in the playing sequence comes (each file starts 0.02 s before its first transient), instead of the one reload wav at the start. Phases credited before a switch do not play their pieces again, and a switch stops the pieces still sounding. Also adds GetMaxRoundsInGun, the magazine plus a chambered round, for the ammo HUD. --- .../weapons/weapon_neobasecombatweapon.cpp | 89 ++++++++++++++++++- .../neo/weapons/weapon_neobasecombatweapon.h | 7 ++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 5c738b7c59..d078d66eb4 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -47,6 +47,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) RecvPropBool(RECVINFO(m_bReloadDry)), RecvPropInt(RECVINFO(m_nReloadTable)), RecvPropTime(RECVINFO(m_flReloadStart)), + RecvPropInt(RECVINFO(m_nReloadSoundsDone)), RecvPropInt(RECVINFO(m_spawnflags)), #else SendPropTime(SENDINFO(m_flSoonestAttack)), @@ -60,6 +61,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropBool(SENDINFO(m_bReloadDry)), SendPropInt(SENDINFO(m_nReloadTable), 4), SendPropTime(SENDINFO(m_flReloadStart)), + SendPropInt(SENDINFO(m_nReloadSoundsDone), RELOAD_PHASE_ROWS_MAX, SPROP_UNSIGNED), SendPropInt(SENDINFO(m_spawnflags)), SendPropExclude("DT_BaseAnimating", "m_nSequence"), #endif @@ -78,6 +80,7 @@ BEGIN_PREDICTION_DATA(CNEOBaseCombatWeapon) DEFINE_PRED_FIELD(m_bReloadDry, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_nReloadTable, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flReloadStart, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nReloadSoundsDone, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -96,6 +99,7 @@ BEGIN_DATADESC( CNEOBaseCombatWeapon ) DEFINE_FIELD(m_bReloadDry, FIELD_BOOLEAN), DEFINE_FIELD(m_nReloadTable, FIELD_INTEGER), DEFINE_FIELD(m_flReloadStart, FIELD_TIME), + DEFINE_FIELD(m_nReloadSoundsDone, FIELD_INTEGER), END_DATADESC() #endif @@ -258,6 +262,7 @@ CNEOBaseCombatWeapon::CNEOBaseCombatWeapon( void ) m_bReloadDry = false; m_nReloadTable = -1; m_flReloadStart = 0.0f; + m_nReloadSoundsDone = 0; } const CNEOWeaponInfo &CNEOBaseCombatWeapon::GetNEOWpnData() const @@ -489,6 +494,9 @@ bool CNEOBaseCombatWeapon::Reload( void ) #endif } +// Each reload sound piece's file starts this long before its first transient +static constexpr float RELOAD_SOUND_LEAD = 0.02f; + // The shipped reload sequence, the dry reload with every phase static constexpr const char *VM_SEQ_RELOAD = "reload"; @@ -578,7 +586,11 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() // The shipped reload plays whatever phases are left; the phases credited before a switch are not credited again m_bReloadDry = IsReloadDry(); m_nReloadTable = iTable; - WeaponSound(RELOAD_NPC); + m_nReloadSoundsDone = 0; + if (!HasReloadSoundPieces()) + { + WeaponSound(RELOAD_NPC); + } SendWeaponAnim(ACT_VM_RELOAD); if (pOwner->IsPlayer()) { @@ -611,6 +623,14 @@ void CNEOBaseCombatWeapon::RunReloadPhases() { continue; } + + // a phase's sound piece may start before the phase is credited, such as the grab before the magazine drops + if (row.szSound[0] && !(m_nReloadSoundsDone & (1 << i)) && flElapsed >= row.flSoundAt - RELOAD_SOUND_LEAD) + { + m_nReloadSoundsDone |= (1 << i); + EmitReloadSoundPiece(row.szSound); + } + if (flElapsed < row.flCredit) { break; @@ -619,6 +639,72 @@ void CNEOBaseCombatWeapon::RunReloadPhases() } } +bool CNEOBaseCombatWeapon::HasReloadSoundPieces() const +{ + const CNEOWeaponInfo &info = GetNEOWpnData(); + for (int t = 0; t < info.m_iNumReloadTables; ++t) + { + for (int i = 0; i < info.m_reloadTables[t].iNumRows; ++i) + { + if (info.m_reloadTables[t].rows[i].szSound[0]) + { + return true; + } + } + } + return false; +} + +// Play a reload sound piece from the owner, as WeaponSound does: predicted for the owner, from the server for the rest +void CNEOBaseCombatWeapon::EmitReloadSoundPiece(const char *pszSound) +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (!pOwner) + { + return; + } + + CSoundParameters params; + if (!GetParametersForSound(pszSound, params, NULL)) + { + return; + } + + CPASAttenuationFilter filter(pOwner, params.soundlevel); + if (IsPredicted() && CBaseEntity::GetPredictionPlayer()) + { + filter.UsePredictionRules(); + } + EmitSound(filter, pOwner->entindex(), pszSound); +} + +// A switch cuts the reload sound pieces off +void CNEOBaseCombatWeapon::StopReloadSoundPieces() +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (!pOwner) + { + return; + } + + const CNEOWeaponInfo &info = GetNEOWpnData(); + for (int t = 0; t < info.m_iNumReloadTables; ++t) + { + for (int i = 0; i < info.m_reloadTables[t].iNumRows; ++i) + { + if (info.m_reloadTables[t].rows[i].szSound[0]) + { + StopSound(pOwner->entindex(), info.m_reloadTables[t].rows[i].szSound); + } + } + } +} + +int CNEOBaseCombatWeapon::GetMaxRoundsInGun() const +{ + return GetMaxClip1() + ((HasReloadPhases() && GetNEOWpnData().m_bChamberedRound) ? 1 : 0); +} + void CNEOBaseCombatWeapon::CreditReloadPhase(ReloadPhase_t phase) { switch (phase) @@ -814,6 +900,7 @@ bool CNEOBaseCombatWeapon::Holster(CBaseCombatWeapon* pSwitchingTo) m_nReloadPhase = RELOAD_PHASE_NONE; } m_nReloadTable = -1; + StopReloadSoundPieces(); } return BaseClass::Holster(pSwitchingTo); diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index e7ae04a0b8..85259371a4 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -264,6 +264,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon virtual float GetFireRate() override final; float GetPenetration() const; + // the magazine plus the round a partial reload keeps in the chamber + int GetMaxRoundsInGun() const; #ifdef CLIENT_DLL float m_flTemperature; int m_spawnflags; @@ -304,6 +306,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon CNetworkVar(int, m_nReloadTable); // when the reload sequence started; phase credits are offsets from it CNetworkVar(float, m_flReloadStart); + // the sound pieces of the playing sequence already started, one bit per phase row + CNetworkVar(int, m_nReloadSoundsDone); private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); @@ -317,6 +321,9 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon void RunReloadPhases(); void CreditReloadPhase(ReloadPhase_t phase); void CheckReloadWithPhases(); + bool HasReloadSoundPieces() const; + void StopReloadSoundPieces(); + void EmitReloadSoundPiece(const char *pszSound); }; From 545bb3e60c7b6557c64b5dc833667bea35758f32 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:13:00 -0600 Subject: [PATCH 14/60] Show the chambered round on the ammo HUD A weapon that keeps a round in the chamber on a partial reload shows one more bullet slot, so a partial reload of a 30-round magazine reads 31/31. During a dry reload the last slot stays empty until the bolt chambers a round. --- src/game/client/neo/ui/neo_hud_ammo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/neo/ui/neo_hud_ammo.cpp b/src/game/client/neo/ui/neo_hud_ammo.cpp index b8038ac8ab..4e1405bbb4 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.cpp +++ b/src/game/client/neo/ui/neo_hud_ammo.cpp @@ -156,7 +156,7 @@ void CNEOHud_Ammo::DrawAmmo() const char fireModeText[2]{ '\0' }; ammoChar = activeWep->GetNEOWpnData().szBulletCharacter; - magSizeMax = activeWep->GetMaxClip1(); + magSizeMax = activeWep->GetMaxRoundsInGun(); magSizeCurrent = activeWep->Clip1(); if(activeWep) From 39ec946b746a691e0b3f7eb31a7aaa9febfa700a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:14:00 -0600 Subject: [PATCH 15/60] Add the ZR68S draw-resume phase tables The ZR68S script lists the phases of draw_resume_drop_{dry,partial} (neoAssets research-reload-phases c61699a), the resume that starts from the draw after the magazine dropped. A weapon may now list up to 12 reload sequences. --- game/neo/scripts/weapon_zr68s.txt | 11 +++++++++++ src/game/shared/neo/weapons/neo_weapon_parse.h | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index 98b52a92e9..8781b643c5 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -60,6 +60,17 @@ WeaponData { "bolt" { "credit" "1.233" "sound" "weapon_zr68.reload_bolt" "at" "0.996" } } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } + "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } + "bolt" { "credit" "2.367" "sound" "weapon_zr68.reload_bolt" "at" "2.129" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } + "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index d9ef6fb52e..3508a83736 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -24,7 +24,7 @@ enum ReloadPhase_t RELOAD_PHASE_COUNT }; -static constexpr int RELOAD_PHASE_TABLES_MAX = 8; +static constexpr int RELOAD_PHASE_TABLES_MAX = 12; static constexpr int RELOAD_PHASE_ROWS_MAX = RELOAD_PHASE_COUNT - 1; static constexpr int RELOAD_SOUND_NAME_MAX = 64; static constexpr int RELOAD_SEQUENCE_NAME_MAX = 32; From 46c405cbda3d87e7aec7830ade8a3e3d093dcd28 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:15:00 -0600 Subject: [PATCH 16/60] Play the ZR68S drop sound from the first hand sound The drop piece now starts with the hand's first soft sounds (neoAssets research-reload-phases c20c13b), so its first transient is at 0.146 s. --- game/neo/scripts/weapon_zr68s.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index 8781b643c5..b6185deaff 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -25,14 +25,14 @@ WeaponData "ChamberedRound" "1" "reload" { - "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.306" } + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.146" } "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } "bolt" { "credit" "2.267" "sound" "weapon_zr68.reload_bolt" "at" "2.029" } } "reload_partial" { - "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.306" } + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.146" } "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } } From 4a54e949442f20f4bfa97fcf675a47c28fe776eb Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:16:00 -0600 Subject: [PATCH 17/60] Select partial and resume reload sequences by name A reload with phases plays the sequence for what is left: the partial reload when a round is chambered, and after a switch the resume that starts with the hand reaching back to the next phase (from the drop, the seat or the slap, dry or partial). After the magazine dropped, the resume starts in the draw itself, which brings the fresh magazine up out of view. A model without the sequence plays the shipped reload and credits only the phases left, as before. --- .../weapons/weapon_neobasecombatweapon.cpp | 124 +++++++++++++++--- .../neo/weapons/weapon_neobasecombatweapon.h | 2 + 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index d078d66eb4..7d148fb425 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -500,6 +500,30 @@ static constexpr float RELOAD_SOUND_LEAD = 0.02f; // The shipped reload sequence, the dry reload with every phase static constexpr const char *VM_SEQ_RELOAD = "reload"; +// The reload sequence for the phase credited last, dry or partial; nullptr: nothing left to play +// (sequences other than the shipped reload carry no activity, so they are picked by name) +static const char *ReloadSequenceName(int iPhaseDone, bool bDry, bool bFromDraw) +{ + switch (iPhaseDone) + { + case RELOAD_PHASE_NONE: + return bDry ? VM_SEQ_RELOAD : "reload_partial"; + case RELOAD_PHASE_DROPPED: + // after a switch the magazine is out: the draw itself brings the fresh one up out of view + if (bFromDraw) + { + return bDry ? "draw_resume_drop_dry" : "draw_resume_drop_partial"; + } + return bDry ? "reload_from_drop_dry" : "reload_from_drop_partial"; + case RELOAD_PHASE_SEATED: + return bDry ? "reload_from_seat_dry" : "reload_from_seat_partial"; + case RELOAD_PHASE_SLAPPED: + return bDry ? "reload_from_slap_dry" : nullptr; + default: + return nullptr; + } +} + bool CNEOBaseCombatWeapon::HasReloadPhases() const { return GetNEOWpnData().m_iNumReloadTables > 0; @@ -576,33 +600,16 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() } } - const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); + m_bReloadDry = IsReloadDry(); + int iSequence = -1; + const int iTable = SelectReloadTable(false, &iSequence); if (iTable < 0) { Warning("%s: ReloadPhases has no \"%s\" sequence\n", GetClassname(), VM_SEQ_RELOAD); return false; } - // The shipped reload plays whatever phases are left; the phases credited before a switch are not credited again - m_bReloadDry = IsReloadDry(); - m_nReloadTable = iTable; - m_nReloadSoundsDone = 0; - if (!HasReloadSoundPieces()) - { - WeaponSound(RELOAD_NPC); - } - SendWeaponAnim(ACT_VM_RELOAD); - if (pOwner->IsPlayer()) - { - static_cast(pOwner)->SetAnimation(PLAYER_RELOAD); - } - - // the wait to fire is never shorter than the animation that plays - const float flEnd = gpGlobals->curtime + SequenceDuration(); - pOwner->SetNextAttack(flEnd); - m_flNextPrimaryAttack = m_flNextSecondaryAttack = flEnd; - m_flReloadStart = gpGlobals->curtime; - m_bInReload = true; + StartReloadSequence(iTable, iSequence); return true; } @@ -639,6 +646,69 @@ void CNEOBaseCombatWeapon::RunReloadPhases() } } +// The phase table, and the viewmodel sequence (-1: the reload activity), for the reload to play now. +// A model without the sequence a phase calls for plays the shipped reload, crediting only the phases left +int CNEOBaseCombatWeapon::SelectReloadTable(bool bFromDraw, int *pSequence) const +{ + const CNEOWeaponInfo &info = GetNEOWpnData(); + *pSequence = -1; + + const char *pszName = ReloadSequenceName(m_nReloadPhase, m_bReloadDry, bFromDraw); + if (pszName && V_strcmp(pszName, VM_SEQ_RELOAD) != 0) + { + const int iTable = info.FindReloadTable(pszName); + const int iSequence = (iTable >= 0) ? const_cast(this)->LookupSequence(pszName) : -1; + if (iSequence >= 0) + { + *pSequence = iSequence; + return iTable; + } + } + + if (bFromDraw) + { + return -1; + } + return info.FindReloadTable(VM_SEQ_RELOAD); +} + +// Play a reload sequence from its start; its phase table credits the phases left from now on +void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence) +{ + CBaseCombatCharacter *pOwner = GetOwner(); + m_nReloadTable = iTable; + m_nReloadSoundsDone = 0; + if (!HasReloadSoundPieces()) + { + WeaponSound(RELOAD_NPC); + } + + if (iSequence >= 0) + { + SetSequence(iSequence); + SendViewModelAnim(iSequence); + } + else + { + SendWeaponAnim(ACT_VM_RELOAD); + } + if (pOwner && pOwner->IsPlayer()) + { + static_cast(pOwner)->SetAnimation(PLAYER_RELOAD); + } + + // the wait to fire is never shorter than the animation that plays + const float flEnd = gpGlobals->curtime + SequenceDuration(); + if (pOwner) + { + pOwner->SetNextAttack(flEnd); + } + m_flNextPrimaryAttack = m_flNextSecondaryAttack = flEnd; + SetWeaponIdleTime(flEnd); + m_flReloadStart = gpGlobals->curtime; + m_bInReload = true; +} + bool CNEOBaseCombatWeapon::HasReloadSoundPieces() const { const CNEOWeaponInfo &info = GetNEOWpnData(); @@ -832,6 +902,18 @@ bool CNEOBaseCombatWeapon::Deploy(void) SendViewModelAnim(iDrawEmpty); } + // A reload interrupted after the magazine dropped resumes in the draw, where the model has that draw + if (HasReloadPhases() && m_nReloadPhase == RELOAD_PHASE_DROPPED && IsReloadPending()) + { + m_bReloadDry = IsReloadDry(); + int iSequence = -1; + const int iTable = SelectReloadTable(true, &iSequence); + if (iTable >= 0) + { + StartReloadSequence(iTable, iSequence); + } + } + #ifdef DEBUG CNEO_Player* pOwner = NULL; if (GetOwner()) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 85259371a4..a5faa0af81 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -324,6 +324,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon bool HasReloadSoundPieces() const; void StopReloadSoundPieces(); void EmitReloadSoundPiece(const char *pszSound); + int SelectReloadTable(bool bFromDraw, int *pSequence) const; + void StartReloadSequence(int iTable, int iSequence); }; From d4df589b555a5ade2cf4e533e8269eb5c27ea7ab Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:17:00 -0600 Subject: [PATCH 18/60] Fill the HUD's overflow mark for a full gun with a round chambered When the bullet row overflows into a + mark, the mark is filled for a full gun; with the chambered round counted, full is the magazine plus one. --- src/game/client/neo/ui/neo_hud_ammo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/neo/ui/neo_hud_ammo.cpp b/src/game/client/neo/ui/neo_hud_ammo.cpp index 4e1405bbb4..994e536e0b 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.cpp +++ b/src/game/client/neo/ui/neo_hud_ammo.cpp @@ -248,7 +248,7 @@ void CNEOHud_Ammo::DrawAmmo() const if (magSizeMax > 0) bullets[magSizeMax - 1] = '+'; - if(maxClip == magSizeCurrent) + if(activeWep->GetMaxRoundsInGun() == magSizeCurrent) { magAmountToDrawFilled = magSizeMax; } else if(magSizeMax - 1 < magSizeCurrent) From 10c0013d1e47cde991a04f2c3bb90b6b960a950a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:18:00 -0600 Subject: [PATCH 19/60] Give the ZR68C reload phases The ZR68C reloads in phases like the ZR68S (same bones, same sound pieces), with its sequences from neoAssets research-reload-phases 5edfe2e. Its ReloadReadyBeforeEnd goes: the seat credit replaces it. --- game/neo/scripts/weapon_zr68c.txt | 56 ++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/weapon_zr68c.txt b/game/neo/scripts/weapon_zr68c.txt index f11a4f6aef..7834324e80 100644 --- a/game/neo/scripts/weapon_zr68c.txt +++ b/game/neo/scripts/weapon_zr68c.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "17" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyBeforeEnd" "0.433" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.146" } + "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } + "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } + "bolt" { "credit" "2.267" "sound" "weapon_zr68.reload_bolt" "at" "2.029" } + } + "reload_partial" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68.reload_drop" "at" "0.146" } + "seat" { "credit" "1.333" "sound" "weapon_zr68.reload_seat" "at" "1.294" } + "slap" { "credit" "1.700" "sound" "weapon_zr68.reload_slap" "at" "1.729" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.233" "sound" "weapon_zr68.reload_seat" "at" "1.194" } + "slap" { "credit" "1.600" "sound" "weapon_zr68.reload_slap" "at" "1.629" } + "bolt" { "credit" "2.167" "sound" "weapon_zr68.reload_bolt" "at" "1.929" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.233" "sound" "weapon_zr68.reload_seat" "at" "1.194" } + "slap" { "credit" "1.600" "sound" "weapon_zr68.reload_slap" "at" "1.629" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.767" "sound" "weapon_zr68.reload_slap" "at" "0.796" } + "bolt" { "credit" "1.333" "sound" "weapon_zr68.reload_bolt" "at" "1.096" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.767" "sound" "weapon_zr68.reload_slap" "at" "0.796" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "1.233" "sound" "weapon_zr68.reload_bolt" "at" "0.996" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } + "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } + "bolt" { "credit" "2.367" "sound" "weapon_zr68.reload_bolt" "at" "2.129" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } + "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } + } + } "TPMuzzleFlashScale" "0.5" From 0e37c6ffb41cdb6f05b7ab2e96092830cb0c66bb Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:19:00 -0600 Subject: [PATCH 20/60] Give the ZR68L reload phases and its own sound pieces The ZR68L reloads in phases with sequences and sound pieces from neoAssets research-reload-phases d18409d; its own wav's pieces join the sound script as weapon_zr68l.reload_{drop,seat,slap,bolt}. Its ReloadReadyBeforeEnd goes: the seat credit replaces it. --- game/neo/scripts/game_sounds_weapons.txt | 38 ++++++++++++++++ game/neo/scripts/weapon_zr68l.txt | 56 +++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index b48815b6c8..e5f1e2ee2e 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -832,6 +832,44 @@ "wave" ")weapons/zr68/zr68l_reload.wav" } +// ZR68L reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_zr68l.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68l_reload_drop.wav" +} + +"weapon_zr68l.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68l_reload_seat.wav" +} + +"weapon_zr68l.reload_slap" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68l_reload_slap.wav" +} + +"weapon_zr68l.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/zr68/zr68l_reload_bolt.wav" +} + //------------------- "weapon_zr68.empty" diff --git a/game/neo/scripts/weapon_zr68l.txt b/game/neo/scripts/weapon_zr68l.txt index aea02e630a..da14b16fa2 100644 --- a/game/neo/scripts/weapon_zr68l.txt +++ b/game/neo/scripts/weapon_zr68l.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "37" "Penetration" "32.0" "CycleTime" "0.5" // time between shots - "ReloadReadyBeforeEnd" "0.167" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68l.reload_drop" "at" "0.183" } + "seat" { "credit" "1.333" "sound" "weapon_zr68l.reload_seat" "at" "1.335" } + "slap" { "credit" "1.700" "sound" "weapon_zr68l.reload_slap" "at" "1.691" } + "bolt" { "credit" "2.267" "sound" "weapon_zr68l.reload_bolt" "at" "1.996" } + } + "reload_partial" + { + "drop" { "credit" "0.700" "sound" "weapon_zr68l.reload_drop" "at" "0.183" } + "seat" { "credit" "1.333" "sound" "weapon_zr68l.reload_seat" "at" "1.335" } + "slap" { "credit" "1.700" "sound" "weapon_zr68l.reload_slap" "at" "1.691" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.267" "sound" "weapon_zr68l.reload_seat" "at" "1.268" } + "slap" { "credit" "1.633" "sound" "weapon_zr68l.reload_slap" "at" "1.624" } + "bolt" { "credit" "2.200" "sound" "weapon_zr68l.reload_bolt" "at" "1.929" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.267" "sound" "weapon_zr68l.reload_seat" "at" "1.268" } + "slap" { "credit" "1.633" "sound" "weapon_zr68l.reload_slap" "at" "1.624" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.800" "sound" "weapon_zr68l.reload_slap" "at" "0.791" } + "bolt" { "credit" "1.367" "sound" "weapon_zr68l.reload_bolt" "at" "1.096" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.800" "sound" "weapon_zr68l.reload_slap" "at" "0.791" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "1.233" "sound" "weapon_zr68l.reload_bolt" "at" "0.963" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.533" "sound" "weapon_zr68l.reload_seat" "at" "1.535" } + "slap" { "credit" "1.900" "sound" "weapon_zr68l.reload_slap" "at" "1.891" } + "bolt" { "credit" "2.467" "sound" "weapon_zr68l.reload_bolt" "at" "2.196" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.533" "sound" "weapon_zr68l.reload_seat" "at" "1.535" } + "slap" { "credit" "1.900" "sound" "weapon_zr68l.reload_slap" "at" "1.891" } + } + } "TPMuzzleFlashScale" "0.5" From 897e3e103f199199895e2423856d94676f84a25d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:20:00 -0600 Subject: [PATCH 21/60] Give the MX and MX-S reload phases and sound pieces Both MX guns reload in phases with the sequences and sound pieces of neoAssets research-reload-phases 0c24082 (the seat rattle sounds at the silent magazine seat, 0.27 s before its place in the wav); the pieces join the sound script as weapon_mx.reload_*. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 38 ++++++++++++++++ game/neo/scripts/weapon_mx.txt | 56 +++++++++++++++++++++++- game/neo/scripts/weapon_mx_silenced.txt | 56 +++++++++++++++++++++++- 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index e5f1e2ee2e..ff6b9a30ee 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -981,6 +981,44 @@ "wave" ")weapons/mx/mx_reload.wav" } +// weapon_mx reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_mx.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mx/mx_reload_drop.wav" +} + +"weapon_mx.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mx/mx_reload_seat.wav" +} + +"weapon_mx.reload_slap" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mx/mx_reload_slap.wav" +} + +"weapon_mx.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mx/mx_reload_bolt.wav" +} + //------------------- "weapon_mx.empty" diff --git a/game/neo/scripts/weapon_mx.txt b/game/neo/scripts/weapon_mx.txt index 2c587ffa45..e34e2be4b5 100644 --- a/game/neo/scripts/weapon_mx.txt +++ b/game/neo/scripts/weapon_mx.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "28" "Penetration" "38.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyBeforeEnd" "0.367" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.467" "sound" "weapon_mx.reload_drop" "at" "0.354" } + "seat" { "credit" "1.300" "sound" "weapon_mx.reload_seat" "at" "1.284" } + "slap" { "credit" "1.700" "sound" "weapon_mx.reload_slap" "at" "1.725" } + "bolt" { "credit" "2.500" "sound" "weapon_mx.reload_bolt" "at" "2.200" } + } + "reload_partial" + { + "drop" { "credit" "0.467" "sound" "weapon_mx.reload_drop" "at" "0.354" } + "seat" { "credit" "1.300" "sound" "weapon_mx.reload_seat" "at" "1.284" } + "slap" { "credit" "1.700" "sound" "weapon_mx.reload_slap" "at" "1.725" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.167" "sound" "weapon_mx.reload_seat" "at" "1.151" } + "slap" { "credit" "1.567" "sound" "weapon_mx.reload_slap" "at" "1.592" } + "bolt" { "credit" "2.367" "sound" "weapon_mx.reload_bolt" "at" "2.067" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.167" "sound" "weapon_mx.reload_seat" "at" "1.151" } + "slap" { "credit" "1.567" "sound" "weapon_mx.reload_slap" "at" "1.592" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.600" "sound" "weapon_mx.reload_slap" "at" "0.625" } + "bolt" { "credit" "1.400" "sound" "weapon_mx.reload_bolt" "at" "1.100" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.600" "sound" "weapon_mx.reload_slap" "at" "0.625" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "1.300" "sound" "weapon_mx.reload_bolt" "at" "1.000" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } + "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } + "bolt" { "credit" "2.600" "sound" "weapon_mx.reload_bolt" "at" "2.300" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } + "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } + } + } "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_mx_silenced.txt b/game/neo/scripts/weapon_mx_silenced.txt index 2f80fc7f50..a9ec2d62b8 100644 --- a/game/neo/scripts/weapon_mx_silenced.txt +++ b/game/neo/scripts/weapon_mx_silenced.txt @@ -13,7 +13,61 @@ WeaponData "Damage" "23" "Penetration" "18.0" "CycleTime" "0.106060" // time between shots - "ReloadReadyBeforeEnd" "0.367" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.467" "sound" "weapon_mx.reload_drop" "at" "0.354" } + "seat" { "credit" "1.300" "sound" "weapon_mx.reload_seat" "at" "1.284" } + "slap" { "credit" "1.700" "sound" "weapon_mx.reload_slap" "at" "1.725" } + "bolt" { "credit" "2.500" "sound" "weapon_mx.reload_bolt" "at" "2.200" } + } + "reload_partial" + { + "drop" { "credit" "0.467" "sound" "weapon_mx.reload_drop" "at" "0.354" } + "seat" { "credit" "1.300" "sound" "weapon_mx.reload_seat" "at" "1.284" } + "slap" { "credit" "1.700" "sound" "weapon_mx.reload_slap" "at" "1.725" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.167" "sound" "weapon_mx.reload_seat" "at" "1.151" } + "slap" { "credit" "1.567" "sound" "weapon_mx.reload_slap" "at" "1.592" } + "bolt" { "credit" "2.367" "sound" "weapon_mx.reload_bolt" "at" "2.067" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.167" "sound" "weapon_mx.reload_seat" "at" "1.151" } + "slap" { "credit" "1.567" "sound" "weapon_mx.reload_slap" "at" "1.592" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.600" "sound" "weapon_mx.reload_slap" "at" "0.625" } + "bolt" { "credit" "1.400" "sound" "weapon_mx.reload_bolt" "at" "1.100" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.600" "sound" "weapon_mx.reload_slap" "at" "0.625" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "1.300" "sound" "weapon_mx.reload_bolt" "at" "1.000" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } + "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } + "bolt" { "credit" "2.600" "sound" "weapon_mx.reload_bolt" "at" "2.300" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } + "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } + } + } "TPMuzzleFlashScale" "0.3" From ef49e0f7abc2f9f8435c2c8be315431d966c268e Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:21:00 -0600 Subject: [PATCH 22/60] Allow a second sound piece per reload phase A phase may name sound2 / at2 beside sound / at, for a phase whose sound comes in two pieces timed apart: the M41's seat has a tap rattle that belongs at the silent seat and tap clicks that stay on the tap. --- .../shared/neo/weapons/neo_weapon_parse.cpp | 8 +++++-- .../shared/neo/weapons/neo_weapon_parse.h | 14 ++++++++--- .../weapons/weapon_neobasecombatweapon.cpp | 23 +++++++++++++------ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index 59bfc81349..42f32c0980 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -75,8 +75,12 @@ void CNEOWeaponInfo::ParseReloadPhases(KeyValues *pBlock) ReloadPhaseRow_t &row = table.rows[table.iNumRows++]; row.phase = phase; row.flCredit = pRow->GetFloat("credit"); - row.flSoundAt = pRow->GetFloat("at"); - V_strncpy(row.szSound, pRow->GetString("sound"), sizeof(row.szSound)); + static constexpr const char *SOUND_KEYS[RELOAD_SOUNDS_PER_ROW][2] = { { "sound", "at" }, { "sound2", "at2" } }; + for (int j = 0; j < RELOAD_SOUNDS_PER_ROW; ++j) + { + row.sounds[j].flAt = pRow->GetFloat(SOUND_KEYS[j][1]); + V_strncpy(row.sounds[j].szName, pRow->GetString(SOUND_KEYS[j][0]), sizeof(row.sounds[j].szName)); + } } } } diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index 3508a83736..672db0e46e 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -28,14 +28,22 @@ static constexpr int RELOAD_PHASE_TABLES_MAX = 12; static constexpr int RELOAD_PHASE_ROWS_MAX = RELOAD_PHASE_COUNT - 1; static constexpr int RELOAD_SOUND_NAME_MAX = 64; static constexpr int RELOAD_SEQUENCE_NAME_MAX = 32; +// a phase may need two sound pieces timed apart, such as the M41's tap rattle and tap clicks +static constexpr int RELOAD_SOUNDS_PER_ROW = 2; -// One phase of a reload sequence: when it is credited, and the sound piece that goes with it +// A sound piece of a phase +struct ReloadSoundPiece_t +{ + float flAt; // when the piece's first transient plays, seconds from the sequence start + char szName[RELOAD_SOUND_NAME_MAX]; +}; + +// One phase of a reload sequence: when it is credited, and the sound pieces that go with it struct ReloadPhaseRow_t { ReloadPhase_t phase; float flCredit; // seconds from the sequence start - float flSoundAt; // when the sound piece's first transient plays, seconds from the sequence start - char szSound[RELOAD_SOUND_NAME_MAX]; + ReloadSoundPiece_t sounds[RELOAD_SOUNDS_PER_ROW]; }; // The phases one reload sequence carries, in order diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 7d148fb425..deaf4ec3e6 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -61,7 +61,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropBool(SENDINFO(m_bReloadDry)), SendPropInt(SENDINFO(m_nReloadTable), 4), SendPropTime(SENDINFO(m_flReloadStart)), - SendPropInt(SENDINFO(m_nReloadSoundsDone), RELOAD_PHASE_ROWS_MAX, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nReloadSoundsDone), RELOAD_PHASE_ROWS_MAX * RELOAD_SOUNDS_PER_ROW, SPROP_UNSIGNED), SendPropInt(SENDINFO(m_spawnflags)), SendPropExclude("DT_BaseAnimating", "m_nSequence"), #endif @@ -632,10 +632,15 @@ void CNEOBaseCombatWeapon::RunReloadPhases() } // a phase's sound piece may start before the phase is credited, such as the grab before the magazine drops - if (row.szSound[0] && !(m_nReloadSoundsDone & (1 << i)) && flElapsed >= row.flSoundAt - RELOAD_SOUND_LEAD) + for (int j = 0; j < RELOAD_SOUNDS_PER_ROW; ++j) { - m_nReloadSoundsDone |= (1 << i); - EmitReloadSoundPiece(row.szSound); + const ReloadSoundPiece_t &piece = row.sounds[j]; + const int iBit = 1 << (i * RELOAD_SOUNDS_PER_ROW + j); + if (piece.szName[0] && !(m_nReloadSoundsDone & iBit) && flElapsed >= piece.flAt - RELOAD_SOUND_LEAD) + { + m_nReloadSoundsDone |= iBit; + EmitReloadSoundPiece(piece.szName); + } } if (flElapsed < row.flCredit) @@ -716,7 +721,7 @@ bool CNEOBaseCombatWeapon::HasReloadSoundPieces() const { for (int i = 0; i < info.m_reloadTables[t].iNumRows; ++i) { - if (info.m_reloadTables[t].rows[i].szSound[0]) + if (info.m_reloadTables[t].rows[i].sounds[0].szName[0]) { return true; } @@ -762,9 +767,13 @@ void CNEOBaseCombatWeapon::StopReloadSoundPieces() { for (int i = 0; i < info.m_reloadTables[t].iNumRows; ++i) { - if (info.m_reloadTables[t].rows[i].szSound[0]) + for (int j = 0; j < RELOAD_SOUNDS_PER_ROW; ++j) { - StopSound(pOwner->entindex(), info.m_reloadTables[t].rows[i].szSound); + const char *pszName = info.m_reloadTables[t].rows[i].sounds[j].szName; + if (pszName[0]) + { + StopSound(pOwner->entindex(), pszName); + } } } } From 146289f2bc29ef6bea5c442c65d25a92529e1a57 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:22:00 -0600 Subject: [PATCH 23/60] Give the M41, M41S and M41L reload phases and sound pieces The M41 guns reload in phases with the slap as its own phase, with the sequences and sound pieces of neoAssets research-reload-phases 3e6d86e. The seat carries two pieces: the tap rattle at the silent magazine seat and the tap clicks on the tap. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 47 ++++++++++++++++++++ game/neo/scripts/weapon_m41.txt | 56 +++++++++++++++++++++++- game/neo/scripts/weapon_m41l.txt | 56 +++++++++++++++++++++++- game/neo/scripts/weapon_m41s.txt | 56 +++++++++++++++++++++++- 4 files changed, 212 insertions(+), 3 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index ff6b9a30ee..010e13f07e 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -597,6 +597,53 @@ "wave" ")weapons/m41/m41_reload.wav" } +// weapon_m41 reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_m41.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/m41/m41_reload_drop.wav" +} + +"weapon_m41.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/m41/m41_reload_seat.wav" +} + +"weapon_m41.reload_seat2" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/m41/m41_reload_seat2.wav" +} + +"weapon_m41.reload_slap" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/m41/m41_reload_slap.wav" +} + +"weapon_m41.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/m41/m41_reload_bolt.wav" +} + //------------------- "weapon_m41.empty" diff --git a/game/neo/scripts/weapon_m41.txt b/game/neo/scripts/weapon_m41.txt index a7727d8038..fa23e805d9 100644 --- a/game/neo/scripts/weapon_m41.txt +++ b/game/neo/scripts/weapon_m41.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "29" "Penetration" "32.0" "CycleTime" "0.166666" // time between shots - "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + "bolt" { "credit" "3.367" "sound" "weapon_m41.reload_bolt" "at" "3.166" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + "bolt" { "credit" "3.300" "sound" "weapon_m41.reload_bolt" "at" "3.099" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + "bolt" { "credit" "1.700" "sound" "weapon_m41.reload_bolt" "at" "1.499" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "0.867" "sound" "weapon_m41.reload_bolt" "at" "0.666" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + "bolt" { "credit" "3.600" "sound" "weapon_m41.reload_bolt" "at" "3.399" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + } + } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_m41l.txt b/game/neo/scripts/weapon_m41l.txt index b98a05166c..194b8d4998 100644 --- a/game/neo/scripts/weapon_m41l.txt +++ b/game/neo/scripts/weapon_m41l.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "45" "Penetration" "32.0" "CycleTime" "0.5" // time between shots - "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + "bolt" { "credit" "3.367" "sound" "weapon_m41.reload_bolt" "at" "3.166" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + "bolt" { "credit" "3.300" "sound" "weapon_m41.reload_bolt" "at" "3.099" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + "bolt" { "credit" "1.700" "sound" "weapon_m41.reload_bolt" "at" "1.499" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "0.867" "sound" "weapon_m41.reload_bolt" "at" "0.666" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + "bolt" { "credit" "3.600" "sound" "weapon_m41.reload_bolt" "at" "3.399" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + } + } "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_m41s.txt b/game/neo/scripts/weapon_m41s.txt index 08ae66d288..b5c7883cb7 100644 --- a/game/neo/scripts/weapon_m41s.txt +++ b/game/neo/scripts/weapon_m41s.txt @@ -15,7 +15,61 @@ WeaponData "Damage" "26" "Penetration" "20.0" "CycleTime" "0.166666" // time between shots - "ReloadReadyBeforeEnd" "0.333" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + "bolt" { "credit" "3.367" "sound" "weapon_m41.reload_bolt" "at" "3.166" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_m41.reload_drop" "at" "0.624" } + "seat" { "credit" "1.300" "sound" "weapon_m41.reload_seat" "at" "1.302" "sound2" "weapon_m41.reload_seat2" "at2" "1.858" } + "slap" { "credit" "2.467" "sound" "weapon_m41.reload_slap" "at" "2.511" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + "bolt" { "credit" "3.300" "sound" "weapon_m41.reload_bolt" "at" "3.099" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.233" "sound" "weapon_m41.reload_seat" "at" "1.235" "sound2" "weapon_m41.reload_seat2" "at2" "1.791" } + "slap" { "credit" "2.400" "sound" "weapon_m41.reload_slap" "at" "2.444" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + "bolt" { "credit" "1.700" "sound" "weapon_m41.reload_bolt" "at" "1.499" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.867" "sound" "weapon_m41.reload_slap" "at" "0.911" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "0.867" "sound" "weapon_m41.reload_bolt" "at" "0.666" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + "bolt" { "credit" "3.600" "sound" "weapon_m41.reload_bolt" "at" "3.399" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } + "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } + } + } "TPMuzzleFlashScale" "0.5" From 35b2bf33f1a80c6e7219b86c53877713285d228a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:23:00 -0600 Subject: [PATCH 24/60] Play a phase's sound pieces after its credit too A phase's sound piece was only considered until the phase was credited, so pieces timed after their credit (the M41's slap click, its tap clicks) never played. Pieces now play on their own times; those of phases credited before the sequence started, and a partial reload's bolt, are marked done when the sequence starts. --- .../weapons/weapon_neobasecombatweapon.cpp | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index deaf4ec3e6..327caa64b4 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -623,18 +623,14 @@ void CNEOBaseCombatWeapon::RunReloadPhases() const ReloadPhaseTable_t &table = GetNEOWpnData().m_reloadTables[m_nReloadTable]; const float flElapsed = gpGlobals->curtime - m_flReloadStart; + + // Sound pieces play on their own times, before or after their phase's credit + // (the pieces of phases credited before this sequence started are marked done when it starts) for (int i = 0; i < table.iNumRows; ++i) { - const ReloadPhaseRow_t &row = table.rows[i]; - if (row.phase <= m_nReloadPhase || (row.phase == RELOAD_PHASE_BOLTED && !m_bReloadDry)) - { - continue; - } - - // a phase's sound piece may start before the phase is credited, such as the grab before the magazine drops for (int j = 0; j < RELOAD_SOUNDS_PER_ROW; ++j) { - const ReloadSoundPiece_t &piece = row.sounds[j]; + const ReloadSoundPiece_t &piece = table.rows[i].sounds[j]; const int iBit = 1 << (i * RELOAD_SOUNDS_PER_ROW + j); if (piece.szName[0] && !(m_nReloadSoundsDone & iBit) && flElapsed >= piece.flAt - RELOAD_SOUND_LEAD) { @@ -642,7 +638,15 @@ void CNEOBaseCombatWeapon::RunReloadPhases() EmitReloadSoundPiece(piece.szName); } } + } + for (int i = 0; i < table.iNumRows; ++i) + { + const ReloadPhaseRow_t &row = table.rows[i]; + if (row.phase <= m_nReloadPhase || (row.phase == RELOAD_PHASE_BOLTED && !m_bReloadDry)) + { + continue; + } if (flElapsed < row.flCredit) { break; @@ -682,7 +686,19 @@ void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence) { CBaseCombatCharacter *pOwner = GetOwner(); m_nReloadTable = iTable; - m_nReloadSoundsDone = 0; + + // the phases already credited, and a partial reload's bolt, play no sound + const ReloadPhaseTable_t &table = GetNEOWpnData().m_reloadTables[iTable]; + int iSoundsDone = 0; + for (int i = 0; i < table.iNumRows; ++i) + { + const ReloadPhase_t phase = table.rows[i].phase; + if (phase <= m_nReloadPhase || (phase == RELOAD_PHASE_BOLTED && !m_bReloadDry)) + { + iSoundsDone |= ((1 << RELOAD_SOUNDS_PER_ROW) - 1) << (i * RELOAD_SOUNDS_PER_ROW); + } + } + m_nReloadSoundsDone = iSoundsDone; if (!HasReloadSoundPieces()) { WeaponSound(RELOAD_NPC); From eda700083d59ddc9ddb80d2fbf3c598c81298b65 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:24:00 -0600 Subject: [PATCH 25/60] Give the Jitte and Jitte scoped reload phases and sound pieces Both Jitte guns reload in phases with the sequences and sound pieces of neoAssets research-reload-phases d58ceec. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 38 ++++++++++++++++++++++ game/neo/scripts/weapon_jitte.txt | 41 +++++++++++++++++++++++- game/neo/scripts/weapon_jittescoped.txt | 41 +++++++++++++++++++++++- 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 010e13f07e..2ba3c77152 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -514,6 +514,44 @@ "wave" ")weapons/jitte/jitte_reload.wav" } +// weapon_jitte reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_jitte.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/jitte/jitte_reload_drop.wav" +} + +"weapon_jitte.reload_drop2" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/jitte/jitte_reload_drop2.wav" +} + +"weapon_jitte.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/jitte/jitte_reload_seat.wav" +} + +"weapon_jitte.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/jitte/jitte_reload_bolt.wav" +} + //------------------- "weapon_jitte.empty" diff --git a/game/neo/scripts/weapon_jitte.txt b/game/neo/scripts/weapon_jitte.txt index 2ab8600053..3d5783aa25 100644 --- a/game/neo/scripts/weapon_jitte.txt +++ b/game/neo/scripts/weapon_jitte.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "15" "Penetration" "12.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyBeforeEnd" "0.667" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.400 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "1.367" "sound" "weapon_jitte.reload_drop" "at" "0.089" "sound2" "weapon_jitte.reload_drop2" "at2" "1.395" } + "seat" { "credit" "2.100" "sound" "weapon_jitte.reload_seat" "at" "1.733" } + "bolt" { "credit" "2.900" "sound" "weapon_jitte.reload_bolt" "at" "2.391" } + } + "reload_partial" + { + "drop" { "credit" "1.367" "sound" "weapon_jitte.reload_drop" "at" "0.089" "sound2" "weapon_jitte.reload_drop2" "at2" "1.395" } + "seat" { "credit" "2.100" "sound" "weapon_jitte.reload_seat" "at" "1.733" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.300" "sound" "weapon_jitte.reload_seat" "at" "0.933" } + "bolt" { "credit" "2.100" "sound" "weapon_jitte.reload_bolt" "at" "1.591" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.300" "sound" "weapon_jitte.reload_seat" "at" "0.933" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "0.867" "sound" "weapon_jitte.reload_bolt" "at" "0.358" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + "bolt" { "credit" "2.267" "sound" "weapon_jitte.reload_bolt" "at" "1.758" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "30" diff --git a/game/neo/scripts/weapon_jittescoped.txt b/game/neo/scripts/weapon_jittescoped.txt index 217946dee2..85fc098e50 100644 --- a/game/neo/scripts/weapon_jittescoped.txt +++ b/game/neo/scripts/weapon_jittescoped.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "15" "Penetration" "20.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyBeforeEnd" "0.667" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.400 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "1.367" "sound" "weapon_jitte.reload_drop" "at" "0.089" "sound2" "weapon_jitte.reload_drop2" "at2" "1.395" } + "seat" { "credit" "2.100" "sound" "weapon_jitte.reload_seat" "at" "1.733" } + "bolt" { "credit" "2.900" "sound" "weapon_jitte.reload_bolt" "at" "2.391" } + } + "reload_partial" + { + "drop" { "credit" "1.367" "sound" "weapon_jitte.reload_drop" "at" "0.089" "sound2" "weapon_jitte.reload_drop2" "at2" "1.395" } + "seat" { "credit" "2.100" "sound" "weapon_jitte.reload_seat" "at" "1.733" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.300" "sound" "weapon_jitte.reload_seat" "at" "0.933" } + "bolt" { "credit" "2.100" "sound" "weapon_jitte.reload_bolt" "at" "1.591" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.300" "sound" "weapon_jitte.reload_seat" "at" "0.933" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "0.867" "sound" "weapon_jitte.reload_bolt" "at" "0.358" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + "bolt" { "credit" "2.267" "sound" "weapon_jitte.reload_bolt" "at" "1.758" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "30" From f24237a8ac2058bd6facb79c1300a1c2c7b24564 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:25:00 -0600 Subject: [PATCH 26/60] Give the SRM and MPN guns reload phases and sound pieces The SRM, SRM-S, MPN and unsilenced MPN reload in phases with the sequences and sound pieces of neoAssets research-reload-phases f6e2f9e. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 58 ++++++++++++++++++++++ game/neo/scripts/weapon_mpn.txt | 41 ++++++++++++++- game/neo/scripts/weapon_mpn_unsilenced.txt | 41 ++++++++++++++- game/neo/scripts/weapon_srm.txt | 41 ++++++++++++++- game/neo/scripts/weapon_srm_s.txt | 41 ++++++++++++++- 5 files changed, 218 insertions(+), 4 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 2ba3c77152..ccc69e09cb 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -353,6 +353,35 @@ "wave" ")weapons/mpn45/mpn45_reload.wav" } +// weapon_mpn45 reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_mpn45.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mpn45/mpn45_reload_drop.wav" +} + +"weapon_mpn45.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mpn45/mpn45_reload_seat.wav" +} + +"weapon_mpn45.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/mpn45/mpn45_reload_bolt.wav" +} + //------------------- "weapon_mpn45.empty" @@ -429,6 +458,35 @@ "wave" ")weapons/srm/srm_reload.wav" } +// weapon_srm reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_srm.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srm/srm_reload_drop.wav" +} + +"weapon_srm.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srm/srm_reload_seat.wav" +} + +"weapon_srm.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srm/srm_reload_bolt.wav" +} + //------------------- "weapon_srm.empty" diff --git a/game/neo/scripts/weapon_mpn.txt b/game/neo/scripts/weapon_mpn.txt index e61bce9774..96d68fbc8e 100644 --- a/game/neo/scripts/weapon_mpn.txt +++ b/game/neo/scripts/weapon_mpn.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots - "ReloadReadyBeforeEnd" "0.500" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.400 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_mpn45.reload_drop" "at" "0.596" } + "seat" { "credit" "1.467" "sound" "weapon_mpn45.reload_seat" "at" "1.447" } + "bolt" { "credit" "2.800" "sound" "weapon_mpn45.reload_bolt" "at" "2.374" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_mpn45.reload_drop" "at" "0.596" } + "seat" { "credit" "1.467" "sound" "weapon_mpn45.reload_seat" "at" "1.447" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.600" "sound" "weapon_mpn45.reload_seat" "at" "1.580" } + "bolt" { "credit" "2.933" "sound" "weapon_mpn45.reload_bolt" "at" "2.507" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.600" "sound" "weapon_mpn45.reload_seat" "at" "1.580" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "1.067" "sound" "weapon_mpn45.reload_bolt" "at" "0.641" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } + "bolt" { "credit" "3.233" "sound" "weapon_mpn45.reload_bolt" "at" "2.807" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "30" diff --git a/game/neo/scripts/weapon_mpn_unsilenced.txt b/game/neo/scripts/weapon_mpn_unsilenced.txt index 4cc79578cb..9067e627c0 100644 --- a/game/neo/scripts/weapon_mpn_unsilenced.txt +++ b/game/neo/scripts/weapon_mpn_unsilenced.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "12" "Penetration" "12.0" "CycleTime" "0.075757" // time between shots - "ReloadReadyBeforeEnd" "0.500" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.400 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_mpn45.reload_drop" "at" "0.596" } + "seat" { "credit" "1.467" "sound" "weapon_mpn45.reload_seat" "at" "1.447" } + "bolt" { "credit" "2.800" "sound" "weapon_mpn45.reload_bolt" "at" "2.374" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_mpn45.reload_drop" "at" "0.596" } + "seat" { "credit" "1.467" "sound" "weapon_mpn45.reload_seat" "at" "1.447" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.600" "sound" "weapon_mpn45.reload_seat" "at" "1.580" } + "bolt" { "credit" "2.933" "sound" "weapon_mpn45.reload_bolt" "at" "2.507" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.600" "sound" "weapon_mpn45.reload_seat" "at" "1.580" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "1.067" "sound" "weapon_mpn45.reload_bolt" "at" "0.641" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } + "bolt" { "credit" "3.233" "sound" "weapon_mpn45.reload_bolt" "at" "2.807" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "30" diff --git a/game/neo/scripts/weapon_srm.txt b/game/neo/scripts/weapon_srm.txt index 2d2170338f..d6f1b764b1 100644 --- a/game/neo/scripts/weapon_srm.txt +++ b/game/neo/scripts/weapon_srm.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "12" "Penetration" "20.0" "CycleTime" "0.060606" // time between shots - "ReloadReadyBeforeEnd" "0.233" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.167 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_srm.reload_drop" "at" "0.496" } + "seat" { "credit" "1.733" "sound" "weapon_srm.reload_seat" "at" "1.634" } + "bolt" { "credit" "2.900" "sound" "weapon_srm.reload_bolt" "at" "2.537" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_srm.reload_drop" "at" "0.496" } + "seat" { "credit" "1.733" "sound" "weapon_srm.reload_seat" "at" "1.634" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.700" "sound" "weapon_srm.reload_seat" "at" "1.601" } + "bolt" { "credit" "2.867" "sound" "weapon_srm.reload_bolt" "at" "2.504" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.700" "sound" "weapon_srm.reload_seat" "at" "1.601" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "1.000" "sound" "weapon_srm.reload_bolt" "at" "0.637" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } + "bolt" { "credit" "2.767" "sound" "weapon_srm.reload_bolt" "at" "2.404" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "50" diff --git a/game/neo/scripts/weapon_srm_s.txt b/game/neo/scripts/weapon_srm_s.txt index e8fb6146d3..6d9c10753d 100644 --- a/game/neo/scripts/weapon_srm_s.txt +++ b/game/neo/scripts/weapon_srm_s.txt @@ -13,9 +13,48 @@ WeaponData "Damage" "12" "Penetration" "10.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyBeforeEnd" "0.233" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.167 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.633" "sound" "weapon_srm.reload_drop" "at" "0.496" } + "seat" { "credit" "1.733" "sound" "weapon_srm.reload_seat" "at" "1.634" } + "bolt" { "credit" "2.900" "sound" "weapon_srm.reload_bolt" "at" "2.537" } + } + "reload_partial" + { + "drop" { "credit" "0.633" "sound" "weapon_srm.reload_drop" "at" "0.496" } + "seat" { "credit" "1.733" "sound" "weapon_srm.reload_seat" "at" "1.634" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.700" "sound" "weapon_srm.reload_seat" "at" "1.601" } + "bolt" { "credit" "2.867" "sound" "weapon_srm.reload_bolt" "at" "2.504" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.700" "sound" "weapon_srm.reload_seat" "at" "1.601" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "1.000" "sound" "weapon_srm.reload_bolt" "at" "0.637" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } + "bolt" { "credit" "2.767" "sound" "weapon_srm.reload_bolt" "at" "2.404" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } + } + } + "TPMuzzleFlashScale" "0.5" "clip_size" "50" From 6f66303ba7c6ebd9ea3741bc6127bee625cff076 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:26:00 -0600 Subject: [PATCH 27/60] Give the AA13 and PZ reload phases and sound pieces The AA13 and PZ reload in phases with the sequences and sound pieces of neoAssets research-reload-phases b5aaa7a; the PZ's drum latch is its slap phase. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 76 ++++++++++++++++++++++++ game/neo/scripts/weapon_aa13.txt | 41 ++++++++++++- game/neo/scripts/weapon_pz.txt | 56 ++++++++++++++++- 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index ccc69e09cb..63b7288f65 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -203,6 +203,35 @@ "wave" ")weapons/aa13/aa13_reload.wav" } +// weapon_aa13 reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_aa13.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/aa13/aa13_reload_drop.wav" +} + +"weapon_aa13.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/aa13/aa13_reload_seat.wav" +} + +"weapon_aa13.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/aa13/aa13_reload_bolt.wav" +} + //------------------ "weapon_aa13.empty" @@ -1244,6 +1273,53 @@ "wave" ")weapons/pz/pz_reload.wav" } +// weapon_pz reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_pz.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/pz/pz_reload_drop.wav" +} + +"weapon_pz.reload_drop2" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/pz/pz_reload_drop2.wav" +} + +"weapon_pz.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/pz/pz_reload_seat.wav" +} + +"weapon_pz.reload_slap" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/pz/pz_reload_slap.wav" +} + +"weapon_pz.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/pz/pz_reload_bolt.wav" +} + //------------------- "weapon_pz.empty" diff --git a/game/neo/scripts/weapon_aa13.txt b/game/neo/scripts/weapon_aa13.txt index f8fe920d0a..afd7ede246 100644 --- a/game/neo/scripts/weapon_aa13.txt +++ b/game/neo/scripts/weapon_aa13.txt @@ -6,7 +6,6 @@ WeaponData "Damage" "25" "Bullets" "5" "CycleTime" "0.333" - "ReloadReadyBeforeEnd" "0.467" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.333 before the end, the alternative value // Weapon data is loaded by both the Game and Client DLLs. @@ -21,6 +20,46 @@ WeaponData "bucket" "0" "bucket_position" "3" + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "1.033" "sound" "weapon_aa13.reload_drop" "at" "0.374" } + "seat" { "credit" "3.367" "sound" "weapon_aa13.reload_seat" "at" "2.905" } + "bolt" { "credit" "4.333" "sound" "weapon_aa13.reload_bolt" "at" "3.764" } + } + "reload_partial" + { + "drop" { "credit" "1.033" "sound" "weapon_aa13.reload_drop" "at" "0.374" } + "seat" { "credit" "3.367" "sound" "weapon_aa13.reload_seat" "at" "2.905" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "2.600" "sound" "weapon_aa13.reload_seat" "at" "2.138" } + "bolt" { "credit" "3.567" "sound" "weapon_aa13.reload_bolt" "at" "2.997" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "2.600" "sound" "weapon_aa13.reload_seat" "at" "2.138" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "1.067" "sound" "weapon_aa13.reload_bolt" "at" "0.497" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "2.467" "sound" "weapon_aa13.reload_seat" "at" "2.005" } + "bolt" { "credit" "3.433" "sound" "weapon_aa13.reload_bolt" "at" "2.864" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "2.467" "sound" "weapon_aa13.reload_seat" "at" "2.005" } + } + } + "clip_size" "16" "default_clip" "48" "autoswitchfrom" "0" diff --git a/game/neo/scripts/weapon_pz.txt b/game/neo/scripts/weapon_pz.txt index 4b9f268184..ec77fc235d 100644 --- a/game/neo/scripts/weapon_pz.txt +++ b/game/neo/scripts/weapon_pz.txt @@ -15,9 +15,63 @@ WeaponData "Damage" "20" "Penetration" "28.0" "CycleTime" "0.090909" // time between shots - "ReloadReadyBeforeEnd" "0.633" // the reload is done at the last reload click, this long before its animation ends // the charging handle is home 0.400 before the end, the alternative value + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "1.500" "sound" "weapon_pz.reload_drop" "at" "0.335" "sound2" "weapon_pz.reload_drop2" "at2" "2.383" } + "seat" { "credit" "3.800" "sound" "weapon_pz.reload_seat" "at" "3.382" } + "slap" { "credit" "4.667" "sound" "weapon_pz.reload_slap" "at" "4.461" } + "bolt" { "credit" "5.367" "sound" "weapon_pz.reload_bolt" "at" "4.802" } + } + "reload_partial" + { + "drop" { "credit" "1.500" "sound" "weapon_pz.reload_drop" "at" "0.335" "sound2" "weapon_pz.reload_drop2" "at2" "2.383" } + "seat" { "credit" "3.800" "sound" "weapon_pz.reload_seat" "at" "3.382" } + "slap" { "credit" "4.667" "sound" "weapon_pz.reload_slap" "at" "4.461" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "2.433" "sound" "weapon_pz.reload_seat" "at" "2.015" } + "slap" { "credit" "3.300" "sound" "weapon_pz.reload_slap" "at" "3.094" } + "bolt" { "credit" "4.000" "sound" "weapon_pz.reload_bolt" "at" "3.435" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "2.433" "sound" "weapon_pz.reload_seat" "at" "2.015" } + "slap" { "credit" "3.300" "sound" "weapon_pz.reload_slap" "at" "3.094" } + } + "reload_from_seat_dry" + { + "slap" { "credit" "0.733" "sound" "weapon_pz.reload_slap" "at" "0.528" } + "bolt" { "credit" "1.433" "sound" "weapon_pz.reload_bolt" "at" "0.869" } + } + "reload_from_seat_partial" + { + "slap" { "credit" "0.733" "sound" "weapon_pz.reload_slap" "at" "0.528" } + } + "reload_from_slap_dry" + { + "bolt" { "credit" "1.000" "sound" "weapon_pz.reload_bolt" "at" "0.435" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "2.767" "sound" "weapon_pz.reload_seat" "at" "2.349" } + "slap" { "credit" "3.633" "sound" "weapon_pz.reload_slap" "at" "3.428" } + "bolt" { "credit" "4.333" "sound" "weapon_pz.reload_bolt" "at" "3.769" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "2.767" "sound" "weapon_pz.reload_seat" "at" "2.349" } + "slap" { "credit" "3.633" "sound" "weapon_pz.reload_slap" "at" "3.428" } + } + } + "TPMuzzleFlashScale" "0.3" "clip_size" "100" From fdaef0bc1b366b5152a4d5cb0353a05218fd966c Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:27:00 -0600 Subject: [PATCH 28/60] Give the SRS reload phases and sound pieces The SRS reloads in phases with the sequences and sound pieces of neoAssets research-reload-phases 346225e. Its per-shot bolting is unchanged; the reload's bolt phase is separate. Its ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 38 ++++++++++++++++++++++ game/neo/scripts/weapon_srs.txt | 41 +++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 63b7288f65..00ec408b8a 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -860,6 +860,44 @@ "wave" ")weapons/srs/srs_reload.wav" } +// weapon_srs reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_srs.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srs/srs_reload_drop.wav" +} + +"weapon_srs.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srs/srs_reload_seat.wav" +} + +"weapon_srs.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srs/srs_reload_bolt.wav" +} + +"weapon_srs.reload_bolt2" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/srs/srs_reload_bolt2.wav" +} + //------------------- "weapon_srs.charge" diff --git a/game/neo/scripts/weapon_srs.txt b/game/neo/scripts/weapon_srs.txt index ab9e1de751..8e2c48d37d 100644 --- a/game/neo/scripts/weapon_srs.txt +++ b/game/neo/scripts/weapon_srs.txt @@ -15,7 +15,46 @@ WeaponData "Damage" "100" "Penetration" "65.0" "CycleTime" "1.4" - "ReloadReadyBeforeEnd" "0.633" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "1.133" "sound" "weapon_srs.reload_drop" "at" "0.453" } + "seat" { "credit" "1.867" "sound" "weapon_srs.reload_seat" "at" "1.711" } + "bolt" { "credit" "3.067" "sound" "weapon_srs.reload_bolt" "at" "2.501" "sound2" "weapon_srs.reload_bolt2" "at2" "3.000" } + } + "reload_partial" + { + "drop" { "credit" "1.133" "sound" "weapon_srs.reload_drop" "at" "0.453" } + "seat" { "credit" "1.867" "sound" "weapon_srs.reload_seat" "at" "1.711" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.067" "sound" "weapon_srs.reload_seat" "at" "0.911" } + "bolt" { "credit" "2.267" "sound" "weapon_srs.reload_bolt" "at" "1.701" "sound2" "weapon_srs.reload_bolt2" "at2" "2.200" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.067" "sound" "weapon_srs.reload_seat" "at" "0.911" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "0.967" "sound" "weapon_srs.reload_bolt" "at" "0.401" "sound2" "weapon_srs.reload_bolt2" "at2" "0.900" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.567" "sound" "weapon_srs.reload_seat" "at" "1.411" } + "bolt" { "credit" "2.767" "sound" "weapon_srs.reload_bolt" "at" "2.201" "sound2" "weapon_srs.reload_bolt2" "at2" "2.700" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.567" "sound" "weapon_srs.reload_seat" "at" "1.411" } + } + } "TPMuzzleFlashScale" "0.25" From 12b1de8de43fe4c35e92ced238bb15c155e014ec Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:28:00 -0600 Subject: [PATCH 29/60] Time the Jitte and AA13 draw-resume for the parked arm Their draw-resume sequences grew by 3 frames (neoAssets research-reload-phases 3be9af7), moving the phases after the draw. --- game/neo/scripts/weapon_aa13.txt | 6 +++--- game/neo/scripts/weapon_jitte.txt | 6 +++--- game/neo/scripts/weapon_jittescoped.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/game/neo/scripts/weapon_aa13.txt b/game/neo/scripts/weapon_aa13.txt index afd7ede246..80966b0dbe 100644 --- a/game/neo/scripts/weapon_aa13.txt +++ b/game/neo/scripts/weapon_aa13.txt @@ -51,12 +51,12 @@ WeaponData } "draw_resume_drop_dry" { - "seat" { "credit" "2.467" "sound" "weapon_aa13.reload_seat" "at" "2.005" } - "bolt" { "credit" "3.433" "sound" "weapon_aa13.reload_bolt" "at" "2.864" } + "seat" { "credit" "2.567" "sound" "weapon_aa13.reload_seat" "at" "2.105" } + "bolt" { "credit" "3.533" "sound" "weapon_aa13.reload_bolt" "at" "2.964" } } "draw_resume_drop_partial" { - "seat" { "credit" "2.467" "sound" "weapon_aa13.reload_seat" "at" "2.005" } + "seat" { "credit" "2.567" "sound" "weapon_aa13.reload_seat" "at" "2.105" } } } diff --git a/game/neo/scripts/weapon_jitte.txt b/game/neo/scripts/weapon_jitte.txt index 3d5783aa25..6e7480e834 100644 --- a/game/neo/scripts/weapon_jitte.txt +++ b/game/neo/scripts/weapon_jitte.txt @@ -46,12 +46,12 @@ WeaponData } "draw_resume_drop_dry" { - "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } - "bolt" { "credit" "2.267" "sound" "weapon_jitte.reload_bolt" "at" "1.758" } + "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } + "bolt" { "credit" "2.367" "sound" "weapon_jitte.reload_bolt" "at" "1.858" } } "draw_resume_drop_partial" { - "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } } } diff --git a/game/neo/scripts/weapon_jittescoped.txt b/game/neo/scripts/weapon_jittescoped.txt index 85fc098e50..1bc203eb39 100644 --- a/game/neo/scripts/weapon_jittescoped.txt +++ b/game/neo/scripts/weapon_jittescoped.txt @@ -46,12 +46,12 @@ WeaponData } "draw_resume_drop_dry" { - "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } - "bolt" { "credit" "2.267" "sound" "weapon_jitte.reload_bolt" "at" "1.758" } + "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } + "bolt" { "credit" "2.367" "sound" "weapon_jitte.reload_bolt" "at" "1.858" } } "draw_resume_drop_partial" { - "seat" { "credit" "1.467" "sound" "weapon_jitte.reload_seat" "at" "1.100" } + "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } } } From 5f64639b5f8635bd6b50265e4b431a7b86456e78 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:29:00 -0600 Subject: [PATCH 30/60] Reload an empty pistol with its slide release A dry reload plays reloadempty where the model has it: the Milso ships one (its slide goes forward at the end), the Tachi's is built. Its slide release is the dry reload's bolt phase, so a switch after the seat resumes with the release alone. Guns without reloadempty play the reload as before. --- .../neo/weapons/weapon_neobasecombatweapon.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 327caa64b4..e9c62092b9 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -499,6 +499,8 @@ static constexpr float RELOAD_SOUND_LEAD = 0.02f; // The shipped reload sequence, the dry reload with every phase static constexpr const char *VM_SEQ_RELOAD = "reload"; +// A pistol's dry reload (the Milso ships it, the Tachi's is built) +static constexpr const char *VM_SEQ_RELOAD_EMPTY = "reloadempty"; // The reload sequence for the phase credited last, dry or partial; nullptr: nothing left to play // (sequences other than the shipped reload carry no activity, so they are picked by name) @@ -507,7 +509,8 @@ static const char *ReloadSequenceName(int iPhaseDone, bool bDry, bool bFromDraw) switch (iPhaseDone) { case RELOAD_PHASE_NONE: - return bDry ? VM_SEQ_RELOAD : "reload_partial"; + // a pistol's dry reload ends with the slide release; magazine guns have no reloadempty and play the reload + return bDry ? VM_SEQ_RELOAD_EMPTY : "reload_partial"; case RELOAD_PHASE_DROPPED: // after a switch the magazine is out: the draw itself brings the fresh one up out of view if (bFromDraw) @@ -538,8 +541,13 @@ bool CNEOBaseCombatWeapon::IsReloadDry() const // The phase that completes a reload: the bolt on a dry one, the last magazine phase otherwise ReloadPhase_t CNEOBaseCombatWeapon::LastReloadPhase(bool bDry) const { + // the full reload of that kind lists every phase it needs (a pistol's slide release is only in reloadempty) const CNEOWeaponInfo &info = GetNEOWpnData(); - const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); + int iTable = bDry ? info.FindReloadTable(VM_SEQ_RELOAD_EMPTY) : -1; + if (iTable < 0) + { + iTable = info.FindReloadTable(VM_SEQ_RELOAD); + } if (iTable < 0) { return RELOAD_PHASE_NONE; From c12716e41af0348ad7fbd3b81342c53e9b23587d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:30:00 -0600 Subject: [PATCH 31/60] Give the Milso and Tachi reload phases and sound pieces The Milso and Tachi reload in phases with the sequences and sound pieces of neoAssets research-reload-phases aaef0b6: a partial reload keeps the chambered round, a dry one plays reloadempty and its slide release. Their ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 40 +++++++++++++++++++++ game/neo/scripts/weapon_milso.txt | 41 +++++++++++++++++++++- game/neo/scripts/weapon_tachi.txt | 44 +++++++++++++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 00ec408b8a..8636c953e1 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -22,6 +22,26 @@ "wave" ")weapons/milso/milso_reload.wav" } +// weapon_milso reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_milso.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/milso/milso_reload_drop.wav" +} + +"weapon_milso.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/milso/milso_reload_seat.wav" +} + //------------------ "weapon_milso.empty" @@ -78,6 +98,26 @@ "wave" ")weapons/tachi/tachi_reload.wav" } +// weapon_tachi reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_tachi.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/tachi/tachi_reload_drop.wav" +} + +"weapon_tachi.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/tachi/tachi_reload_seat.wav" +} + //------------------ "weapon_tachi.empty" diff --git a/game/neo/scripts/weapon_milso.txt b/game/neo/scripts/weapon_milso.txt index 5e220ac1f1..dcc23e9f7a 100644 --- a/game/neo/scripts/weapon_milso.txt +++ b/game/neo/scripts/weapon_milso.txt @@ -13,7 +13,46 @@ WeaponData "Damage" "17" "Penetration" "12.0" "CycleTime" "0.212121" - "ReloadReadyBeforeEnd" "0.467" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.467" "sound" "weapon_milso.reload_drop" "at" "0.292" } + "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } + } + "reloadempty" + { + "drop" { "credit" "0.467" "sound" "weapon_milso.reload_drop" "at" "0.292" } + "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } + "bolt" { "credit" "2.067" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } + "bolt" { "credit" "2.067" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "0.267" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.500" "sound" "weapon_milso.reload_seat" "at" "1.576" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.500" "sound" "weapon_milso.reload_seat" "at" "1.576" } + "bolt" { "credit" "2.400" } + } + } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_tachi.txt b/game/neo/scripts/weapon_tachi.txt index 89d0bf13c1..c82d05c617 100644 --- a/game/neo/scripts/weapon_tachi.txt +++ b/game/neo/scripts/weapon_tachi.txt @@ -14,6 +14,49 @@ WeaponData "bucket" "1" "bucket_position" "0" + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "1" + "reload" + { + "drop" { "credit" "0.467" "sound" "weapon_tachi.reload_drop" "at" "0.266" } + "seat" { "credit" "1.167" "sound" "weapon_tachi.reload_seat" "at" "1.413" } + "bolt" { "credit" "2.067" } + } + "reloadempty" + { + "drop" { "credit" "0.467" "sound" "weapon_tachi.reload_drop" "at" "0.266" } + "seat" { "credit" "1.167" "sound" "weapon_tachi.reload_seat" "at" "1.413" } + "bolt" { "credit" "2.067" } + } + "reload_from_drop_partial" + { + "seat" { "credit" "1.133" "sound" "weapon_tachi.reload_seat" "at" "1.380" } + "bolt" { "credit" "2.033" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.133" "sound" "weapon_tachi.reload_seat" "at" "1.380" } + "bolt" { "credit" "2.033" } + } + "reload_from_seat_dry" + { + "bolt" { "credit" "0.267" } + } + "draw_resume_drop_partial" + { + "seat" { "credit" "1.633" "sound" "weapon_tachi.reload_seat" "at" "1.880" } + "bolt" { "credit" "2.533" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.633" "sound" "weapon_tachi.reload_seat" "at" "1.880" } + "bolt" { "credit" "2.533" } + } + } + "clip_size" "15" "default_clip" "45" "autoswitchfrom" "0" @@ -32,7 +75,6 @@ WeaponData "Damage" "17" "Penetration" "19.0" "CycleTime" "0.121212" - "ReloadReadyBeforeEnd" "0.400" // the reload is done at the last reload click, this long before its animation ends "ViewModelOffset" { From 402163a06614ae612097ea06c63e7d7835a2d7e0 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:31:00 -0600 Subject: [PATCH 32/60] Give the Kyla reload phases and sound pieces The Kyla has no chamber (ChamberedRound 0): every reload loads six, and a reload interrupted after the cylinder opened resumes from the draw with the cylinder open (neoAssets research-reload-phases bb53de2). Its ReloadReadyBeforeEnd goes. --- game/neo/scripts/game_sounds_weapons.txt | 29 ++++++++++++++++++++++++ game/neo/scripts/weapon_kyla.txt | 21 ++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 8636c953e1..5b2885f639 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -184,6 +184,35 @@ "wave" ")weapons/kyla/kyla_reload.wav" } +// weapon_kyla reload in per-phase pieces (research): each file starts 0.02 s before its first transient + +"weapon_kyla.reload_drop" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/kyla/kyla_reload_drop.wav" +} + +"weapon_kyla.reload_seat" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/kyla/kyla_reload_seat.wav" +} + +"weapon_kyla.reload_seat2" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/kyla/kyla_reload_seat2.wav" +} + //------------------ "weapon_kyla.empty" diff --git a/game/neo/scripts/weapon_kyla.txt b/game/neo/scripts/weapon_kyla.txt index 8d56a57f53..d7f972ae97 100644 --- a/game/neo/scripts/weapon_kyla.txt +++ b/game/neo/scripts/weapon_kyla.txt @@ -13,7 +13,26 @@ WeaponData "Damage" "45" //"11" "Penetration" "10.0" "CycleTime" "0.363636" // time between shoots - "ReloadReadyBeforeEnd" "0.600" // the reload is done at the last reload click, this long before its animation ends + + // Reload phases, generated from segments.csv by segments_build.py (ntre proposals/0057): + // credit = when the phase counts, at = when its sound piece's first transient plays, both from the sequence start + "ReloadPhases" + { + "ChamberedRound" "0" + "reload" + { + "drop" { "credit" "0.800" "sound" "weapon_kyla.reload_drop" "at" "0.232" } + "seat" { "credit" "1.333" "sound" "weapon_kyla.reload_seat" "at" "1.216" "sound2" "weapon_kyla.reload_seat2" "at2" "1.673" } + } + "reload_from_drop_dry" + { + "seat" { "credit" "1.733" "sound" "weapon_kyla.reload_seat" "at" "1.616" "sound2" "weapon_kyla.reload_seat2" "at2" "2.073" } + } + "draw_resume_drop_dry" + { + "seat" { "credit" "1.633" "sound" "weapon_kyla.reload_seat" "at" "1.516" "sound2" "weapon_kyla.reload_seat2" "at2" "1.973" } + } + } "TPMuzzleFlashScale" "0.6" From 38ce827bb2007af824d92f2cea8273ee3103a412 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:32:00 -0600 Subject: [PATCH 33/60] Sound the Milso and Tachi slide release with a placeholder The empty pistols' slide release plays a placeholder piece (neoAssets research-reload-phases 3eb7514) as the slide goes home. --- game/neo/scripts/game_sounds_weapons.txt | 22 ++++++++++++++++++++++ game/neo/scripts/weapon_milso.txt | 8 ++++---- game/neo/scripts/weapon_tachi.txt | 14 +++++++------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/game/neo/scripts/game_sounds_weapons.txt b/game/neo/scripts/game_sounds_weapons.txt index 5b2885f639..556a743384 100644 --- a/game/neo/scripts/game_sounds_weapons.txt +++ b/game/neo/scripts/game_sounds_weapons.txt @@ -24,6 +24,17 @@ // weapon_milso reload in per-phase pieces (research): each file starts 0.02 s before its first transient +"weapon_milso.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/milso/milso_reload_bolt.wav" +} + +// weapon_milso reload in per-phase pieces (research): each file starts 0.02 s before its first transient + "weapon_milso.reload_drop" { "channel" "CHAN_ITEM" @@ -100,6 +111,17 @@ // weapon_tachi reload in per-phase pieces (research): each file starts 0.02 s before its first transient +"weapon_tachi.reload_bolt" +{ + "channel" "CHAN_ITEM" + "soundlevel" "SNDLVL_NORM" + "volume" "1.0" + "pitch" "PITCH_NORM" + "wave" ")weapons/tachi/tachi_reload_bolt.wav" +} + +// weapon_tachi reload in per-phase pieces (research): each file starts 0.02 s before its first transient + "weapon_tachi.reload_drop" { "channel" "CHAN_ITEM" diff --git a/game/neo/scripts/weapon_milso.txt b/game/neo/scripts/weapon_milso.txt index dcc23e9f7a..50bd4e2d52 100644 --- a/game/neo/scripts/weapon_milso.txt +++ b/game/neo/scripts/weapon_milso.txt @@ -28,7 +28,7 @@ WeaponData { "drop" { "credit" "0.467" "sound" "weapon_milso.reload_drop" "at" "0.292" } "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } - "bolt" { "credit" "2.067" } + "bolt" { "credit" "2.067" "sound" "weapon_milso.reload_bolt" "at" "2.060" } } "reload_from_drop_partial" { @@ -37,11 +37,11 @@ WeaponData "reload_from_drop_dry" { "seat" { "credit" "1.167" "sound" "weapon_milso.reload_seat" "at" "1.243" } - "bolt" { "credit" "2.067" } + "bolt" { "credit" "2.067" "sound" "weapon_milso.reload_bolt" "at" "2.060" } } "reload_from_seat_dry" { - "bolt" { "credit" "0.267" } + "bolt" { "credit" "0.267" "sound" "weapon_milso.reload_bolt" "at" "0.260" } } "draw_resume_drop_partial" { @@ -50,7 +50,7 @@ WeaponData "draw_resume_drop_dry" { "seat" { "credit" "1.500" "sound" "weapon_milso.reload_seat" "at" "1.576" } - "bolt" { "credit" "2.400" } + "bolt" { "credit" "2.400" "sound" "weapon_milso.reload_bolt" "at" "2.393" } } } diff --git a/game/neo/scripts/weapon_tachi.txt b/game/neo/scripts/weapon_tachi.txt index c82d05c617..d50db1b720 100644 --- a/game/neo/scripts/weapon_tachi.txt +++ b/game/neo/scripts/weapon_tachi.txt @@ -23,37 +23,37 @@ WeaponData { "drop" { "credit" "0.467" "sound" "weapon_tachi.reload_drop" "at" "0.266" } "seat" { "credit" "1.167" "sound" "weapon_tachi.reload_seat" "at" "1.413" } - "bolt" { "credit" "2.067" } + "bolt" { "credit" "2.067" "sound" "weapon_tachi.reload_bolt" "at" "2.060" } } "reloadempty" { "drop" { "credit" "0.467" "sound" "weapon_tachi.reload_drop" "at" "0.266" } "seat" { "credit" "1.167" "sound" "weapon_tachi.reload_seat" "at" "1.413" } - "bolt" { "credit" "2.067" } + "bolt" { "credit" "2.067" "sound" "weapon_tachi.reload_bolt" "at" "2.060" } } "reload_from_drop_partial" { "seat" { "credit" "1.133" "sound" "weapon_tachi.reload_seat" "at" "1.380" } - "bolt" { "credit" "2.033" } + "bolt" { "credit" "2.033" "sound" "weapon_tachi.reload_bolt" "at" "2.027" } } "reload_from_drop_dry" { "seat" { "credit" "1.133" "sound" "weapon_tachi.reload_seat" "at" "1.380" } - "bolt" { "credit" "2.033" } + "bolt" { "credit" "2.033" "sound" "weapon_tachi.reload_bolt" "at" "2.027" } } "reload_from_seat_dry" { - "bolt" { "credit" "0.267" } + "bolt" { "credit" "0.267" "sound" "weapon_tachi.reload_bolt" "at" "0.260" } } "draw_resume_drop_partial" { "seat" { "credit" "1.633" "sound" "weapon_tachi.reload_seat" "at" "1.880" } - "bolt" { "credit" "2.533" } + "bolt" { "credit" "2.533" "sound" "weapon_tachi.reload_bolt" "at" "2.527" } } "draw_resume_drop_dry" { "seat" { "credit" "1.633" "sound" "weapon_tachi.reload_seat" "at" "1.880" } - "bolt" { "credit" "2.533" } + "bolt" { "credit" "2.533" "sound" "weapon_tachi.reload_bolt" "at" "2.527" } } } From bde9bf4f1acfdfc9052a3ec2fb1f5f0b599cc1ad Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:33:00 -0600 Subject: [PATCH 34/60] Supa7: tube, chamber and pump as state The Supa7 keeps its tube as a stack of shells and slugs in load order, a chamber (empty, shell, slug or a fired hull) and a pending rack. A rack ejects what is in the chamber, a live round included, and feeds the tube's top item; a shot leaves a hull that the fire animation's pump ejects. The tube takes what the gun's capacity leaves beside a live chambered round, so a full gun still holds seven. A slug is loaded on top of the tube and racked up when the reload ends, which ejects a chambered shell. m_iClip1 and the slug flag follow the state for the HUD, bots and the base code. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 137 +++++++++++++++---- src/game/shared/neo/weapons/weapon_supa7.h | 25 ++++ 2 files changed, 137 insertions(+), 25 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index d3ca85d258..ec9686dff6 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -12,12 +12,20 @@ BEGIN_NETWORK_TABLE(CWeaponSupa7, DT_WeaponSupa7) RecvPropBool(RECVINFO(m_bWeaponRaised)), RecvPropBool(RECVINFO(m_bShellInChamber)), RecvPropFloat(RECVINFO(m_flNextReload)), + RecvPropInt(RECVINFO(m_nTubeCount)), + RecvPropInt(RECVINFO(m_nTubeSlugs)), + RecvPropInt(RECVINFO(m_nChamber)), + RecvPropBool(RECVINFO(m_bRackPending)), #else SendPropBool(SENDINFO(m_bSlugDelayed)), SendPropBool(SENDINFO(m_bSlugLoaded)), SendPropBool(SENDINFO(m_bWeaponRaised)), SendPropBool(SENDINFO(m_bShellInChamber)), SendPropFloat(SENDINFO(m_flNextReload)), + SendPropInt(SENDINFO(m_nTubeCount), 4, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nTubeSlugs), 8, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nChamber), 2, SPROP_UNSIGNED), + SendPropBool(SENDINFO(m_bRackPending)), #endif END_NETWORK_TABLE() @@ -28,6 +36,10 @@ BEGIN_PREDICTION_DATA(CWeaponSupa7) DEFINE_PRED_FIELD(m_bWeaponRaised, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_bShellInChamber, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flNextReload, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nTubeCount, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nTubeSlugs, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_nChamber, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_bRackPending, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -42,6 +54,10 @@ BEGIN_DATADESC(CWeaponSupa7) DEFINE_FIELD(m_bWeaponRaised, FIELD_BOOLEAN), DEFINE_FIELD(m_bShellInChamber, FIELD_BOOLEAN), DEFINE_FIELD(m_flNextReload, FIELD_FLOAT), + DEFINE_FIELD(m_nTubeCount, FIELD_INTEGER), + DEFINE_FIELD(m_nTubeSlugs, FIELD_INTEGER), + DEFINE_FIELD(m_nChamber, FIELD_INTEGER), + DEFINE_FIELD(m_bRackPending, FIELD_BOOLEAN), END_DATADESC() #endif @@ -73,6 +89,10 @@ CWeaponSupa7::CWeaponSupa7(void) m_bWeaponRaised = false; m_bShellInChamber = true; m_flNextReload = 0.0f; + m_nTubeCount = 0; + m_nTubeSlugs = 0; + m_nChamber = SUPA7_CHAMBER_EMPTY; + m_bRackPending = false; m_fMinRange1 = 0.0; m_fMaxRange1 = 500; @@ -113,7 +133,8 @@ bool CWeaponSupa7::StartReload(void) if (m_iPrimaryAmmoCount <= 0) return false; - if (m_iClip1 >= GetMaxClip1()) + SyncTube(); + if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) return false; SendWeaponAnim(ACT_SHOTGUN_RELOAD_START); @@ -144,7 +165,8 @@ bool CWeaponSupa7::StartReloadSlug(void) return false; } - if (m_iClip1 >= GetMaxClip1()) + SyncTube(); + if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) return false; if (!m_bInReload) @@ -182,7 +204,7 @@ bool CWeaponSupa7::Reload(void) if (m_iPrimaryAmmoCount <= 0) return false; - if (m_iClip1 >= GetMaxClip1()) + if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) return false; // Play reload on different channel as otherwise steals channel away from fire sound @@ -219,11 +241,12 @@ bool CWeaponSupa7::ReloadSlug(void) if (m_iSecondaryAmmoCount <= 0) return false; - if (m_iClip1 >= GetMaxClip1()) + if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) return false; + // the slug goes on top of the tube and is racked into the chamber when the reload ends FillClipSlug(); - m_bShellInChamber = false; + m_bRackPending = true; // Play reload on different channel as otherwise steals channel away from fire sound WeaponSound(SPECIAL1); @@ -249,12 +272,13 @@ void CWeaponSupa7::FinishReload(void) m_bInReload = false; - // Finish reload animation - if (!m_bShellInChamber) + // Finish reload animation: rack when the chamber holds no live round, or to bring a loaded slug up + SyncTube(); + if (!IsChamberLive() || m_bRackPending) { SendWeaponAnim(ACT_SHOTGUN_PUMP); WeaponSound(SPECIAL2); - m_bShellInChamber = true; + Rack(); } else { @@ -273,14 +297,12 @@ void CWeaponSupa7::FillClip(void) if (pOwner == NULL) return; - // Add them to the clip - if (m_iPrimaryAmmoCount > 0) + // Push a shell into the tube + SyncTube(); + if (m_iPrimaryAmmoCount > 0 && m_nTubeCount < TubeCapacity()) { - if (Clip1() < GetMaxClip1()) - { - m_iClip1++; - m_iPrimaryAmmoCount -= 1; - } + TubePush(false); + m_iPrimaryAmmoCount -= 1; } } @@ -293,11 +315,11 @@ void CWeaponSupa7::FillClipSlug(void) return; // Add them to the clip - if (m_iSecondaryAmmoCount > 0 && !m_bSlugLoaded) + SyncTube(); + if (m_iSecondaryAmmoCount > 0 && !m_bSlugLoaded && m_nTubeCount < TubeCapacity()) { - m_iClip1++; + TubePush(true); m_iSecondaryAmmoCount -= 1; - m_bSlugLoaded = true; m_bSlugDelayed = false; } } @@ -323,6 +345,8 @@ void CWeaponSupa7::PrimaryAttack(void) Vector vecSrc = pPlayer->Weapon_ShootPosition(); Vector vecAiming = pPlayer->GetAutoaimVector(AUTOAIM_10DEGREES); + SyncTube(); + // Change the firing characteristics and sound if a slug was loaded if (m_bSlugLoaded) { @@ -352,15 +376,13 @@ void CWeaponSupa7::PrimaryAttack(void) pPlayer->DoMuzzleFlash(); SendWeaponAnim(m_bSlugLoaded ? ACT_VM_PRIMARYATTACK : ACT_VM_SECONDARYATTACK); - m_bSlugLoaded = false; // Don't fire again until fire animation has completed ProposeNextAttack(gpGlobals->curtime + GetFireRate()); - m_iClip1 -= 1; - if (m_iClip1 == 0) - { - m_bShellInChamber = false; - } + + // the shot leaves a hull, and the fire animation's pump ejects it and feeds the tube's top item + m_nChamber = SUPA7_CHAMBER_HULL; + Rack(); // player "shoot" animation pPlayer->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); @@ -413,6 +435,7 @@ void CWeaponSupa7::ItemPostFrame(void) return; } ProcessAnimationEvents(); + SyncTube(); if (m_bInReload) { @@ -436,7 +459,8 @@ void CWeaponSupa7::ItemPostFrame(void) else { // If out of ammo end reload - if ((m_iPrimaryAmmoCount <= 0 || m_bSlugLoaded || m_iClip1 >= GetMaxClip1())) + // (a slug loaded to be fired next is racked up before anything goes on top of it) + if ((m_iPrimaryAmmoCount <= 0 || m_bSlugLoaded || m_bRackPending || m_iClip1 >= GetMaxClip1())) { FinishReload(); } @@ -525,6 +549,69 @@ void CWeaponSupa7::ItemPostFrame(void) } } +// The tube holds what the gun's capacity leaves beside a live round in the chamber +int CWeaponSupa7::TubeCapacity() const +{ + return GetMaxClip1() - (IsChamberLive() ? 1 : 0); +} + +// Rebuild the tube from the clip count when something set the count directly (a new or picked-up gun, a harness give): +// a live shell (or the loaded slug) in the chamber, shells in the tube +void CWeaponSupa7::SyncTube() +{ + const int iLive = m_nTubeCount + (IsChamberLive() ? 1 : 0); + if (iLive == m_iClip1) + { + return; + } + + const int iClip = Max(0, m_iClip1.Get()); + m_nChamber = (iClip > 0) ? (m_bSlugLoaded ? SUPA7_CHAMBER_SLUG : SUPA7_CHAMBER_SHELL) : SUPA7_CHAMBER_EMPTY; + m_nTubeCount = Max(0, iClip - 1); + m_nTubeSlugs = 0; + UpdateClipFromTube(); +} + +void CWeaponSupa7::TubePush(bool bSlug) +{ + if (bSlug) + { + m_nTubeSlugs |= (1 << m_nTubeCount); + } + else + { + m_nTubeSlugs &= ~(1 << m_nTubeCount); + } + m_nTubeCount += 1; + UpdateClipFromTube(); +} + +// Pump: the chamber's hull or live round is ejected (a live round is lost), and the tube's top item is fed +void CWeaponSupa7::Rack() +{ + m_bRackPending = false; + if (m_nTubeCount <= 0) + { + m_nChamber = SUPA7_CHAMBER_EMPTY; + UpdateClipFromTube(); + return; + } + + m_nTubeCount -= 1; + const bool bSlug = IsTubeSlug(m_nTubeCount); + m_nTubeSlugs &= ~(1 << m_nTubeCount); + m_nChamber = bSlug ? SUPA7_CHAMBER_SLUG : SUPA7_CHAMBER_SHELL; + UpdateClipFromTube(); +} + +// The counts the rest of the game reads: live rounds in the gun, the slug flag for the chambered round +void CWeaponSupa7::UpdateClipFromTube() +{ + m_iClip1 = m_nTubeCount + (IsChamberLive() ? 1 : 0); + m_bSlugLoaded = (m_nChamber == SUPA7_CHAMBER_SLUG); + m_bShellInChamber = IsChamberLive(); +} + const WeaponSpreadInfo_t &CWeaponSupa7::GetSpreadInfo() { Assert(m_weaponHandling.weaponID & GetNeoWepBits()); diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index f585f15803..ee2659a045 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -64,6 +64,18 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon bool CanBePickedUpByClass(int classId) OVERRIDE; + // What sits in the chamber: a live shell or slug, the hull of a fired one, or nothing + enum Supa7Chamber_t + { + SUPA7_CHAMBER_EMPTY = 0, + SUPA7_CHAMBER_SHELL, + SUPA7_CHAMBER_SLUG, + SUPA7_CHAMBER_HULL, + }; + int TubeCount() const { return m_nTubeCount; } + bool IsTubeSlug(int iPosition) const { return (m_nTubeSlugs & (1 << iPosition)) != 0; } + Supa7Chamber_t Chamber() const { return static_cast(m_nChamber.Get()); } + protected: virtual float GetFastestDryRefireTime() const OVERRIDE { return 0.2f; } @@ -77,6 +89,13 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon } } + bool IsChamberLive() const { return m_nChamber == SUPA7_CHAMBER_SHELL || m_nChamber == SUPA7_CHAMBER_SLUG; } + int TubeCapacity() const; + void SyncTube(); + void TubePush(bool bSlug); + void Rack(); + void UpdateClipFromTube(); + void SetShotgunShellVisible(bool is_visible) { const int bodygroup_shell = 1; @@ -91,6 +110,12 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon CNetworkVar(bool, m_bShellInChamber); // Slug currently loaded in chamber CNetworkVar(float, m_flNextReload); // Time to load the next shell + // The tube is a stack, the last item loaded is the first fed; m_iClip1 stays the live rounds in the gun + CNetworkVar(int, m_nTubeCount); // items in the tube + CNetworkVar(int, m_nTubeSlugs); // bit i: the item at tube position i (0 = loaded first) is a slug + CNetworkVar(int, m_nChamber); // Supa7Chamber_t + CNetworkVar(bool, m_bRackPending); // a slug was loaded to be fired next: the reload ends with a rack + private: CWeaponSupa7(const CWeaponSupa7 &other); }; From cee96d389a95bac150d4c31732d751c12cf9c566 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:34:00 -0600 Subject: [PATCH 35/60] Supa7: play the shell insert at the shell pace The insert animation (0.733 s) is longer than the 0.5 s between shells, so every shell after the first cut it at 68 % and restarted it with the hand snapping 40 units back to the pouch (classic does the same). Play it at 1.47x so each insert ends as the next one starts. The loading pace is unchanged. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 25 +++++++++++++++++++- src/game/shared/neo/weapons/weapon_supa7.h | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index ec9686dff6..ab3756567a 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -81,6 +81,9 @@ IMPLEMENT_ACTTABLE(CWeaponSupa7); #endif #endif +// One shell or slug goes into the tube every this many seconds +static constexpr float TIME_BETWEEN_SHELLS_LOADED = 0.5f; + CWeaponSupa7::CWeaponSupa7(void) { m_bReloadsSingly = true; @@ -212,8 +215,9 @@ bool CWeaponSupa7::Reload(void) SendWeaponAnim(ACT_VM_RELOAD); pOwner->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); + PlayInsertAtShellPace(); + pOwner->m_flNextAttack = gpGlobals->curtime; - constexpr float TIME_BETWEEN_SHELLS_LOADED = 0.5f; // ProposeNextAttack(gpGlobals->curtime + TIME_BETWEEN_SHELLS_LOADED); m_flNextReload = gpGlobals->curtime + TIME_BETWEEN_SHELLS_LOADED; @@ -252,6 +256,7 @@ bool CWeaponSupa7::ReloadSlug(void) WeaponSound(SPECIAL1); SendWeaponAnim(ACT_VM_RELOAD); pOwner->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); + PlayInsertAtShellPace(); pOwner->m_flNextAttack = gpGlobals->curtime; // ProposeNextAttack(gpGlobals->curtime + SequenceDuration()); @@ -549,6 +554,24 @@ void CWeaponSupa7::ItemPostFrame(void) } } +// The insert animation is longer than the interval between shells, so it was cut and restarted +// with the hand snapping back to the pouch; play it fast enough to finish each shell within the interval +void CWeaponSupa7::PlayInsertAtShellPace() +{ + CBasePlayer *pOwner = ToBasePlayer(GetOwner()); + CBaseViewModel *pVM = pOwner ? pOwner->GetViewModel(m_nViewModelIndex) : nullptr; + if (!pVM) + { + return; + } + + const float flDuration = pVM->SequenceDuration(); + if (flDuration > TIME_BETWEEN_SHELLS_LOADED) + { + pVM->SetPlaybackRate(flDuration / TIME_BETWEEN_SHELLS_LOADED); + } +} + // The tube holds what the gun's capacity leaves beside a live round in the chamber int CWeaponSupa7::TubeCapacity() const { diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index ee2659a045..a3f663c638 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -95,6 +95,7 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon void TubePush(bool bSlug); void Rack(); void UpdateClipFromTube(); + void PlayInsertAtShellPace(); void SetShotgunShellVisible(bool is_visible) { From 93233162656495bbe910dfe3bd32dcb1cc3d83d2 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:35:00 -0600 Subject: [PATCH 36/60] Supa7: rack the pump once on a reload press with the tube full Pressing reload with the tube full plays a rack: the support hand reaches for the pump and racks it once (the rack sequence where the model has it), ejecting the chambered round and feeding the tube's top item at the pump's back stroke, with the pump sound timed to it. With nothing in the tube the press does nothing: a rack would only throw the last round away. A switch before the back stroke cancels the rack. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 76 +++++++++++++++++++- src/game/shared/neo/weapons/weapon_supa7.h | 5 ++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index ab3756567a..2066adceea 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -16,6 +16,8 @@ BEGIN_NETWORK_TABLE(CWeaponSupa7, DT_WeaponSupa7) RecvPropInt(RECVINFO(m_nTubeSlugs)), RecvPropInt(RECVINFO(m_nChamber)), RecvPropBool(RECVINFO(m_bRackPending)), + RecvPropTime(RECVINFO(m_flRackTime)), + RecvPropTime(RECVINFO(m_flRackSoundTime)), #else SendPropBool(SENDINFO(m_bSlugDelayed)), SendPropBool(SENDINFO(m_bSlugLoaded)), @@ -26,6 +28,8 @@ BEGIN_NETWORK_TABLE(CWeaponSupa7, DT_WeaponSupa7) SendPropInt(SENDINFO(m_nTubeSlugs), 8, SPROP_UNSIGNED), SendPropInt(SENDINFO(m_nChamber), 2, SPROP_UNSIGNED), SendPropBool(SENDINFO(m_bRackPending)), + SendPropTime(SENDINFO(m_flRackTime)), + SendPropTime(SENDINFO(m_flRackSoundTime)), #endif END_NETWORK_TABLE() @@ -40,6 +44,8 @@ BEGIN_PREDICTION_DATA(CWeaponSupa7) DEFINE_PRED_FIELD(m_nTubeSlugs, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_nChamber, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_bRackPending, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flRackTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flRackSoundTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -58,6 +64,8 @@ BEGIN_DATADESC(CWeaponSupa7) DEFINE_FIELD(m_nTubeSlugs, FIELD_INTEGER), DEFINE_FIELD(m_nChamber, FIELD_INTEGER), DEFINE_FIELD(m_bRackPending, FIELD_BOOLEAN), + DEFINE_FIELD(m_flRackTime, FIELD_TIME), + DEFINE_FIELD(m_flRackSoundTime, FIELD_TIME), END_DATADESC() #endif @@ -96,6 +104,8 @@ CWeaponSupa7::CWeaponSupa7(void) m_nTubeSlugs = 0; m_nChamber = SUPA7_CHAMBER_EMPTY; m_bRackPending = false; + m_flRackTime = 0.0f; + m_flRackSoundTime = 0.0f; m_fMinRange1 = 0.0; m_fMaxRange1 = 500; @@ -441,6 +451,7 @@ void CWeaponSupa7::ItemPostFrame(void) } ProcessAnimationEvents(); SyncTube(); + RunRack(); if (m_bInReload) { @@ -525,8 +536,12 @@ void CWeaponSupa7::ItemPostFrame(void) if (!(pOwner->m_nButtons & IN_ATTACK) && m_flNextPrimaryAttack <= gpGlobals->curtime && pOwner->m_nButtons & IN_RELOAD && UsesClipsForAmmo1() && !m_bInReload) { - // reload when reload is pressed, or if no buttons are down and weapon is empty. - StartReload(); + // reload when reload is pressed, or if no buttons are down and weapon is empty; + // a press with the tube full racks the pump once instead + if (!StartReload() && (pOwner->m_afButtonPressed & IN_RELOAD) && m_nTubeCount > 0 && m_nTubeCount >= TubeCapacity()) + { + StartRack(); + } } else if (!(pOwner->m_nButtons & IN_ATTACK) && m_flNextPrimaryAttack <= gpGlobals->curtime && pOwner->m_nButtons & IN_ATTACK2 && UsesClipsForAmmo1() && !m_bInReload) { @@ -572,6 +587,63 @@ void CWeaponSupa7::PlayInsertAtShellPace() } } +// The standalone rack: the support hand reaches for the pump and racks it once (the "rack" sequence where the model +// has it, else the reload's finishing rack); the chamber empties at the pump's back stroke, the pump wav timed to it +static constexpr const char *VM_SEQ_RACK = "rack"; +// the pump reaches the back of its stroke this long before either sequence ends (reloadend f12 of f30) +static constexpr float RACK_BACK_BEFORE_END = 0.6f; +// murata_pump.wav's back click comes this far into the file +static constexpr float PUMP_WAV_BACK_CLICK = 0.419f; + +void CWeaponSupa7::StartRack() +{ + CBasePlayer *pOwner = ToBasePlayer(GetOwner()); + if (!pOwner) + { + return; + } + + const int iSeq = LookupSequence(VM_SEQ_RACK); + if (iSeq >= 0) + { + SetSequence(iSeq); + SendViewModelAnim(iSeq); + } + else + { + SendWeaponAnim(ACT_SHOTGUN_PUMP); + } + + const float flDuration = SequenceDuration(); + m_flRackTime = gpGlobals->curtime + Max(0.0f, flDuration - RACK_BACK_BEFORE_END); + m_flRackSoundTime = Max(gpGlobals->curtime, m_flRackTime - PUMP_WAV_BACK_CLICK); + pOwner->m_flNextAttack = gpGlobals->curtime; + ProposeNextAttack(gpGlobals->curtime + flDuration); + SetWeaponIdleTime(gpGlobals->curtime + flDuration); +} + +void CWeaponSupa7::RunRack() +{ + if (m_flRackSoundTime > 0.0f && gpGlobals->curtime >= m_flRackSoundTime) + { + m_flRackSoundTime = 0.0f; + WeaponSound(SPECIAL2); + } + if (m_flRackTime > 0.0f && gpGlobals->curtime >= m_flRackTime) + { + m_flRackTime = 0.0f; + Rack(); + } +} + +bool CWeaponSupa7::Holster(CBaseCombatWeapon *pSwitchingTo) +{ + // a rack cut off before the back stroke does nothing + m_flRackTime = 0.0f; + m_flRackSoundTime = 0.0f; + return BaseClass::Holster(pSwitchingTo); +} + // The tube holds what the gun's capacity leaves beside a live round in the chamber int CWeaponSupa7::TubeCapacity() const { diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index a3f663c638..04a7778e48 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -63,6 +63,7 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon void ClearDelayedInputs(void); bool CanBePickedUpByClass(int classId) OVERRIDE; + bool Holster(CBaseCombatWeapon *pSwitchingTo) OVERRIDE; // What sits in the chamber: a live shell or slug, the hull of a fired one, or nothing enum Supa7Chamber_t @@ -96,6 +97,8 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon void Rack(); void UpdateClipFromTube(); void PlayInsertAtShellPace(); + void StartRack(); + void RunRack(); void SetShotgunShellVisible(bool is_visible) { @@ -116,6 +119,8 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon CNetworkVar(int, m_nTubeSlugs); // bit i: the item at tube position i (0 = loaded first) is a slug CNetworkVar(int, m_nChamber); // Supa7Chamber_t CNetworkVar(bool, m_bRackPending); // a slug was loaded to be fired next: the reload ends with a rack + CNetworkVar(float, m_flRackTime); // a standalone rack's back stroke: the chamber is ejected and fed then + CNetworkVar(float, m_flRackSoundTime); // when its pump sound starts private: CWeaponSupa7(const CWeaponSupa7 &other); From 165e63be117449ae4c87fd1432ecae635935b7fc Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:36:00 -0600 Subject: [PATCH 37/60] Supa7: play a slug insert at its own pace A slug insert waits for its whole animation before the reload goes on, so it keeps the normal playback rate. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index 2066adceea..25ab621f2c 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -266,8 +266,8 @@ bool CWeaponSupa7::ReloadSlug(void) WeaponSound(SPECIAL1); SendWeaponAnim(ACT_VM_RELOAD); pOwner->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); - PlayInsertAtShellPace(); + // a slug insert waits for its whole animation, so it plays at its own pace pOwner->m_flNextAttack = gpGlobals->curtime; // ProposeNextAttack(gpGlobals->curtime + SequenceDuration()); m_flNextReload = gpGlobals->curtime + SequenceDuration(); From faa65c67e86fc34bd5f19f710485553d8c7f4ff5 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 19:37:00 -0600 Subject: [PATCH 38/60] Top up a revolver's cylinder instead of emptying it The Kyla has no chamber, so its reload is one sequence, and the drop emptied the cylinder: a reload at 4 rounds threw those 4 away. Keep the live rounds and fill only the empty slots, as before the phase machine; the full-magazine rule is for magazine guns. --- .../neo/weapons/weapon_neobasecombatweapon.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index e9c62092b9..ed35c8c488 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -813,12 +813,18 @@ void CNEOBaseCombatWeapon::CreditReloadPhase(ReloadPhase_t phase) switch (phase) { case RELOAD_PHASE_DROPPED: - // the old magazine and its rounds are gone, a chambered round stays - m_iClip1 = (m_iClip1 > 0 && !IsReloadDry()) ? 1 : 0; + // the old magazine and its rounds are gone, a chambered round stays; + // a revolver's cylinder (no chamber) keeps its live rounds and drops only the empty cases + if (GetNEOWpnData().m_bChamberedRound) + { + m_iClip1 = (m_iClip1 > 0 && !IsReloadDry()) ? 1 : 0; + } break; case RELOAD_PHASE_SEATED: { - const int iLoad = Min(GetMaxClip1(), m_iPrimaryAmmoCount.Get()); + // a fresh magazine, or the rounds that fill a cylinder's empty slots + const int iRoom = GetNEOWpnData().m_bChamberedRound ? GetMaxClip1() : GetMaxClip1() - m_iClip1; + const int iLoad = Min(iRoom, m_iPrimaryAmmoCount.Get()); m_iClip1 += iLoad; m_iPrimaryAmmoCount -= iLoad; break; From 5d3644be7079a2940c5c4fb5231de7e5faac1fa5 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 21:58:17 -0600 Subject: [PATCH 39/60] Show the reload gesture for phase reloads Phase reloads and draw-resumes set PLAYER_RELOAD, which does nothing under NEO, so other players saw no reload gesture (the shipped reload path sends the animation event). Send PLAYERANIMEVENT_RELOAD as the Supa7 does. A drop or a death now also stops the reload's sound piece. --- src/game/server/neo/neo_player.cpp | 7 +++++++ .../neo/weapons/weapon_neobasecombatweapon.cpp | 12 +++++++++++- .../shared/neo/weapons/weapon_neobasecombatweapon.h | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 4a5278e505..68c8acf21f 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -2386,6 +2386,12 @@ void CNEO_Player::AddPoints(int score, bool bAllowNegativeScore, bool bIgnorePla ConVar cl_neo_spec_auto_observe_killer_if_observing_victim("cl_neo_spec_auto_observe_killer_if_observing_victim", "1", FCVAR_CLIENTDLL | FCVAR_ARCHIVE | FCVAR_USERINFO, "If the current observer target is killed when in eye or following, switch observer target to the killer", true, 0, true, 1); void CNEO_Player::Event_Killed( const CTakeDamageInfo &info ) { + // a death cuts a reload short, whether or not the weapon drops + if (auto *pNeoWeapon = dynamic_cast(GetActiveWeapon())) + { + pNeoWeapon->StopReloadOnDrop(); + } + CBaseEntity* pAttacker = info.GetAttacker(); if (pAttacker && pAttacker->IsPlayer()) // we can only have players as a spectate target atm { @@ -2992,6 +2998,7 @@ void CNEO_Player::Weapon_Drop( CBaseCombatWeapon *pWeapon, pWeapon->m_bInReload = false; pWeapon->StopWeaponSound(RELOAD_NPC); + neoWeapon->StopReloadOnDrop(); Vector vecVel = BodyDirection3D(); diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index ed35c8c488..81ccb9015c 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -721,9 +721,10 @@ void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence) { SendWeaponAnim(ACT_VM_RELOAD); } + // the reload gesture other players see (SetAnimation(PLAYER_RELOAD) does nothing under NEO) if (pOwner && pOwner->IsPlayer()) { - static_cast(pOwner)->SetAnimation(PLAYER_RELOAD); + static_cast(pOwner)->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); } // the wait to fire is never shorter than the animation that plays @@ -777,6 +778,15 @@ void CNEOBaseCombatWeapon::EmitReloadSoundPiece(const char *pszSound) EmitSound(filter, pOwner->entindex(), pszSound); } +// A dropped weapon (a throw, or its owner's death) cuts the reload short: its sound pieces stop +void CNEOBaseCombatWeapon::StopReloadOnDrop() +{ + if (HasReloadPhases()) + { + StopReloadSoundPieces(); + } +} + // A switch cuts the reload sound pieces off void CNEOBaseCombatWeapon::StopReloadSoundPieces() { diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index a5faa0af81..0d996b724c 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -266,6 +266,7 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon float GetPenetration() const; // the magazine plus the round a partial reload keeps in the chamber int GetMaxRoundsInGun() const; + void StopReloadOnDrop(); #ifdef CLIENT_DLL float m_flTemperature; int m_spawnflags; From 74cac6b7be44039e2e33307132127f04e4a05007 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 22:09:49 -0600 Subject: [PATCH 40/60] Start the reload gesture where a resumed reload resumes A resumed reload played the full reload gesture from the start, and a partial or interrupted one let it run on past the reload. Send the gesture's start cycle in the animation event's spare data (the credit time of the last completed phase in the full reload, as a fraction of it; 0 for a fresh reload and the draw-resume), and end the gesture with PLAYERANIMEVENT_RELOAD_END when the reload ends, is holstered or the weapon is dropped. The animation state starts the layer at that cycle and resets the slot on the end event. No new networked fields. --- src/game/server/neo/neo_player.cpp | 2 +- .../shared/hl2mp/hl2mp_playeranimstate.cpp | 36 ++++++++++++ src/game/shared/hl2mp/hl2mp_playeranimstate.h | 1 + .../weapons/weapon_neobasecombatweapon.cpp | 58 +++++++++++++++++-- .../neo/weapons/weapon_neobasecombatweapon.h | 4 +- 5 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 68c8acf21f..e7c3471bfe 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -2996,9 +2996,9 @@ void CNEO_Player::Weapon_Drop( CBaseCombatWeapon *pWeapon, return; // Return early to not drop weapon } + neoWeapon->StopReloadOnDrop(); pWeapon->m_bInReload = false; pWeapon->StopWeaponSound(RELOAD_NPC); - neoWeapon->StopReloadOnDrop(); Vector vecVel = BodyDirection3D(); diff --git a/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp b/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp index 9e441acd87..fcac57f6a4 100644 --- a/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp +++ b/src/game/shared/hl2mp/hl2mp_playeranimstate.cpp @@ -343,6 +343,11 @@ void CHL2MPPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData { #ifdef NEO AddToGestureSlot(GESTURE_SLOT_ATTACK_AND_RELOAD, CalcReloadSequence(), true); + // a resumed reload starts the gesture where the reload resumes: nData is that cycle x 1000 + if (nData > 0) + { + SetGestureSlotCycle(GESTURE_SLOT_ATTACK_AND_RELOAD, nData / 1000.0f); + } #else // Weapon reload. if ( GetBasePlayer()->GetFlags() & FL_DUCKING ) @@ -363,6 +368,16 @@ void CHL2MPPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData } case PLAYERANIMEVENT_RELOAD_END: { +#ifdef NEO + // the reload ended or was cut short: stop its gesture, if the slot still plays it + const int iReloadSequence = CalcReloadSequence(); + if (m_aGestureSlots[GESTURE_SLOT_ATTACK_AND_RELOAD].m_pAnimLayer && iReloadSequence > 0 && + m_aGestureSlots[GESTURE_SLOT_ATTACK_AND_RELOAD].m_pAnimLayer->m_nSequence == iReloadSequence) + { + ResetGestureSlot(GESTURE_SLOT_ATTACK_AND_RELOAD); + } + break; +#endif // Weapon reload. if ( GetBasePlayer()->GetFlags() & FL_DUCKING ) RestartGesture( GESTURE_SLOT_ATTACK_AND_RELOAD, ACT_MP_RELOAD_CROUCH_END ); @@ -1013,3 +1028,24 @@ void CHL2MPPlayerAnimState::DebugShowAnimStateFull(int iStartLine) AnimStateLog("--------------------------------------------\n\n"); } + +#ifdef NEO +// Start a gesture part way through its sequence, as AddVCDSequenceToGestureSlot does for scene gestures +void CHL2MPPlayerAnimState::SetGestureSlotCycle(int iGestureSlot, float flCycle) +{ + GestureSlot_t &slot = m_aGestureSlots[iGestureSlot]; + if (!slot.m_pAnimLayer) + { + return; + } + + slot.m_pAnimLayer->m_flCycle = flCycle; + slot.m_pAnimLayer->m_flPrevCycle = flCycle; +#ifdef CLIENT_DLL + if (CBasePlayer *pPlayer = GetBasePlayer()) + { + pPlayer->m_flOverlayPrevEventCycle[iGestureSlot] = flCycle; + } +#endif +} +#endif // NEO diff --git a/src/game/shared/hl2mp/hl2mp_playeranimstate.h b/src/game/shared/hl2mp/hl2mp_playeranimstate.h index 9a05125539..0ef11f0281 100644 --- a/src/game/shared/hl2mp/hl2mp_playeranimstate.h +++ b/src/game/shared/hl2mp/hl2mp_playeranimstate.h @@ -64,6 +64,7 @@ class CHL2MPPlayerAnimState : public CMultiPlayerAnimState #ifdef NEO bool IsLeaning(CStudioHdr *pStudioHdr); + void SetGestureSlotCycle(int iGestureSlot, float flCycle); #endif // NEO CHL2MP_Player *m_pHL2MPPlayer; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 81ccb9015c..be3dbb8040 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -617,7 +617,7 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() return false; } - StartReloadSequence(iTable, iSequence); + StartReloadSequence(iTable, iSequence, ResumeGestureCycle()); return true; } @@ -690,7 +690,46 @@ int CNEOBaseCombatWeapon::SelectReloadTable(bool bFromDraw, int *pSequence) cons } // Play a reload sequence from its start; its phase table credits the phases left from now on -void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence) +// Where the full reload's gesture should start for a reload resumed after a switch: the time the last completed phase +// counts in the full reload, as a fraction of it (0 for a fresh reload) +float CNEOBaseCombatWeapon::ResumeGestureCycle() const +{ + if (m_nReloadPhase == RELOAD_PHASE_NONE) + { + return 0.0f; + } + + const CNEOWeaponInfo &info = GetNEOWpnData(); + const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); + const int iSequence = const_cast(this)->LookupSequence(VM_SEQ_RELOAD); + if (iTable < 0 || iSequence < 0) + { + return 0.0f; + } + + const float flDuration = const_cast(this)->SequenceDuration(iSequence); + const ReloadPhaseTable_t &table = info.m_reloadTables[iTable]; + for (int i = 0; i < table.iNumRows; ++i) + { + if (table.rows[i].phase == m_nReloadPhase && flDuration > 0.0f) + { + return Clamp(table.rows[i].flCredit / flDuration, 0.0f, 1.0f); + } + } + return 0.0f; +} + +// Stop the reload gesture others see, for a reload that ended or was cut short +void CNEOBaseCombatWeapon::EndReloadGesture() +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (pOwner && pOwner->IsPlayer()) + { + static_cast(pOwner)->DoAnimationEvent(PLAYERANIMEVENT_RELOAD_END); + } +} + +void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence, float flGestureStartCycle) { CBaseCombatCharacter *pOwner = GetOwner(); m_nReloadTable = iTable; @@ -724,7 +763,8 @@ void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence) // the reload gesture other players see (SetAnimation(PLAYER_RELOAD) does nothing under NEO) if (pOwner && pOwner->IsPlayer()) { - static_cast(pOwner)->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); + // the gesture's start cycle travels in the event's spare data, in thousandths + static_cast(pOwner)->DoAnimationEvent(PLAYERANIMEVENT_RELOAD, static_cast(flGestureStartCycle * 1000.0f)); } // the wait to fire is never shorter than the animation that plays @@ -784,6 +824,10 @@ void CNEOBaseCombatWeapon::StopReloadOnDrop() if (HasReloadPhases()) { StopReloadSoundPieces(); + if (m_bInReload) + { + EndReloadGesture(); + } } } @@ -861,6 +905,7 @@ void CNEOBaseCombatWeapon::CheckReloadWithPhases() // the sequence has ended m_bInReload = false; + EndReloadGesture(); m_nReloadTable = -1; if (!IsReloadPending()) { @@ -959,7 +1004,8 @@ bool CNEOBaseCombatWeapon::Deploy(void) const int iTable = SelectReloadTable(true, &iSequence); if (iTable >= 0) { - StartReloadSequence(iTable, iSequence); + // the draw is part of the motion: the full gesture fits the draw-resume + StartReloadSequence(iTable, iSequence, 0.0f); } } @@ -1026,6 +1072,10 @@ bool CNEOBaseCombatWeapon::Holster(CBaseCombatWeapon* pSwitchingTo) // The phases credited so far stay; a reload with nothing left to do is over if (HasReloadPhases()) { + if (m_bInReload) + { + EndReloadGesture(); + } if (!IsReloadPending()) { m_nReloadPhase = RELOAD_PHASE_NONE; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 0d996b724c..9d889415a4 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -326,7 +326,9 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon void StopReloadSoundPieces(); void EmitReloadSoundPiece(const char *pszSound); int SelectReloadTable(bool bFromDraw, int *pSequence) const; - void StartReloadSequence(int iTable, int iSequence); + void StartReloadSequence(int iTable, int iSequence, float flGestureStartCycle); + float ResumeGestureCycle() const; + void EndReloadGesture(); }; From 6040298d2ef98ad45b86f400caa484bc52349e26 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 22:23:03 -0600 Subject: [PATCH 41/60] Look up reload sequences on the viewmodel The partial and resume sequences were looked up on the weapon entity, which on the client carries the world model: the lookup failed there, so the client predicted the shipped reload while the server played the partial or resume sequence, and the resumed gesture started from 0 on the owner's own client. Look them up, and time them, on the owner's viewmodel on both sides. --- .../weapons/weapon_neobasecombatweapon.cpp | 28 ++++++++++++++++--- .../neo/weapons/weapon_neobasecombatweapon.h | 3 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index be3dbb8040..9562457807 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -674,7 +674,7 @@ int CNEOBaseCombatWeapon::SelectReloadTable(bool bFromDraw, int *pSequence) cons if (pszName && V_strcmp(pszName, VM_SEQ_RELOAD) != 0) { const int iTable = info.FindReloadTable(pszName); - const int iSequence = (iTable >= 0) ? const_cast(this)->LookupSequence(pszName) : -1; + const int iSequence = (iTable >= 0) ? LookupReloadSequence(pszName) : -1; if (iSequence >= 0) { *pSequence = iSequence; @@ -701,13 +701,13 @@ float CNEOBaseCombatWeapon::ResumeGestureCycle() const const CNEOWeaponInfo &info = GetNEOWpnData(); const int iTable = info.FindReloadTable(VM_SEQ_RELOAD); - const int iSequence = const_cast(this)->LookupSequence(VM_SEQ_RELOAD); + const int iSequence = LookupReloadSequence(VM_SEQ_RELOAD); if (iTable < 0 || iSequence < 0) { return 0.0f; } - const float flDuration = const_cast(this)->SequenceDuration(iSequence); + const float flDuration = ReloadSequenceDuration(iSequence); const ReloadPhaseTable_t &table = info.m_reloadTables[iTable]; for (int i = 0; i < table.iNumRows; ++i) { @@ -719,6 +719,26 @@ float CNEOBaseCombatWeapon::ResumeGestureCycle() const return 0.0f; } +// Reload sequences are looked up on the owner's viewmodel: on the client the weapon entity carries the world model, +// so a lookup there would fail and the client would predict a different sequence than the server plays +CBaseViewModel *CNEOBaseCombatWeapon::ReloadViewModel() const +{ + CBasePlayer *pOwner = ToBasePlayer(GetOwner()); + return pOwner ? pOwner->GetViewModel(m_nViewModelIndex) : nullptr; +} + +int CNEOBaseCombatWeapon::LookupReloadSequence(const char *pszName) const +{ + CBaseViewModel *pVM = ReloadViewModel(); + return pVM ? pVM->LookupSequence(pszName) : -1; +} + +float CNEOBaseCombatWeapon::ReloadSequenceDuration(int iSequence) const +{ + CBaseViewModel *pVM = ReloadViewModel(); + return pVM ? pVM->SequenceDuration(iSequence) : 0.0f; +} + // Stop the reload gesture others see, for a reload that ended or was cut short void CNEOBaseCombatWeapon::EndReloadGesture() { @@ -768,7 +788,7 @@ void CNEOBaseCombatWeapon::StartReloadSequence(int iTable, int iSequence, float } // the wait to fire is never shorter than the animation that plays - const float flEnd = gpGlobals->curtime + SequenceDuration(); + const float flEnd = gpGlobals->curtime + ((iSequence >= 0) ? ReloadSequenceDuration(iSequence) : SequenceDuration()); if (pOwner) { pOwner->SetNextAttack(flEnd); diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 9d889415a4..378643da1a 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -328,6 +328,9 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon int SelectReloadTable(bool bFromDraw, int *pSequence) const; void StartReloadSequence(int iTable, int iSequence, float flGestureStartCycle); float ResumeGestureCycle() const; + CBaseViewModel *ReloadViewModel() const; + int LookupReloadSequence(const char *pszName) const; + float ReloadSequenceDuration(int iSequence) const; void EndReloadGesture(); }; From faca1033dd04a06162558562fef83407f6055150 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 22:41:17 -0600 Subject: [PATCH 42/60] Network every reload table index The table index was sent in 4 signed bits (-8..7), but a gun has up to 12 tables and the ZR68, MX, M41 and PZ guns use index 8 for draw_resume_drop_partial. The owner's client read another table, so it predicted the wrong credits and replayed the drop sound piece on a draw-resume after a partial reload's drop. --- src/game/shared/neo/weapons/neo_weapon_parse.h | 3 +++ src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index 672db0e46e..9194104f9d 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -25,6 +25,9 @@ enum ReloadPhase_t }; static constexpr int RELOAD_PHASE_TABLES_MAX = 12; +// bits to network a table index, signed so that -1 (no table) fits +static constexpr int RELOAD_PHASE_TABLE_BITS = 5; +static_assert((1 << (RELOAD_PHASE_TABLE_BITS - 1)) >= RELOAD_PHASE_TABLES_MAX, "RELOAD_PHASE_TABLE_BITS cannot hold every table index"); static constexpr int RELOAD_PHASE_ROWS_MAX = RELOAD_PHASE_COUNT - 1; static constexpr int RELOAD_SOUND_NAME_MAX = 64; static constexpr int RELOAD_SEQUENCE_NAME_MAX = 32; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 9562457807..d72c463683 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -59,7 +59,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropInt(SENDINFO(m_nReloadPhase), 3, SPROP_UNSIGNED), SendPropBool(SENDINFO(m_bChamberEmpty)), SendPropBool(SENDINFO(m_bReloadDry)), - SendPropInt(SENDINFO(m_nReloadTable), 4), + SendPropInt(SENDINFO(m_nReloadTable), RELOAD_PHASE_TABLE_BITS), SendPropTime(SENDINFO(m_flReloadStart)), SendPropInt(SENDINFO(m_nReloadSoundsDone), RELOAD_PHASE_ROWS_MAX * RELOAD_SOUNDS_PER_ROW, SPROP_UNSIGNED), SendPropInt(SENDINFO(m_spawnflags)), From 69b0490dcf7548ee6682ebe0b6c8c482896da9de Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 22:58:12 -0600 Subject: [PATCH 43/60] Supa7: hold seven in the tube beside the chamber The tube's capacity was the clip size minus a live chambered round, so the gun held 7 in all, as on master and in classic. The tube now holds the script's clip size (7) and the chamber one more round beside it: a partial reload ends at 8, a dry reload loads 7 and racks one up (chamber + 6, 7 in all) with room for a one-shell top-off, and the standalone rack is offered when the tube holds 7. GetMaxRoundsInGun becomes virtual so the ammo HUD draws 8 slots for the Supa7. GetMaxClip1 stays 7 (the tube) for bots and the base code, as the magazine size does for the other chambered guns. --- .../neo/weapons/weapon_neobasecombatweapon.h | 2 +- src/game/shared/neo/weapons/weapon_supa7.cpp | 26 ++++++++++++------- src/game/shared/neo/weapons/weapon_supa7.h | 4 +++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 378643da1a..51940764ce 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -265,7 +265,7 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon virtual float GetFireRate() override final; float GetPenetration() const; // the magazine plus the round a partial reload keeps in the chamber - int GetMaxRoundsInGun() const; + virtual int GetMaxRoundsInGun() const; void StopReloadOnDrop(); #ifdef CLIENT_DLL float m_flTemperature; diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index 25ab621f2c..7da779cad8 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -147,7 +147,7 @@ bool CWeaponSupa7::StartReload(void) return false; SyncTube(); - if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) + if (m_nTubeCount >= TubeCapacity()) return false; SendWeaponAnim(ACT_SHOTGUN_RELOAD_START); @@ -179,7 +179,7 @@ bool CWeaponSupa7::StartReloadSlug(void) } SyncTube(); - if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) + if (m_nTubeCount >= TubeCapacity()) return false; if (!m_bInReload) @@ -217,7 +217,7 @@ bool CWeaponSupa7::Reload(void) if (m_iPrimaryAmmoCount <= 0) return false; - if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) + if (m_nTubeCount >= TubeCapacity()) return false; // Play reload on different channel as otherwise steals channel away from fire sound @@ -255,7 +255,7 @@ bool CWeaponSupa7::ReloadSlug(void) if (m_iSecondaryAmmoCount <= 0) return false; - if (m_iClip1 >= GetMaxClip1() || m_nTubeCount >= TubeCapacity()) + if (m_nTubeCount >= TubeCapacity()) return false; // the slug goes on top of the tube and is racked into the chamber when the reload ends @@ -474,9 +474,9 @@ void CWeaponSupa7::ItemPostFrame(void) } else { - // If out of ammo end reload + // If out of ammo or the tube is full end reload // (a slug loaded to be fired next is racked up before anything goes on top of it) - if ((m_iPrimaryAmmoCount <= 0 || m_bSlugLoaded || m_bRackPending || m_iClip1 >= GetMaxClip1())) + if ((m_iPrimaryAmmoCount <= 0 || m_bSlugLoaded || m_bRackPending || m_nTubeCount >= TubeCapacity())) { FinishReload(); } @@ -644,10 +644,16 @@ bool CWeaponSupa7::Holster(CBaseCombatWeapon *pSwitchingTo) return BaseClass::Holster(pSwitchingTo); } -// The tube holds what the gun's capacity leaves beside a live round in the chamber +// The script's clip size is the tube, and the chamber holds one more round beside it: +// a dry reload fills the tube and racks one up, leaving room in the tube for a top-off int CWeaponSupa7::TubeCapacity() const { - return GetMaxClip1() - (IsChamberLive() ? 1 : 0); + return GetMaxClip1(); +} + +int CWeaponSupa7::GetMaxRoundsInGun() const +{ + return TubeCapacity() + SUPA7_CHAMBER_ROUNDS; } // Rebuild the tube from the clip count when something set the count directly (a new or picked-up gun, a harness give): @@ -660,9 +666,9 @@ void CWeaponSupa7::SyncTube() return; } - const int iClip = Max(0, m_iClip1.Get()); + const int iClip = Clamp(m_iClip1.Get(), 0, GetMaxRoundsInGun()); m_nChamber = (iClip > 0) ? (m_bSlugLoaded ? SUPA7_CHAMBER_SLUG : SUPA7_CHAMBER_SHELL) : SUPA7_CHAMBER_EMPTY; - m_nTubeCount = Max(0, iClip - 1); + m_nTubeCount = Max(0, iClip - SUPA7_CHAMBER_ROUNDS); m_nTubeSlugs = 0; UpdateClipFromTube(); } diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index 04a7778e48..32db4f67d7 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -63,6 +63,8 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon void ClearDelayedInputs(void); bool CanBePickedUpByClass(int classId) OVERRIDE; + // the tube plus the round in the chamber + int GetMaxRoundsInGun() const OVERRIDE; bool Holster(CBaseCombatWeapon *pSwitchingTo) OVERRIDE; // What sits in the chamber: a live shell or slug, the hull of a fired one, or nothing @@ -73,6 +75,8 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon SUPA7_CHAMBER_SLUG, SUPA7_CHAMBER_HULL, }; + // the chamber holds one round, outside the tube + static constexpr int SUPA7_CHAMBER_ROUNDS = 1; int TubeCount() const { return m_nTubeCount; } bool IsTubeSlug(int iPosition) const { return (m_nTubeSlugs & (1 << iPosition)) != 0; } Supa7Chamber_t Chamber() const { return static_cast(m_nChamber.Get()); } From 64a9290b7610cecd17b15a348a5c8987e692baf3 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:06:47 -0600 Subject: [PATCH 44/60] Supa7: rack before firing when the chamber is empty A dry reload switched away from before its end rack leaves shells in the tube and the chamber empty. The trigger then fired anyway, and the shot's own pump fed the tube's top shell, so that shot cost no round (7 in the gun before and after). The trigger now racks the pump once (the standalone rack) and the next press fires. Master and classic fire at once in this state and pump on the next reload; this rack only ever delays the shot, never brings it earlier. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index 7da779cad8..b1e456c2d9 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -515,6 +515,12 @@ void CWeaponSupa7::ItemPostFrame(void) { DryFire(); } + // a reload cut off before its rack leaves rounds in the tube and none in the chamber, + // so the trigger racks one up instead of firing a round the chamber does not hold + else if (!IsChamberLive()) + { + StartRack(); + } // Fire underwater? else if (pOwner->GetWaterLevel() == WL_Eyes && !m_bFiresUnderwater) { From 01547b2a50ad9d5f8e5ea92af5b097f180a13f79 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:14:29 -0600 Subject: [PATCH 45/60] Supa7: load shells behind a chambered slug A chambered slug blocked every shell reload, a rule from master's single clip count, where the slug was simply the next round to fire. With the chamber outside the tube, shells now load into the tube behind a chambered slug and the reload ends without a rack, so a gun with a slug up still tops off to 8. Loading a second slug onto a chambered one stays refused, so a slug is never racked out by accident. --- src/game/shared/neo/weapons/weapon_supa7.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index b1e456c2d9..5b0380bc6f 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -121,11 +121,9 @@ CWeaponSupa7::CWeaponSupa7(void) } // Purpose: Override so only reload one shell at a time +// (a chambered slug stays where it is while shells go into the tube behind it) bool CWeaponSupa7::StartReload(void) { - if (m_bSlugLoaded) - return false; - CBaseCombatCharacter* pOwner = GetOwner(); if (pOwner == NULL) @@ -476,7 +474,7 @@ void CWeaponSupa7::ItemPostFrame(void) { // If out of ammo or the tube is full end reload // (a slug loaded to be fired next is racked up before anything goes on top of it) - if ((m_iPrimaryAmmoCount <= 0 || m_bSlugLoaded || m_bRackPending || m_nTubeCount >= TubeCapacity())) + if ((m_iPrimaryAmmoCount <= 0 || m_bRackPending || m_nTubeCount >= TubeCapacity())) { FinishReload(); } From 09b30e9d54d7b6a553f2303bd895ee9a52dbc90a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:15:10 -0600 Subject: [PATCH 46/60] Lock a pistol's slide back on its last shot The shot that emptied a Tachi or Milso played its normal fire sequence, whose slide closes within 0.2 s, and the slide went back only when the empty idle followed 0.4 s later. Both pistols ship a dry fire sequence that is a shot's kick with the slide held back throughout, ending on the empty idle's pose, so the last round's shot plays that instead. --- .../neo/weapons/weapon_neobasecombatweapon.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 5fec7bd26f..074d14f938 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -1045,8 +1045,6 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) #endif // GAME_DLL } - SendWeaponAnim(GetPrimaryAttackActivity()); - SetWeaponIdleTime(gpGlobals->curtime + 2.0); if (IsSemiAuto()) { m_bTriggerReset = false; @@ -1090,11 +1088,21 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) m_iPrimaryAmmoCount -= info.m_iShots; } - // the last round's shot goes straight on to the empty idle when it ends - if (UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle()) + // the last round's shot locks a pistol's slide back: + // the pistols' dry fire sequence is a shot's kick with the slide held back, + static constexpr NEO_WEP_BITS_UNDERLYING_TYPE NEO_WEP_SLIDE_LOCK = NEO_WEP_MILSO | NEO_WEP_TACHI; + if (UsesClipsForAmmo1() && m_iClip1 <= 0 && (GetNeoWepBits() & NEO_WEP_SLIDE_LOCK)) { - SetWeaponIdleTime(gpGlobals->curtime + SequenceDuration()); + SendWeaponAnim(ACT_VM_DRYFIRE); } + else + { + SendWeaponAnim(GetPrimaryAttackActivity()); + } + + // and goes straight on to the empty idle when it ends + const bool bEmptyIdleNext = UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle(); + SetWeaponIdleTime(gpGlobals->curtime + (bEmptyIdleNext ? SequenceDuration() : 2.0)); info.m_flDistance = MAX_TRACE_LENGTH; info.m_iAmmoType = m_iPrimaryAmmoType; From d934bbd9d976ba10419ad49cd82929d9cf41f8fe Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:16:40 -0600 Subject: [PATCH 47/60] Reload an empty Milso with its slide held back A reload always played the reload sequence, whose slide is forward from its first frame, so the slide of an empty pistol closed as the reload began. The Milso ships a reload for an empty gun (ACT_VM_RELOAD_EMPTY) that keeps the slide back until its release near the end, the same length as the reload; an empty weapon now plays it where the model has one. --- src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 074d14f938..b962378cf5 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -416,6 +416,12 @@ void CNEOBaseCombatWeapon::Equip(CBaseCombatCharacter* pOwner) bool CNEOBaseCombatWeapon::Reload( void ) { + // an empty weapon reloads with its slide held back until the release, where the model has that reload + if (UsesClipsForAmmo1() && m_iClip1 <= 0 && SelectWeightedSequence(ACT_VM_RELOAD_EMPTY) != ACTIVITY_NOT_AVAILABLE) + { + return DefaultReload(GetMaxClip1(), GetMaxClip2(), ACT_VM_RELOAD_EMPTY); + } + return BaseClass::Reload(); #if(0) From 3adcb384463f2b6e675d12a725defa4c98b9c673 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:18:05 -0600 Subject: [PATCH 48/60] Show the reload gesture on an empty Milso reload --- .../shared/neo/weapons/weapon_neobasecombatweapon.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index b962378cf5..b0cb08bdfb 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -419,7 +419,14 @@ bool CNEOBaseCombatWeapon::Reload( void ) // an empty weapon reloads with its slide held back until the release, where the model has that reload if (UsesClipsForAmmo1() && m_iClip1 <= 0 && SelectWeightedSequence(ACT_VM_RELOAD_EMPTY) != ACTIVITY_NOT_AVAILABLE) { - return DefaultReload(GetMaxClip1(), GetMaxClip2(), ACT_VM_RELOAD_EMPTY); + if (!DefaultReload(GetMaxClip1(), GetMaxClip2(), ACT_VM_RELOAD_EMPTY)) + { + return false; + } + + // others see the reload, as for a reload with rounds left + ToHL2MPPlayer(GetOwner())->DoAnimationEvent(PLAYERANIMEVENT_RELOAD); + return true; } return BaseClass::Reload(); From b7b8a086434587210ddf5cfb45ce03ec0d0e48c1 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:01:20 -0600 Subject: [PATCH 49/60] Draw and idle a pistol with its slide back until the release A dry Milso or Tachi reload interrupted after the seat has rounds in the magazine but none chambered, and its slide is still back: the resume is the slide release alone. The draw and the idle chose their empty sequences by an empty magazine only, so the slide went forward on the draw and snapped back when the release resumed. --- .../weapons/weapon_neobasecombatweapon.cpp | 25 ++++++++++++++++--- .../neo/weapons/weapon_neobasecombatweapon.h | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 2db766ab2b..792a1a77cd 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -448,6 +448,9 @@ void CNEOBaseCombatWeapon::Equip(CBaseCombatCharacter* pOwner) } +// The pistols whose slide locks back on the last shot, until a reload releases it +static constexpr NEO_WEP_BITS_UNDERLYING_TYPE NEO_WEP_SLIDE_LOCK = NEO_WEP_MILSO | NEO_WEP_TACHI; + bool CNEOBaseCombatWeapon::Reload( void ) { if (HasReloadPhases()) @@ -1025,7 +1028,7 @@ bool CNEOBaseCombatWeapon::Deploy(void) // An empty weapon draws with its slide held back where the model has that draw, // which carries no activity, so it is picked by name static constexpr const char *VM_SEQ_DRAW_EMPTY = "drawempty"; - const int iDrawEmpty = (UsesClipsForAmmo1() && m_iClip1 <= 0) ? LookupSequence(VM_SEQ_DRAW_EMPTY) : ACTIVITY_NOT_AVAILABLE; + const int iDrawEmpty = IsSlideLockedBack() ? LookupSequence(VM_SEQ_DRAW_EMPTY) : ACTIVITY_NOT_AVAILABLE; if (iDrawEmpty != ACTIVITY_NOT_AVAILABLE) { SetSequence(iDrawEmpty); @@ -1524,7 +1527,7 @@ void CNEOBaseCombatWeapon::DryFire() // An empty weapon idles on its empty sequence where the model has one, such as a pistol with its slide held back bool CNEOBaseCombatWeapon::SendWeaponAnim(int iActivity) { - if (iActivity == ACT_VM_IDLE && UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle()) + if (iActivity == ACT_VM_IDLE && IsSlideLockedBack() && HasEmptyIdle()) { iActivity = ACT_VM_IDLE_EMPTY; } @@ -1532,6 +1535,23 @@ bool CNEOBaseCombatWeapon::SendWeaponAnim(int iActivity) return BaseClass::SendWeaponAnim(iActivity); } +// An empty weapon shows its empty sequences, and so does a pistol whose dry reload seated a magazine +// but has not released the slide yet +bool CNEOBaseCombatWeapon::IsSlideLockedBack() const +{ + if (!UsesClipsForAmmo1()) + { + return false; + } + + if (m_iClip1 <= 0) + { + return true; + } + + return HasReloadPhases() && m_bChamberEmpty && (GetNeoWepBits() & NEO_WEP_SLIDE_LOCK); +} + bool CNEOBaseCombatWeapon::HasEmptyIdle() { return SelectWeightedSequence(ACT_VM_IDLE_EMPTY) != ACTIVITY_NOT_AVAILABLE; @@ -1680,7 +1700,6 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) // the last round's shot locks a pistol's slide back: // the pistols' dry fire sequence is a shot's kick with the slide held back, - static constexpr NEO_WEP_BITS_UNDERLYING_TYPE NEO_WEP_SLIDE_LOCK = NEO_WEP_MILSO | NEO_WEP_TACHI; if (UsesClipsForAmmo1() && m_iClip1 <= 0 && (GetNeoWepBits() & NEO_WEP_SLIDE_LOCK)) { SendWeaponAnim(ACT_VM_DRYFIRE); diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 51940764ce..516e5c027a 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -313,6 +313,7 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); bool HasEmptyIdle(); + bool IsSlideLockedBack() const; bool HasReloadPhases() const; bool IsReloadDry() const; From 0d8e2b6f273f8603d4da97e54d5fdc89cfcf1aa3 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:01:40 -0600 Subject: [PATCH 50/60] Fade out a reload gesture that ends early The NEO anim state ends a gesture at once, so a reload cut short by a partial magazine's end or a death snapped the third-person gun from the reload pose to the aim pose in one frame. Fade it out over 0.2 s as the HL2MP gesture slots did. --- src/game/shared/neo/neo_playeranimstate.cpp | 25 +++++++++++++++++---- src/game/shared/neo/neo_playeranimstate.h | 2 ++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/game/shared/neo/neo_playeranimstate.cpp b/src/game/shared/neo/neo_playeranimstate.cpp index 8d5b5e237f..e47a33ecd4 100644 --- a/src/game/shared/neo/neo_playeranimstate.cpp +++ b/src/game/shared/neo/neo_playeranimstate.cpp @@ -29,6 +29,10 @@ static constexpr const char *FIRE_RUN_NAME = "run_shoot_"; static constexpr int GESTURE_LAYER = AIMSEQUENCE_LAYER + NUM_AIMSEQUENCE_LAYERS; static constexpr int NUM_LAYERS = GESTURE_LAYER + 1; +// A reload cut short fades its gesture out over this long instead of snapping to the aim pose, +// as the gesture slots of the HL2MP anim state did +static constexpr float GESTURE_FADE_OUT_TIME = 0.2f; + // The legs play at the rate of the player's speed over the speed the walk, run and crouch walk sequences cover, // which is constant as in classic NEOTOKYO: two of the original models' forward walk and run animations carry no movement static constexpr float ANIM_TOPSPEED_WALK = 100.0f; @@ -73,6 +77,7 @@ CNEOPlayerAnimState::CNEOPlayerAnimState( CHL2MP_Player *pPlayer ) #endif m_iGestureSequence = 0; m_flGestureCycle = 0.0f; + m_flGestureFadeStart = 0.0f; CModAnimConfig config; config.m_flMaxBodyYawDegrees = NEO_ANIMSTATE_MAX_BODY_YAW_DEGREES; @@ -124,10 +129,10 @@ void CNEOPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) break; case PLAYERANIMEVENT_RELOAD_END: - // the reload ended or was cut short: stop its gesture, if the layer still plays it - if ( m_iGestureSequence > 0 && m_iGestureSequence == CalcReloadSequence() ) + // the reload ended or was cut short: fade its gesture out, if the layer still plays it + if ( m_iGestureSequence > 0 && m_iGestureSequence == CalcReloadSequence() && m_flGestureFadeStart <= 0.0f ) { - m_iGestureSequence = 0; + m_flGestureFadeStart = gpGlobals->curtime; } break; @@ -161,6 +166,7 @@ void CNEOPlayerAnimState::StartGesture( int iSequence ) m_iGestureSequence = iSequence; m_flGestureCycle = 0.0f; + m_flGestureFadeStart = 0.0f; m_hGestureWeapon = m_pPlayer->GetActiveWeapon(); } @@ -185,11 +191,22 @@ void CNEOPlayerAnimState::ComputeGestureSequence( CStudioHdr *pStudioHdr ) return; } + float flWeight = 1.0f; + if ( m_flGestureFadeStart > 0.0f ) + { + flWeight = 1.0f - ( gpGlobals->curtime - m_flGestureFadeStart ) / GESTURE_FADE_OUT_TIME; + if ( flWeight <= 0.0f ) + { + m_iGestureSequence = 0; + return; + } + } + CAnimationLayer *pLayer = m_pOuter->GetAnimOverlay( GESTURE_LAYER ); pLayer->m_nSequence = m_iGestureSequence; pLayer->m_flCycle = m_flGestureCycle; pLayer->m_flPlaybackRate = 1.0f; - pLayer->m_flWeight = 1.0f; + pLayer->m_flWeight = flWeight; pLayer->m_nOrder = GESTURE_LAYER; #ifndef CLIENT_DLL pLayer->m_fFlags |= ANIM_LAYER_ACTIVE; diff --git a/src/game/shared/neo/neo_playeranimstate.h b/src/game/shared/neo/neo_playeranimstate.h index 4c23f6fb84..454c5519e4 100644 --- a/src/game/shared/neo/neo_playeranimstate.h +++ b/src/game/shared/neo/neo_playeranimstate.h @@ -70,6 +70,8 @@ class CNEOPlayerAnimState : public CBasePlayerAnimState int m_iGestureSequence; float m_flGestureCycle; EHANDLE m_hGestureWeapon; + // a reload cut short fades its gesture out from this time, 0 while it plays in full + float m_flGestureFadeStart; }; CNEOPlayerAnimState *CreateNEOPlayerAnimState( CHL2MP_Player *pPlayer ); From 4788a5e8e3de6e6f133282236c918e7db82fb3b4 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:02:00 -0600 Subject: [PATCH 51/60] Fade out a reload gesture on a weapon switch A switch mid-reload ended the third-person reload gesture at once, the same snap from the reload pose to the aim pose that a reload ending early had. Fade it out over 0.2 s too; a fire or throw gesture still stops at once on a switch. --- src/game/shared/neo/neo_playeranimstate.cpp | 23 +++++++++++++++------ src/game/shared/neo/neo_playeranimstate.h | 10 +++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/game/shared/neo/neo_playeranimstate.cpp b/src/game/shared/neo/neo_playeranimstate.cpp index e47a33ecd4..a5905ff0e3 100644 --- a/src/game/shared/neo/neo_playeranimstate.cpp +++ b/src/game/shared/neo/neo_playeranimstate.cpp @@ -78,6 +78,7 @@ CNEOPlayerAnimState::CNEOPlayerAnimState( CHL2MP_Player *pPlayer ) m_iGestureSequence = 0; m_flGestureCycle = 0.0f; m_flGestureFadeStart = 0.0f; + m_eGestureKind = GESTURE_FIRE; CModAnimConfig config; config.m_flMaxBodyYawDegrees = NEO_ANIMSTATE_MAX_BODY_YAW_DEGREES; @@ -115,11 +116,11 @@ void CNEOPlayerAnimState::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) switch ( event ) { case PLAYERANIMEVENT_ATTACK_PRIMARY: - StartGesture( CalcFireSequence() ); + StartGesture( CalcFireSequence(), GESTURE_FIRE ); break; case PLAYERANIMEVENT_RELOAD: - StartGesture( CalcReloadSequence() ); + StartGesture( CalcReloadSequence(), GESTURE_RELOAD ); // a resumed reload starts the gesture where the reload resumes: nData is that cycle x 1000 if ( nData > 0 && m_iGestureSequence > 0 ) @@ -157,7 +158,7 @@ void CNEOPlayerAnimState::ComputeSequences( CStudioHdr *pStudioHdr ) ComputeGestureSequence( pStudioHdr ); } -void CNEOPlayerAnimState::StartGesture( int iSequence ) +void CNEOPlayerAnimState::StartGesture( int iSequence, GestureKind_t eKind ) { if ( iSequence <= 0 ) { @@ -167,6 +168,7 @@ void CNEOPlayerAnimState::StartGesture( int iSequence ) m_iGestureSequence = iSequence; m_flGestureCycle = 0.0f; m_flGestureFadeStart = 0.0f; + m_eGestureKind = eKind; m_hGestureWeapon = m_pPlayer->GetActiveWeapon(); } @@ -177,11 +179,20 @@ void CNEOPlayerAnimState::ComputeGestureSequence( CStudioHdr *pStudioHdr ) return; } - // a weapon switch ends the gesture of the previous weapon + // a weapon switch ends the gesture of the previous weapon: + // a reload fades out as when it ends early, any other gesture stops at once if ( m_pPlayer->GetActiveWeapon() != m_hGestureWeapon.Get() ) { - m_iGestureSequence = 0; - return; + if ( m_eGestureKind != GESTURE_RELOAD ) + { + m_iGestureSequence = 0; + return; + } + + if ( m_flGestureFadeStart <= 0.0f ) + { + m_flGestureFadeStart = gpGlobals->curtime; + } } m_flGestureCycle += m_pOuter->GetSequenceCycleRate( pStudioHdr, m_iGestureSequence ) * gpGlobals->frametime; diff --git a/src/game/shared/neo/neo_playeranimstate.h b/src/game/shared/neo/neo_playeranimstate.h index 454c5519e4..978950197e 100644 --- a/src/game/shared/neo/neo_playeranimstate.h +++ b/src/game/shared/neo/neo_playeranimstate.h @@ -49,7 +49,12 @@ class CNEOPlayerAnimState : public CBasePlayerAnimState const char *GetAimSuffix() const; int CalcFireSequence(); int CalcReloadSequence(); - void StartGesture( int iSequence ); + enum GestureKind_t + { + GESTURE_FIRE, + GESTURE_RELOAD, + }; + void StartGesture( int iSequence, GestureKind_t eKind ); void ComputeGestureSequence( CStudioHdr *pStudioHdr ); bool HandleJumping(); @@ -70,8 +75,9 @@ class CNEOPlayerAnimState : public CBasePlayerAnimState int m_iGestureSequence; float m_flGestureCycle; EHANDLE m_hGestureWeapon; - // a reload cut short fades its gesture out from this time, 0 while it plays in full + // a reload cut short or switched away fades its gesture out from this time, 0 while it plays in full float m_flGestureFadeStart; + GestureKind_t m_eGestureKind; }; CNEOPlayerAnimState *CreateNEOPlayerAnimState( CHL2MP_Player *pPlayer ); From a0c23f1eb44d099da85c08d087b8ff53e86375b7 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:23:44 -0600 Subject: [PATCH 52/60] Expose the chambered round apart from the magazine The clip counts the chambered round on guns with reload phases. Four getters split it out for the HUD and the fire rules: whether the gun has a chamber slot, whether a live round is in it, the rounds in the magazine and the magazine's capacity. The Supa7 answers from its tube and chamber. --- .../weapons/weapon_neobasecombatweapon.cpp | 21 +++++++++++++++++++ .../neo/weapons/weapon_neobasecombatweapon.h | 8 +++++++ src/game/shared/neo/weapons/weapon_supa7.h | 4 ++++ 3 files changed, 33 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 792a1a77cd..6126bd74ab 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -901,6 +901,27 @@ int CNEOBaseCombatWeapon::GetMaxRoundsInGun() const return GetMaxClip1() + ((HasReloadPhases() && GetNEOWpnData().m_bChamberedRound) ? 1 : 0); } +bool CNEOBaseCombatWeapon::HasChamberSlot() const +{ + return HasReloadPhases() && GetNEOWpnData().m_bChamberedRound; +} + +// The clip counts the chambered round, and the chamber is empty after the last shot until a dry reload's bolt +bool CNEOBaseCombatWeapon::IsRoundChambered() const +{ + return HasChamberSlot() && m_iClip1 > 0 && !m_bChamberEmpty; +} + +int CNEOBaseCombatWeapon::RoundsInMagazine() const +{ + return m_iClip1 - (IsRoundChambered() ? 1 : 0); +} + +int CNEOBaseCombatWeapon::MagazineCapacity() const +{ + return GetMaxClip1(); +} + void CNEOBaseCombatWeapon::CreditReloadPhase(ReloadPhase_t phase) { switch (phase) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 516e5c027a..58c2e5991b 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -266,6 +266,14 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon float GetPenetration() const; // the magazine plus the round a partial reload keeps in the chamber virtual int GetMaxRoundsInGun() const; + // the gun has a chamber that holds one round apart from the magazine (every gun with reload phases but the Kyla) + virtual bool HasChamberSlot() const; + // a live round is in the chamber + virtual bool IsRoundChambered() const; + // the rounds in the magazine (or tube), the chambered round not counted + virtual int RoundsInMagazine() const; + // how many rounds the magazine (or tube) holds + virtual int MagazineCapacity() const; void StopReloadOnDrop(); #ifdef CLIENT_DLL float m_flTemperature; diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index 32db4f67d7..d3a0aa6c5d 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -65,6 +65,10 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon bool CanBePickedUpByClass(int classId) OVERRIDE; // the tube plus the round in the chamber int GetMaxRoundsInGun() const OVERRIDE; + bool HasChamberSlot() const OVERRIDE { return true; } + bool IsRoundChambered() const OVERRIDE { return IsChamberLive(); } + int RoundsInMagazine() const OVERRIDE { return m_nTubeCount; } + int MagazineCapacity() const OVERRIDE { return TubeCapacity(); } bool Holster(CBaseCombatWeapon *pSwitchingTo) OVERRIDE; // What sits in the chamber: a live shell or slug, the hull of a fired one, or nothing From 4fc9bcbc0db5de854eb03530858a42beb21bd93b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:25:41 -0600 Subject: [PATCH 53/60] Show the chambered round on the ammo HUD A gun with a chamber draws its magazine row and, at the row's right end, one larger glyph for the chamber: lit while a live round is chambered, in the empty color otherwise. The row's overflow mark now counts the magazine alone. Number mode shows 29+1/30. --- game/neo/resource/ClientScheme.res | 52 +++++++++++++++++++++++++ src/game/client/neo/ui/neo_hud_ammo.cpp | 41 ++++++++++++++++--- src/game/client/neo/ui/neo_hud_ammo.h | 2 + 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 95680e932d..149bcacc93 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -2619,6 +2619,58 @@ Scheme "additive" "1" } } + // the chambered round on the ammo HUD, a size up from NHudBullets + NHudBulletsChamber + { + "1" + { + "name" "NOCR" + "tall" "48" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "480 599" + "additive" "1" + } + "2" + { + "name" "NOCR" + "tall" "50" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "600 767" + "additive" "1" + } + "3" + { + "name" "NOCR" + "tall" "55" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "768 1023" + "antialias" "1" + "additive" "1" + } + "4" + { + "name" "NOCR" + "tall" "62" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1024 1199" + "antialias" "1" + "additive" "1" + } + "5" + { + "name" "NOCR" + "tall" "85" + "weight" "0" + "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A + "yres" "1200 6000" + "antialias" "1" + "additive" "1" + } + } NHudNumbers { "1" diff --git a/src/game/client/neo/ui/neo_hud_ammo.cpp b/src/game/client/neo/ui/neo_hud_ammo.cpp index 994e536e0b..8777a24b8b 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.cpp +++ b/src/game/client/neo/ui/neo_hud_ammo.cpp @@ -75,6 +75,11 @@ void CNEOHud_Ammo::ApplySchemeSettings(vgui::IScheme* pScheme) m_hSmallTextFont = pScheme->GetFont("NHudOCRSmall"); m_hBulletFont = pScheme->GetFont("NHudBullets"); + m_hChamberFont = pScheme->GetFont("NHudBulletsChamber"); + if (!m_hChamberFont) + { + m_hChamberFont = m_hBulletFont; + } m_hTextFont = pScheme->GetFont("NHudOCR"); surface()->GetScreenSize(m_resX, m_resY); @@ -150,14 +155,16 @@ void CNEOHud_Ammo::DrawAmmo() const int fireModeWidth = 0, fireModeHeight = 0; int magSizeMax = 0; int magSizeCurrent = 0; + // a gun with a chamber draws the magazine row and, at its right end, the chambered round as its own glyph + const bool hasChamber = activeWep->HasChamberSlot(); if ((activeWep->UsesClipsForAmmo1() && !(activeWep->GetNeoWepBits() & NEO_WEP_THROWABLE)) || (activeWep->GetNeoWepBits() & NEO_WEP_BALC)) { char fireModeText[2]{ '\0' }; ammoChar = activeWep->GetNEOWpnData().szBulletCharacter; - magSizeMax = activeWep->GetMaxRoundsInGun(); - magSizeCurrent = activeWep->Clip1(); + magSizeMax = hasChamber ? activeWep->MagazineCapacity() : activeWep->GetMaxRoundsInGun(); + magSizeCurrent = hasChamber ? activeWep->RoundsInMagazine() : activeWep->Clip1(); if(activeWep) { @@ -206,16 +213,26 @@ void CNEOHud_Ammo::DrawAmmo() const surface()->DrawSetTextFont(m_hBulletFont); surface()->DrawSetTextPos(digit_xpos + xpos, digit_ypos + ypos); wchar_t bullets[22]; - V_swprintf_safe(bullets, L"%i/%i", magSizeCurrent, magSizeMax); - surface()->DrawPrintText(bullets, (int)(magSizeCurrent == 0 ? 1 : log10(magSizeCurrent) + 1) + (int)(log10(magSizeMax) + 1) + 1); + if (hasChamber && activeWep->IsRoundChambered()) + { + V_swprintf_safe(bullets, L"%i+1/%i", magSizeCurrent, magSizeMax); + } + else + { + V_swprintf_safe(bullets, L"%i/%i", magSizeCurrent, magSizeMax); + } + surface()->DrawPrintText(bullets, V_wcslen(bullets)); return; } if (ammoChar == nullptr) return; - const int maxSpaceAvailableForBullets = digit_max_width; const int bulletWidth = surface()->GetCharacterWidth(m_hBulletFont, *ammoChar); + // the chamber glyph sits half a glyph to the right of the magazine row, inside the same width + const int chamberGap = hasChamber ? bulletWidth / 2 : 0; + const int chamberWidth = hasChamber ? surface()->GetCharacterWidth(m_hChamberFont, *ammoChar) : 0; + const int maxSpaceAvailableForBullets = digit_max_width - chamberGap - chamberWidth; const int plusWidth = surface()->GetCharacterWidth(m_hBulletFont, '+'); const int maxBulletsWeCanDisplay = bulletWidth == 0 ? 0 : (maxSpaceAvailableForBullets / bulletWidth); @@ -224,6 +241,7 @@ void CNEOHud_Ammo::DrawAmmo() const const int maxBulletsWeCanDisplayWithPlus = bulletWidth == 0 ? 0 : ((maxSpaceAvailableForBullets - plusWidth) / bulletWidth); const bool bulletsOverflowing = maxBulletsWeCanDisplay < magSizeMax; + const int magSizeFull = magSizeMax; if(bulletsOverflowing) { @@ -248,7 +266,7 @@ void CNEOHud_Ammo::DrawAmmo() const if (magSizeMax > 0) bullets[magSizeMax - 1] = '+'; - if(activeWep->GetMaxRoundsInGun() == magSizeCurrent) + if(magSizeFull == magSizeCurrent) { magAmountToDrawFilled = magSizeMax; } else if(magSizeMax - 1 < magSizeCurrent) @@ -278,6 +296,17 @@ void CNEOHud_Ammo::DrawAmmo() const surface()->DrawPrintText(&unicodeBullets[magAmountToDrawFilled], magSizeMax - magAmountToDrawFilled); } } + + if (hasChamber) + { + // bottom-aligned with the row, lit while a live round is chambered + wchar_t chamberGlyph[2]{ static_cast(*ammoChar), L'\0' }; + const int chamberY = digit_ypos + ypos + surface()->GetFontTall(m_hBulletFont) - surface()->GetFontTall(m_hChamberFont); + surface()->DrawSetTextFont(m_hChamberFont); + surface()->DrawSetTextColor(activeWep->IsRoundChambered() ? ammo_color : emptied_ammo_color); + surface()->DrawSetTextPos(digit_xpos + xpos + bulletWidth * magSizeMax + chamberGap, chamberY); + surface()->DrawPrintText(chamberGlyph, 1); + } } void CNEOHud_Ammo::DrawNeoHudElement() diff --git a/src/game/client/neo/ui/neo_hud_ammo.h b/src/game/client/neo/ui/neo_hud_ammo.h index 7ea1901233..f38077839f 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.h +++ b/src/game/client/neo/ui/neo_hud_ammo.h @@ -33,6 +33,8 @@ class CNEOHud_Ammo : public CNEOHud_ChildElement, public CHudElement, public vgu vgui::HFont m_hSmallTextFont = 0; vgui::HFont m_hTextFont = 0; vgui::HFont m_hBulletFont = 0; + // the chambered round's glyph, a size up from the magazine's + vgui::HFont m_hChamberFont = 0; int m_resX, m_resY; int m_smallFontWidth, m_smallFontHeight; From 04619ed7cab4af66609c29f2ca0f1dc0643dc390 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:27:23 -0600 Subject: [PATCH 54/60] Let the trigger end a reload before the drop or after the seat A trigger press during a reload with phases, while a round can fire, ends the reload where it is and fires on the same tick: before the old magazine drops it keeps the partly empty magazine, after the fresh one is seated it keeps what was loaded. A dry reload before its bolt, and a gun whose magazine is out, ignore the press. Sound pieces of phases not reached stop; the first shot blends in with fire_blend where the model has it. One entry point, TryFireDuringReload, called from ItemBusyFrame; RELOAD_FIRE_INTERRUPTS turns it off. --- .../weapons/weapon_neobasecombatweapon.cpp | 98 +++++++++++++++++++ .../neo/weapons/weapon_neobasecombatweapon.h | 4 + 2 files changed, 102 insertions(+) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 6126bd74ab..a376d69458 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -640,6 +640,103 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() return true; } +// A trigger press can end a reload; turn this off and the trigger waits for the reload to end, as before +static constexpr bool RELOAD_FIRE_INTERRUPTS = true; +// The first shot after a cancelled reload, the shipped fire blended in from the reload pose instead of snapped +static constexpr const char *VM_SEQ_FIRE_BLEND = "fire_blend"; + +// The one way a trigger press acts on a reload in progress (the owner is busy, so it reaches only ItemBusyFrame): +// before the magazine drops, or once the fresh one is seated, the reload ends and the gun fires; +// a dry reload, or a gun whose magazine is out, ignores the press +bool CNEOBaseCombatWeapon::TryFireDuringReload() +{ + if (!RELOAD_FIRE_INTERRUPTS || !m_bInReload || m_nReloadTable < 0) + { + return false; + } + + CBasePlayer *pPlayer = ToBasePlayer(GetOwner()); + if (!pPlayer || !(pPlayer->m_afButtonPressed & IN_ATTACK) || ShootingIsPrevented()) + { + return false; + } + + if (m_nReloadPhase == RELOAD_PHASE_DROPPED || !HasRoundToFire()) + { + return false; + } + + CancelReloadAndFire(); + return true; +} + +// A round the trigger can fire now: a chambered one, or a revolver's with its cylinder closed +bool CNEOBaseCombatWeapon::HasRoundToFire() const +{ + if (HasChamberSlot()) + { + return IsRoundChambered(); + } + return m_iClip1 > 0 && m_nReloadPhase != RELOAD_PHASE_DROPPED; +} + +// End the reload where it is, with the phases credited so far, and fire on this tick +void CNEOBaseCombatWeapon::CancelReloadAndFire() +{ + StopPendingReloadSoundPieces(); + EndReloadGesture(); + m_bInReload = false; + m_nReloadTable = -1; + m_nReloadPhase = RELOAD_PHASE_NONE; + + CBaseCombatCharacter *pOwner = GetOwner(); + if (pOwner) + { + pOwner->SetNextAttack(gpGlobals->curtime); + } + m_flNextPrimaryAttack = m_flNextSecondaryAttack = gpGlobals->curtime; + SetWeaponIdleTime(gpGlobals->curtime); + m_bTriggerReset = true; + + const int iClipBefore = m_iClip1; + PrimaryAttack(); + + // the hand comes back onto the gun with the recoil, where the model has that blend + const int iFireBlend = LookupReloadSequence(VM_SEQ_FIRE_BLEND); + if (m_iClip1 < iClipBefore && m_iClip1 > 0 && iFireBlend >= 0) + { + SetSequence(iFireBlend); + SendViewModelAnim(iFireBlend); + } +} + +// Stop the sound pieces of the phases the reload has not reached (a piece can start before its phase's credit); +// the pieces of credited phases play out +void CNEOBaseCombatWeapon::StopPendingReloadSoundPieces() +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (!pOwner || m_nReloadTable < 0) + { + return; + } + + const ReloadPhaseTable_t &table = GetNEOWpnData().m_reloadTables[m_nReloadTable]; + for (int i = 0; i < table.iNumRows; ++i) + { + if (table.rows[i].phase <= m_nReloadPhase) + { + continue; + } + for (int j = 0; j < RELOAD_SOUNDS_PER_ROW; ++j) + { + if (table.rows[i].sounds[j].szName[0]) + { + StopSound(pOwner->entindex(), table.rows[i].sounds[j].szName); + } + } + } +} + // Credit every phase of the playing reload sequence whose time has come void CNEOBaseCombatWeapon::RunReloadPhases() { @@ -1208,6 +1305,7 @@ void CNEOBaseCombatWeapon::ItemBusyFrame(void) if (HasReloadPhases()) { RunReloadPhases(); + TryFireDuringReload(); return; } diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index 58c2e5991b..e858ae0e54 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -341,6 +341,10 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon int LookupReloadSequence(const char *pszName) const; float ReloadSequenceDuration(int iSequence) const; void EndReloadGesture(); + bool TryFireDuringReload(); + bool HasRoundToFire() const; + void CancelReloadAndFire(); + void StopPendingReloadSoundPieces(); }; From 4149c4ed5fdfc934391a85cecbc6db5dc4b0bd9c Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 19:30:16 -0600 Subject: [PATCH 55/60] Fire the chambered round while the magazine is out With sv_neo_reload_chamber_shot on, a trigger press while a partial reload has the magazine out plays fire_chamber_onehand: the gun flicks to the hold one-handed, the bullet fires at the table's fire_at, and the reload goes on dry from the insert. A switch during the flick cancels the shot. A model without the sequence, the SRS, and a gun with an empty chamber refuse the shot. The shot keeps the reload's timers, animation and gesture. The cvar defaults to 0 until every gun's model has the sequence; the weapon scripts carry the sequence's phase tables, generated from segments.csv. --- game/neo/scripts/weapon_aa13.txt | 6 + game/neo/scripts/weapon_jitte.txt | 6 + game/neo/scripts/weapon_jittescoped.txt | 6 + game/neo/scripts/weapon_m41.txt | 7 ++ game/neo/scripts/weapon_m41l.txt | 7 ++ game/neo/scripts/weapon_m41s.txt | 7 ++ game/neo/scripts/weapon_milso.txt | 6 + game/neo/scripts/weapon_mpn.txt | 6 + game/neo/scripts/weapon_mpn_unsilenced.txt | 6 + game/neo/scripts/weapon_mx.txt | 7 ++ game/neo/scripts/weapon_mx_silenced.txt | 7 ++ game/neo/scripts/weapon_pz.txt | 7 ++ game/neo/scripts/weapon_srm.txt | 6 + game/neo/scripts/weapon_srm_s.txt | 6 + game/neo/scripts/weapon_tachi.txt | 6 + game/neo/scripts/weapon_zr68c.txt | 7 ++ game/neo/scripts/weapon_zr68l.txt | 7 ++ game/neo/scripts/weapon_zr68s.txt | 7 ++ .../shared/neo/weapons/neo_weapon_parse.cpp | 1 + .../shared/neo/weapons/neo_weapon_parse.h | 1 + src/game/shared/neo/weapons/weapon_aa13.cpp | 13 +- .../weapons/weapon_neobasecombatweapon.cpp | 118 +++++++++++++++--- .../neo/weapons/weapon_neobasecombatweapon.h | 4 + 23 files changed, 235 insertions(+), 19 deletions(-) diff --git a/game/neo/scripts/weapon_aa13.txt b/game/neo/scripts/weapon_aa13.txt index 80966b0dbe..4d32c9570e 100644 --- a/game/neo/scripts/weapon_aa13.txt +++ b/game/neo/scripts/weapon_aa13.txt @@ -58,6 +58,12 @@ WeaponData { "seat" { "credit" "2.567" "sound" "weapon_aa13.reload_seat" "at" "2.105" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "2.200" "sound" "weapon_aa13.reload_seat" "at" "1.738" } + "bolt" { "credit" "3.167" "sound" "weapon_aa13.reload_bolt" "at" "2.597" } + } } "clip_size" "16" diff --git a/game/neo/scripts/weapon_jitte.txt b/game/neo/scripts/weapon_jitte.txt index 6e7480e834..95dd95420c 100644 --- a/game/neo/scripts/weapon_jitte.txt +++ b/game/neo/scripts/weapon_jitte.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.367" "sound" "weapon_jitte.reload_seat" "at" "1.000" } + "bolt" { "credit" "2.167" "sound" "weapon_jitte.reload_bolt" "at" "1.658" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_jittescoped.txt b/game/neo/scripts/weapon_jittescoped.txt index 1bc203eb39..da2cc5c093 100644 --- a/game/neo/scripts/weapon_jittescoped.txt +++ b/game/neo/scripts/weapon_jittescoped.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.567" "sound" "weapon_jitte.reload_seat" "at" "1.200" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.367" "sound" "weapon_jitte.reload_seat" "at" "1.000" } + "bolt" { "credit" "2.167" "sound" "weapon_jitte.reload_bolt" "at" "1.658" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_m41.txt b/game/neo/scripts/weapon_m41.txt index fa23e805d9..3d2e6eed5c 100644 --- a/game/neo/scripts/weapon_m41.txt +++ b/game/neo/scripts/weapon_m41.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.267" "sound" "weapon_m41.reload_seat" "at" "1.269" "sound2" "weapon_m41.reload_seat2" "at2" "1.825" } + "slap" { "credit" "2.433" "sound" "weapon_m41.reload_slap" "at" "2.478" } + "bolt" { "credit" "3.333" "sound" "weapon_m41.reload_bolt" "at" "3.133" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_m41l.txt b/game/neo/scripts/weapon_m41l.txt index 194b8d4998..afc551abe9 100644 --- a/game/neo/scripts/weapon_m41l.txt +++ b/game/neo/scripts/weapon_m41l.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.267" "sound" "weapon_m41.reload_seat" "at" "1.269" "sound2" "weapon_m41.reload_seat2" "at2" "1.825" } + "slap" { "credit" "2.433" "sound" "weapon_m41.reload_slap" "at" "2.478" } + "bolt" { "credit" "3.333" "sound" "weapon_m41.reload_bolt" "at" "3.133" } + } } "TPMuzzleFlashScale" "0.25" diff --git a/game/neo/scripts/weapon_m41s.txt b/game/neo/scripts/weapon_m41s.txt index b5c7883cb7..e40fd1a36f 100644 --- a/game/neo/scripts/weapon_m41s.txt +++ b/game/neo/scripts/weapon_m41s.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.533" "sound" "weapon_m41.reload_seat" "at" "1.535" "sound2" "weapon_m41.reload_seat2" "at2" "2.091" } "slap" { "credit" "2.700" "sound" "weapon_m41.reload_slap" "at" "2.744" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.267" "sound" "weapon_m41.reload_seat" "at" "1.269" "sound2" "weapon_m41.reload_seat2" "at2" "1.825" } + "slap" { "credit" "2.433" "sound" "weapon_m41.reload_slap" "at" "2.478" } + "bolt" { "credit" "3.333" "sound" "weapon_m41.reload_bolt" "at" "3.133" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_milso.txt b/game/neo/scripts/weapon_milso.txt index 50bd4e2d52..83cae37492 100644 --- a/game/neo/scripts/weapon_milso.txt +++ b/game/neo/scripts/weapon_milso.txt @@ -52,6 +52,12 @@ WeaponData "seat" { "credit" "1.500" "sound" "weapon_milso.reload_seat" "at" "1.576" } "bolt" { "credit" "2.400" "sound" "weapon_milso.reload_bolt" "at" "2.393" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.233" "sound" "weapon_milso.reload_seat" "at" "1.310" } + "bolt" { "credit" "2.133" "sound" "weapon_milso.reload_bolt" "at" "2.127" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn.txt b/game/neo/scripts/weapon_mpn.txt index 96d68fbc8e..1ff223bf16 100644 --- a/game/neo/scripts/weapon_mpn.txt +++ b/game/neo/scripts/weapon_mpn.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } } + "fire_chamber_onehand" + { + "fire_at" "0.200" + "seat" { "credit" "1.300" "sound" "weapon_mpn45.reload_seat" "at" "1.280" } + "bolt" { "credit" "2.633" "sound" "weapon_mpn45.reload_bolt" "at" "2.207" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mpn_unsilenced.txt b/game/neo/scripts/weapon_mpn_unsilenced.txt index 9067e627c0..c3340e9a9a 100644 --- a/game/neo/scripts/weapon_mpn_unsilenced.txt +++ b/game/neo/scripts/weapon_mpn_unsilenced.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.900" "sound" "weapon_mpn45.reload_seat" "at" "1.880" } } + "fire_chamber_onehand" + { + "fire_at" "0.200" + "seat" { "credit" "1.300" "sound" "weapon_mpn45.reload_seat" "at" "1.280" } + "bolt" { "credit" "2.633" "sound" "weapon_mpn45.reload_bolt" "at" "2.207" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_mx.txt b/game/neo/scripts/weapon_mx.txt index e34e2be4b5..8983315bf7 100644 --- a/game/neo/scripts/weapon_mx.txt +++ b/game/neo/scripts/weapon_mx.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.133" "sound" "weapon_mx.reload_seat" "at" "1.117" } + "slap" { "credit" "1.533" "sound" "weapon_mx.reload_slap" "at" "1.558" } + "bolt" { "credit" "2.333" "sound" "weapon_mx.reload_bolt" "at" "2.033" } + } } "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_mx_silenced.txt b/game/neo/scripts/weapon_mx_silenced.txt index a9ec2d62b8..7f2c54e969 100644 --- a/game/neo/scripts/weapon_mx_silenced.txt +++ b/game/neo/scripts/weapon_mx_silenced.txt @@ -67,6 +67,13 @@ WeaponData "seat" { "credit" "1.400" "sound" "weapon_mx.reload_seat" "at" "1.384" } "slap" { "credit" "1.800" "sound" "weapon_mx.reload_slap" "at" "1.825" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.133" "sound" "weapon_mx.reload_seat" "at" "1.117" } + "slap" { "credit" "1.533" "sound" "weapon_mx.reload_slap" "at" "1.558" } + "bolt" { "credit" "2.333" "sound" "weapon_mx.reload_bolt" "at" "2.033" } + } } "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_pz.txt b/game/neo/scripts/weapon_pz.txt index ec77fc235d..65279b6ef0 100644 --- a/game/neo/scripts/weapon_pz.txt +++ b/game/neo/scripts/weapon_pz.txt @@ -70,6 +70,13 @@ WeaponData "seat" { "credit" "2.767" "sound" "weapon_pz.reload_seat" "at" "2.349" } "slap" { "credit" "3.633" "sound" "weapon_pz.reload_slap" "at" "3.428" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "2.500" "sound" "weapon_pz.reload_seat" "at" "2.082" } + "slap" { "credit" "3.367" "sound" "weapon_pz.reload_slap" "at" "3.161" } + "bolt" { "credit" "4.067" "sound" "weapon_pz.reload_bolt" "at" "3.502" } + } } "TPMuzzleFlashScale" "0.3" diff --git a/game/neo/scripts/weapon_srm.txt b/game/neo/scripts/weapon_srm.txt index d6f1b764b1..5076fa1e5c 100644 --- a/game/neo/scripts/weapon_srm.txt +++ b/game/neo/scripts/weapon_srm.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.333" "sound" "weapon_srm.reload_seat" "at" "1.234" } + "bolt" { "credit" "2.500" "sound" "weapon_srm.reload_bolt" "at" "2.137" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_srm_s.txt b/game/neo/scripts/weapon_srm_s.txt index 6d9c10753d..d67a86f761 100644 --- a/game/neo/scripts/weapon_srm_s.txt +++ b/game/neo/scripts/weapon_srm_s.txt @@ -53,6 +53,12 @@ WeaponData { "seat" { "credit" "1.600" "sound" "weapon_srm.reload_seat" "at" "1.501" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.333" "sound" "weapon_srm.reload_seat" "at" "1.234" } + "bolt" { "credit" "2.500" "sound" "weapon_srm.reload_bolt" "at" "2.137" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_tachi.txt b/game/neo/scripts/weapon_tachi.txt index d50db1b720..d0a5336aff 100644 --- a/game/neo/scripts/weapon_tachi.txt +++ b/game/neo/scripts/weapon_tachi.txt @@ -55,6 +55,12 @@ WeaponData "seat" { "credit" "1.633" "sound" "weapon_tachi.reload_seat" "at" "1.880" } "bolt" { "credit" "2.533" "sound" "weapon_tachi.reload_bolt" "at" "2.527" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.367" "sound" "weapon_tachi.reload_seat" "at" "1.613" } + "bolt" { "credit" "2.267" "sound" "weapon_tachi.reload_bolt" "at" "2.260" } + } } "clip_size" "15" diff --git a/game/neo/scripts/weapon_zr68c.txt b/game/neo/scripts/weapon_zr68c.txt index 7834324e80..fea659d5e3 100644 --- a/game/neo/scripts/weapon_zr68c.txt +++ b/game/neo/scripts/weapon_zr68c.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.167" "sound" "weapon_zr68.reload_seat" "at" "1.127" } + "slap" { "credit" "1.533" "sound" "weapon_zr68.reload_slap" "at" "1.562" } + "bolt" { "credit" "2.100" "sound" "weapon_zr68.reload_bolt" "at" "1.862" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68l.txt b/game/neo/scripts/weapon_zr68l.txt index da14b16fa2..3dbe10cf2c 100644 --- a/game/neo/scripts/weapon_zr68l.txt +++ b/game/neo/scripts/weapon_zr68l.txt @@ -69,6 +69,13 @@ WeaponData "seat" { "credit" "1.533" "sound" "weapon_zr68l.reload_seat" "at" "1.535" } "slap" { "credit" "1.900" "sound" "weapon_zr68l.reload_slap" "at" "1.891" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.267" "sound" "weapon_zr68l.reload_seat" "at" "1.268" } + "slap" { "credit" "1.633" "sound" "weapon_zr68l.reload_slap" "at" "1.624" } + "bolt" { "credit" "2.200" "sound" "weapon_zr68l.reload_bolt" "at" "1.929" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/game/neo/scripts/weapon_zr68s.txt b/game/neo/scripts/weapon_zr68s.txt index b6185deaff..56adc700a1 100644 --- a/game/neo/scripts/weapon_zr68s.txt +++ b/game/neo/scripts/weapon_zr68s.txt @@ -71,6 +71,13 @@ WeaponData "seat" { "credit" "1.433" "sound" "weapon_zr68.reload_seat" "at" "1.394" } "slap" { "credit" "1.800" "sound" "weapon_zr68.reload_slap" "at" "1.829" } } + "fire_chamber_onehand" + { + "fire_at" "0.133" + "seat" { "credit" "1.167" "sound" "weapon_zr68.reload_seat" "at" "1.127" } + "slap" { "credit" "1.533" "sound" "weapon_zr68.reload_slap" "at" "1.562" } + "bolt" { "credit" "2.100" "sound" "weapon_zr68.reload_bolt" "at" "1.862" } + } } "TPMuzzleFlashScale" "0.5" diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index 42f32c0980..302f9d419c 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -61,6 +61,7 @@ void CNEOWeaponInfo::ParseReloadPhases(KeyValues *pBlock) { ReloadPhaseTable_t &table = m_reloadTables[m_iNumReloadTables++]; V_strncpy(table.szSequence, pSeq->GetName(), sizeof(table.szSequence)); + table.flFireAt = pSeq->GetFloat("fire_at"); table.iNumRows = 0; for (KeyValues *pRow = pSeq->GetFirstTrueSubKey(); pRow && table.iNumRows < RELOAD_PHASE_ROWS_MAX; pRow = pRow->GetNextTrueSubKey()) diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index 9194104f9d..d0fa21eae4 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -53,6 +53,7 @@ struct ReloadPhaseRow_t struct ReloadPhaseTable_t { char szSequence[RELOAD_SEQUENCE_NAME_MAX]; + float flFireAt; // a chambered-round shot's sequence: when the bullet fires, seconds from its start; 0 otherwise int iNumRows; ReloadPhaseRow_t rows[RELOAD_PHASE_ROWS_MAX]; }; diff --git a/src/game/shared/neo/weapons/weapon_aa13.cpp b/src/game/shared/neo/weapons/weapon_aa13.cpp index f1e8e1cc72..d223efc5df 100644 --- a/src/game/shared/neo/weapons/weapon_aa13.cpp +++ b/src/game/shared/neo/weapons/weapon_aa13.cpp @@ -93,12 +93,19 @@ void CWeaponAA13::PrimaryAttack(void) #endif pPlayer->DoMuzzleFlash(); - SendWeaponAnim(ACT_VM_PRIMARYATTACK); + // a chambered-round shot plays inside its reload sequence + if (!m_bInReload) + { + SendWeaponAnim(ACT_VM_PRIMARYATTACK); + } m_iClip1 -= 1; - // player "shoot" animation - pPlayer->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); + // player "shoot" animation (a chambered-round shot keeps the reload gesture) + if (!m_bInReload) + { + pPlayer->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); + } Vector vecSrc = pPlayer->Weapon_ShootPosition(); Vector vecAiming = pPlayer->GetAutoaimVector(AUTOAIM_10DEGREES); diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index a376d69458..b8db3fdfcb 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -48,6 +48,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) RecvPropInt(RECVINFO(m_nReloadTable)), RecvPropTime(RECVINFO(m_flReloadStart)), RecvPropInt(RECVINFO(m_nReloadSoundsDone)), + RecvPropBool(RECVINFO(m_bChamberShotPending)), RecvPropInt(RECVINFO(m_spawnflags)), #else SendPropTime(SENDINFO(m_flSoonestAttack)), @@ -62,6 +63,7 @@ BEGIN_NETWORK_TABLE( CNEOBaseCombatWeapon, DT_NEOBaseCombatWeapon ) SendPropInt(SENDINFO(m_nReloadTable), RELOAD_PHASE_TABLE_BITS), SendPropTime(SENDINFO(m_flReloadStart)), SendPropInt(SENDINFO(m_nReloadSoundsDone), RELOAD_PHASE_ROWS_MAX * RELOAD_SOUNDS_PER_ROW, SPROP_UNSIGNED), + SendPropBool(SENDINFO(m_bChamberShotPending)), SendPropInt(SENDINFO(m_spawnflags)), SendPropExclude("DT_BaseAnimating", "m_nSequence"), #endif @@ -81,6 +83,7 @@ BEGIN_PREDICTION_DATA(CNEOBaseCombatWeapon) DEFINE_PRED_FIELD(m_nReloadTable, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flReloadStart, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_nReloadSoundsDone, FIELD_INTEGER, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_bChamberShotPending, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -100,6 +103,7 @@ BEGIN_DATADESC( CNEOBaseCombatWeapon ) DEFINE_FIELD(m_nReloadTable, FIELD_INTEGER), DEFINE_FIELD(m_flReloadStart, FIELD_TIME), DEFINE_FIELD(m_nReloadSoundsDone, FIELD_INTEGER), + DEFINE_FIELD(m_bChamberShotPending, FIELD_BOOLEAN), END_DATADESC() #endif @@ -263,6 +267,7 @@ CNEOBaseCombatWeapon::CNEOBaseCombatWeapon( void ) m_nReloadTable = -1; m_flReloadStart = 0.0f; m_nReloadSoundsDone = 0; + m_bChamberShotPending = false; } const CNEOWeaponInfo &CNEOBaseCombatWeapon::GetNEOWpnData() const @@ -642,12 +647,17 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() // A trigger press can end a reload; turn this off and the trigger waits for the reload to end, as before static constexpr bool RELOAD_FIRE_INTERRUPTS = true; +// A trigger press while the magazine is out fires the chambered round with the one-handed fire_chamber_onehand +// sequence; off by default until every gun's model has that sequence (a model without it refuses the shot) +ConVar sv_neo_reload_chamber_shot("sv_neo_reload_chamber_shot", "0", FCVAR_REPLICATED, + "A trigger press while the magazine is out of a reloading gun fires the chambered round"); +static constexpr const char *VM_SEQ_FIRE_CHAMBER_ONEHAND = "fire_chamber_onehand"; // The first shot after a cancelled reload, the shipped fire blended in from the reload pose instead of snapped static constexpr const char *VM_SEQ_FIRE_BLEND = "fire_blend"; // The one way a trigger press acts on a reload in progress (the owner is busy, so it reaches only ItemBusyFrame): // before the magazine drops, or once the fresh one is seated, the reload ends and the gun fires; -// a dry reload, or a gun whose magazine is out, ignores the press +// with the magazine out, the chambered round fires one-handed and the reload goes on dry; a dry reload ignores it bool CNEOBaseCombatWeapon::TryFireDuringReload() { if (!RELOAD_FIRE_INTERRUPTS || !m_bInReload || m_nReloadTable < 0) @@ -661,7 +671,12 @@ bool CNEOBaseCombatWeapon::TryFireDuringReload() return false; } - if (m_nReloadPhase == RELOAD_PHASE_DROPPED || !HasRoundToFire()) + if (m_nReloadPhase == RELOAD_PHASE_DROPPED) + { + return StartChamberShot(); + } + + if (!HasRoundToFire()) { return false; } @@ -670,6 +685,58 @@ bool CNEOBaseCombatWeapon::TryFireDuringReload() return true; } +// With the magazine out, the shot's sequence flicks the gun to the hold one-handed and goes on with a dry reload +// from the insert; the bullet fires when the gun reaches the hold (the table's fire_at) +bool CNEOBaseCombatWeapon::StartChamberShot() +{ + if (!sv_neo_reload_chamber_shot.GetBool() || m_bChamberShotPending || !IsRoundChambered()) + { + return false; + } + + // a bolt action works its bolt from its own attack code + if (GetNeoWepBits() & NEO_WEP_SRS) + { + return false; + } + + const int iTable = GetNEOWpnData().FindReloadTable(VM_SEQ_FIRE_CHAMBER_ONEHAND); + const int iSequence = (iTable >= 0) ? LookupReloadSequence(VM_SEQ_FIRE_CHAMBER_ONEHAND) : -1; + if (iSequence < 0 || GetNEOWpnData().m_reloadTables[iTable].flFireAt <= 0.0f) + { + return false; + } + + StopPendingReloadSoundPieces(); + // the round about to fire leaves the chamber empty: the rest of the reload works the bolt + m_bReloadDry = true; + m_bChamberShotPending = true; + StartReloadSequence(iTable, iSequence, ResumeGestureCycle()); + return true; +} + +// The chambered round's bullet, fired from the reload: no fire animation or gesture, the reload's timers kept +void CNEOBaseCombatWeapon::FireChamberShot() +{ + m_bChamberShotPending = false; + + const float flNextPrimary = m_flNextPrimaryAttack; + const float flNextSecondary = m_flNextSecondaryAttack; + const float flIdle = m_flTimeWeaponIdle; + m_flNextPrimaryAttack = gpGlobals->curtime; + m_bTriggerReset = true; + PrimaryAttack(); + m_flNextPrimaryAttack = flNextPrimary; + m_flNextSecondaryAttack = flNextSecondary; + m_flTimeWeaponIdle = flIdle; + + // the chamber is empty once its round is gone (the AA13 fires through its own attack code) + if (m_iClip1 <= 0) + { + m_bChamberEmpty = true; + } +} + // A round the trigger can fire now: a chambered one, or a revolver's with its cylinder closed bool CNEOBaseCombatWeapon::HasRoundToFire() const { @@ -748,6 +815,11 @@ void CNEOBaseCombatWeapon::RunReloadPhases() const ReloadPhaseTable_t &table = GetNEOWpnData().m_reloadTables[m_nReloadTable]; const float flElapsed = gpGlobals->curtime - m_flReloadStart; + if (m_bChamberShotPending && flElapsed >= table.flFireAt) + { + FireChamberShot(); + } + // Sound pieces play on their own times, before or after their phase's credit // (the pieces of phases credited before this sequence started are marked done when it starts) for (int i = 0; i < table.iNumRows; ++i) @@ -960,6 +1032,7 @@ void CNEOBaseCombatWeapon::StopReloadOnDrop() if (HasReloadPhases()) { StopReloadSoundPieces(); + m_bChamberShotPending = false; if (m_bInReload) { EndReloadGesture(); @@ -1233,6 +1306,12 @@ bool CNEOBaseCombatWeapon::Holster(CBaseCombatWeapon* pSwitchingTo) { EndReloadGesture(); } + // a switch during the flick cancels the chambered-round shot: the round stays, the reload is partial again + if (m_bChamberShotPending) + { + m_bChamberShotPending = false; + m_bReloadDry = IsReloadDry(); + } if (!IsReloadPending()) { m_nReloadPhase = RELOAD_PHASE_NONE; @@ -1766,8 +1845,11 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) m_bTriggerReset = false; } - // player "shoot" animation - pPlayer->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); + // player "shoot" animation (a chambered-round shot keeps the reload gesture) + if (!m_bInReload) + { + pPlayer->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); + } FireBulletsInfo_t info; info.m_vecSrc = pPlayer->Weapon_ShootPosition(); @@ -1817,20 +1899,24 @@ void CNEOBaseCombatWeapon::PrimaryAttack(void) } } - // the last round's shot locks a pistol's slide back: - // the pistols' dry fire sequence is a shot's kick with the slide held back, - if (UsesClipsForAmmo1() && m_iClip1 <= 0 && (GetNeoWepBits() & NEO_WEP_SLIDE_LOCK)) - { - SendWeaponAnim(ACT_VM_DRYFIRE); - } - else + // a chambered-round shot plays inside its reload sequence + if (!m_bInReload) { - SendWeaponAnim(GetPrimaryAttackActivity()); - } + // the last round's shot locks a pistol's slide back: + // the pistols' dry fire sequence is a shot's kick with the slide held back, + if (UsesClipsForAmmo1() && m_iClip1 <= 0 && (GetNeoWepBits() & NEO_WEP_SLIDE_LOCK)) + { + SendWeaponAnim(ACT_VM_DRYFIRE); + } + else + { + SendWeaponAnim(GetPrimaryAttackActivity()); + } - // and goes straight on to the empty idle when it ends - const bool bEmptyIdleNext = UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle(); - SetWeaponIdleTime(gpGlobals->curtime + (bEmptyIdleNext ? SequenceDuration() : 2.0)); + // and goes straight on to the empty idle when it ends + const bool bEmptyIdleNext = UsesClipsForAmmo1() && m_iClip1 <= 0 && HasEmptyIdle(); + SetWeaponIdleTime(gpGlobals->curtime + (bEmptyIdleNext ? SequenceDuration() : 2.0)); + } info.m_flDistance = MAX_TRACE_LENGTH; info.m_iAmmoType = m_iPrimaryAmmoType; diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h index e858ae0e54..931e3a9e97 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.h @@ -317,6 +317,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon CNetworkVar(float, m_flReloadStart); // the sound pieces of the playing sequence already started, one bit per phase row CNetworkVar(int, m_nReloadSoundsDone); + // a chambered-round shot is under way and its bullet has not fired yet + CNetworkVar(bool, m_bChamberShotPending); private: CNEOBaseCombatWeapon(const CNEOBaseCombatWeapon &other); @@ -345,6 +347,8 @@ class CNEOBaseCombatWeapon : public CBaseHL2MPCombatWeapon bool HasRoundToFire() const; void CancelReloadAndFire(); void StopPendingReloadSoundPieces(); + bool StartChamberShot(); + void FireChamberShot(); }; From f047c762d7301c90246217f70a2edde49e4df415 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 20:18:48 -0600 Subject: [PATCH 56/60] Show the chamber on the fire-mode glyph's round icon The large chamber glyph at the row's end crowded the ammo counter. The fire-mode glyph already carries a small round icon (a bullet, or a shell on the Supa7) above its letter; it now shows the chamber instead: the filled color while a round is chambered, the empty color otherwise, with the letter in the text color as before. The row keeps counting the magazine alone. Guns without a chamber slot are drawn as before. --- game/neo/resource/ClientScheme.res | 52 ------------------- src/game/client/neo/ui/neo_hud_ammo.cpp | 66 ++++++++++++++++--------- src/game/client/neo/ui/neo_hud_ammo.h | 3 +- 3 files changed, 44 insertions(+), 77 deletions(-) diff --git a/game/neo/resource/ClientScheme.res b/game/neo/resource/ClientScheme.res index 149bcacc93..95680e932d 100644 --- a/game/neo/resource/ClientScheme.res +++ b/game/neo/resource/ClientScheme.res @@ -2619,58 +2619,6 @@ Scheme "additive" "1" } } - // the chambered round on the ammo HUD, a size up from NHudBullets - NHudBulletsChamber - { - "1" - { - "name" "NOCR" - "tall" "48" - "weight" "0" - "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "480 599" - "additive" "1" - } - "2" - { - "name" "NOCR" - "tall" "50" - "weight" "0" - "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "600 767" - "additive" "1" - } - "3" - { - "name" "NOCR" - "tall" "55" - "weight" "0" - "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "768 1023" - "antialias" "1" - "additive" "1" - } - "4" - { - "name" "NOCR" - "tall" "62" - "weight" "0" - "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "1024 1199" - "antialias" "1" - "additive" "1" - } - "5" - { - "name" "NOCR" - "tall" "85" - "weight" "0" - "range" "0x0000 0x017F" // Basic Latin, Latin-1 Supplement, Latin Extended-A - "yres" "1200 6000" - "antialias" "1" - "additive" "1" - } - } NHudNumbers { "1" diff --git a/src/game/client/neo/ui/neo_hud_ammo.cpp b/src/game/client/neo/ui/neo_hud_ammo.cpp index 8777a24b8b..0768863a2d 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.cpp +++ b/src/game/client/neo/ui/neo_hud_ammo.cpp @@ -6,6 +6,9 @@ #include "iclientmode.h" #include #include +#include +#include "VGuiMatSurface/IMatSystemSurface.h" +#include "cdll_client_int.h" #include #include #include @@ -75,11 +78,6 @@ void CNEOHud_Ammo::ApplySchemeSettings(vgui::IScheme* pScheme) m_hSmallTextFont = pScheme->GetFont("NHudOCRSmall"); m_hBulletFont = pScheme->GetFont("NHudBullets"); - m_hChamberFont = pScheme->GetFont("NHudBulletsChamber"); - if (!m_hChamberFont) - { - m_hChamberFont = m_hBulletFont; - } m_hTextFont = pScheme->GetFont("NHudOCR"); surface()->GetScreenSize(m_resX, m_resY); @@ -155,7 +153,7 @@ void CNEOHud_Ammo::DrawAmmo() const int fireModeWidth = 0, fireModeHeight = 0; int magSizeMax = 0; int magSizeCurrent = 0; - // a gun with a chamber draws the magazine row and, at its right end, the chambered round as its own glyph + // a gun with a chamber counts the magazine in the row, and the round icon of the fire-mode glyph shows the chamber const bool hasChamber = activeWep->HasChamberSlot(); if ((activeWep->UsesClipsForAmmo1() && !(activeWep->GetNeoWepBits() & NEO_WEP_THROWABLE)) || (activeWep->GetNeoWepBits() & NEO_WEP_BALC)) @@ -183,10 +181,16 @@ void CNEOHud_Ammo::DrawAmmo() const g_pVGuiLocalize->ConvertANSIToUnicode(fireModeText, unicodeFireModeText, sizeof(unicodeFireModeText)); surface()->DrawSetTextFont(m_hBulletFont); - surface()->DrawSetTextPos(icon_xpos + xpos, icon_ypos + ypos); - surface()->DrawPrintText(unicodeFireModeText, V_wcslen(unicodeFireModeText)); - surface()->GetTextSize(m_hBulletFont, unicodeFireModeText, fireModeWidth, fireModeHeight); + if (hasChamber) + { + DrawFireModeWithChamber(unicodeFireModeText, fireModeHeight, activeWep->IsRoundChambered()); + } + else + { + surface()->DrawSetTextPos(icon_xpos + xpos, icon_ypos + ypos); + surface()->DrawPrintText(unicodeFireModeText, V_wcslen(unicodeFireModeText)); + } } } else { @@ -228,11 +232,8 @@ void CNEOHud_Ammo::DrawAmmo() const if (ammoChar == nullptr) return; + const int maxSpaceAvailableForBullets = digit_max_width; const int bulletWidth = surface()->GetCharacterWidth(m_hBulletFont, *ammoChar); - // the chamber glyph sits half a glyph to the right of the magazine row, inside the same width - const int chamberGap = hasChamber ? bulletWidth / 2 : 0; - const int chamberWidth = hasChamber ? surface()->GetCharacterWidth(m_hChamberFont, *ammoChar) : 0; - const int maxSpaceAvailableForBullets = digit_max_width - chamberGap - chamberWidth; const int plusWidth = surface()->GetCharacterWidth(m_hBulletFont, '+'); const int maxBulletsWeCanDisplay = bulletWidth == 0 ? 0 : (maxSpaceAvailableForBullets / bulletWidth); @@ -296,17 +297,36 @@ void CNEOHud_Ammo::DrawAmmo() const surface()->DrawPrintText(&unicodeBullets[magAmountToDrawFilled], magSizeMax - magAmountToDrawFilled); } } +} - if (hasChamber) - { - // bottom-aligned with the row, lit while a live round is chambered - wchar_t chamberGlyph[2]{ static_cast(*ammoChar), L'\0' }; - const int chamberY = digit_ypos + ypos + surface()->GetFontTall(m_hBulletFont) - surface()->GetFontTall(m_hChamberFont); - surface()->DrawSetTextFont(m_hChamberFont); - surface()->DrawSetTextColor(activeWep->IsRoundChambered() ? ammo_color : emptied_ammo_color); - surface()->DrawSetTextPos(digit_xpos + xpos + bulletWidth * magSizeMax + chamberGap, chamberY); - surface()->DrawPrintText(chamberGlyph, 1); - } +// The fire-mode glyph's top rows are a round icon (bullet or shell), its letter sits below them +static constexpr float FIRE_MODE_ROUND_ICON_HEIGHT = 0.184f; + +// Draw the fire-mode glyph with its round icon lit while a round is chambered, in the empty color otherwise; +// the letter keeps the text color. Two passes, each clipped to its part of the glyph +void CNEOHud_Ammo::DrawFireModeWithChamber(const wchar_t *pszGlyph, int iGlyphTall, bool bChambered) const +{ + const int iX = icon_xpos + xpos; + const int iY = icon_ypos + ypos; + int iScreenX = 0, iScreenY = 0; + vgui::ipanel()->GetAbsPos(GetVPanel(), iScreenX, iScreenY); + const int iSplitY = iScreenY + iY + static_cast(iGlyphTall * FIRE_MODE_ROUND_ICON_HEIGHT + 0.5f); + + int iLeft, iTop, iRight, iBottom; + bool bClippingDisabled; + g_pMatSystemSurface->GetClippingRect(iLeft, iTop, iRight, iBottom, bClippingDisabled); + + g_pMatSystemSurface->SetClippingRect(iLeft, iTop, iRight, Min(iBottom, iSplitY)); + surface()->DrawSetTextColor(bChambered ? ammo_color : emptied_ammo_color); + surface()->DrawSetTextPos(iX, iY); + surface()->DrawPrintText(pszGlyph, V_wcslen(pszGlyph)); + + g_pMatSystemSurface->SetClippingRect(iLeft, Max(iTop, iSplitY), iRight, iBottom); + surface()->DrawSetTextColor(ammo_text_color); + surface()->DrawSetTextPos(iX, iY); + surface()->DrawPrintText(pszGlyph, V_wcslen(pszGlyph)); + + g_pMatSystemSurface->SetClippingRect(iLeft, iTop, iRight, iBottom); } void CNEOHud_Ammo::DrawNeoHudElement() diff --git a/src/game/client/neo/ui/neo_hud_ammo.h b/src/game/client/neo/ui/neo_hud_ammo.h index f38077839f..742eebf925 100644 --- a/src/game/client/neo/ui/neo_hud_ammo.h +++ b/src/game/client/neo/ui/neo_hud_ammo.h @@ -27,14 +27,13 @@ class CNEOHud_Ammo : public CNEOHud_ChildElement, public CHudElement, public vgu private: void DrawAmmo() const; + void DrawFireModeWithChamber(const wchar_t *pszGlyph, int iGlyphTall, bool bChambered) const; void DrawHeatMeter(C_NEOBaseCombatWeapon* activeWep) const; private: vgui::HFont m_hSmallTextFont = 0; vgui::HFont m_hTextFont = 0; vgui::HFont m_hBulletFont = 0; - // the chambered round's glyph, a size up from the magazine's - vgui::HFont m_hChamberFont = 0; int m_resX, m_resY; int m_smallFontWidth, m_smallFontHeight; From 00fe54257c63fd6b365da516e7db83fd4325bf35 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 20:27:06 -0600 Subject: [PATCH 57/60] Fire the chambered round by default Every gun with a chamber now has its fire_chamber_onehand sequence, so sv_neo_reload_chamber_shot defaults to 1. --- src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index b8db3fdfcb..7950bc60c6 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -648,8 +648,8 @@ bool CNEOBaseCombatWeapon::ReloadWithPhases() // A trigger press can end a reload; turn this off and the trigger waits for the reload to end, as before static constexpr bool RELOAD_FIRE_INTERRUPTS = true; // A trigger press while the magazine is out fires the chambered round with the one-handed fire_chamber_onehand -// sequence; off by default until every gun's model has that sequence (a model without it refuses the shot) -ConVar sv_neo_reload_chamber_shot("sv_neo_reload_chamber_shot", "0", FCVAR_REPLICATED, +// sequence (a model without it refuses the shot) +ConVar sv_neo_reload_chamber_shot("sv_neo_reload_chamber_shot", "1", FCVAR_REPLICATED, "A trigger press while the magazine is out of a reloading gun fires the chambered round"); static constexpr const char *VM_SEQ_FIRE_CHAMBER_ONEHAND = "fire_chamber_onehand"; // The first shot after a cancelled reload, the shipped fire blended in from the reload pose instead of snapped From 802488cdba74bfa15971f5b25e44453950655f04 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 20:36:03 -0600 Subject: [PATCH 58/60] SRS: show the spent case in the chamber until the bolt After a shot the SRS chamber holds a spent case until the bolt cycle brings the next round home, so the chamber icon dims at the shot and lights when the bolt is home, with the magazine row dropping by one then instead of at the shot. The moment comes from a "charge" phase table in the weapon script (the Bolt bone home in the charge sequence, frame 25). A quick switch or a reload cancels the bolt cycle as before and the round counts as chambered. Ammo and fire timing are unchanged. --- game/neo/scripts/weapon_srs.txt | 5 +++++ src/game/shared/neo/weapons/weapon_srs.cpp | 24 ++++++++++++++++++++++ src/game/shared/neo/weapons/weapon_srs.h | 4 ++++ 3 files changed, 33 insertions(+) diff --git a/game/neo/scripts/weapon_srs.txt b/game/neo/scripts/weapon_srs.txt index 8e2c48d37d..74d3d9c5b8 100644 --- a/game/neo/scripts/weapon_srs.txt +++ b/game/neo/scripts/weapon_srs.txt @@ -54,6 +54,11 @@ WeaponData { "seat" { "credit" "1.567" "sound" "weapon_srs.reload_seat" "at" "1.411" } } + // the bolt cycle after a shot: "bolt" is when the bolt is home with the next round (Bolt bone, charge f25) + "charge" + { + "bolt" { "credit" "0.833" } + } } "TPMuzzleFlashScale" "0.25" diff --git a/src/game/shared/neo/weapons/weapon_srs.cpp b/src/game/shared/neo/weapons/weapon_srs.cpp index ae21ebf7ee..ec5f171da8 100644 --- a/src/game/shared/neo/weapons/weapon_srs.cpp +++ b/src/game/shared/neo/weapons/weapon_srs.cpp @@ -10,8 +10,10 @@ BEGIN_NETWORK_TABLE(CWeaponSRS, DT_WeaponSRS) DEFINE_NEO_BASE_WEP_NETWORK_TABLE #ifdef CLIENT_DLL RecvPropBool(RECVINFO(m_bNeedsBolting)), + RecvPropTime(RECVINFO(m_flBoltHomeTime)), #else SendPropBool(SENDINFO(m_bNeedsBolting)), + SendPropTime(SENDINFO(m_flBoltHomeTime)), #endif END_NETWORK_TABLE() @@ -19,6 +21,7 @@ END_NETWORK_TABLE() BEGIN_PREDICTION_DATA(CWeaponSRS) DEFINE_NEO_BASE_WEP_PREDICTION DEFINE_PRED_FIELD(m_bNeedsBolting, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flBoltHomeTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -28,6 +31,9 @@ LINK_ENTITY_TO_CLASS(weapon_srs, CWeaponSRS); PRECACHE_WEAPON_REGISTER(weapon_srs); +// The bolt cycle after a shot; its phase table in the weapon script holds the moment the bolt is home +static constexpr const char *VM_SEQ_CHARGE = "charge"; + CWeaponSRS::CWeaponSRS() { m_flSoonestAttack = gpGlobals->curtime; @@ -35,6 +41,7 @@ CWeaponSRS::CWeaponSRS() m_nNumShotsFired = 0; m_bNeedsBolting = false; + m_flBoltHomeTime = 0.0f; m_weaponSeeds = { "srspx", @@ -83,6 +90,12 @@ void CWeaponSRS::ItemPreFrame() SendWeaponAnim(ACT_VM_PULLBACK); WeaponSound(SPECIAL1); m_bNeedsBolting = false; + + // the script's "charge" table gives when the bolt is home with the next round + const CNEOWeaponInfo &info = GetNEOWpnData(); + const int iTable = info.FindReloadTable(VM_SEQ_CHARGE); + const bool bHasCredit = iTable >= 0 && info.m_reloadTables[iTable].iNumRows > 0; + m_flBoltHomeTime = gpGlobals->curtime + (bHasCredit ? info.m_reloadTables[iTable].rows[0].flCredit : 0.0f); } BaseClass::ItemPreFrame(); @@ -93,6 +106,7 @@ bool CWeaponSRS::Deploy() // This is on purpose; the player is allowed to cancel the bolting animation // with a quick-switch input. m_bNeedsBolting = false; + m_flBoltHomeTime = 0.0f; return BaseClass::Deploy(); } @@ -102,10 +116,20 @@ bool CWeaponSRS::Reload() // The reloading animation already contains the bolting motion; // we don't want to bolt twice. m_bNeedsBolting = false; + m_flBoltHomeTime = 0.0f; return BaseClass::Reload(); } +bool CWeaponSRS::IsRoundChambered() const +{ + if (m_bNeedsBolting || gpGlobals->curtime < m_flBoltHomeTime) + { + return false; + } + return BaseClass::IsRoundChambered(); +} + bool CWeaponSRS::CanBePickedUpByClass(int classId) { return classId != NEO_CLASS_JUGGERNAUT; diff --git a/src/game/shared/neo/weapons/weapon_srs.h b/src/game/shared/neo/weapons/weapon_srs.h index b6c632f153..285d488799 100644 --- a/src/game/shared/neo/weapons/weapon_srs.h +++ b/src/game/shared/neo/weapons/weapon_srs.h @@ -46,6 +46,8 @@ class CWeaponSRS : public CNEOBaseCombatWeapon bool CanBePickedUpByClass(int classId) OVERRIDE; inline bool GetNeedsBolting() const { return m_bNeedsBolting; } + // after a shot the chamber holds a spent case until the bolt brings the next round home + bool IsRoundChambered() const override; protected: virtual float GetFastestDryRefireTime() const OVERRIDE { return 0.2f; } @@ -54,6 +56,8 @@ class CWeaponSRS : public CNEOBaseCombatWeapon CWeaponSRS(const CWeaponSRS &other); CNetworkVar(bool, m_bNeedsBolting); + // when the bolt cycle in progress chambers the next round, 0 when none is in progress + CNetworkVar(float, m_flBoltHomeTime); }; #endif // NEO_WEAPON_SRS_H From a531257225ebf3eb2b14589422ad41a229413c8b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 20:41:12 -0600 Subject: [PATCH 59/60] Supa7: show the fed round in the tube until the pump is home The shot already racks the pump in the gun's state; the HUD now shows the chamber dim and the fed round still in the tube until the fire animation's pump is home, never later than the next shot is allowed. The pump moments come from the weapon script (ChamberAfterShot 0.600 for a shell, ChamberAfterSlugShot 1.000 for a slug: the Pump bone home in altfire f18 and fire f30). Ammo and fire timing are unchanged. --- game/neo/scripts/weapon_supa7.txt | 3 +++ src/game/shared/neo/weapons/neo_weapon_parse.cpp | 4 ++++ src/game/shared/neo/weapons/neo_weapon_parse.h | 3 +++ src/game/shared/neo/weapons/weapon_supa7.cpp | 9 +++++++++ src/game/shared/neo/weapons/weapon_supa7.h | 7 +++++-- 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/game/neo/scripts/weapon_supa7.txt b/game/neo/scripts/weapon_supa7.txt index 55315864d4..e4f2454b69 100644 --- a/game/neo/scripts/weapon_supa7.txt +++ b/game/neo/scripts/weapon_supa7.txt @@ -5,6 +5,9 @@ WeaponData // Weapon characteristics: "Damage" "18" "Bullets" "8" + // when the fire animation's pump is home with the next round (Pump bone: altfire f18, fire f30) + "ChamberAfterShot" "0.600" + "ChamberAfterSlugShot" "1.000" "CycleTime" "0.8" // Weapon data is loaded by both the Game and Client DLLs. diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.cpp b/src/game/shared/neo/weapons/neo_weapon_parse.cpp index 302f9d419c..b9073bab3a 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.cpp +++ b/src/game/shared/neo/weapons/neo_weapon_parse.cpp @@ -25,6 +25,8 @@ CNEOWeaponInfo::CNEOWeaponInfo() m_bDropOnDeath = true; iAimFOV = 0; m_flReloadReadyBeforeEnd = 0.0f; + m_flChamberAfterShot = 0.0f; + m_flChamberAfterSlugShot = 0.0f; m_bChamberedRound = false; m_iNumReloadTables = 0; @@ -106,6 +108,8 @@ void CNEOWeaponInfo::Parse( KeyValues *pKeyValuesData, const char *szWeaponName m_iBullets = pKeyValuesData->GetInt( "Bullets", 1 ); m_flCycleTime = pKeyValuesData->GetFloat( "CycleTime", 0.15 ); m_flReloadReadyBeforeEnd = pKeyValuesData->GetFloat( "ReloadReadyBeforeEnd", 0.0f ); + m_flChamberAfterShot = pKeyValuesData->GetFloat("ChamberAfterShot", 0.0f); + m_flChamberAfterSlugShot = pKeyValuesData->GetFloat("ChamberAfterSlugShot", 0.0f); ParseReloadPhases(pKeyValuesData->FindKey("ReloadPhases")); const char *notFoundStr = "notfound"; diff --git a/src/game/shared/neo/weapons/neo_weapon_parse.h b/src/game/shared/neo/weapons/neo_weapon_parse.h index d0fa21eae4..a26e8142cc 100644 --- a/src/game/shared/neo/weapons/neo_weapon_parse.h +++ b/src/game/shared/neo/weapons/neo_weapon_parse.h @@ -71,6 +71,9 @@ class CNEOWeaponInfo : public CHL2MPSWeaponInfo int m_iBullets; float m_flCycleTime; float m_flReloadReadyBeforeEnd; + // a pump gun: when the fire animation's pump is home with the next round, seconds after a shell / slug shot + float m_flChamberAfterShot; + float m_flChamberAfterSlugShot; bool m_bChamberedRound; // a partial reload keeps the round in the chamber int m_iNumReloadTables; // 0: the weapon reloads without phases ReloadPhaseTable_t m_reloadTables[RELOAD_PHASE_TABLES_MAX]; diff --git a/src/game/shared/neo/weapons/weapon_supa7.cpp b/src/game/shared/neo/weapons/weapon_supa7.cpp index 5b0380bc6f..0e3a0e8507 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.cpp +++ b/src/game/shared/neo/weapons/weapon_supa7.cpp @@ -18,6 +18,7 @@ BEGIN_NETWORK_TABLE(CWeaponSupa7, DT_WeaponSupa7) RecvPropBool(RECVINFO(m_bRackPending)), RecvPropTime(RECVINFO(m_flRackTime)), RecvPropTime(RECVINFO(m_flRackSoundTime)), + RecvPropTime(RECVINFO(m_flPumpHomeTime)), #else SendPropBool(SENDINFO(m_bSlugDelayed)), SendPropBool(SENDINFO(m_bSlugLoaded)), @@ -30,6 +31,7 @@ BEGIN_NETWORK_TABLE(CWeaponSupa7, DT_WeaponSupa7) SendPropBool(SENDINFO(m_bRackPending)), SendPropTime(SENDINFO(m_flRackTime)), SendPropTime(SENDINFO(m_flRackSoundTime)), + SendPropTime(SENDINFO(m_flPumpHomeTime)), #endif END_NETWORK_TABLE() @@ -46,6 +48,7 @@ BEGIN_PREDICTION_DATA(CWeaponSupa7) DEFINE_PRED_FIELD(m_bRackPending, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flRackTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), DEFINE_PRED_FIELD(m_flRackSoundTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), + DEFINE_PRED_FIELD(m_flPumpHomeTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE), END_PREDICTION_DATA() #endif @@ -66,6 +69,7 @@ BEGIN_DATADESC(CWeaponSupa7) DEFINE_FIELD(m_bRackPending, FIELD_BOOLEAN), DEFINE_FIELD(m_flRackTime, FIELD_TIME), DEFINE_FIELD(m_flRackSoundTime, FIELD_TIME), + DEFINE_FIELD(m_flPumpHomeTime, FIELD_TIME), END_DATADESC() #endif @@ -105,6 +109,7 @@ CWeaponSupa7::CWeaponSupa7(void) m_nChamber = SUPA7_CHAMBER_EMPTY; m_bRackPending = false; m_flRackTime = 0.0f; + m_flPumpHomeTime = 0.0f; m_flRackSoundTime = 0.0f; m_fMinRange1 = 0.0; @@ -394,6 +399,9 @@ void CWeaponSupa7::PrimaryAttack(void) ProposeNextAttack(gpGlobals->curtime + GetFireRate()); // the shot leaves a hull, and the fire animation's pump ejects it and feeds the tube's top item + // (the HUD shows the fed round in the tube until the pump is home, never after the next shot is allowed) + const float flPumpHome = m_bSlugLoaded ? GetNEOWpnData().m_flChamberAfterSlugShot : GetNEOWpnData().m_flChamberAfterShot; + m_flPumpHomeTime = Min(gpGlobals->curtime + flPumpHome, m_flNextPrimaryAttack.Get()); m_nChamber = SUPA7_CHAMBER_HULL; Rack(); @@ -645,6 +653,7 @@ bool CWeaponSupa7::Holster(CBaseCombatWeapon *pSwitchingTo) // a rack cut off before the back stroke does nothing m_flRackTime = 0.0f; m_flRackSoundTime = 0.0f; + m_flPumpHomeTime = 0.0f; return BaseClass::Holster(pSwitchingTo); } diff --git a/src/game/shared/neo/weapons/weapon_supa7.h b/src/game/shared/neo/weapons/weapon_supa7.h index d3a0aa6c5d..6d8d7e868e 100644 --- a/src/game/shared/neo/weapons/weapon_supa7.h +++ b/src/game/shared/neo/weapons/weapon_supa7.h @@ -66,8 +66,9 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon // the tube plus the round in the chamber int GetMaxRoundsInGun() const OVERRIDE; bool HasChamberSlot() const OVERRIDE { return true; } - bool IsRoundChambered() const OVERRIDE { return IsChamberLive(); } - int RoundsInMagazine() const OVERRIDE { return m_nTubeCount; } + bool IsRoundChambered() const OVERRIDE { return IsChamberLive() && !IsPumpCycling(); } + // while the shot's pump cycles, the round it feeds still shows in the tube + int RoundsInMagazine() const OVERRIDE { return m_nTubeCount + ((IsChamberLive() && IsPumpCycling()) ? 1 : 0); } int MagazineCapacity() const OVERRIDE { return TubeCapacity(); } bool Holster(CBaseCombatWeapon *pSwitchingTo) OVERRIDE; @@ -100,6 +101,7 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon bool IsChamberLive() const { return m_nChamber == SUPA7_CHAMBER_SHELL || m_nChamber == SUPA7_CHAMBER_SLUG; } int TubeCapacity() const; + bool IsPumpCycling() const { return gpGlobals->curtime < m_flPumpHomeTime; } void SyncTube(); void TubePush(bool bSlug); void Rack(); @@ -129,6 +131,7 @@ class CWeaponSupa7 : public CNEOBaseCombatWeapon CNetworkVar(bool, m_bRackPending); // a slug was loaded to be fired next: the reload ends with a rack CNetworkVar(float, m_flRackTime); // a standalone rack's back stroke: the chamber is ejected and fed then CNetworkVar(float, m_flRackSoundTime); // when its pump sound starts + CNetworkVar(float, m_flPumpHomeTime); // when the shot's pump is home with the next round, for the HUD private: CWeaponSupa7(const CWeaponSupa7 &other); From a2ad5180e0be4776e451e6d7e2940834e3d16150 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 16 Oct 2026 21:10:06 -0600 Subject: [PATCH 60/60] End a reload by trigger only after its slap On a gun whose reload slaps or taps the magazine home, the trigger now ends a partial reload only after the slap credit, not at the seat, so a cancel skips only the steps already complete and stays close to the full reload time. Guns without a slap still end at the seat; the rule reads the reload's own phase table. --- .../shared/neo/weapons/weapon_neobasecombatweapon.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp index 7950bc60c6..cfd51ecb0b 100644 --- a/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp +++ b/src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp @@ -656,7 +656,8 @@ static constexpr const char *VM_SEQ_FIRE_CHAMBER_ONEHAND = "fire_chamber_onehand static constexpr const char *VM_SEQ_FIRE_BLEND = "fire_blend"; // The one way a trigger press acts on a reload in progress (the owner is busy, so it reaches only ItemBusyFrame): -// before the magazine drops, or once the fresh one is seated, the reload ends and the gun fires; +// before the magazine drops, or once the fresh one is seated (and slapped, on a gun with a slap), the reload ends +// and the gun fires; // with the magazine out, the chambered round fires one-handed and the reload goes on dry; a dry reload ignores it bool CNEOBaseCombatWeapon::TryFireDuringReload() { @@ -676,6 +677,13 @@ bool CNEOBaseCombatWeapon::TryFireDuringReload() return StartChamberShot(); } + // once the magazine is out, the reload ends only when its last phase is done (a slap gun's slap, else the seat), + // so a cancel skips only what is already complete + if (m_nReloadPhase != RELOAD_PHASE_NONE && m_nReloadPhase < LastReloadPhase(m_bReloadDry)) + { + return false; + } + if (!HasRoundToFire()) { return false;