# source2rosetta-gen

Render a published [source2rosetta](../../README.md) 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:

```sh
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.

```sh
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](#if-you-derived-the-artifact-yourself).)

### 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:

- `metamod` writes `Games { <game_key> { … } }`. The consuming plugin looks that section up by the engine's
  own `GetGameDir()` — see [cs2kz-metamod's reader][kz] — so `dota` is what a Dota 2 plugin will look for.
  Metamod takes Dota 2 as a first-class SDK target ([`dota.json`][mm], `define: DOTA`, `source2: true`).
- `plugify` writes `{ "<game_key>": { … } }`, matched against the `S2SDK_GAME_NAME` its s2sdk plugin was
  BUILT with (default `csgo`).

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.

[kz]: https://github.com/KZGlobalTeam/cs2kz-metamod/blob/dev/src/utils/gameconfig.cpp
[mm]: https://github.com/alliedmodders/hl2sdk-manifests/blob/master/manifests/dota.json

### 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) |

```sh
# 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](../../README.md#artifacts-schemas--output-formats).

## 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`, `netvars` and `moddota` have 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.** `validated` is `null` on 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](../../README.md) · [AGPL-3.0](../../LICENSE).
