Skip to content

Rename any player in a demo from the console, any length, everywhere their name shows (part of #1021) - #1030

Merged
ccoventry merged 6 commits into
devfrom
feat/player-change-name-1021
Oct 25, 2026
Merged

ccoventry merged 6 commits into
devfrom
feat/player-change-name-1021

Conversation

@ccoventry

Copy link
Copy Markdown
Owner

Release note: New: ds_player_change_name renames a player everywhere their name shows during playback (kill feed, scoreboard, chat), by SteamID, slot or self, for one demo or all.

Part of #1021: the ds_player_change_name half. ds_hud_deathmsg replace is #1026.

ds_player_change_name add id <slot | SteamID | self> [in <demo>[,<demo>...]] to <new name>
ds_player_change_name list
ds_player_change_name remove <n>
ds_player_change_name clear

What it does

  • One detour covers most places. client.dll's copy of pfnGetPlayerInfo (engine table slot 21, 19 call sites) now hands out the new name for a renamed slot: kill feed, scoreboard, capture lines, aim name, voice icon. The names are leaked on purpose and never freed, since client.dll keeps the pointer. The hook's own lookups (deathmsg SteamIDs, sprays, this module) still go through the engine's table, so they see the real player.
  • Once per frame, then lock-free. poll (from HUD_Frame) works out each slot (SteamID from the userinfo, self, the loaded demo's name) and publishes the result in per-slot atomics: the new name, plus a hash of the real name it was resolved for. The detour and the message filters only read those. The rule list sits behind an RwLock that the frame only try_reads.
  • SayText: the server wrote the sender's name into the line, so the line is rewritten. The game then colours the new name, because it finds the detour's name in the line.
  • TextMsg: a string argument equal to a renamed player's real name is swapped (*<name> joined Axis). This runs in the filter clan_text already installs.
  • Long names: a name over 31 characters lifts the kill feed's copy cap. The two push 0x20 before strncpy become 0x40, and the two terminators move from [31] to [63] inside the existing 64-byte szKiller/szVictim. The terminators are ARRAY_REFS entries, so max keeps them lifted (name_cap::field_for). tools/verify_killfeed_name_cap_offsets.py checks every patched byte on both installs' client.dll. hw.dll is untouched.
  • Every other place the 19 call sites put a name was checked offline: a bounded copy, or a sprintf into a buffer of 128 bytes or more. That's why a name can be at most 63 characters.

Decisions

  • Named parameters, in any order (the agreed shape). docs/dodstudio_commands.md gets the rule: more than three values, or optional ones, means named parameters.
  • : and , tokens are glued back, because GoldSrc's tokenizer splits them. So STEAM_0:1:123 and in a,b work unquoted. A name with spaces or : , ( ) { } ' goes in quotes. An unknown word after to gets a hint saying so.
  • Refused, with the reason: add without id and to, any unknown word (named), a value given twice, a bad id, a name over 63 bytes, % (the game can read it as a format code), and control characters.
  • Warned, not refused: self or a slot without in.
  • Which rename wins: one with in beats one without, then the one added last. add with the same id and in replaces the name instead of adding a line, so a batch that re-runs its commands doesn't grow the list.
  • in matching: against the loaded demo's file name, without folder or .dem, case-insensitive. Only * is a wildcard. in rules apply only while a demo plays.
  • Cap lift: happens on the first long name and stays for the session. The engine's own names are at most 31 characters, so they look the same.
  • SayText: only the sender's own name is swapped, first occurrence, so the message text is never touched. TextMsg: arguments only, never the key or text.
  • Command tiers: none. It's an ordinary user command, allowed in Initial and Scheduled Commands. A test in cfg_scan.rs and one in command_suggest.test.js pin that.
  • deathmsg.rs changes are kept to the cap lift: mod name_cap, one line in apply_max, lift_name_cap/name_cap_lifted, and pub(crate) on parse_steam_id/classic_steam_id. usage() is untouched.
  • console_commands_data.js was regenerated. It also picks up two hints that were stale on dev (ds_debug_log_view_bob, ds_hud_hide_element).

Test done

All runs used the Brain account, through the game lock, with local\test-1021\. Grabs are in local\review-images\1021\hook\. The final DLL was rebuilt after a clippy-only change (a variant rename and ptr::eq, no behaviour change). It is the one in local\test-1021\.

POST, ktps8w3-dyelife_ih_anzio_h1: PASS.

  • add id self to "SELF[: :]Renamed", plus seanality by STEAM_0:1:748805 to a 44-character name.
  • Kill feed at 857 s: SELF[: :]Renamed kills Somebody With A Much Longer Name Than Before, all 44 characters, in Axis red.
  • Chat SELF[: :]Renamed: glhf at 813 s.
  • Scoreboard renamed; the column cuts the long name, as it does for any long name.
  • Join lines at 793 s: *Somebody With A Much Longer Name Than Before joined Spectators/Axis.
  • list, remove 1, clear: every real name is back on the scoreboard.

PRE, same demo and steps: PASS.

  • Same results as POST.
  • Slot 10 renamed with in ktps8w3-dyelife_*,other_demo shows Stealth By Slot: GLHF in the Allies colour, like the rest of the team.
  • in no_such_demo_*: "nobody here", no change.
  • Refusals (vicitm, add without id) and the ds_debug_status line are as expected.
  • One grab was lost to focus (a Discord overlay popup), not to the hook.
  • PRE's movie.cfg turns chat off, so chat was turned on live (hud_saytext 1; hud_saytext_internal 1; hud_saytext_time 6). No .cfg was written.

PRE, solifotw-greetz1 (laggy demo), done before the switch to dyelife:

  • The 44-character name shows in full in the kill feed, as killer and victim.
  • dicE[: :]Gorilla[bc shows on the scoreboard, in the kill feed, in capture lines and in console kill lines.
  • list, remove, clear, a non-matching in and the refusals all worked.

Offline:

  • verify_killfeed_name_cap_offsets.py ALL OK on PRE and POST client.dll; verify_deathmsg_offsets.py TABLES VERIFIED.
  • cargo test -p goldsrc-hooks (795 pass); the native cfg_scan test.
  • npm run test:unit (854 pass) and app-boot.spec.js.
  • Pinned clippy (1.98.1) on host and i686 is clean, and so is cargo fmt.

Not seen in-game:

  • The aim-at-teammate name, the voice icon, the spectator bar and overview names (HLTV), and a renamed long name inside a chat line.
  • Studio's pre-batch command check doesn't warn about self/slot without in; only the console does.

Test steps for you (PRE then POST)

  1. Pick 1021 in the Hooks DLL dropdown (or local\test-1021\dodstudio_goldsrc_hooks.dll). Play ktps8w3-dyelife_ih_anzio_h1 with viewdemo and wait for "Demo file completely loaded".
  2. Run ds_player_change_name add id self to "dicE[: :]Test". You should get the warning about self. Press TAB: your row reads dicE[: :]Test.
  3. Run ds_player_change_name add id STEAM_0:1:748805 to "Somebody With A Much Longer Name Than Before", then ds_demo_seek_to 849. At about 856 s the kill feed shows your kill on that full 44-character name.
  4. On PRE, turn chat on: hud_saytext 1; hud_saytext_internal 1; hud_saytext_time 6. Seek to 787: the join lines at 793 s carry the long name, and your glhf at 813 s shows the new name in the team colour.
  5. Run ds_player_change_name list, then remove 1, then clear. TAB shows the real names again.

Other open PRs on the same files

🤖 Generated with Claude Code

…ring playback (part of #1021)

A detour of client.dll's copy of pfnGetPlayerInfo hands out the new name;
SayText and TextMsg join/team lines are rewritten too, and a long name lifts
the kill feed's 31-character copy cap to 63 (verify_killfeed_name_cap_offsets.py).
Named parameters in any order: add id <slot|SteamID|self> [in <demo>,...] to <name>,
list, remove <n>, clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccoventry and others added 2 commits October 25, 2026 00:56
…und a : or ,

GoldSrc's console splits an unquoted name at : and , and drops the
spaces beside them, so to dicE[: :]Alex arrived as dicE[::]Alex with no
word of it. The glued name is kept, with a warning that shows it as it
now reads and says to quote it (part of #1021).

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

Copy link
Copy Markdown
Owner Author

Review

I read the whole diff: player_names.rs, deathmsg/name_cap.rs, the clan_text and msglog hand-offs, the plumbing, the docs and the verify script.

What I checked, and found fine:

  • The frame never waits. poll takes the rule list with try_read, and publishes per-slot atomics. The detour and the SayText/TextMsg filters only read those.
  • Engine-thread patching. The cap lift and the gEngfuncs slot write run from the console command or poll, both on the engine thread.
  • Names stay valid. Every name handed out is leaked, so a remove or clear never frees one that client.dll still holds. The cap lift checks every byte before it writes, and apply_max keeps the moved terminators.
  • Plumbing: Commands.txt, console_commands_data.js, NEW_SINCE_555, the cfg_scan tier test, and the docs.
  • verify_killfeed_name_cap_offsets.py: ALL OK on PRE and POST.

Fixed: e7fc9856

An unquoted name with : or , lost its spaces without a word. GoldSrc's console splits to dicE[: :]Alex into dicE[, :, :, ]Alex. The glue-back then made it dicE[::]Alex, and nothing said so. That is the example from the agreed shape on #1021.

  • rejoin now marks the tokens it glued.
  • add keeps the name but warns: "the name was typed without quotes, so the console dropped any spaces around its : or , and it reads "dicE[::]Alex"; put it in quotes to keep it as typed".
  • A quoted name, a SteamID and an in list don't warn. A unit test covers each case, and the docs got a line.

Merged origin/dev (no conflict): 5bf522a4. Results after the merge:

  • Pinned fmt and clippy (1.98.1) pass on host and i686.
  • The hook tests pass: 809 on i686, 796 on host.
  • native console_prefix passes, and CI is green.

In-game, POST

Brain account, through the lock. Demo ktps8w3-dyelife_ih_anzio_h1, DLL local\test-1021-review\. Report: local\game-probe\20261025-010646 (PASS).

  • Unquoted: the warning printed, and the name read dicE[::]Alex.
  • The same line quoted: the name was replaced by dicE[: :]Alex. list showed now: slot 8.
  • Kill feed after ds_demo_seek_to 846: both of ddL's kills show dicE[: :]Alex, in his team colour. Grab: local\review-images\1021\review\post-anzio-h1-feed_852.png.
  • clear worked, and there was no crash.

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

  • deathmsg.rs: keep both mod name_cap; and mod replace;.
  • docs/dodstudio_commands.md: each PR has its own copy of the named-parameters rule. Keep one paragraph.

Verdict: ready to merge.

…e-1021

# Conflicts:
#	docs/dodstudio_commands.md
#	goldsrc-hooks/src/deathmsg.rs
#	studio/src/console_commands_data.js
@ccoventry

Copy link
Copy Markdown
Owner Author

Merged dev (after #1026 and #1034) in; head 135bdd14.

  • goldsrc-hooks/src/deathmsg.rs: kept both mod name_cap; and mod replace;. Everything else merged by itself. replace rewrites the slot bytes of a DeathMsg and name_cap lifts the strncpy lengths, so the two don't interact.
  • docs/dodstudio_commands.md: one "Named parameters" paragraph, worded generally, with both sides' examples and this PR's note on quotes.
  • console_commands_data.js: regenerated (PRE + POST + HLAE).
  • No hook code changed in the merge beyond the mod line, so I skipped the in-game smoke test.
  • Local: hook tests i686 and host, console_prefix, pinned fmt, clippy for i686 and host, npm run test:unit: all pass. verify_killfeed_name_cap_offsets.py: ALL OK on PRE and POST. CI green.
  • I also did a trial merge of this branch into the merged Kill feed: an even layout where every weapon icon has the same height and nothing is cut off (#1022) #1023. It merges with no conflicts, and the i686 hook tests and clippy pass on the result.

…e-1021

# Conflicts:
#	studio/src/console_commands_data.js
@ccoventry
ccoventry merged commit c43b63c into dev Oct 25, 2026
5 checks passed
@ccoventry
ccoventry deleted the feat/player-change-name-1021 branch October 25, 2026 08: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