Skip to content

Kill feed: change who got a kill, who died or the weapon, or hide one kill, from the console (part of #1021) - #1026

Merged
ccoventry merged 2 commits into
devfrom
feat/deathmsg-replace-1021
Oct 25, 2026
Merged

ccoventry merged 2 commits into
devfrom
feat/deathmsg-replace-1021

Conversation

@ccoventry

@ccoventry ccoventry commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Release note: New: ds_hud_deathmsg replace changes one kill-feed line (killer, victim or weapon) or hides it, matched by players, time and demo.

Part of #1021 (the ds_player_change_name half is a separate PR).

ds_hud_deathmsg replace [killer <id>] [victim <id>] [at <time>] [in <demo>[,<demo>...]] with [killer <id>] [victim <id>] [weapon <weapon>]
ds_hud_deathmsg replace ... with hide
ds_hud_deathmsg replace list | remove <n> | clear

The command shape is the one agreed on #1021. The hook already catches DeathMsg (killer, victim, weapon index). A matched message goes to client.dll as a copy with those bytes rewritten, or doesn't go at all. The game colours each name by its slot's team, so the colours follow the new killer and victim without extra code. New code is in goldsrc-hooks/src/deathmsg/replace.rs. deathmsg.rs only gains the handler step, the replace arm, usage and the status line.

Decisions

  • Clock: at is the demo player's world clock, the one ds_demo_seek_to uses. In play it reads the kill's own time: on PRE greetz1 the matches logged 442.5, 464.4 and 537.9, the analyzer's times to the tenth. During a stepped forward seek, kills are read up to one 5 s step behind the clock, so a match then accepts that whole step. cl.time doesn't help here: under viewdemo it doesn't jump with a seek (47.3 at world 614). Under playdemo (no demo bar) at falls back to cl.time. That fallback is not measured.
  • Lock-free: the handler reads an immutable copy of the rules through one AtomicPtr load. Commands publish a new copy and leak the old one (a few hundred bytes per command), so a reader can never see a list that has been freed. Matching keeps no state, so a kill replayed after a seek (or the backward-seek duplicate) matches again.
  • First matching line wins. block then filters the feed as replace left it. fake lines are not changed by replace.
  • New player missing from the demo: the line shows as recorded, and the console says so once per line.
  • The same line sent twice is kept once, because a batch runs its Initial Commands again for every demo.
  • The console's "X killed Y with Z" line follows the rewrite (client.dll prints it from the same message). The scoreboard, red screen and kill sound don't use DeathMsg, and the demo file is never written.
  • console_print now sends long text in pieces of 900 bytes or less (commands.rs). The engine cut ds_hud_deathmsg's usage mid-word at about 1 KB (it stopped at "list each pla"), so the new replace lines never showed. This also fixes the cut for every long reply.
  • Docs: docs/dodstudio_commands.md gains the general rule that a command with more than three values, or optional ones, names its parameters. It's worded so the ds_player_change_name PR can carry the same rule. The deathmsg table and a replace section are also added. Commands.txt's hint now mentions replace. The first sentence of the docs entry is unchanged, so console_commands_data.js needs no regeneration.
  • Not in this PR: the warning in Studio's pre-batch command check (cfg_scan) for self or a slot without in. For now the console warns.

Test done

Final build (ce5161c1). Demo ktps8w3-dyelife_ih_anzio_h1: a POV recorded by ddL , who is slot 8 and self. Kills come from the analyzer cache. Played under viewdemo on the Brain account, test DLL local\test-1021-replace\. Grabs are in local\review-images\1021\replace\ (pre-anzio-*, post-anzio-*, plus _feed crops). Both installs gave identical hook-log lines and both probes passed; the game was still running at the end, with no crash.

PRE and POST (same results on each):

  • Killer change, by self: the 851.5 kill logged rule 1 made 8 -> 12 d_garand into 13 -> 12 d_garand (clock 851.5).
    • The feed read dicE deeebeee [Garand] icyHOT monday, with deeebeee in Allies colour.
    • The console read dicE[: :]deeebeee killed icyHOT ~ monday with garand.
  • Weapon change (parameters given in a different order): the 855.7 kill became d_kar, and the feed showed a K98 for ddL → seanality.
  • Hide: the 961.5 ddL → monday line, matched with the ktps8w3-dyelife* pattern, was hidden. kanguhhh → warchyldd at 961.8 still showed.
  • An in that doesn't match: in nothing_here hid nothing. expect_not rule 4 hid passed, and the 966.2 kill stayed as recorded.
  • Seek landing after a kill: a forward seek from 968 to 1013 landed 3.9 s after the 1009.1 kill. It matched inside the seek step: clock 1004.4..1009.4. The feed showed ddL → hildebrand with a K98.
  • Usage: a bare ds_hud_deathmsg now prints the whole usage, through scale default, including both replace lines. Before the console_print change it stopped at about 1 KB.
  • Console surface: each refusal named its reason (vicitm, nothing after with, no killer or victim). self without in warned. The same line sent twice was kept once. list, remove 5, remove 9 (refused, 1..4) and clear all worked.

Earlier run, greetz1 (laggy demo), PRE only, previous build: the same kinds of checks passed on solifotw-greetz1. That demo warps and jitters, so take those results with care. The run did find two bugs, both fixed in this PR: the usage cut at about 1 KB, and a clock idea based on cl.time that doesn't follow a seek.

Unit tests cover the parser (any order, m:ss/h:mm:ss/seconds, in lists, *, .dem, refusals, warnings) and the matcher (2 s window, seek span, demo filter, first match, SteamID in any slot, missing player). cargo test -p goldsrc-hooks passes (790). Clippy and fmt pass with the pinned 1.98.1 toolchain on the host and on i686.

Test steps

  1. Play ktps8w3-dyelife_ih_anzio_h1 with viewdemo and this DLL.
  2. Change the killer: ds_hud_deathmsg replace killer self victim 76561197983650610 at 14:11.5 in ktps8w3-dyelife_ih_anzio_h1 with killer 76561197960525375, then ds_demo_seek_to 846.
    • At 851.5 the feed should read dicE deeebeee [Garand] icyHOT monday.
  3. Change the weapon: ds_hud_deathmsg replace in ktps8w3-dyelife_ih_anzio_h1 victim 76561197961763339 killer 76561198113870514 at 855.7 with weapon kar.
    • At 855.7, ddL → seanality should show a K98.
  4. Hide: ds_hud_deathmsg replace killer 76561198113870514 victim 76561197983650610 at 16:01.5 in ktps8w3-dyelife* with hide, then seek to 958.
    • ddL → monday at 961.5 should be missing, and kanguhhh → warchyldd at 961.8 should still show.
  5. An in that doesn't match: ds_hud_deathmsg replace killer 76561198113870514 at 966.2 in nothing_here with hide.
    • The 966.2 kill should still show.
  6. Seek past a kill: ds_hud_deathmsg replace killer self at 1009.1 with weapon kar (this warns about self), then ds_demo_seek_to 1013.
    • ddL → hildebrand should show a K98.
  7. Run ds_hud_deathmsg replace list, remove 1 and clear.
    • A bare ds_hud_deathmsg should print the whole usage.

Other open PRs on the same files

🤖 Generated with Claude Code

…rt of #1021)

Matches a DeathMsg by killer, victim, demo-bar time and demo name, and
rewrites its killer, victim or weapon byte, or drops it. Rules are read
lock-free on the message path. Long console prints now go over in pieces,
since the engine cut the command's usage at about 1 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry

Copy link
Copy Markdown
Owner Author

Review

I read the whole diff: replace.rs, the handler step in deathmsg.rs, the console_print split in commands.rs, the docs and Commands.txt.

What I checked, and found fine:

  • No locks in the handler. The handler reads the rules through one AtomicPtr load, and old copies are leaked, so the handler never reads freed memory. The demo name comes from demo_reload's thread-local, and the seek check uses try_lock. RULES is locked only by console commands.
  • Bounds checks. The rewritten copy is only built when size >= 3. Slots are range-checked when the line is parsed, and again before a slot byte is written. Weapon indices come from parse_weapon. console_pieces never cuts inside a UTF-8 character (there's a test for this).
  • Seeks and reloads. Matching keeps no state, so a kill played again matches again. in uses the name of the last demo played, without its folder.
  • No new console name, so console_commands_data.js and NEW_SINCE_555 need no change.
  • Comments follow CLAUDE.md.

Merged origin/dev (studio-only commits, no conflict): fd681f1e. Pinned fmt and clippy (1.98.1) pass on host and i686. The hook tests pass: 803 on i686, 790 on host.

Merge order with #1030: whichever of #1026 and #1030 merges second conflicts in two places:

Nothing else conflicts; I tried each merge. #1034 and #1038 merge cleanly on top of this one.

No bugs found and no code changed, so I didn't run another game test. The PR's own PRE and POST runs on dyelife h1 cover the main path.

Verdict: ready to merge.

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