Skip to content

Hold a pistol's slide back from the last shot to the reload - #2248

Draft
sunzenshen wants to merge 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:vm-empty-draw
Draft

sunzenshen wants to merge 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:vm-empty-draw

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

The tachi/milso slide locks back on the last shot, stays open when empty and between changing weapon slots, and stays back through a reload from empty. This PR includes the commit of #2247 (closed), the parity animation where the slide moves back and stays open when empty, so it stands on its own.


Case 8: first-person weapon animations.
Review page: exported review page (Dropbox) · live review page (opening it lets you request access with your email or Claude account).

The Tachi and Milso viewmodels ship an idle01empty sequence (ACT_VM_IDLE_EMPTY) with the slide held back, but neo never plays it. After the last round, an empty pistol holds the fire sequence's last frame for 2.0 s and then idles with the slide forward. In the original NEOTOKYO, the slide goes back about 0.5 s after the last shot and stays back until the reload; drawing the empty pistol again plays the normal draw, then the slide goes back.

This PR makes five changes, all in CNEOBaseCombatWeapon (+57 / -2 lines in weapon_neobasecombatweapon.cpp/.h):

  • Empty idle. This uses the SDK's own pattern for an empty weapon's idle, from CWeaponCrossbow::SendWeaponAnim. While the clip is empty, CNEOBaseCombatWeapon::SendWeaponAnim sends ACT_VM_IDLE_EMPTY in place of ACT_VM_IDLE, when the model has that sequence. A weapon without idle01empty behaves exactly as before.
  • Last shot. The shot that empties a Tachi or Milso plays the model's fireempty (ACT_VM_DRYFIRE) instead of a fire sequence. On both pistols that sequence is a shot's kick with the slide held back from the first frame, ending on the idle01empty pose, so the slide locks at the shot and the empty idle follows when it ends (0.4 s, the same length as the fire sequences). The fire sequences close the slide within 0.17 s, so with the empty idle alone the slide would lock only as the empty idle faded in.
  • Reload from empty. An empty weapon reloads with ACT_VM_RELOAD_EMPTY where the model has it: the Milso's shipped reloadempty, the same 2.1 s as reload, holds the slide back until it is released over its last 0.3 s. The Tachi ships no such sequence and keeps reload, whose slide is forward from its first frame. Tachi: reload from empty with the slide held back neoAssets#170 adds one, and this code plays it with no further change.
  • Third-person reload. The empty reload sends PLAYERANIMEVENT_RELOAD as a reload with rounds left does, so other players still see the reload gesture. This was found in review of the change above; a server log of the event in game showed it at clip 0 (empty reload) and at clip 9 (partial reload).
  • Empty draw. When you switch to an already-empty Tachi or Milso, the draw plays with the slide forward, and the slide then pulls back over the 0.2 s crossfade into the empty idle. The original NEOTOKYO does the same. Both models already ship a drawempty sequence: the same 0.8 s draw with the slide held back. Bone data shows it differs from draw only in the slide, and by up to 2.1 u at one forearm bone. It has no activity tag, so nothing could select it. CNEOBaseCombatWeapon::Deploy now picks it by name when the clip is empty. Models without a drawempty sequence are unchanged, and so are loaded draws.

The empty idle is classic parity. The lock at the last shot, the empty draw and the slide held through the reload go past it, as asked in review.

The last-shot check names the two pistols. On the rifles, fireempty is a small trigger click, and idle01empty is identical to idle01 in every frame and bone in the MX, SRM, ZR68S, MPN and AA13, so an empty rifle has no slide or bolt pose to show. Their empty idle plays but looks the same as before. Neither pistol's reload sound has a slide release; that is unchanged.

For reviewers who want to merge the Tachi asset at the same time: #2259 is the same code as this PR, paired with NeotokyoRebuild/neoAssets#170. Merge this PR or #2259, not both.

How this was compared

