# Hard-won findings — things that cost days *Shard of mgsv-modding-pack — generated 2026-08-14. 6 files. Provenance is on every entry as `source:path`.* Solved problems that exist in no repository. Each entry is symptom -> evidence -> dead ends -> root cause -> fix -> confirmation. Read this BEFORE starting any native-hook, injection or toolchain investigation; a match here can save days. ## Findings — solved problems that exist in no repository ### `findings:capture-cage-ground-material.md` #### Ground material readout via GetCaptureCageInfo — the return shape is verified **Domain:** Lua API / gameplay · **Confidence:** the return shape is now VERIFIED from vanilla source; whether it reflects the ground the cage sits on is the one remaining SUSPECTED step, settled by an in-game test. ##### The lead There is no direct Lua call for "material under the player" (see negative-capabilities). But `TppPlaced.GetCaptureCageInfo()` returns per-cage records that include a **material** field, and the runtime exposes a `CaptureCageMaterial` enum of 78 surface names. A placed cage may therefore be a usable ground-material probe. ##### What is now VERIFIED (from vanilla TppPlayer.lua, not inference) The corpus copy elides the body, but the deminified tree has the real file (`ih_kap:tpp/data1_dat-lua/Assets/tpp/script/lib/TppPlayer.lua`). At lines 2845 and 2892 the vanilla game does: ```lua local cageInfo = TppPlaced.GetCaptureCageInfo() for _, a in pairs(cageInfo) do this.EvaluateCaptureCage(a.x, a.z, a.grade, a.material) end ``` So the return shape is settled: - `GetCaptureCageInfo()` takes **no arguments** and returns a **list**, iterated with `pairs()`. - Each record has `.x`, `.z`, `.grade`, and `.material`. - `.material` is passed into `EvaluateCaptureCage` and used there as a lookup key (`animalMaterial[...]`), consistent with it being a `CaptureCageMaterial` enum value. This is no longer "inferred from name" — the field names and call shape are copied from shipping code. ##### The one remaining unknown Whether `.material` reflects the **ground the cage was placed on** (making this a ground probe) or is just cage metadata. Vanilla uses it to decide which animals a cage catches, which is plausibly surface-driven, but that is not proof. The in-game test settles it: place a cage on sand, read `.material`; place one on concrete, read again. Different values that resolve to `MTR_SAND_*` / `MTR_CONC_*` confirm it. Same value on both kills the lead cheaply. ##### The reverse map `CaptureCageMaterial` is a lazy luaext proxy — `pairs()` over it returns nothing (see lua-reference-notes), so resolve names by direct index. The 78 members: ``` MTR_ALRM_A MTR_BRIC_A MTR_CLOT_A MTR_CLOT_B MTR_CLOT_C MTR_CLOT_D MTR_CLOT_E MTR_CONC_A MTR_CONC_B MTR_COPS_A MTR_COPS_B MTR_FENC_A MTR_FENC_B MTR_FENC_F MTR_FWOD_A MTR_GLAS_A MTR_GLAS_B MTR_GLAS_C MTR_GRAV_A MTR_IRON_A MTR_IRON_B MTR_IRON_C MTR_IRON_D MTR_IRON_E MTR_IRON_F MTR_IRON_G MTR_IRON_M MTR_IRON_N MTR_IRON_W MTR_LEAF MTR_MOSS_A MTR_PAPE_A MTR_PAPE_B MTR_PAPE_C MTR_PAPE_D MTR_PIPE_A MTR_PIPE_B MTR_PIPE_S MTR_PLAS_A MTR_PLAS_B MTR_PLAS_W MTR_PLNT_A MTR_RLEF MTR_RLEF_B MTR_ROCK_A MTR_ROCK_B MTR_ROCK_P MTR_RUBB_A MTR_RUBB_B MTR_SAND_A MTR_SAND_B MTR_SAND_C MTR_SOIL_A MTR_SOIL_B MTR_SOIL_C MTR_SOIL_D MTR_SOIL_E MTR_SOIL_F MTR_SOIL_G MTR_SOIL_H MTR_SOIL_R MTR_SOIL_W MTR_TILE_A MTR_TIN_A MTR_TLRF_A MTR_TURF_A MTR_VINL_A MTR_VINL_W MTR_WATE_A MTR_WATE_B MTR_WATE_C MTR_WOOD_A MTR_WOOD_B MTR_WOOD_C MTR_WOOD_D MTR_WOOD_G MTR_WOOD_M MTR_WOOD_W ``` Build `value -> name` by indexing each explicitly, then annotate whatever `.material` holds. ### `findings:grtools-stubbed.md` #### GrTools texture API exists in Lua and is entirely stubbed **Domain:** rendering / Lua API · **Confidence:** VERIFIED against MockFox and the Lua reference. ##### The trap Someone reasonably concludes that MGSV has no Lua way to swap a material's texture, because the obvious material interface doesn't offer it. MockFox's `MaterialInfo` exposes only `SetFrictionCoefficient`, `SetRestitutionCoefficient` and `SetName` — physics properties, no texture assignment. `Ph` in the sibling `PhMaterialInfo` gives it away: that is a *physics* material, friction and restitution, not a render material. The wrong conclusion drawn from that is "no texture-swap API exists, so this must be done natively from scratch." Half right. ##### What actually exists The texture-assignment API is present and reachable from Lua: - `GrTools.SetMaterialTexture` - `GrTools.SetTerrainMaterialTexture` - `GrTools.SetMaterialParamBinary` - `TerrainRender.ExportPackedMaterialTexture` - `TerrainRender.ClearPackedMaterialTexture` `SetMaterialTexture` is exactly the call you would want. It is listed in MockFox and present in a live session. ##### Why it doesn't work anyway `GrTools` is 53 functions, present in the live session, and **every one is wired to a single dead stub in both the retail and dev executables** (VERIFIED — this is stated in the TPP Lua reference, and confirmed by the functions all resolving to one address). Registered, callable, silently does nothing. Calling `GrTools.SetMaterialTexture` raises no error and has no effect — the archetypal Fox Engine silent failure. So the practical answer matches the wrong conclusion, but for the opposite reason: not "the API is absent" but "the API is present and inert." ##### Why the distinction matters "No API exists" sends you hunting for a Lua path that isn't there. "The API exists and is stubbed" tells you the native layer has to **re-point GrTools' binding** to live code — the stub is the thing to replace. That is hook work at the same layer as IHHook / Fox_Parser texture-override, not a search for a missing Lua function. ##### The one live exception `TerrainRender` is 2 functions, present in the live session, and is **not** in the stubbed set. Whether `ExportPackedMaterialTexture` actually does something is untested here. If it is live it is a real, terrain-only handle on packed material textures — worth ten minutes with a live session before assuming it is as dead as GrTools. ##### There is also no Lua route to a surface material id Related dead end: there is no Lua call that returns the material under the player. MockFox has `Geo.RegisterMaterial`/`UnregisterMaterial` (registration), `PhDaemon.SetCollision*` (setters) and `TppCollection.IsMaterialByType` (a predicate) — no raycast, ground query or surface probe that yields an id. Any working "what material is the player on" code is therefore native, reading a physics-material field off a collision result. Which is convenient: if you are already native for that, you are already at the layer where the GrTools stub can be replaced. ### `findings:ihhook-dxvk-dummy-device.md` #### IHHook fails to load when DXVK is present **Domain:** native hooks / injection · **Cost:** several sessions **Confidence:** VERIFIED at both endpoints (symptom and fix reproduced from logs); the middle of the diagnosis is reconstructed and marked below. ##### Symptom Infinite Heaven does nothing. No menu, no crash. `ihhook_log.txt` shows the hook attempt failing at the earliest possible moment: ``` info: Hooking D3D11 info: Creating dummy D3D11 device. error: Failed to create dummy D3D11 device. HRESULT=-7fffbffb max_feature=0 ``` Context: MGSV routed through DXVK (`d3d11.dll` / `dxgi.dll` in the game folder), IHHook loaded as `dinput8.dll`. ##### Reading the evidence - `HRESULT=-7fffbffb` is `0x80004005` — `E_FAIL`, the generic "unspecified failure". The log prints it signed; convert before assuming it's exotic. - `max_feature=0` means the feature level was never negotiated at all. The failure is inside device creation, before any swapchain or window handle is touched. - `0x80004005` matches DXVK's own top-level handler in `D3D11CreateDevice`: `catch (const DxvkError& e) { ...; return E_FAIL; }`. So DXVK threw an internal error — this is not IHHook doing something malformed. `debugMode` in IHHook is on by default, so this detail is already in the log. Don't rebuild to add logging before reading what's there. ##### Dead ends - **"It's a load-order problem."** Too generic to test, and it wasn't. - **Reasoning from IHHook's source alone.** The log had the answer; source reading produced a plausible theory that the evidence then contradicted. - **Mid-investigation, `max_feature=0` was read as ruling out the window-handle theory** — on the grounds that it fails earlier than the swapchain step. SUSPECTED: that inference was too strong, since the window-handle fix is what ultimately worked. Treat `max_feature=0` as "feature level never got set", not as a reliable ordering claim about which line threw. ##### Root cause IHHook creates a throwaway D3D11 device plus swapchain to obtain the vtable it patches, and passes `GetDesktopWindow()` as the target window. DXVK rejects a swapchain built against the desktop window handle and fails the whole device-creation call. ##### Fix In `D3D11Hook.cpp`, replace `GetDesktopWindow()` with a **message-only window** for the dummy swapchain, then rebuild. Two unrelated blockers surface when rebuilding on a current toolchain (VS2026, v145, Windows SDK 10.0.26100.0): - `stdext::checked_array_iterator` has been removed from newer MSVC STL. - `Hooking.Patterns.h` relies on an include it no longer gets transitively; add the explicit `#include`. ##### Confirmation A clean run looks like this — check for all four lines, not just the last: ``` info: Hooking D3D11 info: Creating dummy D3D11 device. info: Created dummy D3D11 device. HRESULT=0 max_feature=b000 info: Hooked D3D11 ``` `HRESULT=0` is `S_OK`; `max_feature=0xb000` is `D3D_FEATURE_LEVEL_11_0`. The hook installing is not the same as the feature working — look further down for `Initializing ImGui D3D11` and real menu population (`SetContent`, `AddToTable`), and for `DLL_PROCESS_DETACH` rather than a crash at exit. ##### Transferable lesson MGSV has active anti-tamper that blocks generic D3D11 injectors (ReShade, 3Dmigoto, RenderDoc all fail or crash against it). Approaches that work route around the D3D11 layer rather than through it — DXVK translating to Vulkan, with a Vulkan implicit layer intercepting `vkCreateSwapchainKHR`, `vkGetSwapchainImagesKHR` and `vkQueuePresentKHR`. When a second thing also wants to hook D3D11 in that process, expect it to interact with DXVK's device creation, and read *its* log before theorising. ### `findings:negative-capabilities.md` #### What the Lua/IHHook API cannot do A running list of capabilities people reach for that **do not exist**, so the next person stops at the wall instead of searching for a door that isn't there. Each is VERIFIED against the pack unless noted. "Doesn't exist" here means: not in the verified IHHook export list, not in MockFox, not in the Lua reference. ##### Rendering / hit inspection - **No reticle/hit-to-texture getter.** Nothing in the `IHH` export list returns the texture under the crosshair. VERIFIED — the full IHHook Lua surface is in `native-hooks`; it has no raycast, hit-result, material, model or texture getter. - **No hit-to-material or hit-to-model getter.** Same source, same absence. - **`GrTools.SetMaterialTexture` exists but is a dead retail stub AND a setter.** Even if it were live it sets, it doesn't read. See the GrTools findings entry. - **No Lua call returns the surface material under the player.** Only registration (`Geo.RegisterMaterial`), setters (`PhDaemon.SetCollision*`) and a predicate (`TppCollection.IsMaterialByType`). **FALSE FRIEND:** the name reads like a surface-material check but it is not — its only vanilla call site (`TppPlayer.lua:2594`) sits beside `IsHerbByType`/`IsDiamondByType` and fires `find_processed_res`. It tests whether a **collectible resource** is a crafting material, nothing to do with the ground. Do not probe it for surface type. Any working "material under player" code is native. ##### Line-of-sight / raycast - **`GeoSearchService.SearchObjectLine` exists but its arguments are uncaptured.** MockFox shows `SearchObjectLine=function(...)end` — the `...` means parameters were never recorded, and it is called nowhere in IH or vanilla Lua, so there is no example to copy. Calling it is guesswork until someone decompiles it. See its own findings entry. - **`TppSightObstructionManager.CheckSight()` returns only a boolean.** It tells you whether sight is blocked, not what blocked it. VERIFIED in the Lua reference. ##### The pattern When the ask is "identify the thing the player is looking at / standing on," the verified surface gives you *booleans and setters, not hit-result getters*. The correct fallback is usually the marker system: a manually placed binocular/map marker exposes `gameObjectId`, position and (via IH's reverse lookup) a name. That's a fallback, not the raycast — say so. ##### The data exists natively (just unbound) "Impossible in Lua" is not "impossible". The exe carries the footstep-material machinery — the footstep material-index getters are in `exe-symbols` (VERIFIED present): **`GetMaterialIndexLegL` and `GetMaterialIndexLegR`** (bucket 007) — the per-foot pair — plus `GetRightLegMaterialIndex` (bucket 008). Use the `GetMaterialIndexLegL`/`R` names when you go looking; they are the ones that read cleanly as the left/right footstep pair. The value is computed every footstep; it is simply never surfaced to script. So a ground-material readout is a **native binding** job (expose the getter through IHHook/V Framework), not an impossibility. That is the actionable form of the verdict: not "no", but "native only, and here's the symbol". ### `findings:no-per-weapon-soundbanks.md` #### Weapon sounds are Wwise events, not per-weapon files **Domain:** audio / assets · **Confidence:** VERIFIED from the sound-asset path list. ##### The trap Asked for "the sound files of the suppressed shotgun / chopper minigun / tank weapons", the natural move is to grep the path dictionaries for `shotgun`, `minigun`, `tank`. **That returns nothing, and the nothing is misleading** — the sounds absolutely exist, they're just not named that way. ##### What's actually there Under `/Assets/tpp/sound/asset/` there are ~211 soundbanks, grouped by prefix: - `bgm_*` — music - `env_*` — ambience - `se_*` — sound effects. `se_e_*` are enemy SFX (`se_e_btlg`, `se_e_mgs`…), `se_b_*` boss, `se_s*` per-mission. - `vox_*` — voice - `vehicle_1NN_*` — vehicles. `vehicle_106_mbt_media` is the main battle tank; the other `10N` slots are the other vehicle classes. There is **no `weapon_shotgun.sbp`**. A specific weapon sound — a suppressed shotgun report, the chopper minigun spin-up, the tank main gun — is a **Wwise event inside one of the `se_*` or `vehicle_*` banks**, addressed by event id, not a filename. The `wwise-rtpc`/`wwise-states` tables carry the parameter/state vocabulary, but the individual play events are inside the compiled banks. ##### The honest answer to "where is weapon X's sound" 1. Identify the likely bank from the prefix scheme above (enemy weapon → `se_e_*`; vehicle-mounted → the `vehicle_*` bank; player weapon → a `se_*` common bank). Use the `sound-assets` lookup table for exact filenames. 2. Extract that bank (`.sbp` → via the `.bnk`/`.sab`/`.stp` chain in the `techniques` sound-swapping entry). 3. The specific weapon event is inside — there is no shortcut that skips opening the bank, because the game doesn't store one file per weapon sound. Grepping for a weapon name will always fail. That failure is the answer, not a gap. ### `findings:searchobjectline-args-missing.md` #### GeoSearchService.SearchObjectLine: the raycast exists, its arguments don't **Domain:** Lua API / reverse engineering · **Confidence:** VERIFIED absence. ##### The wall MGSV has a line-search primitive — `GeoSearchService.SearchObjectLine` — that is exactly what you'd want for a "what am I looking at" reticle raycast. It exists, it's callable, and you still can't use it safely. ##### Evidence - MockFox lists the full `GeoSearchService`: `SearchObject`, `SearchObjectLine`, `SearchObjectSphere`, `SearchObjectSquarePyramid`, each with an `InSpecificScene` variant. - But the dump is `SearchObjectLine=function(...)end`. The `...` is not shorthand — it means the parameter list was never captured. - `exe-symbols` confirms the native functions exist: `SearchObjectLine`, `SearchObjectLineInSpecificScene`, `GeoSearchObject`. - Grep across all IH and vanilla Lua in the pack: **zero call sites.** Nothing in shipping code calls it, so there is no worked example to copy the argument order and return shape from. ##### Why this matters This is not a documentation gap the corpus can close by adding a page. The information does not exist anywhere yet — no repo, no dump, no wiki. Reaching the verdict "unsafe to call" is correct and final *with current data*. ##### What would close it A Ghidra pass on `SearchObjectLine` at its exe address, reading the parameter loads and the return-structure writes, to recover: origin, direction, length, collision mask, and the hit-result layout. That converts every "reticle raycast" attempt from a marker fallback into the real thing. It is the single highest-value audio-style trace still outstanding on the gameplay side. ##### Until then Use the marker fallback: a placed binocular/map marker gives `gameObjectId`, position, and an IH reverse-name lookup. Document it as a fallback, not a raycast.