Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Create Preprocessor Scripts

ASecurity

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.

3 stars
0 votes
0 copies
0 views
Added 9/27/2026
businesspythonrustgobashgitapi

Works with

cliapimcp

Security Analysis

A100/100

Scanned 9/27/2026

Install to Claude Code

$npx -y skills add mrc4tt/CS2_VibeSignatures --skill create-preprocessor-scripts --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Create Preprocessor Scripts?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Create Preprocessor Scripts
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mrc4tt-create-preprocessor-scripts/badge)](https://www.skillsdirectory.com/skills/mrc4tt-create-preprocessor-scripts)

More formats (shields.io, HTML) on the badges page.

Download with Pro
Files
SKILL.md
---
name: create-preprocessor-scripts
description: |
  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.
disable-model-invocation: true
---

# 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:

| Field | Description | Example |
|-------|-------------|---------|
| **Function name(s)** | Target symbol(s) to find | `CPlayer_MovementServices_PlayWaterStepSound` |
| **Module** | Which DLL/SO the function lives in | `server`, `engine`, `networksystem`, `client` |
| **Category** | Symbol type | `func`, `vfunc`, `structmember`, `patch`, `vtable` |
| **xref_strings** | Debug strings for xref-based discovery | `"CT_Water.StepLeft"` |
| **xref_gvs** | Global variable VA (e.g. vtable address) to find functions that reference it | vtable VA from `SomeClass_vtable.{platform}.yaml` |
| **xref_funcs** | Known callee function name to find its callers | `"CPlayerCommandQueue_ctor"` |
| **Predecessor function** | Function to decompile for LLM_DECOMPILE patterns | `CBaseEntity_TakeDamageOld` |
| **VTable class** | Class owning the vtable (for vfuncs) | `CBasePlayerPawn` |
| **Desired YAML fields** | Which fields the output YAML needs | `func_name, func_sig, func_va, func_rva, func_size` |
| **Dependencies** | Input YAMLs this skill depends on | `CCSPlayer_MovementServices_vtable.{platform}.yaml` |
| **Aliases** | Alternative names for the symbol | `CPlayer_MovementServices::PlayWaterStepSound` |

## Overview

Twelve preprocessor patterns exist. The 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 (standard); or slot-only mode for abstract/interface vfuncs where only offset/index is needed | 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 |
| **I** -- Interface vfunc offset via thunk instruction walk | Walk a known concrete-class thunk via `py_eval` + `idaapi.decode_insn`, extract `jmp [reg+disp]` displacement as vfunc_offset | No | No | No | No | No |
| **J** -- IGameSystem vfunc via dispatch scan | Scan `IGameSystem_DispatchCall(idx, callback, ...)` call sites in a known predecessor; map targets by scan/index order using `_igamesystem_dispatch_common` | No | No | No | No | No |
| **K** -- IGameSystem vfunc via slot dispatch scan | Walk 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`) | No | No | No | No | No |
| **L** -- Interface vfunc slot via indirect vcall scan | Scan 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 I | 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: 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:

- [Pattern A -- Regular function via xref strings](references/pattern-A.md)
- [Pattern B -- Virtual function via xref strings](references/pattern-B.md)
- [Pattern C -- Virtual function via LLM_DECOMPILE](references/pattern-C.md)
- [Pattern D -- Regular function via LLM_DECOMPILE](references/pattern-D.md)
- [Pattern E -- Struct member offset via LLM_DECOMPILE](references/pattern-E.md)
- [Pattern F -- Virtual function via INHERIT_VFUNCS](references/pattern-F.md) (standard + slot-only variant)
- [Pattern G -- ConCommand handler function](references/pattern-G.md)
- [Pattern H -- Secondary (ordinal) vtable](references/pattern-H.md)
- [Pattern I -- Interface vfunc offset via thunk walk](references/pattern-I.md)
- [Pattern J -- IGameSystem vfunc via dispatch scan](references/pattern-J.md)
- [Pattern L -- Interface vfunc slot via indirect vcall scan (reusable)](references/pattern-L.md)
- [Pattern M -- Inline/noinline fallback chain (de-inlined helper)](references/pattern-M.md)

### 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.

```python
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`

```python
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:

```python
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.

```python
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):

```yaml
        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.

```python
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

