Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/dodstudio_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,24 @@ the death cam).
| `ds_sound_mute_voice_commands` | `0` | silences "fire in the hole!" and the rest, without touching the game's own `.wav` files; subtitles and speaker icons still show | [`goldsrc_hud_suppression.md`](goldsrc_hud_suppression.md) |
| `ds_hud_hide_crosshair` | `0` | hides the crosshair and keeps it hidden, which the stock `crosshair` cvar can't do because `CHud::Redraw` forces the value back every frame | [`goldsrc_crosshair.md`](goldsrc_crosshair.md) |
| `ds_spec_hide_bars` | `0` | hides the spectator panel while spectating, in a demo or live: the two dark bands across the top and bottom of the screen, the score, timer and player name on them, and the menu row DUCK brings up. On screen, with no capture running | [`goldsrc_spectator_bars.md`](goldsrc_spectator_bars.md) |
| `ds_spec_hide_text` | `0` | hides the two lines the game prints when the view becomes a spectator camera: the camera mode's name, centred just above the bottom bar ("Locked Chase Cam", "Free Chase Cam", "Free Look", "First Person", `#Spec_Mode1`..`4`), and the green "Press DUCK for Spectator Menu" (`#Spec_Duck`). Both show in a POV demo when the player dies, and in HLTV when the camera mode changes. The mode name comes twice, from the game itself and as a `TextMsg` the server sent; both go. Every other centre-print and HUD message still shows. A line already on screen when it's turned on stays until it times out (#677) | `goldsrc-hooks/src/spectator_text.rs` |
| `ds_spec_lock` | `0` | HLTV demos: the camera stays on the player being watched when they die. Without it the game moves to the next player four seconds later. The viewer's own keys still change player | [`goldsrc_spectator_camera.md`](goldsrc_spectator_camera.md) |
| `ds_spec_deathcam` | `0` | HLTV demos: when the player being watched in first person dies, the view becomes the death cam their own recording shows, the camera 64 units behind their body pulling back and turning to the killer, with no gun, for 2.4 s; then it is the game's own view from their eyes again until they respawn. Without it the game rolls the dead player's eye view 80° and the dropped weapon fills the clip. A switch of its own, not part of `ds_spec_match_pov`: a movie wants it, a shoutcast following the fight does not. Stands down in a POV demo (`goldsrc-hooks/src/spectator_deathcam.rs`) | [`goldsrc_spectator_camera.md`](goldsrc_spectator_camera.md) |
| `ds_hud_overview_players` | `0` | which players get an icon on the overview map: `0` the game's own (the team of the player being watched), `1` everyone, `2` only the other team, `3` only the player being watched (the recorder in a POV demo), `4` nobody. A POV demo only holds the enemies the recorder was sent, so in one they come and go; HLTV demos have everybody | `goldsrc-hooks/src/overview_players.rs` |
| `ds_player_overrides` | `0` | sprays and crosshairs in demos follow Studio's Players table (Configuration > Players, #428); 0 leaves both to the game. Studio sets it from the tab's "Use in the game" box when it starts the game, a review or a batch, and sends it to a running game when the box changes. A flip reaches the crosshair on the next frame, and sprays loaded after it: a spray already on a wall keeps its texture, so reload the demo to compare | `goldsrc-hooks/src/player_overrides.rs`, `player_crosshair.rs`, `spray.rs` |
| `ds_view_use_cl_bob` | `0` | a POV demo bobs the view and the gun by the `cl_bob` (and `cl_bobcycle`, `cl_bobup`) set now (#359). At `0` a POV demo shows the bob it recorded, the recording player's own settings (some recorders had none), because the engine replaces the view `client.dll` works out live with the recorded one (HLTV demos already follow the cvars). At `1` the recorded bob is swapped for the live one and keeps the rest of the recorded view. The gun's bob tilt stays out, as in every POV demo | `goldsrc-hooks/src/pov_live_bob.rs` |
| `ds_capture_run_in_background` | `0` | the game runs at full speed while another window is in front. The engine otherwise waits up to 20 ms for input every frame while it isn't the active app (`CEngine::Frame` calls `SleepUntilInput` when `IsActiveApp` is false), about 45 frames a second, which slows a capture batch's fast-forward between clips 5-8x; this skips that wait. A menu left in the background keeps drawing at full speed while it's on | `goldsrc-hooks/src/run_in_background.rs` |
| `ds_capture_stay_behind` | `0` | the game window stays behind the other windows while it records: HLAE no longer brings it to the front, topmost, for every recording. A minimised game records black frames (its window owns no pixels), so while this is on a minimise parks the game off-screen instead: restored without being activated and moved left of the whole desktop, same size, taskbar button kept (#723). Activating it (its taskbar button, alt-tab) or turning this off moves it back where it was. Studio turns it on for every batch unless Keep the Game Behind Your Windows While Capturing is off | `goldsrc-hooks/src/hlae_window.rs` |
| `ds_hud_overview_team_marker` | `0` | HLTV: marks the player being watched on the overview map with their team's camera icon (`allies_camera.spr` / `axis_camera.spr` / `brit_camera.spr`) instead of `spec_camera.spr`. DoD Studio's bordered ones go in `dod_addon/sprites` | `goldsrc-hooks/src/overview_marker.rs` |
| `ds_hud_overview_team_marker` | `0` | which camera icon marks the player being watched on the overview map: `0` the game's own, `1` their team's in HLTV too, `2` none. `1` uses `allies_camera.spr` / `axis_camera.spr` / `brit_camera.spr` instead of `spec_camera.spr`; DoD Studio's bordered ones go in `dod_addon/sprites`. `2` works in POV and HLTV, so `spec_camera.spr` needs no blanking, and the player icons stay (#677) | `goldsrc-hooks/src/overview_marker.rs` |
| `ds_hud_overview_hide_speaker` | `0` | leaves the speaker icon (`mapsprites/speakerIcon.spr`) off the players' icons on the overview map, so the sprite needs no blanking; the player icons stay, unlike `ds_hud_overview_players 4`. What makes the game draw it (a player talking or using a voice command) is a guess (#677) | `goldsrc-hooks/src/overview_speaker.rs` |
| `ds_view_hide_headshot_red_screen` | `0` | POV demos: drops the red screen flash (a server `ScreenFade` of (128,0,0) at alpha 200, flags 0) that comes with every hit on the player's head hitbox, fatal or not, grenades included (#606). The black fades (the clear at respawn, the end of the map) still play | `goldsrc-hooks/src/screen_effects.rs` |
| `ds_view_hide_screen_shake` | `0` | POV demos: drops every server `ScreenShake`, which in DoD demos is the blast shake from a grenade going off nearby (always amplitude 15 for 1 s; no WSOD25 map has an `env_shake`) (#606) | same |
| `ds_hud_show_scope` | `1` | shows the sniper scope while a demo plays. The game turns it off for playback (`CHudScope::DrawTriApiScope` skips its draw when `IsPlayingback()`). A POV demo shows the recorder's own scope whenever they zoomed. Watching in-eye, the game always picks the Springfield's scope; with this on it is the watched player's rifle: the scoped K98 and scoped FG42 get `scope_k43`, the Springfield `scope_spring`, the scoped Enfield `scope_enfield` (that one only when the game's scoped-weapon flag is set, which an HLTV viewer may not have). Still only while the view is zoomed (#307). The round scope keeps its size and everything outside its frame is black, at any FOV: DoD sizes the frame for the view it expects, so `gl_widescreen_yfov 1` (25th Anniversary) or a wider `mirv_fov` used to show the map around it; demos only, live play keeps the game's own scope (#799) | `goldsrc-hooks/src/demo_scope.rs`, `goldsrc-hooks/src/demo_scope/fill.rs`, [`goldsrc_hud_elements.md`](goldsrc_hud_elements.md) |
| `ds_player_hide_hand_signals` | `0` | replaces any `hs_*` body sequence (the nod, the point, the wave -- players miming their own voice commands) with that player's last ordinary one, for everyone in view | [`goldsrc_hltv_animation_fix.md`](goldsrc_hltv_animation_fix.md) §12 |
| `ds_hud_hide_voice_menus` | `0` | hides the numbered menus a POV demo's player picked voice commands and hand signals from: the three voice-command menus (`voice_menu1`-`3`, which the server sends as `ShowMenu` with the nine `#Menu_*Voice*` keys) and the command menu `+commandmenu` opens, where the hand signals are. The menus still take the recorded number presses, so weapon switches replay as recorded; other `ShowMenu` text menus (server plugin votes) still show. While on, `+commandmenu` doesn't open in live play either. Turning it on or off with a voice menu up hides or shows it at once | `goldsrc-hooks/src/voice_menus.rs` |
| `ds_hud_hide_map_text` | `0` | hides the text a map puts on screen itself -- the `dod_anzio` mortar warning, the round result -- by matching each `HudText` message against the `message` strings the loaded map's own entities declare. DoD's own prompts on the same channel (`#Clan_allies_ready` and friends) still show. Reads the map's BSP once per level | `goldsrc-hooks/src/map_text.rs` |
| `ds_hud_hide_clan_text` | `0` | hides all of DoD's clan-match text: the warm-up and ready rules, "Warmup Mode" on the left, the countdown to the match and "MATCH IS LIVE!". Drops the `#clan_*` `TextMsg` keys, swallows `ClanTimer` (which draws both the countdown and "MATCH IS LIVE!") and clears `GameRules`' warm-up flag. Turning it off mid-countdown brings the countdown back at once; turning it on mid-countdown removes "Warmup Mode" but not yet a countdown already showing (#618). Leaves everything else alone, round-start text included | `goldsrc-hooks/src/clan_text.rs` |
| `ds_hud_hide_center_text` | `0` | hides the text the server prints in the middle of the screen: every `TextMsg` to `HUD_PRINTCENTER` (destination 4), such as match plugins' `print_center` countdowns and notices. Chat, joins and leaves (destination 3) still show. Turning it on also ends the centre line showing at that moment. Takes the server's copy of the spectator camera mode's name too, but not the game's own (`ds_spec_hide_text` hides both); doesn't reach the clan countdown (`ds_hud_hide_clan_text`) or the engine's own `svc_centerprint` (#677) | `goldsrc-hooks/src/center_text.rs` |
| `ds_demo_ex_interp_max` | `100` (the engine's own ceiling) | raises the engine's clamp on `ex_interp` above its stock 100 ms ceiling, for smoother entity motion between snapshots; refuses `<=50` or `>1000`. Mechanism live-proven on pre-Anniversary, no specific value settled on yet. On the 25th Anniversary build (where HLTV demos already get 200 ms) any value but the default sets both paths | [`goldsrc_ex_interp.md`](goldsrc_ex_interp.md) |
| `ds_hd_enabled` | `1` if there's a `dodstudio_hd` folder (in `dod_addon`, or `dod` for an install built before #415), else `0`; `GOLDSRC_HOOKS_TEXTURE_HIRES=1`/`0` at launch overrides | HD textures on/off: map textures, model skins, sprites (world and HUD, #728), decals, detail textures and skies from `dodstudio_hd`, and player sprays from PNGs in `logos` (`ds_player_spray`). `ds_debug_status` counts each kind's loads and replacements (HUD sprites apart from world sprites). A change applies to what loads next -- walls, detail and skies from the next map, models, sprites and decals already loaded after a restart. Turning it on in a session that started off installs the hook then | `goldsrc-hooks/src/texture_hires.rs`, `goldsrc-hooks/tools/hd/README.md` |
| `ds_hd_style` | `ultrasharp` | which `dodstudio_hd/<type>/<style>` folder to use; a name with no folder means originals (plus `overrides`). Same timing as `ds_hd_enabled` | same |
Expand Down
21 changes: 16 additions & 5 deletions docs/goldsrc_hud_elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,26 @@ ever been seen to make it draw:

### `vgui2print` is narrower than its name suggests

`CHudVGUI2Print` has three real callers, but only one goes through the
vtable slot this element hides:
`CHudVGUI2Print` has two queueing methods and a drawing method. Only the
queued text goes through the vtable slot this element hides:

- `CHudDoDCommon` (key `Dod_mg_reload` -- "Deploy your machine gun to
Reload!") calls a *queueing* method (`client+0x3a990`) that only writes
text/position/color/expiry into `CHudVGUI2Print`'s own fields. The pixels
are drawn later, when `CHudVGUI2Print::Draw` itself runs and reads that
state -- the vtable-dispatched path this element's hide actually
intercepts. Live-confirmed.
- A second queueing method (`client+0x3a830`, the same fields) takes every
centre-print: each `HUD_PRINTCENTER` `TextMsg` (`MsgFunc_TextMsg`'s
destination-4 branch, `client+0x4c3e9`), which includes the spectator
camera mode's name (`CHudSpectator::SetMode` calls `MsgFunc_TextMsg`
directly), and the clan-match countdown (`DrawClanTimer`,
`client+0x2d3f7`). `CHudVGUI2Print::Draw` (`client+0x3aa10`) draws these
too, so hiding `vgui2print` should hide **all centre text** along with the
MG-reload hint. Found offline (#677), not checked in the game. The narrow
switches are `ds_hud_hide_center_text` (server centre text),
`ds_spec_hide_text` (the camera mode's name) and
`ds_hud_hide_clan_text` (the countdown).
- `CObjectiveIcons` (key `clan_warmup_mode` -- "Warmup Mode") calls a
*drawing* method (`client+0x3a3f0`) that renders immediately via a shared
low-level helper (`client+0x3a4b0`) at a fixed address, never touching
Expand All @@ -166,9 +177,9 @@ vtable slot this element hides:
`menu`'s own vftable hide is what stops this, by not letting
`CHudMenu::Draw` run at all.

So `vgui2print` only ever hides queued, timed prompts like the MG-reload
message. Anything drawn immediately through the shared helper is a different
code path this element's vtable slot cannot see.
So `vgui2print` only ever hides queued, timed prompts: the MG-reload message
and every centre-print. Anything drawn immediately through the shared helper
is a different code path this element's vtable slot cannot see.

### `all 1` is refused

Expand Down
160 changes: 160 additions & 0 deletions goldsrc-hooks/src/center_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! `ds_hud_hide_center_text`: drop every `TextMsg` the server sends to the
//! middle of the screen (`HUD_PRINTCENTER`, destination 4) -- match plugins'
//! `print_center` countdowns and notices among them (#677).
//!
//! ## Where it comes from
//!
//! `CHudTextMessage::MsgFunc_TextMsg` (`+0x4c250`) switches on the first
//! byte. Destination 4 goes to `+0x4c36a`, which posts the line to the
//! centre-print object (`+0x175c6c`) through `+0x3a830`; it then shows until
//! 4 s after the post. Destinations 1 (notify), 2 (console) and 3 (chat) go
//! elsewhere and are left alone, so joins, leaves and chat lines still show.
//!
//! The only narrower-than-this switch before was `ds_hud_hide_element
//! vgui2print 1`, which also takes the machine gun's "Deploy to reload" hint.
//!
//! ## What it doesn't reach
//!
//! - The spectator camera mode's name: `CHudSpectator::SetMode` calls
//! `MsgFunc_TextMsg` directly, not through the message dispatcher, so this
//! never sees the client's own copy. (The server's copy, a `#Spec_Mode`
//! `TextMsg` a demo carries, is centre text like any other and goes.)
//! `ds_spec_hide_text` hides both.
//! - The viewport's own way in: entry 13 of the function table at
//! `+0xc1aa0` (`+0x20480`) also calls `MsgFunc_TextMsg` directly. What, if
//! anything, uses it has not been looked into.
//! - The clan-match countdown: `ClanTimer` posts to the same object every
//! frame without a `TextMsg`; `ds_hud_hide_clan_text` covers it.
//! - The engine's own `svc_centerprint`, which `client.dll` never sees.
//!
//! ## Mechanism
//!
//! `clan_text` already keeps a `TextMsg` handler at the front of the engine's
//! list on every level, so its filter asks [`hide`] rather than a second
//! handler taking turns with it. Turning the cvar on also ends the line on
//! screen at that moment (`clan_text::end_center_line`), so a notice isn't
//! left up for its last 4 s.

use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};

use crate::engine::CvarSPartial;
use crate::names::console_name;

pub const NAME: &str = console_name!("hud_hide_center_text");

/// `TextMsg`'s destination byte for the middle of the screen.
const HUD_PRINTCENTER: u8 = 4;

/// Hidden messages logged one by one before only the count goes on.
const LOGGED_MAX: u32 = 20;

static CVAR: AtomicPtr<CvarSPartial> = AtomicPtr::new(std::ptr::null_mut());
/// The cvar's value as last seen: what the message handler reads.
static WANTED: AtomicBool = AtomicBool::new(false);
/// Messages hidden this session.
static HIDDEN: AtomicU32 = AtomicU32::new(0);

/// Called by `commands.rs` once the cvar is registered.
pub fn set_cvar(cvar: *mut CvarSPartial) {
CVAR.store(cvar, Ordering::Release);
}

/// Whether a `TextMsg` payload (destination byte, then the text or key) goes
/// to the middle of the screen.
fn is_center_text(payload: &[u8]) -> bool {
payload.first() == Some(&HUD_PRINTCENTER)
}

/// Whether `clan_text`'s `TextMsg` filter should drop this payload. Called
/// with the game's message handling waiting on the answer, so it costs an
/// atomic load and a byte compare unless it hides something.
pub(crate) fn hide(payload: &[u8]) -> bool {
if !WANTED.load(Ordering::Relaxed) || !is_center_text(payload) {
return false;
}
let count = HIDDEN.fetch_add(1, Ordering::Relaxed) + 1;
if count <= LOGGED_MAX {
let text = payload[1..].split(|&b| b == 0).next().unwrap_or_default();
unsafe {
crate::debug::report(&format!(
"center_text: hid \"{}\"{}",
String::from_utf8_lossy(text).trim(),
if count == LOGGED_MAX {
" (the rest are only counted)"
} else {
""
}
))
};
}
true
}

/// Follows the cvar. Called every frame from `commands::poll`.
pub fn poll() {
let cvar = CVAR.load(Ordering::Acquire);
if cvar.is_null() {
return;
}
let wanted = unsafe { (*cvar).value } != 0.0;
if wanted == WANTED.swap(wanted, Ordering::Relaxed) {
return;
}
let cleared = if wanted {
match crate::clan_text::end_center_line() {
Ok(()) => ", the line on screen ended".to_string(),
Err(why) => format!(", a line on screen stays up to 4 s: {why}"),
}
} else {
String::new()
};
unsafe {
crate::debug::report(&format!(
"center_text: {NAME} = {}{cleared}",
if wanted {
"1 (server centre text hidden)"
} else {
"0 (normal)"
}
))
};
}

/// A `ds_debug_status` line, or `None` while off and never used.
pub fn status_line() -> Option<String> {
let wanted = WANTED.load(Ordering::Relaxed);
let hidden = HIDDEN.load(Ordering::Relaxed);
if !wanted && hidden == 0 {
return None;
}
Some(format!(
"{NAME} = {} -- {hidden} centre-text message(s) hidden this session",
u8::from(wanted)
))
}

#[cfg(test)]
mod tests {
use super::*;

fn text_msg(dest: u8, text: &str) -> Vec<u8> {
let mut out = vec![dest];
out.extend(text.as_bytes());
out.push(0);
out
}

#[test]
fn only_destination_four_is_centre_text() {
assert!(is_center_text(&text_msg(4, "Match starts in 10")));
assert!(is_center_text(&text_msg(4, "#Clan_time_remaining")));
for dest in [1, 2, 3, 5, 0] {
assert!(
!is_center_text(&text_msg(dest, "#Game_joined_game")),
"{dest}"
);
}
assert!(!is_center_text(&[]));
assert!(is_center_text(&[4]), "an empty centre line is still one");
}
}
Loading
Loading