From 362eb29123db527ff3fb2363591bcffcf0aef04f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 23:30:00 -0600 Subject: [PATCH 1/5] 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 2/5] 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 09b30e9d54d7b6a553f2303bd895ee9a52dbc90a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 15 Oct 2026 23:15:10 -0600 Subject: [PATCH 3/5] 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 4/5] 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 5/5] 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();