| .. | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
source2rosetta-gen
Render a published source2rosetta release into whatever format your framework reads.
source2rosetta does the hard part — deriving CS2 / Dota 2 gamedata from the stripped engine and
validating it on a live server — and publishes one file per game, rosetta-<game>.json.
source2rosetta-gen turns that into CounterStrikeSharp, Metamod/SourceMod, ModSharp, Swiftly, Plugify, a
typed C# SDK, or the Dota script API, locally, in a second.
It's deliberately tiny: it links only source2rosetta-core (serde + the format emitters) — no ELF reader,
no disassembler, no ptrace. So a consumer who "just wants the files" downloads one release + this small
binary and generates exactly what they need, instead of every format being pre-baked into the release.
Get it
Grab the prebuilt source2rosetta-gen from the release page, or build it from source:
cargo build --release -p source2rosetta-core
# -> target/release/source2rosetta-gen
(The gen binary lives in the source2rosetta-core crate, so a plain cargo build --release at the repo root
does not build it — use -p source2rosetta-core or --workspace.)
Use it
One input, one flag. --format says who the output is for; --out is a directory, because most
formats write more than one file.
R=https://git.lo.sh/kamal/source2rosetta/releases/download/cs2-latest
curl -fsSLO $R/rosetta-cs2.json
# CounterStrikeSharp: the combined gamedata + typed call sites for the same functions
source2rosetta-gen --from rosetta-cs2.json --format cssharp --out ./csharp
# Metamod:Source / SourceMod: the gamedata VDF + a C++ prototype header
source2rosetta-gen --from rosetta-cs2.json --format metamod --out ./mm
# A typed C# SDK from the schema — one `static class` per engine class, `const` offsets + types
source2rosetta-gen --from rosetta-cs2.json --format cs-sdk --out ./sdk
# The Dota script API: ModDota's dota-data shape AND the TypeScript declarations
source2rosetta-gen --from rosetta-dota2.json --format moddota --out ./dota
Each run prints what it wrote.
Formats
A framework gets two files, and it needs both. The gamedata says where a function is; the call sites say how to call it. They were separate inputs when the release was four files; one artifact makes them one command.
--format |
writes | notes |
|---|---|---|
cssharp (default) |
gamedata.json + RosettaFunctions.cs |
the gamedata is JSONC — banner comments, so a strict JSON parser will reject it. The .cs is MemoryFunction* fields / VirtualFunction* factories |
metamod |
<game_key>.games.txt + rosetta_prototypes.h |
the VDF also covers SourceMod. Metamod plugins are C++, so the prototypes are a header of using X_t = RET (*)(…) plus an X_vtidx constant per slot |
modsharp |
gamedata.json + RosettaCalls.cs |
[AddressKey] interface for its Roslyn generator, plus a vtable-dispatch class |
swiftly |
gamedata.json + prototypes.json |
signature entries only in the gamedata; that framework takes offsets through a separate file |
plugify |
gamedata.json + prototypes.json |
runtime type arrays ({"paramTypes":["pointer","string"],"retType":"void"}) |
cs-sdk |
Schema.cs |
typed C# SDK: static class per schema class, const int field offsets tagged with their type, plus the engine's own enums at their real width |
netvars |
netvars.json |
flat schema map, { class: { field: offset } } |
moddota |
api.json + api.d.ts |
the VScript API in ModDota dota-data's shape (their toolchain renders from it), plus TypeScript declarations for authors using the published packages as-is — one interface per class, Valve's own description as the doc comment |
flat |
gamedata-flat.json |
the selected tiers as one name → locator map, format-neutral |
Every published artifact renders every format on this list. The releases are always derived against a running server, so nothing here is conditional on how the artifact was made. (If you derive your own, that changes — see below.)
Which games a format covers
The artifact states its own game (meta.game_key, csgo or dota) and the output follows it — there is
no --game flag, because a second place to state one fact is a second place for it to be wrong.
Two formats are game-keyed, and for both the key is the game DIRECTORY the server runs out of, which is
what game_key already holds:
metamodwritesGames { <game_key> { … } }. The consuming plugin looks that section up by the engine's ownGetGameDir()— see cs2kz-metamod's reader — sodotais what a Dota 2 plugin will look for. Metamod takes Dota 2 as a first-class SDK target (dota.json,define: DOTA,source2: true).plugifywrites{ "<game_key>": { … } }, matched against theS2SDK_GAME_NAMEits s2sdk plugin was BUILT with (defaultcsgo).
The rest are game-neutral in shape: modsharp and cssharp carry no game key at all (flat, keyed only by
platform), and flat / cs-sdk / netvars / moddota are plain data.
Two consumers cannot run on Dota 2 at all, and gen declines rather than write a file that can never
load: CounterStrikeSharp resolves its binaries out of <dir>/csgo/bin/, and Swiftly initialises against the
csgo game directory. --force renders anyway. It is a warning rather than a rule on purpose — that is a
claim about somebody else's project, read out of their source at one point in time, and projects add games.
ModSharp is CS2-first but not excluded. Its own paths are hardcoded (../../csgo/steam.inf), yet its
gamedata carries no game key whatsoever — flat Addresses / VFuncs, platform-keyed — so the file rendered
here is the same one whatever game the build targets.
Descriptions
Every call site is emitted with a sentence saying what the function is FOR, wherever that target's
readers hover: a C# XML <summary>, so IntelliSense shows it; a comment above the C++ typedef; a
description field in the data formats. The prototype keeps a home of its own — a <remarks> in C#, the
identity line in the header — so nothing is lost to make room. That is every emittable call site: 2,073 on
CS2, 2,326 on Dota.
Each one says whose sentence it is, and that is not decoration. Some are Valve's own, read out of a
registry in the binary (717 CS2 / 774 Dota); the rest are this project's reading of the build, and the two
carry very different weight. So every generated source prints the origin beside the text, the data formats
carry it as an id (valve / derived / generated), and moddota — which deliberately mirrors a shape the
Dota ecosystem already publishes — keeps ours under keys of our own name rather than in the description
field their toolchain renders as Valve's word.
Valve's text always wins. Where the binary documents a function, that is what ships; a generated
description only ever fills a gap, so the two can never disagree in an output. Between Valve's own two
registries the script one wins: a console registration's help text documents the COMMAND an operator types,
while a script binding documents the function. In the .d.ts, a member Valve documents reads exactly as it
did before — bare, the way the published types do — and only a gap Valve left is filled and marked.
cs-sdk and netvars render the schema, which is classes and field offsets. There are no functions in them
to describe, so they carry none.
What the call sites will and won't emit
Only functions the deriver could stand behind: status: verified or lower-bound (the declaration passes
registers the callee never reads and contradicts it in none — safe to call, and marked as such in every
output), a receiver settled by evidence (the declaration names it, a live-validated vtable slot proves it, or
the measurement independently agrees), and every parameter mappable onto an ABI class. A function whose return
nobody declared is still emitted — otherwise Dota would lose 2,304 of its 3,732 call sites — but it is
marked as such in every output (ret_declared / retDeclared in the data formats, prose in the generated
source), and the value is documented as the raw return register rather than a typed result.
The two locator forms are not interchangeable, and every output distinguishes them. A signature resolves to
one address; a vtable slot is entered through the object, so the framework reaches it by a different call
entirely — VirtualFunctionVoid(instance, slot) rather than GameData.GetSignature(key), GetVFuncIndex
rather than GetAddress, (*(void***)self)[idx] rather than a scanned pointer. Roughly a quarter of the
call sites are vtable-located.
A few entries carry BOTH, which is worth knowing if you consume the gamedata rather than these call
sites: model::Entry allows it and five CS2 core entries use it. Both locators are live-validated
independently, and validated: true means both passed — an entry whose signature checked out but whose slot
could not be reached ships null, never true. The call-site emitters here pick one form per function, so
this only affects what you read out of the artifact directly.
The receiver is always in the type list. Where the declaration came from an Itanium-mangled symbol this
is invisible, so it is prepended, spelled from the function's own class (CBaseEntity*, not void*) and
marked [this] in the C++ header. It is a real register in the call frame — leaving it out shifts every
argument by one.
Two things moddota states honestly rather than guesses. Parameters are declared ...args: any[],
because the registry does not carry them — types appear only inside Valve's prose descriptions, inconsistently,
in about a fifth of entries. It is visibly ugly on purpose: nobody should mistake these for complete
declarations, and inventing plausible arity would emit declarations that lie rather than abstain. And
available is always server, because a dedicated server never maps libclient, so this derivation cannot
see the client side at all.
Confidence tier
The locator half takes a --tier, cumulative and defaulting to high_confidence:
--tier |
includes |
|---|---|
core |
only the guaranteed, first-class entries |
high_confidence (default) |
core + the promoted (verified-name) entries |
experimental |
the above + every graded name guess (each has a resolvable locator, but an unverified name) |
# only the rock-solid set:
source2rosetta-gen --from rosetta-cs2.json --format cssharp --tier core --out ./csharp
The schema and script-API formats ignore --tier.
What is in the artifact that gen does not render
rosetta-<game>.json is plain JSON and readable as-is, and it carries more than these formats consume: the
Pulse binding registry (typed signatures, call policy, and a callable shim per binding), entity outputs,
map classname → C++ class, ConVars with decoded flags, and the declared rows that belong to no function here
(surfaces.unjoined). Those have no loader to render them into — read them directly. The per-function
descriptions are in there too, and they DO reach the outputs, but only for the ~2,000 functions that become
call sites; the artifact describes about twice as many. See the artifacts section.
If you derived the artifact yourself
Skip this if you downloaded the artifact — it is about source2rosetta produce, not about gen.
The releases are derived against a running server, which is where two whole surfaces come from. Run
produce without --game-dir and it derives what it can offline, honestly, and states the rest as absent —
so gen gets an artifact that is real but smaller, and three things follow:
cs-sdk,netvarsandmoddotahave nothing to render, and say so rather than writing an empty file. The schema's field TYPES are runtime-resolved, and a VScript binding's owning class is reached through a register loaded from memory, so neither is readable from the file alone. All three group by one or the other.- No call site is reached through a vtable. A slot is only recorded once live validation has confirmed it is really a vtable slot and not a carried member offset, so an offline run states no slot rather than guess one. Roughly a quarter of a live manifest's call sites are vtable-located; an offline one has none. Same shape, fewer entries — worth knowing before diffing two outputs made different ways.
- Nothing has been checked against a running server.
validatedisnullon every record rather than a verdict, and the locator half drops only what live validation confidently REJECTED — so an offline artifact keeps entries a live one would have thrown out. The tiers still mean what they mean; they just have not been tested.
Part of source2rosetta · AGPL-3.0.