# Create Preprocessor Scripts

> |

- **Type:** Skill
- **Install:** `agentstack add skill-hlnd2t-cs2-vibesignatures-create-preprocessor-scripts`
- **Verified:** Pending review
- **Seller:** [HLND2T](https://agentstack.voostack.com/s/hlnd2t)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [HLND2T](https://github.com/HLND2T)
- **Source:** https://github.com/HLND2T/CS2_VibeSignatures/tree/main/.claude/skills/create-preprocessor-scripts

## Install

```sh
agentstack add skill-hlnd2t-cs2-vibesignatures-create-preprocessor-scripts
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Create Preprocessor Scripts from Scratch

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

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

Eleven 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 |

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 I**
   - 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**

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 `config.yaml` skill entry.

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)

### 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,
    )
```

**config.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": [],
    },
]
```

**config.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`:

```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"]),
    ("SomeStruct_m_someField", ["struct_name", "member_name", "offset", "size", "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 config.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, f

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [HLND2T](https://github.com/HLND2T)
- **Source:** [HLND2T/CS2_VibeSignatures](https://github.com/HLND2T/CS2_VibeSignatures)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-hlnd2t-cs2-vibesignatures-create-preprocessor-scripts
- Seller: https://agentstack.voostack.com/s/hlnd2t
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
