Convert an existing find-XXXX SKILL.md into a preprocessor Python script, updating configs/<GAMEVER>.yaml
and removing the old SKILL.md. Covers xref-string-based and LLM_DECOMPILE-based discovery patterns.
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Convert an existing find-XXXX SKILL.md into a preprocessor Python script, updating configs/<GAMEVER>.yaml
and removing the old SKILL.md. Covers xref-string-based and LLM_DECOMPILE-based discovery patterns.
disable-model-invocation
true
Convert Finder SKILL.md to Preprocessor Script
Port an existing .claude/skills/find-XXXX/SKILL.md into an ida_preprocessor_scripts/find-XXXX.py
preprocessor script, update configs/<GAMEVER>.yaml entries, and delete the old SKILL.md.
When to Use
A find-XXXX SKILL.md exists in .claude/skills/ and needs to be converted to a preprocessor script
The SKILL.md uses either xref-string search (find_regex / xrefs_to) or decompile-based vtable analysis to discover functions
Overview
Eight preprocessor patterns exist. The SKILL.md's discovery method and target type determine which to use:
Pattern
Discovery Method
Has FUNC_XREFS
Has LLM_DECOMPILE
Has INHERIT_VFUNCS
Has FUNC_VTABLE_RELATIONS
preprocess_skill has llm_config
A — Regular function via xref strings
find_regex + xrefs_to on debug strings
Yes
No
No
No
No
B — Virtual function via xref strings
Same as A, but function is in a vtable
Yes
No
No
Yes
No
C — Virtual function via LLM_DECOMPILE
Decompile a known predecessor function, identify vfunc call offsets
No
Yes
No
Yes
Yes
D — Regular function via LLM_DECOMPILE
Decompile a known predecessor function, identify direct call targets
No
Yes
No
No
Yes
E — Struct member offset via LLM_DECOMPILE
Decompile a known predecessor function, identify struct field access offsets
No
Yes
No
No
Yes
F — Virtual function via INHERIT_VFUNCS
Inherit vtable slot index from a known base-class vfunc, look up same slot in derived-class vtable
No
No
Yes
No
No
G — ConCommand handler function
Find the handler callback registered via RegisterConCommand by matching command name and help string
No (uses COMMAND_NAME/HELP_STRING)
No
No
No
No
H — Secondary (ordinal) vtable
Locate a class's secondary vtable via mangled symbol (Windows) or offset-to-top (Linux)
No
No
No
No
No
Additionally, struct member offsets can be mixed into any pattern as a secondary target (see "Struct Member Mixin" section below).
Step 1: Read and Analyze the SKILL.md
Read the target .claude/skills/find-XXXX/SKILL.md.
Extract:
Target function names — all functions the skill identifies (may be 1 or many)
Target struct member names — all struct member offsets the skill identifies (e.g. CCheckTransmitInfo_m_nPlayerSlot)
Discovery method for each target:
Does it use find_regex / xrefs_to with debug strings? → xref-string based (Patterns A/B)
Does it search for a ConCommand registration (command name + help string) and extract the handler callback? → ConCommand handler (Pattern G)
Does it load a predecessor YAML, decompile that function, and extract vfunc offsets / struct offsets from code patterns? → LLM_DECOMPILE based (Patterns C/D/E)
Is the target a derived-class override of a known base-class vfunc (same vtable slot, different class)? → INHERIT_VFUNCS based (Pattern F)
Does the SKILL.md locate a secondary vtable using a mangled symbol name (Windows @@6B@_0) or offset-to-top (Linux)? → ordinal vtable (Pattern H)
Function category — func (regular), vfunc (virtual, has vtable slot), structmember, or vtable
VTable class name — if virtual, e.g. CBaseEntity, CBasePlayerPawn, INetworkMessages
Xref strings — debug strings used in find_regex patterns (for xref-string patterns). Check if these differ between Windows and Linux — if so, you need platform-specific FUNC_XREFS_WINDOWS / FUNC_XREFS_LINUX. Use the FULLMATCH: prefix (e.g. "FULLMATCH:Precache") when you need exact-string matching instead of substring matching — this prevents false positives when the target string is short or generic (e.g. "Precache", "userid", "team").
Predecessor function — the function whose decompiled code reveals the target (for LLM_DECOMPILE patterns)
Base vfunc for inheritance — if the target is a derived-class override of a known base-class vfunc, the base vfunc name (for INHERIT_VFUNCS pattern)
Dependencies — which existing YAMLs are needed as inputs (vtable YAMLs, predecessor function YAMLs, base vfunc YAMLs)
Step 2: Plan the Split
If the SKILL.md discovers multiple functions using different methods or from different starting points, split them into separate preprocessor scripts. Each script handles one "discovery unit" — a group of functions findable from the same method and starting point.
Same script: Functions found from the same xref string, or from the same decompiled reference.
Separate scripts: Functions found by xref strings vs. functions found by decompiling one of those xref-found functions.
CRITICAL — LLM_DECOMPILE dependency chains: When LLM_DECOMPILE targets form a chain (FuncA → FuncB → FuncC, where each is the predecessor of the next), they MUST be in separate scripts — one script per link in the chain. A single script CANNOT handle chained LLM_DECOMPILE predecessors because:
The LLM_DECOMPILE fallback resolves the predecessor's address from its output YAML (func_va field)
Within a single script run, FuncB's output YAML doesn't exist yet when FuncC's LLM_DECOMPILE tries to use FuncB as predecessor
The IDA name-lookup fallback also fails because the predecessor wasn't renamed in IDA yet
Rule of thumb: If target X's LLM_DECOMPILE references target Y as predecessor, and Y is also discovered by LLM_DECOMPILE (not xref strings), then X and Y MUST be in different scripts with a configs/.yaml dependency chain.
Example split (what we did for CBaseEntity_TakeDamageOld):
Script 1: find-CBaseEntity_TakeDamageOld.py — finds TakeDamageOld via xref string (Pattern A)
Script 2: find-CBaseEntity_OnTakeDamage.py — finds OnTakeDamage by decompiling TakeDamageOld (Pattern C)
Script 3: find-CBaseEntity_OnTakeDamage_Alive-AND-Dying-AND-Dead.py — finds 3 vfuncs by decompiling OnTakeDamage (Pattern C)
Platform-Specific Xref Strings (Patterns A & B variant)
When xref strings differ between Windows and Linux (e.g. Windows has full ClassName::Method assertion strings while Linux has only ./filename.cpp:linenum), split into two variables:
Then in preprocess_skill, use a ternary to select the right one:
func_xrefs=FUNC_XREFS_WINDOWS if platform == "windows"else FUNC_XREFS_LINUX,
This applies to both Pattern A and Pattern B — the only change is replacing the single FUNC_XREFS with the platform-specific pair.
Pattern C — Virtual function via LLM_DECOMPILE
Use when: function IS virtual (has vtable slot), discovered by decompiling a known predecessor function and reading vfunc call offsets from the decompiled code.
IMPORTANT — func_va in output YAMLs: If this function will be used as a predecessor by a downstream LLM_DECOMPILE script (i.e., another script decompiles this function to find further targets), you MUST include func_va, func_rva, and func_size in GENERATE_YAML_DESIRED_FIELDS. The downstream script resolves the predecessor's address by reading func_va from the output YAML. Without it, the LLM_DECOMPILE fallback fails with "failed to resolve llm_decompile target function address". When in doubt, always include func_va — it never hurts.
#!/usr/bin/env python3"""Preprocess script for find-{SKILL_NAME} skill."""from ida_analyze_util import preprocess_common_skill
TARGET_FUNCTION_NAMES = [
"{FUNC_NAME_1}",
# "{FUNC_NAME_2}", # Add more if the skill finds multiple functions from the same reference
]
LLM_DECOMPILE = [
# (symbol_name, path_to_prompt, path_to_reference)# ONE entry per target function. All entries sharing the same reference# YAML will be resolved from the same decompiled predecessor code.
(
"{FUNC_NAME_1}",
"prompt/call_llm_decompile.md",
"references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
),
(
"{FUNC_NAME_2}",
"prompt/call_llm_decompile.md",
"references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
),
# ... one entry per target function, all pointing to the same reference
]
FUNC_VTABLE_RELATIONS = [
# (func_name, vtable_class)
("{FUNC_NAME_1}", "{VTABLE_CLASS}"),
("{FUNC_NAME_2}", "{VTABLE_CLASS}"),
# ... one entry per target function
]
GENERATE_YAML_DESIRED_FIELDS = [
# (symbol_name, generate_yaml_fields)# Include func_va/func_rva/func_size if this function is a predecessor for downstream LLM_DECOMPILE
(
"{FUNC_NAME_1}",
[
"func_name",
"func_va",
"func_rva",
"func_size",
"vfunc_sig",
"vfunc_offset",
"vfunc_index",
"vtable_name",
],
),
(
"{FUNC_NAME_2}",
[
"func_name",
"func_va",
"func_rva",
"func_size",
"vfunc_sig",
"vfunc_offset",
"vfunc_index",
"vtable_name",
],
),
# ... one entry per target function
]
asyncdefpreprocess_skill(
session, skill_name, expected_outputs, old_yaml_map,
new_binary_dir, platform, image_base, llm_config=None, debug=False,
):
"""Reuse previous gamever func_sig to locate target function(s) and write YAML."""returnawait preprocess_common_skill(
session=session,
expected_outputs=expected_outputs,
old_yaml_map=old_yaml_map,
new_binary_dir=new_binary_dir,
platform=platform,
image_base=image_base,
func_names=TARGET_FUNCTION_NAMES,
func_vtable_relations=FUNC_VTABLE_RELATIONS,
llm_decompile_specs=LLM_DECOMPILE,
llm_config=llm_config,
generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
debug=debug,
)
Pattern D — Regular function via LLM_DECOMPILE
Use when: function is NOT virtual, discovered by decompiling a known predecessor function and identifying direct call targets (not vtable-based calls) from the decompiled code.
Pattern E — Struct member offset via LLM_DECOMPILE
Use when: target is a struct member offset (not a function), discovered by decompiling a known predecessor function and identifying struct field access patterns (e.g. *(int *)(ptr + 0x240)).
Uses TARGET_STRUCT_MEMBER_NAMES instead of TARGET_FUNCTION_NAMES
Passes struct_member_names= instead of func_names= to preprocess_common_skill
YAML fields are struct-specific: struct_name, member_name, offset, size, offset_sig, offset_sig_disp
No FUNC_VTABLE_RELATIONS
configs/.yaml symbol category is structmember (not func or vfunc)
Pattern F — Virtual function via INHERIT_VFUNCS
Use when: the target is a derived-class override of a known base-class virtual function. The base vfunc has already been found (by another script), and this script inherits its vtable slot index to look up the same slot in the derived class's vtable.
This is the simplest pattern — no xref strings, no LLM decompilation needed. Just a vtable slot lookup.
target_func_name — name for the derived-class function (e.g. "CBaseEntity_Precache")
inherit_vtable_class — class whose vtable to look up (e.g. "CBaseEntity")
base_vfunc_name — YAML artifact stem of the base-class vfunc that defines the slot index (e.g. "CEntityInstance_Precache"). Can be cross-module: "../engine/INetworkMessages_FindNetworkGroup"
generate_func_sig — (optional, default True) whether to generate a func_sig if no old YAML exists
Key differences from other patterns:
No TARGET_FUNCTION_NAMES, FUNC_XREFS, LLM_DECOMPILE, or FUNC_VTABLE_RELATIONS
Uses inherit_vfuncs= parameter instead of func_names=
No llm_config parameter in preprocess_skill
configs/.yaml expected_input must include both the base vfunc YAML and the derived class vtable YAML
configs/.yaml symbol category is vfunc
Pattern G — ConCommand handler function
Use when: the SKILL.md searches for a ConCommand registration (e.g. find_regex pattern="bot_kill.*all" → xrefs_to → handler callback). The target is the handler function registered via RegisterConCommand, identified by matching the command name string and/or help string.
This pattern uses a dedicated helper (_registerconcommand.py) instead of preprocess_common_skill. It scans for the exact command name and help string in the binary's string table, finds xrefs to those strings, locates nearby RegisterConCommand calls, and recovers the handler function pointer from the call arguments.
Imports preprocess_registerconcommand_skill from ida_preprocessor_scripts._registerconcommand instead of preprocess_common_skill from ida_analyze_util
Uses COMMAND_NAME and HELP_STRING variables instead of FUNC_XREFS
Uses SEARCH_WINDOW_BEFORE_CALL and SEARCH_WINDOW_AFTER_XREF (typically 96 bytes each) to control the scan window around xrefs
The preprocess_skill function ignores old_yaml_map (_ = skill_name, old_yaml_map)
Calls preprocess_registerconcommand_skill() with command_name=, help_string=, rename_to= instead of func_xrefs=
configs/.yaml category is func, no expected_input needed
The handler function is always a regular function (not virtual), so no FUNC_VTABLE_RELATIONS
When to recognize this pattern in a SKILL.md:
The SKILL.md searches for a command string (e.g. find_regex pattern="bot_kill.*all")
It traces xrefs to find a ConCommand registration call
The target is the handler callback address extracted from the registration
Struct Member Mixin (for any pattern)
Struct member offsets can also be mixed into a function-finding script when they are discovered from the same function via signature matching (not LLM_DECOMPILE). Add TARGET_STRUCT_MEMBER_NAMES alongside TARGET_FUNCTION_NAMES and pass struct_member_names= to preprocess_common_skill:
FUNC_VTABLE_RELATIONS is required for ANY target whose GENERATE_YAML_DESIRED_FIELDS includes vtable_name or vfunc_sig — not just Pattern B and C. Without it, the LLM_DECOMPILE slot-only fallback fails with "slot-only fallback missing vtable_name" and the entire skill fails.
This applies even when:
The target is a vfunc call-site offset (e.g. call [rax+128h]) rather than an actual function body in a vtable
No vtable YAML exists for that class in configs/.yaml (no expected_input for the vtable needed)
The script also finds non-vfunc targets (global variables, struct offsets) alongside the vfunc target
The vtable_name from FUNC_VTABLE_RELATIONS is used as metadata written to the output YAML — it does NOT require an actual vtable lookup. For example, ("IGameTypes_CreateWorkshopMapGroup", "IGameTypes") provides the vtable class name IGameTypes even though no IGameTypes_vtable.{platform}.yaml exists.
Rule of thumb: If any field in GENERATE_YAML_DESIRED_FIELDS starts with vfunc_ or equals vtable_name, the target MUST have an entry in FUNC_VTABLE_RELATIONS.
Each preprocessor script needs a corresponding skill entry under the appropriate module's skills: list.
Find the module section (e.g. server, engine, networksystem) and add/update entries.
Template:
-name:find-{SKILL_NAME}expected_output:- {FUNC_NAME_1}.{platform}.yaml# - {FUNC_NAME_2}.{platform}.yaml # One per target function# expected_input only if the skill depends on other YAMLs:expected_input:- {PREDECESSOR_FUNC}.{platform}.yaml# For Pattern C: the reference function- {VTABLE_CLASS}_vtable.{platform}.yaml# For Patterns B & C: the vtable
Rules:
expected_output: One .{platform}.yaml per target function in the script
expected_input: Include predecessor function YAML (Patterns C & D) and/or vtable YAML (Patterns B & C & F)
Pattern A with no vtable: typically NO expected_input (Pattern D still needs predecessor in expected_input)
Pattern F: needs both the derived class vtable YAML and the base vfunc YAML in expected_input
If splitting a combined skill, each new entry should have its own expected_input referencing the predecessor's output
Dependency chain example (3-script split):
# Pattern A: found via xref string, no dependencies-name:find-FuncAexpected_output:-FuncA.{platform}.yaml# Pattern C: found by decompiling FuncA, needs FuncA + vtable-name:find-FuncBexpected_output:-FuncB.{platform}.yamlexpected_input:-FuncA.{platform}.yaml-SomeClass_vtable.{platform}.yaml# Pattern C: found by decompiling FuncB, needs FuncB + vtable-name:find-FuncC1-AND-FuncC2-AND-FuncC3expected_output:-FuncC1.{platform}.yaml-FuncC2.{platform}.yaml-FuncC3.{platform}.yamlexpected_input:-FuncB.{platform}.yaml-SomeClass_vtable.{platform}.yaml
4b. Symbols Section
For each NEW target function, add a symbol entry under the same module's symbols: list (if not already present).
# Regular function (Pattern A)-name: {FUNC_NAME}
category:funcalias:- {ClassName}::{MethodName}# e.g. CBaseEntity::TakeDamageOld# Virtual function (Patterns B & C)-name: {FUNC_NAME}
category:vfuncalias:- {ClassName}::{MethodName}# e.g. CBasePlayerPawn::OnTakeDamage# Struct member offset (Pattern E)-name: {STRUCT_MEMBER_NAME}
category:structmemberstruct: {STRUCT_NAME}
member: {MEMBER_NAME}
alias:- {StructName}::{MemberName}# e.g. CCheckTransmitInfo::m_nPlayerSlot
Check existing symbols before adding — do NOT create duplicates.
Step 5: Handle Reference YAMLs (Patterns C & D)
Pattern C and D scripts reference a predecessor function's YAML at:
ida_preprocessor_scripts/references/{module}/{PREDECESSOR_FUNC}.{platform}.yaml
These reference files contain the decompiled code of the predecessor function (both disasm_code and procedure fields) so the LLM can identify call patterns.
If NOT present, generate them using generate_reference_yaml.py:
# Windows — always pass -platform windows explicitly
uv run generate_reference_yaml.py -func_name {PREDECESSOR_FUNC} -auto_start_mcp -binary "bin/{gamever}/{module}/{binary_name}.dll" -platform windows -debug
# Linux — always pass -platform linux explicitly
uv run generate_reference_yaml.py -func_name {PREDECESSOR_FUNC} -auto_start_mcp -binary "bin/{gamever}/{module}/lib{module}.so" -platform linux -debug
For example, for the server module:
uv run generate_reference_yaml.py -func_name CCSGameRules_TerminateRound -auto_start_mcp -binary "bin/{gamever}/server/server.dll" -platform windows -debug
uv run generate_reference_yaml.py -func_name CCSGameRules_TerminateRound -auto_start_mcp -binary "bin/{gamever}/server/libserver.so" -platform linux -debug
where {gamever} can be obtain from .env -> CS2VIBE_GAMEVER, or 14141c if you can't read .env.
YOU MUST: rename known symbols / add necessary comments in the generated reference YAMLs the so LLM can find desired symbols by comparing reference ones with raw procedure/disassembly read from new binaries.
For example, if we want the LLM to find CEntityInstance_AcceptInput in the owner function:
Prerequisites: The predecessor function must already be named in the IDA database for the target binary. If it is not named yet, ask the user to either:
Connect IDA Pro MCP and rename the function first, or
Manually rename it in IDA before running the script
IMPORTANT — generate_reference_yaml.py address resolution: The script resolves the predecessor function's address by reading func_va from the existing output YAML at bin/{gamever}/{module}/{PREDECESSOR_FUNC}.{platform}.yaml. If the predecessor is one of the target functions being converted (e.g., splitting a combined skill where Script 1 finds FuncA and Script 2 decompiles FuncA), you MUST generate the reference YAMLs BEFORE deleting existing output YAMLs in Step 7. Otherwise, the address data needed by generate_reference_yaml.py will be destroyed and you'll need to recreate temporary YAMLs or ask the user for the function address.
IMPORTANT — When the predecessor is a NEW function (no existing output YAMLs): If the predecessor function is brand new (discovered by another new script you're creating in the same conversion), its output YAMLs don't exist yet and generate_reference_yaml.py cannot resolve its address. You must use a multi-phase workflow:
Phase 1: Create ALL scripts (vtable, xref_string, LLM_DECOMPILE) and update configs/.yaml
Phase 2: Run uv run ida_analyze_bin.py -debug — the vtable and xref_string scripts will succeed and populate the NEW predecessor's output YAMLs. The LLM_DECOMPILE script's target may be skipped if old output YAMLs with valid func_sig still exist.
Phase 3: Now that the predecessor has output YAMLs, run generate_reference_yaml.py to create reference YAMLs, then annotate them.
Phase 4: Delete the old target output YAMLs (so the LLM_DECOMPILE path is actually exercised)
Phase 5: Run uv run ida_analyze_bin.py -debug again — this time the LLM_DECOMPILE path runs and the full pipeline is validated.
IMPORTANT — Run generate_reference_yaml.py sequentially, NOT in parallel. All invocations share the same IDA MCP connection. Running them in parallel will cause connection conflicts and failures. Run one command at a time, waiting for each to complete before starting the next.
Run the command once per platform (windows/linux) that needs a reference YAML. The -module is inferred from the -binary path automatically.
IMPORTANT — Always pass -platform explicitly. While -platform can theoretically be inferred from the binary extension (.dll → windows, .so → linux), auto-inference is unreliable and may produce the wrong platform's reference YAML. Always pass -platform windows or -platform linux explicitly.
Step 6: Delete the SKILL.md
After the preprocessor script is created and configs/.yaml is updated:
Delete the SKILL.md file: .claude/skills/find-{SKILL_NAME}/SKILL.md
Delete the now-empty directory: .claude/skills/find-{SKILL_NAME}/
If a combined SKILL.md was split into multiple preprocessor scripts, delete the single original SKILL.md.
Step 7: Delete Existing Output YAMLs
IMPORTANT: This step MUST happen AFTER Step 5 (reference YAML generation). The generate_reference_yaml.py script reads func_va from these output YAMLs to locate functions in IDA. Deleting them first will break reference generation.
After the preprocessor script is created, the old SKILL.md is deleted, and any needed reference YAMLs are generated, remove all previously generated output YAMLs so the user can validate the new preprocessor script from scratch by running uv run ida_analyze_bin.py.
For each target function, delete all matching YAMLs across all game versions:
For example, if the skill targets CBasePlayerController_HandleCommand_JoinTeam in the server module:
find bin -name "CBasePlayerController_HandleCommand_JoinTeam.*.yaml" -delete
If the skill was split into multiple scripts with multiple target functions, delete YAMLs for ALL target functions.
Step 8: Remove Entry from docs/claude_skills_stats.yaml
After the conversion is complete and validated, delete the converted skill's entry from docs/claude_skills_stats.yaml. This file tracks skills that still use the old SKILL.md format — once converted to a preprocessor script, the entry is no longer relevant.
Remove the entire YAML block for each converted symbol, e.g.:
# Delete this entire block:-symbol_name:CBasePlayerController_HandleCommand_JoinTeamskill_name:find-CBasePlayerController_HandleCommand_JoinTeamclassicy:with_xref_stringsowner_func_name:CBasePlayerController_HandleCommand_JoinTeamowner_module:server
If the original SKILL.md covered multiple symbols, delete ALL corresponding entries from the stats file.
Step 9: Run Tests
After all conversion steps are complete, run the full preprocessor test to validate the new script works.
Because the output is very long, redirect it to a temp file and then read just the summary:
uv run ida_analyze_bin.py -debug > /tmp/ida_test_output.txt 2>&1; tail -10 /tmp/ida_test_output.txt
Check the Summary at the end of the output:
Failed: 0 means the conversion is correct
If any failures, search the full output for the failing skill name to investigate:
grep -A 5 "Failed\|Error" /tmp/ida_test_output.txt
This step is mandatory — do not report completion without running and passing this validation.
Step 10: Commit Changes
After validation passes, commit all conversion-related changes to git.
IMPORTANT — Never commit directly to the main branch. If the current branch is main, create and switch to a dev branch first:
# Check current branch
git branch --show-current
# If on main, switch to dev (create it if it doesn't exist)
git checkout dev 2>/dev/null || git checkout -b dev
Then commit:
git add <preprocessor_script> <deleted_skill_md> <configs/<GAMEVER>.yaml if changed> docs/claude_skills_stats.yaml
git commit -m "Convert find-{SKILL_NAME} SKILL.md to preprocessor script"
Include all files changed during the conversion:
The new/updated preprocessor script
The deleted SKILL.md
Any configs/.yaml changes
The updated docs/claude_skills_stats.yaml
Do NOT include unrelated changes (e.g. .claude/settings.json permission changes).
Checklist
Before finishing, verify:
Preprocessor script file name matches the name field in configs/.yaml skill entry
TARGET_FUNCTION_NAMES lists all functions the script should find
FUNC_XREFS xref strings match the debug strings from the original SKILL.md (Pattern A/B)
LLM_DECOMPILE reference path points to the correct predecessor function YAML (Patterns C/D)
FUNC_VTABLE_RELATIONS lists correct vtable class for EVERY target that has vtable_name or vfunc_sig in its GENERATE_YAML_DESIRED_FIELDS — required even for vfunc call-site offsets and even if no vtable YAML exists (Patterns B/C, and any LLM_DECOMPILE target with vfunc fields)
GENERATE_YAML_DESIRED_FIELDS uses correct field set for the pattern (Pattern C/D: include func_va if function is a predecessor for downstream LLM_DECOMPILE)
LLM_DECOMPILE dependency chains are split into separate scripts (one per chain link), NOT combined in a single script
preprocess_skill signature includes llm_config=None if and only if LLM_DECOMPILE is used (NOT for Pattern F)
3 target functions, 3 LLM_DECOMPILE entries (all referencing the same YAML), 3 FUNC_VTABLE_RELATIONS entries, 3 GENERATE_YAML_DESIRED_FIELDS entries
Each LLM_DECOMPILE entry references references/server/CBaseEntity_OnTakeDamage.{platform}.yaml
Example: Split xref-string + LLM_DECOMPILE regular function (Patterns A + D)
Before:.claude/skills/find-CCSGameRules_TerminateRound-AND-CEntityInstance_AcceptInput/SKILL.md — found TerminateRound via find_regex pattern="TerminateRound", then decompiled it to find AcceptInput called with "CTsWin"/"TerroristsWin" string arguments.
Example: Split xref-string vfunc + LLM_DECOMPILE struct offset (Patterns B + E with platform-specific xrefs)
Before:.claude/skills/find-CSource2GameEntities_CheckTransmit-AND-CCheckTransmitInfo/SKILL.md — found CheckTransmit via find_regex pattern="CSource2GameEntities::CheckTransmit", then examined the decompiled code to find CCheckTransmitInfo::m_nPlayerSlot at offset 0x240.
Split into two scripts:
ida_preprocessor_scripts/find-CSource2GameEntities_CheckTransmit.py (Pattern B with platform-specific xrefs):
FUNC_XREFS_WINDOWS containing "CSource2GameEntities::CheckTransmit" (full assertion string on Windows)
FUNC_XREFS_LINUX containing "./gameinterface.cpp:30" (shorter path on Linux — Windows string not present in Linux binary)
GENERATE_YAML_DESIRED_FIELDS with struct_name, member_name, offset, size, offset_sig, offset_sig_disp
Reference YAMLs annotated with ; 0x240 = CCheckTransmitInfo::m_nPlayerSlot in disasm and // 576 = 0x240 = CCheckTransmitInfo::m_nPlayerSlot in procedure
Before:.claude/skills/find-CBaseEntity_Precache/SKILL.md — found CBaseEntity_Precache via a wrapper function referencing "bloodspray". CBaseEntity_Precache is actually a derived-class override of CEntityInstance::Precache.
Split into two scripts with swapped relationship:
ida_preprocessor_scripts/find-CEntityInstance_Precache.py (Pattern B with FULLMATCH):
FUNC_XREFS containing "FULLMATCH:Precache" (exact match to avoid false positives on short string)
Key insight: The SKILL.md originally found CBaseEntity_Precache directly. When converting, we recognized that Precache is a base-class virtual method on CEntityInstance, and CBaseEntity::Precache is just an override at the same vtable slot. So we split into: find the base method via xref strings → inherit to find the derived override.
Example: Split vtable + xref-string vfunc + LLM_DECOMPILE regular function (vtable + Patterns B + D)
Before:.claude/skills/find-LegacyGameEventListener/SKILL.md — found CSource2GameClients_StartHLTVServer via find_regex pattern="CSource2GameClients::StartHLTVServer: game event %s not found", then decompiled it to find LegacyGameEventListener called with a2 parameter.
Key insight — multi-phase workflow: The predecessor CSource2GameClients_StartHLTVServer was a brand-new function with no existing output YAMLs. So generate_reference_yaml.py couldn't run until the xref_string script populated its output. The workflow was:
Create all 3 scripts + config entries
Run ida_analyze_bin.py -debug → vtable + xref_string scripts succeed, LegacyGameEventListener skipped (old output YAMLs with valid func_sig still exist)
Run generate_reference_yaml.py for both platforms using the newly created StartHLTVServer output YAMLs
Annotate reference YAMLs (rename sub_180B1AC80 → LegacyGameEventListener in both disasm and procedure)
Delete old LegacyGameEventListener output YAMLs
Run ida_analyze_bin.py -debug again → LLM_DECOMPILE path runs and succeeds
Example: Split ConCommand handler + LLM_DECOMPILE virtual function (Patterns G + C, multi-phase)
Before:.claude/skills/find-CBasePlayerPawn_CommitSuicide/SKILL.md — searched for "bot_kill" command string via find_regex pattern="bot_kill.*all", traced xrefs to find the ConCommand handler, then decompiled the handler to find the CommitSuicide vfunc call (at offset 0xC80 in CBasePlayerPawn vtable).
Key insight — multi-phase workflow:BotKill_CommandHandler was a brand-new function with no existing output YAMLs. The workflow was:
Create both scripts + config entries, delete old SKILL.md
Run ida_analyze_bin.py -debug → Pattern G script succeeds and creates BotKill_CommandHandler YAMLs; CommitSuicide skipped (old output YAMLs with valid func_sig still exist)
Run generate_reference_yaml.py for both platforms using the newly created BotKill_CommandHandler output YAMLs
Annotate reference YAMLs (add ; 0xC80 = CBasePlayerPawn_CommitSuicide comments for the vfunc call in the kill loop)
Delete old CBasePlayerPawn_CommitSuicide output YAMLs
Run ida_analyze_bin.py -debug again → LLM_DECOMPILE path runs and succeeds
Example: Split xref-string function + LLM_DECOMPILE global variable & vfunc offset (Patterns A + LLM_DECOMPILE with gv + vfunc, multi-phase)
Before:.claude/skills/find-g_pGameTypes-AND-IGameTypes_CreateWorkshopMapGroup/SKILL.md — searched for "mapgroup workshop" string, found CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup, then decompiled it to find g_pGameTypes (global variable at qword_XXXX) and IGameTypes_CreateWorkshopMapGroup (vfunc call at offset 0x128/0x130).
LLM_DECOMPILE referencing references/server/CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup.{platform}.yaml (both targets share the same reference)
CRITICAL:FUNC_VTABLE_RELATIONS: ("IGameTypes_CreateWorkshopMapGroup", "IGameTypes") — required because GENERATE_YAML_DESIRED_FIELDS includes vtable_name and vfunc_sig. Without this, the slot-only fallback fails with "slot-only fallback missing vtable_name". Note: no IGameTypes_vtable YAML exists — the vtable name is purely metadata.
GENERATE_YAML_DESIRED_FIELDS for IGameTypes_CreateWorkshopMapGroup: func_name, vtable_name, vfunc_offset, vfunc_index, vfunc_sig
Reference YAMLs annotated: qword_XXXX renamed to g_pGameTypes in both disasm and procedure; call [rax+128h] annotated with ; 0x128 = IGameTypes_CreateWorkshopMapGroup; 296LL annotated with // 296LL = 0x128 = IGameTypes_CreateWorkshopMapGroup
Key insight — FUNC_VTABLE_RELATIONS for vfunc offsets: Even though IGameTypes_CreateWorkshopMapGroup is a vfunc call-site offset (not a function in a vtable we own), FUNC_VTABLE_RELATIONS is still required because the GENERATE_YAML_DESIRED_FIELDS include vtable_name and vfunc_sig. The system uses the vtable class name from FUNC_VTABLE_RELATIONS as metadata — it does NOT attempt to look up an IGameTypes_vtable YAML.