//! 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 <map>`, in order. A value of `"{bots}"` is substituted with the runtime bot
    /// count (CS2's `+bot_quota <n>`); 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<String> {
        let mut a: Vec<String> = 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
    /// A RECORDED REFERENCE value for `IsPlayerPawn`'s vtable slot — cross-checked against, never called.
    ///
    /// The live CALL test uses the slot THIS build derived and skips entirely when the build derived none;
    /// this constant only decides whether that run prints a "the slot moved, update me" note. It is not a
    /// fallback, and must not become one: the slot has taken six distinct values in ten months, and calling
    /// a stale index would inject a call to whatever now occupies it. A new game may record 0 until measured.
    pub is_player_pawn_slot: u64,
}

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/<pid>/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,
    /// Floor on VScript bindings. Its own floor for the usual reason — a THIRD identification test,
    /// distinct from both the command sentinel and the convar convergence: a record base computed by the
    /// initialiser's own `idx*5 << 4 + [class+0x28]`. A codegen change that reshapes that arithmetic
    /// yields zero bindings while every other surface keeps reading perfectly.
    ///
    /// Set well under the observed count, which is the house rule, but the margin here is deliberately
    /// wide: the reader recovers three distinct registration forms (the packed name pair, the
    /// `movddup` single-string form, and a base copied between registers), and losing any ONE of them
    /// would still clear a tight floor while quietly dropping a third of the surface.
    pub min_vscript: usize,
    /// Floor on VScript bindings attributed to an OWNING CLASS — and the only floor here that a full run
    /// checks and an offline one skips, because zero is correct by construction offline: the descriptor
    /// reaches its class through a register loaded from memory, so nothing static recovers it.
    ///
    /// Separate from `min_vscript` because it fails independently and in the opposite direction. That floor
    /// guards the offline READER against a Valve reshape; this one guards the LIVE WALK — the string-anchor
    /// instance search, the owner read at the record's `+0x30`, the class-name read behind it. Any of those
    /// breaking leaves every binding recovered, described and located, with no class on any of them: a
    /// release that clears every other gate. It is `class` that `gen`'s `moddota` format GROUPS BY, so
    /// the artifact would ship intact while both of the files it writes came out empty.
    pub min_vscript_classed: usize,
    pub min_schema_enums: usize,
    /// Collapse floor for the recovered schema CLASS table — the largest table the deriver reads, and the
    /// one every other schema claim rests on: the artifact's whole `schema` section, the entity-output and
    /// datadesc joins, the derived type layouts, and `Identity::class_size`, which is half the identity
    /// check's conjunction.
    ///
    /// It needs its own floor because nothing else covers it. `min_schema_enums` does not — `enumerate_enums`
    /// uses classes only to exclude field arrays, so it keeps passing at zero classes. The live oracle's
    /// class gate does not either: it is SKIPPED below `ORACLE_MIN_SAMPLE` checked classes, and the sample
    /// IS the class count, so a collapse into that range disables the check that would catch it. And the
    /// offline/live layout comparison reads the same bytes through the same `CI_*` constants, so whatever
    /// survives a reshape agrees with itself.
    pub min_schema_classes: usize,
    /// Collapse floor for the DERIVED function tiers — `core + high_confidence`.
    ///
    /// Every table read out of the binary has one of these; the tool's headline product did not, and the
    /// gap is structural rather than an oversight of one number: the live oracle gates a PASS RATE over
    /// entries that reached the gamedata document, and a signature that failed to resolve never enters it.
    /// So a derive that emits forty functions instead of four thousand passes at 100% — a stale corpus
    /// model, a `--target` from the wrong branch or a missing secondary library all land there.
    ///
    /// A collapse detector, not a tight bound: set well below the observed count, like every sibling floor.
    pub min_core_functions: usize,
    /// Output game-key the game-keyed emitters use (Metamod `Games { <key> {..} }`, Plugify `{ "<key>": {..} }`).
    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
    /// `rosetta-<token>.json` / `model-<token>.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<PawnAnchor>,
}

/// 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/<pid>/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_vscript: 180,
    // observed live: 271 of 300 bindings attributed across 24 classes
    min_vscript_classed: 150,
    min_schema_enums: 250,
    // CS2 recovers 1,899. A floor at 1,200 is well clear of build-to-build drift and nowhere near
    // the range a `SchemaClassInfoData_t` reshape would leave.
    min_schema_classes: 1_200,
    // CS2 ships 1,086 core + 2,899 high-confidence = 3,985.
    min_core_functions: 2_500,
    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_vscript: 1200,
    // observed live: 1,638 of 1,841 bindings attributed across 63 classes
    min_vscript_classed: 900,
    min_schema_enums: 350,
    // Dota recovers 2,962.
    min_schema_classes: 2_000,
    // Dota ships 1,096 + 4,047 = 5,143.
    min_core_functions: 3_000,
    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<String> = [
            "-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);
    }
}