Per-frame viewmodel logs on nt_rise_ctg (a harness-only client log of the local viewmodel's sequence, cycle and clip each frame), and the slide bone's position per frame read from the shipped models. Video was recorded on a black background with only the lit viewmodel drawn, so the slide can be read. The original game was filmed in the same way, as Jinrai Assault with the Tachi.

The scripted pass, the same in every build, with cl_autoreload_when_empty 0 in neo:

  1. Empty the magazine with single taps, then wait 3 s.
  2. Pull the trigger twice on empty.
  3. Switch to the primary and back.
  4. Wait 3 s, then reload.
  5. As a control, fire 3 rounds and reload a partial magazine.

Release builds of master (f426c22) and this PR.

How to test

  1. Tachi or Milso: fire the magazine dry. The slide locks back at the last shot and stays back.
  2. With cl_autoreload_when_empty 0, switch away and back. The slide is back from the first frame of the draw and stays back.
  3. Milso: reload from empty. The slide stays back until the end of the reload, and other players see the reload gesture. A partial reload plays the normal reload.
  4. Tachi: reload from empty plays the normal reload (slide forward from its start). With Tachi: reload from empty with the slide held back neoAssets#170 it behaves as the Milso in step 3.
  5. A loaded pistol, the MX or the MPN: firing, drawing and reloading behave as before.

Results

original NEOTOKYO master this PR
Tachi, last shot fire, slide back ~0.5 s after the shot fire, pose held 2.0 s, slide forward fireempty: slide back at the shot, idle01empty 0.40 s later
Tachi, dry pull starts the reload fireempty, then slide forward fireempty, then idle01empty
Tachi, drawn while empty normal draw, then slide back draw, then idle01 (slide forward) drawempty, then idle01empty (slide back throughout)
Tachi, reload from empty slide forward after the reload reload (slide forward from its start) reload (unchanged without the neoAssets PR; reloadempty with it)
Milso - as the Tachi on master as the Tachi, and the reload from empty plays reloadempty (slide released at 1.77-2.07 s)
MX, after the magazine - idle01 idle01empty, which looks the same
draw to idle - 0.81 s 0.81 s
draw to first shot, loaded Tachi (attack held) - 0.792 / 0.815 s 0.810 / 0.802 s

With the default cl_autoreload_when_empty 1, the empty idle shows until the automatic reload 3 s later, or until a dry pull starts the reload.

Unchanged:

  • Shot intervals: MX 0.106 / 0.107 s, Tachi and Milso the same in both builds.
  • Reload lengths: Tachi 2.11 s, Milso 2.09 s, MX 2.88 s in both; the Milso's reloadempty reaches idle01 2.07 s after it starts.
  • With net_fakelag 75, the Tachi pass showed no switching between the loaded and empty idles, with autoreload off or on.

A Debug client with 12 bots ran 210 s each on ntre_rise_ctg and ntre_skyline_ctg. The local player emptied the Tachi, then switched away and back. It logged only known asserts: neo_root.cpp (378) localizedWszStr at startup, plus one clientleafsystem.cpp (1025) on ntre_rise_ctg.

Rerun after the last-shot and reload changes (Debug, ntre_rise_ctg, last shot, dry, draw, reload from empty, partial): only the known neo_root.cpp (378) at startup. The third-person reload fix was checked in a Release build.

Trial merges with #2246, #2249, #2250 and #2244 are clean. #2245 conflicts in Reload(), where both change the last lines: the merge keeps this PR's empty-reload branch and sets #2245's ready time after either reload (as done in #2251).

Not changed: the Milso's holsterempty, which is a second ACT_VM_HOLSTER sequence that the SDK already picks at random, and the 2.0 s hold after shots that do not empty the clip.

Related

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.
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.
@sunzenshen sunzenshen added the Animations Animation stuff. Including the syncronizing of animations between client and server based on state. label Oct 16, 2026
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.
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.
@sunzenshen
sunzenshen marked this pull request as ready for review October 16, 2026 10:11
@sunzenshen
sunzenshen requested a review from a team October 16, 2026 10:11
@sunzenshen sunzenshen changed the title Draw an empty pistol with its slide held back Hold a pistol's slide back from the last shot to the reload Oct 17, 2026
@sunzenshen

Copy link
Copy Markdown
Contributor Author

PR (2259) is the more complete solution, but I am proposing this PR #2248 simultaneously, because I think we need to figure out how to handle the raw precompiled animation files from NeotokyoRebuild/neoAssets#170 . Maybe they shouldn't be in neoAssets? So it might be easier to merge this PR first before we figure out the answer to animation file management.

// 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)
{
if (!DefaultReload(GetMaxClip1(), GetMaxClip2(), ACT_VM_RELOAD_EMPTY))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have any weapons that benefit from this change? It might be a good idea regardless, but from what I can tell the tachi reload has the slide forward when reloading while empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point, and maybe for consistency, arguably the full animations need to be implemented for this to be cohesive.

This PR is the same idea but with a custom animation to fix the tachi slide back:

#2259

These are the differences between master and the two PRs:

case8c-slide-reload-tachi.mp4

@sunzenshen
sunzenshen marked this pull request as draft October 24, 2026 01:37
@sunzenshen

Copy link
Copy Markdown
Contributor Author

Drafting in favor of #2259

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Animations Animation stuff. Including the syncronizing of animations between client and server based on state.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tachi slide shouldn't move when dry firing

2 participants