read what the binary says about itself: names, signatures, prototypes; gen v2
This commit is contained in:
parent
54ef572202
commit
c458b4cb50
34 changed files with 58363 additions and 192 deletions
544
src/valvetab.rs
Normal file
544
src/valvetab.rs
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
//! 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<String>,
|
||||
/// The author-facing documentation string ("The entity origin (absolute).").
|
||||
pub description: Option<String>,
|
||||
/// 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,
|
||||
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<String>,
|
||||
/// 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<DatadescOutput> {
|
||||
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<EntityClass> {
|
||||
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<String> {
|
||||
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<PulseBinding> {
|
||||
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,
|
||||
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<DatadescInput> {
|
||||
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<DatadescInput>,
|
||||
}
|
||||
|
||||
/// 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<DatadescArray> {
|
||||
let named = |at: u64| img.read_ptr(at).and_then(|p| table_string(img, p));
|
||||
let input_at = |at: u64| -> Option<DatadescInput> {
|
||||
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<ValveName>, 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<String> = 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<u64>> = 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<ValveName> = 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<int>::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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue