//! Per-game knobs — the only Source-2-*title*-specific constants, gathered in one place so a second //! game (Deadlock, Dota 2) is a data change, not a code hunt. CS2 and Dota 2 are registered today; the //! schema/RTTI/xref/oracle machinery around them is already game-generic. //! //! Everything a second game varies lives here: library names, the output game-key, the live-oracle //! launch spec, the player-pawn anchor, and the class/field/message-prefix literals the derivation and //! validation paths reference. (The SchemaSystem struct layout is deliberately NOT here — it tracks the //! engine BUILD ERA, not the game, so it lives as a per-binary `schema::SchemaLayout`.) /// How to launch a vanilla server populated with alive units, for the live oracle. Structured (not a /// flat arg string) because a second Source-2 game selects its mode and fills its world completely /// differently (Dota 2 has no game_type/game_mode deathmatch, no bot_quota). The `args` builder /// reproduces the exact CS2 arg order, so a byte-identical launch is assertable. pub struct LaunchSpec { /// Cvars set before `-maxplayers`/`+map`, in order (CS2: the deathmatch `game_type 1` / `game_mode 2`). pub pre_map_cvars: &'static [(&'static str, &'static str)], /// Cvars set after `+map `, in order. A value of `"{bots}"` is substituted with the runtime bot /// count (CS2's `+bot_quota `); every other value is passed through verbatim. pub post_map_cvars: &'static [(&'static str, &'static str)], } impl LaunchSpec { /// The vanilla dedicated-server args to spawn `bots` alive units on `map`, in the exact order the /// live launch requires. pub fn args(&self, map: &str, bots: u32) -> Vec { let mut a: Vec = vec![ "-dedicated".into(), "-insecure".into(), "-port".into(), "24015".into(), ]; for (k, v) in self.pre_map_cvars { a.push(format!("+{k}")); a.push((*v).into()); } a.push("-maxplayers".into()); a.push((bots + 4).to_string()); a.push("+map".into()); a.push(map.to_string()); for (k, v) in self.post_map_cvars { a.push(format!("+{k}")); a.push(if *v == "{bots}" { bots.to_string() } else { (*v).into() }); } a } } /// The live-oracle player-pawn anchor: a pawn RTTI class, a liveness netvar, and the IsPlayerPawn vtable /// slot. Used to find an alive instance in a running server, prove a derived offset is really callable, /// and sweep this-only query methods. `Copy` so it round-trips out of a `const` profile cheaply. #[derive(Clone, Copy)] pub struct PawnAnchor { pub pawn_class: &'static str, // player-pawn RTTI class — the live-oracle instance anchor pub health_field: &'static str, // a reliable "is this instance alive" netvar pub is_player_pawn_slot: u64, // gamedata vtable offset of IsPlayerPawn (call-live smoke test) } pub struct GameProfile { pub server_lib: &'static str, // the gameplay library (schema classes, most signatures) pub engine_lib: &'static str, // engine2 (entity system, networking) /// Every server-mapped Source-2 library the corpus model spans, ORDERED by resolution precedence /// (an earlier lib wins a class name present in more than one). `server_lib`/`engine_lib` are the first /// two and remain the "primary lib" for command defaults + the live-oracle readiness anchor. The /// client-render stack (libclient/panorama/rendersystemvulkan/cairo/…) is deliberately absent: the /// dedicated server never maps it (confirmed against `/proc//maps` of a running server). pub libs: &'static [&'static str], /// How many vtable slots to read per class. A hard stop, not a hint: `rtti::read_slots` returns what it /// read with no truncation marker, so a class with more slots than this is INDISTINGUISHABLE from one /// that genuinely ends here — its tail silently vanishes, and `validate_offset` reports a legitimate /// slot past the cap as out-of-bounds. `extract_build_vtables` WARNS when a class lands exactly on the /// cap, which is how a game that needs a bigger one is discovered. /// /// At 2048: CS2's deepest class is ~464 and Dota's ~541 (the `CDOTA_BaseNPC_*` / `CDOTA_Unit_Hero_*` /// family). 2048 is deliberately far above need: `read_slots` stops at the first slot that isn't /// executable code, so a normal class costs nothing extra and only genuinely deep vtables scan further. /// /// **Raising this invalidates that game's model.** Slot counts and per-slot fingerprints are recorded /// under the cap in force at distill time; a derive that reads deeper vtables than the model was built /// from is comparing different objects. A raise is a re-distill, not a config tweak — change it and the /// model together. pub max_vtable_slots: usize, /// Collapse tripwires, ONE PER INDEPENDENTLY-SHAPED TABLE the deriver reads out of the binary rather /// than deriving. Each is read by its own record shape, so a layout change Valve makes to one yields /// fewer records from that one alone — safe, but SILENT, and a release shipping zero of any of them at /// exit 0 is exactly the failure "degrades or stops loudly, never lies" exists to prevent. /// /// Deliberately NOT one summed floor across all of them: a sum is satisfied by the tables that still /// work, so it cannot detect the single-table collapse it exists to catch. Set far below the observed /// count (collapse detectors, not tight bounds); a new game starts every field at 0 and gets no gate /// until someone measures one. pub min_pulse_bindings: usize, /// Bindings whose TYPED SIGNATURE was recovered from their descriptor initializer. Its own floor /// because it has its own failure mode: the registry can still read perfectly while the descriptor /// layout moves, and the result would be a release that ships every binding with no signature at all /// — a silent capability loss rather than a wrong answer, which is precisely what a floor is for. pub min_pulse_typed: usize, /// Floor on Pulse bindings whose invocation shim is HOST-CALLABLE (`call.needs == "args-only"`). /// Its own floor because it has its own failure mode: the registry can read perfectly and the /// signatures recover perfectly while a codegen change makes every shim appear to read another slot, /// which would silently retire the one callable tier instead of failing the release. pub min_pulse_callable: usize, pub min_entity_io: usize, pub min_entity_classes: usize, /// Console commands recovered from their registration calls. Its own floor because it has its own /// failure mode, and a quiet one: the registrar is identified by SHAPE (it opens by writing the /// invalid-handle sentinel), so a build that reworks that constructor yields zero commands rather /// than wrong ones — correct, and invisible without this. pub min_commands: usize, /// Floor on recovered ConVars. Its own floor because convar registration is identified by a DIFFERENT /// test from the command one — convergence of registrar wrappers on a shared core, not a sentinel in the /// callee — so it can fail while commands keep working. pub min_convars: usize, pub min_schema_enums: usize, /// Output game-key the game-keyed emitters use (Metamod `Games { {..} }`, Plugify `{ "": {..} }`). pub game_key: &'static str, /// The `--game` CLI token / per-release filename suffix (`cs2`, `dota2`) — distinct from `game_key` (the /// content-dir token `csgo`/`dota` that framework formats key on). Names the artifacts /// `gamedata-.json` / `model-.json` / `netvars-.json`. pub token: &'static str, /// Dedicated-server launcher binary under `bin/linuxsteamrt64/` (CS2: `cs2`). pub executable: &'static str, /// Default map for the live-oracle server. pub default_map: &'static str, /// This game's user-message class prefix, dropped as wire/serializer dead weight (CS2: `CCSUsrMsg`). pub usermsg_prefix: &'static str, /// Dead-weight / name-classification vocabulary — the retunable taxonomy a fork edits per game. /// `foreign_namespaces`: RTTI namespaces that are never gameplay (the C++ runtime, Steam GC SDK, Valve /// container templates, the V8 vscript backend). `proto_prefixes`: protobuf message-class name prefixes /// (wire/GC protocol). Most values are Source-2-universal, but they ride the profile so a fork retunes /// one const block instead of hunting a second file. pub foreign_namespaces: &'static [&'static str], pub proto_prefixes: &'static [&'static str], /// Protobuf serializer method names: a lone HARD one decisively marks its class generated wire plumbing; /// SOFT ones can be legit game methods, so they only count toward the ≥3-method protobuf-class cluster. pub hard_serializer: &'static [&'static str], pub soft_serializer: &'static [&'static str], /// Method-name prefixes for a this-only blind-callable boolean query — the live call-smoke-test gate. pub query_prefixes: &'static [&'static str], /// Live-oracle "famous field" spotlight: per class, the netvars whose live offsets the oracle prints /// field-by-field (the ones mods actually read). CS2 gameplay fields on the generic `CBaseEntity`. pub spotlight_fields: &'static [(&'static str, &'static [&'static str])], /// Human-readable game name for the shipped gamedata banner. pub display_name: &'static str, /// How the live oracle spawns alive units. pub launch: LaunchSpec, /// RTTI class of the always-present gamerules proxy. A pawn-less game (Dota) uses a live instance of it /// as the live-oracle readiness signal (a live one means the map loaded and libserver is ready) in /// place of an alive pawn; set for every game though pawn games use the alive-pawn poll. pub ready_class: &'static str, /// The live-oracle player-pawn anchor, or `None` for a pawn-less game (Dota 2 units are /// CDOTA_BaseNPC/heroes, not a spawned CCSPlayerPawn). `Some` runs the alive-pawn poll + IsPlayerPawn /// call test + callable-method sweep; `None` skips them — the pawn-less live flow (an alternate /// entity anchor, or a bot-match-with-no-players readiness signal) is a placeholder TBD at bring-up. pub pawn_anchor: Option, } /// Counter-Strike 2. pub const CS2: GameProfile = GameProfile { server_lib: "libserver.so", engine_lib: "libengine2.so", // Every Valve Source-2 library the dedicated server maps (confirmed from /proc//maps), ordered // by resolution precedence: gameplay (server) then engine2 win shared-infra class-name collisions, // then the systems by rough dependency depth. Excludes the client-render stack (never server-mapped) // and the V8/Steam vendored runtimes (foreign code, filtered by taxonomy). libs: &[ "libserver.so", "libengine2.so", "libtier0.so", "libnetworksystem.so", "libschemasystem.so", "libresourcesystem.so", "libscenesystem.so", "libsoundsystem.so", "libanimationsystem.so", "libvphysics2.so", "libmeshsystem.so", "libparticles.so", "libworldrenderer.so", "libmaterialsystem2.so", "libscenefilecache.so", "libfilesystem_stdio.so", "liblocalize.so", "libhost.so", "libmatchmaking.so", "libpulse_system.so", "librendersystemempty.so", "libvscript.so", ], max_vtable_slots: 2048, // observed: 580 Pulse, 715 inputs + 226 outputs, 474 entity classnames, 784 commands, 555 enums min_pulse_bindings: 300, min_pulse_typed: 300, min_pulse_callable: 90, min_entity_io: 400, min_entity_classes: 200, min_commands: 400, min_convars: 900, min_schema_enums: 250, game_key: "csgo", token: "cs2", executable: "cs2", default_map: "de_dust2", usermsg_prefix: "CCSUsrMsg", foreign_namespaces: &[ "google::protobuf", "std::", "__gnu_cxx", "__cxxabiv1", "GCSDK::", "CUtl", "v8::", ], proto_prefixes: &[ "CMsg", "CSVCMsg", "CNETMsg", "CCLCMsg", "CMsgGC", "CDataGC", "CGC", "CSO", "PB_", ], hard_serializer: &[ "GetCachedSize", "ByteSizeLong", "IsInitialized", "GetMetadata", "MergePartialFromCodedStream", "SerializeWithCachedSizes", "InternalSerialize", "GetClassData", "MergeImpl", "_InternalParse", ], soft_serializer: &[ "New", "Clear", "CopyFrom", "MergeFrom", "SharedCtor", "SharedDtor", ], query_prefixes: &["Is", "Has", "Can", "Should", "Are", "Will"], spotlight_fields: &[( "CBaseEntity", &["m_iHealth", "m_iTeamNum", "m_hOwnerEntity"], )], display_name: "CS2", launch: LaunchSpec { pre_map_cvars: &[("game_type", "1"), ("game_mode", "2")], post_map_cvars: &[ ("sv_hibernate_when_empty", "0"), ("bot_join_after_player", "0"), ("bot_quota", "{bots}"), ("bot_quota_mode", "fill"), ("bot_difficulty", "2"), ("mp_warmuptime", "0"), ], }, ready_class: "CCSGameRulesProxy", pawn_anchor: Some(PawnAnchor { pawn_class: "CCSPlayerPawn", health_field: "m_iHealth", is_player_pawn_slot: 168, }), }; /// Dota 2. Dota has no deathmatch `game_type`/`game_mode` or `bot_quota`, so the live oracle needs a /// different keep-alive combination than CS2; the `launch` cvars are the real bot-match cvars found in /// `libserver.so` (see `launch`). pub const DOTA: GameProfile = GameProfile { server_lib: "libserver.so", // generic Source-2 (same filename as CS2) engine_lib: "libengine2.so", // The full Source-2 server lib set (same names as CS2 — shared engine; Dota's build supplies its own // versions). Superset-safe: `load_build_images` skips any lib absent from Dota's build. Confirm against a // running Dota server's /proc/maps if a Dota-specific server lib ever appears outside this set. libs: &[ "libserver.so", "libengine2.so", "libtier0.so", "libnetworksystem.so", "libschemasystem.so", "libresourcesystem.so", "libscenesystem.so", "libsoundsystem.so", "libanimationsystem.so", "libvphysics2.so", "libmeshsystem.so", "libparticles.so", "libworldrenderer.so", "libmaterialsystem2.so", "libscenefilecache.so", "libfilesystem_stdio.so", "liblocalize.so", "libhost.so", "libmatchmaking.so", "libpulse_system.so", "librendersystemempty.so", "libvscript.so", ], max_vtable_slots: 2048, // observed: 500 Pulse, 624 inputs, 3,528 entity classnames, 855 commands, 743 enums min_pulse_bindings: 250, min_pulse_typed: 250, min_pulse_callable: 65, min_entity_io: 300, min_entity_classes: 1000, min_commands: 400, min_convars: 600, min_schema_enums: 350, game_key: "dota", token: "dota2", executable: "dota2", // bin/linuxsteamrt64/dota2 default_map: "dota", usermsg_prefix: "CDOTAUserMsg", // Same Source-2-universal dead-weight vocabulary as CS2; only `usermsg_prefix` above is genuinely // per-game. A Dota-specific tune would edit here. foreign_namespaces: &[ "google::protobuf", "std::", "__gnu_cxx", "__cxxabiv1", "GCSDK::", "CUtl", "v8::", ], proto_prefixes: &[ "CMsg", "CSVCMsg", "CNETMsg", "CCLCMsg", "CMsgGC", "CDataGC", "CGC", "CSO", "PB_", ], hard_serializer: &[ "GetCachedSize", "ByteSizeLong", "IsInitialized", "GetMetadata", "MergePartialFromCodedStream", "SerializeWithCachedSizes", "InternalSerialize", "GetClassData", "MergeImpl", "_InternalParse", ], soft_serializer: &[ "New", "Clear", "CopyFrom", "MergeFrom", "SharedCtor", "SharedDtor", ], query_prefixes: &["Is", "Has", "Can", "Should", "Are", "Will"], spotlight_fields: &[( "CBaseEntity", &["m_iHealth", "m_iTeamNum", "m_hOwnerEntity"], )], display_name: "Dota 2", // Dota 2 has no deathmatch `game_type`/`game_mode` or `bot_quota`; a headless AI/bot match uses these // cvars (all present in libserver.so). `sv_hibernate_when_empty 0` is REQUIRED — without it an empty // dedicated server hibernates and quits immediately (exit 0). The pawn-only stages gate on `pawn_anchor` // being `Some`, so Dota (whose `pawn_anchor` is `None`) validates through the schema + sig oracles // without a pawn, polling `ready_class` (a live CDOTAGamerulesProxy = map loaded) in place of an alive // pawn. launch: LaunchSpec { // Keep an empty headless Dota server ALIVE long enough to attach + validate: sv_cheats enables dev // commands, hibernate-off stops it quitting when empty, and the huge auto-surrender timeout defeats // the empty-match abandon that otherwise closes it after a few minutes. Bots are NOT populated here // (that needs a post-map-load stdin command and is only for the entity oracle); sig validation needs // only libserver loaded + a map. pre_map_cvars: &[("sv_cheats", "1"), ("dota_force_gamemode", "1")], post_map_cvars: &[ ("sv_hibernate_when_empty", "0"), ("dota_auto_surrender_all_disconnected_timeout", "999999"), ("dota_local_bot_match_difficulty", "1"), ], }, ready_class: "CDOTAGamerulesProxy", // Dota 2 units are `CDOTA_BaseNPC_Hero` NPCs, not a spawned `CCSPlayerPawn` — no player-pawn anchor. The // pawn-based oracle (alive-pawn poll + IsPlayerPawn call + this-only method sweep) is skipped; the SCHEMA // oracle (attach + read SchemaSystem, no pawn) is the portable core. A hero-NPC anchor is the pawn-less // extension, TBD at bring-up. pawn_anchor: None, }; // (There is deliberately NO process-wide "active profile" global. `main` resolves `--game` to a // `&'static GameProfile` and threads it explicitly through every engine entry point, so the library is // reusable per-call — CS2 and Dota can be derived in the same process — and no code path can silently // run one game's assumptions on another.) #[cfg(test)] mod tests { use super::*; #[test] fn cs2_launch_args_are_byte_identical_to_the_old_hand_synced_vec() { // The exact arg vec the live launch requires for map="de_dust2", bots=9 — pins the LaunchSpec // builder to a byte-identical launch. let expected: Vec = [ "-dedicated", "-insecure", "-port", "24015", "+game_type", "1", "+game_mode", "2", "-maxplayers", "13", "+map", "de_dust2", "+sv_hibernate_when_empty", "0", "+bot_join_after_player", "0", "+bot_quota", "9", "+bot_quota_mode", "fill", "+bot_difficulty", "2", "+mp_warmuptime", "0", ] .iter() .map(|s| s.to_string()) .collect(); assert_eq!(CS2.launch.args("de_dust2", 9), expected); } }