# Task recipes — worked routes through common goals *Shard of mgsv-modding-pack — generated 2026-08-16. 5 files. Provenance is on every entry as `source:path`.* GUIDANCE, not retrieved fact. Each recipe is an authored route through a task that touches several shards, with the dead ends walked first and every factual claim cited to a shard you can verify. The ordering is a judgement call and can go stale; check the cited sources. Read the matching recipe before starting a task it covers — it can save you rediscovering a wall. ## Task recipes (local) — authored how-to routes, every claim cited ### `recipes_local:capabilities-verdicts.md` #### Capability verdicts — is this even possible? **Status:** GUIDANCE. A fast-path index of "can I do X from Lua" so you don't spend five fetches confirming a no. Each verdict cites where it's established; fetch that only if you need the detail. If a capability isn't listed, it isn't answered here — check `findings` and the lookup tables. | I want to... | Lua? | verdict / native path | source | |---|---|---|---| | Get the surface material under the player | **NO** | native only — `GetMaterialIndexLegL`/`R` exist in the exe (bucket 007), unbound | `findings` neg-caps | | Get the texture/material under the reticle | **NO** | native only — no IHHook hit-getter | `findings` neg-caps, `recipes` | | Raycast / line-of-sight hit result | **NO** | `SearchObjectLine` exists but args uncaptured — unsafe | `findings` SearchObjectLine | | Know *if* sight is blocked (boolean) | **YES** | `CheckSight()` → bool | `lua-functions` | | Swap a texture on disk | **YES** | asset swap via modbldr-tools | `recipes` swap-a-texture | | Swap a texture at runtime from Lua | **NO** | `GrTools.SetMaterialTexture` is a dead stub + setter | `findings` GrTools | | Read player position/rotation | **YES** | `vars.playerPosX/Y/Z`, `playerRotY` | `reference` lua-input | | Bind a hotkey | **YES** | `InfButton.OnButtonDown(mask)` — not `IsButtonDown` | `reference` lua-input | | Show on-screen text | **YES** | `AnnounceLogView(string)` free text; `...LangId` for ids | `reference` lua-input | | Get what animal a capture cage caught (+ its material) | **PARTIAL** | `GetCaptureCageInfo()` returns records w/ `.material`; ground-link unconfirmed | `findings` capture-cage | | Check if a collected resource is a crafting material | **YES** | `TppCollection.IsMaterialByType` — NOT a surface check | `findings` neg-caps | | Declare a setting the user can change | **YES** | ivars — full schema in `ih-contract` | `ih-contract` | | Find a specific weapon's sound file (shotgun, minigun, tank) | **PARTIAL** | no per-weapon file — it's a Wwise event inside an `se_*`/`vehicle_*` bank; use `sound-assets` table then open the bank | `sound-assets`, `techniques` | | Positional / 3D HRTF audio for game sounds | **PARTIAL — proven** | verified working chain exists (Fox world XYZ + listener + pre-mix PCM → HRTF); native, build-specific | `audio-positional` | ##### The pattern behind the noes Every "NO (Lua)" above has the same shape: the value exists in the engine, is computed natively, and is simply never surfaced to script. So a "no" is really "native binding required" — expose the getter through IHHook / V Framework. When you hit one, the next move is the exe symbol, not a workaround. ### `recipes_local:identify-what-player-looks-at.md` #### Recipe: identify what the player is looking at **Status:** GUIDANCE, not retrieved fact. A recipe is an authored route through a task — the reasoning and ordering are a judgement call and can go stale when IH or the game updates. Every *claim* below is backed by a shard you can verify; the *sequence* is the opinion. Check the cited sources before trusting a step. **Goal:** in-game, report what the player's camera/reticle is pointed at. **Verdict up front:** the exact texture/material under the reticle is **not obtainable** with current tools. Build the marker fallback instead. The rest of this recipe is how to reach the closest safe result without rediscovering that wall. ##### Stop here first — what's impossible Before writing anything, know the dead ends so you don't design around them: - **No IHHook hit-getter.** The verified `IHH` Lua surface has no raycast, hit-result, material, model or texture getter. (`native-hooks`; and the "What the Lua/IHHook API cannot do" entry in `findings`.) - **The raycast primitive exists but is unusable.** `GeoSearchService.SearchObjectLine` is real, but its arguments were never captured and nothing calls it, so calling it is guesswork. (`findings`: "SearchObjectLine: the raycast exists, its arguments don't".) - **`CheckSight` is boolean only** — tells you *if* sight is blocked, not *what* blocks it. (`lua-functions`.) - **`GrTools` texture calls are dead retail stubs, and setters anyway.** (`findings`: GrTools entry.) So the answer to "texture under reticle" is: not available. Deliver the fallback and label the texture/material/model fields UNAVAILABLE. ##### The fallback, in order 1. **Camera pose.** Read camera position and rotation — the verified camera variables. This is the "where is the player aiming" half. (`lua-api`.) 2. **Newest marker as the target.** A manually placed binocular/map marker exposes a zero-based latest-marker record: `gameObjectId` and position. That's the "what is out there" half, standing in for the raycast hit. (`ih-framework`, marker module.) 3. **Resolve a name — throttled.** IH's reverse lookup turns a `gameObjectId` into an entity name/type. IH itself warns this is expensive, so run it only on marker change or on sample, never every frame. (`ih-framework`.) 4. **Output.** Short on-screen line via `TppUiCommand.AnnounceLogView`; full record to `IHH.Log`. (`techniques`, `native-hooks`.) 5. **Be honest in the output.** Print texture / material / model as UNAVAILABLE with a one-line reason, so the user knows it's a fallback, not a failed raycast. ##### Module shape Standard IH module (see the `ih-contract` shard for the full contract): `registerIvars` for an enable toggle and a sample key, `registerMenus` to wire them, a `this.Update` gated on the enable ivar as its first line, and the name lookup fired from a key press or marker-change rather than the update loop. ##### What would remove the fallback Only new native work: a Ghidra pass recovering `SearchObjectLine`'s signature (origin, direction, length, mask, hit-result layout), which would turn step 2 from a marker into a real reticle raycast. Until that exists, the marker fallback is the correct answer, not a workaround for a missing doc. ### `recipes_local:package-a-mod.md` #### Recipe: package a mod into a .mgsv **Status:** GUIDANCE, not retrieved fact. The metadata.xml template and version matrix are in the `packaging` shard; this orders them into a build. Confirm the versions against the IH release you build for. **Goal:** turn a finished mod folder into an installable SnakeBite `.mgsv`, or install it loose. ##### Two ways to install - **Loose (fastest for testing):** merge your `GameDir` into `MGS_TPP`. No packaging step. Good for iterating; not for distribution. (`packaging`.) - **Packaged (.mgsv, for distribution):** run the package directory through MakeBite. This is the shareable artifact. (`packaging`.) ##### Building the .mgsv 1. **Lay out the tree.** Your files sit under `GameDir` mirroring their in-game paths — e.g. a module at `GameDir\mod\modules\YourMod.lua`. (`packaging`.) 2. **Write metadata.xml.** Copy the real template from the `packaging` shard and fill in Name, Version, Author. Keep the structure — `MGSVersion` is the exe version (e.g. 1.0.15.4, the "1.15" retail patch), `SBVersion` is the SnakeBite version, and `QarEntries`/`FpkEntries` self-close for loose-file mods; MakeBite fills them on build. The file is UTF-8 with a BOM; keep it. (`packaging`.) 3. **Confirm the version matrix.** Match your metadata to the target: MGSV exe 1.0.15.4, SnakeBite 0.9.2.3, IH r266 era, IHHook r17 — but these move, so confirm against the IH release you build for rather than trusting the matrix blindly. (`packaging`.) 4. **Run MakeBite** on the package directory to produce the `.mgsv`. 5. **Install via SnakeBite** and test in-game. Note that SnakeBite unpacks and repacks large data files, so install is not instant. (`wiki`, Chelly's guide.) ##### Load order and conflicts Two mods that touch the same packed file conflict — last one packed wins, with no warning. If a change "doesn't apply," suspect either a conflict or the game's archive cache not being invalidated before ruling out your mod. (Hard rules.) ##### Native hooks are separate If your mod includes a native component (an IHHook build, a DLL), that is not a SnakeBite package — it ships as a proxy DLL alongside the game, and binds to specific exe offsets, so it is version-locked in a way the .mgsv is not. (`native-hooks`; `findings` IHHook/DXVK entry for what that binding involves.) ### `recipes_local:swap-a-texture.md` #### Recipe: swap a texture in the game **Status:** GUIDANCE, not retrieved fact. Claims are cited; the route is a judgement call — verify against the cited shards before trusting it. **Goal:** replace a texture the game renders (a character, a weapon, terrain). ##### The one thing to understand first There are two completely different meanings of "swap a texture," and picking the wrong one wastes days: - **Asset swap (this recipe):** replace the `.ftex` file on disk, repack, ship. This is how essentially every texture mod works. Supported, well-trodden. - **Runtime swap (don't):** call a Lua function to change a material's texture while the game runs. `GrTools.SetMaterialTexture` looks like exactly this — it is a dead retail stub wired to nothing, and a setter with no matching getter. (`findings`: GrTools entry.) Do not build on it. So "swap a texture" means edit the file, not call an API. ##### The asset-swap route 1. **Find the real path.** Use `paths-tpp-full` (has extensions) or `paths-qar` to get the exact `.ftex` path — do not construct one, a wrong path hashes to a wrong PathCode64 and fails silently. For a character, the folder is the character code: Quiet is `qui`, not the display name. (`texture-paths` lookup table description.) 2. **Extract it.** `modbldr-tools ` to unpack the containing qar/fpk. Keep the `.ftex` together with its numbered `.ftexs` mip sidecars — they are one texture split across files. (Hard rules; `formats`.) 3. **Convert to DDS.** `modbldr-tools` on the `.ftex` produces a `.dds` plus a `.ftex.json` sidecar holding the header metadata. **Keep the json** — it is how the header survives the round-trip. (Hard rules.) 4. **Edit the DDS**, preserving format and mip count where you can. 5. **Convert back**, letting the `.ftex.json` restore the header, and **diff the result against the original** before trusting it — silent byte corruption is caught here or not at all. 6. **Repack and package.** Repack the archive with `modbldr-tools`, then package per the "package a mod to .mgsv" recipe. ##### Where it goes wrong - Editing the `.ftexs` instead of the parent `.ftex`. Work on the `.ftex`. - Dropping the `.ftex.json`, so the header is wrong and the game renders it broken. - A constructed path that looks right and hashes wrong — always look it up. ### `recipes_local:write-an-ih-module.md` #### Recipe: write an Infinite Heaven module from scratch **Status:** GUIDANCE, not retrieved fact. The module contract is verified in the `ih-contract` shard; this is the contract turned into an ordered walkthrough. Verify against that shard, and against the actual IH release you target — the internal API is not stable across versions. **Goal:** a working IH module that adds a feature, with a menu toggle and a gated update loop. ##### The skeleton An IH module is a Lua file in `\mod\modules\` that returns a table. The non-negotiable is the last line. (`ih-contract`.) ```lua local this = {} this.debugModule = false this.registerIvars = { } -- your settings live here this.langStrings = { } this.registerMenus = { } function this.Init() end -- once, early; nothing cross-module here function this.PostAllModulesLoad(isReload) end -- other modules exist now; do setup here function this.PostModuleReload(prevModule) end -- carry state across a live reload function this.Update(currentChecks, currentTime, execChecks, execState) end return this -- omit this and the module loads as nil with NO error ``` ##### Order of operations 1. **Return the table.** Before anything else, make sure `return this` is the last line. Its absence is the single most common silent failure. (`ih-contract`.) 2. **Declare an enable ivar** in `registerIvars`, wire it to a menu in `registerMenus`, read it as `ivars.`. Don't invent a config file for what ivars already do. (`ih-contract`.) 3. **Do cross-module setup in `PostAllModulesLoad`, not `Init`.** `Init` runs before the rest of the module set exists, so anything depending on another module belongs in `PostAllModulesLoad`. (`ih-contract`.) 4. **Gate `Update` on your enable ivar as its first line:** `if ivars. == 0 then return end`. `Update` runs constantly; unconditional work here is the main way modules tank frame time. (`ih-contract`.) 5. **Preserve state across reload.** IH reloads modules live. Anything you want to survive a reload must be copied off `prevModule` in `PostModuleReload` — what you don't copy is gone. This is the usual cause of "works until I reload." (`ih-contract`.) 6. **For data, not behaviour, use addon infos, not a module.** Locations, missions, quests, body/fova info and weapon id tables are declared as info scripts in their own `mod\\` folder and picked up by the owning IH module — adding a location does not need a module. (`ih-contract`.) ##### Verifying it loaded Fox Engine fails silently, so add deliberate logging gated on `this.debugModule`, and print on-screen via `TppUiCommand.AnnounceLogView` / to `IHH.Log` when you need to confirm a code path ran. (`techniques`, `native-hooks`.) ##### When you need a value the API doesn't expose Check the `findings` negative-capability list before assuming a getter exists — several plausible ones (hit-to-texture, reticle raycast, surface material) do not. If your feature needs one of those, it is native work, not a module.