initial commit
This commit is contained in:
commit
a2922b8bad
59 changed files with 2684583 additions and 0 deletions
367
src/profile.rs
Normal file
367
src/profile.rs
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
//! 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()];
|
||||
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/<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,
|
||||
/// 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
|
||||
/// `gamedata-<token>.json` / `model-<token>.json` / `netvars-<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 `verify-live` 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,
|
||||
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,
|
||||
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",
|
||||
"+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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue