本文へ移動
cccskills
無料GitHub で公開

create-preprocessor-scripts

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.

インストール方法を見る

含まれるファイル(14)

  • SKILL.md69.0 KB
  • agents/openai.yaml43 B
  • references/pattern-A.md3.2 KB
  • references/pattern-B.md3.5 KB
  • references/pattern-C.md4.5 KB
  • references/pattern-D.md2.0 KB
  • references/pattern-E.md4.1 KB
  • references/pattern-F.md6.7 KB
  • references/pattern-G.md3.0 KB
  • references/pattern-H.md3.7 KB
  • references/pattern-I.md8.7 KB
  • references/pattern-J.md5.2 KB
  • references/pattern-L.md8.4 KB
  • references/pattern-M.md11.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Create Preprocessor Scripts from Scratch

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/<GAMEVER>.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/<GAMEVER>.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/<GAMEVER>.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/<GAMEVER>.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

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 F (standard): needs both the derived class vtable YAML and the base vfunc YAML in expected_input
  • Pattern F (slot-only): needs ONLY the base vfunc YAML in expected_input -- no vtable YAML for the interface class
  • Pattern J: needs both the predecessor function YAML and IGameSystem_vtable.{platform}.yaml in expected_input
  • Pattern K: needs ONLY {DISPATCHER_YAML_STEM}.{platform}.yaml in expected_input -- no IGameSystem_vtable.{platform}.yaml needed
  • Multi-function scripts use -AND- in the name: find-FuncA-AND-FuncB
  • Place the new entry near related functions (e.g. CCSPlayer_MovementServices_* entries together)

Dependency chain example (multi-script):

      # Pattern A: found via xref string, no dependencies
      - name: find-FuncA
        expected_output:
          - FuncA.{platform}.yaml

      # Pattern C: found by decompiling FuncA, needs FuncA + vtable
      - name: find-FuncB
        expected_output:
          - FuncB.{platform}.yaml
        expected_input:
          - FuncA.{platform}.yaml
          - SomeClass_vtable.{platform}.yaml

3b. Symbols Section

For each target symbol, add a symbol entry under the same module's symbols: list (if not already present).

      # Regular function (Pattern A)
      - name: {FUNC_NAME}
        category: func
        alias:
          - {ClassName}::{MethodName}   # e.g. CPlayer_MovementServices::PlayWaterStepSound

      # Virtual function (Patterns B & C)
      - name: {FUNC_NAME}
        category: vfunc
        alias:
          - {ClassName}::{MethodName}   # e.g. CBasePlayerPawn::OnTakeDamage

      # Struct member offset (Pattern E)
      - name: {STRUCT_MEMBER_NAME}
        category: structmember
        struct: {STRUCT_NAME}
        member: {MEMBER_NAME}
        alias:
          - {StructName}::{MemberName}

CRITICAL -- Declare the parent struct for every struct member

For every symbol with category: structmember, verify that the same module's symbols: list already contains its parent struct as a metadata-only declaration:

      - name: {STRUCT_NAME}
        category: struct
        # Include platform only when the struct is platform-restricted.
        platform: {platform}
  • If the parent struct is missing, add the declaration immediately before its first struct member.
  • If it already exists, reuse it; do not add a duplicate declaration or narrow its existing platform coverage.
  • Match platform coverage: use the member's platform when all members are restricted to that platform; omit platform when the struct has members on both Windows and Linux.
  • A category: struct entry is metadata-only. Do not add source_alias, an expected_output, or a separate preprocessor skill for it.

Without this declaration, gamedata config validation fails with '<StructName>' is not a declared struct in this module.

Check existing symbols before adding -- do NOT create duplicates.

Place the new symbol near related symbols (same class/subsystem).


Step 4: Handle Reference YAMLs (Patterns C, D & E only)

Pattern C, D, and E scripts reference a predecessor function's YAML at: ida_preprocessor_scripts/references/{module}/{PREDECESSOR_FUNC}.{platform}.yaml

Check if the reference YAML already exists:

  • ida_preprocessor_scripts/references/{module}/{PREDECESSOR_FUNC}.linux.yaml
  • ida_preprocessor_scripts/references/{module}/{PREDECESSOR_FUNC}.windows.yaml

Always 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

where {gamever} can be obtained from .env -> CS2VIBE_GAMEVER.

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.

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.

YOU MUST: rename known symbols / add necessary comments in the generated reference YAMLs so the LLM can find desired symbols by comparing reference ones with raw procedure/disassembly read from new binaries. Always annotate both disasm_code and procedure fields. Format by target type:

Direct function call — rename sub_XXXX to the target function name in both fields.

Virtual function call — add offset comment:

  • disasm_code: call qword ptr [rax+3F0h] ; 3F0h = CBaseEntity_OnTakeDamage
  • procedure: (*a1 + 1008LL)... // 1008LL = 0x3F0 = CBaseEntity_OnTakeDamage

Global variable — rename qword_XXXX to the target name in both fields.

Struct member access — add comments using the (structmember, struct=X, member=Y) tag:

  • disasm_code: cmp dil, [rsi+0C1h] ; 0C1h = SDL_Mouse::relative_mode (structmember, struct=SDL_Mouse, member=relative_mode)
  • procedure: a1 != Mouse->field // 0xC1 = SDL_Mouse::relative_mode (structmember, struct=SDL_Mouse, member=relative_mode)

The (structmember, struct=StructName, member=member_name) tag is required for all struct member annotations — it tells the LLM which struct and member name to report back. Annotate every access site for the target field.

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 at the same time), its output YAMLs don't exist yet and generate_reference_yaml.py cannot resolve its address. You must use a multi-phase workflow:

  1. Phase 1: Create ALL scripts (vtable, xref_string, LLM_DECOMPILE) and update configs/<GAMEVER>.yaml
  2. Phase 2: Seed a checkout-external artifact root from tracked bin_artifacts, remove the new outputs only there, and run the required module/skills without -force_all, with -oldgamever none and explicit -artifactdir/-oldartifactdir. The vtable and xref-string scripts populate the new predecessor in the isolated root.
  3. Phase 3: Now that the predecessor has output YAMLs, run generate_reference_yaml.py to create reference YAMLs, then annotate them.
  4. Phase 4: Re-run the seeded affected-closure validation (the same root is fine once the reference YAMLs exist); then copy the canonical closure to tracked bin_artifacts and run the repository artifact contract. Full force-all recomputation is delegated to the PR pipeline's selected execution (see Step 5).

