act on what the binary declares: callable Pulse shims, ConVars, string anchors; gen v2.1
This commit is contained in:
parent
3de955c4ff
commit
71ce34edd2
14 changed files with 1507 additions and 54 deletions
338
src/pipeline.rs
338
src/pipeline.rs
|
|
@ -968,16 +968,19 @@ fn fold_valve_tables(
|
|||
source_build: source_build.to_string(),
|
||||
pulse: 0,
|
||||
pulse_typed: 0,
|
||||
pulse_callable: 0,
|
||||
entity_inputs: 0,
|
||||
entity_outputs: 0,
|
||||
entity_classes: 0,
|
||||
commands: 0,
|
||||
convars: 0,
|
||||
},
|
||||
pulse: BTreeMap::new(),
|
||||
entity_inputs: Vec::new(),
|
||||
entity_outputs: Vec::new(),
|
||||
entity_classes: BTreeMap::new(),
|
||||
commands: Vec::new(),
|
||||
convars: Vec::new(),
|
||||
},
|
||||
};
|
||||
let (mut folded, mut ambiguous, mut unmakeable) = (0u32, 0usize, 0u32);
|
||||
|
|
@ -1005,6 +1008,22 @@ fn fold_valve_tables(
|
|||
// table-emptiness skip below.
|
||||
let commands = concmd::console_commands(&img);
|
||||
let lib = lib_name_from_file(f);
|
||||
// ConVars: the other half of the console surface, read by the same pass. Documentation, not a
|
||||
// locator — a consumer finds a convar by name at runtime; the flags are what it cannot get itself.
|
||||
out.bindings
|
||||
.convars
|
||||
.extend(
|
||||
concmd::convars(&img, &lib)
|
||||
.into_iter()
|
||||
.map(|c| model::ConVar {
|
||||
name: c.name,
|
||||
library: c.library,
|
||||
description: c.description,
|
||||
flags: c.flags,
|
||||
flags_raw: c.flags_raw,
|
||||
addr: c.addr,
|
||||
}),
|
||||
);
|
||||
{
|
||||
let addrs: Vec<u64> = commands.iter().map(|c| c.handler).collect();
|
||||
let sigs = parallel_map(&addrs, threads, |&a| emit::make_sig(&img, a, sig_cap));
|
||||
|
|
@ -1260,6 +1279,16 @@ fn fold_valve_tables(
|
|||
returns: sig.as_ref().map(|s| s.returns.clone()).unwrap_or_default(),
|
||||
typed: sig.is_some(),
|
||||
descriptor: format!("{:#x}", b.descriptor),
|
||||
shim: (b.shim != 0).then(|| format!("{:#x}", b.shim)),
|
||||
// Measured per shim rather than assumed from the tier: the calling contract is fixed, but
|
||||
// WHICH slots a given shim reads is the whole difference between host-callable and not.
|
||||
call: (b.shim != 0)
|
||||
.then(|| pulse::shim_reads(&img, b.shim))
|
||||
.flatten()
|
||||
.map(|r| model::ShimCall {
|
||||
needs: r.needs().to_string(),
|
||||
reads: r.reads.iter().map(|s| s.to_string()).collect(),
|
||||
}),
|
||||
};
|
||||
// The registry is keyed by qualified name across libraries, so a binding registered by
|
||||
// more than one module keeps ONE row. That is the documented lossiness — of the LIBRARY,
|
||||
|
|
@ -1300,6 +1329,14 @@ fn fold_valve_tables(
|
|||
.sort_by(|a, b| (&a.input, &a.handler, &a.addr).cmp(&(&b.input, &b.handler, &b.addr)));
|
||||
out.bindings.meta.pulse = out.bindings.pulse.len();
|
||||
out.bindings.meta.pulse_typed = out.bindings.pulse.values().filter(|b| b.typed).count();
|
||||
// Counted from the emitted rows rather than tallied during the fold, so the number in `meta` cannot
|
||||
// drift from the number of rows a consumer can actually act on.
|
||||
out.bindings.meta.pulse_callable = out
|
||||
.bindings
|
||||
.pulse
|
||||
.values()
|
||||
.filter(|b| b.call.as_ref().is_some_and(|c| c.needs == "args-only"))
|
||||
.count();
|
||||
out.bindings.meta.entity_inputs = out.bindings.entity_inputs.len();
|
||||
out.bindings
|
||||
.entity_outputs
|
||||
|
|
@ -1310,6 +1347,7 @@ fn fold_valve_tables(
|
|||
.commands
|
||||
.sort_by(|a, b| (&a.name, &a.addr).cmp(&(&b.name, &b.addr)));
|
||||
out.bindings.meta.commands = out.bindings.commands.len();
|
||||
out.bindings.meta.convars = out.bindings.convars.len();
|
||||
|
||||
if cmd_total > 0 {
|
||||
let libs: BTreeSet<&str> = out
|
||||
|
|
@ -1459,6 +1497,245 @@ fn binding_kind(f: valvetab::PulseFlags) -> model::BindingKind {
|
|||
/// Assemble the monolith in memory and render its CS# gamedata (the string the live validate stage checks).
|
||||
/// Writes NOTHING — `produce` holds the `Monolith` (to annotate it live) plus this render, and writes the
|
||||
/// monolith exactly once at the end (after live validation, if a game is present).
|
||||
/// Fold string anchors onto every monolith entry whose name carries one, in every tier.
|
||||
///
|
||||
/// Returns how many landed. That number is REPORTED rather than assumed because the two populations are
|
||||
/// independent: the catalogue says which names have anchors, the derive says which names got a locator, and
|
||||
/// an anchor for a name that never resolved has nowhere to go. A large gap is a fact about the build, not a
|
||||
/// bug — but it should be visible rather than inferred from an artifact diff.
|
||||
fn attach_anchors(mono: &mut model::Monolith, anchors: &BTreeMap<String, Vec<String>>) -> usize {
|
||||
let mut n = 0;
|
||||
for tier in [
|
||||
&mut mono.core,
|
||||
&mut mono.high_confidence,
|
||||
&mut mono.experimental,
|
||||
] {
|
||||
for (name, e) in tier.iter_mut() {
|
||||
if let Some(a) = anchors.get(name) {
|
||||
// Deduplicated on the way in: the same anchor can appear on several catalogue variants,
|
||||
// and this list ships in a byte-reproducible artifact.
|
||||
for s in a {
|
||||
if !e.locator.anchors.contains(s) {
|
||||
e.locator.anchors.push(s.clone());
|
||||
}
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// A string worth anchoring on: long enough to be distinctive, printable, and not a lone format specifier.
|
||||
///
|
||||
/// The thresholds are the knob this whole feature turns on. Loosening them raises coverage and lowers
|
||||
/// distinctiveness; they were measured, not guessed — at these values 4,322 of libserver's 70,288 functions
|
||||
/// have a unique anchor, and 27% of the shipped set does.
|
||||
fn usable_anchor(s: &str) -> bool {
|
||||
s.len() >= 8
|
||||
&& s.len() <= 200
|
||||
&& s.is_ascii()
|
||||
&& s.chars().filter(|c| c.is_ascii_alphanumeric()).count() >= 5
|
||||
}
|
||||
|
||||
/// Where every anchorable string in `img` is referenced FROM: string address -> the instruction addresses
|
||||
/// that load it, plus the string itself.
|
||||
///
|
||||
/// Deliberately instruction-level and function-agnostic. Attributing a string to a function needs function
|
||||
/// BOUNDARIES, and this binary does not reliably supply them — `.eh_frame_hdr` describes 8,327 of libserver's
|
||||
/// ~70,000 functions, so a `[entry, next_entry)` range routinely spans a real function plus one or more
|
||||
/// unindexed neighbours, and every neighbour's strings then look like the first function's. An instruction
|
||||
/// address, by contrast, is exactly what it is. The caller decides membership against an extent it walked
|
||||
/// itself, which is the only claim available that does not depend on the entry list being complete.
|
||||
fn string_refs(img: &CodeImage) -> HashMap<u64, (String, Vec<u64>)> {
|
||||
let mut entries = crate::locate::candidate_entries(img);
|
||||
entries.extend(img.eh_frame_functions().into_iter().map(|(s, _)| s));
|
||||
entries.sort_unstable();
|
||||
entries.dedup();
|
||||
|
||||
let mut out: HashMap<u64, (String, Vec<u64>)> = HashMap::new();
|
||||
for (i, &start) in entries.iter().enumerate() {
|
||||
let end = entries.get(i + 1).copied().unwrap_or(u64::MAX);
|
||||
let Some(code) = img.code_range(start, end) else {
|
||||
continue;
|
||||
};
|
||||
let mut insn = iced_x86::Instruction::default();
|
||||
let mut dec = iced_x86::Decoder::with_ip(64, code, start, iced_x86::DecoderOptions::NONE);
|
||||
while dec.can_decode() {
|
||||
dec.decode_out(&mut insn);
|
||||
if insn.is_invalid() || !insn.is_ip_rel_memory_operand() {
|
||||
continue;
|
||||
}
|
||||
let va = insn.ip_rel_memory_address();
|
||||
if let Some(e) = out.get_mut(&va) {
|
||||
e.1.push(insn.ip());
|
||||
} else if let Some(s) = img.read_c_string(va).filter(|s| usable_anchor(s)) {
|
||||
out.insert(va, (s, vec![insn.ip()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Instruction addresses reachable from `entry` by following control flow, and the string addresses it loads.
|
||||
///
|
||||
/// The function's OWN extent, determined by where its branches go and where it returns, rather than by the
|
||||
/// next symbol. That is what makes the anchor check sound without a complete function list.
|
||||
fn reachable_strings(img: &CodeImage, entry: u64) -> Option<(HashSet<u64>, Vec<u64>)> {
|
||||
const CAP: u64 = 0x4000;
|
||||
let all = img.code_at(entry)?;
|
||||
let extent = (all.len() as u64).min(CAP);
|
||||
let code = &all[..extent as usize];
|
||||
let mut seen: HashSet<u64> = HashSet::new();
|
||||
let mut loads: Vec<u64> = Vec::new();
|
||||
// Saturating for the same reason `pulse::shim_reads` is: a file-controlled extent must not wrap the
|
||||
// range inside out under the overflow-checked build the fuzzers use.
|
||||
let end = entry.saturating_add(extent);
|
||||
let mut work = vec![entry];
|
||||
let mut insn = iced_x86::Instruction::default();
|
||||
while let Some(at) = work.pop() {
|
||||
if at < entry || at >= end || !seen.insert(at) || seen.len() > 40000 {
|
||||
continue;
|
||||
}
|
||||
let mut dec = iced_x86::Decoder::with_ip(
|
||||
64,
|
||||
&code[(at - entry) as usize..],
|
||||
at,
|
||||
iced_x86::DecoderOptions::NONE,
|
||||
);
|
||||
if !dec.can_decode() {
|
||||
continue;
|
||||
}
|
||||
dec.decode_out(&mut insn);
|
||||
if insn.is_invalid() || insn.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
if insn.is_ip_rel_memory_operand() {
|
||||
loads.push(insn.ip_rel_memory_address());
|
||||
}
|
||||
match insn.flow_control() {
|
||||
iced_x86::FlowControl::Return
|
||||
| iced_x86::FlowControl::IndirectBranch
|
||||
| iced_x86::FlowControl::Exception
|
||||
| iced_x86::FlowControl::Interrupt => {}
|
||||
iced_x86::FlowControl::UnconditionalBranch => work.push(insn.near_branch_target()),
|
||||
iced_x86::FlowControl::ConditionalBranch => {
|
||||
work.push(at + insn.len() as u64);
|
||||
work.push(insn.near_branch_target());
|
||||
}
|
||||
_ => work.push(at + insn.len() as u64),
|
||||
}
|
||||
}
|
||||
Some((seen, loads))
|
||||
}
|
||||
|
||||
/// Derive an anchor for every entry that has none, from the address its SHIPPED signature resolves to.
|
||||
///
|
||||
/// Three conditions, each closing a way this can name the wrong function:
|
||||
///
|
||||
/// 1. **The resolved address must be a function ENTRY POINT.** ModSharp's `refs.strings` locates a
|
||||
/// *function*; a great many shipped locators deliberately point MID-function (`CBaseButton::InputPress`
|
||||
/// resolves to a `mov`, `BotNavIgnore` to a `je` — patterns anchored at a hook site, not a prologue). An
|
||||
/// anchor cannot denote the same thing as one of those, so those entries get none rather than a locator
|
||||
/// that resolves somewhere else.
|
||||
/// 2. **The string must be referenced from inside the function's OWN flow-reachable code**, walked from the
|
||||
/// entry, not from a `[entry, next_entry)` range. `.eh_frame_hdr` covers a small fraction of these
|
||||
/// binaries' functions, so such a range routinely swallows unindexed neighbours and inherits their
|
||||
/// strings — which is exactly how a first cut of this produced "`CBaseButton::InputPress` references
|
||||
/// *Traced intervals in %.3fus*".
|
||||
/// 3. **Every instruction that loads the string must be inside that same reachable set.** This is the
|
||||
/// uniqueness test, done at instruction level so it never consults a function boundary. A string also
|
||||
/// loaded from elsewhere locates nothing and is dropped.
|
||||
///
|
||||
/// Server-library only, the restriction [`recover_by_string_anchor`] already carries: the fold holds that one
|
||||
/// image, and loading a second full set of 22 keyed images to reach the rest would add several hundred MB to a
|
||||
/// pipeline that has already been OOM-killed on Dota.
|
||||
///
|
||||
/// Returns `(attached, considered)`. Degrades quietly toward FEWER anchors and never toward a wrong one.
|
||||
fn attach_derived_anchors(
|
||||
mono: &mut model::Monolith,
|
||||
img: &CodeImage,
|
||||
server_lib: &str,
|
||||
) -> (usize, usize) {
|
||||
let wants = |e: &model::MonoEntry| {
|
||||
e.locator.anchors.is_empty()
|
||||
&& e.locator
|
||||
.signature
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.library == server_lib)
|
||||
};
|
||||
let any = [&mono.core, &mono.high_confidence, &mono.experimental]
|
||||
.iter()
|
||||
.any(|t| t.values().any(&wants));
|
||||
if !any {
|
||||
return (0, 0);
|
||||
}
|
||||
let refs = string_refs(img);
|
||||
// Condition 1's test set: the addresses this image treats as function starts.
|
||||
let mut starts = crate::locate::candidate_entries(img);
|
||||
starts.extend(img.eh_frame_functions().into_iter().map(|(s, _)| s));
|
||||
starts.sort_unstable();
|
||||
starts.dedup();
|
||||
|
||||
// Why each candidate was rejected, so a low yield is a FACT rather than a mystery. The three
|
||||
// conditions fail for very different reasons and the mix differs sharply between games (CS2 derives
|
||||
// ~5% of candidates, Dota ~0.5%); without this the difference is unattributable.
|
||||
let (mut attached, mut considered) = (0usize, 0usize);
|
||||
let (mut no_resolve, mut mid_fn, mut no_unique) = (0usize, 0usize, 0usize);
|
||||
for tier in [
|
||||
&mut mono.core,
|
||||
&mut mono.high_confidence,
|
||||
&mut mono.experimental,
|
||||
] {
|
||||
for e in tier.values_mut() {
|
||||
if !wants(e) {
|
||||
continue;
|
||||
}
|
||||
considered += 1;
|
||||
let Some(sig) = e.locator.signature.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(pat) = crate::sig::Pattern::parse(&sig.linux) else {
|
||||
continue;
|
||||
};
|
||||
let hits = img.find(&pat);
|
||||
let [addr] = hits.as_slice() else {
|
||||
no_resolve += 1;
|
||||
continue;
|
||||
};
|
||||
let addr = *addr;
|
||||
if starts.binary_search(&addr).is_err() {
|
||||
mid_fn += 1; // condition 1: mid-function locator, not a function an anchor can name
|
||||
continue;
|
||||
}
|
||||
let Some((reach, loads)) = reachable_strings(img, addr) else {
|
||||
continue;
|
||||
};
|
||||
// Candidates this function actually loads, longest first then lexicographic — deterministic,
|
||||
// because this lands in a byte-reproducible artifact.
|
||||
let mut cands: Vec<&(String, Vec<u64>)> = loads
|
||||
.iter()
|
||||
.filter_map(|va| refs.get(va))
|
||||
.filter(|(_, from)| from.iter().all(|ip| reach.contains(ip)))
|
||||
.collect();
|
||||
cands.sort_unstable_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.0.cmp(&b.0)));
|
||||
match cands.first() {
|
||||
Some((s, _)) => {
|
||||
e.locator.anchors.push(s.clone());
|
||||
attached += 1;
|
||||
}
|
||||
None => no_unique += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
" anchor derivation: {attached} attached; rejected {no_resolve} (pattern did not resolve \
|
||||
uniquely), {mid_fn} (locator is mid-function, which an anchor cannot name), {no_unique} (no string \
|
||||
unique to the function)"
|
||||
);
|
||||
(attached, considered)
|
||||
}
|
||||
|
||||
fn build_monolith(
|
||||
prof: &GameProfile,
|
||||
source_build: &str,
|
||||
|
|
@ -1470,8 +1747,10 @@ fn build_monolith(
|
|||
t3: &BTreeMap<String, model::Entry>,
|
||||
prov: &BTreeMap<String, model::Provenance>,
|
||||
abi: &BTreeMap<String, model::AbiShape>,
|
||||
anchors: &BTreeMap<String, Vec<String>>,
|
||||
server_img: Option<(&CodeImage, &str)>,
|
||||
) -> Result<(model::Monolith, String)> {
|
||||
let mono = assemble_monolith(
|
||||
let mut mono = assemble_monolith(
|
||||
prof,
|
||||
source_build,
|
||||
version,
|
||||
|
|
@ -1483,6 +1762,22 @@ fn build_monolith(
|
|||
prov,
|
||||
abi,
|
||||
)?;
|
||||
// Attached AFTER assembly, across every tier at once, rather than at the three MonoEntry construction
|
||||
// sites: an anchor belongs to a NAME, not to a tier, and one pass cannot leave a tier out by omission.
|
||||
let attached = attach_anchors(&mut mono, anchors);
|
||||
// Then DERIVE one for everything the catalogue does not cover. Reported separately from the catalogued
|
||||
// count: they are different claims — one is a curated string somebody chose, the other is this build's
|
||||
// own machine code answering the same question — and collapsing them would hide either going to zero.
|
||||
let (derived, considered) = match server_img {
|
||||
Some((img, lib)) => attach_derived_anchors(&mut mono, img, lib),
|
||||
None => (0, 0),
|
||||
};
|
||||
eprintln!(
|
||||
" string anchors: {attached} of {} catalogued reached the monolith; {derived} DERIVED for {considered} \
|
||||
server entries that had none ({} total anchored)",
|
||||
anchors.len(),
|
||||
attached + derived
|
||||
);
|
||||
let cssharp = render::render_monolith_cssharp(&mono, model::TierSelect::HighConfidence);
|
||||
eprintln!(
|
||||
" monolith: {} core + {} high-conf + {} experimental + {} unresolved",
|
||||
|
|
@ -1513,6 +1808,8 @@ pub(crate) struct FoldArgs<'a> {
|
|||
pub unverified: &'a BTreeSet<String>,
|
||||
/// The derive's measured argument footprints, folded onto the monolith entries.
|
||||
pub abi: &'a BTreeMap<String, model::AbiShape>,
|
||||
/// The derive's catalogue string anchors, folded onto the monolith entries the same way.
|
||||
pub anchors: &'a BTreeMap<String, Vec<String>>,
|
||||
pub sig_cap: usize,
|
||||
pub version: &'a str,
|
||||
pub full_names: Option<&'a Path>,
|
||||
|
|
@ -1578,6 +1875,7 @@ pub(crate) fn build_gamedata_cmd(prof: &GameProfile, a: FoldArgs) -> Result<Fold
|
|||
flagged,
|
||||
unverified,
|
||||
abi,
|
||||
anchors,
|
||||
sig_cap,
|
||||
version,
|
||||
full_names,
|
||||
|
|
@ -1882,6 +2180,8 @@ pub(crate) fn build_gamedata_cmd(prof: &GameProfile, a: FoldArgs) -> Result<Fold
|
|||
&t3,
|
||||
&prov,
|
||||
&abi_all,
|
||||
anchors,
|
||||
Some((&img, &default_lib)),
|
||||
)?;
|
||||
Ok(Folded {
|
||||
mono,
|
||||
|
|
@ -2144,7 +2444,6 @@ fn assemble_monolith(
|
|||
name.clone(),
|
||||
MonoEntry {
|
||||
locator: render::entry_from_value(v),
|
||||
class: None,
|
||||
abi: abi.get(name.as_str()).cloned(),
|
||||
provenance: Provenance {
|
||||
source: Some(source.into()),
|
||||
|
|
@ -2167,7 +2466,6 @@ fn assemble_monolith(
|
|||
name.clone(),
|
||||
MonoEntry {
|
||||
locator,
|
||||
class: None,
|
||||
abi: abi.get(name.as_str()).cloned(),
|
||||
provenance: provenance.clone(),
|
||||
validated: None,
|
||||
|
|
@ -2202,8 +2500,13 @@ fn assemble_monolith(
|
|||
..Provenance::with_tier(g.tier)
|
||||
};
|
||||
exp_tier.entry(g.name.clone()).or_insert(MonoEntry {
|
||||
locator: locator.clone(),
|
||||
class: g.class.clone(),
|
||||
// The class rides on the LOCATOR now, not beside it: for an offset entry the class is what
|
||||
// makes the slot index mean anything, and keeping them together is what lets it reach the
|
||||
// emitters through `Monolith::select`.
|
||||
locator: model::Entry {
|
||||
class: g.class.clone(),
|
||||
..locator.clone()
|
||||
},
|
||||
// The band's own measurement first: it was taken at the guess's exact address, which IS
|
||||
// what the locator resolves to. The name-keyed map is the fallback.
|
||||
abi: g.abi.clone().or_else(|| abi.get(g.name.as_str()).cloned()),
|
||||
|
|
@ -3486,6 +3789,13 @@ pub(crate) struct Derived {
|
|||
/// Per-entry argument footprint measured in the target binary — the machine half of the
|
||||
/// locator/prototype split, and what a declared prototype is checked against.
|
||||
pub abi: BTreeMap<String, model::AbiShape>,
|
||||
/// Per-entry string ANCHORS from the catalogue — distinctive literals the function references.
|
||||
///
|
||||
/// Its own side table for the same reason `abi` is: the derive→fold transport for `core` is the cssharp
|
||||
/// locator shape, which has no anchor field, so anything ridden in on an `Entry` there would be dropped
|
||||
/// when the fold re-parses it. Carrying them separately keeps the cssharp artifact unchanged — CS# has
|
||||
/// no `refs` feature and should not grow a key it cannot read.
|
||||
pub anchors: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Derive a target build's gamedata OFFLINE and return it in memory.
|
||||
|
|
@ -3710,11 +4020,22 @@ pub(crate) fn gamedata(
|
|||
.collect();
|
||||
flagged.extend(off_flag);
|
||||
flagged.extend(abi_drift);
|
||||
// Every catalogue entry's anchors, whether or not a byte sig located it. `recover_by_string_anchor`
|
||||
// uses them only as a FALLBACK locator; this ships them as a supplement, which is a different job — an
|
||||
// entry that resolved perfectly still benefits from a second locator with a different failure mode.
|
||||
let anchors: BTreeMap<String, Vec<String>> = cat
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
let a: Vec<String> = string_anchors(f).into_iter().map(str::to_string).collect();
|
||||
(!a.is_empty()).then(|| (f.name.clone(), a))
|
||||
})
|
||||
.collect();
|
||||
Ok(Derived {
|
||||
core,
|
||||
flagged,
|
||||
unverified: unverified.into_iter().collect(),
|
||||
abi: abi_shapes,
|
||||
anchors,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -3870,6 +4191,10 @@ fn derive_offsets(
|
|||
match chain_and_vote(&anchors, hv, target_idx) {
|
||||
Some((pred, conf)) if conf >= 80 => {
|
||||
gd.set_offset(f.name.clone(), pred as i64);
|
||||
// The class the slot was chained THROUGH — the only one that makes the index meaningful.
|
||||
// Recorded here rather than reconstructed later from the name, which would be a different
|
||||
// (and sometimes wrong) fact: a base-declared method sits in a derived class's vtable.
|
||||
gd.set_class(f.name.clone(), f.class.clone());
|
||||
off_ok += 1;
|
||||
}
|
||||
Some((pred, conf)) => {
|
||||
|
|
@ -4506,7 +4831,6 @@ mod tests {
|
|||
"CBaseEntity::TakeDamage".to_string(),
|
||||
MonoEntry {
|
||||
locator: Entry::signature("server", "48 8B 05 ? ? ? ?"),
|
||||
class: None,
|
||||
abi: None,
|
||||
provenance: Provenance {
|
||||
source: Some("catalogue".into()),
|
||||
|
|
@ -4519,7 +4843,6 @@ mod tests {
|
|||
"CCSPlayerPawn::IsBot".to_string(),
|
||||
MonoEntry {
|
||||
locator: Entry::offset(42),
|
||||
class: None,
|
||||
abi: None,
|
||||
provenance: Provenance::with_tier(Tier::SelfNamed),
|
||||
validated: None,
|
||||
|
|
@ -4547,7 +4870,6 @@ mod tests {
|
|||
use model::{Counts, Entry, MonoEntry, MonoMeta, Monolith, Provenance, Tier};
|
||||
let entry = |loc: Entry, tier| MonoEntry {
|
||||
locator: loc,
|
||||
class: None,
|
||||
abi: None,
|
||||
provenance: Provenance::with_tier(tier),
|
||||
validated: None,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue