regen-gamedata-linux
Regenerate the LINUX side of one or more CounterStrikeSharp-style gamedata.json files (signatures AND vtable offsets) against a freshly-opened CS2 libserver.so in a running IDA Pro GUI + ida-pro-mcp.
SKILL.md.claude/skills/regen-gamedata-linux/SKILL.md
GitHubRegenerate gamedata (linux) for a new game version
One-shot refresh of the linux side of the gamedata files listed in
references/targets.json (CSS gamedata.json, matchzy matchzy.json,
weaponpaints weaponpaints.json). Drives the already-running IDA Pro GUI +
ida-pro-mcp loaded on the new build's libserver.so. The windows side of
every entry is preserved verbatim — this only touches signatures.linux and
offsets.linux.
Tooling stack (hybrid) — where this skill fits
Three tools split the work on a new game version; use each for what it's best at:
| tool | role | drive |
|---|---|---|
cs2-sig-tracker (~/cs2-sig-tracker) |
headless CI triage — which signatures broke | git pull → reports/<proj>/status.json |
| this skill (regen-gamedata-linux) | headless bulk regen — 36 sigs + the 14 clean class-vtable offsets, LLM-free | MCP py_eval on running IDA |
IDA-GameDataTracker (gdt64 plugin, GUI) |
the hard tail — the 4 non-class-vtable offsets (AddResource, GameEntitySystem, GameEventManager, CTakeDamageInfo_HitGroup) + windows offsets |
Edit → GameDataTracker (interactive, minutes/update) |
GDT is a GUI plugin (dialog I/O, no headless mode) — run it by hand for the low-
frequency hard entries; don't try to headless it. Its actions are registered
(gamedata_tracker:import_gamedata / backtrack / export_gamedata) and also
fire via ida_kernwin.process_ui_action(...), but each pops a file dialog, so
it stays a human-in-the-loop step. Companion plugins installed alongside:
vtable64 (VTableExplorer — Itanium + MSVC RTTI, use for windows offsets)
and fusion64 (IDA-Fusion sigmaker).
Two entry kinds, two mechanisms — both deterministic (no LLM) on the fast path:
| gamedata key | mechanism |
|---|---|
signatures.linux |
relocate old sig → resolve func start → regen fresh unique sig |
offsets.linux (int) |
look up offsets-anchors.json → per-kind re-find (vtable RTTI walk / structoffset / global) → validate/re-derive |
Offset entries are not all vtable slots — the anchor map classifies each as
vtable, structoffset, or global. See Section B.
The whole point: self-healing. Most entries relocate + regen with zero
human input as long as the function head is unchanged. Only genuinely-changed
functions fall through to the per-symbol find-<Symbol> skill.
Prerequisites
- IDA Pro GUI open on the new build's
libserver.so, ida-pro-mcp server running (see "Auto-enable MCP" below to avoid the manualCtrl+Alt+M). - Confirm the loaded binary is the linux server module:
The root filename must bemcp__ida-pro-mcp__server_health # is MCP up? mcp__ida-pro-mcp__survey_binary detail_level=minimallibserver.so. If multiple IDA instances are open (e.g. also server.dll for the windows side), pick the linux one withmcp__ida-pro-mcp__select_instancefirst. Abort if a.dllis loaded — this skill regenerates the linux side only. - Auto-analysis finished (
survey_binaryreturns a sha256/md5).
Signature ↔ IDA wildcard convention
gamedata stores one ? per wildcard byte; IDA find_bytes / parse_binpat
and the sig generator use ??. Convert on the way in and out:
- to search a stored sig:
old.replace('?', '??')(only lone?tokens) - to store a generated sig:
new.replace('??', '?')
Helper (matches regen_css_linux.py):
def gd_to_ida(s): return " ".join("??" if t == "?" else t for t in s.split())
def ida_to_gd(s): return " ".join("?" if t in ("??", "?") else t for t in s.split())
Procedure
Load references/targets.json and references/offsets-anchors.json. For each
file (expand ~), read the JSON, then walk every entry. Keep a per-file
summary: regen / same / broken / offset-ok / offset-stale.
LLM-free contract (default) + when the LLM is allowed
The whole fast path is deterministic — no agent/LLM. Everything below is
pure py_eval / find_bytes / RTTI pointer math on the loaded image:
- signature relocate + regen (Section A.1–A.2)
- offset anchor + RTTI walk (Section B, all three kinds)
The only step that can spend an LLM is the find-<Symbol> fallback
(Section A.3 / B when the anchor itself is gone), and even most find-skills
resolve via strings/xrefs without decompiling. So pick a mode up front:
| mode | broken/stale handling | LLM |
|---|---|---|
| mechanical (default) | relocate + regen only; anything that can't self-heal is reported, not fixed | none, ever |
| llm-fallback | mechanical first; genuine misses go to the find-<Symbol> skill (may decompile) |
only on real misses |
Run mechanical for a fast unattended refresh right after a game update — it
regenerates every unchanged sig/offset with zero token spend and hands you a
short "needs attention" list. Escalate to llm-fallback only for that list.
Mirrors the main pipeline's -oldgamever reuse ("unchanged symbols ~free, no
agent/LLM").
Phase 0 (optional but recommended): triage with cs2-sig-tracker
Before opening/scanning anything, narrow the worklist for free. The tracker
(~/cs2-sig-tracker, mirror of git.miksen.me/mikkel/cs2-signatures) already
scans each signature against the live binaries every update and records the
result per platform.
git -C ~/cs2-sig-tracker pull --ff-only
Then for each file whose tracker_project is non-null, read
~/cs2-sig-tracker/reports/<project>/status.json:
# results[].name, kind ('signature'|'offset'), platforms.linux.status
# status ∈ ok | broken | ambiguous | no_binary | missing_sig
worklist = [r["name"] for r in status["results"]
if r["kind"] == "signature"
and r["platforms"]["linux"]["status"] in ("broken", "ambiguous")]
Guard rail: the tracker's verdict is only valid if it ran on the same
build you have in IDA. Compare status["manifest"] (and the doc's "Last
updated") against your loaded build; if the tracker is behind, ignore its
verdict and validate everything in IDA.
What the tracker does and does not give you:
- ✅ tells you exactly which signature entries broke/ambiguous → your regen
worklist. If all
okand manifests match, you can skip IDA for that file's sigs entirely. - ❌ does not emit a replacement sig — its
sigfield is the current (possibly broken) stored sig, same as gamedata.json. Regen is still IDA + find-skill. - ❌ does not validate offsets at all (
kind: "offset",tally.offset_only). A vtable index isn't a byte pattern, so there is no external signal — offsets can only be checked by the RTTI walk in Section B. - ❌ weaponpaints is not tracked (
tracker_project: null) — no triage, always validate its 2 sigs in IDA.
Use Phase 0 to shrink the loop: prioritise the tracker's broken/ambiguous signature entries, but still run Section B on every offset entry (they have no tracker coverage).
A. Signature entries (signatures.linux present)
1. Relocate the old sig on the new binary. Count matches + containing-func
starts across .text in one py_eval:
mcp__ida-pro-mcp__py_eval code="""
import json, ida_bytes, ida_ida, ida_funcs
sig = "<OLD_SIG_AS_IDA>" # old linux sig, ? -> ??
pat = ida_bytes.compiled_binpat_vec_t()
err = ida_bytes.parse_binpat_str(pat, ida_ida.inf_get_min_ea(), sig, 16)
res = {"parse_err": err, "hits": [], "func_starts": []}
if not err:
ea, end = ida_ida.inf_get_min_ea(), ida_ida.inf_get_max_ea()
for _ in range(20):
out = ida_bytes.bin_search(ea, end, pat, ida_bytes.BIN_SEARCH_FORWARD)
hit = out[0] if isinstance(out, tuple) else out
if hit in (None, ida_ida.BADADDR, 0xffffffffffffffff): break
res["hits"].append(int(hit))
f = ida_funcs.get_func(hit)
res["func_starts"].append(int(f.start_ea) if f else int(hit))
ea = hit + 1
print(json.dumps(res))
"""
Interpret hits / unique func_starts:
- exactly 1 hit, 1 unique func start → function relocated cleanly. Go to A.2.
- 0 hits or >1 func start → BROKEN (function changed / sig no longer unique). Go to A.3.
2. Regenerate a fresh minimal-unique sig. Call
/generate-signature-for-function with addr=<func_start>. Take its
func_sig, convert ?? → ?, and if different from the stored sig, stage the
change (signatures.linux = new). Log REGEN; if identical log same and
leave as-is.
3. BROKEN → re-discover. In mechanical mode do not spawn anything:
log BROKEN — needs llm-fallback (skill exists) or BROKEN — manual (no skill)
and move on. Only in llm-fallback mode proceed to re-discover via the
symbol's find-skill. Resolve it by globbing the sibling skills dir for the
symbol name:
ls -d ~/CS2_VibeSignatures/.claude/skills/find-*<SYMBOL>* 2>/dev/null
Symbols are frequently bundled (e.g.
find-CCSPlayer_ItemServices_GiveNamedItem-AND-...), so a substring match on
the entry name finds the right combined skill.
- skill found → invoke it (it re-anchors via strings/xrefs/vtable, renames
the func, and emits a fresh sig). Take its resulting func addr → run
/generate-signature-for-function→ stagesignatures.linux. LogREGEN (via find-skill). - no skill (many CSS entries) → leave the old sig, log
BROKEN — manualand add to the report. Do not guess. These are the only entries needing hand work; the report tells you exactly which.
B. Offset entries (offsets.linux present, integer)
Offset entries are heterogeneous — a slot int can be a vtable index, a
struct-member offset, or a global/member offset. Look the symbol up in
references/offsets-anchors.json (files.<tag>.<symbol>) to get its kind and
re-find recipe. Never assume "offset == vtable slot".
"CCSPlayerController_ChangeTeam": {
"offsets": {"windows": 103, "linux": 102},
"kind": "vtable", "vtable_class": "CCSPlayerController",
"anchor_strings": ["ChangeTeam() CTMDBG"], "func_find": "find-...", "needs_review": false
}
If needs_review: true, confirm the class/kind against the binary the first
time (the 6 flagged CSS entries), then flip it in the map.
kind = vtable (14 of 18 CSS offsets)
Validate the stored slot, re-derive if stale (from validate-gamedata-offsets,
proven on stripped libserver.so):
- Locate the target function on the new binary (LLM-free where possible):
anchor_stringspresent →find_bytes(anchor_string); the func containing that string ref is the target. (ChangeTeam only, so far.)- else → the func has its own find recipe (
func_find): if it also carries asignatures.linuxentry, relocate that sig (Section A.1) → func start; else inllm-fallbackmode invoke thefind-<Symbol>skill.
- RTTI walk to the primary vtable of
vtable_class(no symbols):- typeinfo NAME bytes
"<len><ClassName>\0",len= char count (CCSPlayerController= 19). Locate viafind_byteson hex. find_bytes(ptr_to_name_str)→ typeinfo = match where the name ptr sits atstruct+8.find_bytes(ptr_to_typeinfo)→ each match = a vtable's typeinfo slot (vtable_base+8); candidate =match-8.- primary vtable = candidate whose qword at
base+0(offset_to_top) == 0.
- typeinfo NAME bytes
- Slot → func:
func = *(vtable_base + 0x10 + N*8)(slot 0 atbase+0x10; first two qwords are offset_to_top + typeinfo). N = stored linux offset. - Compare: slot-N func == the func from step 1 → offset-ok, keep.
Mismatch → offset-stale: scan the whole vtable for the step-1 func addr,
stage
offsets.linux = new_index. Skills/get-vtable-index,/get-vtable-addressimplement steps 2–4.
Gotchas (proven on build 14171 — heed these):
- Raise the typeinfo→vtable search cap for base classes. In step 2, the
"pointers to typeinfo" scan for a heavily-inherited base (e.g.
CBaseEntity) has thousands of hits (every derived__si_class_type_inforeferences it). A small cap (~80) truncates before the real vtable and yields a false "no primary vtable". Usecap >= 20000for that inner search; keep the name-string and name-pointer scans small. - Verify the slot by decompiling, not
XrefsTo. When the strings cache is cold (server_health→strings_cache_ready:false),idautils.XrefsTo(str)returns nothing, so the "func containing the anchor string" route fails. The reliable anchored check is: decompile the slot-N func and substring-match the anchor (confirmed: slot 102 ofCCSPlayerControllerdecompiles containing"ChangeTeam() CTMDBG"). For symbols with no anchor string (most), fall back to structural validation: confirm slot N is< vtable_lengthand points into.text. A plausible-but-unverified slot with no anchor goes on theneeds reviewlist, not silently trusted. - Entity vfunc offsets → validate on a CONCRETE class, not the abstract base.
An overridable entity method (e.g.
SetStateChanged, slot 29) is anullsubplaceholder in the abstract base vtable (CEntityInstance) and only filled in on concrete entities. Resolve such offsets againstCBaseEntity(or a concrete entity likeCCSPlayerController, which share the slot), not the declared base. Confirmed 14171:CEntityInstanceslot 29 =nullsub;CBaseEntity/CCSPlayerControllerslot 29 = the realSetStateChangeddispatcher. - Class name → typeinfo string is length-prefixed:
"<len><ClassName>\0",len= character count as decimal (CBaseEntity→11CBaseEntity,CCSPlayerController→19CCSPlayerController). Some gamedata "class" prefixes have no such RTTI name (e.g.CEntityResourceManifest,CGameResourceService— absent on 14171): abstract interfaces / non-RTTI managers. Resolve those via a concrete impl or anchor-relocation, not a name walk.
Cold xref-DB workarounds (the analysis DB often lacks data/rip-relative xrefs
on a freshly-loaded stripped binary — strings_cache_ready:false):
idautils.XrefsTo(string_or_vtable)returns nothing for data targets → don't rely on it. ButXrefsTo(func)DOES work — call xrefs (E8) are indexed, so caller-chains resolve fine.- To find who references a vtable (the ctor), scan for the rip-relative
lea reg, [rip+disp]whose target isvtable_base + 0x10(the vptr stored in objects is the first method slot, not the vtable base). Patterns4[8C] 8D {05,0D,15,1D,25,2D,35,3D} ?? ?? ?? ??; for each,target = ea + 7 + int32(disp). Use a highfind_bytescap (≥100k) —48 8D 05alone occurs ~120k times in libserver.so; a small cap silently truncates before your hit (this bit twice on 14171). - To find call sites of a func, prefer
XrefsTo(func)over scanning allE8(which exceeds 100k and truncates).
kind = structoffset (CTakeDamageInfo_HitGroup, CheckTransmitPlayerSlot)
Not a vtable index — it's a byte offset of a struct member. Re-derive with the
struct-offset flow (/generate-signature-for-structoffset +
/write-structoffset-as-yaml), anchored on the accessor described in
source_script (e.g. find-CCheckTransmitInfo_m_nPlayerSlot.py). The stored
int is the member offset; confirm it against the instruction the sig pins.
kind = global (GameEntitySystem, GameEventManager)
Member/global offset, not a vtable slot. Resolve via the global's find recipe
(source_script, e.g. find-s_GameEventManager.py / find-CGameEntitySystem_ctor.py)
and its TARGET_GLOBALVAR_NAMES anchor; the stored int is the member offset in
the owning struct. needs_review: true — validate the first time.
no recipe
kind: "unknown" or a missing anchor → in mechanical mode log
offset — no anchor and leave as-is; the report lists it for manual work. Do
not guess a slot.
C. Write back (preserve windows)
After all entries of a file are processed, write the JSON back only if something changed, preserving key order and the untouched windows/library keys. Use 2-space indent + trailing newline (matches the pipeline). Back up first:
cp <file> <file>.bak.<mtime> # timestamped backup
Then write the merged dict (stage changes onto the original loaded dict; never
drop signatures.windows / offsets.windows / library).
D. Report
Print one block per file, then a combined tail:
=== <tag> (<path>) [mode: mechanical|llm-fallback] ===
REGEN : <n> (fresh linux sig written)
same : <n> (function unchanged, sig still unique)
offset : <ok>/<stale> by kind vtable/structoffset/global (stale re-indexed)
BROKEN — needs llm-fallback : <list> (skill exists; mechanical mode skipped it)
BROKEN — via find-skill : <list> (llm-fallback mode re-found)
BROKEN — manual : <list> (no skill / no anchor — needs you)
offset — needs review : <list> (anchor map flagged; confirm once)
The bottom three lists are the deliverable: a short, exact set of symbols that
didn't self-heal. In mechanical mode everything else regenerated with zero
token spend; re-run those in llm-fallback (or by hand) when convenient.
Windows side (later)
This skill is linux-only by design (your workflow starts linux-first). For the
windows sig, re-run Section A in a server.dll IDA instance and merge into each
file's signatures.windows. The CSS repo's update_css_gamedata.sh already
does both platforms headlessly if you prefer batch over GUI.
Windows offsets are harder. The Section B RTTI walk is Itanium (Linux) only.
Windows uses MSVC RTTI — RTTICompleteObjectLocator with image-relative
32-bit RVAs and a different vtable/typeinfo layout — so the walk must be
rewritten for PE before offsets.windows can self-heal. Until then, re-derive
windows offsets with /get-vtable-index on the loaded server.dll (it handles
either layout) and merge manually. This is the intended "windows last" follow-up.
Auto-enable MCP (skip the Ctrl+Alt+M)
The ida-pro-mcp plugin already supports autostart — it's a per-IDB
preference (netnode $ ida_mcp.autostart, default ON) that fires on
ready_to_run when running in the GUI. If you keep needing Ctrl+Alt+M, that
IDB has autostart turned off. Fix once, persists into the .i64:
Edit → Plugins → MCP Configuration → tick "Autostart server when IDA opens".
For a brand-new build's fresh .i64 the default is already ON, so
~/lin.sh <gamever> should start MCP with no keypress. If you want a
belt-and-braces global guarantee across every build, drop this in
~/.idapro/idapythonrc.py (runs on every DB open; harmless if the plugin
already autostarted — run restarts cleanly):
# Force ida-pro-mcp to start in the GUI without Ctrl+Alt+M.
import ida_kernwin, ida_loader
class _MCPAuto(ida_kernwin.UI_Hooks):
def ready_to_run(self):
if ida_kernwin.is_idaq():
ida_loader.load_and_run_plugin("ida_mcp", 0) # plugin file = ida_mcp
self.unhook()
_mcp_auto = _MCPAuto(); _mcp_auto.hook()
Symbol → find-skill coverage (as of build 14171)
Routing is dynamic (glob), but for reference — coverage of the broken-sig fallback:
- matchzy.json — 5/5 have find-skills (all
*_Create+CCSGameRules_PostCleanUp). - weaponpaints.json — 2/2 (
CAttributeList_SetOrAddAttributeValueByName,UpdateItemView). - CSS gamedata.json — ~half. NO-SKILL entries (e.g.
UTIL_Remove,CGameEventManager_Init,CCSPlayerController_ChangeTeamoffset,SetStateChanged,GameEntitySystem) self-heal via relocation; if a NO-SKILL entry breaks it lands in "BROKEN — manual".