Skip to content

POV demos: the red crosshair opens when the player runs, as it did live (follows #1187) - #1238

Merged
ccoventry merged 4 commits into
devfrom
feat/pov-demo-dynamic-crosshair
Oct 31, 2026
Merged

ccoventry merged 4 commits into
devfrom
feat/pov-demo-dynamic-crosshair

Conversation

@ccoventry

@ccoventry ccoventry commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Release note: New: with cl_xhair_style 0 and cl_dynamic_xhair 1, a POV demo's red crosshair now opens while the player runs and closes when he stops, as it did when he played.

Follows #1187 (#308). No issue to close.

Why a POV demo's crosshair never moved

Checked in client.dll (both installs, byte-identical), not taken from #1187. The gap function client+0x2cfb0 has three inputs:

input read from in a POV demo
weapon the HUD's current weapon (client+0x223b0), set by the CurWeapon message recorded: works
deployed g_iUser3 == 2 (client+0xe88dc) or the sandbag flag (client+0xea46c), both written at client+0x311bc / +0x311d9 from the player's entity state recorded: works
speed the length of the vector at client+0x1080f8 stale: its only writer in .text is client+0x12656, inside HUD_WeaponsPostThink (client+0x12360), which copies the predicted player's velocity in. Prediction does not run in demo playback, so it stays at whatever it was (zero in a fresh game)

So only the speed term was missing, which is what #1187 saw (5 px over the whole rush).

What changes

  • Commit 1, move only: the red crosshair's gap call (client+0x2cda9), the scans that find it and the hook that answers it move out of spectator_crosshair_gap.rs into a new crosshair_gap.rs, so a second answer can share them. The spectator's table, arithmetic and speed stay in spectator_crosshair_gap.rs (ready() reads the table before spectator_crosshair routes the red draw, as the scan did before). No behaviour change; git diff --color-moved shows it.
  • Commit 2, the feature: pov_crosshair_gap.rs. While a POV demo plays (pov_demo_playing: playing back and not an HLTV demo), crosshair_gap points the gap call at its hook; in the demo's own view (g_iUser1 0) the hook puts the view's velocity into client+0x1080f8 for one call of the game's own gap function and puts the old value back after it. The velocity is ref_params_s.simvel as the engine hands it to V_CalcRefdef for the main view (now pub in engine.rs), kept in three atomics; the hook reads atomics only. Everything else is the game's code: cl_dynamic_xhair, the fixed 4 px for grenades and rockets, the weapon table, the 3D speed, the 3 px a frame easing and the x87 precision.
  • ds_debug_status gets a POV demo red crosshair: gap N px, the recorder at N u/s line while a POV demo plays.
  • tools/verify_spectator_crosshair_offsets.py reads both Rust files and gains check 12: VELOCITY_AT is the vector the spread function pushes for its length call, exactly one writer stores its x, y, z (client+0x12656), and every other use only pushes it. All checks pass on PRE and POST.
  • Docs: docs/goldsrc_crosshair.md §2 (the POV paragraph rewritten), docs/dodstudio_commands.md ("Stock settings the hook extends": cl_dynamic_xhair), lib.rs module list.

Decisions

Test done

In-game, Brain account, game_probe.py (game behind, game_lock), this branch's release DLL (local\test-povxhair\dodstudio_goldsrc_hooks.dll) against a dev build of 1369693 (local\test-povxhair\dev-13696938\), cl_xhair_style 0, cl_dynamic_xhair 1, clips recorded by HLAE at 5 fps (a frame is 0.2 s of demo time), each frame scanned for the right arm's distance from the centre (±1 px; a 4 px gap reads 4-5).

  • PRE, wsod25_grp2_h1_warchyld (K43) from 13:27:35 on the viewdemo bar, 389 frames: dev 4-6 px on every frame; this branch 5 at rest, 25-26 running (157 frames), easing 3 px a frame between, 4 on a stick grenade. cl_dynamic_xhair 0: 4-5 px in both builds.
  • POST, same demo through the ../ path, 370 frames: the same (dev 4-6; this branch 5 / 25-26 / grenade 4; cl_dynamic_xhair 0 4-5).
  • Same moment, HLTV (wsod25_grp2_h1_hltv from 13:26:00, in-eye on warchyld, ds_spec_match_pov 1), POV vs HLTV frame by frame (frames matched by picture, the crosshair left out): this branch within 3 px of HLTV on 213 of 252 frames (PRE) and 232 of 265 (POST); dev on 111 of 252 and 113 of 263. The misses are the ease-in/ease-out frames a few frames apart and the frames HLTV's Spectator crosshair follows POV's rules (weapon switch, airborne, sprint, prone transitions, snipers, melee, undeployed MG, reload) #310 rule hides that the POV demo shows (pre-existing).
  • HLTV, this branch vs dev: the same gaps (rest 4-5, running 25-26) on both builds: the move changed nothing there.
  • Three other recorders and matches, both installs: ktps8w11sf-gorilla_gskill_m2_armory_h2 (Gorilla, BAR) from 5:15:00: 7-8 at rest, 27-28 running; ktps8w10qf-element_soul_m1_saints_h2 (element, Garand and a K98) from 8:14:00: 25 running with the Garand, 34 with the K98, 4 on a grenade; ktps8w5-db_thunder_thunder2_h2 (deeebeee, MP40) from 7:56:00: 27-28 running. All as the weapon table says.
  • Review images and video (POV before | POV after | HLTV, stopwatch in demo seconds): local\review-images\povxhair\ (pre_pov_before_after_hltv.mp4, _0.25x, post_*, *_running_*.png, *_resting_*.png, *_grenade_*.png, per-frame gaps in *_gaps.tsv).
  • The runs above used the build of 75fcc9f; the last commit only adds the hook-log lines when the call is redirected and put back. Re-run with the head's DLL on PRE, the same warchyld moment: the log line appeared, ds_debug_status printed the POV demo red crosshair: line, and the gaps were the same (5 rest, 25-26 running, 4 on the grenade).
  • cargo test -p goldsrc-hooks --target i686-pc-windows-msvc: 1110 passed. Clippy clean on i686 and host (1.98.1), fmt clean. Commit 1 alone also builds, passes the tests (1108) and the offsets check.
  • Not tested: a deployed bipod or MG in a POV demo (the stretches recorded had none; the deploy term is the game's own and unchanged), a rocket.

Needs a look

🤖 Generated with Claude Code

ccoventry and others added 2 commits October 30, 2026 18:12
The call site at client+0x2cda9, the scans that find it and the hook that
answers it move out of spectator_crosshair_gap.rs into crosshair_gap.rs, so
a second answer (a POV demo's, next commit) can share them. The spectator's
arithmetic, its table and its speed stay where they were; the table is read
by spectator_crosshair_gap::ready() before spectator_crosshair routes the red
crosshair, as the scan read it before. Behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1187)

DoD's style-0 crosshair widens its gap above 105 u/s from a velocity that
only HUD_WeaponsPostThink writes, from the client's own prediction, which
does not run in demo playback; so a POV demo held the resting gap however
fast its recorder ran. While a POV demo plays, crosshair_gap now also points
the gap call at its hook, and pov_crosshair_gap runs the game's own gap
function with the view's simvel (the recorded velocity) in that slot for the
one call, putting the old value back after it. cl_dynamic_xhair, the fixed
grenade and rocket gap, the weapon table and the easing stay the game's.
Live play and HLTV demos are unchanged.

verify_spectator_crosshair_offsets.py gains check 12: the velocity is the
vector the spread function takes the length of, and one function writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccoventry and others added 2 commits October 30, 2026 18:29
One hook-log line per change (a POV demo starts or stops), not per frame,
so a user's log shows the crosshair fix was in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry
ccoventry merged commit f72a072 into dev Oct 31, 2026
6 checks passed
@ccoventry
ccoventry deleted the feat/pov-demo-dynamic-crosshair branch October 31, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant