Create Preprocessor Scripts from Scratch

SkillDev tools

Create a new find-XXXX preprocessor Python script from scratch (no existing SKILL.md), add configs/<GAMEVER>.yaml skill and symbol entries. Covers xref-string-based and LLM_DECOMPILE-based discovery patterns. Use when a GitHub issue or user instruction specifies a new function to find.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Create Preprocessor Scripts from Scratch skill

What this skill tells your AI

The instructions your AI receives, as published by hlnd2t/cs2_vibesignatures in .claude/skills/create-preprocessor-scripts/SKILL.md and read by ahel’s review.

Create an ida_preprocessor_scripts/find-XXXX.py preprocessor script and add the corresponding configs/<GAMEVER>.yaml entries for a newly requested function, vtable, or struct member offset.

Resolve GAMEVER from the user's explicit request or CS2VIBE_GAMEVER, set the edit target to configs/$GAMEVER.yaml, and stop if that exact file does not exist. Never edit another version as a fallback.

When to Use

  • A GitHub issue or user instruction requests adding support for finding a new function/symbol
  • No existing .claude/skills/find-XXXX/SKILL.md needs conversion (for that, use convert-finder-skill-to-preprocessor-scripts)

Inputs

The user or issue will provide some or all of:

FieldDescriptionExample
Function name(s)Target symbol(s) to findCPlayer_MovementServices_PlayWaterStepSound
ModuleWhich DLL/SO the function lives inserver, engine, networksystem, client
CategorySymbol typefunc, vfunc, structmember, patch, vtable
xref_stringsDebug strings for xref-based discovery"CT_Water.StepLeft"
xref_gvsGlobal variable VA (e.g. vtable address) to find functions that reference itvtable VA from SomeClass_vtable.{platform}.yaml
xref_funcsKnown callee function name to find its callers"CPlayerCommandQueue_ctor"
Predecessor functionFunction to decompile for LLM_DECOMPILE patternsCBaseEntity_TakeDamageOld
VTable classClass owning the vtable (for vfuncs)CBasePlayerPawn
Desired YAML fieldsWhich fields the output YAML needsfunc_name, func_sig, func_va, func_rva, func_size
DependenciesInput YAMLs this skill depends onCCSPlayer_MovementServices_vtable.{platform}.yaml
AliasesAlternative names for the symbolCPlayer_MovementServices::PlayWaterStepSound

Overview

Twelve preprocessor patterns exist. The discovery method and target type determine which to use:

PatternDiscovery MethodHas FUNC_XREFSHas LLM_DECOMPILEHas INHERIT_VFUNCSHas FUNC_VTABLE_RELATIONSpreprocess_skill has llm_config
A -- Regular function via xref stringsfind_regex + xrefs_to on debug stringsYesNoNoNoNo
B -- Virtual function via xref stringsSame as A, but function is in a vtableYesNoNoYesNo
C -- Virtual function via LLM_DECOMPILEDecompile a known predecessor function, identify vfunc call offsetsNoYesNoYesYes
D -- Regular function via LLM_DECOMPILEDecompile a known predecessor function, identify direct call targetsNoYesNoNoYes
E -- Struct member offset via LLM_DECOMPILEDecompile a known predecessor function, identify struct field access offsetsNoYesNoNoYes
F -- Virtual function via INHERIT_VFUNCSInherit vtable slot index from a known base-class vfunc, look up same slot in derived-class vtable (standard); or slot-only mode for abstract/interface vfuncs where only offset/index is neededNoNoYesNoNo
G -- ConCommand handler functionFind the handler callback registered via RegisterConCommand by matching command name and help stringNo (uses COMMAND_NAME/HELP_STRING)NoNoNoNo
H -- Secondary (ordinal) vtableLocate a class's secondary vtable via mangled symbol (Windows) or offset-to-top (Linux)NoNoNoNoNo
I -- Interface vfunc offset via thunk instruction walkWalk a known concrete-class thunk via py_eval + idaapi.decode_insn, extract jmp [reg+disp] displacement as vfunc_offsetNoNoNoNoNo
J -- IGameSystem vfunc via dispatch scanScan IGameSystem_DispatchCall(idx, callback, ...) call sites in a known predecessor; map targets by scan/index order using _igamesystem_dispatch_commonNoNoNoNoNo
K -- IGameSystem vfunc via slot dispatch scanWalk an IGameSystem_Loop*AllSystems dispatcher function body; extract [rax+offset] vtable call displacements via _igamesystem_slot_dispatch_common; output is slot-only (no func_sig)NoNoNoNoNo
L -- Interface vfunc slot via indirect vcall scanScan a known thunk/caller for its unique register-indirect vtable call (jmp/call qword ptr [reg+disp]) via _indirect_vcall_target_common; read the displacement as vfunc_offset; output is slot-only (no func_sig). Reusable form of Pattern INoNoNoNoNo

