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:
| Section | What it holds |
|---|---|
_meta | The CS2 version and Steam build ID the file was made for. |
functions | Engine functions: a signature and identity anchors for each. |
rtti | Engine classes, found by their RTTI type name. |
vtable_indices | Virtual function slots the core patches or calls. |
layouts | Struct 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 }
]
}| Key | Meaning |
|---|---|
linux | The byte signature. ? is a wildcard byte. |
hook | "funchook" if the core detours this function. |
required | If true and the function is not found, Ready Up turns itself off at load and the server runs as plain CS2. Keep this rare. |
anchors | Identity 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.
| Type | Passes when |
|---|---|
string_ref | The function body references the string within window bytes of its start. |
caller_string | A 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_string | A direct call in the first window bytes goes to a function that references the string. |
global_string | The function loads a global in its first 32 bytes, and that global is referenced near the string. For small accessors with no strings. |
mov_disp | The 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:
- The class's vtable is found through its RTTI type name.
- The slot's target must equal the named
function, or pass its ownanchors. - 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.jsonreadyup_sigcheckchecks functions, RTTI names, vtable slots and layouts. It exits non-zero on any failure.readyup_hookcheckresolves 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.mdThe CI watcher
Two GitHub workflows run the same checks:
build.ymlruns on every push and pull request. It builds the release, runs the tests, then runsreadyup_sigcheckandreadyup_hookcheckagainst the current public CS2 build.cs2-update-watch.ymlruns every 15 minutes. It reads CS2's public build ID. When the build orengine-surface.jsonchanged 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-buildbranch.
- On a failure, it opens or updates an issue with the label
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.jsonThen 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.jsonplugins/skins/docs/engine-surface.md in the repo describes each skins function and how it was checked.