IMPORTANT -- When the reference YAML already existed: generate_reference_yaml.py regenerates the file from scratch and silently overwrites any hand-written annotation comments. After running it, check the diff for each regenerated file:

git diff ida_preprocessor_scripts/references/{module}/{PREDECESSOR_FUNC}.{platform}.yaml

Look for removed lines (prefixed with - in the diff) that are annotation comments: lines beginning with ; inside disasm_code or // inside procedure. If any were dropped, restore them verbatim by copying directly from the - lines in the diff output into the correct locations in the regenerated file. Do not reconstruct comments from memory -- copy from the diff.


Step 5: Run Tests

After all creation steps are complete, validate the affected closure in a checkout-external seeded artifact root. Seed the root from tracked bin_artifacts/<GAMEVER>/, remove only the new/changed outputs there, and run the affected modules without -force_all (unrelated skills are skipped automatically because their outputs already exist in the seed). Never delete or rewrite tracked expected artifacts merely to make a test run.

Because the output is very long, redirect it to a temp file and then read just the summary:

uv run ida_analyze_bin.py -gamever <GAMEVER> -configyaml configs/<GAMEVER>.yaml \
  -artifactdir <checkout-external-seeded-root> -oldartifactdir bin_artifacts \
  -oldgamever none -modules <AFFECTED_MODULE> -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 affected closure 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 it. A local full -force_all run is not required: the PR pipeline recomputes the affected closure itself through the trusted base-inherited-selected execution strategy (pr-self-runner.yml -> -selected_execution manifest, falling back to -force_all only when no manifest is published) and compares the rebuilt bytes against the PR, so a local full-force run is redundant. After the seeded run passes, copy the computed affected/downstream closure into bin_artifacts/<GAMEVER>/ and run the repository artifact contract.


Step 6: Run Regression Tests

Run the non-MCP unittest suite:

uv run python -c "from pathlib import Path; import sys, unittest; excluded={'test_ida_mcp_session', 'test_smoke_ida_mcp_2'}; modules=[f'tests.{path.stem}' for path in Path('tests').glob('test_*.py') if path.stem not in excluded]; result=unittest.TextTestRunner(buffer=True).run(unittest.defaultTestLoader.loadTestsFromNames(modules)); sys.exit(not result.wasSuccessful())"

This intentionally excludes the IDA MCP adapter and smoke modules (test_ida_mcp_session, test_smoke_ida_mcp_2) to keep preprocessor work fast. Run those modules separately when changing MCP routing or lifecycle code.

Keep 0 selected unittest failures before delivery. If any test fails, investigate and fix it before staging the task changes.


Step 7: Commit Changes to dev

After validation passes, ensure the delivery branch is dev. Never commit directly to main. If the local dev branch exists, switch to it. Otherwise, switch to main first and create dev from main:

if git show-ref --verify --quiet refs/heads/dev; then
  git switch dev
else
  git switch main
  git switch -c dev
fi

If any branch switch fails, stop and report the error. Review git status --short, then explicitly stage the task-related source/config/reference files and complete bin_artifacts closure. Never use git add -A:

git add -- ida_preprocessor_scripts/find-{SKILL_NAME}.py configs/<GAMEVER>.yaml
git add -- <generated-reference-yamls>
git add -- <bin_artifacts-affected-and-downstream-closure>
git diff --cached --name-only

Include every task-related implementation file changed:

  • The new preprocessor script
  • configs/<GAMEVER>.yaml changes
  • Any reference YAMLs generated (for Patterns C/D/E)
  • Every canonical bin_artifacts/<GAMEVER>/ A/M/D/R in the computed closure

Stop if the staged-path list contains anything unrelated to this task. Commit only the staged task changes using the repository commit format:

git commit -m "feat(preprocessor): add find-{SKILL_NAME}" -m "Co-Authored-By: Codex <codex@openai.com>"

Do not call /create-pr, push the branch, or open a pull request unless the user separately requests it. Finish by reporting the commit hash, game version, and successful ida_analyze_bin.py and non-MCP unittest results.


Checklist