Additionally, struct member offsets can be mixed into any pattern as a secondary target (see "Struct Member Mixin" section below).


Step 1: Determine the Pattern

From the user's input, determine:

  1. Is the target a function, vfunc, or struct member offset?

    • Has xref_strings + category func -> Pattern A
    • Has xref_strings + category vfunc -> Pattern B
    • Has xref_gvs (vtable VA from a vtable YAML) + category func -> Pattern A with dynamic FUNC_XREFS (read vtable VA at runtime; see "Dynamic FUNC_XREFS via xref_gvs" note)
    • Has xref_gvs (vtable VA from a vtable YAML) + category vfunc -> Pattern B with dynamic FUNC_XREFS
    • Has xref_funcs (known callee function name) + category func -> Pattern A (static FUNC_XREFS; see "xref_funcs: finding callers of a known function" note)
    • Has xref_funcs (known callee function name) + category vfunc -> Pattern B (static FUNC_XREFS)
    • Has predecessor function + category vfunc -> Pattern C (vfunc_sig is ALWAYS required in GENERATE_YAML_DESIRED_FIELDS -- see "vfunc_sig is MANDATORY for Pattern C" note below)
    • Has predecessor function + category func -> Pattern D
    • Has predecessor function + category structmember -> Pattern E
    • Has base vfunc name + category vfunc (derived-class override of known base vfunc) -> Pattern F
      • If the target is an abstract/interface vfunc (no real function body, only vfunc_offset/vfunc_index needed) -> use Pattern F slot-only: generate_func_sig=False, desired fields = {func_name, vtable_name, vfunc_offset, vfunc_index}, NO vtable YAML required for the interface class
    • Has COMMAND_NAME + HELP_STRING (ConCommand handler callback) -> Pattern G
    • Has mangled vtable symbol / offset-to-top + category vtable (secondary vtable for a class) -> Pattern H
    • Target is an interface vfunc offset with no feasible func_sig/vfunc_sig, and the offset can be read from a concrete-class thunk's jmp [reg+disp] instruction -> Pattern L (preferred, reusable helper) or Pattern I (bespoke py_eval walk; use only if you need the old-gamever reuse fast path or a custom operand filter)
    • Target is an IGameSystem vfunc visible as the callback argument to IGameSystem_DispatchCall(...) in a known predecessor's decompile -> Pattern J
    • Target is an IGameSystem abstract vfunc (slot-only output: func_name, vtable_name, vfunc_offset, vfunc_index; no func_sig) dispatched by a known IGameSystem_Loop*AllSystems function that iterates all game systems via vtable; the dispatcher's output YAML (func_va) is already available -> Pattern K
    • Target is an abstract/interface vfunc dispatched by a thin thunk/caller whose body has exactly one register-indirect vtable call (jmp/call qword ptr [reg+disp]), and no func_sig/vfunc_sig is feasible (a jmp [reg+disp8] for offset <= 0x7F is only 3 bytes and cannot be signed uniquely) -> Pattern L (slot-only output: func_name, vtable_name, vfunc_offset, vfunc_index; a downstream Pattern F standard override consumes the vfunc_index)
    • Target X was found by a single Pattern A/B finder, but a helper that used to be inlined into X de-inlined on some build (the anchor string/call left X, so X.{platform}.yaml stopped being produced and the fail-fast run aborts the module) -> Pattern M (split into a helper + X-noinline + X-inlined fallback chain)
  2. Do xref strings differ between Windows and Linux? If yes, use platform-specific FUNC_XREFS_WINDOWS / FUNC_XREFS_LINUX variant.

  3. Are there multiple functions? If they share the same discovery method and starting point, put them in the same script with -AND- in the name. Otherwise, split into separate scripts.

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), and within a single script run the predecessor's output YAML doesn't exist yet. The IDA name-lookup fallback also fails because the predecessor wasn't renamed yet.


