//! The canonical derived-gamedata model — the single in-memory representation the derivation produces //! and the emitters consume. Deliberately format-agnostic (no serde_json shapes here): `render::*` //! turns it into CSSharp JSON, Metamod VDF, etc. Keeping the shape here (not smeared through `json!` //! call sites) is what lets one derivation feed every output format and a standalone generator. use std::collections::BTreeMap; /// A byte-pattern signature located in a specific library. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Sig { pub library: String, // "server", "engine2", … (the module the pattern scans) pub linux: String, // space-hex pattern with `?` wildcards, e.g. "55 48 89 ? E5" } /// One gamedata function: a vtable-method offset, a scan signature, or (rarely) both. #[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Entry { #[serde(default, skip_serializing_if = "Option::is_none")] pub signature: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub offset: Option, // vtable slot index (or a carried member offset) } impl Entry { /// A signature-only locator (the deriver's sig-XOR-offset invariant as a constructor). pub fn signature(library: impl Into, linux: impl Into) -> Entry { Entry { signature: Some(Sig { library: library.into(), linux: linux.into(), }), offset: None, } } /// A vtable-offset-only locator. pub fn offset(linux: i64) -> Entry { Entry { signature: None, offset: Some(linux), } } } /// The default output game-key when none is set — CS2's Steam content-dir token. Games-keyed emitters /// (Metamod/Plugify) fall back to this so an older model JSON (no `game_key`) still renders as CS2. fn default_game_key() -> String { "csgo".to_string() } /// The derived gamedata for one build: function name -> entry. BTreeMap so iteration/output is /// deterministically key-sorted, matching serde_json's Map key ordering byte-for-byte. /// /// Serializing this IS the canonical "model JSON" — the single per-build artifact the deriver /// publishes and the standalone `source2rosetta-gen` reads back to produce every framework's format. #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct Gamedata { pub entries: BTreeMap, /// The game token the game-keyed emitters wrap output in (Metamod `Games { {..} }`, /// Plugify `{ "": {..} }`). Persisted here because `source2rosetta-gen` renders from the model /// JSON alone, with no access to the deriver's `GameProfile`. Defaults to CS2's `csgo`. #[serde(default = "default_game_key")] pub game_key: String, } impl Default for Gamedata { fn default() -> Self { Self { entries: BTreeMap::new(), game_key: default_game_key(), } } } /// Why the derivation could not produce a shipped locator — the closed domain the `unresolved` tier reports. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "kebab-case")] pub enum FlagReason { /// A catalogued signature that no longer resolves uniquely / wasn't recovered in the target. SigDrifted, /// A vtable offset whose recency-weighted vote fell below the confidence bar. OffsetLowConf, /// No chainable anchor / no reference history at all. Unresolved, /// The sig SHIPPED, but its ABI prototype shape drifted from the model consensus (review the prototype). AbiDrift, } impl FlagReason { /// The kebab id — the same string the `kebab-case` serialization emits, for callers that carry the /// reason across to the monolith's `Unresolved.reason` (a `String`, kept stable for byte-reproducibility). pub fn as_str(self) -> &'static str { match self { FlagReason::SigDrifted => "sig-drifted", FlagReason::OffsetLowConf => "offset-low-conf", FlagReason::Unresolved => "unresolved", FlagReason::AbiDrift => "abi-drift", } } } /// A catalogue entry the derivation could NOT confidently produce. Emitted as a first-class sidecar /// (never guessed into the gamedata — safety > recall) so it is both reviewable and machine-readable. #[derive(Debug, Clone, serde::Serialize)] pub struct Flagged { pub name: String, pub reason: FlagReason, /// The signal we do have: the carried value, vote confidence, "no reference", … pub detail: String, } impl Flagged { pub fn new(name: impl Into, reason: FlagReason, detail: impl Into) -> Self { Self { name: name.into(), reason, detail: detail.into(), } } } impl Gamedata { pub fn set_signature( &mut self, name: impl Into, library: impl Into, linux: impl Into, ) { self.entries.entry(name.into()).or_default().signature = Some(Sig { library: library.into(), linux: linux.into(), }); } pub fn set_offset(&mut self, name: impl Into, linux: i64) { self.entries.entry(name.into()).or_default().offset = Some(linux); } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } } // =========================================================================================== // The monolith model — the shipped `gamedata-.json`. Four confidence tiers with provenance // + live-validation folded inline; `source2rosetta-gen` renders it into any framework format, and it is // equally readable as-is by a consumer. A `MonoEntry` EMBEDS `Entry`, so the locator shape stays // single-sourced on `render::locator_value` and never diverges. Lib-agnostic: an entry carries its // `library` in the signature locator, so the monolith spans every derived library, not just libserver. // =========================================================================================== /// A monolith entry's confidence tier — its finer label within a section, serialized as kebab strings /// (`core`, `self-named`, `dict-exact`, `contextual`, `corroborated`, `high`, `medium`, `low`). #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Tier { Core, SelfNamed, /// Dictionary-corroborated by the FOLD (an exact hit in the harvested name catalogue). DictExact, Contextual, /// Dictionary-corroborated by the EXPERIMENTAL band, and also the label the macOS ground-truth /// transfer carries. Kept distinct from [`Tier::DictExact`] rather than merged: the two are produced /// by different paths, and folding them together would additionally conflate ground-truth symbols /// with dictionary guesses. Anything counting "corroborated" for display must count BOTH — see /// [`Tier::is_dict_corroborated`]. Corroborated, High, Medium, Low, } impl Tier { /// Is this tier a dictionary/ground-truth corroboration, under either of its two labels? pub fn is_dict_corroborated(self) -> bool { matches!(self, Tier::DictExact | Tier::Corroborated) } } /// Per-entry provenance, folded inline. Optional by tier: a `core` entry carries almost nothing, an /// `experimental` guess carries the full grading. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Provenance { pub tier: Tier, /// Raw address in THIS build — a debugging/trace anchor (the monolith is per-build, so it's coherent). #[serde(default, skip_serializing_if = "Option::is_none")] pub addr: Option, /// The confidence LABEL as the harvest records it — `"high"` / `"medium"` / `"low"` — a separate axis /// from `tier` (a self-named entry can still be low-confidence). A string, not a number. #[serde(default, skip_serializing_if = "Option::is_none")] pub confidence: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub self_named: Option, /// Return is struct-by-value → unsafe to naive-call (the `RetClass::ByValue` flag). #[serde(default, skip_serializing_if = "Option::is_none")] pub by_value: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ret_class: Option, /// "catalogue" | "source2rosetta-nameext" | "contribution:" | … #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub rtti_class: Option, /// high_confidence tier only — the naming rationale. #[serde(default, skip_serializing_if = "Option::is_none")] pub rationale: Option, /// experimental tier only — how the dictionary corroborated the guess (`"exact"` / `"bare"` / `"none"`). #[serde(default, skip_serializing_if = "Option::is_none")] pub corroboration: Option, /// experimental tier only — the same name was guessed at more than one address. #[serde(default, skip_serializing_if = "Option::is_none")] pub collision: Option, /// experimental tier only — protobuf/serializer/foreign plumbing (flagged, kept for completeness). #[serde(default, skip_serializing_if = "Option::is_none")] pub dead_weight: Option, /// core only — the target's ABI prototype-shape differs from the model's consensus (`"target [..] vs /// history [..]"`). The signature still ships (a drifted arg-list is a loader-hook seam a byte-sig can't /// see, not a wrong locator), but a consumer that ptrace-calls it should re-check the prototype. #[serde(default, skip_serializing_if = "Option::is_none")] pub abi_drift: Option, } impl Tier { /// The tier's kebab id — the same string the `kebab-case` serialization emits, for a deriver that needs /// it as a plain `&str` (e.g. a count-by-tier tally) without going through serde. pub fn as_str(self) -> &'static str { match self { Tier::Core => "core", Tier::SelfNamed => "self-named", Tier::DictExact => "dict-exact", Tier::Contextual => "contextual", Tier::Corroborated => "corroborated", Tier::High => "high", Tier::Medium => "medium", Tier::Low => "low", } } /// Parse a tier from its kebab id (the inverse of the `kebab-case` serialization) — so a deriver holding /// a tier as a computed string can lift it to the typed enum without a serde_json round-trip. `None` for /// an unrecognised id (the caller decides whether that is a hard error). pub fn from_id(s: &str) -> Option { Some(match s { "core" => Tier::Core, "self-named" => Tier::SelfNamed, "dict-exact" => Tier::DictExact, "contextual" => Tier::Contextual, "corroborated" => Tier::Corroborated, "high" => Tier::High, "medium" => Tier::Medium, "low" => Tier::Low, _ => return None, }) } } impl Provenance { /// A provenance with only its tier set (all optional fields `None`) — the base for `core` entries and /// the start point for functional-update construction (`Provenance { source: …, ..with_tier(t) }`). pub fn with_tier(tier: Tier) -> Self { Self { tier, addr: None, confidence: None, self_named: None, by_value: None, ret_class: None, source: None, rtti_class: None, rationale: None, corroboration: None, collision: None, dead_weight: None, abi_drift: None, } } } /// One monolith function: a locator (the `signature`/`offset` model shape, flattened in from [`Entry`] so /// the shape is single-sourced) plus its provenance and live-validation verdict. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct MonoEntry { #[serde(flatten)] pub locator: Entry, /// experimental offsets only: the vtable class the slot lives on (a reader's eyeball check). #[serde(default, skip_serializing_if = "Option::is_none")] pub class: Option, pub provenance: Provenance, /// Live-validation verdict: `Some(true)` passed, `Some(false)` dropped confident-bad, `None` unvalidated. #[serde(default)] pub validated: Option, } /// A catalogued function the derivation could not confidently produce — kept in-file (never a shipped /// locator) so the monolith is the complete catalogue picture. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Unresolved { pub reason: String, // "sig-drifted" | "offset-low-conf" | "unresolved" | … pub detail: String, } /// Entry counts per section — a struct (not a map) so it serializes in this logical order, deterministically. #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct Counts { pub core: usize, pub high_confidence: usize, pub experimental: usize, pub unresolved: usize, } /// The monolith's intrinsic release identity. NO wall-clock field — volatile release metadata (`produced_at`, /// `status`, urls, sha256, `based_on`) lives in the per-buildid manifest, so the monolith is fully /// byte-reproducible. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct MonoMeta { pub game_key: String, pub game: String, // display name pub source_build: String, /// `--` — the buildid is embedded here; the per-buildid MANIFEST carries it as its /// own field (volatile metadata is kept OUT of this byte-reproducible monolith). pub version: String, pub counts: Counts, } /// The full derived gamedata for one build — the shipped `gamedata-.json`. Four confidence tiers, each /// a key-sorted map. `source2rosetta-gen` renders it into any framework format; a consumer can equally read it /// directly, gating `experimental` behind a runtime toggle off each entry's tier. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Monolith { pub meta: MonoMeta, pub core: BTreeMap, pub high_confidence: BTreeMap, pub experimental: BTreeMap, pub unresolved: BTreeMap, } /// Which of the monolith's three SHIPPABLE tiers a render includes — cumulative, most-confident first /// (`unresolved` is never rendered; it has no locator). The `--tier` arg of `source2rosetta-gen`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TierSelect { /// `core` only — the guaranteed, live-validated set. Core, /// core + high_confidence — adds the promoted name-extrapolations. HighConfidence, /// core + high_confidence + experimental — every locatable guess. Experimental, } impl TierSelect { /// Parse the `--tier` id (the monolith tier names, plus a couple of intuitive aliases). pub fn from_id(s: &str) -> Option { match s { "core" => Some(TierSelect::Core), "high_confidence" | "high-confidence" | "stable" => Some(TierSelect::HighConfidence), "experimental" | "full" => Some(TierSelect::Experimental), _ => None, } } } /// Every `--tier` id `from_id` accepts (canonical names) — for help text. pub const TIER_IDS: &[&str] = &["core", "high_confidence", "experimental"]; impl Monolith { /// Flatten the tiers up to `select` into one `name -> Entry` map — the input the flat framework emitters /// (metamod/modsharp/swiftly/plugify) consume. Drops confident-bad entries (`validated == Some(false)`); /// `unresolved` is never included (no locator); a more-confident tier wins a name collision. pub fn select(&self, select: TierSelect) -> Gamedata { let mut gd = Gamedata { entries: BTreeMap::new(), game_key: self.meta.game_key.clone(), }; let mut add = |m: &BTreeMap| { for (name, e) in m { if e.validated == Some(false) { continue; } gd.entries .entry(name.clone()) .or_insert_with(|| e.locator.clone()); } }; add(&self.core); if select != TierSelect::Core { add(&self.high_confidence); } if select == TierSelect::Experimental { add(&self.experimental); } gd } } /// One typed schema field (the field NAME is the map key). `offset` is static; `ty`/`kind`/`size` are /// runtime-resolved (empty/zero when derived offline without a live process). #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Field { pub offset: i32, #[serde(rename = "type", default, skip_serializing_if = "String::is_empty")] pub ty: String, #[serde(default)] pub kind: FieldKind, pub size: usize, pub name_hash: u64, } /// How a schema field holds its value — a closed runtime domain (Source-2 `CSchemaType` category). /// Serializes to lowercase tokens, the values `netvars-.json` consumers expect. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum FieldKind { /// Builtin / atomic / declared class / declared enum — held inline (the common case). #[default] Ref, /// A pointer to the value. Ptr, /// A fixed-size inline array. FixedArray, } /// The typed schema — the shipped `netvars-.json`. Merges field offsets with runtime types: /// class -> field -> [`Field`]. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Schema { pub meta: SchemaMeta, pub classes: BTreeMap>, } /// The typed schema's intrinsic identity (no wall-clock field, same rationale as [`MonoMeta`]). #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct SchemaMeta { pub game_key: String, pub source_build: String, pub typed: usize, pub untyped: usize, } #[cfg(test)] mod monolith_tests { use super::*; #[test] fn mono_entry_flattens_locator_and_kebabs_tier() { let e = MonoEntry { locator: Entry { signature: None, offset: Some(158), }, class: None, provenance: Provenance { source: Some("catalogue".into()), ..Provenance::with_tier(Tier::Core) }, validated: Some(true), }; let v = serde_json::to_value(&e).unwrap(); assert_eq!(v["offset"], 158); // locator flattened to the top level assert!(v.get("signature").is_none()); // None locator field omitted assert_eq!(v["provenance"]["tier"], "core"); // kebab-case enum assert_eq!(v["provenance"]["source"], "catalogue"); assert!(v["provenance"].get("confidence").is_none()); // None provenance field skipped assert_eq!(v["validated"], true); } #[test] fn experimental_entry_keeps_class_and_serializes_null_validated() { let e = MonoEntry { locator: Entry { signature: None, offset: Some(40), }, class: Some("CFoo".into()), provenance: Provenance { confidence: Some("low".into()), self_named: Some(false), collision: Some(true), dead_weight: Some(false), ..Provenance::with_tier(Tier::Low) }, validated: None, }; let v = serde_json::to_value(&e).unwrap(); assert_eq!(v["class"], "CFoo"); assert_eq!(v["provenance"]["tier"], "low"); assert_eq!(v["provenance"]["collision"], true); assert_eq!(v["validated"], serde_json::Value::Null); // present as null, not omitted } #[test] fn monolith_round_trips() { let mut m = Monolith { meta: MonoMeta { game_key: "csgo".into(), game: "CS2".into(), source_build: "2026-07-15_003539".into(), version: "cs2-12345-0".into(), counts: Counts { core: 1, high_confidence: 0, experimental: 0, unresolved: 1, }, }, core: BTreeMap::new(), high_confidence: BTreeMap::new(), experimental: BTreeMap::new(), unresolved: BTreeMap::new(), }; m.core.insert( "A::b".into(), MonoEntry { locator: Entry { signature: Some(Sig { library: "server".into(), linux: "55 48 89 E5".into(), }), offset: None, }, class: None, provenance: Provenance { source: Some("catalogue".into()), ..Provenance::with_tier(Tier::Core) }, validated: Some(true), }, ); m.unresolved.insert( "C::d".into(), Unresolved { reason: "sig-drifted".into(), detail: "no unique/recovered signature in target".into(), }, ); let s = serde_json::to_string_pretty(&m).unwrap(); let back: Monolith = serde_json::from_str(&s).unwrap(); assert_eq!(back.core.len(), 1); assert_eq!( back.core["A::b"] .locator .signature .as_ref() .unwrap() .library, "server" ); assert_eq!(back.unresolved["C::d"].reason, "sig-drifted"); assert_eq!(back.meta.counts.unresolved, 1); } }