# source2rosetta **Re-derives Source-2 engine gamedata from stripped Valve binaries โ€” and proves it on a live server.** When Valve ships a CS2 or Dota 2 engine update, every Metamod / CounterStrikeSharp plugin breaks until someone hand-reverse-engineers fresh gamedata โ€” function signatures, vtable offsets, netvar layouts. Historically that can be up to **weeks**. source2rosetta reads the stripped `.so` libraries a dedicated server maps and re-derives the whole surface in **minutes**, then launches its own vanilla server and *calls the functions* to prove they're right. It's a **standalone Rust tool, not a plugin or a framework** โ€” the gamedata it emits renders into whatever your stack already speaks (CounterStrikeSharp, Metamod/SourceMod, ModSharp, Swiftly, Plugify, or a typed C# SDK). CS2 and Dota 2 are both live-validated; a new game is a `--game` arm away. ## Docs - **[ATTRIBUTIONS.md](ATTRIBUTIONS.md) โ€” start here.** This tool stands on a decade of community reverse-engineering, catalogues, dumpers, and research. The credits come first because the work does. - **๐ŸŽฏ Just want the gamedata?** โ†’ **[Grab a release and render it for your framework.](crates/source2rosetta-core/README.md)** Download the published CS2 / Dota 2 gamedata, point `source2rosetta-gen` at it, and get CounterStrikeSharp / Metamod / ModSharp / Swiftly / Plugify / a typed C# SDK in one command. No build, no corpus โ€” the 30-second path, and what most people are here for. - [CONTRIBUTING.md](CONTRIBUTING.md) โ€” add or back-fill a gamedata entry. - [LICENSE](LICENSE) โ€” AGPL-3.0. ## Results Ballpark from a recent build, on a 16-core desktop. These move build-to-build โ€” treat them as orders of magnitude, not guarantees. | | derived functions | typed schema | model | one-time distill | |---|---|---|---|---| | **CS2** | ~1,150 `core` + ~1,200 `high_confidence`, all live-validated, plus ~4,400 `experimental` name guesses | ~1,900 classes / ~12,300 fields | ~48 MB (a few MB gzipped) | ~15 min | | **Dota 2** | ~1,900 `core` + ~1,000 `high_confidence`, plus ~6,100 `experimental` | ~2,960 classes / ~17,700 fields | ~570 MB | ~1 hr | Both derive **0-dropped** โ€” every offset and signature that ships passed live validation. Distilling the model is a one-time cost; after that, a per-build update (fold the new build in, re-derive) is **a couple of minutes** โ€” the "minutes, not weeks" the headline is about. --- ## How it works โ€” read โ†’ derive โ†’ validate โ†’ emit ### 1. Read โ€” the whole server, not just `libserver` A dedicated server maps roughly 22 shared libraries, and `libserver.so` is only a fraction of the reachable engine surface. source2rosetta reads **all of them** (multilib): `libserver`, `libengine2`, `libtier0`, `libnetworksystem`, `libschemasystem`, and the rest โ€” each locator it emits carries the library it belongs to. ELF parsing is done by hand; `.rela.dyn` relocations are resolved so that `.data.rel.ro` pointer slots (zero on disk) come back as their true as-loaded values. ### 2. Derive **Offsets & netvars โ€” near-deterministic.** Vtable offsets come from **Itanium C++ RTTI**: the type hierarchy and vtable layout are read straight out of the binary, so a method's slot index is a fact, not a guess. Field layouts come from **Valve's own SchemaSystem** reflection tables โ€” the engine emits class/field metadata (name, type, offset) as static data for its own use, and source2rosetta reads it directly. No fingerprints, no guessing. **Non-virtual signatures โ€” located, then verified.** A stripped, non-virtual function has no slot and no symbol, so it has to be *found*. source2rosetta computes a **recompilation-invariant fingerprint** of each catalogued function โ€” CFG shape, call-graph degree, mnemonic histograms, imported-symbol references. These are deliberately **abstracted statistics, never raw bytes**. Locating a function in a new build is **nearest-history under a plain, unweighted L1 distance** over those features, accepted only within a small fixed recompile threshold. To be exact about what this is *not*: there is **no trained model, no machine learning, no learned or weighted metric, no embedding network** โ€” it's a deterministic nearest-neighbour lookup against recent history, and the per-game "model" it reads is a bundle of derived *facts*, not a network. Every located address is then **re-verified independently of the match**: a fresh byte-signature is regenerated at the predicted address and confirmed to be **unique** in the target library and on a **function prologue** โ€” the same check a loader does. A match that doesn't verify isn't shipped. **ABI-shape guard.** A byte-signature sees a function's *body* drift and re-derives it, but it can't see the *argument list* change while the prologue stays recognisable โ€” the sig still resolves and points at real code, yet a caller using the old prototype passes the wrong registers. source2rosetta recovers each function's observable **SysV-AMD64 ABI shape** (which argument registers are live-in, plus the return class) via a bounded backward-liveness pass and diffs it across builds, flagging exactly those prototype changes and marking struct-by-value (sret) returns that are unsafe to blind-call. ### 3. Validate โ€” against a live server, not a spec This is what separates source2rosetta from a static dumper. `produce` and `integration-test` **launch their own** vanilla dedicated server for the game (bots on an empty deathmatch for pawn games; a pawn-less game like Dota waits on a `ready_class` proxy) โ€” no Steam, no separate instance, no human. Then, reading the running process through `/proc//mem` (read-only ptrace โ€” no injection, no debugger), it checks against ground truth: - every **offset** lands on a real vtable slot, and every **signature** on live executable code; - a gamedata function is **actually called** via ptrace to prove it's the semantically right function, not a plausible byte-match (pawn games); - derived probes are **fuzzed across changing game state** for many iterations; - field **types** are read from the live process for the typed netvars โ€” and fields that are non-null live but zero on disk (e.g. `m_pSchemaBinding`) confirm the reader is seeing real live state, not stale disk bytes. The contract is blunt: **"degrades or stops loudly, never lies."** An entry live validation confidently rejects is dropped, not shipped under a banner claiming it resolves; if the oracle can't run, it fails loudly rather than emit an unverified result. ### 4. Confidence tiers โ€” nothing vanishes silently The output is a per-game **monolith** in which every catalogue entry is accounted for, sorted into four tiers: | tier | meaning | |---|---| | `core` | derived and, in a full run, **live-validated** โ€” the load-bearing gamedata | | `high_confidence` | corroborated names folded in as verified offsets/sigs (dictionary-exact or macOS ground-truth transfer) | | `experimental` | the least-filtered band โ€” every graded name guess, each with a **resolvable locator** but an **unverified name** | | `unresolved` | catalogued but not confidently produced this build, with a reason (`sig-drifted`, `offset-low-conf`, โ€ฆ) and no locator | A function that couldn't be derived this build shows up as `unresolved` with a reason โ€” it never just disappears. ### 5. Emit The canonical model and every format emitter live in `crates/source2rosetta-core`; the deriver writes format-neutral JSON and the tiny `source2rosetta-gen` binary renders it into any framework's shape (see [Artifacts & formats](#artifacts-schemas--output-formats)). Two things to keep straight. **Multi-game:** the ELF / RTTI / SysV / SchemaSystem machinery is engine-generic; game-specific knobs (library set, launch spec, schema-probe classes, pawn anchor) live on a `profile::GameProfile`, and `--game ` selects one โ€” adding a game is a `const` plus a clap arm. **Naming is not derivation:** AI-assisted name extrapolation exists but is producer-side dev tooling, not part of the shipped deriver, and every proposed name is gated on self-naming or ground-truth corroboration and then live-validated. A wrong *name* only mislabels a real slot; it never touches the **offset**, which comes from RTTI. --- ## Install & CLI usage ```sh # The deriver (`source2rosetta`) โ€” the root binary. cargo build --release # โ†’ ./target/release/source2rosetta # The renderer (`source2rosetta-gen`) lives in the core crate and is NOT built by the root # build โ€” build it explicitly (or use --workspace). See crates/source2rosetta-core/README.md. cargo build --release -p source2rosetta-core # โ†’ ./target/release/source2rosetta-gen ``` `--game ` is a global flag (default `cs2`), given before the subcommand: `source2rosetta --game dota2 produce โ€ฆ`. | command | one line | |---|---| | `produce` | The whole per-game build in one command: derive โ†’ fold โ†’ (with `--game-dir`) validate-live + typed netvars โ†’ roll the model forward, into `--out-dir`. **`--game-dir` present = full live-validated build; absent = fast offline build (gamedata + model only). That flag is the entire offline/full switch.** | | `corpus-model` | Distill a corpus of past builds into one shippable model (vtable-alignment hops, reference fingerprints, slot timelines), so future derivation needs only the model + the target binary, not the corpus. | | `fold-model` | Roll an existing model forward by ONE build (`model N + build โ†’ N+1`), reading only the model and that one binary โ€” equal to a full re-distill. The production update path (also a sidecar inside `produce`). | | `integration-test` | Stand-alone CI live oracle: launch a vanilla server, populate it, and verify derived gamedata against it โ€” schema oracle, a semantic ptrace CALL on a live pawn, and (with `--gamedata`) a full validate-live plus optional live fuzzing. | | `backfill` | Give an extrapolated name a real cross-build timeline โ€” resolve its string anchor in every corpus build, or chain a vtable slot through the model โ€” and report history depth + consistency (how a guess graduates to first-class). | | `classify-change` | `--prev`/`--new` โ†’ `skip` / `normal` / `shift` + the exact % of function bodies that changed, comparing with position-dependent bytes masked so a pure layout shift reads as unchanged. Decides whether a build even warrants a re-derive. | | `filter-corpus` | Collapse runs of code-identical builds to one representative, label each transition `normal`/`shift`, and segment the timeline into toolchain eras. Writes the selection manifest the distill reads. | ### Quickstart Releases live on the **[releases page](https://git.lo.sh/kamal/source2rosetta/releases)**. For the offline path you need one file from there โ€” the model (`model-.json`) โ€” plus the derive inputs, which ship in this repo under `mappings/`. Put the downloaded model wherever you like; the examples assume it's in the working directory. ```sh # OFFLINE โ€” derive gamedata + roll the model forward. No server, fully deterministic. ./target/release/source2rosetta --game cs2 produce \ --seed mappings/seed-cs2.json \ --corpus-model model-cs2.json \ --target \ --out-dir out # FULL โ€” the same, plus it launches its own vanilla+bots server to validate on the live # process and read field types for the typed netvars. Adding --game-dir is the only change. ./target/release/source2rosetta --game cs2 produce \ --seed mappings/seed-cs2.json \ --corpus-model model-cs2.json \ --target \ --game-dir \ --out-dir out ``` - `--target ` (required) โ€” the build **directory** to derive from; its libraries are searched by name, so pass the directory, not a bare `.so`. - `--seed ` โ€” one file bundling every derive input (catalogue + optional naming/offset/sig sections). The loose equivalent is `--catalogue ` plus the optional `--promotable` / `--candidates` / `--full-names` / `--extra-offsets` / `--extra-sigs`, all defaulting to empty โ€” **so a brand-new game needs only a catalogue to start deriving.** - Corpus signal โ€” exactly one of `--corpus-model ` (the normal path: forward-derive from the model + the target binary, and roll the model Nโ†’N+1 as a sidecar) or `--corpus ` (fingerprint the raw build binaries on the fly). --- ## Fork it & distill your own model Nothing is hosted โ€” fork it, `cargo build --release`, and point it at a build on disk. Two ways to run, depending on whether you already have a model: - **Have a model** (downloaded from releases, or distilled): `--corpus-model model-.json` forward-derives from the model plus *only the target binary* โ€” no corpus on disk. This is the normal path. - **No model yet:** distill one from a corpus of past builds. "Distilling" is what this project means by "training" โ€” there's no ML (see [Derive](#2-derive) above); the model is the bundle of facts already described: vtable-alignment hops, reference-fingerprint windows, ABI-shape consensus, slot timelines. ```sh # Distill a corpus into a model (streaming, bounded RAM even over Dota's ~1k builds). ./target/release/source2rosetta --game cs2 corpus-model \ --seed mappings/seed-cs2.json \ --corpus corpus/binaries \ --out model-cs2.json ``` `--class-scope` (default `clean` โ€” every real game class, enough for any modding offset to derive model-only) picks which classes get slot hops. **Whatever scope you distill with, `fold-model` and `produce`'s sidecar fold must use the same one.** ### Keeping a model fresh โ€” the incremental fold Once a model exists you never need the corpus again. `fold-model` rolls it forward one build, reading only the model plus the single new binary โ€” identical to a full re-distill: ```sh ./target/release/source2rosetta --game cs2 fold-model \ --model model-cs2.json \ --seed mappings/seed-cs2.json \ --build \ --out model-cs2.next.json ``` `produce --corpus-model` runs exactly this fold as a sidecar, so a full build both derives *and* advances the model in one command. (`--class-scope` must match the model's.) ### Getting the corpus (only to bootstrap a model) A corpus is a directory of past builds, one subdirectory of `.so` files per build (`corpus/binaries/