Before finishing, verify:

  • Preprocessor script file name matches the name field in configs/<GAMEVER>.yaml skill entry
  • GENERATE_YAML_DESIRED_FIELDS uses correct field set for the pattern
  • A struct member located by lea omits size; a non-lea memory access includes it
  • configs/<GAMEVER>.yaml expected_output has one entry per target
  • configs/<GAMEVER>.yaml expected_input correctly chains dependencies
  • configs/<GAMEVER>.yaml symbols section has entries for all targets (no duplicates)
  • Every structmember parent has one metadata-only category: struct declaration in the same module with compatible platform coverage
  • Pattern-specific checks pass (see the Checklist section in the chosen pattern reference file)
  • Seeded-root ida_analyze_bin.py run over the affected closure passes with 0 failures and does not modify checkout expected artifacts (full force-all recomputation is delegated to the PR pipeline's selected execution)
  • Complete canonical bin_artifacts closure is staged; no snapshot/gamedata/release-manifest output is staged
  • Non-MCP unittest command above passes with 0 failures
  • The current branch is dev (created from main when it did not already exist)
  • Only task-related source/config/reference files and their computed artifact closure are explicitly staged and committed
  • /create-pr was not called; no push or PR was performed without a separate user request

Real-World Examples

Example: Regular function via xref string (Pattern A)

Issue says: CPlayer_MovementServices_PlayWaterStepSound is a regular function in server dll. xref_strings: "CT_Water.StepLeft". Fields needed: func_name, func_sig, func_va, func_rva, func_size.

Result: ida_preprocessor_scripts/find-CPlayer_MovementServices_PlayWaterStepSound.py with:

  • FUNC_XREFS containing "CT_Water.StepLeft"
  • GENERATE_YAML_DESIRED_FIELDS with func_name, func_sig, func_va, func_rva, func_size
  • No FUNC_VTABLE_RELATIONS, no LLM_DECOMPILE
  • configs/<GAMEVER>.yaml skill entry with expected_output: CPlayer_MovementServices_PlayWaterStepSound.{platform}.yaml
  • configs/<GAMEVER>.yaml symbol entry with category: func, alias CPlayer_MovementServices::PlayWaterStepSound

Example: Virtual function via xref string (Pattern B)

Issue says: CSource2GameEntities_CheckTransmit is a vfunc of CSource2GameEntities in server dll. xref_strings: "CSource2GameEntities::CheckTransmit" (Windows), "./gameinterface.cpp:30" (Linux).

Result: ida_preprocessor_scripts/find-CSource2GameEntities_CheckTransmit.py with:

  • Platform-specific FUNC_XREFS_WINDOWS / FUNC_XREFS_LINUX
  • FUNC_VTABLE_RELATIONS: ("CSource2GameEntities_CheckTransmit", "CSource2GameEntities")
  • GENERATE_YAML_DESIRED_FIELDS with vtable fields
  • configs/<GAMEVER>.yaml expected_input: CSource2GameEntities_vtable.{platform}.yaml

Example: Multiple functions from same xref (Pattern A, multi-target)

Issue says: Find both FuncA and FuncB in server. Both use xref string "SharedDebugString".

Result: ida_preprocessor_scripts/find-FuncA-AND-FuncB.py with:

  • Two entries in TARGET_FUNCTION_NAMES
  • Two entries in FUNC_XREFS (each with the same or different xref strings)
  • Two entries in GENERATE_YAML_DESIRED_FIELDS
  • configs/<GAMEVER>.yaml skill name: find-FuncA-AND-FuncB
  • configs/<GAMEVER>.yaml: two expected_output entries, two symbol entries

Example: Derived-class vfunc via INHERIT_VFUNCS (Pattern F)

Issue says: CBaseEntity_Precache is a vfunc on CBaseEntity that overrides CEntityInstance::Precache at the same vtable slot. CEntityInstance_Precache is already found by another script.

Result: ida_preprocessor_scripts/find-CBaseEntity_Precache.py with:

  • INHERIT_VFUNCS: ("CBaseEntity_Precache", "CBaseEntity", "CEntityInstance_Precache", True)
  • GENERATE_YAML_DESIRED_FIELDS with vtable fields
  • No FUNC_XREFS, no LLM_DECOMPILE, no FUNC_VTABLE_RELATIONS
  • configs/<GAMEVER>.yaml expected_input: CBaseEntity_vtable.{platform}.yaml + CEntityInstance_Precache.{platform}.yaml
  • configs/<GAMEVER>.yaml symbol: category vfunc, alias CBaseEntity::Precache

Example: Virtual function via xref string with FULLMATCH (Pattern B)

Issue says: CEntityInstance_Precache is a vfunc on CEntityInstance. xref_string: "Precache" (exact match needed since substring would hit PrecacheModel, etc.).

Result: ida_preprocessor_scripts/find-CEntityInstance_Precache.py with:

  • FUNC_XREFS containing "FULLMATCH:Precache" (exact match)
  • FUNC_VTABLE_RELATIONS: ("CEntityInstance_Precache", "CEntityInstance")
  • GENERATE_YAML_DESIRED_FIELDS with vtable fields
  • configs/<GAMEVER>.yaml expected_input: CEntityInstance_vtable.{platform}.yaml

Example: vtable + xref-string vfunc + LLM_DECOMPILE regular function (vtable + Patterns B + D, multi-phase)

User says: Find LegacyGameEventListener in server. It's a regular function called from CSource2GameClients::StartHLTVServer (a vfunc of CSource2GameClients). The xref string for StartHLTVServer is "CSource2GameClients::StartHLTVServer: game event %s not found".

Result -- three scripts:

  1. ida_preprocessor_scripts/find-CSource2GameClients_vtable.py (vtable discovery):

    • TARGET_CLASS_NAMES: ["CSource2GameClients"]
    • Pure vtable lookup, no dependencies
  2. ida_preprocessor_scripts/find-CSource2GameClients_StartHLTVServer.py (Pattern B):

    • FUNC_XREFS containing "CSource2GameClients::StartHLTVServer: game event %s not found"
    • FUNC_VTABLE_RELATIONS: ("CSource2GameClients_StartHLTVServer", "CSource2GameClients")
    • configs/<GAMEVER>.yaml expected_input: CSource2GameClients_vtable.{platform}.yaml
  3. ida_preprocessor_scripts/find-LegacyGameEventListener.py (Pattern D):

    • LLM_DECOMPILE referencing references/server/CSource2GameClients_StartHLTVServer.{platform}.yaml
    • No FUNC_VTABLE_RELATIONS (regular function)
    • Reference YAMLs annotated: sub_180B1AC80 / sub_1516AB0 renamed to LegacyGameEventListener in both disasm and procedure
    • configs/<GAMEVER>.yaml expected_input: CSource2GameClients_StartHLTVServer.{platform}.yaml

configs/<GAMEVER>.yaml dependency chain:

      - name: find-CSource2GameClients_vtable
        expected_output:
          - CSource2GameClients_vtable.{platform}.yaml

      - name: find-CSource2GameClients_StartHLTVServer
        expected_output:
          - CSource2GameClients_StartHLTVServer.{platform}.yaml
        expected_input:
          - CSource2GameClients_vtable.{platform}.yaml

      - name: find-LegacyGameEventListener
        expected_output:
          - LegacyGameEventListener.{platform}.yaml
        expected_input:
          - CSource2GameClients_StartHLTVServer.{platform}.yaml

Key insight -- multi-phase workflow required: CSource2GameClients_StartHLTVServer was a brand-new function with no existing output YAMLs. generate_reference_yaml.py needs func_va from the predecessor's output YAML to locate it in IDA. So the workflow was:

  1. Create all 3 scripts + config entries
  2. Run ida_analyze_bin.py -debug -> vtable + xref scripts succeed and create StartHLTVServer YAMLs
  3. Run generate_reference_yaml.py using the newly created output YAMLs
  4. Annotate reference YAMLs
  5. Run ida_analyze_bin.py -debug again -> LLM_DECOMPILE path runs and succeeds

Example: ConCommand handler function (Pattern G)

User says: Find BotKill_CommandHandler in server. It's the handler callback for the bot_kill console command. COMMAND_NAME="bot_kill", HELP_STRING="bot_kill <all> <t|ct> <type> <difficulty> <name> - Kills a specific bot, or all bots, matching the given criteria.".

Result: ida_preprocessor_scripts/find-BotKill_CommandHandler.py with:

  • COMMAND_NAME = "bot_kill"
  • HELP_STRING = "bot_kill <all> <t|ct> <type> <difficulty> <name> - Kills a specific bot, or all bots, matching the given criteria."
  • SEARCH_WINDOW_BEFORE_CALL = 96, SEARCH_WINDOW_AFTER_XREF = 96
  • Uses preprocess_registerconcommand_skill() from _registerconcommand.py
  • GENERATE_YAML_DESIRED_FIELDS with func_name, func_sig, func_va, func_rva, func_size
  • configs/<GAMEVER>.yaml skill entry with no expected_input
  • configs/<GAMEVER>.yaml symbol entry with category: func, alias CCSBotManager::BotKillCommand

Example: ConCommand handler + LLM_DECOMPILE virtual function (Patterns G + C, multi-phase)

User says: Find CBasePlayerPawn_CommitSuicide in server. It's a vfunc on CBasePlayerPawn called from the bot_kill command handler via call qword ptr [rax+0xC80]. The handler iterates matched bots and calls pPlayerPawn->CommitSuicide(false, false).

Result -- two scripts:

  1. ida_preprocessor_scripts/find-BotKill_CommandHandler.py (Pattern G):

    • COMMAND_NAME = "bot_kill", HELP_STRING = "bot_kill <all> ..."
    • No dependencies
  2. ida_preprocessor_scripts/find-CBasePlayerPawn_CommitSuicide.py (Pattern C):

    • LLM_DECOMPILE referencing references/server/BotKill_CommandHandler.{platform}.yaml
    • FUNC_VTABLE_RELATIONS: ("CBasePlayerPawn_CommitSuicide", "CBasePlayerPawn")
    • Reference YAMLs annotated with ; 0xC80 = CBasePlayerPawn_CommitSuicide in disasm and // 3200LL = 0xC80 = CBasePlayerPawn_CommitSuicide in procedure

configs/<GAMEVER>.yaml dependency chain:

      - name: find-BotKill_CommandHandler
        expected_output:
          - BotKill_CommandHandler.{platform}.yaml

      - name: find-CBasePlayerPawn_CommitSuicide
        expected_output:
          - CBasePlayerPawn_CommitSuicide.{platform}.yaml
        expected_input:
          - BotKill_CommandHandler.{platform}.yaml
          - CBasePlayerPawn_vtable.{platform}.yaml

Multi-phase workflow: BotKill_CommandHandler is a new function, so:

  1. Create both scripts + config entries
  2. Run ida_analyze_bin.py -debug -> Pattern G script succeeds, creates BotKill_CommandHandler YAMLs
  3. Run generate_reference_yaml.py for both platforms
  4. Annotate reference YAMLs with CommitSuicide vfunc call comments
  5. Run ida_analyze_bin.py -debug again -> LLM_DECOMPILE path runs and succeeds

Example: xref-string function + LLM_DECOMPILE global variable & vfunc offset (Patterns A + LLM_DECOMPILE with gv + vfunc, multi-phase)

User says: Find g_pGameTypes (global variable) and IGameTypes_CreateWorkshopMapGroup (vfunc offset at call [rax+128h]) in server. They are found by decompiling CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup, which is discoverable via xref string "mapgroup workshop".

Result -- two scripts:

  1. ida_preprocessor_scripts/find-CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup.py (Pattern A):

    • FUNC_XREFS containing "mapgroup workshop"
    • GENERATE_YAML_DESIRED_FIELDS with func_name, func_sig, func_va, func_rva, func_size
    • No FUNC_VTABLE_RELATIONS, no LLM_DECOMPILE
  2. ida_preprocessor_scripts/find-g_pGameTypes-AND-IGameTypes_CreateWorkshopMapGroup.py (LLM_DECOMPILE):

    • TARGET_FUNCTION_NAMES: ["IGameTypes_CreateWorkshopMapGroup"]
    • TARGET_GLOBALVAR_NAMES: ["g_pGameTypes"]
    • LLM_DECOMPILE with two entries (one per target), both referencing the same predecessor YAML
    • CRITICAL: FUNC_VTABLE_RELATIONS: ("IGameTypes_CreateWorkshopMapGroup", "IGameTypes") -- required because GENERATE_YAML_DESIRED_FIELDS includes vtable_name and vfunc_sig. Without this, 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
    • GENERATE_YAML_DESIRED_FIELDS for g_pGameTypes: gv_name, gv_va, gv_rva, gv_sig, gv_sig_va, gv_inst_offset, gv_inst_length, gv_inst_disp
    • configs/<GAMEVER>.yaml symbols: g_pGameTypes with category: gv, IGameTypes_CreateWorkshopMapGroup with category: vfunc

configs/<GAMEVER>.yaml dependency chain:

      - name: find-CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup
        expected_output:
          - CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup.{platform}.yaml

      - name: find-g_pGameTypes-AND-IGameTypes_CreateWorkshopMapGroup
        expected_output:
          - g_pGameTypes.{platform}.yaml
          - IGameTypes_CreateWorkshopMapGroup.{platform}.yaml
        expected_input:
          - CDedicatedServerWorkshopManager_SwitchToWorkshopMapGroup.{platform}.yaml

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.

Example: Secondary vtable via ordinal lookup (Pattern H)

User says: Find CLoopTypeClientServerService_vtable2 in engine. Windows mangled name: ??_7CLoopTypeClientServerService@@6B@_0. Linux: _ZTI28CLoopTypeClientServerService with dq -56 ; offset to this.

Result: ida_preprocessor_scripts/find-CLoopTypeClientServerService_vtable2.py with:

  • TARGET_CLASS_NAME = "CLoopTypeClientServerService"
  • TARGET_OUTPUT_STEM = "CLoopTypeClientServerService_vtable2"
  • WINDOWS_SYMBOL_ALIASES = ["??_7CLoopTypeClientServerService@@6B@_0"]
  • LINUX_EXPECTED_OFFSET_TO_TOP = -56
  • Uses preprocess_ordinal_vtable_via_mcp with ordinal=0
  • configs/<GAMEVER>.yaml skill entry with no expected_input
  • configs/<GAMEVER>.yaml symbol entry with category: vtable

Example: Interface vfunc offset via thunk instruction walk (Pattern I)

User says: Find ILoopMode_HandleInputEvent in engine. It's a vfunc of ILoopMode (shared interface). The offset can be read from CLoopTypeClientServerService_HandleInputEvent, a thin thunk that does:

Windows: mov rcx, [rcx+0E0h] / mov rax, [rcx] / jmp [rax+28h]
Linux:   mov rdi, [rdi+0E0h] / mov rax, [rdi] / jmp [rax+28h]

No unique func_sig is feasible for the thunk (2-3 generic instructions). func_sig for the jmp itself is also not unique.

Result: ida_preprocessor_scripts/find-ILoopMode_HandleInputEvent.py with:

  • PREDECESSOR_STEM = "CLoopTypeClientServerService_HandleInputEvent"
  • TARGET_FUNC_NAME = "ILoopMode_HandleInputEvent"
  • VTABLE_CLASS = "ILoopMode"
  • _PY_EVAL_TEMPLATE walks idaapi.decode_insn loop, finds first jmp with op.type == idaapi.o_displ, reads op.addr & 0xFFFF_FFFF as vfunc_offset
  • Writes func_name, vtable_name, vfunc_offset, vfunc_index via write_func_yaml
  • "Reuse previous gamever" fast path reads vfunc_offset from old_yaml_map[TARGET_FUNC_NAME]

configs/<GAMEVER>.yaml:

      - name: find-ILoopMode_HandleInputEvent
        expected_output:
          - ILoopMode_HandleInputEvent.{platform}.yaml
        expected_input:
          - CLoopTypeClientServerService_HandleInputEvent.{platform}.yaml

Output YAML (both platforms):

func_name: ILoopMode_HandleInputEvent
vtable_name: ILoopMode
vfunc_offset: '0x28'
vfunc_index: 5

Key insight -- when to choose Pattern I over B/C:

  • Pattern B would need an xref_signature for the thunk body -- but 48 8B ?? 48 FF 60 ?? (Windows) / 48 8B ?? FF 60 ?? (Linux) are too generic to sign uniquely without the concrete displacement byte filled in
  • Pattern C (LLM_DECOMPILE) would work but is heavyweight for a 2-3 instruction function; the LLM also requires a SKILL.md file which generates a "Skill file not found" error if missing
  • Pattern I avoids both issues: no signature needed, no LLM needed -- the displacement byte is read deterministically via idaapi.decode_insn

Example: Interface vfunc slot index via INHERIT_VFUNCS slot-only (Pattern F slot-only)

User says: Find ILoopMode_LoopInit in client and server. It's a vfunc of the abstract interface ILoopMode. CLoopModeGame_LoopInit already overrides it at the same vtable slot. No func_sig or vfunc_sig needed.

Result: ida_preprocessor_scripts/find-ILoopMode_LoopInit.py with:

  • INHERIT_VFUNCS: ("ILoopMode_LoopInit", "ILoopMode", "CLoopModeGame_LoopInit", False)
  • GENERATE_YAML_DESIRED_FIELDS: exactly func_name, vtable_name, vfunc_offset, vfunc_index -- triggers slot-only mode
  • No FUNC_XREFS, no LLM_DECOMPILE, no FUNC_VTABLE_RELATIONS
  • configs/<GAMEVER>.yaml expected_input: ONLY CLoopModeGame_LoopInit.{platform}.yaml -- no ILoopMode_vtable.{platform}.yaml
  • configs/<GAMEVER>.yaml symbol: category: vfunc, alias ILoopMode::LoopInit
  • Same single script file referenced in BOTH client and server module sections of configs/<GAMEVER>.yaml

configs/<GAMEVER>.yaml entry (in both client and server sections):

      - name: find-ILoopMode_LoopInit
        expected_output:
          - ILoopMode_LoopInit.{platform}.yaml
        expected_input:
          - CLoopModeGame_LoopInit.{platform}.yaml

Output YAML:

func_name: ILoopMode_LoopInit
vtable_name: ILoopMode
vfunc_offset: '0x28'
vfunc_index: 5

Key insight -- slot-only vs Pattern I: ILoopMode_HandleInputEvent used Pattern I (thunk instruction walk) because its offset comes from reading jmp [reg+disp] inside a thin wrapper. ILoopMode_LoopInit uses Pattern F slot-only because CLoopModeGame_LoopInit already has a vfunc_index in its output YAML -- no instruction walking needed, just copy the slot index with a different vtable_name.

Example: Interface vfunc via indirect vcall scan + derived override via INHERIT_VFUNCS (Pattern L + Pattern F)

User says: Create find-INetworkGameServer_ServerAdvanceTick where INetworkGameServer::ServerAdvanceTick is a vfunc of the abstract interface INetworkGameServer, resolved from the predecessor CNetworkServerService_OnServerAdvanceTick. Then create find-CNetworkGameServerBase_ServerAdvanceTick via INHERIT_VFUNCS, where CNetworkGameServerBase_ServerAdvanceTick is a vfunc of CNetworkGameServerBase_vtable.

CNetworkServerService_OnServerAdvanceTick is a thin thunk whose entire body is one indirect vtable call:

Windows: mov rcx, [rcx+150h] / test rcx, rcx / jz ... / mov rax, [rcx] / jmp qword ptr [rax+68h]
Linux:   mov rdi, [rdi+150h] / test rdi, rdi / jz ... / mov rax, [rdi] / jmp qword ptr [rax+68h]

Why NOT Pattern C: the first attempt used Pattern C (LLM_DECOMPILE). The LLM correctly found offset 0x68, but vfunc_sig generation failed -- jmp qword ptr [rax+68h] encodes as just FF 60 68 (3 bytes, disp8 since 0x68 <= 0x7F) and cannot be signed uniquely. preprocess_common_skill aborted with "failed to generate slot-only vfunc_sig". (The sibling ServerEndSimulate only worked because offset 0x88 forces a longer disp32 encoding FF A0 88 00 00 00.) Pattern L's deterministic scan avoids signing entirely.

Result -- two scripts:

  1. ida_preprocessor_scripts/find-INetworkGameServer_ServerAdvanceTick.py (Pattern L):

    • SOURCE_FUNCTION_NAME = "CNetworkServerService_OnServerAdvanceTick" (thunk, already found by another skill)
    • TARGET_FUNCTION_NAME = "INetworkGameServer_ServerAdvanceTick", VTABLE_CLASS = "INetworkGameServer"
    • Uses preprocess_indirect_vcall_target_skill() from _indirect_vcall_target_common.py
    • GENERATE_YAML_DESIRED_FIELDS: slot-only func_name, vtable_name, vfunc_offset, vfunc_index
    • No FUNC_VTABLE_RELATIONS, no LLM_DECOMPILE, no reference YAML, no llm_config
    • preprocess_skill ignores old_yaml_map/image_base (_ = skill_name, old_yaml_map, image_base)
  2. ida_preprocessor_scripts/find-CNetworkGameServerBase_ServerAdvanceTick.py (Pattern F standard):

    • INHERIT_VFUNCS: ("CNetworkGameServerBase_ServerAdvanceTick", "CNetworkGameServerBase", "INetworkGameServer_ServerAdvanceTick", True)
    • Full fields: func_name, func_va, func_rva, func_size, func_sig, vtable_name, vfunc_offset, vfunc_index
    • Inherits slot 13 from the Pattern L base YAML, looks it up in CNetworkGameServerBase_vtable, resolves the real function body

configs/<GAMEVER>.yaml dependency chain (engine module):

      - name: find-INetworkGameServer_ServerAdvanceTick
        expected_output:
          - INetworkGameServer_ServerAdvanceTick.{platform}.yaml
        expected_input:
          - CNetworkServerService_OnServerAdvanceTick.{platform}.yaml
      - name: find-CNetworkGameServerBase_ServerAdvanceTick
        expected_output:
          - CNetworkGameServerBase_ServerAdvanceTick.{platform}.yaml
        expected_input:
          - CNetworkGameServerBase_vtable.{platform}.yaml
          - INetworkGameServer_ServerAdvanceTick.{platform}.yaml

configs/<GAMEVER>.yaml symbols: both category: vfunc (INetworkGameServer::ServerAdvanceTick, CNetworkGameServerBase::ServerAdvanceTick).

Pattern L output YAML (both platforms, identical offset/index):

func_name: INetworkGameServer_ServerAdvanceTick
vtable_name: INetworkGameServer
vfunc_offset: '0x68'
vfunc_index: 13

Key insight -- Pattern L over Pattern I: both read a jmp [reg+disp] displacement, but Pattern L uses the reusable _indirect_vcall_target_common.py helper, scans call and jmp register-indirect operands, and fails loudly unless exactly one unique 8-byte-aligned slot is found (rather than silently taking the first). Prefer Pattern L for new work; keep Pattern I only when you need its old-gamever reuse fast path or a bespoke operand filter.

Key insight -- chaining into Pattern F: the abstract-interface slot from Pattern L is slot-only (no func_sig), which is exactly what a downstream Pattern F standard override needs -- INHERIT_VFUNCS reads only vfunc_index from the base YAML, so dropping vfunc_sig on the base does not affect the derived lookup.

Example: IGameSystem vfuncs via dispatch scan -- single predecessor, two targets (Pattern J)

User says: Find IGameSystem_OnServerPreEntityThink and IGameSystem_OnServerPostEntityThink in server. Both appear as callback arguments to IGameSystem_DispatchCall(...) in CSource2Server_GameFrame. The decompile shows:

IGameSystem_DispatchCall(v30, (__int64 (__fastcall *)(...))GameSystem_OnServerPreEntityThink, (__int64)&v45);
// ... entity simulation ...
IGameSystem_DispatchCall(v37, (__int64 (__fastcall *)(...))GameSystem_OnServerPostEntityThink, (__int64)&v45);

Result: ida_preprocessor_scripts/find-IGameSystem_OnServerPreEntityThink-AND-IGameSystem_OnServerPostEntityThink.py with:

  • SOURCE_YAML_STEM = "CSource2Server_GameFrame" -- predecessor already found by a Pattern B script
  • TARGET_SPECS: two entries, rename_to values taken directly from the decompile callback names
  • VIA_INTERNAL_WRAPPER = False -- CSource2Server_GameFrame contains the dispatch calls directly (no nested helper)
  • INTERNAL_RENAME_TO = None
  • MULTI_ORDER = "index" -- two targets, two dispatches, use index order for stable mapping
  • No EXPECTED_DISPATCH_COUNT -- target count equals dispatch count (2 == 2), default is sufficient
  • configs/<GAMEVER>.yaml expected_input: CSource2Server_GameFrame.{platform}.yaml + IGameSystem_vtable.{platform}.yaml
SOURCE_YAML_STEM = "CSource2Server_GameFrame"
TARGET_SPECS = [
    {"target_name": "IGameSystem_OnServerPreEntityThink",  "rename_to": "GameSystem_OnServerPreEntityThink"},
    {"target_name": "IGameSystem_OnServerPostEntityThink", "rename_to": "GameSystem_OnServerPostEntityThink"},
]
VIA_INTERNAL_WRAPPER = False
INTERNAL_RENAME_TO = None
MULTI_ORDER = "index"

configs/<GAMEVER>.yaml:

      - name: find-IGameSystem_OnServerPreEntityThink-AND-IGameSystem_OnServerPostEntityThink
        expected_output:
          - IGameSystem_OnServerPreEntityThink.{platform}.yaml
          - IGameSystem_OnServerPostEntityThink.{platform}.yaml
        expected_input:
          - CSource2Server_GameFrame.{platform}.yaml
          - IGameSystem_vtable.{platform}.yaml

Key insight -- choosing MULTI_ORDER:

  • "scan" preserves the textual order of IGameSystem_DispatchCall sites as they appear in the function body -- safe only when there is exactly 1 target or when all targets are extracted (no dispatch_rank filtering).
  • "index" sorts collected entries by (vfunc_index, vfunc_offset) before mapping -- required for multi-target scripts because compiler instruction scheduling can reorder lea rdx, callback emissions independently of semantic call order, making index-based sorting more stable across game updates.

Key insight -- VIA_INTERNAL_WRAPPER:

  • Set to False when the predecessor function itself contains the IGameSystem_DispatchCall sites directly (like CSource2Server_GameFrame).
  • Set to True when the predecessor immediately tail-calls or inlines a distinct named sub-function that holds the actual dispatch calls (e.g. CLoopModeGame_OnClientPreOutput → CLoopModeGame_OnClientPreOutputInternal). In that case also set INTERNAL_RENAME_TO to the wrapper's intended name so it gets annotated in IDA.

Example: IGameSystem abstract vfunc via slot dispatch scan (Pattern K)

User says: Find IGameSystem_OnGamePreShutdown in server. It's an abstract IGameSystem vfunc (slot-only, no func_sig needed) dispatched by IGameSystem_LoopPreShutdownAllSystems, which iterates all game systems and calls their GamePreShutdown vfunc via [rax+offset]. The dispatcher YAML stem is IGameSystem_LoopPreShutdownAllSystems.

Result: ida_preprocessor_scripts/find-IGameSystem_OnGamePreShutdown.py with:

from ida_preprocessor_scripts._igamesystem_slot_dispatch_common import (
    preprocess_igamesystem_slot_dispatch_skill,
)

DISPATCHER_YAML_STEM = "IGameSystem_LoopPreShutdownAllSystems"

TARGET_SPECS = [
    {
        "target_name": "IGameSystem_OnGamePreShutdown",
        "vtable_name": "IGameSystem",
        "dispatch_rank": 0,
    },
]

EXPECTED_DISPATCH_COUNT = 1

async def preprocess_skill(session, skill_name, expected_outputs, old_yaml_map,
                           new_binary_dir, platform, image_base, debug=False):
    _ = skill_name; _ = old_yaml_map; _ = image_base
    return await preprocess_igamesystem_slot_dispatch_skill(
        session=session,
        expected_outputs=expected_outputs,
        new_binary_dir=new_binary_dir,
        platform=platform,
        dispatcher_yaml_stem=DISPATCHER_YAML_STEM,
        target_specs=TARGET_SPECS,
        multi_order="index",
        expected_dispatch_count=EXPECTED_DISPATCH_COUNT,
        debug=debug,
    )

configs/<GAMEVER>.yaml:

      - name: find-IGameSystem_OnGamePreShutdown
        expected_output:
          - IGameSystem_OnGamePreShutdown.{platform}.yaml
        expected_input:
          - IGameSystem_LoopPreShutdownAllSystems.{platform}.yaml

configs/<GAMEVER>.yaml symbol entry:

      - name: IGameSystem_OnGamePreShutdown
        category: vfunc
        alias:
          - IGameSystem::GamePreShutdown

Output YAML (both platforms):

func_name: IGameSystem_OnGamePreShutdown
vtable_name: IGameSystem
vfunc_offset: '0x...'
vfunc_index: ...

Key insight -- Pattern K vs Pattern J vs Pattern F slot-only:

  • Pattern J (_igamesystem_dispatch_common): the target IS the callback function argument passed to IGameSystem_DispatchCall(...) -- a full concrete function with func_va, func_sig, etc. Use when the dispatch scan is done inside a game-loop function like CSource2Server_GameFrame.
  • Pattern K (_igamesystem_slot_dispatch_common): the target is an abstract vfunc offset extracted from a dedicated IGameSystem_Loop*AllSystems dispatcher. The dispatcher walks all game systems and calls their vfunc at a fixed [rax+offset]. Output is slot-only (no func_sig). Use when the dispatcher is one of the IGameSystem_Loop*AllSystems family.
  • Pattern F slot-only: use when a concrete override of the same slot already has a known vfunc_index in its output YAML. Pattern K is preferred when no concrete override YAML exists yet but the IGameSystem_Loop*AllSystems dispatcher YAML is available.

EXPECTED_DISPATCH_COUNT: Set to the number of unique vtable call sites in the dispatcher. For IGameSystem_Loop*AllSystems functions that dispatch exactly one vfunc, set it to 1. If the dispatcher has N unique [rax+offset] calls mapping to N different abstract vfuncs, set it to N and add all N targets to TARGET_SPECS with unique dispatch_rank values.

preprocess_skill signature note: Pattern K does NOT take an llm_config parameter (unlike Patterns C/D/E). The signature is:

async def preprocess_skill(session, skill_name, expected_outputs, old_yaml_map,
                           new_binary_dir, platform, image_base, debug=False):

Example: Constructor via vtable xref_gvs (Pattern A, dynamic FUNC_XREFS)

User says: Find CPlayerCommandQueue_ctor in server. It's the constructor -- identifiable as the function that writes the CPlayerCommandQueue vtable pointer. Find it via xref_gvs on CPlayerCommandQueue_vtable.

Result: Two scripts required (vtable first, then ctor):

  1. ida_preprocessor_scripts/find-CPlayerCommandQueue_vtable.py (vtable discovery, same as any other vtable script):

    • TARGET_CLASS_NAMES = ["CPlayerCommandQueue"]
  2. ida_preprocessor_scripts/find-CPlayerCommandQueue_ctor.py (Pattern A, dynamic FUNC_XREFS):

    • Imports os and yaml
    • _read_vtable_va() helper reads vtable_va from the vtable YAML
    • preprocess_skill builds func_xrefs at runtime with xref_gvs: [vtable_va]
    • Linux had 2 xref candidates; added exclude_signatures = ["66 83 ?? FF"] if platform == "linux" else []

configs/<GAMEVER>.yaml:

      - name: find-CPlayerCommandQueue_vtable
        expected_output:
          - CPlayerCommandQueue_vtable.{platform}.yaml

      - name: find-CPlayerCommandQueue_ctor
        expected_output:
          - CPlayerCommandQueue_ctor.{platform}.yaml
        expected_input:
          - CPlayerCommandQueue_vtable.{platform}.yaml

Key insight -- vtable VA is runtime-only: The vtable VA changes with every binary update, so it cannot be hardcoded. The _read_vtable_va() helper reads it from the vtable YAML written earlier in the same ida_analyze_bin.py run. The vtable skill must appear first in configs/<GAMEVER>.yaml (via expected_input) so it executes before the ctor skill.

Key insight -- multiple xref candidates: A vtable is typically written by the constructor AND sometimes by a destructor or copy constructor. If >1 function is found, the skill fails with "xref intersection yielded N function(s) (need exactly 1)". Read the first few bytes of each candidate in IDA, pick the non-constructor, and add an exclude_signatures entry. Use a platform conditional if the ambiguity only appears on one platform.

Example: Virtual function via xref_funcs (Pattern B, static FUNC_XREFS)

User says: Find CCSPlayerController_Connect in server. It's a vfunc of CCSPlayerController. It calls CPlayerCommandQueue_ctor internally, so use xref_funcs: ["CPlayerCommandQueue_ctor"].

Result: ida_preprocessor_scripts/find-CCSPlayerController_Connect.py with:

  • Static FUNC_XREFS -- no dynamic building needed, function name is known at write time
  • xref_funcs: ["CPlayerCommandQueue_ctor"]
  • FUNC_VTABLE_RELATIONS: ("CCSPlayerController_Connect", "CCSPlayerController")
  • GENERATE_YAML_DESIRED_FIELDS with func_name, func_sig, func_va, func_rva, func_size, vtable_name, vfunc_offset, vfunc_index

configs/<GAMEVER>.yaml:

      - name: find-CCSPlayerController_Connect
        expected_output:
          - CCSPlayerController_Connect.{platform}.yaml
        expected_input:
          - CPlayerCommandQueue_ctor.{platform}.yaml
          - CCSPlayerController_vtable.{platform}.yaml

Key insight -- expected_input for xref_funcs: The xref_funcs lookup resolves the callee by its IDA name. The callee is only renamed when its output YAML is written. Always list the callee's YAML in expected_input to guarantee it runs (and gets renamed in IDA) before this script executes. Without this ordering, the name lookup silently finds nothing and the skill fails.


Example: De-inlined helper via inline/noinline fallback chain (Pattern M)

User says: find-CNetworkGameServer_DirectUpdate stopped producing CNetworkGameServer_DirectUpdate.linux.yaml at 14168 -- CNetworkStringTableContainer::DirectUpdate (which owns the VProf string the finder anchors on) de-inlined out of the vfunc on Linux, so the string left the vtable member and the string-cap-vtable intersection went empty (then the fail-fast run aborted the rest of engine/linux).

Result: the single finder is replaced by a 3-skill chain:

  • find-CNetworkStringTableContainer_DirectUpdate -- helper; xref_strings on the string; optional_output + skip_if_exists; left unregistered as a gamedata symbol.
  • find-CNetworkGameServer_DirectUpdate-noinline -- xref_funcs: ["CNetworkStringTableContainer_DirectUpdate"]
    • CNetworkGameServer_vtable; optional_output + prerequisite the helper.
  • find-CNetworkGameServer_DirectUpdate-inlined -- the renamed original (xref_strings + vtable); expected_output + skip_if_exists + prerequisite the -noinline.

func_sig kept on both target paths (the de-inlined body is substantial). Validated 14167 (inlined)

  • 14168 (Linux de-inlined) x win/linux -> vtable index 59 / offset 0x1d8, Failed 0.

Full recipe (templates, configs/<GAMEVER>.yaml chain, func_sig keep/drop rule, validation, inverted-topology variant): see Pattern M.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Clean up the local workspace after a dev branch has been merged into main. Verifies the current dev branch is fully merged into origin/main, then switches to main, pulls the latest, deletes the local dev branch, and deletes the remote dev branch if it still exists. STOPS and warns the user if the current branch is not yet merged. Triggers: cleanup workspace, cleanup branch, delete merged branch, finish dev branch, post-merge cleanup

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

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.

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

Create an Agent SKILL.md fallback for an existing find-XXXX finder that relies on a fragile discovery foundation — above all LLM_DECOMPILE (patterns C/D/E), which matches the decompiled shape of a predecessor function against a stored reference and breaks when a symbol is inlined or de-inlined in a way the reference does not cover. The generated fallback coexists with the preprocessor and runs only when it returns failure, recovering every target robustly by decompiling the predecessor and following the inline/de-inline boundary with semantic anchors. Use when a finder broke on a game update, or you want to durably backstop one before it does. The recipe generalizes to any finder foundation. Triggers: create agent skill fallback, add SKILL.md fallback, robust fallback for finder, backstop LLM_DECOMPILE finder, final guarantee skill

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

Create a new cpp_tests entry for validating a C++ interface vtable layout against binary reference YAMLs. Creates the .cpp test file in cpp_tests/ and appends a configs/<GAMEVER>.yaml entry under cpp_tests:. Use when a user asks to add vtable layout validation for a new hl2sdk_cs2 interface class.

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

create-pr

無料

Create a GitHub pull request from staged task changes or an already-committed current branch. Deliver source/config/ reference changes together with their computed source-owned `bin_artifacts` closure. PR validation routing is owned by the default-branch trusted workflow; snapshots, gamedata, and manifests are Release-derived only.

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

Batch-dump vtables from IDA Pro MCP by searching mangled symbol patterns, then write a merged YAML file beside the binary. Use this skill when you need to find and export all vtables matching a name pattern (e.g., all GameSystem vtables) in one shot. Triggers: dump vtables, batch vtable dump, export vtables, dump all vtables matching pattern

日本語の概要は準備中です。原文の説明を表示しています。

mrc4tt/CS2_VibeSignatures32026年10月10日 更新

mrc4tt のスキルをすべて見る

このスキルの問題を報告する