| Aspect | Pattern 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_XREFS | Yes | Yes | No | No | No | No | No (uses COMMAND_NAME/HELP_STRING) | No | No | No | No | No |
| FUNC_VTABLE_RELATIONS | No | Yes | Yes | No | No | No | No | No | No | No | No | No |
| INHERIT_VFUNCS | No | No | No | No | No | Yes | No | No | No | No | No | No |
| LLM_DECOMPILE | No | No | Yes | Yes | Yes | No | No | No | No | No | No | No |
| `llm_config` param | No | No | Yes | Yes | Yes | No | No | No | No | No | No | No |
| Helper module | `preprocess_common_skill` | `preprocess_common_skill` | `preprocess_common_skill` | `preprocess_common_skill` | `preprocess_common_skill` | `preprocess_common_skill` | `preprocess_registerconcommand_skill` | `preprocess_ordinal_vtable_via_mcp` | `py_eval` + `write_func_yaml` (custom) | `preprocess_igamesystem_dispatch_skill` | `preprocess_igamesystem_slot_dispatch_skill` (from `_igamesystem_slot_dispatch_common`) | `preprocess_indirect_vcall_target_skill` (from `_indirect_vcall_target_common`) |
| Target list | `TARGET_FUNCTION_NAMES` | `TARGET_FUNCTION_NAMES` | `TARGET_FUNCTION_NAMES` | `TARGET_FUNCTION_NAMES` | `TARGET_STRUCT_MEMBER_NAMES` | (none -- defined in INHERIT_VFUNCS) | `TARGET_FUNCTION_NAMES` | `TARGET_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 param | `func_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 fields | func_name, func_sig, func_va, func_rva, func_size | Same + vtable_name, vfunc_offset, vfunc_index | **vfunc_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_name | func_name, func_sig, func_va, func_rva, func_size | struct_name, member_name, offset, offset_sig, offset_sig_disp; add size only for a non-`lea` memory access | Standard: 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_index | func_name, func_sig, func_va, func_rva, func_size | (vtable YAML via write_vtable_yaml) | func_name, vtable_name, vfunc_offset, vfunc_index | func_name, func_va, func_rva, func_size, func_sig, vtable_name, vfunc_offset, vfunc_index | func_name, vtable_name, vfunc_offset, vfunc_index | func_name, vtable_name, vfunc_offset, vfunc_index |
| config category | `func` | `vfunc` | `vfunc` | `func` | `structmember` | `vfunc` | `func` | `vtable` | `vfunc` | `vfunc` | `vfunc` | `vfunc` |

---

## 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:**

```yaml
      - 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):

```yaml
      # 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).

```yaml
      # 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:

```yaml
      - 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`:

```bash
# 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:

```bash
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:

```bash
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:
  ```bash
  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:

```bash
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`:

```bash
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`:

```bash
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:

```bash
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:**
```yaml
      - 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:**
```yaml
      - 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:**
```yaml
      - 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:**
```yaml
      - name: find-ILoopMode_HandleInputEvent
        expected_output:
          - ILoopMode_HandleInputEvent.{platform}.yaml
        expected_input:
          - CLoopTypeClientServerService_HandleInputEvent.{platform}.yaml
```

**Output YAML (both platforms):**
```yaml
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):**
```yaml
      - name: find-ILoopMode_LoopInit
        expected_output:
          - ILoopMode_LoopInit.{platform}.yaml
        expected_input:
          - CLoopModeGame_LoopInit.{platform}.yaml
```

**Output YAML:**
```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):**
```yaml
      - 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):**
```yaml
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:
```c
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`

```python
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:**
```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:

```python
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:**
```yaml
      - name: find-IGameSystem_OnGamePreShutdown
        expected_output:
          - IGameSystem_OnGamePreShutdown.{platform}.yaml
        expected_input:
          - IGameSystem_LoopPreShutdownAllSystems.{platform}.yaml
```

**configs/<GAMEVER>.yaml symbol entry:**
```yaml
      - name: IGameSystem_OnGamePreShutdown
        category: vfunc
        alias:
          - IGameSystem::GamePreShutdown
```

**Output YAML (both platforms):**
```yaml
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:
```python
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:**
```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:**
```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](references/pattern-M.md).

Attribution

mrc4ttmrc4tt
View sourceMore from mrc4tt →
SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Related Skills

Solution Architect

Designs system architecture, component specifications, and technical integration strategy. Use when: designing solutions, system architecture, technology stack, or integration approaches.

192 votes

Akorchak:Venture Assessment

Generate a comprehensive VC investment assessment report for a company

72 votes

Stock Analysis

Analyze stocks and cryptocurrencies using Yahoo Finance data. Supports portfolio management (create, add, remove assets), crypto analysis (Top 20 by market cap), and periodic performance reports (daily/weekly/monthly/quarterly/yearly). 8 analysis dimensions for stocks, 3 for crypto. Use for stock analysis, portfolio tracking, earnings reactions, or crypto monitoring.

6511 votes

Just Fucking Cancel

Find and cancel unwanted subscriptions by analyzing bank transactions. Detects recurring charges, calculates annual waste, and helps you cancel with direct URLs and browser automation. Use when: 'cancel subscriptions', 'audit subscriptions', 'find recurring charges', 'what am I paying for', 'save money', 'subscription cleanup', 'stop wasting money'. Supports CSV import (Apple Card, Chase, Amex, Citi, Bank of America, Capital One, Mint, Copilot) OR Plaid API for automatic transaction pull. Out...

6511 votes

Telegram Compose

Compose rich, readable Telegram messages using HTML formatting via direct Telegram API. Use when: (1) Sending any Telegram message beyond a simple one-line reply, (2) Creating structured messages with sections, lists, or status updates, (3) Need formatting unavailable via Clawdbot's Markdown conversion (underline, spoilers, expandable blockquotes, user mentions by ID), (4) Sending alerts, reports, summaries, or notifications to Telegram, (5) Want professional, scannable message formatting wit...

6511 votes
View all in business →