Step 2: Create the Preprocessor Script

Script location: ida_preprocessor_scripts/find-{skill_name}.py

The filename MUST match the name field in configs/<GAMEVER>.yaml skill entry.

The legacy new_binary_dir parameter name in preprocessor ABIs now receives the active artifact module directory. Per-symbol YAML reads/writes must stay under the explicit artifact root; only binary/IDA operations use bin/.

Read the reference for your chosen pattern:

Cross-Cutting Notes

FULLMATCH: Prefix for Xref Strings (Patterns A & B)

When the xref string is short or generic (e.g. "Precache", "userid", "team"), use the FULLMATCH: prefix to require exact string matching instead of substring matching. Without it, "Precache" would match "PrecacheModel", "PrecacheSound", etc.

FUNC_XREFS = [
    {
        "func_name": "CEntityInstance_Precache",
        "xref_strings": [
            "FULLMATCH:Precache",  # Only matches the exact string "Precache"
        ],
        "xref_gvs": [], "xref_signatures": [], "xref_funcs": [],
        "exclude_funcs": [], "exclude_strings": [], "exclude_gvs": [], "exclude_signatures": [],
    },
]
Dynamic FUNC_XREFS via xref_gvs (vtable VA)

When the target function is the constructor (or any other function that references a class's vtable), use xref_gvs with the vtable's virtual address. Because the vtable VA is only known after IDA analysis, it cannot be hardcoded -- it must be read from the vtable's output YAML at runtime.

This requires a custom preprocess_skill that:

  1. Reads vtable_va from {VtableClass}_vtable.{platform}.yaml in new_binary_dir
  2. Builds func_xrefs dynamically with the VA in xref_gvs
  3. Passes the dynamic list to preprocess_common_skill
import os
try:
    import yaml
except ImportError:
    yaml = None

def _read_vtable_va(yaml_path):
    try:
        with open(yaml_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
        if isinstance(data, dict):
            va = data.get("vtable_va")
            if va:
                return str(va)
    except Exception:
        pass
    return None

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, debug=False,
):
    vtable_yaml_path = os.path.join(new_binary_dir, f"SomeClass_vtable.{platform}.yaml")
    vtable_va = _read_vtable_va(vtable_yaml_path)
    if not vtable_va:
        if debug:
            print("    Preprocess: SomeClass_vtable vtable_va not found, cannot resolve xref_gvs")
        return False

    func_xrefs = [
        {
            "func_name": "SomeClass_ctor",
            "xref_strings": [],
            "xref_gvs": [str(vtable_va)],
            "xref_signatures": [],
            "xref_funcs": [],
            "exclude_funcs": [],
            "exclude_strings": [],
            "exclude_gvs": [],
            "exclude_signatures": [],
        },
    ]
    return await 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_xrefs=func_xrefs,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

configs/.yaml expected_input: must include the vtable YAML so it is guaranteed to be resolved before this script runs.

Multiple xrefs / exclude_signatures: If more than one function references the vtable (e.g. constructor + destructor), the intersection yields >1 result and the skill fails. Use exclude_signatures to exclude the unwanted function(s). If the ambiguity is platform-specific, make the exclusion conditional:

exclude_signatures = ["66 83 ?? FF"] if platform == "linux" else []

To find the right bytes to exclude: look up the two candidate addresses in IDA, read the first ~4 bytes of the function to exclude, and use those as the exclude_signatures pattern with ?? wildcards where needed.

xref_funcs: Finding Callers of a Known Function (Patterns A & B)

When the target function is discoverable as a caller of another already-known function, use xref_funcs with the callee's name. Unlike xref_gvs, the function name is available at script-write time, so FUNC_XREFS can be a static module-level constant -- no dynamic building required.

FUNC_XREFS = [
    {
        "func_name": "TargetFunc",
        "xref_strings": [],
        "xref_gvs": [],
        "xref_signatures": [],
        "xref_funcs": ["KnownCalleeFunc"],   # callee that the target calls
        "exclude_funcs": [],
        "exclude_strings": [],
        "exclude_gvs": [],
        "exclude_signatures": [],
    },
]

configs/.yaml expected_input: include the callee's output YAML to guarantee it is renamed in IDA before this script runs (the name lookup requires the rename to have happened):

        expected_input:
          - KnownCalleeFunc.{platform}.yaml        # ensures callee is renamed first
          - TargetClass_vtable.{platform}.yaml     # if target is a vfunc (Pattern B)
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:

Choose size from the locating instruction, not from the member name. Include size only when the annotated instruction reads or writes the member and therefore has a natural operand width (for example, mov, movss, or cmp). When the locating instruction is lea reg, [base+offset], it only computes the address of an embedded member; omit size from GENERATE_YAML_DESIRED_FIELDS. A lea result can identify a member offset but cannot reliably establish the member's extent.

TARGET_FUNCTION_NAMES = [
    "SomeFunction",
]

TARGET_STRUCT_MEMBER_NAMES = [
    "SomeStruct_m_someField",
]

GENERATE_YAML_DESIRED_FIELDS = [
    ("SomeFunction", ["func_name", "func_sig", "func_va", "func_rva", "func_size"]),
    # This target is located by `lea`; add "size" only for a real memory access.
    ("SomeStruct_m_someField", ["struct_name", "member_name", "offset", "offset_sig", "offset_sig_disp"]),
]

# In preprocess_skill:
    return await preprocess_common_skill(
        ...
        func_names=TARGET_FUNCTION_NAMES,
        struct_member_names=TARGET_STRUCT_MEMBER_NAMES,
        ...
    )
CRITICAL -- FUNC_VTABLE_RELATIONS and vfunc fields

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.

CRITICAL -- vfunc_sig is MANDATORY for Pattern C (vfunc via LLM_DECOMPILE)

For ANY vfunc discovered via LLM_DECOMPILE (Pattern C), GENERATE_YAML_DESIRED_FIELDS MUST include vfunc_sig. This is non-negotiable -- the slot index alone is not stable across binary updates without a signature anchor on the actual vfunc body.

This rule applies to BOTH variants:

  • Standard Pattern C (also a downstream predecessor): func_name, func_va, func_rva, func_size, vfunc_sig, vfunc_offset, vfunc_index, vtable_name
  • Slim Pattern C (not a downstream predecessor): func_name, vfunc_sig, vfunc_offset, vfunc_index, vtable_name

Pure slot-only output (func_name, vtable_name, vfunc_offset, vfunc_index with NO vfunc_sig) is reserved for Pattern F slot-only / Pattern I / Pattern K / Pattern L -- it is NOT a valid output shape for Pattern C. Examples that follow this rule: find-CEntityInstance_ScriptEntityIO.py, find-CEntityInstance_Restore.py, find-CEntityInstance_RequiredEdictIndex.py, find-CEntityInstance_PreDataUpdate.py, find-CEntityInstance_PostDataUpdate.py, find-CEntityInstance_NetworkUpdateState.py.

Key Differences Between Patterns

AspectPattern A (func + xref)Pattern B (vfunc + xref)Pattern C (vfunc + LLM)Pattern D (func + LLM)Pattern E (structmember + LLM)Pattern F (vfunc + inherit)Pattern G (ConCommand handler)Pattern H (ordinal vtable)Pattern I (iface vfunc thunk walk)Pattern J (IGameSystem dispatch)Pattern K (IGameSystem slot dispatch)Pattern L (iface vfunc vcall scan)
FUNC_XREFSYesYesNoNoNoNoNo (uses COMMAND_NAME/HELP_STRING)NoNoNoNoNo
FUNC_VTABLE_RELATIONSNoYesYesNoNoNoNoNoNoNoNoNo
INHERIT_VFUNCSNoNoNoNoNoYesNoNoNoNoNoNo
LLM_DECOMPILENoNoYesYesYesNoNoNoNoNoNoNo
llm_config paramNoNoYesYesYesNoNoNoNoNoNoNo
Helper modulepreprocess_common_skillpreprocess_common_skillpreprocess_common_skillpreprocess_common_skillpreprocess_common_skillpreprocess_common_skillpreprocess_registerconcommand_skillpreprocess_ordinal_vtable_via_mcppy_eval + write_func_yaml (custom)preprocess_igamesystem_dispatch_skillpreprocess_igamesystem_slot_dispatch_skill (from _igamesystem_slot_dispatch_common)preprocess_indirect_vcall_target_skill (from _indirect_vcall_target_common)
Target listTARGET_FUNCTION_NAMESTARGET_FUNCTION_NAMESTARGET_FUNCTION_NAMESTARGET_FUNCTION_NAMESTARGET_STRUCT_MEMBER_NAMES(none -- defined in INHERIT_VFUNCS)TARGET_FUNCTION_NAMESTARGET_CLASS_NAME (single string)TARGET_FUNC_NAME + PREDECESSOR_STEM (module-level constants)TARGET_SPECS (list of dicts with target_name, rename_to, optional dispatch_rank)TARGET_SPECS (list of dicts with target_name, vtable_name, optional dispatch_rank)SOURCE_FUNCTION_NAME + TARGET_FUNCTION_NAME + VTABLE_CLASS (module-level constants)
preprocess paramfunc_names=func_names=func_names=func_names=struct_member_names=inherit_vfuncs=command_name=, help_string=class_name=, ordinal=(custom: reads YAML, calls py_eval)source_yaml_stem=, target_specs=, via_internal_wrapper=, multi_order=dispatcher_yaml_stem=, target_specs=, multi_order=, expected_dispatch_count=source_yaml_stem=, target_name=, vtable_name=
YAML fieldsfunc_name, func_sig, func_va, func_rva, func_sizeSame + vtable_name, vfunc_offset, vfunc_indexvfunc_sig ALWAYS required. Standard: func_name, func_va, func_rva, func_size, vfunc_sig, vfunc_offset, vfunc_index, vtable_name. Slim (not a downstream predecessor): func_name, vfunc_sig, vfunc_offset, vfunc_index, vtable_namefunc_name, func_sig, func_va, func_rva, func_sizestruct_name, member_name, offset, offset_sig, offset_sig_disp; add size only for a non-lea memory accessStandard: func_name, func_va, func_rva, func_size, func_sig, vtable_name, vfunc_offset, vfunc_index; Slot-only: func_name, vtable_name, vfunc_offset, vfunc_indexfunc_name, func_sig, func_va, func_rva, func_size(vtable YAML via write_vtable_yaml)func_name, vtable_name, vfunc_offset, vfunc_indexfunc_name, func_va, func_rva, func_size, func_sig, vtable_name, vfunc_offset, vfunc_indexfunc_name, vtable_name, vfunc_offset, vfunc_indexfunc_name, vtable_name, vfunc_offset, vfunc_index
config categoryfuncvfuncvfuncfuncstructmembervfuncfuncvtablevfuncvfuncvfuncvfunc

Step 3: Update configs/.yaml

3a. Skills Section

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 entries in logical order (near related functions).

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 Patterns C & D: the reference function
          - {VTABLE_CLASS}_vtable.{platform}.yaml  # For Patterns B & C: the vtable

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
65
Forks
10
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
create-preprocessor-scripts
Source
github.com/hlnd2t/cs2_vibesignatures