//! Valve's in-binary NAME tables — the one place a stripped Source-2 module names its own functions. //! //! Two static tables sit in writable data, and they give different things: //! //! - **Entity-IO datadesc** (112-byte stride, in the game library) pairs a NAME with the FUNCTION: the //! C++ handler name (`InputKill`), the map-facing input name (`Kill`), and the handler's address. //! - **Pulse bindings** (80-byte stride, in every module registering Pulse cells or an entity API) //! pairs a fully-qualified `Class::Method` with the author-facing display name and description, two //! metadata words — and two code pointers that are DESCRIPTOR ACCESSORS, not the bound function //! (see [`names`]). It documents the callable surface; it does not locate it. //! //! Both are ground truth from the shipped binary — the names are what Valve compiled in, not a transfer //! from another game and not an inference — so they outrank every other naming source, and extracting //! them belongs beside the RTTI and schema readers rather than in a side-channel: the source travels //! with the binary, so it re-derives on every build for free and needs no genesis input. //! //! Both readers are deliberately shape-driven, not offset-driven: a record is accepted only when its //! name pointer resolves to a plausibly-shaped string AND its function pointer lands in executable //! code. A layout change therefore yields FEWER records, never wrong ones. use crate::elf::CodeImage; use std::collections::HashMap; /// Bytes of one Pulse binding record. The scan steps 8 bytes rather than by stride (a table's first /// record is not aligned to any section-relative boundary), so this only bounds the read. const PULSE_STRIDE: usize = 80; /// Bytes of one entity-IO datadesc record — a `typedescription_t`. The array interleaves plain field /// descriptors, inputs and outputs at this one stride; the `Input` name prefix selects the inputs. const DATADESC_STRIDE: usize = 112; /// Longest string either table is expected to hold. Descriptions are prose, names are identifiers; a /// pointer that resolves to anything longer is not a record field, so the cap doubles as a validity gate. const MAX_STR: usize = 256; /// One Pulse scripting binding, as the module registers it. #[derive(Clone, Debug)] pub struct PulseBinding { /// Fully-qualified `Class::Method`. pub name: String, /// The author-facing label Valve shows in the Pulse graph editor ("Get Abs Origin"). pub display: Option, /// The author-facing documentation string ("The entity origin (absolute)."). pub description: Option, /// Accessor returning the binding's static descriptor — a lazy-init singleton, NOT the bound /// function. Useful as the anchor a runtime walks to reach the descriptor; useless as a locator. pub descriptor: u64, /// A second accessor of the same shape, for the binding's argument descriptor. pub arg_descriptor: u64, /// The binding's own INVOCATION shim — the one code pointer in the record that is an entry point /// rather than a descriptor accessor. Zero when the slot holds no executable code (8 of 485 on CS2). /// /// Measured as a fixed-signature marshalling stub: seven integer arguments returning int, where the /// fifth is an array of pointers to the argument values (element *k* at `+8+8k`) and the seventh is an /// output sink read by exactly the bindings that declare a return. /// /// It IS emitted, and the measurement above is not the reason to trust it — the live oracle is. The /// address and its measured read-set ship as `surfaces.pulse[].shim` / `.call`; `verify_pulse_shims` /// actually CALLS every `args-only` shim on a running server each build (CS2 186 of 193 clean, Dota /// 211 of 211); and `GameProfile::min_pulse_callable` floors the population that survives. A locator /// nobody has exercised would be a claim — this one is exercised every derive. pub shim: u64, pub flags: PulseFlags, } /// The metadata words a Pulse binding carries, decoded. /// /// Pulse is Source 2's TYPED graph VM, so a binding cannot be registered without the engine knowing how /// it may be called — and it records that as data. Each decoded flag below is named for what it was /// measured to separate across the 1,217 CS2 bindings, not for what its field is presumed to mean: /// every one of them partitions the table almost perfectly along a naming convention that is /// independent of the bytes. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct PulseFlags { /// Free-function binding on a library class — no receiver. Set for every `CPulse*lib` / `*Funcs` /// binding and, being the complement of `instance`, never together with it. pub library: bool, /// Binding requires an entity receiver. Separates the `*API::` classes from everything else /// exactly: 345 of 353 `*API::` bindings set it, and 0 of the other 864 do. pub instance: bool, /// Binding writes state — the const-correctness bit. Across the entity APIs it is set on 100% of /// `Set`/`Add`/`Remove`/`Destroy`/`Turn`/`Toggle`/`Play`/`Stop` bindings and on ~0% of /// `Get`/`Is`/`Has`/`Find` ones. pub mutating: bool, /// Binding may suspend the calling cursor rather than returning within the frame — every record /// carrying it is a `Wait` / `Yield` / `Pause` / timer-`Start` / long-running-sequence binding. pub blocking: bool, /// The two undecoded words as read, so a consumer can re-derive meaning if a later build repurposes /// a bit rather than silently inheriting today's reading. pub raw: (u32, u32), } impl PulseFlags { // Both words hold their booleans one-per-BYTE rather than packed one-per-bit, which is what a // plain `bool` struct member compiles to — so the flags are read as byte tests, not masks. fn decode(w0: u32, w1: u32) -> Self { PulseFlags { library: w0 & 0xff != 0, mutating: w0 >> 8 & 0xff != 0, instance: w0 >> 16 & 0xff != 0, blocking: w1 >> 3 & 1 != 0, raw: (w0, w1), } } } /// The byte offset a FIELD descriptor records its member at. Established against the SchemaSystem at /// **601/601** on records genuinely inside a datadesc array — the qualification that matters, because a /// sweep of all writable data mostly finds the schema's OWN field tables, which carry a name pointer and /// an offset too and therefore match themselves. const FIELD_OFFSET_SLOT: u64 = 8; /// One entity-IO input handler: the C++ method name, the input name a map fires, and the handler. #[derive(Clone, Debug)] pub struct DatadescInput { /// The C++ handler name, e.g. `InputKill`. Not class-qualified BY THE RECORD — the descriptor /// carries no owning class. [`datadesc_arrays`] recovers it from the array instead; see `class`. pub handler: String, /// The class that owns this handler, where the array it sits in identified one. `InputEnable` is a /// distinct handler on 48 classes, and this is what tells them apart. pub class: Option, /// The entity-IO input name a map or another entity fires, e.g. `Kill`. pub io_name: String, pub func: u64, } /// One entity-IO output: an event an entity fires, and the member holding its subscriber list. /// /// The mirror of [`DatadescInput`] in the same array — an input is something you SEND an entity, an output /// is something it TELLS you, which is what a mod hooks to react to gameplay. Carries a member OFFSET /// rather than a function pointer: an output is data on the instance, not code. #[derive(Clone, Debug)] pub struct DatadescOutput { /// The member holding the output, e.g. `m_OnStartTouch`. pub member: String, /// The entity-IO name a map wires to, e.g. `OnStartTouch`. Usually the member minus `m_`, but NOT /// reliably so (`m_OnBombExplode` fires `BombExplode`), which is why both are kept. pub output: String, /// Byte offset of the member within its entity. pub offset: u32, } /// Every entity-IO output the game library declares. /// /// Shares the array and the stride with [`datadesc_inputs`]; the discriminator is structural rather than /// lexical — an output has NO handler at `+40` (measured: zero for all 226 CS2 outputs, non-zero for every /// input), because it is a subscriber list rather than a function. pub fn datadesc_outputs(img: &CodeImage) -> Vec { let mut out = Vec::new(); scan_records(img, DATADESC_STRIDE, |at| { let Some(member) = img.read_ptr(at).and_then(|p| table_string(img, p)) else { return; }; // Valve's output convention, the counterpart of the `Input` prefix the input reader keys on. if !member.starts_with("m_On") || member.len() <= 4 { return; } let (Some(output), Some(handler)) = ( img.read_ptr(at + 24).and_then(|p| table_string(img, p)), img.read_ptr(at + 40), ) else { return; }; // A handler here means this is an INPUT record that happens to be named `m_On…`. if handler != 0 { return; } let Some(offset) = img.read_u32(at + 8).filter(|&o| o > 0 && o < (1 << 20)) else { return; }; out.push(DatadescOutput { member, output, offset, }); }); out.sort_by(|a, b| (&a.output, &a.member, a.offset).cmp(&(&b.output, &b.member, b.offset))); out.dedup_by(|a, b| a.member == b.member && a.output == b.output && a.offset == b.offset); out } /// One entity-factory record: the classname a map spawns by, and the C++ class it constructs. /// /// Carries no function pointer — this is the vocabulary a level designer writes (`func_door`) bound to /// the class the schema describes (`CBaseDoor`), which is the join a consumer needs to spawn or identify /// an entity by name. It is NOT a naming source. #[derive(Clone, Debug)] pub struct EntityClass { /// The map-facing classname, e.g. `func_door`. pub classname: String, /// The C++ class it constructs, e.g. `CBaseDoor` — a schema-registered class. pub class: String, } /// Every entity classname the game library binds to a class. /// /// Two adjacent string pointers, gated on their SHAPES being complementary: a map classname is /// lowercase-with-underscores by Valve's own convention, and a C++ class name starts uppercase. That /// asymmetry is what separates these records from the many other adjacent string pairs in `.data`. pub fn entity_classes(img: &CodeImage) -> Vec { let map_name = |s: &str| { s.len() >= 3 && s.starts_with(|c: char| c.is_ascii_lowercase()) && s.bytes() .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_') }; // Source's universal class prefix: a capital `C` followed by another capital. Plain // uppercase-first is far too loose — `.data` is full of adjacent string pairs, and it accepted // `chicken_server` -> `GameSessionManifest_server`, two unrelated neighbours. let cpp_name = |s: &str| { s.len() >= 3 && s.starts_with('C') && s.as_bytes()[1].is_ascii_uppercase() && s.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_') }; let mut out = Vec::new(); let mut seen = HashMap::new(); scan_records(img, 16, |at| { let (Some(classname), Some(class)) = ( img.read_ptr(at).and_then(|p| table_string(img, p)), img.read_ptr(at + 8).and_then(|p| table_string(img, p)), ) else { return; }; if !map_name(&classname) || !cpp_name(&class) { return; } // One binding per classname; a duplicate that disagrees is dropped rather than guessed at. match seen.entry(classname.clone()) { std::collections::hash_map::Entry::Occupied(e) => { if *e.get() != class { out.retain(|x: &EntityClass| x.classname != classname); } } std::collections::hash_map::Entry::Vacant(e) => { e.insert(class.clone()); out.push(EntityClass { classname, class }); } } }); out.sort_by(|a, b| a.classname.cmp(&b.classname)); out } /// A name the binary vouches for, at an address it can be trusted to LOCATE. #[derive(Clone, Debug)] pub struct ValveName { pub name: String, pub addr: u64, } /// Read the NUL-terminated string at `vaddr`, rejecting anything that is not a short printable-ASCII /// run. Every table field this validates is a C identifier or a UI string, so a pointer that lands on /// binary data fails here rather than being carried forward as a garbage name. fn table_string(img: &CodeImage, vaddr: u64) -> Option { if vaddr == 0 { return None; } let s = img.read_c_string(vaddr)?; (!s.is_empty() && s.len() <= MAX_STR && s.bytes().all(|c| (0x20..0x7f).contains(&c))) .then_some(s) } /// Is `s` a fully-qualified `Class::Method` of C identifiers? The Pulse table's defining shape — and a /// strong enough gate on its own that a false positive would have to be a deliberately-planted string /// followed by two code pointers. fn is_qualified(s: &str) -> bool { let Some((class, method)) = s.split_once("::") else { return false; }; let ident = |p: &str| { let mut c = p.chars(); c.next() .is_some_and(|f| f.is_ascii_alphabetic() || f == '_') && c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') }; ident(class) && ident(method) } /// Walk every writable data section in 8-byte steps, calling `f` with each candidate record address. /// `stride` reserves the record's own length so a reader never runs off the end of its section. fn scan_records(img: &CodeImage, stride: usize, mut f: impl FnMut(u64)) { for (va, len) in img.data_blocks() { let Some(last) = len.checked_sub(stride) else { continue; }; for off in (0..=last).step_by(8) { f(va + off as u64); } } } /// Every Pulse binding `img` registers. pub fn pulse_bindings(img: &CodeImage) -> Vec { let mut out = Vec::new(); scan_records(img, PULSE_STRIDE, |at| { let Some(name) = img.read_ptr(at).and_then(|p| table_string(img, p)) else { return; }; if !is_qualified(&name) { return; } // Both code pointers are required: one alone also matches a plain `{name, …, fnptr}` pair, // whereas a second in the very next slot is specific to a binding record. let (Some(descriptor), Some(arg_descriptor)) = (img.read_ptr(at + 24), img.read_ptr(at + 32)) else { return; }; if !img.is_code(descriptor) || !img.is_code(arg_descriptor) { return; } out.push(PulseBinding { name, display: img.read_ptr(at + 8).and_then(|p| table_string(img, p)), description: img.read_ptr(at + 16).and_then(|p| table_string(img, p)), descriptor, arg_descriptor, shim: img .read_ptr(at + 72) .filter(|&p| img.is_code(p)) .unwrap_or(0), flags: PulseFlags::decode( img.read_u32(at + 56).unwrap_or(0), img.read_u32(at + 60).unwrap_or(0), ), }); }); out } /// Every entity-IO input handler `img` declares. pub fn datadesc_inputs(img: &CodeImage) -> Vec { let mut out = Vec::new(); scan_records(img, DATADESC_STRIDE, |at| { let Some(handler) = img.read_ptr(at).and_then(|p| table_string(img, p)) else { return; }; // `Input` + at least one more character: the naming convention Valve's entity-IO macros emit, // and what separates the input records from the field and output descriptors sharing the array. if !handler.starts_with("Input") || handler.len() <= 5 { return; } let (Some(io_name), Some(func)) = ( img.read_ptr(at + 24).and_then(|p| table_string(img, p)), img.read_ptr(at + 40), ) else { return; }; if !img.is_code(func) { return; } out.push(DatadescInput { handler, class: None, io_name, func, }); }); out } /// One datadesc array: the FIELD descriptors that identify its owning class, and the input handlers /// that class owns. pub struct DatadescArray { /// `(member name, byte offset)` for each field descriptor, which the SchemaSystem states /// independently — the fingerprint that names the array's class. pub fields: Vec<(String, i32)>, pub inputs: Vec, } /// Segment the datadesc into ARRAYS, so an input handler can be attributed to the class that owns it. /// /// The record carries no owning class, which is why `names` has to drop a handler name the table gives /// more than one address. But the array does: it also holds FIELD descriptors, and a field is a /// `(member, offset)` pair the SchemaSystem states from a completely different table. The class whose /// schema contains EVERY pair in an array owns the array, and therefore owns its handlers. /// /// An array is found from a confirmed input outward — a record with a name at `+0` continues it — so a /// run is anchored on something already known to be a datadesc record rather than on a shape guess. pub fn datadesc_arrays(img: &CodeImage) -> Vec { let named = |at: u64| img.read_ptr(at).and_then(|p| table_string(img, p)); let input_at = |at: u64| -> Option { let handler = named(at).filter(|h| h.starts_with("Input") && h.len() > 5)?; let io_name = img.read_ptr(at + 24).and_then(|p| table_string(img, p))?; let func = img.read_ptr(at + 40).filter(|&f| img.is_code(f))?; Some(DatadescInput { handler, class: None, io_name, func, }) }; let mut out = Vec::new(); let mut claimed: Vec<(u64, u64)> = Vec::new(); for (va, len) in img.data_blocks() { let Some(last) = len.checked_sub(DATADESC_STRIDE) else { continue; }; let end = va + last as u64; let mut at = va; while at <= end { if input_at(at).is_none() { at += 8; continue; } if claimed.iter().any(|&(s, e)| at >= s && at <= e) { at += DATADESC_STRIDE as u64; continue; } let stride = DATADESC_STRIDE as u64; let (mut lo, mut hi) = (at, at); while lo >= va + stride && named(lo - stride).is_some() { lo -= stride; } while hi + stride <= end && named(hi + stride).is_some() { hi += stride; } claimed.push((lo, hi)); let (mut fields, mut inputs) = (Vec::new(), Vec::new()); let mut cur = lo; while cur <= hi { if let Some(i) = input_at(cur) { inputs.push(i); } else if let Some(n) = named(cur).filter(|n| n.starts_with("m_") && n.len() > 3) && let Some(o) = img.read_u32(cur + FIELD_OFFSET_SLOT) { fields.push((n, o as i32)); } cur += stride; } if !inputs.is_empty() { out.push(DatadescArray { fields, inputs }); } at = hi + stride; } } out } /// The names the tables can be trusted to LOCATE, with AMBIGUOUS ones dropped. /// /// **Only the entity-IO datadesc qualifies.** The Pulse table's two code pointers are both descriptor /// accessors, not the bound function: every one of the 485 CS2 `libserver` bindings measures the same /// empty footprint (`int=0 float=0`), and they disassemble to a lazy-init singleton that returns a /// pointer to a static descriptor. Folding a Pulse name would ship `CBaseEntityAPI::GetAbsOrigin` /// pointing at a zero-argument accessor instead of the getter — a locator that resolves, passes live /// validation as executable code, and is still the wrong function. The bindings are shipped as a /// registry instead, where the address is labelled for what it is. /// /// A name is ambiguous when the table gives it more than one address in this image: `InputEnable` is a /// distinct handler on each of 48 classes and the record carries no owning class to tell them apart, so /// picking one is a coin flip. The whole name is dropped instead. Returns `(names, dropped)`. pub fn names(inputs: &[DatadescInput]) -> (Vec, usize) { // A handler whose array identified its class is keyed by the QUALIFIED name, which is what makes it // unambiguous: `InputEnable` on 48 classes is 48 distinct names once each carries its own. Only the // ones still unqualified can collide, and those still drop rather than guess. let qualified: Vec = inputs .iter() .map(|i| match &i.class { Some(c) => format!("{c}::{}", i.handler), None => i.handler.clone(), }) .collect(); let mut by_name: HashMap<&str, Vec> = HashMap::new(); for (i, q) in inputs.iter().zip(&qualified) { let e = by_name.entry(q.as_str()).or_default(); if !e.contains(&i.func) { e.push(i.func); } } let dropped = by_name.values().filter(|a| a.len() > 1).count(); let mut out: Vec = by_name .into_iter() .filter(|(_, a)| a.len() == 1) .map(|(name, a)| ValveName { name: name.to_string(), addr: a[0], }) .collect(); out.sort_by(|x, y| x.name.cmp(&y.name)); // deterministic fold order (out, dropped) } #[cfg(test)] mod tests { use super::*; #[test] fn qualified_names_are_two_c_identifiers() { assert!(is_qualified("CBaseEntityAPI::GetAbsOrigin")); assert!(is_qualified("_Priv::_m0")); assert!(!is_qualified("CBaseEntityAPI")); // unqualified assert!(!is_qualified("::GetAbsOrigin")); // empty class assert!(!is_qualified("CFoo::9Bar")); // method starts with a digit assert!(!is_qualified("CFoo::Bar")); // a demangled template, not a table string assert!(!is_qualified("Get Abs Origin")); // the display name, not the qualified one } #[test] fn a_class_qualified_handler_is_no_longer_ambiguous() { // The same handler NAME on two classes is a collision only while both are unqualified. Once the // array's field descriptors identify the owner, they are two distinct names and BOTH survive — // which is the whole point of reading the array rather than the record. let q = |handler: &str, class: &str, func| DatadescInput { handler: handler.into(), class: Some(class.into()), io_name: "Enable".into(), func, }; let (names, dropped) = names(&[ q("InputEnable", "CBaseDoor", 0x100), q("InputEnable", "CBaseTrigger", 0x200), ]); assert_eq!(dropped, 0); assert_eq!(names.len(), 2); assert_eq!(names[0].name, "CBaseDoor::InputEnable"); assert_eq!(names[1].name, "CBaseTrigger::InputEnable"); } #[test] fn a_handler_name_at_several_addresses_is_dropped_not_guessed() { let input = |handler: &str, io: &str, func| DatadescInput { handler: handler.into(), class: None, io_name: io.into(), func, }; let (names, dropped) = names(&[ input("InputKill", "Kill", 0x100), // The same handler NAME on two classes — the record carries no owning class, so neither // address can be claimed for the bare name. input("InputEnable", "Enable", 0x200), input("InputEnable", "Enable", 0x300), // The same handler serving two inputs is ONE function, not an ambiguity. input("InputToggle", "Toggle", 0x400), input("InputToggle", "ToggleAlias", 0x400), ]); assert_eq!(dropped, 1); let got: Vec<(&str, u64)> = names.iter().map(|n| (n.name.as_str(), n.addr)).collect(); assert_eq!(got, [("InputKill", 0x100), ("InputToggle", 0x400)]); // sorted, no InputEnable } #[test] fn flags_decode_per_byte_not_per_bit() { // A library getter: library byte set, mutating and instance clear. let lib_get = PulseFlags::decode(0x00_00_01, 0); assert!(lib_get.library && !lib_get.mutating && !lib_get.instance); // An entity setter: instance + mutating, library clear. let api_set = PulseFlags::decode(0x01_01_00, 0); assert!(api_set.instance && api_set.mutating && !api_set.library); // An entity getter: instance only. let api_get = PulseFlags::decode(0x01_00_00, 0); assert!(api_get.instance && !api_get.mutating); // The yield bit lives in the second word. assert!(PulseFlags::decode(0x00_01_00, 0x08).blocking); assert!(!PulseFlags::decode(0x00_01_00, 0x20).blocking); // The raw words survive decoding so a later build can be re-read. assert_eq!(api_set.raw, (0x01_01_00, 0)); } }