Auto Tournament
SDK

Engine surface

How the core finds engine functions, how CI watches CS2 updates, and how to fix the core after one.

This page is for people who work on the core. Plugin authors do not need it: plugins never touch the engine.

The rule

If code depends on a byte pattern, an offset, a vtable index, a struct layout, a hook or an engine interface, it belongs in the core. Everything else is policy and belongs in a plugin.

Every place the core touches Valve's libserver.so is listed in one file, gamedata/engine-surface.json. The CounterStrikeSharp gamedata is not used.

  • The file is built into the core at build time.
  • The release also puts a copy next to the core, readyup/bin/linuxsteamrt64/engine-surface.json. If that copy exists, it wins. So a signature can be fixed on a server without a new build.
  • If the file on disk does not parse, the core falls back to the built-in copy and logs why.

engine-surface.json

The file has these sections:

SectionWhat it holds
_metaThe CS2 version and Steam build ID the file was made for.
functionsEngine functions: a signature and identity anchors for each.
rttiEngine classes, found by their RTTI type name.
vtable_indicesVirtual function slots the core patches or calls.
layoutsStruct offsets, each tied to code that proves them.

functions

"Host_Say": {
  "library": "server",
  "hook": "funchook",
  "linux": "55 48 89 E5 41 57 49 89 F7 41 56 41 55 41 89 D5",
  "required": false,
  "description": "Host_Say(controller, CCommand&, bool teamonly, int, const char*). Detoured (chat interception).",
  "anchors": [
    { "type": "string_ref", "string": "say_team", "window": 2048 },
    { "type": "string_ref", "string": "%s %s @ %s: ", "window": 2560 }
  ]
}
KeyMeaning
linuxThe byte signature. ? is a wildcard byte.
hook"funchook" if the core detours this function.
requiredIf true and the function is not found, Ready Up turns itself off at load and the server runs as plain CS2. Keep this rare.
anchorsIdentity checks. All must pass.

A function resolves only if its signature matches exactly once and every anchor passes. Otherwise it is unresolved.

Anchor types

An anchor proves that the match is the right function, not just a function with the same first bytes.

TypePasses when
string_refThe function body references the string within window bytes of its start.
caller_stringA direct call to the function (or to a thunk that jumps to it, with via_thunk) has a reference to the string within window bytes before it.
callee_stringA direct call in the first window bytes goes to a function that references the string.
global_stringThe function loads a global in its first 32 bytes, and that global is referenced near the string. For small accessors with no strings.
mov_dispThe first window bytes have a mov with this displacement. Pins a struct offset to the code that reads it.

rtti, vtable_indices and layouts

A vtable slot is checked before the core patches or calls it:

  1. The class's vtable is found through its RTTI type name.
  2. The slot's target must equal the named function, or pass its own anchors.
  3. At runtime, the live object's vtable pointer must equal that vtable.

A slot that fails is never patched.

A layout, such as the argc and argv offsets of CCommand, is only trusted when the entry named in verified_by passes. That entry's anchors read exactly those offsets.

Features

Every feature lists the engine surface it needs in core/src/readyup/features.cpp. For example, ready_hud needs the GameFrame hook, LegacyGameEventListener and the game event manager.

  • A feature is on only while all its needs are met.
  • If a need is known to be missing, the feature turns off and logs one line: feature <name> DISABLED: needs <what>. Everything else keeps working.
  • A need that is not known yet, such as the entity system before the first map, keeps the feature pending. It is off, with no log line.

ru selftest shows this table under [features]. See Troubleshooting.

When you add engine code, add its entries to engine-surface.json with at least one anchor each, and list them as needs of the feature that uses them.

Checking a CS2 build offline

Two tools check the file against a copy of Valve's libserver.so, without a running server:

build/readyup_sigcheck /path/to/game/csgo/bin/linuxsteamrt64/libserver.so gamedata/engine-surface.json
build/readyup_hookcheck /path/to/libserver.so gamedata/engine-surface.json
  • readyup_sigcheck checks functions, RTTI names, vtable slots and layouts. It exits non-zero on any failure.
  • readyup_hookcheck resolves each "hook": "funchook" entry and prepares the detour on a copy of the binary, without installing it. It fails if the function's first instructions cannot be moved into a trampoline, for example because of a jump back into them.

To get the files without a CS2 install:

scripts/ci/fetch-cs2-binaries.sh /tmp/cs2        # about 18 MB, no Steam login
scripts/ci/verify-cs2.sh /tmp/cs2 build-sniper   # sigcheck + hookcheck + report.md

The CI watcher

Two GitHub workflows run the same checks:

  • build.yml runs on every push and pull request. It builds the release, runs the tests, then runs readyup_sigcheck and readyup_hookcheck against the current public CS2 build.
  • cs2-update-watch.yml runs every 15 minutes. It reads CS2's public build ID. When the build or engine-surface.json changed since the last run, it downloads only the needed Linux server files (depots 2347773 and 2347770, anonymous) and runs both checks.
    • On a failure, it opens or updates an issue with the label cs2-update, and posts to Discord if that is set up.
    • On a pass, it comments CS2 build N verified, and closes the open issue if there is one.
    • It keeps its state (last build ID, file hash, result) on the cs2-build branch.

Fixing the core after a CS2 update

See what broke

Read the cs2-update issue, or run scripts/ci/verify-cs2.sh yourself. It lists each entry that no longer resolves, and why: no match, more than one match, or a failed anchor.

Find the function again

Open the new libserver.so in a disassembler. Start from the anchors: find the string, then the code that references it, then the function. The description and the anchors in the entry say what to look for.

Update the entry

Write a new signature that matches exactly once. Keep the anchors, or add better ones if the old ones no longer fit. Update _meta with the new CS2 version and build ID.

Check it

./build.sh
build/readyup_sigcheck /tmp/cs2/game/csgo/bin/linuxsteamrt64/libserver.so gamedata/engine-surface.json
build/readyup_hookcheck /tmp/cs2/game/csgo/bin/linuxsteamrt64/libserver.so gamedata/engine-surface.json

Then run ru selftest on a test server with the new build. It must end with PASS.

Ship it

Open a pull request. The fixed engine-surface.json can also go to servers right away: put it next to the core and restart. Plugins do not need a rebuild.

If only a struct offset moved, check layouts and the mov_disp anchors. Game fields, such as CCSPlayerPawn::m_EconGloves, are looked up by name at runtime and do not need a change.

Skins

Skins need their own engine functions (item attributes, knife subclass, models, bodygroups). The core still owns them, but their entries live in a separate file, gamedata/engine-surface.skins.json, that only ships with the skins package. It sits next to the core as readyup/bin/linuxsteamrt64/engine-surface.skins.json.

  • At start, the core adds the entries from that file to its own list. A file may only add entries, not change the core's.
  • Without the file, the core does not look for these functions, and the matching API functions (econ_attr_set_by_name, entity_change_subclass, entity_set_model, entity_set_bodygroup_by_name) return 0.
  • The core only reads the file at start. After you add or change it, restart the server.

Check both files together:

build/readyup_sigcheck /path/to/libserver.so gamedata/engine-surface.json gamedata/engine-surface.skins.json

plugins/skins/docs/engine-surface.md in the repo describes each skins function and how it was checked.

On this page