From 383a7c5169f298a85e89f308781d8808d99f6dd8 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 75bcfe18e3b8464243328492659b5c19435173df 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 95cbfd3614c89760189d09e2edf73043911ce332 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 35aaa10b5874e21a754312fe16cecb105784f36d 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 e84fa2e6ec21fdc1c7e36689e06c23b0f3782de9 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();