OBR Mod Creation Wiki
// by CosmicBoogaloo
ESC
Search every lesson, tool, hook, and reference
The Elder Scrolls IV: Oblivion Remastered

OBR Mod Creation Wiki

A complete guide from first texture swap to full custom armor with cloth physics, scripted spells, and new rooms, every step in order, every tool linked, honest about what works in mid-2026.

The One Thing You Need to Know

Oblivion Remastered is two game engines stacked together. Once you understand this, everything else clicks.

🎨

Unreal Engine 5.3.2, visual

Models · textures · lighting · materials · animations · UI · cloth physics · audio (Wwise)

◇   Altar translation layer, the bridge between both worlds   ◇
🧠

Gamebryo 2006, logic

Stats · quests · scripts · AI · die rolls · item records · cells · saves

What This Covers

  • Retextured & reskinned items
  • Brand-new weapons & armor (full 3D pipeline)
  • Capes & cloth physics workarounds
  • Custom animations & retargeting from any source
  • Scripted abilities via OBScript + Lua bridge
  • New interior rooms & player homes
  • Custom creatures via blueprint duplication
  • Facial animation with MetaHuman / DNA files
  • Wwise audio integration & External Sources
  • UE4SS Lua scripting, hooks, snippets, recipes
  • Blueprint behavior mods (custom UI, spawning)
  • Debugging, crash logs & rapid testing workflow
  • Mod distribution, compatibility patching & save safety
  • NPCs, custom races, hair modding
  • Dialogue, voice lines, lip sync workarounds

Known Limits (mid-2026)

  • UE-side terrain editing (no clean workflow yet)
  • New exterior worldspace cells (unsolved)
  • Custom Chaos Cloth Assets (crash on load)
  • Brand-new standalone sounds (experimental only)
  • NPC pathfinding in new cells still needs a NavMeshBoundsVolume + RecastNavMesh in the base level (see Cells & Interior Maps)
  • Multiple enchantments (backend blocks it)
  • Fully custom voiced dialogue indoors: interior Wwise bank residency is still unsolved (the pipeline is otherwise mapped: see Dialogue, Voice & Lip Sync)
  • Game Pass / Epic stores (no OBSE64)
  • Classic .nif meshes (dead format for rendering)
  • Arbitrary body slot expansion (Altar enums fixed)

Your Learning Path

These steps are designed to be followed in order, each builds on the last.

1

How Modding Works

The dual-engine model. Ten minutes that make everything else make sense.

2

Set Up Your Tools

Every tool, in order, with links. FModel, xEdit, UE4SS, UE 5.3.2, Blender, all of it.

3

Make Mods: Basics

Retexture, swap sounds, edit stats, add new gear with existing models. Easiest first.

4

Make Mods: Advanced

Custom 3D armor, cloth physics, animations, MetaHuman faces, Wwise audio, new rooms.

5

Scripting & Custom Code

OBScript, OBSE64, UE4SS Lua, the deepest layer, where "impossible" mods live.

Reference, Always Here

CTD fixes, what-works status board, full glossary, console commands.

All Links & Resources

Every tool, every Nexus mod, every guide, every link in one place.

About This Codex Oblivion Remastered works differently from every older Elder Scrolls game. Old NIF formats don’t apply, there’s no official mod tool, and the community has reverse-engineered everything from scratch since April 2025. Old "how to mod Oblivion" guides actively mislead you. This codex is specific to the remaster, built by CosmicBoogaloo, author of C.A.F.E. and 60+ published mods with 15,000+ downloads, including work covered by PC Gamer, GameSpot, and others. Compiled from thousands of messages across every research channel in the OBR modding Discord.
New: Engine Internals (the CK Bible) A deep reverse-engineered reference for the Altar bridge layer now lives in the sidebar. It covers the DataTables that drive everything, the full NPC/race/face/hair pipeline, the dialogue + lip-sync system, the character state machine, weapons, spells, cells, and the CosmicBridge runtime + Construction Kit plan. Compiled from live UE4SS probe sessions and FModel dumps.

Step 1 of 5

How Modding This Game Works

Before installing a single tool, understand the one idea that reframes every decision you will ever make about Oblivion Remastered modding.

The Dual-Engine Architecture

When you look at Oblivion Remastered you see Unreal Engine 5 graphics. But underneath, the original 2006 Gamebryo engine is still running the actual game, quests, stats, scripts, AI, die rolls. UE5 handles rendering only.

The original Oblivion.esm is essentially byte-identical to the classic game. The remaster’s changes are layered on top through the Altar ESPs rather than rewriting the master file.

🎨

The Face, Unreal Engine 5.3.2 visual

Models · textures · materials · lighting · menus · animations · cloth physics · audio (Wwise)

◇   Altar, proprietary translation layer developed by Virtuous   ◇
🧠

The Brain, Gamebryo 2006 logic

Logic · data · rules · OBScript · AI packages · inventory · quests · cells · saves

The One QuestionEvery time you want to make a mod, ask first: is the thing I want to change part of the brain (Gamebryo logic), the visual (UE5 rendering), or the bridge between them? Everything else is locating yourself on that map.

Two Kinds of Mod Files

brain   ESP / ESM, Logic

Classic Bethesda plugin files. Item names and stats, creature health, quest steps, AI packages, script behavior. Same format as the 2006 game.

Location: ...\\Content\\Dev\\ObvData\\Data\\

visual   PAK, Visuals

Unreal Engine content packages (.utoc + .ucas + .pak) containing all 3D models, textures, materials, and animations. All three files must be present together.

Location: ...\\Content\\Paks\\~mods\\

The Trap That Burns Classic Oblivion ModdersIn classic Oblivion, models were .nif files and textures were BSA-packed. None of that applies to the visuals here. UE5 reads all models and textures from PAK files. The old NIF format is dead for rendering. Loose BSA assets are simply ignored. This invalidates a huge amount of 15-year-old modding knowledge, exactly why this game-specific codex exists.

The Altar ESPs (never disable)

The remaster ships three special plugins that must stay enabled and correctly ordered at all times:

  • AltarESPMain.esp, applies the remaster’s record changes over the vanilla originals. Crucially, it rewrites all item names to UE5-managed localization keys (LOC_FN_IronSword, etc.). These keys are resolved at runtime from the UE localization system in the PAK files.
  • AltarDeluxe.esp, Deluxe Edition content (present in all editions).
  • AltarESPLocal.esp, referenced in Plugins.txt; may not be physically present on disk.
The Localization TrapBecause AltarESPMain rewrites every vanilla item name to a localization key, if your mod edits a vanilla record without forwarding that change, the display name reverts to the raw Gamebryo string and shows up tagged [nl]Something in-game. The NL-Tag Remover mod (mods/473) fixes this artifact on your new items.

The FormID Bridge, How the Engines Talk

Every item, NPC, and cell with a visual representation has a corresponding UE5 asset (Blueprint / form file) in the PAK files. This is how they connect:

  1. Gamebryo processes game logic (equipping a sword, placing an NPC) using internal FormIDs.
  2. Altar receives the FormID, converts it from hex to decimal integer, and looks it up in a pre-loaded map called the SyncMap.
  3. The SyncMap maps FormID → UE asset path.
  4. UE loads and renders the corresponding Blueprint/mesh/material.
FormID ConversionxEdit shows FormIDs as hex: 0x0003AA82. UE form files use the decimal equivalent: 240258. The first two hex digits (e.g. 03) are the load order index, always zero these out when referencing from the UE side. Formula: int(hexFormID, 16) with the first byte zeroed.
Adding a new sword requiresEngineFile type
The sword’s name, damage, weight, value, FormIDbrainESP record
The actual 3D model and texture the player seesvisualPAK asset
Wiring the ESP record to the PAK modelbridgeSyncMap .ini
The Big 2025-2026 BreakthroughTesSyncMapInjector (mods/1272) is a runtime UE4SS Lua mod that injects FormID-to-UE-asset mappings at game startup. This means a new item that reuses an existing vanilla model needs no PAK file at all, only an ESP and a small mapping .ini. A genuinely new model still requires the full UE5 asset pipeline.

The PAK Container Format

Oblivion Remastered uses UE5’s IO Store format, not traditional loose PAK files. This means every visual mod is three files that must all be present together:

  • .pak, loose files (config, fonts), packed with standard UnrealPak
  • .ucas, Content Addressable Store, contains all cooked .uasset data
  • .utoc, Table of Contents, the index for the .ucas file

The global.ucas / global.utoc pair provides dependency resolution for the whole game.

No Official Toolkit

  • Bethesda does not officially support mods for Oblivion Remastered and there is no Construction Kit built for the remaster.
  • The entire toolchain is community-built and partly reverse-engineered since April 2025. Capabilities change month to month.
  • The Xbox Game Pass version shipped with extra debug data which significantly accelerated community reverse engineering.
  • There is an estimated ~30% chance an official CK will be released, no announcement as of mid-2026.
  • The Altar plugin code is proprietary (licensed third-party components from Virtuous) and is extremely unlikely to ever be publicly released.
Date Your ClaimsAnything technical in this codex is “true as of mid-2026, verify the version on the source page.” A research project today can be a one-click tool in three months.

Step 2 of 5

Set Up Your Tools

From zero to a fully working modding workbench. Follow these steps in order, each is the foundation for the next.

Before You StartRead Step 1, How Modding Works first. Tags below indicate which engine each tool touches: brain = logic · visual = visuals · bridge = links them.
0

Confirm Your Install & Back Up

  • Steam build only. OBSE64 supports the Steam version only, not Game Pass, Microsoft Store, or Epic. If you own a non-Steam version, most of this codex still applies but scripting tools will not.
  • Note your exact game version (build number visible in-game), several tools are matched to specific builds and will break on updates.
  • Back up your save files and make a clean copy of Plugins.txt before installing anything.
1

Mod Manager - pick one

  • pick oneMod Organizer 2, profiles + virtual file system. Recommended for creators, keeps your game folder clean. Install the MO2 OR plugin (mods/366) for automatic argument passing.
  • altVortex, Nexus’s official manager, beginner-friendly. nexusmods.com/site/mods/1
  • altNORMM, dedicated Oblivion Remastered manager built specifically for this game.
2

Loaders & Bridge bridge

The foundation everything else runs on. Install in this exact order.

  • requiredOBSE64, script extender / DLL plugin loader. Drop .dll + .exe in Binaries\Win64. Currently only a plugin loader, not a full script extender yet. Steam only.
  • requiredAddress Library for OBSE Plugins (mods/4475), makes OBSE DLL plugins version-independent. Required by most plugins.
  • requiredUE4SS (mods/32, OR build v3.0.1), Blueprint mod loader and Lua scripting runtime for UE5. Required by TesSyncMapInjector. Install to Binaries\Win64\. Use the Nexus OR build, not the GitHub experimental build, the generic build doesn’t hook into UE 5.3 properly.
  • requiredTesSyncMapInjector (mods/1272), the bridge that links new ESP items to UE5 meshes at runtime. Runtime UE4SS Lua mod.
  • when neededMagicLoader + MagicPatcher (mods/1966), enables new interior cells. Install when you reach Lesson 14.
  • handyOBRConsole (mods/2205), lets UE4SS Lua mods run Oblivion console commands. ⚠ Crashes if you tab out of the game while active.
  • altSML / Simple BP Mod Loader (mods/1172), alternative Blueprint loader with better input action recognition and documentation.
The #1 Setup CrashUE4SS and OBSE fight on first launch. Launch the game once without OBSE so UE4SS can configure itself, then launch with OBSE normally thereafter.
3

UE4SS Configuration visual

After installing UE4SS to Binaries\Win64\, configure it for development work:

UE4SS-settings.ini, enable the GUI console

[Debug]
ConsoleEnabled = 1
GuiConsoleEnabled = 1
GuiConsoleVisible = 1
GraphicsAPI = d3d11

With this enabled, UE4SS opens a GUI window alongside the game showing logs, a live object viewer, and function hooks. Press Ctrl+Numpad 6 in the GUI console to dump an up-to-date .usmap mappings file.

Generating Fresh MappingsAlways regenerate the .usmap file after any game patch, asset structures change between updates. The Nexus mappings file may be outdated. UE4SS generates your own from the live game.

Hot Reloading Lua Mods

Enable hot reloading in ue4ss-settings.ini and press Ctrl+R to reload Lua mods without restarting the game. Note: mods that hook into game-spawned actors may not hot-reload cleanly.

4

ESP / Data Tools brain

  • requiredxEdit 4.1.5n+ (TES4R / OR build), primary ESP authoring and conflict detection. Get the OR-compatible build from the official xEdit Discord #xedit-builds channel, not the standard release. Launch with: TES4R64.exe -D:"...\ObvData\Data" -I:"...\ObvData\Oblivion.ini"
  • requiredNL-Tag Remover (mods/473), fixes the [nl] artifact on new item names caused by Altar localization.
  • requiredLOOT, automatic load-order sorting. Sort with LOOT, then hand-fix the special cases below.
  • handyRuntime EditorIDs (mods/1331), surfaces EditorIDs in the in-game console for quick testing.
  • situationalConstruction Set (2006 original) + CSE, visual editor for cells and containers. Setup details below. The Altar ESPs are not safe to load in CS by default, CSE fails to recognize and load their dependencies correctly and will error out. However you can load them: add all the dependency ESPs as masters one by one in xEdit first, then load in CS. Not recommended for most use cases.

Construction Set Extender (CSE) Full Setup

  1. Download: OBSE for original Oblivion · CSE (mods/36370) · Official Construction Set · vcredist_x86.exe
  2. Extract all into your ObvData directory.
  3. Run TES4_Construction_Set_1.2.404.exe and install to your ObvData folder. Ignore the “Oblivion not installed” error.
  4. Edit Launch CSE.bat and replace its contents with: obse_loader.exe -editor -notimeout
  5. Right-click Launch CSE.batRun as Administrator.
CSE Tips
  • Your saved .esp appears in ObvData\Data\Data, move it up one level to ObvData\Data and add it to plugins.txt.
  • For protected directories: create a file named BGSEE_DirectoryCheckOverride (no extension) in ObvData.
  • For MO2 without elevation: create BGSEE_ElevatedPrivilegesCheckOverride (no extension) in ObvData.
  • Missing DLL error: install vcredist_x86.exe, the x64 version alone is insufficient.
  • Use the CSE beta from the Nexus Mirrors tab for fixes to script editor crashes and dialog size issues.
5

UE5 Asset Pipeline visual

Large downloads, install now so the workbench is complete before you need them.

  • requiredBlender + PSK/PSA plugin, model and animation authoring. If the current PSK plugin breaks on your imports, try the older Befzz plugin.
  • requiredUnreal Engine 5.3.2, cook art assets. Use 5.3.2 specifically, not the latest version. Install via the Epic Games Launcher to an unprotected path like C:/, not Program Files or Desktop.
  • requiredFModel + mappings file (.usmap, mods/47), browse and extract game assets. Set Archive Directory to game root (not a subfolder), UE Version to GAME_UE5_3.
  • requiredOR Mod Tools bundle (mods/3918), retoc + UAssetGUI + oo2core in one package. Use the Nexus version of UAssetGUI, not the GitHub release (it has a critical PackageName bug fix).
  • materialsJsonAsAsset (C0bra5+Tectors fork recommended) + j0.dev, import vanilla materials from FModel JSON exports into your UE project.
  • armor/formsC.A.F.E. (mods/4891), CosmicBoogaloo’s tool for setting asset paths in armor and weapon forms quickly.
  • texturesNNRM Merge/Split Tool (mods/3051), split and recombine the NNRM channel-packed textures that OBR uses.
  • blueprintsOR SDK (Kein/Altar) + Visual Studio 2022, the Altar stub project for custom blueprints and cloth physics. No full Unreal Engine source build required.
  • nanitesimple-nanite-parser (c0bra5), extract full-detail Nanite meshes that FModel only exports the fallback version of.
  • audiosound2wem, convert audio files to Wwise .wem format for sound replacement.
  • audiowwiser (github.com/bnnm/wwiser), browse Wwise .bnk soundbank files to locate specific .wem audio file IDs.
6

Build the Altar SDK Project (for Blueprints & Cloth Physics)

Required only if you’re doing custom blueprints, cloth physics, or C++ plugins. Skip if you’re only doing texture/sound/ESP work. Alternatively, use a precompiled build (see Community Starter Packs below) and skip steps 1-4 entirely.

  1. Download and extract github.com/Kein/Altar.
  2. Install Visual Studio 2022 with the workload: Game Development with C++ + individual component: MSVC v143 - VS 2022 C++ x64/x86 build tools (v14.38-17.8).
  3. Right-click OblivionRemastered.uprojectGenerate Visual Studio project files.
  4. Open OblivionRemastered.sln in VS2022 → right-click the project in Solution Explorer → Build. Wait for it to finish, then close VS.
  5. Open the .uproject with UE 5.3.2.
  6. On first launch: if it warns about a missing water collision channel, click “Add to Engine.ini” and continue.
Toolchain Version ErrorIf the build fails with “Unable to find valid 14.38.33130 C++ toolchain”, edit C:\Users\YOU\AppData\Roaming\Unreal Engine\UnrealBuildTool\BuildConfiguration.xml:
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
  <WindowsPlatform>
    <CompilerVersion>14.38.33130</CompilerVersion>
    <ToolchainVersion>14.38.33130</ToolchainVersion>
  </WindowsPlatform>
</Configuration>
LNK2019/LNK2021 Linker Errors (Post-June 12 Altar)After Kein uncommented certain dummied-out files in a June 2025 commit, building Altar directly may produce linker errors. Fix: navigate to Source/OblivionRemastered/ in Windows Explorer and physically delete (not just exclude in VS) the 6 problem files listed in this Discord post: fix thread. VS “Remove from project” is not enough, delete from disk. Credit: agentlefox, dicene.
What Precompiled Builds DON’T FixThe Altar SDK has several C++ stubs (search for // Fix throughout the project to find them all). These affect fundamental classes like VPairedPawn and VWeapon, preventing certain Details panel fields from working. .yeah.nah.yeah.’s precompiled build has fixed additional stubs beyond the base Kein project. If you need full functionality for fields like the Form entry in blueprints, use the precompiled build or build from source.
7

Enable Chunking in UE (Required for Packaging)

  1. Edit → Project Settings → Packaging: enable Generate Chunks and Use IO Store. Disable Share Material Shader Code.
  2. Edit → Editor Preferences: search “chunk” → enable Allow ChunkID Assignments.

Creating a Chunk Assignment

  1. In the Content Browser, create a Data Asset → Primary Asset Label.
  2. Set: Priority = 1 · Chunk ID = any number 1-300 · uncheck Apply Recursively · Cook Rule = Always Cook · check Label Assets in My Directory.
  3. Name it ChunkYOURNUMBER for easy reference.
Chunking RulesNever chunk skeletons, the game already provides them. Never put blueprints in the same chunk as art assets, blueprint paks go in the LogicMods folder while art paks go in ~mods. Materials can “un-assign themselves from chunks” when re-opening the project, fix by ensuring Allow ChunkID Assignments is enabled.
8

First Launch & Verify

  1. Launch the game once WITHOUT OBSE so UE4SS can configure itself properly.
  2. Then launch through OBSE.
  3. Verify OBSE is working (in-game version check command: GetObseVersion in console).
  4. Verify UE4SS is working (its GUI console window should appear).
  5. Check load order in LOOT, Altar/base ESPs should be present and correctly ordered.
Make a SandboxIn MO2, create a dedicated test profile separate from your play profile. Park a throwaway save in a quiet interior for quick checks. Confirm your clean Plugins.txt backup is still safe before doing anything else.

Community Starter Packs

The community has assembled pre-packaged tool bundles to get you started faster:

Step 3 of 5

Make Mods: The Basics

Your first real mods, easiest first. Retexture, swap sounds, edit stats, add new gear without any 3D work required.

1

Retexture or Recolor Something visual

FModel · retoc · image editor, the single easiest mod type

Changing how an existing item, surface, or character looks by editing its texture. This is where everyone should start.

  1. Open FModel. Set Archive Directory to your game root (the folder containing OblivionRemastered.exe), UE Version to GAME_UE5_3, and load your .usmap mappings file.
  2. Browse the Folders tab to find your texture. Right-click → Save Texture. Always use TGA format, not PNG, TGA preserves all channels including alpha, which PNG discards.
  3. Edit the image in Photoshop, GIMP, or Substance. The game uses DirectX normal map format, if you’re working with normal maps, the Y channel is not inverted (unlike OpenGL).
  4. Import into your UE project at the exact same folder path as the original. Check FModel to match the original’s Texture Group and Compression Settings.
  5. Assign to a chunk. Package. Rename output files with _P suffix. Drop all three files (.pak/.ucas/.utoc) in ~mods.
The NNRM Texture System, What You Need to Know

OBR packs multiple maps into one image in the NNRM format (Normal/Normal/Roughness/Metallic):

ChannelContainsNotes
RNormal XDirectX format
GNormal YDirectX format (NOT inverted like OpenGL)
BRoughnessBlack = shiny, White = matte
A (alpha)MetallicBlack = non-metal, White = full metal

Variants: NNRS (specular instead of metallic) · NNRE (emissive) · NNR (no metallic) · NNRAO (ambient occlusion).

Import settings in UE: use BC7 compression and untick sRGB. Do not use the Normal compression preset for NNRMs, it breaks them.

In Blender: when working with OBR assets, invert the Green channel of the NNRM (UE uses DirectX -Y, Blender uses OpenGL Y). Set color space to Non-Color.

Use the free NNRM Merge/Split Tool (mods/3051) to split channels and recombine them.

Photoshop Alpha / Transparent Pixel WarningPhotoshop blacks out transparent pixel data when exporting PNG, it strips the color information from pixels with alpha=0. For diffuse textures that pack data in the alpha channel, use GIMP or this ffmpeg command to preserve RGB while setting alpha to opaque:

ffmpeg command, preserve RGB, set alpha=255

ffmpeg -i input.png -filter_complex "[0]geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255'" out.png
Diffuse Alpha Channel, It’s Not Always EmptyMany diffuse (_D) textures in OBR pack extra data in their alpha channel. What that data encodes depends on the material. Common examples: gem/crystal materials use the alpha to define where gem-like inner reflections appear (IOR-like effect, higher alpha = more gem-like). Fabric/cloth materials may use alpha for roughness variation. When a texture looks nearly transparent in Photoshop or Blender but renders correctly in-game, this is usually why. To investigate: split the texture into RGBA channels using the ffmpeg scripts or Substance Designer and look at the alpha channel independently. When replacing or modifying diffuse textures, preserve the original alpha unless you know what it controls. Credit: c0bra5.

Making an NNRM in GIMP

  1. Open Normal, Roughness, and Metallic maps in GIMP. Add an alpha channel to the Normal map (Layers → Transparency → Add Alpha Channel).
  2. On the Normal map: Colors → Components → Decompose. Set Color Model to RGBA, click OK. A new tab opens with the channels as separate layers.
  3. Set Roughness and Metallic maps to Grayscale mode (Image → Mode → Grayscale).
  4. Create a new blank image the same size as your maps, using Fill: Transparency (not white or any color).
  5. From the decomposed Normal layers, select the Red layer, copy (Ctrl+C), paste into the blank image (Ctrl+V), then flatten into a new layer and rename it “red”.
  6. Repeat for the Green layer, place it below red. Then paste your Roughness as “blue” and Metallic as “alpha”. Do not merge these, keep them as separate layers.
  7. Go to Colors → Components → Compose (NOT Re-compose). Set Color Model to RGBA: R = red layer, G = green layer, B = roughness, A = metallic. Click OK.
  8. Export as .tga 32-bit. PNG will drop the alpha channel.
GIMP Alpha GotchaIf your metallic map is pure black, the alpha channel becomes fully transparent, making the texture disappear. Fix: make the black areas a very dark grey (e.g. #010101) in the metallic map before composing. Also: GIMP sometimes refuses to copy-paste into channels, if this happens, close and reopen GIMP and try again.

Making an NNRM in Photoshop

  1. Open your Normal, Roughness, and Metallic maps.
  2. In the Channels panel of a new document: paste Roughness → Blue · Metallic → Alpha · Normal R → Red · Normal G → Green.
  3. Save as .TGA 32-bit, PNG will discard the alpha channel.

NNRM ffmpeg Split Scripts (c0bra5)

Split an NNRM into component images

# Normal XY channels (produces a blueish image - that's correct)
ffmpeg -hide_banner -v error -i "%1" -filter_complex "[0]geq=r='r(X,Y)':g='g(X,Y)':b='255':a='255'" "%~n1_n.png"
# Roughness (from B channel)
ffmpeg -hide_banner -v error -i "%1" -filter_complex "[0]geq=r='b(X,Y)':g='b(X,Y)':b='b(X,Y)':a='255'" "%~n1_r.png"
# Metallic (from Alpha channel)
ffmpeg -hide_banner -v error -i "%1" -filter_complex "[0]geq=r='255':g='255':b='255':a='p(X,Y)'" "%~n1_m.png"

For a model with no normal map detail, create a flat NNRM: set R=128, G=128 (neutral normal), fill B with your roughness value, fill Alpha with your metallic value.

Skin materials use SubsurfaceProfile (SP_SkinBase). Diffuse/BaseColor format: RGB = color, A = emissive mask. Hair textures use the _RAUD suffix multi-channel format, always export TGA from FModel, the alpha channel contains the opacity/strand mask.

2

Swap or Replace a Sound visual

Wwise .wem files · wwiser for finding voice line IDs

The game uses Audiokinetic Wwise for all audio. Sounds are stored as .wem files (Wwise Encoded Media) inside .bnk soundbank containers.

  1. Use FModel + retoc to unpack the .wem audio file you want to replace.
  2. Convert your replacement audio to .wem format using sound2wem. Alternatively, use older Wwise installer tools; see the RE Modding forum guide.
  3. Swap in your audio keeping the exact same filename. Repack with retoc into a _P pak. Drop in ~mods.

Finding a Specific Voice Line

  1. In FModel, browse to Localization/String Tables. Find the line you want and copy its event name number (looks like 00047660).
  2. Browse to WWise/Event/English. Find the .bnk bank file matching the NPC race and dialogue category.
  3. Open that bank in wwiser and navigate the tree to find your number in a CAkSound node. That node’s value is the SOURCE ID, the number matching the corresponding .wem file.
  4. Extract that .wem, replace with your converted audio, repack.
Adding Brand-New SoundsReplacing an existing sound works reliably. Adding a brand-new sound that wasn’t in the game requires full Wwise integration setup (version 2023.1.8.8601.3258, building against the Altar project, and the External Sources workflow), see Lesson 12 in Step 4. Legacy ObScript PlaySound commands only work for a subset of sounds explicitly wired through Altar.

Dialogue Timing Without Voice

Placing .mp3 files in the legacy Sound/Voice/ folder matching the vanilla dialogue filename convention controls subtitle display duration, even though no audio plays. The mp3 file duration equals the subtitle display time, useful for localization subtitle syncing.

3

Unpack & Repack Game Files visual

retoc · UAssetGUI, the fundamental skill under every visual mod

The game’s files are sealed in the IO Store container format. Pull one out, change it, seal it back. This underlies almost every visual mod.

Extracting with retoc

retoc.exe to-legacy --filter "OblivionRemastered/Content/YourPath/YourFile" ^
  "[GameDir]\Content\Paks" "C:\Output"

Use forward slashes in the filter path. To extract multiple files at once, add multiple --filter arguments.

FModel vs retoc for Different File TypesFModel is great for browsing assets and exporting textures/meshes. But for .uasset/.uexp blueprint files (forms, BDPs), use retoc to extract, not FModel, FModel doesn’t export .uexp files from IO Store archives, giving you only half the file.

Editing with UAssetGUI

UAssetGUI needs both the .uasset and .uexp files in the same folder. Set the version dropdown to 5.3. Load your .usmap mappings file via Utils → Import Mappings. If you get “Failed to Parse X Exports”, extract the referenced dependency files alongside the main asset in the same directory structure.

Repacking with retoc

retoc.exe to-zen --version=UE5_3 "OblivionRemastered" "MyMod_P.utoc"

Input directory must be the OblivionRemastered folder (not deeper). This produces all three files (.pak, .ucas, .utoc) at once.

The ubulk / Blurry Texture FixUE5 may split large textures into .ubulk files. If retoc doesn’t pack these correctly, textures appear blurry in-game. Fix: set “Never Stream” on the texture in UE Editor (the old ini-based setting is from UE4 and doesn’t work in UE5). Update retoc to a recent version, GitHub issue #22 was resolved. Check: if the .ubulk is larger than .uexp → fix not applied. If .uexp is larger → fix worked.

You can combine art assets and form assets in the same retoc folder before packing to produce one single pak for your entire mod.

4

Change an Item’s Stats or Enchantment brain

xEdit, your first edit to the Gamebryo logic layer

Making a sword hit harder, armor weigh less, or adding enchantments. Uses xEdit to edit game data records on the Gamebryo side.

Launch xEdit with the correct flags

TES4R64.exe -D:"[GameDir]\Content\Dev\ObvData\Data" -I:"[GameDir]\Content\Dev\ObvData\Oblivion.ini"
  1. Let xEdit fully load Oblivion.esm and the DLC files.
  2. Find your item inside AltarESPMain.esp. Right-click it → “Copy as new record into…” a new mod file. Never edit the originals directly.
  3. Give it a fresh EditorID so it can’t conflict with anything, then edit the numbers in the DATA section, damage, weight, value, enchantments.
  4. Add your new .esp to Plugins.txt and test.
Renaming ItemsRenaming an item through a basic ESP edit makes the name show as [nl]Something in-game. Use the NL-Tag Remover mod (mods/473) to fix the broken tag, or use xEdit’s new-string workflow for properly localized names.
Beyond Enchantment Caps with xEditUse Fortify (actor value) to target any stat, including hidden ones like Shield (acts as extra armor rating) and Attack Bonus (increases damage). This bypasses the CS enchantment caps entirely.

Finding Your FormIDs In-Game

Your mod’s load order index determines the first two hex digits of all FormIDs it creates. Formula: (line number in plugins.txt minus the comment lines) → convert to hex. Example: your mod is the 23rd line → index 21 → hex 15 → your FormIDs start with 15XXXXXX. Install the Runtime EditorIDs mod (mods/1331) to surface EditorIDs directly in the console.

Game Settings (GMSTs) via GameSettings Loader

Use the Game Settings Loader (mods/833) to override game settings from a config file without touching ESPs:

[GameSettings]
fJumpHeightMin = 64
fJumpHeightMax = 128
fHealthRegenDelay = 999999
5

Add a New Item Using an Existing Model bridge

TesSyncMapInjector shortcut, new item, zero 3D work needed

Creating a genuinely new item, your own named, custom-stat weapon or armor, while reusing a model the game already has. No Blender or UE required thanks to TesSyncMapInjector.

  1. In xEdit, create your new item record (new EditorID, set your stats). In the model/mesh field, point it at an existing in-game model path you found in FModel.
  2. Run the Smart Mapper xEdit script. It reads your ESP and automatically writes a .ini link file to xEdit\SyncMap\.
  3. Copy that .ini file to [GameDir]\Content\Dev\ObvData\Data\SyncMap\.
  4. Put your .esp in ObvData\Data and add it to Plugins.txt.
  5. Critical load order rule: your plugin must appear above AltarDeluxe.esp in Plugins.txt. Below it, new items go invisible or crash on cell entry.
TSMI .ini Format

If you’re writing the SyncMap .ini manually:

[Meshes]
; DecimalFormID = /Game/path/to/UEasset.UEasset
002350=/Game/Forms/items/armor/BP_DaedricCuirass.BP_DaedricCuirass

Where the decimal FormID is converted from the hex FormID in xEdit (with load order bytes zeroed). Multiple mods coexist fine with separate INI files.

TSMI Enchanted Item BugCustom standalone armor becomes invisible when enchanted. When a player enchants an item, the game creates a new item with a dynamically assigned FormID at the high end of the address table. TSMI maps specific FormIDs to UE meshes, it doesn’t know about this new dynamic FormID. Result: enchanted version has no mesh. Workaround: don’t change the load order of the ESP after creating enchanted gear. If load order changed → the FormID changes → re-enchant. Status: TSMI developer researching a proper fix as of mid-2025.
6

Add a New Container or World Object bridge

Construction Set · MagicLoader, place new objects without crashing
  1. In the Construction Set, load MagicLoader’s example file (IntTestMod.esp) and click Set As Active File.
  2. Duplicate an existing chest or container, give it a new EditorID, say “Yes” to creating a new object. Place it in the world, save, close.
  3. Open the mod in xEdit (with all Altar files loaded) and run Haphestia’s Fix and Port Script (fixmod). This fixes the two missing parameters that the Remaster requires but the CS doesn’t add.
  4. Run the Smart Mapper xEdit script. Copy the resulting .ini to Data\SyncMap.
  5. Run MagicLoader → “Do Magic!” once to register the new cell entries.
  6. Enable your esp in Plugins.txt, above AltarDeluxe.esp.
MagicLoader v2MagicLoader has been updated. Follow its current Nexus page (mods/1966) if any steps look different from this description. It modifies the CellsToMapPath DataTable to link Gamebryo cells to UE map files.
6b

Add Items to a Vendor’s Inventory brain

xEdit leveled lists, make merchants stock your new items

Vendors in Oblivion Remastered sell items through leveled lists, the same system as the original game. Leveled lists are records in the ESP that tell the engine “when this merchant restocks, pick from these items at these levels.”

Finding the Right Leveled List

  1. In xEdit, open Oblivion.esm (or OblivionRemastered.esm) and expand Leveled Item records.
  2. Merchant inventories are typically referenced by the merchant’s NPC record → Merchant Container field, which points to a chest with leveled list entries.
  3. Alternatively, search for the merchant by EditorID (e.g. SheopetraTrader), open their NPC record, find Buy/Sell and Training Flags section and the Merchant Container reference. Follow that reference to find the inventory chest.
  4. For generic class-based merchants (all blacksmiths carry the same list), find the LeveledItem records named something like LvlWeaponBlacksmith, these are what you want to inject into.

Adding Your Item to a Leveled List

  1. Right-click the leveled list record you found → Copy as Override into → select your ESP as the destination. This creates a patched copy in your mod rather than editing the master file.
  2. Expand the override. You’ll see a list of entries with Level and Count columns plus Object references.
  3. Right-click an existing entry → Add to create a new slot. Set the Level (1 means it always appears), Count (how many units per restock), and Object (your new item’s FormID).
  4. Save your ESP. The next time that merchant restocks (rest/wait 2 days or use pcb in console), your item should appear in their inventory.

Leveled List Flags

  • Calculate from all levels ≤ PC’s level, if checked, all entries at or below player level are eligible; if unchecked, only the closest level gets picked.
  • Calculate for each item in count, rolls the list separately for each unit of Count. Useful for randomized stacks.
  • Use All, gives the player every item on the list instead of picking one.
Full ReferenceThe Nexus Modding Wiki: How to Add Objects for Sale covers this in detail including the exact UI flow in the original Construction Set. The xEdit approach above is generally easier for OBR since the CS has some quirks with Remaster data. Leveled lists work identically between original Oblivion and the Remaster on the Gamebryo/ESP side.
Quick Tip: Vendor RestockingMerchants restock every 2 in-game days. If you want to test immediately: coc to a different cell, wait 48 hours (waitprompt), return. Or use ResetInventory on the merchant in the console, but this clears any items they currently have equipped.
✓ Basics CompleteYou can now retexture, replace sounds, edit item stats, add new gear with existing models, and place new world objects. Step 4 is where you start making things from scratch, your own 3D armor, cloth physics, animations, and more.

Step 4 of 5

Make Mods: Advanced

Custom 3D armor, cloth physics, animations, MetaHuman facial rigging, Wwise audio, world editing, new rooms. Requires Blender and Unreal Engine 5.3.2.

7

Build Custom 3D Armor from Scratch bridge

Blender → UE 5.3.2 → BDP Blueprint → C.A.F.E. → ESP → SyncMap

Asset Structure, What You Need to Create

Each armor piece in OBR consists of 7 components. For a mesh replacer you only need to swap the skeletal mesh. For a standalone new item you need all of them:

#ComponentPath patternRequired for
1ESP Form (Gamebryo side)ObvData/Data/MyMod.espAll new items
2Form Blueprint/Game/Forms/items/armor/ArmName.uassetNew items
3BDP Blueprint/Game/Forms/items/armor/BP_BDP_ArmName.uassetNew items, the key file
4Skeletal Mesh/Game/Art/Equipment/armor/type/SK_Piece.uassetReplacers + new items
5Material Instance (MIC)/Game/Materials/MIC_Piece.uassetCustom textures
6GND mesh (ground model)/Game/Art/armor/SM_Piece_gnd.uassetDropped item appearance
7Icon texture/Game/Art/UI/Icons/.../T_MyPieceInventory icon

A, Blender Setup

  1. Import SK_HumanoidFull (extracted from FModel via Save Model as PSK) as your body reference mesh. Color body yellow and head red for visual clarity.
  2. Import your armor mesh PSK. On PSK import in Blender: set Linear Color, scale 3m, scale factor 0.01.
  3. Rename the armature object to Armature in Blender’s Outliner. PSK meshes from FModel carry an extra root bone; this naming tells UE to skip that extra bone on import. Exception: do NOT rename NBO (New Body Options) skeletons, they are already set up correctly.
  4. For static meshes (SM_ prefix, like ground models): remove the armature entirely before exporting.
PSK vs UEFormat Tradeoffs

PSK issues: causes seam splitting and blendshape/morph artifacts; adds extra joints that cause Joint Count Mismatch errors.

UEFormat advantages/disadvantages: avoids morph issues. Problem: exports vertex colors as sRGB instead of linear, causing incorrect body part hiding in-game. Also doesn’t export sockets. Plugin: github.com/h4lfheart/UEFormat

Workaround: use UEFormat to get correct morphs, use PSK to get correct vertex colors, then copy the vertex color data from the PSK import to the UEFormat import in Blender.

B, Material Slots (Control First-Person Visibility)

SlotPurposeNotes
0Body main materialHidden in first-person view
1Sleeves / armsRemains visible in first-person
2Skirt / physics proxyShould have no material assigned, disable in LOD 0 sections
MaterialSlotsHiddenInFirstPerson (Emperor’s Robe Method, mods/3671)

An alternative to the BDP bitmask for first-person slot hiding: edit the form blueprint with retoc and UAssetGUI to implement the MaterialSlotsHiddenInFirstPerson property directly. This approach was documented by the Emperor's Robe First Person Mesh Fix (mods/3671). Workflow: extract form with retoc, open .uasset/.uexp pair in UAssetGUI, find or add MaterialSlotsHiddenInFirstPerson, set the appropriate slot indices, save, repack. No UE project needed.

First-Person Invisible FixIf Blender merges both material slots because they share the same name, the item turns invisible in first-person view. Fix: rename one of the duplicate material slots to something different (e.g. MIC_Piece_TEMP) before exporting FBX. In UE, open the skeletal mesh editor and rename it back to the correct name, then assign the correct MIC to both slots.

C, Weight Painting

  1. Parent your clothing mesh to the armature: select mesh, Shift-click armature, Ctrl+PObject. For NBO armors: first un-parent with Alt+PKeep Transformations.
  2. Transfer weights using the Data Transfer modifier: Source = Humanoid/NBO mesh, Mapping = “Nearest Face Interpolated” → click Generate Data Layers → Apply.
  3. Add an Armature modifier, do NOT apply this one.
  4. Delete unused vertex groups (use a Blender plugin for bulk cleanup, vanilla body meshes ship with 407 vertex groups). Hand-paint any problem areas in Pose mode.
Bind Pose & Weight Transfer AccuracyThe bind pose must match between your mesh and the reference (SK_HumanoidFull) at the time of weight transfer only, not for runtime. Data Transfer uses nearest-face spatial projection, so if your mesh is in a wildly different pose than the reference, weights may bleed incorrectly across body parts (e.g. shoulder weights onto neck). T-pose to T-pose gives the cleanest projection. Once weights are baked and you’re in UE, the game drives everything from animations, the original bind pose is irrelevant at runtime.
Mixamo T-Pose Trick for Custom MeshesIf you have a custom mesh in a non-standard pose (from another game, Sketchfab, etc.) and need it in T-pose for clean weight transfer: upload it to Mixamo. Mixamo auto-rigs it in T-pose. Export back out, delete the Mixamo rig entirely, you don’t want those weights. You just used Mixamo as a free automatic T-pose corrector. Now parent to the OBR armature and run Data Transfer from SK_HumanoidFull as normal. This bypasses the most intimidating manual pose-correction step for newcomers.
Static Helmets, Make Them Skeletal AnywayEven if a helmet is visually static and doesn’t deform, make it technically skeletal by weighting it entirely to the Head bone (paint all vertices red). Truly static meshes placed in UE end up at world origin, not on the character’s head.

Character Mesh Editing, Key Insight (MikhailTeslov, mods/214)

The pioneering tutorial for character body mesh editing in OBR, from the Nordic Muscles mod (mods/214) by MikhailTeslov. The critical rule that the entire mesh modding scene built on:

Vertex Color Linear Colorspace (Critical)When importing and exporting character body meshes, vertex colors must be in linear colorspace. These vertex colors control which body parts get hidden when armor is equipped (the body-part hiding system). Importing as sRGB (the default in most tools) produces incorrect hiding behavior in-game. This was the first community-documented explanation of how body part hiding works in OBR. Credit: MikhailTeslov.

Blender Sculpt Mode Tips

  • Use Sculpt Mode instead of Edit Mode for mesh fitting adjustments, prevents accidental backface selection.
  • Grab and Smooth tools are most useful for fine-tuned fitting to the body.
  • Enable Backface Culling in Viewport Shading to catch normal direction issues early.

Fixing Blade Mesh Shadow Issues

  • Don’t use Merge by Distance on blade edges, it breaks the sharp normals that give blades their edge.
  • Fix: in Edit Mode, select the sharp edges → Mark as Sharp (turns blue).
  • Or: Mesh → Normals → Average Vectors → Face Area.
  • To reset normals entirely: Mesh → Normals → Reset Vectors.

D, FBX Export Settings from Blender

Confirmed Export Settings Scale: 0.01 (critical, wrong value produces a massively oversized mesh in UE) · Forward: X · Up: Z · Smoothing: Edge (Face also works; some users get UE “unknown export” errors with Edge on specific meshes, switch to Face if that happens) · Vertex Colors: Linear (critical for correct body part hiding) · Apply Modifiers: ✓ · Only Deform Bones: ✓ · Add Leaf Bones: only for skeletal meshes, NOT for static meshes where it causes issues · Animation: unchecked.

Name skeletal meshes with SK_SetName_PieceName_m convention (_m for male, _f for female variants).

E, Import into UE 5.3.2

  1. Replicate the game’s exact directory structure in your UE Content folder. This is non-negotiable, the game resolves paths at runtime.
  2. For Skeletal Meshes: assign the correct skeleton on import:
    • Regular clothing/armor → SKEL_HumanoidSkeleton
    • NBO-specific female armor → SKEL_HumanoidFemaleAdd
    • Helmets → usually SKEL_HumanoidSkeleton or HumanoidHeadRig
  3. Set Import Normals and Tangents to preserve your custom normals (not Compute Normals).
  4. Enable Import Morph Targets if you’re editing head/face meshes.
  5. For Static Ground Meshes: set Collision preset to Custom. Enable Allow CPU Access (required for enchantment Niagara VFX on weapons). Set Collision Complexity to “Simple As Complex” and check Customized Collision.

F, Vertex Colors & Body Part Hiding System

Vertex colors on the body mesh control which parts disappear when armor is worn, via material function MF_HideByVertexColorInt3. They must be linear, the game crashes if exported as sRGB.

Complete Body Part Color Table (community research, deathwrench)
Body PartChannelValueBody PartChannelValue
pecs + mid-backR128shoulderG2
sternumR64upper-backG1
front-absR32bicepG4
side-abs + lumbarR16forearmG8
thighR8handG32
kneeR4underwearG128
low-calfR2braG64
foot-topR1low-ankleG16
high-ankleB64toes + foot-bottomB128
Reference: nyyxn body-part chart (mods/2583). Exact per-channel-to-body-part mapping is also documented in the Altar_enums.hpp dump under EBodyPartSlot. Note: Ears are NOT in this vertex color hiding system, hair/ear hiding is separate, handled by the head slot / biped slot. Merged armor meshes don’t support morphs. Use Blender’s Vertex Color Controls addon (BlenderMarket) to accurately sample linear RGBA values from imported meshes.

G, Bitmask Calculator (BP_BDP Body Section Hidden)

  1. Extract the item’s BP_BDP blueprint with retoc. Open in UAssetGUI. View → Expand All. Find MaleBodySectionHidden and FemaleBodySectionHidden. Add them if not present (add as 0 first, save, then change).
  2. Copy the current number into Windows Calculator → Programmer mode → Bit Toggling Keyboard.
  3. Using the body part color table above, set each bit to 1 (hidden) or 0 (visible).
  4. Copy the resulting number back into UAssetGUI. Save. Repack. Test.
The “0 Means Inherit” TrapIn a blueprint, setting the value to 0 means inherit hidden parts from parent blueprint, NOT “show all body parts.” To explicitly keep parts visible (e.g. sandals showing feet), set a non-zero value with all relevant bits cleared. The one exception: when editing directly in UAssetGUI, literal 0 works as true zero and doesn’t inherit.

Tails are separate, add or remove the HideTail property independently. Setting an item to the Amulet biped slot preserves hair and ears. Setting armor on the Tail biped slot makes it not render at all regardless of BDP settings.

Known working values: 4294967040 = hides everything except hair · 4294918400 = Mythic Dawn armor value · 10240 = hides head only · 0 = inherit from parent.
Helmet That Keeps Hair, Non-Head BDP TrickHelmets hide hair through the BDP superclass (an Altar-level class property), not through the BodySectionHidden bitmask. There are no bitmask bits for hair. So setting BodySectionHidden flags won’t prevent hair hiding on head-slot items. The fix: use a non-head-based BDP as your template. For example, use a boots BDP (BP_Generic_BDP_LB_Boots) as the base for your helmet form. The hair slot (Hair biped) also inherits this superclass behavior, so using a boots-based BDP for a Hair biped slot item keeps hair intact. This pattern was identified from the Demoria Armor mod. Credit: qunai.

The Three Levels of Item Forms

Standalone armor requires understanding that Virtuos implemented a three-level hierarchy, not two. When you look for item properties in FModel, they may be at any level:

  • Level 1, The base form (e.g. EbonyCuirass.uasset), EditorID, ESP-side properties
  • Level 2, The BP_ form (e.g. BP_EbonyCuirass), mesh references, GND (world model), some hiding properties
  • Level 3, The BP_BDP_ form (e.g. BP_BDP_EbonyCuirass), body part hiding bitmask, first-person material slots, skeletal mesh assignments

Virtuos was inconsistent about where they put things: the world model (GND) is declared in Level 1 for some armor (e.g. Ebony Cuirass) and in Level 2 for others (e.g. Monk Robe). Always check both when troubleshooting missing GND or mesh references. Most standalone workflows only require touching Levels 1 and 3, which is why Level 2 often gets skipped, but it’s there if you need it.

H, Materials in UE 5.3.2

You cannot create fully custom materials from scratch in UE, they crash on load. You can only inherit from vanilla base materials. The authoritative workflow from c0bra5:

  1. Pick a MIC from the game (inspect it in FModel → Export Properties JSON to see its parent chain).
  2. Create a new material in your UE project at the exact same path as the vanilla parent (e.g. /Game/Art/Character/Imperial/MIC_HumanUnderwear_M). Make it inherit from M_Base_Char.
  3. Create a new MIC (the location doesn’t matter) inheriting from that stub in step 2. Set up your textures only in this new MIC.
  4. Files to include in your mod pak: your own MICs + the skeletal mesh + your textures. Everything else (stubs, vanilla materials) stays out of the chunk, it’s reference-only.
Three Rules That Prevent Grey-World Disasters 1. Never change a static switch on a material, even to the same value. Results in the UE grey grid replacing your item.
2. Never pak the base material stub, packing it overwrites the real game material and greys out everything that shares it (potentially all weapons or all armor of one type).
3. Materials can un-assign themselves from chunks when re-opening the project. Fix: confirm Allow ChunkID Assignments is enabled in Editor Preferences and set Cook Rule to Always Cook.
Two Material Parents Worth Knowing
  • FPSClippingFix, the correct parent for any first-person-visible gear (weapons, gloves, cuirass sleeves). Prevents clipping in first-person view.
  • TWO-SIDED, good parent for ground drop meshes (SM_ prefix).

I, BP_BDP Blueprint Parent Chains

Your armor blueprint must inherit from the correct parent chain. Create dummy parent blueprints in Dev/clothing/GenericChild/, they just need to exist with no content. Weapons do NOT require BDP blueprints.

Piece TypeRequired Parent Chain
CuirassVUpperBodyModularPart → BP_Generic_BDP_UpperBody → BP_Generic_BDP_UB_Cuirass
Full ArmorVUpperBodyModularPart → BP_Generic_BDP_UpperBody → BP_Generic_BDP_UB_FullArmor
GreavesBP_Generic_BDP_LowerBody → BP_Generic_BDP_LB_Greaves
Skeletal Helmet / HoodBP_Generic_BDP_SkeletalHelmet
AmuletVAmuletModularBodyPart

J, Ground Models (GND Assets)

When armor is dropped on the ground, it uses a separate static mesh (SM_ prefix, _gnd suffix convention). Where the GND reference is declared varies, Virtuos was inconsistent. Check both the main form asset (e.g. EbonyCuirass.uasset) and the BP_ form. To add a GND reference via UAssetGUI, add a NewWorldModels ArrayProperty of SoftObjectProperty to the BP form, then View → Recalculate Nodes. GND meshes must have proper collision or they fall through the floor. Object Channels must all be set; Complex Collision set to Default.

K, Forms with C.A.F.E., ESP, SyncMap & Packaging

  1. Use C.A.F.E. (mods/4891) to create armor forms. The form name must match the name in the asset path exactly.
  2. Create your ESP in xEdit with item records. Run Smart Mapper to generate Data\SyncMap\YourMod.ini.
  3. Place your ESP above AltarDeluxe.esp in Plugins.txt.
WhatWhere it goes
Body blueprints (LogicMods)...\\Content\\Paks\\LogicMods\\YourModName\\
Art assets + form blueprints...\\Content\\Paks\\~mods\\YourModName\\
ESP + SyncMap .iniContent\\Dev\\ObvData\\Data\ and \Data\SyncMap\

Additional Field Notes

  • Eye color editing is possible (discovered by _trungus), method still being fully documented as of mid-2025. Check the OBR modding Discord #research-modeling for the current workflow.
  • Amulet biped slot preserves hair and ears, useful for items like head accessories or circlets that shouldn’t hide hair. Note the mesh will be positioned for the neck, so clipping adjustments may be needed.
  • Tail biped slot makes armor invisible regardless of BDP settings, don’t use it for regular armor.
  • Weapons do NOT require BDP blueprints, they are not assigned to body parts. Only armor/clothing needs them.
  • Vertex weight limits: too many bones deforming the same area produces warped/jagged edges. Delete unused vertex groups. Body meshes ship with 407 vertex groups, use a Blender plugin for bulk cleanup.

Beast Race Armor Compatibility

  • Import all skeletal meshes with SKEL_HumanoidSkeleton. Never include the skeleton in your pak chunk.
  • Never include physics assets, delete them from your project before packaging.
  • Khajiit material slot order must be preserved, procedural overlays depend on it.
  • Wrong skeleton assignment = “origami” deformed character model in-game.
7b

Weapon FX: Enchant Glow, Blood Splatter & Swing Trail visual

Vertex color mask · weapon sockets · standalone vs replacer workflows

Custom weapons need specific setup to get enchantment glow, blood splatter, and swing trail effects. These are driven by vertex color channels and mesh sockets.

Two Weapon Workflows

  • Replacer, ship mesh + textures only. Keep the vanilla MIC name. The game loads the real MIC from Bethesda’s pak automatically.
  • Standalone, ship mesh + textures + extracted MIC + extracted BP/Form. Use UAssetGUI to copy vanilla assets, rename everything (e.g. MIC_Moonveil, BP_BDP_Moonveil_1H), fix paths, pack the art chunk as *_Art_P and forms as *_Forms_P.

Enchantment Glow Mask (Vertex Colors)

The glow effect reads the Red channel of vertex colors. Red = glows, Black = no effect.

  • In Blender: go into Mesh Paint mode (Shift+4), paint the blade red and the hilt black. Export using linear vertex colors (must be linear, not sRGB).
  • UEModel exports vertex colors incorrectly (wrong channel mapping). Use PSK format for accurate vertex colors from FModel.
  • If importing via FBX directly into UE without Blender, vertex colors from PSK are preserved as-is from FModel.
Blood Splatter & Multi-Material SlotsBlood splatter will NOT work on a weapon with more than one material slot. You must combine your textures into a single larger texture (pack multiple UV islands at 0.5 scale onto one sheet) and use a single material. This is a hard engine limitation.

Required Sockets

Add these in UE’s Socket Manager after importing your mesh. These drive the swing trail and blood/impact VFX:

  • Socket_Start, blade root (bottom of blade)
  • Socket_Impact, blade tip (top)
  • Socket_End, offset away from the blade (for blood spray and sparks, not on the blade surface)
  • BloodEffect_Socket, on the blade surface

If parenting sockets in Blender for FBX export, prefix with Socket_ (e.g. Socket_BloodEffect_Socket) because Unreal strips one Socket_ prefix on import. When adding in UE’s socket manager directly, use the name as-is.

Accurate Socket PositionsThe best way to get accurate socket positions for a vanilla weapon replacement is to use JsonAsAsset’s “Import Static Mesh Data” option, this imports sockets onto the vanilla mesh in UE. You can then copy transforms from the vanilla mesh to your custom one.

Full Standalone Checklist

  • Import mesh with Collision ON and Allow CPU Access checked
  • Duplicate vanilla MIC (or create custom). Assign to mesh. For replacer: exclude MIC from pak. For standalone: include custom MIC.
  • Paint vertex color red/black mask in Blender (blade = red, hilt = black). Apply and save.
  • Add 4 sockets. Save mesh.
  • Test enchant glow, blood, trail in-game.
  • Cook and copy pak to Content\Paks\~mods\
7c

Physics-Driven Weapons (Flails, Chains) visual

Skeletal mesh weapon · custom armature per bone · Physics Asset constraints

Weapons like flails, ball-and-chains, or any weapon where parts need to physically swing and collide are handled as skeletal meshes with their own custom armature, rather than static meshes. The physics is driven by a Physics Asset using capsule bodies and constraints, same system as cloth physics but applied to weapon bones.

Blender Setup

  1. Model the weapon in parts: handle, chain links (one bone per link), and ball/head.
  2. Create a custom armature with a bone for each physics-driven part. Name bones clearly (e.g. chain_01, chain_02, ball).
  3. Auto-weight paint the mesh to the armature. For rigid parts (each chain link, the ball), weight all vertices 100% to their respective bone, no blending needed.
  4. Export as FBX at scale 0.01 (same as all other skeletal meshes).

UE Setup

  1. Import the FBX. Select skeleton = None on import (create new skeleton for the weapon, it doesn’t use SKEL_Humanoid).
  2. UE auto-generates a Physics Asset. Accept it and open the Physics Asset editor.
  3. The auto-generated capsule bodies and constraints often work well out of the box, each chain link gets its own capsule and the constraints allow angular movement. Adjust constraint limits as needed.
  4. Create the weapon’s BP_ blueprint form (not a standard BP_ weapon, make your own custom blueprint class).
  5. Add the skeletal mesh component to the blueprint.
  6. The physics asset runs automatically on the weapon when equipped.
Skip Cloth Physics EntirelyFor chain-type physics, you don’t need Chaos Cloth at all. Simulate the capsule bodies via the Physics Asset instead. This is simpler and more robust than cloth sim for rigid chain links. Credit: Deleted User (CosmicBoogaloo modding discord).

A flail mod was successfully created using this approach (September 2025). Each chain link had its own bone, auto-weighting worked correctly, and auto-generated Physics Asset bodies produced natural swinging behavior without additional configuration.

8

Materials, Blueprints & Custom Icons visual

JsonAsAsset · UAssetGUI material override · ModActor blueprint entry point

Override Material via Blueprint (No Mesh Edit)

  1. Extract the item’s BP_BDP with retoc. Open in UAssetGUI → Import Data tab.
  2. In the bottom empty row, add: ClassPackage = Script/CoreUObject · ClassName = Package · OuterIndex = 0 · ObjectName = full game path of your material · BImportOptional = False. Click away and back to make it stick. A new empty row appears below.
  3. Fill the new row: ClassPackage = /Script/Engine · ClassName = MaterialInstanceConstant · OuterIndex = negative row number from step 2 · ObjectName = material name only.
  4. Go to Export Data tab → find the ChaosClothComponent (or appropriate mesh component) → look for or add OverrideMaterials ArrayProperty of ObjectProperty. Inside it, map slot numbers to your import rows.
  5. Save → repack with retoc → test.

JsonAsAsset (Import Vanilla Materials)

  1. Install the JAA plugin into Plugins/JsonAsAsset in your Altar project. Enable, restart, configure the export directory and mappings file path in plugin settings. Launch j0.dev.exe.
  2. In FModel, right-click a material → Export Properties (.json). Keep the export path short with no spaces, close to the root drive (e.g. C:\FModelExports).
  3. In UE, click the JAA plugin button → select the JSON → wait. Enable the Stubs checkbox (new in 1.4.1) for crash-free import of any material without a pre-made material store.
  4. MIC parents are not automatically assigned in 1.4.1, set the parent manually after import. If the imported material is missing layers (the MIC Layers tab), edit that material in UAssetGUI instead, JAA doesn’t import the Layers tab.
  5. If JAA isn’t working: close FModel before running JAA; restart UE and re-check plugin settings.

Transparent / Masked Materials

MethodResultNotes
Blend Mode: Masked + Opacity MaskBinary cutout (no gradient)Works reliably for tattered cloth, hair cards
MIC_Necromancer_Amulet_GemTrue translucencyPath: Content/Art/Clothes/Amulet/
/Engine/EngineDebugMaterials/M_SimpleUnlitTranslucentUnlit translucencyColor only, can’t add textures or roughness

Custom Inventory Icons

  1. In UE: create folder Content/Art/UI/Icons/Dynamic_Icons/menus/Icons/armor/YourFolderName/
  2. Import your 256×256 PNG. In UE, prefix it with T_ (e.g. T_MyCuirass). Without the prefix, the game won’t detect the asset.
  3. In xEdit, set the Icon Image path to: Art/UI/Icons/.../YourFolderName/MyCuirass.dds, with .dds at the end even though there’s no .dds file. Do NOT include the T_ prefix in the xEdit path.

Blueprint Behavior Mods (ModActor Entry Point)

  1. Create Content\Mods\YourModName_P\ in your UE project. The folder name must match your final pak filenames exactly.
  2. Create a Blueprint of type Actor and name it exactly ModActor. UE4SS BPModLoaderMod auto-discovers and injects this on game load.
  3. Add a Widget Blueprint (WBP_ModHud) as your mod’s UI layer. In ModActor’s Event Graph, on Event BeginPlay: Create Widget → Add to Viewport. This persists across level changes.
  4. Use Event Tick + Was Input Key Just Pressed on the ModActor (not on a hidden widget, hidden widgets don’t tick) for keypress detection.
  5. Save mod data using SaveGame to slot "Mods/YourModName" (not the root, to prevent crashes when the mod is removed).
  6. Package and place files in Paks\LogicMods\YourModName_P\.
Scroll Box CrashUE’s standard ScrollBox widget crashes on some setups when included in a packaged pak. Workaround: create the ScrollBox at runtime using a Create Widget node rather than placing it in the widget designer. VModernScrollBox and VAltarNavigableScrollBox are custom Altar variants that partially address this but don’t solve all cases.
9

Add Cloth Physics to Clothing bridge

CCA crash explanation · blueprint workaround · standalone cape via amulet slot
Why Custom CCA Files Crash, The Deep ReasonCCA (Chaos Cloth Asset) files crash on load when custom-made, both empty and full CCAs crash. CCA files receive their physics data through a dataflow graph. These dataflows cannot be packed into a pak and are not present in the game files. The dataflow applies data onto the CCA itself at project build time in Virtuous’s internal tools. Without that step, the CCA is incomplete and crashes. Additionally: cloth paint on armor cuirasses renders the painted area static in-game. The suspected culprit is the SkeletalVariants property in generic armor BPs which forces CCA files to be mandatory. Hair and amulets work because they don’t have SkeletalVariants.
What You CAN Do With CCAs, DuplicationWhile you cannot create new CCA shapes from scratch, you can duplicate an existing vanilla CCA and assign it new materials. This means you can have a robe skirt that moves exactly like the vanilla mage robe skirt but uses entirely different textures. Same workflow applies to all CCA slots. Use retoc to extract the CCA, UAssetGUI to change the material reference, repack with retoc. Similarly, blood splatter materials can be duplicated and reassigned using UAssetGUI.
Hair & Amulets Skip Most of ThisHair simulates without the workaround, do only the UE Editor steps below and skip the blueprint setup. Amulets also work directly. The full blueprint workaround is only needed for cuirasses, greaves, and similar.

UE Editor Steps (Apply Cloth Physics)

  1. Import your cloth mesh separately from the armor body. They’ll be re-attached via blueprints.
  2. Open the skeletal mesh → right-click the section to simulate → Create Clothing Data from Section. Name the asset, leave Physics Asset as None for now.
  3. Right-click again → Apply Clothing Data.
  4. Click Activate Cloth Paint: white = simulated (moves freely), black = anchored (fixed to skeleton). Top edge = black, bottom hangs free.
  5. Tune ClothConfigs for physics behavior: stiffness, damping, gravity scale. Reference: UE cloth painting tutorial.
  6. Right-click mesh → Create Physics Asset. Name it PA_YourMod_Cloak.
  7. Open the Physics Asset → replace auto-generated capsules with hand-placed ones on the bones that matter. Set all capsule Physics Type to Kinematic.
  8. In the clothing config, assign your physics asset.

Physics Proxy Mesh

A low-poly duplicate mesh called “PhysicsProxy” is often included in OBR meshes. Physics is calculated on the low-poly version and then interpolated to the actual mesh. If your armor doesn’t need cloth physics: right-click the PhysicsProxy section in LOD 0 → Disable. Or delete the slot entirely. A material assigned to the proxy causes z-fighting and clipping artifacts.

Blueprint Setup for Cuirass + Cape

  1. Create Content/Mods/YourName/. Folder name must match pak filenames exactly.
  2. Create a blueprint named exactly ModActor (parent: Actor). UE4SS looks for this specific name.
  3. Create dummy parent blueprints for your armor type (see Lesson 7 parent chain table).
  4. Create your main blueprint (parent = deepest appropriate dummy). Set Male Mesh and Female Mesh to the armor body mesh (without cloth). Configure Material Slots Hidden In First Person.
  5. Add two Skeletal Mesh components as children of Root (one male cape, one female cape).
  6. Event graph: Event BeginPlay → Branch → Get Female Mesh → Is Valid Soft Object Reference → Set Leader Pose Component → Set Hidden In Game (male cape, check Hidden). Compile.
  7. Assign blueprint + ModActor to a different chunk number from your mesh assets. Package. Drop into Paks\LogicMods\YourName\.
  8. In UAssetGUI, update the BP_BDP path in your biped model form to point to your new blueprint.
Cloth Mesh Appears Doubled (Static + Dynamic)This happens when the cloth mesh is set as the Root Skeletal Mesh Component. The cloth mesh must be a child of the root, never the root itself.
v1.2 Changes to Cloth SetupAfter patch 1.2: VAmuletModularBodyPart behavior changed, you must now add the cape as a child to the Root Skeletal Mesh Component, not assign it directly. For the root, create an empty skeletal mesh (duplicate any SK → disable LOD0). Also: gender swapping via Get Female/Male Mesh nodes in the event graph no longer works as of patch 1.2.

Standalone Capes (Amulet Slot)

Route standalone capes through the amulet slot only, amulets are the only item type that accepts physics meshes directly without the CCA issue. Blueprint parent: VAmuletModularBodyPart. In xEdit, ensure the item uses the Amulet biped flag. Use the Hair mesh slot as an alternative if you don’t mind the workaround. Performance: cloth simulation is CPU-bound and varies significantly between systems. Use a proxy mesh (simplified low-poly version) for high-poly capes: proxy mesh guide.

10

Custom Animations & Replacers visual

JAA skeleton import · framerate · notifies · blendspaces · retargeting

Step 1, Import SKEL_HumanoidSkeleton via JsonAsAsset (Critical First Step)

Use JAA 1.3.7 or newer. This imports the skeleton with virtual bones (IK targets) intact. Virtual bones cannot be recovered from a PSK export, JAA is the only reliable method. The correct skeleton has 378-379 bones (without sockets exported as bones).

Why JAA Skeleton Import Is MandatoryImporting from PSK/FBX without JAA produces a skeleton with a different bone index order. Wrong bone index order = animations that look perfect in the UE editor but play completely broken in-game. The “22 bones missing” warning that appears on import is normal, ignore it if the animation previews correctly.

Step 2, Export SK_HumanoidFull from FModel Correctly

In FModel: Settings → Models → Socket Format → “Don’t Export Bone Sockets”. This prevents sockets from being exported as bones, which adds ~28 extra bones and breaks all IK at runtime.

Step 3, Fix the Extra Root Bone in Blender

The PSK importer adds a root bone named after the mesh file (e.g. SK_HumanoidFull). Fix: rename the armature object to Armature in Blender’s Outliner. This makes UE skip the extra root, and the bone index order matches correctly.

Animation Framerate, Critical for Notify TimingWrong framerate = notifies fire at incorrect points in-game even if they appear correctly placed in the UE timeline.

Correct workflow:
1. Set Blender timeline to 30fps before importing PSA/PSK.
2. Edit or create your animation at 30fps.
3. Export as FBX.
4. Import into UE with custom sample rate of 60fps.

Common mistake: Blender defaults to 24fps. Importing at 24fps makes the total animation length longer than the vanilla version, shifting all notifies out of sync. Verify by exporting the vanilla animation’s JSON from FModel and comparing FrameCount and Duration.

Retargeting Animations from Other Games

Any animation from any source (Elden Ring, The Witcher 3, Skyrim, Paragon, stock UE Mannequin, Mixamo, CMU mocap library, Fab marketplace free tier) can be retargeted to the OBR skeleton.

Recommended UE 5.4 bridge method (krasuepisac):

  1. In your 5.3.2 modding project, migrate the OBR skeleton to a UE 5.4 project via Content → Migrate.
  2. In 5.4: create an IK Rig and IK Retargeter for the OBR skeleton. UE 5.4’s retargeting tools are 1-3 clicks vs. building an entire IK rig manually in 5.3.2.
  3. Retarget your source animations onto the OBR skeleton in 5.4. Export the retargeted animations as FBX.
  4. Re-import FBX into your 5.3.2 modding project. (Assets can’t be migrated back from 5.4 to 5.3.2, but FBX as an intermediate works perfectly.)

For Skyrim animations specifically: import NIF animations into Blender using the NIF importer plugin, export as FBX, then use the retargeter method above.

Animation Notifies, Combat Combo Chaining

Simply importing notifies via JAA is often insufficient, they may need to be added manually. For combat combos to chain correctly, both of the following notifies are required:

BP_ActionNotifyState_ChainWindow
  Begin Action Event Tag: ActionEvent.Attack.ChainingWindow.Enter
  End Action Event Tag:   ActionEvent.Attack.ChainingWindow.Exit

VAnimNotify_ActionNotifyState
  Begin Action Event Tag: ActionEvent.Attack.InputWindow.Enter
  (no End tag)

Removing either one breaks chaining. Confirmed by kei7855 on their shortsword animation replacer mod (mods/2489).

For block to interrupt an attack mid-swing, the start time of this notify must fall within both VAnim_ActionMeleeHitWindow and VAnimNotifyState_ImpactSystem:

BP_ActionNotifyState_TagContainer
  Tags To Transmit: [Input.Action.Combat.Block]
  BeginActionEventTag: ActionEvent.Attack.CancelTags.Add
  EndActionEventTag:   ActionEvent.Attack.CancelTags.Remove
TagContainer Blueprint Packaging RuleThe TagContainer Blueprint must NOT be assigned to any chunk or PrimaryAssetLabel. Its default values must be set to None. Violating either condition prevents tags from exporting correctly, the mod will cook without errors but behave incorrectly in-game with no error message.

Replacing Locomotion Blendspaces Persistently

Injecting a blendspace via AnimSet gets reset when the player equips or unequips a weapon, because the game stores all valid blendspaces in a TMap keyed by GameplayTags. Solution (krasuepisac’s discovery):

  1. Find the TMap that maps GameplayTags to blendspaces inside the AnimBP.
  2. Swap the entry for the relevant tag directly. Tag the actor after modification to avoid re-applying every frame.
  3. This persists through cell transitions, the player actor reference survives cell loads (unusual for UE games but intentional here).

Velocity reference values: Walk ~155 · Run (blendspace activates) ~509. Sprint uses a plain AnimSequence, not a blendspace. Root motion appears fully disabled, everything is root-locked; set root lock to zero on import.

Playing Animations at Runtime (Lua/Blueprint)

Triggering PlayAnimation from Lua works for the first play, but after the animation completes the character stops playing all normal animations permanently (frozen on last frame or T-pose). Use PlayMontage instead, montages blend in and out automatically via UE’s montage system and return the character to ABP-controlled state after finishing. The FullBody animation slot overwrites all other blending when active, use it for full-character override animations like emotes or cutscene poses.

ABP Architecture, Template & Linked Layers

The game uses a template ABP with linked layer instances. Each animation category (combat, idle, locomotion) is a separate linked layer instance within the template. Target the correct layer for the animation type you are replacing, this is why some replacements work and others break unexpectedly.

StrideWarping (standard UE plugin node) handles foot placement based on LocomotionSpeed. Foot lock positions only update when the character starts or stops moving.

Actor Persistence Across Cell Loads

  • The player actor reference persists through cell transitions (unusual for UE games). GameplayTags on the player also persist.
  • NPC actor references are completely destroyed on any cell load. Store persistent NPC mod data in a widget or custom UObject with a reference loop.

Mesh Merging & Morph Targets

UE’s Merge Meshes system combines all equipped armor pieces into one SkeletalMesh at runtime. This process destroys morph targets. NBO avoids this by using bone scaling instead, bone scale survives merging.

Gamebryo Animation (Legacy)

PlayGroup and PickIdle execute on the Gamebryo side but have no visual effect since rendering moved to UE5. AI Package idles still work. NPCs can get stuck in triggered idles, cast a spell on the NPC to break the animation lock.

Animation Workflow, Full Setup Path (miken1ke)

There are two approaches to setting up an animation project:

  • Source build UE 5.3.2 (miken1ke's method), download from EpicGames GitHub (requires account linked to Epic). Run Setup.bat, GenerateProjectFiles.bat, open UE5.sln in VS2022, build. Takes 30min-5hrs. Then switch your OblivionRemastered.uproject to point at the compiled source engine. Community fork available: C0bra5/UnrealEngine.
  • Kein/Altar project (the method documented in this wiki), pre-stubbed project, compiles in minutes. Works for most animation use cases without needing a full source build.

Which to use: For basic animation replacement, the Kein/Altar method is faster. For complex retargeting setups or advanced animation system work where you need the full editor, the source build gives you more control.

Animation File Naming and Folder Structure

From miken1ke's guide: folder structure AND filenames must match the game's exactly. Use FModel to verify the exact path. Example: replacing one-handed sprint = file must be named A_Humanoid_OneHand_Sprint.fbx placed at Content\Art\Animation\Humanoid\ThirdPerson\OneHanded\Locomotion\Normal. Wrong folder or name = animation does not load.

Packaging Settings for Animation Mods (miken1ke)

In Project Settings, ensure: Use Pak File = True, Use Io Store = True, Generate Chunk = True. Clear both "Additional Asset Directories to Cook" and "List of maps to Include in a packaged build". Build Configuration = Shipping (note: this differs from the general mod guide which uses Development, for animation replacers only, Shipping is confirmed to work by miken1ke).

Reference Mods & Resources

  • miken1ke Humanoid Rig (mods/2069), standard community Blender rig built with Rigify, custom-tailored for OBR. About 400 bones with a Rig Layers panel for hiding/solo-ing bone collections. Includes a Costume system (Item tab sidebar in Blender) for body type and equipment visibility, first-person and third-person animation workspaces, and a one-click Export Animation button in the sidebar. Discord: discord.gg/E2f2umtWGd. Articles: Animating for OBR and Rig Manual.
  • kei7855 shortsword animation mod (mods/2489), reference for notify setup
  • Custom bow animations require two notifies: VDrawArrow (fires the arrow, C++ class from Altar) and VAnimNotify_PlayWwiseSound (plays the fire sound, from the Wwise plugin). Both must be present. Building a Wwise-only project without Altar gives you PlayWwiseSound but removes VDrawArrow, breaking arrow firing. Credit: .yeah.nah.yeah.
  • Leaning disable mod (mods/2570), disables forward-lean locomotion behavior
  • Alpakit for UE 5.3.2, OBR-compatible fork; enables one-click auto-deploy of BP mods
  • Jake Alaimo live mocap, live mocap modded into OBR, reference implementation
11

Facial Animation & the MetaHuman System visual

DNA files · RigLogic · Audio2Face · voice-line AnimSequence sync
Community DiscoveryThe game uses the MetaHuman system for facial animation. Face is animated entirely with bones, not morph targets (morph targets are only used in CharGen character creation). The face curves are identical to MetaHuman curves, confirmed by comparison with MetaHuman example projects. Discovered by izedev_55749.

Architecture

  • Every voice line has its own AnimSequence stored in Content/Art/Animation/Humanoid/Facial. Warning: the sheer number of files in this folder will freeze FModel on browse, navigate by known path instead.
  • Audio and face animation play independently, they start at the same time with no synchronization tracking. This means replacing either leaves the other running unchanged.
  • Mouth/speech sync uses a UE function called speech2face (procedural, no per-frame data).
  • TABP_FacialPose is the facial pose AnimBP.

Setup for Custom Head Facial Animation

  1. Enable the RigLogic, MetaHuman, and MetaHuman Identity plugins in your Altar UE project.
  2. Attach the appropriate .dna file to your face SkeletalMesh asset. DNA files are NOT standard .uasset files, they require CUE4Parse to extract from the game:
foreach (var kv in provider.Files) {
    if (kv.Key.EndsWith(".dna")) {
        var outPath = Path.Combine("extracted", Path.GetFileName(kv.Key));
        await using var stream = provider.SaveAsset(kv.Value,
            new FileStream(outPath, FileMode.Create));
    }
}
  1. Create and reference a dummy ABP_HeadPostProcess blueprint. This is required for the DNA to drive the facial bones.
MetaHuman Plugin WarningEnabling the MetaHuman plugin may permanently break your UE project for some users. Test in a project copy first. Khajiit uses a different head rig from the humanoid races.

Community DNA Files

WSDog extracted and publicly shared DNA files for all base races and named NPCs:

Generating New Facial Animations from Audio (Audio2Face Workflow)

  1. Use NVIDIA Audio2Face to generate blendshape/shape key animation from your audio file.
  2. Pipe through MetaHuman Live Link into UE 5.3.2. Record the result as a UE AnimSequence.
  3. OBR face curves share the exact same names as MetaHuman curves, they map directly with no translation needed.
  4. Replace the existing AnimSequence at the matching path in Content/Art/Animation/Humanoid/Facial.
  5. MetaHuman plugin supports batch processing for automating generation from multiple audio files at once.

Audio-Anim Sync for Localization

Since audio and face animation play independently, to fix out-of-sync dubbed audio (where the new audio is longer than the original): replace the AnimSequence at the correct path with a new one timed to match your audio duration.

12

Wwise Audio Integration visual standalone audio solved

External Sources · full Wwise setup · custom standalone audio (solved 2026)

The game uses Audiokinetic Wwise 2023.1.8.8601.3258 for all audio. UE’s built-in audio system has only two dummy assets (DummySoundClass, DummySoundCue).

What Works Without Full Wwise Integration

  • Sound replacement: swap existing .wem files inside a pak (see Lesson 2)
  • Posting vanilla Wwise events from Blueprints: use the VAudioHandlers subsystem and BPF_PostEvent (or PostEventAtLocation for spatial audio)
  • Turning vanilla audio events on/off from Blueprints using vanilla AkAudioEvent class defaults

Full Wwise Integration Setup

Version Must Match ExactlyUse Wwise version 2023.1.8.8601.3258 exactly. Mismatched versions cause binary incompatibilities that are difficult to debug.
  1. Install Wwise from the Audiokinetic Launcher, select exactly the version above.
  2. In your Altar project folder, delete the existing Wwise and WwiseNiagara plugin folders.
  3. Use the Launcher: Unreal Engine → Integrate Wwise into Project.
  4. Fix the compile error in Source/Altar/Public/VAltarAkPortalComponent.h: update the include to "AkAcousticPortal.h" and parent class to public UAkPortalComponent (the Altar project stubs reference the outdated name AkPortalComponent).
  5. Build via Visual Studio. Name the Wwise project Altar and place it at C:\Altar-main\WwiseAltar\ to match paths in the game’s DefaultEngine.ini (not strictly required but simplifies cross-referencing).

External Sources, Custom .wem Files Without Rebuilding Soundbanks

External Sources allow specifying which .wem file Wwise loads at runtime without rebuilding soundbanks. This enables one framework mod to handle the Wwise plumbing while other mods simply ship .wem files.

Enable External Sources by overriding the game’s DefaultEngine.ini from your mod pak:

[Audio]
WwiseFileHandlerModuleName=WwiseSimpleExternalSource
Critical External Sources Bug (.yeah.nah.yeah.)External Sources produce a valid Playing ID and correct Wwise stats, but produce absolutely no audio unless the mod actor has a static mesh component with an actual mesh assigned and packaged. The moment a mesh is present in the packaged mod, External Sources work correctly. The reason for this requirement is not fully understood as of mid-2025.

Required Setup for External Sources

  1. Create a DataTable using the External Source cookie struct (provided by Wwise).
  2. Populate with: cookie value, media ID, codec ID, media name (filename of your .wem).
  3. Override DefaultEngine.ini to enable WwiseSimpleExternalSource.
  4. Include your .wem file in the pak at the correct staging path.
  5. Ensure your mod actor has a packaged static mesh component with a mesh assigned.

Getting a Free Non-Commercial Wwise License

Creating new Wwise audio events (new sound forms in the ESP with new UE audio) requires Wwise’s authoring tools to generate new .bnk soundbank files. Getting a free non-commercial license requires a manual review by Audiokinetic staff, fill out the form at audiokinetic.com/en/pricing honestly.

The Custom Audio Research Wall (Sep 2025 deep-dive by .yeah.nah.yeah.)

This section documents what actually happens when you push beyond sound replacement, based on months of research. Understanding these limits saves enormous time.

The Project GUID ProblemEvery Wwise project has a unique GUID. The game’s vanilla GUID is a specific non-zero value (~147545). A fresh Wwise project has GUID = 0. When you try to load a custom soundbank, Wwise checks GUIDs against its Init.bnk and rejects mismatches silently, audio appears to play (valid Playing ID, correct Wwise stats) but produces absolutely no sound. This is the primary wall for custom audio. The GUID must either match the vanilla project or be spoofed via a C++ DLL hook.
  • What IS exposed via UAkGameplayStatics: PostEvent, PostEventAtLocation, PostEventOutdoors, SetRTPCValue, SetSwitch, SetState, LoadInitBank, StopAllSounds. These let you trigger, modify, and stop existing vanilla events.
  • What is NOT exposed: LoadBankByName. The game uses auto-bank loading via UAkAudioEvent.RequiredBank + project setting “Load Banks Automatically.” You cannot load additional custom banks without C++.
  • SID (ShortID) matching: The event’s Short ID and the soundbank’s Short ID must match. Set the soundbank’s hardcoded ID in UE to match the event Short ID. The long GUID strings in FModel appear to be ignored at runtime, only the SIDs matter.
  • Audio routing is Wwise-only: The shipped game uses WwiseOnly audio routing set at package time. AudioMixer and AudioLink are compiled out. Changing DefaultGame.ini audio routing at runtime appears to have no effect. A dummy SoundClass and SoundCue exist in /Dev/ but are empty stubs.
  • UE native audio (non-Wwise) CAN play: au.debug.generator 1 in console plays a UE native test tone (disable with au.debug.generator 0, don’t do this in any menu). This confirms UE audio is not fully stripped, just disabled for normal use.
  • BNK injection via BNK Editor: You can inject new events into existing vanilla .bnk files using BNK Editor. Keeps all IDs in the vanilla Wwise project context, so GUIDs match.
  • The External Sources route: Wwise events with External Sources pre-configured at project-build time accept custom .wem files at runtime without rebuilding soundbanks. Check if any vanilla events have External Sources enabled (rare but possible).
  • Audiokinetic-provided UE nodes: IsGame and IsEditor nodes work correctly in shipped builds. Use these in BP to guard audio logic. Wwise nodes like Post Event and Wait for End function in editor and fire correctly in-game when triggered by Event Begin Play on a ModActor.
  • ModActor Event Begin Play quirk: Event Begin Play fires on ModActor spawn (typically game load / new game only). It does NOT fire on subsequent save loads if the actor already exists. Use Event Tick with a delay gate, or trigger audio from in-game hooks instead of Begin Play for persistent behavior.

The path the community assumed: for a long time a C++ DLL hook wrapping FAkAudioDevice::LoadBank() and FAkAudioDevice::PostEvent() was thought to be the only fully general solution. See dicene/OblivionRE_CppMods for the C++ approach. That is no longer the only way, see the breakthrough below.

SOLVED 2026 · Custom Standalone Audio Plays With No C++ HookBrand-new Wwise audio (not a vanilla replacement) now plays in the packaged shipping build using pure UE4SS Lua. Proven by the ZA WARUDO / TimeStop mod (CosmicBoogaloo / DreamEater). The “research wall” above was real, but the actual blocker turned out to be the post method, not the bank or the GUID. The bank was correct the whole time. Full step-by-step writeup: see the How They Did It → Custom Standalone Wwise Audio page.

The Working Pipeline (condensed)

  1. Wwise plugin must be exactly 2023.1.8.8601.3258, any other build produces silent bank-version mismatches (bank version X vs runtime 150).
  2. Three Altar source fixes before the project compiles with Wwise: in VAltarAkPortalComponent.h include "AkAcousticPortal.h"; in VMusicPlayer.h swap "EAkCallbackType.h""AkGameplayTypes.h"; null-guard FAkAudioDevice::AddDefaultListener (both in_pListener and IWwiseSoundEngineAPI::Get()). Build with Build.bat AltarEditor Win64 Development, not the VS GUI.
  3. Delete the Wwise Motion device from the project (Default_Motion_Device, Motion Factory Bus, the Factory Motion tree) and save, otherwise SoundBankGeneration fails entirely (MissingPlugin / WwiseConsole failed with error 1) and banks silently never update.
  4. Keep media in-memory (Stream unchecked) so the audio bakes into the bank’s DATA chunk. Route to the default Master Audio Bus (FNV-1 hash 3803692087, same ID the vanilla master bus uses, so no GUID spoofing needed). Generate banks through the UE Wwise integration, and delete any stale loose banks at the GeneratedSoundBanks\ root that shadow the fresh Windows\ ones.
  5. Two paks, two tools (because OBR uses IoStore): cooked uassets (AkAudioEvents, ModActor) go through retoc to-zen as a .pak+.utoc+.ucas trio into LogicMods; loose .bnk/.wem go through UnrealPak with an explicit filelist into ~mods. A lone UnrealPak pak will not mount cooked uassets. .bnk paths are flat in WwiseAudio/; .wem filenames must be the media SID.
  6. NEVER ship an Init.bnk / InitBank.uasset and never call LoadInitBank() at runtime, either one silences ALL game audio by overwriting the vanilla bus routing.
  7. Post the event from Lua with PostEventAtLocation, it is the one exposed post method with a signature UE4SS Lua can call (no blocking PostEventCallback delegate). The BP Post Event node crashes; PostEvent/PostOnActor/PostOnComponent all hit the delegate wall.
local AkStatics = StaticFindObject("/Script/AkAudio.Default__AkGameplayStatics")
local ev = StaticFindObject("/Game/WwiseAudio/Events/TimeStop/TimeStop.TimeStop")
if not (ev and ev:IsValid()) then LoadAsset("/Game/WwiseAudio/Events/TimeStop/TimeStop"); ev = StaticFindObject("/Game/WwiseAudio/Events/TimeStop/TimeStop.TimeStop") end
local player = UEHelpers.GetPlayer()
local loc = player:K2_GetActorLocation()
local rot = {Pitch = 0, Yaw = 0, Roll = 0}
-- Returns a valid (non-zero) playing ID and plays the custom in-memory media:
local id = AkStatics:PostEventAtLocation(ev, loc, rot, UEHelpers.GetWorld())
Key RealizationThe empty Media array on an in-memory AkAudioEvent is normal, the media lives in the bank, not the event asset. And FModel failing to parse your .bnk (IndexOutOfRange) is an FModel parser fragility, not a broken bank, verify via SoundBanksInfo.json or a manual chunk parse instead. Don’t chase the bank; the bank is almost always fine.
13

World & Mapping, What’s Actually Possible visual

CK vs UE terrain · cells · grass · navmesh · Nanite · heightmap tools
The Core Architecture BreakThe single most important thing to understand: terrain no longer lives in the Construction Set. All actual terrain geometry and heightmap data is on the UE side, stored in generated .umap files. The CK terrain editor exists in name but has no visible effect at runtime, Unreal’s terrain takes over.

Current Status Table (mid-2025 community research)

FeatureStatusNotes
New interior cellsWorkingVia MagicLoader (Haphestia’s tool)
New exterior cellsNot workingnafnaf_95 confirmed, as of mid-2025
New worldspacesNot workingAs of mid-2025
CK terrain editingNo effectUE terrain takes over; CK data unused
UE heightmap editingNo clean workflowNo polished mod tools yet
Grass type modification via CSNo effectGrass is UE-side foliage paint
Navmesh editingNot possibleBaked into UE umaps
LOD configurationNot neededNanite handles it automatically
.umap direct editingWorks via UAssetGUIVersion-sensitive; breaks on updates
Map bordersCan be disabledTerrain ends at square boundary
Custom map loadingWorkingMust use MagicLoader for Gamebryo sync

Grass & Foliage

All visible grass is part of generated UE maps, painted onto landscape materials during UE map generation. TESGrass records in the CS have no in-game effect. Adding custom trees and grass:

  • TESSyncMapInjector approach (most ergonomic): link a UE static mesh to a TES mesh record in the CS and place it using standard CS workflows. Demonstrated for water meshes (mods/4874) and trees (mods/4740).
  • Blueprint/Lua spawn approach: a custom Blueprint spawns a mesh at dynamic coordinates from Lua. Requires manual coordinate input.
  • Mad’s placement system (minuteready): spray mesh instances organically by running around in-game using Mad’s “rapid spell” + “add instance on X” system, writing positions to a text file, then importing them. Labor-intensive but gives natural placement feel.
  • Removing existing UE grass: edit each .umap via UAssetGUI to remove grass foliage actors (tedious, version-sensitive), or hook the grass Blueprint creation in Lua and destroy instances on spawn (theoretical).

Heightmap

The old Gamebryo heightmap still exists in game files but does NOT drive the visible terrain. The UE side has its own heightmap. No clean extraction or re-injection workflow exists yet.

Using surf / blenderumap3: importing .umap files via surf/blenderumap3 confirms the layer-per-category structure, exporting only the umap for a specific area returns a single tree mesh, not the full scene. The layers need to be loaded together to reconstruct the full environment. Tools: search for BlenderUMap2 and surf in the UE modding community.

The “All of Tamriel” heightmap from Nexus (Skyrim mods/52675) has been discussed as a source for adding adjacent provinces. Key note from grimlock_arts: TES game heightmap data is deceptively low detail, the games rely heavily on placed rock models pushed into the ground to create the impression of complex terrain. The raw heightmap is just a flat base layer.

Heightmap tools referenced by the community:

Map Borders & Open Space

Map borders can be disabled. A large open stretch of land runs from roughly 1/3 to 1/2 the size of the main game map, fully walkable and tree-free, extending all the way to the ocean. No water in the way. This is the most viable space for exterior expansion content as of mid-2025.

Navmesh

Navmesh editing is not currently possible from the modder side. NPCs cannot be directed reliably in new areas without navmesh. Workaround: add a small gatehouse cell or confined space to prevent NPCs from wandering beyond intended areas.

Nanite & LODs

OBR uses Nanite for static mesh geometry, which handles LODs automatically. You do not need to create LOD meshes or configure LOD settings. There is no DynDOLOD equivalent. FModel exports only the fallback mesh (low-poly version for Lumen), not the full Nanite detail. For full extraction use c0bra5’s simple-nanite-parser (Python-based, exports to GLTF with vertex colors).

CK-UE Terrain Height Mismatch Workaround

When building outside the vanilla map border, CK terrain height and UE terrain height diverge. Workaround (nafnaf_95): work one cell at a time, build at CK-default height, then select all objects and shift them vertically until they sit correctly on the UE terrain surface. Tedious but workable for exterior static object placement.

Trees & SpeedTree

Trees are handled via SpeedTree assets with built-in wind animation, baked into per-cell UE umap files with foliage instancers. Map file names are hashed making systematic modification very difficult. Adding new trees as individual Blueprint actors works but incurs significant performance cost. The SpeedTree SDK (required for full authoring) has enterprise-only commercial licensing (~$100k+).

14

Build a New Interior Room or Player Home bridge

Construction Set · MagicLoader · SyncMap, no NPC pathfinding

New interior cells work via MagicLoader. The tool modifies the CellsToMapPath DataTable with new entries that map Gamebryo cells to UE map files, making the game load the correct UE level when a cell is entered.

  1. In the Construction Set, load MagicLoader’s example file (IntTestMod.esp) → Set As Active File.
  2. In Cell view, create a new interior cell. Build the room using existing tileset pieces. Place furniture, containers, lights. Add a door and use Door Teleport to connect it to an exterior location.
  3. Save the esp. Open in xEdit (all Altar files loaded) and run Haphestia’s Fix and Port Script (fixmod).
  4. Run the Smart Mapper xEdit script. Copy the resulting .ini to Data\SyncMap.
  5. Run MagicPatcher to build the string table patch .json for room names and door labels.
  6. Run MagicLoader → “Do Magic!” once.
  7. Enable your esp in Plugins.txt, above AltarDeluxe.esp.
Every Placed Object Must Be SyncMappedEvery object you place that isn’t already in the base game’s SyncMap needs its own entry. Anything missing becomes invisible or causes a CTD on cell entry. Run Smart Mapper after every significant edit session.

Essential External Guides

Reference Proof-of-ConceptThe first mod to demonstrate placing static objects in a custom interior cell: “New Player Home, Abandoned House” (mods/828).
Known Limitation, No NPC PathfindingNPCs and creatures placed in new cells have no pathfinding data and stand frozen. No clean fix as of mid-2026. Design around this: player homes, storage rooms, and static setpiece spaces work great. NPCs are best avoided in new cells or confined with physical barriers.

MagicLoader Known Bug (Fixed in v1.1)

An early version of the Fix and Port Script overwrote FULL name fields on existing records, causing custom items and locations to lose their names. This was fixed in v1.1: “Fixes clearing IDs on newly added records by mistake.”

Map Marker Shows <MISSING STRING TABLE ENTRY>

If your player home map marker displays <MISSING STRING TABLE ENTRY>, a string table entry is missing from MagicLoader's configuration. Fix: in xEdit, set the map marker's FULL, Name field to the naming scheme LOC_FN_NameOfCell (e.g. LOC_FN_MyHideout). Your MagicLoader .json config file must have a matching entry:

{
    "Plugin": "MyMod.esp",
    "NewCells": ["ed3"],
    "FullNames": {
        "LOC_FN_MyHideout": "My Hideout"
    }
}

Source: Selene310187's Player Home Problem Solving Guide

Later Changes to Placed Objects Not Taking Effect

If you move an object (e.g. reposition a bed) and the change doesn't appear in-game even after reinstalling the mod: make a clean save without the plugin loaded, then reload with the plugin. This forces the cell to regenerate from the ESP rather than from cached save data. Source: Selene310187.

Missing Walls in New Cells

Statics placed in the original CS editor have no XAAG and XACN records, which the Remaster requires for geometry rendering. Haphestia's Fix and Port Script (fixmod) adds these records automatically. This is why running fixmod is a mandatory step, skipping it causes walls, floors, and other statics to be invisible or missing. Source: Selene310187.

15

Custom Creatures via Blueprint Duplication bridge

The “Panther Method”, duplicate only as far up the chain as needed
  1. In FModel, find the base creature blueprint you want to modify. Note which blueprints it references.
  2. Trace references upward, those BPs reference their own parents. You’ll find a hierarchy of AI, animation, skeleton, etc.
  3. Duplicate only as many levels as you need. Stop at the level where you want to diverge. To replace mesh and textures while keeping vanilla AI and animations: duplicate only 2-3 levels.
  4. In your duplicated top-level blueprint, swap in your new mesh, textures, or behavior values.
  5. Create the ESP form for your new creature in xEdit. SyncMap it via TesSyncMapInjector. Package blueprints and form.
This Pattern GeneralizesParent-chain duplication applies to any actor edit where you want to keep most vanilla behavior and diverge at exactly one point. Works for NPCs, creatures, special actors, any blueprint hierarchy in the game.
New NPCs Crash Without SyncMappingNew NPC records created entirely from scratch crash the cell unless properly registered with TesSyncMapInjector. NPCs must reference an existing Blueprint for their UE-side representation. You can copy an existing NPC form and modify it, faces don’t always assign correctly but the NPC will function.
16

NPCs, Custom Races & Hair Modding bridge

Race form setup · AllRaceModifications table · hair phenotypes · strand hair

Adding New NPCs

  1. Create the NPC record in CS or xEdit. Use an existing vanilla race for reliable results, non-vanilla races default to Imperial appearance at runtime if the UE form can’t be found.
  2. For the UE side: clone an NPC form file from FModel that matches the intended race/gender.
  3. Edit the cloned form in UAssetGUI to change name references.
  4. Register in TesSyncMapInjector.
  5. Place the NPC in a cell via CS.
Custom Race NPC Loading OrderIf getting an NPC to use a non-vanilla race, ensure the custom race PAK loads early by prefixing the pak filename with 0000_. At runtime, if UE can’t find the race form, it silently defaults to Imperial.

Custom Races

Custom races require:

  1. An ESP with the new race record.
  2. A UE race form blueprint at /Game/Forms/actors/race/CustomRace.uasset.
  3. Entries in the AllRaceModifications data table (edit via JsonAsAsset or retoc + UAssetGUI).
  4. Phenotype and hair tables updated to include the race.

Adding fully custom playable races with all CharGen sliders is still in research as of mid-2026. Non-playable custom races (for NPCs) work when given a vanilla voice type. The race menu UI reads from UE-side data tables.

Hair Modding

Hair phenotype files live at /Game/Dev/Phenotypes/Hair/ as data tables listing available hairstyles per race.

  1. Extract the existing HP_[Race]_HR_[StyleName].uasset file using retoc.
  2. Inspect with UAssetGUI or JsonAsAsset.
  3. Create your new hair mesh in UE project.
  4. Add an entry to the race’s phenotype hair data table.
  5. Use RMU (Race Menu Utility) for runtime loading without opening ShowRaceMenu: rmu set hair /Game/Dev/Phenotypes/Hair/HP_RaceName_HR_HairName.HP_RaceName_HR_HairName
Hair LODs and Hairstyle Menu (mods/1671)Custom hairstyles have no LODs by default, incurs a performance cost at distance. Adding hairstyles to the CharGen menu list requires icon entries and is more complex than classic Oblivion. The RMU console command is the reliable workaround for applying custom hair without UI integration. Reference: Colovian Cuts (mods/1671).
Hair Import Scale IssueA known issue causes hair meshes to import at the wrong scale in some workflows. If hair appears too large or too small in-game, check the mesh dimensions in UE against a known-working vanilla hair mesh and scale accordingly.

Hair texture format: use the _RAUD suffix multi-channel format. Always export as TGA from FModel, the alpha channel contains the opacity/strand mask, critical for proper hair card rendering.

Facial animation compatibility: modifying head meshes (including head-attached hair) can break facial lip-sync if the mesh topology changes significantly. Blinking and face morphs may continue to work even when lip-sync breaks.

Strand hair: true strand (groom) hair via UE5’s Groom system has been partially achieved but the full binding pipeline is not documented. Hair card replacement is the stable workflow.

17

Dialogue, Voice Lines & Lip Sync bridge

TSMI dialogue mapping · subtitle timing · lip sync workarounds

Text / Subtitle Changes

ESP-side text: changing dialogue text in CS/xEdit works for most dialogue. Display names use the LOC_FN prefix system, forward AltarESPMain changes or names break. Localization files: some UI strings are in UE localization tables inside the PAK and require editing the localization .uasset files. Note: only one mod’s localization table wins in case of PAK conflict.

Reusing Existing Voice Lines for New NPCs

  1. Create the dialogue topic in your ESP.
  2. Use TesSyncMapInjector to map your new dialogue FormID to an existing dialogue form blueprint path. Format: TOPICTYPE_espname_FormID. Example:
    GREETING_MyMod_00000ED6=/Game/Forms/miscellaneous/dialog/GREETING_Someactor_000479e1.GREETING_Someactor_000479e1
  3. The NPC’s voice type (set in the race ESP record) must match the target dialogue form’s voice set. Using a non-vanilla race? Set the race to a vanilla voice type to prevent crashes.

Subtitle Display Duration Without Voice

Placing .mp3 files in the legacy Sound/Voice/ folder matching the vanilla dialogue filename convention controls subtitle display duration, even though no actual audio plays. The mp3 file’s duration equals the subtitle display time, useful for subtitle-only localization mods.

Lip Sync

Lip sync is handled by UE animation assets linked to dialogue forms. Current workaround: use an existing dialogue form that has entries for multiple races, the game will attempt to use the matching race’s lip sync animation. The lip-sync side is now fully mapped. It is a baked facial AnimSequence referenced from the TESTopicInfo asset, not a Wwise/audio-driven system (the “speech2face” mystery is resolved). See the Dialogue, Voice & Lip Sync deep-dive for the complete pipeline. The remaining blocker is interior Wwise bank residency (custom banks post fine outdoors but return playing_id 0 indoors).

✓ You’ve Finished the Making-Things SectionEvery type of mod in Oblivion Remastered is covered. Step 5 is writing actual code, OBScript, OBSE64, and UE4SS Lua, to make the game do things impossible any other way.

Step 5 of 5

Scripting & Custom Code

Three scripting surfaces mirror the two-engine architecture. The real craft is making them communicate with each other.

The Three Scripting Surfaces

brain   1 · OBScript (Construction Set)

The classic Oblivion scripting language, bread and butter for enchantments, spell script effects, object/quest scripts, and AI packages. The UESP Construction Set wiki is ~20 years of OBScript documentation and is the essential reference. Any function marked as OBSE on that wiki will not work in the Remaster.

brain   2 · OBSE64 (C++ DLL Plugins)

OBSE64 is currently only a DLL/plugin loader, it does NOT extend OBScript with new functions, despite the misleading name. To write C++ DLL plugins, use commonlibob64 for reverse-engineered class headers. OBSE64 is version-locked: any game binary update breaks it until offsets are manually updated. See a working plugin example at cnf13/DeleteSpells.

visual   3 · UE4SS Lua + Blueprint Loaders

Some capabilities exist only on the Unreal side: hiding armor pieces, driving animation, Niagara VFX, spawning actors. Use the Nexus OR-specific UE4SS build (v3.0.1-394-g437a8ff), the generic GitHub experimental build doesn’t hook into UE 5.3 properly and may not recognize Execute Console Command nodes from Blueprints.

Reality Check on Custom Magic EffectsCustom magic effects are hardcoded-unsupported in the Remaster, use OBScript Script Effects instead. The power comes from combining surfaces, which the notification bridge below enables.

The Notification Bridge, OBScript → Lua

Worked out by MadAborModding on top of a notification-parsing method by Dicene. This is the breakthrough that unlocks “impossible” mods like levitation, custom physics casts, and transformations.

OBScript can send on-screen notifications, and Lua can read and instantly hide them. A notification becomes a silent message channel from the Gamebryo brain to the UE face.

Brain side, OBScript attached to a spell, quest, or item

ScriptName madLevitationScript

begin ScriptEffectStart
    message "madLevitationScriptStart"
end

begin ScriptEffectFinish
    message "madLevitationScriptEnd"
end

Face side, Lua hook that reads, hides, and reacts

RegisterHook("Function /Script/Altar.VHUDSubtitleViewModel:ConsumeNotification",
function(hudVM)
    local hudVM = hudVM:get()
    local text = hudVM.Notification.Text:ToString()
    if text:match("madLevitationScriptStart") then
        hudVM.Notification.ShowSeconds = 0.0001   -- hide from player instantly
        ToggleFly()
        return
    end
    if text:match("madLevitationScriptEnd") then
        hudVM.Notification.ShowSeconds = 0.0001
        DispelFly()
        return
    end
end)

Setting ShowSeconds = 0.0001 effectively hides the notification before it can appear on screen. Source: VHUDSubtitleViewModel.cpp. Live reference implementation: Levitation, UE4SS (mods/3334) version “A”.

Post-June 2025 Patchconsole.ExecuteConsole() now requires a fourth true parameter to work correctly. If your console commands silently stopped working after a patch, add true as the fourth argument: Kismet:ExecuteConsoleCommand(pc.player, command, pc, true).

Lua → OBScript (The Reverse Direction)

Both OBScript and UE4SS have access to Oblivion global variables. This is the primary reverse channel:

  1. In Lua, on your event, call: Kismet:ExecuteConsoleCommand(pc.player, "ObvConsole set MyGlobalFlag to 1", pc, true)
  2. An Oblivion quest script (Begin GameMode) polls that global; when it sees 1, does its thing (can fire a “silent” notification back to Lua), then resets the global to 0.
  3. Lua reads the return notification and continues.

UE4SS Lua, API, Patterns & Gotchas

Essential Functions

  • RegisterKeyBind(Key.N, fn), bind a hotkey
  • RegisterHook("Function /Script/Altar.Class:Method", fn), run on an engine function call
  • NotifyOnNewObject(class, fn), react when a new object of a class spawns; the standard way to grab a VPairedPawn reference instead of polling
  • FindAllOf("ClassName"), get all live instances of a class
  • StaticFindObject(path) / LoadAsset(path), find or load an asset
  • require("UEHelpers"), player, controller, world, math helpers
  • ExecuteInGameThread(fn), run on the game thread (required for spawning)
  • LoopAsync(ms, fn), repeating timer; return true to stop; save the handle to cancel later
  • FName.new("E"), construct FNames correctly. Passing a raw string where an FName is expected hard-crashes.
Common Gotchas That Cost Hours
  • TArray iteration: use items:Get(i) and #items, NOT items[i]. Use value:get() inside loops to access the real object.
  • tostring() on a UE object can crash the game.
  • Post-hook doesn’t see changes a pre-hook made to local values, post-hook sees original values only.
  • Debug UE4SS build needs the right UE4SS-settings.ini, the end-user ini can crash on launch with “no debugging symbols.”
  • Many UE4SS mods together → CTD: a mod stable alone can crash when many UE4SS mods load together. Problem is timing/order via mods.txt/mods.json. Deferred hook system is the solution (see dotaxis’s snippet in #ue4ss-snippets).
  • RefreshAppearance crashes when multiple mods call it simultaneously. Limit to only the aspect you’re changing; call it twice within your own mod.
  • StaticFindObject('/Script/Engine.Default__KismetSystemLibrary') is the correct way to get KismetSystemLibrary, UClass.Load(...) is not valid in this context.
  • LiveView can crash if left focused on an Actor too long (confirmed crash on FakeRootDistanceInterpSpeedFactorCurve). Dump to file instead.

Deferred Hook System

Solves hooks failing when their target function isn’t loaded at Lua startup, waits until FindFirstOf succeeds. Source: #ue4ss-snippets Discord (dotaxis, May 2025). For TypeScript fans: TypeScript-to-Lua compiles TypeScript to UE4SS Lua with better type checking.

Spawning Actors from Oblivion Forms (Blueprint)

Use OblivionActorFactorySpawn Actor from Form at Location. Pass a live TESForm reference. Spawned actor may not have its pairing set correctly but can receive calls like Jump(). Example: blueprintue.com/blueprint/fo7y5owq/.

Widget & UI Blueprint Tips

  • Widget Switchers can replace Scroll Boxes if Scroll Boxes crash in packaged paks.
  • Hidden widgets do not tick, put tick/keypress logic on the mod actor instead. Or use opacity 0 + Not Hit-Testable (Self & All Children) visibility.
  • To get a ViewModel reference from Blueprints: find it from Lua and pass via console command or custom event.
  • References: Custom Logger (Dmgvol) · Config Variables & Key Remapping
Two Naming Conventions to InternalizeOblivion’s UE classes are prefixed V (e.g. VInventoryMenuViewModel, VMisc, VHUDSubtitleViewModel). The central actor class for player and NPCs is AVPairedPawn / AVPairedCharacter, most character data hangs off its components. Standard UE conventions: A = Actor · U = non-actor UObject · F = struct · T = container/template · leading lowercase b = boolean.

Finding Hooks

The Kein/Altar SDK repo is the hook directory. Search it on GitHub for a class/function name (e.g. repo:Kein/Altar UVActorBehaviorBase) to find the exact /Script/Altar.Class:Function path and its C++ signature. Also use the UE4SS GUI / LiveView. Use regex search: ^Function.+Cast to find hookable functions efficiently.

Console Commands from Lua (Robust Method)

local UEHelpers = require("UEHelpers")
local pc = UEHelpers.GetPlayerController()
local Kismet = StaticFindObject('/Script/Engine.Default__KismetSystemLibrary')
ExecuteInGameThread(function()
  if pc:IsValid() then
    Kismet:ExecuteConsoleCommand(pc.player, command, pc, true)  -- true required post-June 2025
  end
end)

The UE console connects to the Gamebryo console only after the game starts. Console commands work after load, not at startup. get commands via console do NOT return values to Lua, use FindFirstOf("ATMSubsystem"):GetTime() patterns for reading state.

Blueprint Function Multiple Return Values (Lua Gotcha)

BP functions with multiple output parameters return values in inconsistent positions depending on whether any output is a TArray.

-- BP function returning (string1, bool1):
-- result: param1.string1, param1.bool1

-- BP function returning (TArray_strings, bool2):
-- result: param1 = the TArray directly, param2.bool2 = the bool

-- Rule: always regenerate Lua types via the UE4SS property viewer
-- (Tools → Generate Lua Types) before calling unfamiliar functions.
-- Never assume output positions blind.

Detecting Player vs NPC

  • actor:IsPlayerCharacter() → returns true for the player. Alternative: NPCs have .AVPairedPawnAIController; the player doesn’t.
  • No clean “player death” hook exists. Pattern: hook DoRagdoll (fires for any ragdoll, including paralyze/KO) and filter with IsPlayerCharacter() plus a real “is dead” check so KO and paralyze don’t false-trigger.

Inventory from Lua, Traps

Inventory ViewModel TrapsFindFirstOf("VInventoryMenuViewModel") returns a fake/empty instance. The real one is hidden inside WBPModernPlayer. Reading the fake one gives an empty list and items with .form = 0000. Fetch inventory right after the player loads and BEFORE opening the inventory menu, opening it wipes/reshuffles the exposed list. An AddItem mod can cause an inventory wipe that breaks other mods reading the viewmodel. Struct fields: Name, Price, Weight, WeaponDamage, ArmorRating, Health, Count, Icon, Type, bIsEquiped, bIsFavorite, InventoryIndex, form, StatusFlags.

Time & Fast Travel

Get current time: FindFirstOf("ATMSubsystem"):GetTime(). Enum: EATMTimeUpdateSource {FROM_INIT, FROM_TICK, FROM_GAMEPLAY, FROM_DEBUG}. To freeze time across fast travel: store the hour on OnFadeToBlackBeginEventReceived, restore on OnFadeToGameBeginEventReceived. Setting time via the property directly doesn’t persist, use set gamehour to N via console.

Magic / Enchantment Internals (Advanced)

  • TESSync.DynamicForms is a TMap (~246 entries) mapping runtime FormIDs → forms (e.g. 0xFF005EC7 → TESObjectCLOT /Game/Forms/items/clothing/TESObjectCLOT_2147479875). This is the way to reach dynamically-added forms at runtime.
  • Enchantment form chain: UTESEnchantment → UTESMagicItemForm → UTESForm. UTESEffectSetting holds EnchantEffect, EffectShader, CastingBlueprintClass, ProjectileBlueprintClass, AreaEffectBlueprintClass, HitEffectBlueprintClass, socket names, and GetEnchantEffectID/EffectShaderID/AssociatedItemID().
  • Magic VFX bounce: you can get the magic effect that shaped a projectile (e.g. FIDG for fire) but not the actual spell that was cast.
  • EVMusicType enum contains all music state data. No hooks have been found yet to intercept individual music state transitions reliably.

Inventory Struct Fields (Reference)

The full FOriginalInventoryMenuItemProperties struct: Name, Price, Weight, WeaponDamage, ArmorRating, Health, Count, Icon, Type, bIsEquiped, bIsFavorite, InventoryIndex, form (UTESForm*), StatusFlags, bIsInventoryItem, bIsInContainerMenu.

Built-in Debug Widgets

  • WBPPairedPawnDebugInfo_C, built-in AI debug widget that ships with the game. Use FindFirstOf("WBPPairedPawnDebugInfo_C") from Lua to get a live reference to NPC AI state without building your own debug overlay.
  • KismetDebugger, UE4SS includes this tool which works on your own custom Blueprints. You can step through your Blueprint mod logic in the UE4SS GUI while the game is running.

ESL Support

ESL (Light Plugin) format is not yet available in Oblivion Remastered. The 255 plugin limit from classic Oblivion applies (one-byte mod index, FF reserved for the save game). A separate ~512 cap covers file handles (active plugins + BSAs combined). Don’t confuse the two. Plan your mod’s FormID space accordingly.

GitHub Actions for Lua Mod Releases

A GitHub Actions template for auto-releasing UE4SS Lua mods on tag push circulates in the OBR modding Discord. This enables CI/CD: push a git tag, and your .zip is automatically built and attached to a GitHub Release. Ask in #resources for the current template.

Known Unsolved Frontiers

  • frontier Spawning Niagara systems from pure Lua, not figured out.
  • frontier Adding new audio for shout/Fus-Ro-Dah-style effects, effect works, custom audio doesn’t.
  • frontier Chameleon visual bug, can force-remove it but it returns ~1s later 99% of the time. Effect ID: 1280133187.
  • frontier Multi-effect armor enchantment, backend validation blocks it despite UI hook research.
  • frontier Setting weapon enchantment charges, UVItemDetailsViewModel can read them but setters are UI-only.

Confirmed Hook Catalog

Hooks people actually got working. Format: /Script/Altar.Class:Function unless noted. Source: community research via Kein/Altar SDK.

Hook PathFires when / use for
VLevelChangeData:OnFadeToGameBeginEventReceivedWorld finished loading, best “after load screen” hook
VLevelChangeData:OnFadeToBlackBeginEventReceivedA fade-out starts (entering load / fast travel)
VLevelChangeData:OnFadeToBlackOverBeforeFastTravelJust before a fast travel resolves
Engine.PlayerController:ClientRestartPlayer (re)possessed / load finished, good init hook
VAltarTelemetrySubsystem:OnSaveStartedA save is starting
VPairedPawn:OnCombatHitTakenThe “something got damaged” hook
VPairedPawn:OnCombatHitDealtAn actor deals a hit
VPairedPawn:DoRagdollANY ragdoll (death, paralyze, knockdown). Filter with IsPlayerCharacter()
VPairedPawn:OnWeaponChangedEquipped weapon changed
VPairedPawn:OnChangeActionStateAction state changes (⚠ gives a pointer, not a name, can’t filter by string)
VHitBoxComponent:OnOverlapTriggered / StartHitMelee hit/impact detection
VAltarPlayerController:OnJumpPressedJump pressed
VEnhancedAltarPlayerController:ToggleSneakSneak toggled (hookable, but calling it to drive sneak does nothing)
VEnhancedAltarPlayerController:OnAttackRequestPressedAttack button pressed
VEnhancedAltarPlayerController:OnLoadFinishedPlayer controller load finished
VHUDSubtitleViewModel:ConsumeNotificationThe notification bridge (OBScript → Lua)
VMagicSpellVFX:OnSpellProjectileBounceSpell projectile bounced
VAmmunition:OnBounceArrow bounced
VActorValuesPairingComponent:OnAllActorValueChangedA stat/actor-value changed (⚠ delegate, not plain UFunction, needs different binding method)
VDoor:OnBeginOverlapPreLoadBoxDoor pre-load trigger
VOblivionPlayerCharacter:RequestPowerAttackPower attack requested
BP_OblivionPlayerCharacter_C:ReceiveTickPer-frame tick on the player (⚠ two variants exist with/without underscore, check your build)
BP_OblivionPlayerCharacter_C:OnEnterUnderwaterPlayer enters water
WBP_LegacyMenu_Main_C:OnConfirmNewGameNew game confirmed
WBP_Modern_MapWidget_C:OnIconHoveredMap icon hovered
WBPModernMenuEnchantmentMenu_C:OnEffectClickedEnchant menu effect clicked (used in multi-enchant research)
WBP_LockPick_C:OnFocus / BreakLockpickLockpicking UI events
AIModule.AIPerceptionComponent:GetPerceivedHostileActorsAI perception, who an NPC sees as hostile
VPhysicsControllerComponent:HandleCollisionSoundOnBeginOverlapPhysics collision sounds
VPairedPawn:OnHitReaction / OnCapsuleHit / OnDeathVFX / SendJumpHit reactions, capsule hits, death VFX, jump events
BPCI_StatusEffect_Light_C:OnStartPlayStaticLight status effect start (used for spawn-light recipe)
Engine.Actor:K2_GetActorRotation / K2_GetActorLocation / DisableInputCommon actor utilities. Rotation returns Yaw/Pitch/Roll.
TABP_LookAt_C:EvaluateGraphExposedInputs_ExecuteUbergraph_TABP_LookAt_AnimGraphNode_AdvancedLookAt_A0FAD3894A8BA22925668A80995EC038Fires while NPC look-at is active. Hook to interrupt NPC head-tracking. Set K2Node_PropertyAccess to False each frame to disable. Credit: dicene.

OBScript Reference

Custom Script Effect Names (MagicLoader LOC_FN Fix)

When you create a custom spell with a Script Effect, the name displayed in the magic menu is pulled from a localization tag. There’s a specific quirk with how OBR handles these:

  • The script name in the ESP should NOT have the LOC_FN__ prefix.
  • But in MagicLoader’s JSON NewStrings block, you MUST prefix it with LOC_FN_.

Example: if your Script Effect is named RakeMoneyEnchScriptName, your MagicLoader JSON entry looks like:

{
    "Plugin": "HeavyRakeRevamped.esp",
    "NewStrings": {
        "LOC_FN_RakeMoneyEnchScriptName": "Raking in Money!"
    }
}

The name registered in xEdit should be RakeMoneyEnchScriptName (no prefix). The JSON key adds the LOC_FN_ prefix. This is the working combination as of mid-2025.

Common Script Block Types

BlockWhen it fires
Begin GameModeEvery quest tick (~5 seconds default). Set fQuestDelayTime to change frequency.
Begin MenuMode [id]While a specific menu is open.
Begin OnEquip [actorID]When item is equipped. Runs even on broken items; does NOT run on unequip if item breaks.
Begin OnUnequip [actorID]When item is unequipped.
Begin ScriptEffectStartOnce when spell effect begins.
Begin ScriptEffectUpdateFires every frame while the effect is active. For steady timing, accumulate GetSecondsPassed and gate on a threshold (frame-rate-independent). It complements this block, it doesn’t replace it.
Begin ScriptEffectFinishOnce when spell effect ends.
Begin OnDeath [actorID]When scripted actor is killed. Parameter is the killer, not the target.
Begin OnHitWhen scripted actor is hit. Only works on NPCs and Creatures.
Begin OnActivate [actorID]When specified actor activates this object.
Begin OnLoadWhen the cell containing the object loads.
Begin OnAdd [containerID]When the item is added to an inventory/container.
Begin OnDrop [actorID]When the item is dropped from an inventory.
Begin OnStartCombatWhen the actor enters combat.
Begin OnMagicEffectHit [effectID]When the actor is hit by a given magic effect.
Begin OnAlarm [Trespass/Steal/Attack/Pickpocket]When the actor witnesses the specified crime.
Begin OnPackageStart / OnPackageDone / OnPackageChange [packageID]AI package lifecycle events.
Begin OnResetWhen the cell resets (3-day respawn).
Begin SayToDoneWhen a forced SayTo dialogue line finishes.

This is the commonly-used subset, not the full list. There are more block types (and trigger variants like OnTrigger/OnTriggerActor/OnTriggerMine); see the UESP CS wiki Blocktypes category for the complete reference.

OBScript Variables & Types

  • Variable types: short (integer, limited range), long (larger integer), float, ref (object reference). All must be declared before script blocks.
  • ref type stores a reference (and can hold a base form too). It earns its keep when the form is dynamic or unknown at compile time, e.g. capturing a player-created enchantment at runtime, then acting on it later. If you already know the editor ID, skip the indirection and call the function directly: If Player.IsSpellTarget MySpell == 1 / Player.RemoveSpell MySpell / EndIf.
  • player (base object, 00000007) and playerRef (the reference, 00000014) both work in most contexts. Prefer playerRef when a function specifically needs a reference, or when storing/comparing in a ref var. To test whether a ref is the player, use GetIsReference player (checks the specific reference) rather than GetIsID (checks the base form).
  • GetItemCount counts by base ObjectID (editor ID / FormID, or a ref var holding one), not display name. Two different items that share a display name are counted separately; only instances of the same base form stack. Use unique editor IDs for custom items (the real mod-conflict hygiene rule). Display names can collide freely with no effect on counting.

GetStage vs GetStageDone

  • GetStage QuestName, returns the current active stage number only.
  • GetStageDone QuestName Stage#, returns 1 if that specific stage was ever completed, regardless of current stage. Use this when checking whether a past event happened, not just whether you're currently at a stage. Example: if stages 25 and 35 both lead to 35, but you need to check whether stage 10 was triggered at any point, use GetStageDone.

OBScript AOE Pattern (xMarker + Script Effect)

To apply an effect to all hostile NPCs near the player (since OBScript can't iterate actors directly):

; Add this ability to ALL hostile creatures in the CS.
; The script effect checks if the creature is currently fighting the player.
scn MyAuraCurseApplicatorScript

ref rSelf

Begin ScriptEffectUpdate
    set rSelf to GetSelf
    if ( rSelf.GetDistance PlayerRef <= 600 ) && ( rSelf.GetCombatTarget == PlayerRef )
        if ( rSelf.IsSpellTarget MyDebuffSpell == 0 )
            MyXMarkerRef.MoveTo rSelf
            MyXMarkerRef.Cast MyDebuffSpell rSelf
        endif
    else
        dispel MyDebuffSpell
    endif
End

The xMarker (placed in the world, invisible) acts as the caster. GetCombatTarget == PlayerRef is the clean way to check if an NPC is hostile to the player. Credit: molagbal1187.

Complete Example: A Toggleable Damage Aura (4 scripts)

A full working version of the pattern above, contributed by Alexander Preit. You cast one spell on yourself to toggle it on. While you are in combat it tags every nearby enemy with a damaging ability, and each enemy drops the ability once it leaves your radius. It splits into four small scripts, each with one job.

1. The toggle spell (cast on yourself)

A self-cast spell whose ScriptEffectStart flips a quest on or off. A quest variable (AuraActive) is the on/off latch, so re-casting the spell toggles it.

scn CheatAuraToggleSpellScript

Begin ScriptEffectStart

    if CheatAuraQuest.AuraActive == 0
        StartQuest CheatAuraQuest
        Message "Aura of death is Active"
        set CheatAuraQuest.AuraActive to 1
    else
        StopQuest CheatAuraQuest
        Message "Aura of death is dormant"
        set CheatAuraQuest.AuraActive to 0
    endif
end

2. The quest loop (the heartbeat)

Runs in GameMode on a one-second tick (fquestdelaytime). While you are in combat it moves an xMarker to you and casts the area spell from that marker onto your position. The marker is the caster, you are the anchor point.

scn CheatAuraQuestScript

short AuraActive
float fquestdelaytime

Begin GameMode

    set fquestdelaytime to 1

    if player.IsInCombat == 1
        CheatMarkerRef.moveto Player
        CheatMarkerRef.cast CheatAuraSpell Player
        Message "Casting aura"
    endif

end

3. The aura spell (per-target filter)

This is the area effect. Because it is an area effect, its ScriptEffectUpdate runs on every actor it touches, so self is each nearby actor in turn. It keeps only the ones that are not you (GetIsReference Player == 0), are currently fighting you (GetCombatTarget == Player), and are not already tagged (IsSpellTarget CheatAuraAB == 0), then adds the ability.

scn CheatAuraSpellScript

ref self

Begin ScriptEffectUpdate
    set self to getself
    if self.GetIsReference Player == 0
        if self.GetCombatTarget == Player
            if self.IsSpellTarget CheatAuraAB == 0
                self.AddSpell CheatAuraAB
            endif
        endif
    endif
end

4. The ability (the effect + self-cleanup)

The actual "aura of death" (drain, damage, whatever you design) lives on the CheatAuraAB ability you attach. Its one housekeeping job here is to remove itself once the target gets more than 1000 units from you, so anything that flees or that you outrun drops the effect on its own.

scn CheatAuraABScript

ref self

Begin ScriptEffectUpdate
    set self to getself

    if self.GetDistance Player >= 1000
        self.RemoveSpell CheatAuraAB
    endif

end
What This Example TeachesFive reusable tricks in one system. A marker you MoveTo each tick gives a moving AoE origin centered on the player without the player being the caster. GetCombatTarget == Player hits only actors actually fighting you, not neutral NPCs caught in the radius. GetIsReference Player == 0 excludes yourself (the same identity check from the player vs playerRef note above). IsSpellTarget … == 0 gates the add so the per-frame update does not re-stack the ability. And a self-removing ability (the GetDistance gate) means cleanup is automatic with no central tracking list.
The One Thing to Get RightFor step 3 to run on enemies at all, CheatAuraSpell has to be an area / target-location magic effect, so the engine runs its script effect on each actor in the radius. A plain single-target self-cast would only ever run on the player, and the filter would find nothing to tag. If an aura like this "does nothing," the effect's area setting is the first thing to check.

Cheat Aura example courtesy of Alexander Preit.

OBScript Bugs in the Remaster

  • Message formatting (%g, %.0f, etc.) does not work, the Altar interpretation layer does not support dynamic string formatting.
  • ScriptEffectUpdate fires per frame; GetSecondsPassed is the per-frame delta. For frame-rate-independent timing: set elapsed to elapsed + GetSecondsPassed, then act when elapsed >= your interval. (This is a timing technique, not a fix for the block failing to fire.)
  • SetActorsAI 0 called on a ref in another cell crashes the game as of update 1.1.
  • PlayMagicEffectVisuals does not apply visual effects on actors.
  • SetPos z on actors is unreliable depending on terrain height.
  • All OBSE functions (HasSpell, IsKeyPressed, CloseAllMenus, etc.) do not work, OBSE64 is only a plugin loader.
  • return inside a flat if/endif does not work as expected, always nest logic properly.
  • Quest scripts can sometimes lose their attached script in the CS. Re-attach it if behavior suddenly breaks. (Community-reported from Discord; exact cause unconfirmed.)
  • ESPs created in the old CS are missing 2 parameters the Remaster requires, but this only bites if you edit Cells or Worldspaces. Fix the plugin in xEdit before use: Haphestia’s Fix and Port Script (vibrantruin) for the general record fix, or Batch Copy Altar Cell Data for cells your mod edits.
  • HTML-style text formatting in books/messages still works, just with a smaller palette than classic Oblivion (see the note below). The old claim that it is fully broken is wrong.
Book & Message Formatting (What Still Works)Write it in the CS exactly as you would for classic Oblivion. Confirmed working: <br> line breaks, <hr> dividers, <div align="left|center|right"> alignment, <font face="N">, and page breaks. The one real limit is fonts: the face values resolve to only about two distinct typefaces, so you can vary style a little but not freely. Correction confirmed by Alexander Preit.

Useful OBScript Patterns

Toggle a spell without OBSE

If ( Player.IsSpellTarget MySpell == 0 )
    Player.AddSpell MySpell
Else
    Player.RemoveSpell MySpell
EndIf

Run code once on game start

scn MyOneTimeScript
Begin GameMode
    player.AddItem MyCustomItem 1
    StopQuest MyStartupQuest
End

Attach to a quest with Start Game Enabled checked. Quest runs once and stops itself.

ScriptEffectUpdate timer workaround

Begin ScriptEffectUpdate
    set elapsed to elapsed + GetSecondsPassed
    if elapsed >= 0.016
        ; your logic here
        set elapsed to 0
    endif
End

OBScript Pattern: Item Equip with Infamy Check

scn GrayFoxCuirassScript
begin OnEquip player
    if ( GetPCInfamy >= 10 ) && ( GetPCFame < GetPCInfamy )
        if ( GetPCInfamy < 20 )
            player.addspell GrayFoxArmorEnchant01
        elseif ( GetPCInfamy >= 20 )
            player.addspell GrayFoxArmorEnchant02
        endif
    endif
end
begin OnUnequip player
    player.removespell GrayFoxArmorEnchant01
end

Pattern: Menu After Inventory Closes

Opening ShowEnchantment while inventory is open freezes controls. Use a quest script waiting for GameMode:

; Item script fires on equip:
Begin OnEquip player
    StartQuest MySpellMenuQuest
End
; Quest script (only runs when no menus are open):
Begin GameMode
    ShowEnchantment
    StopQuest MySpellMenuQuest
End

Quest & AI Notes

  • Quest stages can only go forward. Enable Allow Repeated Stages for stages that need to fire multiple times.
  • Quest scripts die at stage 100 / StopQuest. Don’t mix stages into existing vanilla quests, breaks saves.
  • FollowPlayer AI packages may not work as expected, buggy in the Remaster.
  • PushActorAway with negative values may push instead of pull. Results are inconsistent.
  • NPCs activate furniture to use it; they don’t just play animations near it.

Console Commands for Debugging

ObvConsole set QuestName.VarName to 1    ; set a quest variable
ObvConsole show QuestName.VarName         ; read a single variable
sqv QuestName                             ; dump all variables for a quest
GetGlobalValue GlobalVarName              ; read a global variable
ToggleDebugCamera                         ; free camera (replaces old tfc)
Altar.StandOutOblivionAsset 1             ; turn legacy assets pink
Altar.Cheat.AllowSetStage 1              ; enable SetStage in console
SloMo 0                                   ; freeze time

A community-compiled data table of all known console commands covering OBR-specific commands, native UE ccmds/cvars, original Oblivion commands, OBSE commands, and SML Dev Resource commands is available in the OBR Modding Discord resources channel. The data table includes both commands that currently work and those that potentially could work. Searching for specific cvars: try r.Lumen, r.Shadow, sg.PostProcessQuality, etc.

Using Config Variables Instead of Console CommandsFor Blueprint variables you want to be user-configurable, the Engine.ini approach (see the Config Variables section above) is zero-cost compared to polling. For static defaults on C++ classes, target them with [/Script/ClassName] prefix in Engine.ini, this can configure C++ default values without needing Lua code at all.

Ready-to-Use Lua Snippets

BP → Lua Communication: PrintToModLoader & RegisterCustomEvent

The BPML_GenericFunctions mod (included with UE4SS, may need enabling in Mods/ folder) bridges Blueprint custom events to Lua. Enable it if Blueprint → Lua communication isn’t working.

-- In your Lua mod: listen for a custom event fired by a Blueprint
RegisterCustomEvent("MyModCustomEvent", function(params)
    -- params contains whatever the Blueprint passed
    print("[MyMod] Got event from BP!")
end)
-- Docs: https://docs.ue4ss.com/dev/lua-api/global-functions/registercustomevent.html

In your Blueprint (ModActor), add a Custom Event node named MyModCustomEvent and call PrintToModLoader with a string to test the bridge. The PrintToModLoader custom event is provided by BPML_GenericFunctions, call it from BP with any string to print to the UE4SS console.

MCM ↔ Lua via Custom Console Commands

-- Register a custom console command in Lua (not added by MCM, but MCM can call it)
RegisterConsoleCommandHandler("MyMod.Reload", function(full, params)
    -- runs when someone types "MyMod.Reload" in console, or MCM fires it
    ReloadMyModSettings()
    return true  -- consume the command
end)
-- MCM can trigger this via its "run on close" command field
-- Reference implementation: tommn's Mad Experience mod

MCM tip from tommn: MCM cannot run code when an individual slider/toggle changes, only when the menu closes. Fire your reload logic as a console command on-close. Also: MCM cannot call Lua functions directly, but it CAN fire UE4SS-registered console commands. Credit: tommn.

Detect Weapon Type & Power Attack

-- pawn is AVPairedPawn reference
local function GetWeaponType(pawn)
    local wa = pawn.WeaponsPairingComponent.WeaponActor
    if wa and wa:IsValid() then
        return wa.WeaponTypeTag.TagName:ToString()
    end
    return "HandToHand"
end
-- Example output: "Weapon.Type.Sword.One.Hand", "Weapon.Type.Bow", "HandToHand"

local powerTag = { TagName = FName("Actor.Action.Combat.Attacking.Power") }
local function IsPowerAttack(pawn) return pawn:HasGameplayTag(powerTag) end

Detect Spell Cast Type

-- Returns: 0=Self, 1=Touch, 2=Target, 3=Unknown, 4=MAX, -1=Error
local function GetSpellCastType(pawn)
    local s = pawn.OblivionActorStatePairingComponent
    if not s or not s.SpellCastType then return -1 end
    return tonumber(s.SpellCastType)
end

Efficient Proximity Detection (SphereOverlapActors)

-- ~100x faster than iterating all actors and manually calculating distance
local playerLocation = playerPawn:K2_GetActorLocation()
local actorList = {}
UEHelpers.GetKismetSystemLibrary():SphereOverlapActors(
    UEHelpers.GetWorldContextObject(), playerLocation,
    500, { someClass }, someClass, { }, actorList)
for i = 1, #actorList do
    local actor = actorList[i]:get()
    -- do something with actor
end

Kill / NPC Death Detection

-- Check if an NPC is dead (lootable = dead in this game)
local function IsNPCDead(pawn)
    if not pawn or not pawn:IsValid() then return false end
    -- Check death state via actor value
    local healthAV = pawn.OblivionActorStatePairingComponent
    if healthAV then
        return healthAV.bIsDead == true
    end
    return false
end

-- Detect NPCs in range and check for death (scan approach)
-- For detecting WHO killed an NPC, hook VPairedPawn:OnCombatHitDealt
-- and track the last attacker FormID against DoRagdoll events
-- Note: scanning for all nearby actors has performance cost - use a timer
local function ScanNearbyDeaths(radius)
    local actorList = {}
    UEHelpers.GetKismetSystemLibrary():SphereOverlapActors(
        UEHelpers.GetWorldContextObject(),
        UEHelpers.GetPlayer():K2_GetActorLocation(),
        radius, {}, nil, {}, actorList)
    for i = 1, #actorList do
        local actor = actorList[i]:get()
        if actor and actor:IsValid() and IsNPCDead(actor) then
            -- actor is a dead NPC within range
        end
    end
end
-- For plants/interactive objects: look in /Game/Dev/InteractibleObjects/
-- Same SphereOverlap approach, just different blueprint class filter

Notes (credit: .yeah.nah.yeah., _weapon_): To verify you were the killer, also hook VPairedPawn:OnCombatHitDealt and track the attacking actor, compare against the ragdolling NPC via VPairedPawn:DoRagdoll. For plants and Nirnroots, use SphereOverlapActors filtering on their specific blueprint class found in /Game/Dev/InteractibleObjects/.

Spawn a Blueprint Actor at Location

local UEHelpers = require("UEHelpers")
local function SpawnBP(path, location, rotation)
    local bp_path = path
    if bp_path:match("^OblivionRemastered/Content/.+%.uasset$") then
        bp_path = bp_path:gsub("OblivionRemastered/Content/", "/Game/")
        local fn = bp_path:match("/([a-zA-Z0-9_]+).uasset")
        local dir = bp_path:match("^(.+/)[a-zA-Z0-9_]+.uasset")
        bp_path = dir .. fn .. "." .. fn .. "_C"
    end
    LoadAsset(bp_path)
    local bp_class = StaticFindObject(bp_path) or CreateInvalidObject()
    if not bp_class:IsValid() then return end
    local loc = location or UEHelpers.GetPlayer():K2_GetActorLocation()
    local rot = rotation or {Pitch=0, Roll=0, Yaw=0}
    local spawned = UEHelpers.GetWorld():SpawnActor(bp_class, loc, rot) or CreateInvalidObject()
    if not spawned:IsValid() then return end
    spawned.Tags[#spawned.Tags + 1] = FName("ManuallySpawned")
    spawned:K2_GetRootComponent():SetMobility(2)   -- Movable
    spawned:SetActorEnableCollision(false)
    return spawned
end

RegisterKeyBind(Key.N, function()
    ExecuteInGameThread(function()
        SpawnBP("/Game/Dev/Creatures/BP_Generic_Flameatronach.BP_Generic_Flameatronach_C")
    end)
end)

Dump Player Inventory

-- IMPORTANT: fetch BEFORE opening the inventory menu, not after
local vm = FindAllOf("VInventoryMenuViewModel")[1]
for i, item in ipairs(vm:GetInventory()) do
    local d = item.DisplayName
    print(d.Name:ToString(), d.Weight, d.Price, d.Count, d.bisEquiped, d.form)
end

Get All Skeletal Mesh Sockets on a Pawn

-- pawn is a valid AVPairedPawn reference
local socketCount = pawn.MainSkeletalMeshComponent.SkeletalMeshAsset:NumSockets()
print("Number of sockets: " .. socketCount)
for i = 0, socketCount - 1 do
    local sock = pawn.MainSkeletalMeshComponent.SkeletalMeshAsset:GetSocketByIndex(i)
    print("Socket: " .. sock.SocketName:ToString() .. " | Bone: " .. sock.BoneName:ToString())
end

Smooth Tween / Interpolation Helper

Smoothly transition a numeric value over time using an easing function. Requires the easing_functions module. Visual reference: easings.net.

local Math = require("easing_functions")  -- provides Math.EasingOptions table
local CurrentTweenTask = nil

local function TweenToValue(GivenVariable, TotalDuration, StartingValue, EndingValue, EasingFunc)
    local startTime = os.clock()
    GivenVariable = StartingValue
    CurrentTweenTask = LoopAsync(1, function()
        local progress = (os.clock() - startTime) / TotalDuration
        GivenVariable = EasingFunc(StartingValue, EndingValue, progress)
        if GivenVariable >= EndingValue then
            GivenVariable = EndingValue
            return true  -- stops the loop
        end
        return false
    end)
end
-- Usage: TweenToValue(myVar, 2.0, 0.0, 100.0, Math.EasingOptions.EaseLinear)

Full Notification Bridge (Complete Pattern)

-- OBScript side: message "MY_SIGNAL" from any script block
-- Lua side: intercept, hide, react
RegisterHook("Function /Script/Altar.VHUDSubtitleViewModel:ConsumeNotification",
function(hudVM)
    local vm = hudVM:get()
    local text = vm.Notification.Text:ToString()
    if text:match("MY_UNIQUE_SIGNAL") then
        vm.Notification.ShowSeconds = 0.0001  -- hide from player
        -- do whatever you want here
        return
    end
end)
-- Lua -> OBScript reverse:
-- Kismet:ExecuteConsoleCommand(pc.player, "ObvConsole set MyFlag to 1", pc, true)
-- Quest script checks: if (MyFlag == 1) ... set MyFlag to 0 ... endif

Deferred Hook System (Hook Not Yet Available)

When a hook isn’t available at script startup (actor not yet spawned), use this pattern to defer registration until it exists:

-- Waits until FindFirstOf succeeds, then registers the hook.
-- Prevents scripts failing when their target isn't available at load time.
local function DeferHook(className, hookPath, callback)
    local task = LoopAsync(100, function()
        local obj = FindFirstOf(className)
        if obj and obj:IsValid() then
            RegisterHook(hookPath, callback)
            return true  -- stop polling
        end
        return false
    end)
end

-- Usage:
DeferHook("VMyTargetClass",
    "Function /Script/Altar.VMyTargetClass:OnSomeEvent",
    function(self) print("hook fired") end)

Spawn a Blueprint Actor via Lua

local UEHelpers = require("UEHelpers")

local function SpawnBP(bpPath, location, enableCollision, simulatePhysics)
    -- Convert FModel path to UE internal path
    if bpPath:match("^OblivionRemastered/Content/.+%.uasset$") then
        bpPath = bpPath:gsub("OblivionRemastered/Content/", "/Game/")
        local name = bpPath:match("/([a-zA-Z0-9_]+).uasset")
        bpPath = bpPath:match("^(.+/)[a-zA-Z0-9_]+.uasset") .. name .. "." .. name .. "_C"
    end
    LoadAsset(bpPath)
    local bp_class = StaticFindObject(bpPath) or CreateInvalidObject()
    if not bp_class:IsValid() then print("Could not find: " .. bpPath) return end
    location = location or UEHelpers.GetPlayer():K2_GetActorLocation()
    local spawned = UEHelpers.GetWorld():SpawnActor(bp_class, location, {Pitch=0,Roll=0,Yaw=0})
    if spawned:IsValid() then
        spawned:SetActorEnableCollision(enableCollision or false)
        if spawned:K2_GetRootComponent():IsValid() then
            spawned:K2_GetRootComponent():SetMobility(2)
        end
    end
    return spawned
end

-- Example: spawn a flame atronach 300 units in front of player
RegisterKeyBind(Key.N, function()
    ExecuteInGameThread(function()
        local fwd = UEHelpers.GetKismetMathLibrary():GetForwardVector(
            UEHelpers.GetPlayer():K2_GetActorRotation())
        local loc = UEHelpers.GetKismetMathLibrary():Add_VectorVector(
            UEHelpers.GetPlayer():K2_GetActorLocation(),
            UEHelpers.GetKismetMathLibrary():Multiply_VectorInt(fwd, 300))
        SpawnBP("/Game/Dev/Creatures/BP_Generic_Flameatronach.BP_Generic_Flameatronach_C", loc, true)
    end)
end)

Print All Items in Player Inventory

local function DumpInventory()
    local vms = FindAllOf("VInventoryMenuViewModel")
    if #vms == 0 then print("Open inventory first"); return end
    local items = vms[1]:GetInventory()
    for i, item in ipairs(items) do
        local p = item.DisplayName
        if p then
            print(string.format("[%d] %s | Weight:%.1f | Value:%d | Count:%d",
                i, p.Name:ToString(), p.Weight, p.Price, p.Count))
        end
    end
end
-- Must be called while inventory menu is open

Combat, Damage & Mounted-State Patterns

Hard-won UE4SS patterns from building real native damage and mounted combat (the first-of-its-kind mod). Full narrative in How They Did It → Mounted Combat; the reusable facts are here.

OBR Routes None of UE’s Standard Damage PathsThese are confirmed dead ends, do not spend time on them: UGameplayStatics::ApplyDamage is a complete no-op (fires clean, does nothing). Actor-value setters by guessed name (SetHealth, DamageHealth…) do not exist. OnCombatHitTaken / OnCombatHitDealt take a PairedOblivionHitEvent struct that UE4SS cannot marshal a Lua table into, not invocable.

Working Damage: SendMeleeHitOnPairedPawn

Discovered by walking the pawn’s class hierarchy with reflection (ForEachFunction). It is the game’s own paired-pawn melee entry and routes through native combat, real damage, hit reaction, death, and loot, no struct to fabricate. Call it on the rider (BP_OblivionPlayerCharacter_C), not the horse.

-- third arg = knockdown/ragdoll flag
-- pcall does NOT catch native SEH (EXCEPTION_ACCESS_VIOLATION).
-- The target can be GC'd between your check and the call, so re-validate
-- BOTH objects immediately before ProcessEvent fires:
if IsValidObj(rider) and IsValidObj(target) then
    rider:SendMeleeHitOnPairedPawn(target, DAMAGE_AMOUNT, false)
end

Direct Health via Actor Value (fallback / guaranteed kill)

Health is actor value index 10 on this build, on ActorValuesPairingComponent. Use as a fallback or to guarantee the kill if native damage is resisted/scaled. The working setter here is SetFloatModifiedActorValue (probe order if unsure: SetFloatModifiedActorValueModifyActorValueSetActorValueForceSetActorValueSetBaseActorValue).

local avc = pawn.ActorValuesPairingComponent
local hp  = avc:GetFloatModifiedActorValue(10)            -- 10 = Health
avc:SetFloatModifiedActorValue(10, math.max(0, hp - DAMAGE_AMOUNT))
Gotcha: bIsDead Is a Number, Not a Booleanpawn.bIsDead returns a number (0 or 1), so if pawn.bIsDead == false then is always false even when the pawn is alive. Use == 0 for alive / == 1 for dead, or drop the alive filter and guard with IsValidObj instead.

Stable Mounted-State Detection

Do not use VLocomotionHorseRiderAnimInstance for mount state, it is created/destroyed every few ticks, producing 100+ phantom mount/dismount events per minute. The stable signal is the horse’s rider reference: if any BP_Generic_Horse_C has a rider that passes IsPlayerCharacter(), you’re mounted. Remember: when mounted, player.Pawn is the horse; the rider is a separate BP_OblivionPlayerCharacter_C pawn.

local function IsPlayerMounted()
    local ok, horses = pcall(function() return FindAllOf("BP_Generic_Horse_C") end)
    if not ok or not horses then return false end
    for _, h in ipairs(horses) do
        if IsValidObj(h) then
            for _, prop in ipairs({ "Rider", "RiderPawn", "RiderActor", "MountedActor", "Driver" }) do
                local rok, rider = pcall(function() return h[prop] end)
                if rok and IsValidObj(rider) then
                    local pok, isP = pcall(function() return rider:IsPlayerCharacter() end)
                    if pok and isP then return true end
                end
            end
        end
    end
    return false
end

Weapon Attach: SnapToTarget, Not KeepWorld

When re-attaching a weapon (WeaponsPairingComponent.WeaponActor) to a socket, KeepWorld (rule 2,2,2) freezes whatever world offset exists at attach time, so the weapon floats off the hand during swings. SnapToTarget (rule 0,0,0) adopts the socket transform exactly and rides the hand bone every frame. Hand socket: Weapon_Socket; one-handers sheathe to SideWeapon_Socket, two-handers to BackWeapon_Socket.

weaponActor:K2_AttachToComponent(riderMesh, FName.new("Weapon_Socket"), 0, 0, 0, false)
UE4SS Punctuation Key Constants & Closure TrapPunctuation keys use OEM names: Key.OEM_PERIOD (.), Key.OEM_COMMA (,), Key.OEM_SEMICOLON (;). Using Key.COMMA passes nil to RegisterKeyBind (“no overload found”). Also: Lua closures inside ExecuteWithDelay/LoopAsync can hold stale UObject references after a delay, always re-fetch the pawn/animInst inside the delayed callback rather than relying on the captured value.
Disabling a Mod in mods.txt Does NOT Stop Its LuaSetting a mod to 0 in mods.txt does not prevent its Lua scripts from loading and hooking, they still execute. To stop a crashing mod you must delete its folder entirely. (A “disabled” PlayerCamera mod calling IsVisible on a null object caused phantom per-attack crashes throughout mounted-combat development.)

Useful Classes & Structs Reference

ClassPurpose / Key Methods
VPairedPawnBase pawn for all characters. WeaponsPairingComponent, OnCombatHitTaken, DoRagdoll, IsPlayerCharacter()
VPairedCharacterCharacter subclass. Has VHumanoidHeadComponent, VHumanoidHeadAnimBP
VActorValuesPairingComponentAttribute values (Strength, Health, etc.). OnAllActorValueChanged delegate.
VHumanoidHeadComponentFacial expressions/emotion. Search “Emotion” in class dump.
VEnhancedAltarPlayerControllerPlayer controller. OnLoadStarted, OnLoadFinished, OnAttackRequestPressed, ToggleSneak.
VLevelChangeDataLevel transitions. OnFadeToGameBeginEventReceived, the primary “load screen done” hook.
VAltarUISubSystemUI subsystem. GetInventoryHoveredActor, menu access.
VInventoryMenuViewModelInventory menu data. Contains item arrays. See gotchas for fake-instance trap.
VHUDSubtitleViewModelHUD message notifications. The notification bridge key hook.
UTESMagicItemFormMagic item / spell form. FullName, EffectSettings TArray.
VOblivionGameInstanceSubSystemGame instance subsystem. Persistent across level loads.
UVTESObjectRefComponentObject reference component. FormIDInstance (uint32), TESForm*, GetHexFormRefID().

UE Config Variables (Engine.ini Blueprint Settings)

You can configure Blueprint variables without UE4SS by editing Engine.ini. This is an alternative to lua polling, set defaults that are read at startup with zero runtime cost. Useful for modder-facing config files.

File location: C:\Users\USERNAME\Documents\My Games\Oblivion Remastered\Saved\Config\Windows\Engine.ini

Format for a Blueprint in the base game folder:

[/Game/PathToYourBlueprint/BP_BlueprintName.BP_BlueprintName_C]
Variable=Value

Format for a Blueprint in a content-only plugin (Alpakit mod):

[/PluginName/PathToYourBlueprint/BP_BlueprintName.BP_BlueprintName_C]
Variable=Value
Path Rule“Content” is never included in the path. Use /Game/ for game folder or /PluginName/ for plugin. Example for a debug toggle:

[/Game/Forms/items/ammo/BP_Daedric_BoundArrow_Equipped.BP_Daedric_BoundArrow_Equipped_C]
bGlobalDebug=True

Structs can also be set this way:

[/Game/PathToYourBlueprint/BP_BlueprintName.BP_BlueprintName_C]
MyStruct=(MyStructProperty1=Value1,MyStructProperty2=Value2)

Arrays of structs are also supported, useful for storing per-actor data accessible by your Blueprint without polling. The [/Script/ prefix is specifically for C++ classes.

vs. Lua PollingConfig variables are read once at startup, so they’re zero-cost at runtime vs. Lua setters that poll every frame. Use them for modder-facing settings. Use UE4SS for dynamic values that need to change at runtime. Full UE docs: Configuration Files in Unreal Engine.

Object Browsers & Reference Resources

Mod How-Tos

How They Did It

First-person build logs from mod developers, the working architecture and the dead ends, so the next person doesn’t spend a day rediscovering them. Each entry is a real shipped feature, documented by the person who actually built it.

About This SeriesThe rest of this wiki tells you what to do. This page tells you how a feature actually got built, and why it was hard. Each writeup walks the real path: what got tried, what broke and the actual reason it broke, and the thing that finally worked. Built something new and want it added here? Same format every time, the result up top, then the dead ends, then the working method. Reach out on the Credits page.
In This Series

Mounted Combat in Oblivion Remastered

A first-of-its-kind feature build. By CosmicBoogaloo / DreamEater · shipping version v10.10.8 (with the SafeMeleeHit C++ crash-guard companion, Lessons 9-10).

Oblivion Remastered ships with no mounted combat, you can’t attack from horseback. Adding it isn’t a content problem, it’s an animation-graph and asset-loading problem, and almost every “obvious” approach fails for a non-obvious reason. This is the path that actually worked.

End ResultDraw/sheathe and a multi-hit attack combo from horseback, rider stays seated, with omnidirectional extended-range hit detection and real native damage (enemies take damage, react, and die), driven entirely by a UE4SS Lua mod plus one loader Blueprint and a pak of custom animations. No engine source edits, no override of base-game animations.
The Single Hardest Piece Was Damage, Not AnimationOBR routes none of the standard UE damage paths, and the working call (SendMeleeHitOnPairedPawn) is undocumented, found by reflecting over the pawn’s class. Lesson 7 below has the full answer so nobody has to brute-force it.
1

Why It’s Hard: The Mounted Animation Graph

The riding layer bypasses the combat slots and the spine mask

When you mount, the player mesh (BP_OblivionPlayerCharacter_C, running ABP_Reverse_ThirdPerson_Humanoid) links in the horse-riding layer, TABP_HorseRiding_Ground via the ALI_HorseRiding interface. The critical fact:

The Core TrapThe riding layer is self-contained. It has no animation slot nodes and generates the entire seated pose from blendspaces, replacing the locomotion/combat branch of the graph.

The four slots you’d normally play montages on, UpperBody, LowerBody, Sequencer, FullBody, live in TABP_ReverseCharacter (the template). And here’s the trap:

  • UpperBody slot feeds the masked LowerAndUpperBodyLayer, which lives in the CombatPose branch, the branch the riding layer bypasses. So a montage on UpperBody while mounted plays successfully but is never evaluated, you see nothing. PlaySlotAnimationAsDynamicMontage returns no error, which is what makes this so confusing to debug.
  • FullBody slot (bAlwaysUpdateSourcePose = true) feeds the master FullBodyPose cache and wraps the final assembled body, including the riding output. So it is visible while mounted, but a normal standing attack played here carries an absolute standing pelvis, which replaces the seated pelvis and launches the rider into a standing pose above the saddle (~66-unit pop).

The spine-mask LayeredBlendPerBone that normally keeps attacks upper-body-only is inside that bypassed CombatPose branch, so slot-based masking does not work while mounted. The only slot that’s visible mounted is full-body and unmasked.

2

The Animation Solution: Additive, Leg-Stripped, on FullBody

Stop fighting the mask, use additive animation math

The way out is to stop relying on the slot/mask and use additive animation math instead:

  1. Strip the lower-body tracks (pelvis + legs) from your attack clips.
  2. Set Additive Anim Type = AAT_RotationOffsetMeshSpace (mesh space, won’t twist when the horse turns).
  3. Set Base Pose Type to a standing one-hand idle frame, not Skeleton Reference Pose. Ref pose bakes the arms-down-from-T-pose offset into the delta and distorts the swing on a seated body. A standing idle base makes the delta the actual swing motion. Clips with extreme torso lean (e.g. axe power attacks) may need the seated riding-idle sample as the base instead.
  4. Play on the FullBody slot via PlaySlotAnimationAsDynamicMontage.

Why this works: an additive montage on a slot adds its per-bone delta to the source pose instead of replacing it. The riding layer seats the legs; the stripped clip has zero leg keys → zero leg delta → legs stay seated. The upper body has keys → the swing plays. The bypassed mask is irrelevant because you’re not using it. This is the same mechanism the game uses for hit-reactions and aim-offsets over locomotion.

This Is the Correct Method, Not a WorkaroundAdditive type and leg-stripping are baked into the asset. You cannot make a base-game clip “use only the upper body” at runtime, and you shouldn’t override base-game clips (that breaks them for all NPCs and on foot). Cook your own copies, stripped + additive.
3

The Real Wall: Loading New-Path Assets

Why LoadAsset silently does nothing, and the loader-BP fix

This is where the time goes. Your custom animations live at a new path (/Game/Mods/MountedCombat_P/...) that nothing in the game references, and that breaks loading in ways specific to the Altar UE4SS build:

  • LoadAsset is registry-gated. It resolves through the asset registry, so it silently does nothing for unregistered new-path mod assets, runs clean, loads nothing.
  • StaticLoadObject does not exist in the Altar UE4SS build. Calling it at the main menu, before a world exists, hard-crashes with nothing logged.
  • Your standalone item mods load fine because TesSyncMapInjector maps a FormID → asset and the game loads it on use. AnimSequences have no FormID, so TesSyncMap can’t load them directly.
An AnimSequence at a New Path Will Not Load On Its OwnIt needs a hard reference from something the game actually loads.

The loader Blueprint (the fix)

Make a Blueprint that BPModLoaderMod spawns, and give it hard references to your animations. When the BP loads, the refs are in memory.

  • The actor must be named ModActor, that’s what BPModLoaderMod spawns.
  • Add object-reference variables (Anim1, Anim2, …), type Anim Sequence (object reference, not soft), and set each default value to one of your sequences.
  • Verify with Reference Viewer that ModActor points at every clip. A BP that merely exists anchors nothing, the references are the entire point.
4

BPModLoaderMod Conventions (the costliest gotchas)

Pak name = folder name · refs on ModActor · one bundled pak
RuleSymptom if violated
The pak file name must match the content subfolder holding ModActor. BPModLoaderMod looks for /Game/Mods/<PAK_NAME>/ModActor.ModClass for <name> is not valid, ModActor never spawns. (Pak named MountedCombat, folder MountedCombat_P → it looks in the wrong folder.)
Hard refs go on ModActor itself, not a separate loader BP. BPModLoaderMod only spawns ModActor.A separate BP_*Loader is never touched; its refs never fire.
Put ModActor + the anims in ONE pak in LogicMods.Splitting BP→LogicMods and anims→~mods creates a cross-pak dependency that IO Store fails to resolve silently, ModActor spawns, but the anim refs in the other pak never come with it.
The General Art/BP Split Doesn’t Apply HereThe usual “art→~mods, blueprints→LogicMods” rule does not apply to a self-contained loader mod. Bundle ModActor and its referenced art together, like the working flying/animation mods do (which also include SK_HumanoidFull as a resource to anchor the skeleton chain).

Build-specific quirk: read refs OFF the actor, not by path

On the Altar build, two things bit: hard-ref variable defaults did not reliably auto-pull dependencies the way standard UE cooking would, and GetName() returns blank on object references read back from BP variables. The robust pattern: don’t search for the anims by path at all, read them straight off the spawned ModActor’s variables. If ModActor holds the reference, the object is in memory by definition. Identify each clip by its full name (GetFullName), since GetName is blank, with a positional fallback.

local function HarvestFromLoader()
    local actor = FindFirstOf("ModActor_C")
    if not actor or not actor:IsValid() then return end
    for i = 1, 8 do
        local v
        pcall(function() v = actor["Anim" .. i] end)
        if v and v:IsValid() then
            local fn = v:GetFullName()            -- GetName() is blank on this build
            for role, shortName in pairs(SEQ) do
                if fn:find(shortName, 1, true) then seqCache[role] = v end
            end
        end
    end
end
5

Cooking & Packing

Cook for Windows · retoc from the root · the trio in LogicMods
  • Cook with Cook Content for Windows (UE 5.3.2), not Package Project. Cooked output: Saved\Cooked\Windows\OblivionRemastered\Content\Mods\MountedCombat_P\.
  • Repack with retoc from the OblivionRemastered root (not deeper) so internal paths stay intact:
    retoc.exe to-zen --version=UE5_3 "...\OblivionRemastered" "MountedCombat_P.utoc"
  • Place the trio (.pak + .ucas + .utoc, all three, same base name) in ...\Content\Paks\LogicMods\, named to match the content folder.
6

The Lua Side: Playback, Isolation, Targeting

Mount detection that doesn’t flap · target the rider · weapon grip

Detect mounted state, and this is one that changed late. The obvious signal, the rider anim instance, turned out to be unusable:

VLocomotionHorseRiderAnimInstance FlapsIt is created and destroyed every few ticks while mounted, so a poll loop watching FindFirstOf(...):IsValid() sees mounted → not mounted → mounted several times a second. That produced 100+ spurious mount/dismount events in a few minutes and the horse-exclusion cache never settled.

The stable signal is the horse’s rider reference: if any BP_Generic_Horse_C has a rider property that IsPlayerCharacter(), you’re mounted. (Same rider-prop scan the HorseSpeedBoost mod uses.)

local function IsPlayerMounted()
    local ok, horses = pcall(function() return FindAllOf("BP_Generic_Horse_C") end)
    if not ok or not horses then return false end
    for _, h in ipairs(horses) do
        if IsValidObj(h) then
            for _, prop in ipairs({ "Rider", "RiderPawn", "RiderActor", "MountedActor", "Driver" }) do
                local rok, rider = pcall(function() return h[prop] end)
                if rok and IsValidObj(rider) then
                    local pok, isP = pcall(function() return rider:IsPlayerCharacter() end)
                    if pok and isP then return true end
                end
            end
        end
    end
    return false
end

Target the rider, not the horse. When mounted, player.Pawn is the horse. The rider is BP_OblivionPlayerCharacter_C; play montages on its mesh’s anim instance.

Weapon grip: attach with SnapToTarget, not KeepWorld. The weapon (WeaponsPairingComponent.WeaponActor) lagged/floated off the hand during swings. Cause: K2_AttachToComponent with the KeepWorld rule (2,2,2) froze whatever world offset the weapon had at attach time. Switching to SnapToTarget (0,0,0) makes it adopt the socket transform exactly and ride the hand bone every frame:

wa:K2_AttachToComponent(mesh, FName.new("Weapon_Socket"), 0, 0, 0, false)

The skeleton’s Weapon_Socket sits under a Weapon_Anchor bone under hand_r; SnapToTarget resolved the float without touching the anchor chain. Sheathe to the right socket by weapon type: one-handers to SideWeapon_Socket, two-handers to BackWeapon_Socket (detect via WeaponActor.bIsTwoHanded / WeaponType == 2, with a weapon-name keyword fallback). On dismount, stop the FullBody slot so a mounted swing can’t bleed into ground animation:

animInst:StopSlotAnimation(0.15, FName.new("FullBody"))
7

Hit Detection & Damage, The Real Answer

Every standard damage path is a dead end · SendMeleeHitOnPairedPawn

Because attacks are custom montages played outside the normal combat action state, the game’s own melee detection doesn’t run, so it’s done in two layers.

Layer 1, sound/impact (reuse the game). Add notify states to the attack clips at the contact frame: VAnimNotifyState_ImpactSystem (impact FX + sound) and VAnim_ActionMeleeHitWindow (the game’s damage window). These normally fire inside the combat action state; outside it they’re best-effort. Damage does not depend on them.

Layer 2, extended-range detection (Lua, full control). Mounted, the player is elevated and the game’s melee reach overshoots ground enemies. Run a proximity check at the swing’s contact moment (~260ms after the swing starts) with a radius you control. Two details that mattered:

  • Measure from the rider mesh, not the rider actor. When mounted, the rider actor’s location is stale (it’s attached to the horse). Use mesh:K2_GetComponentLocation() as the sphere origin.
  • The check is omnidirectional, pure distance, no facing cone. Anything within HIT_RADIUS (450u) gets hit regardless of camera direction. HIT_RADIUS is the mounted hitbox size.

Exclusion is belt-and-suspenders: skip the player, the loader, the specific horse you’re riding (cached by full name on each mount event), and any horse at all if the cache hasn’t resolved yet, so you never damage your own mount.

Damage application, the real answer

This was the single hardest part, and every standard path is a dead end in OBR. What was ruled out, so nobody repeats it:

TriedResult
UGameplayStatics::ApplyDamage(target, amt, pc, causer, nil)No-op. Fires clean, does nothing. OBR pawns don’t route through UE’s TakeDamage (confirmed by hitting the player with it every swing for 30s, never died).
Actor-value setters by guessed name (GetHealth, SetHealth, DamageHealth…)Don’t exist. Reading returns a UObject wrapper but calling them errors.
OnCombatHitTaken / OnCombatHitDealt with a Lua-table structNot callable. Take one PairedOblivionHitEvent{Attacker, Target} struct; UE4SS won’t marshal a Lua table into it, and even a captured live struct replayed returns false, they’re engine notifications, not invocable functions.
DoRagdoll()Callable but no visible effect; not a damage path.

The function that actually works, found by enumerating the pawn’s class with UE4SS reflection (ForEachFunction walking the class hierarchy), is:

The Working Callrider:SendMeleeHitOnPairedPawn(targetPawn, damage, false), the game’s own paired-pawn melee entry. Called on the rider (BP_OblivionPlayerCharacter_C) with the target pawn, it routes through native combat: real damage, hit reaction, death, and loot, no struct to fabricate.

Backup HP write via the actor-value component. As a fallback (and to guarantee the kill if native damage is resisted/scaled), read and reduce Health directly. Actor-value index 10 = Health on this build:

local avc = pawn.ActorValuesPairingComponent
local hp  = avc:GetFloatModifiedActorValue(10)          -- 10 = Health
avc:SetFloatModifiedActorValue(10, math.max(0, hp - DAMAGE_AMOUNT))

The shipping code probes for the correct setter once (SetFloatModifiedActorValueModifyActorValueSetActorValueForceSetActorValueSetBaseActorValue), caches whichever succeeds, and reuses it. SetFloatModifiedActorValue is the one that works here.

Crash hardening (learned the hard way)

SendMeleeHitOnPairedPawn runs through ProcessEvent, and pcall does not catch native SEH exceptions. An EXCEPTION_ACCESS_VIOLATION writing 0x0 came from the target pawn being garbage-collected between the safety check and the actual UFunction call. The fix is a redundant IsValidObj() on both rider and target immediately before the call:

if not IsValidObj(rider) or not IsValidObj(pawn) then return end
rider:SendMeleeHitOnPairedPawn(pawn, DAMAGE_AMOUNT, false)

Also: a hitThisSwing table keyed by pawn dedupes so one swing applies damage once per target, and a separate unrelated mod (PlayerCamera) was crashing on every attack input via IsVisible on a null object, fully remove that folder, disabling it in mods.txt isn’t enough since its scripts still load and hook attack input.

8

Failure → Cause → Fix (the whole journey at a glance)

The complete debugging table
SymptomReal causeFix
UpperBody montage plays, nothing visible while mountedRiding layer bypasses the CombatPose branch the UpperBody slot lives inUse FullBody slot instead
Rider stands up / pops ~66u on attackNon-additive full-body clip overrides seated pelvisMake clips additive + strip legs
Swing distorted/offsetAdditive base pose = Skeleton Reference Pose (T-pose)Base pose = standing one-hand idle frame
LoadAsset runs, asset never in memoryNew-path asset unreferenced; LoadAsset is registry-gatedHard-reference it from a loader BP
Boot crash, log ends mid-loadStaticLoadObject called at menu (and it’s absent in this build)Remove it; load via loader BP, read off the actor
ModClass not valid, ModActor never spawnsPak name ≠ content folder nameRename pak to match /Game/Mods/<folder>/
ModActor spawns, anims still missingRefs on a separate BP, or split across paksRefs on ModActor; bundle everything in one LogicMods pak
ModActor refs read as valid but unidentifiedGetName() blank on this buildMatch by GetFullName, positional fallback
Hits don’t register from horsebackCustom montage skips the game’s melee detection; reach overshootsLua proximity detection at a tuned radius, measured from the rider mesh
Damage never applies (ApplyDamage, AV setters, OnCombatHit*)OBR uses none of the standard UE damage pathsrider:SendMeleeHitOnPairedPawn(target, dmg, false) + AV index-10 HP write
Weapon floats off the hand during swingsK2_AttachToComponent used KeepWorld (froze a bad offset)Attach with SnapToTarget (0,0,0)
100+ phantom mount/dismount events; horse cache never settlesVLocomotionHorseRiderAnimInstance is created/destroyed every few ticksDetect via horse’s rider ref + IsPlayerCharacter()
EXCEPTION_ACCESS_VIOLATION writing 0x0 on hitTarget GC’d between safety check and ProcessEvent; pcall can’t catch native SEHRedundant IsValidObj() on both objects immediately before the call
Game crashes on every attack pressUnrelated PlayerCamera mod calls IsVisible on nullDelete the PlayerCamera folder (disabling in mods.txt isn’t enough)

Key takeaways

  • The mounted graph bypasses the combat slots and the spine mask, additive-on-FullBody is the only path to seated upper-body attacks.
  • New-path animations need a hard-reference anchor (a ModActor loader BP) to load at all; LoadAsset won’t and StaticLoadObject doesn’t exist here.
  • BPModLoaderMod’s contract is strict: pak name = content folder name, refs on ModActor, one bundled pak.
  • When the engine won’t resolve assets by path, read them off the spawned actor’s variables instead.
  • Damage in OBR is SendMeleeHitOnPairedPawn on the rider, not any UE damage path, with actor-value index 10 = Health as the direct fallback. Found by reflecting over the pawn’s class, not by guessing.
  • Don’t trust the rider anim instance for mount state, it flaps. Use the horse’s rider reference.
  • pcall can’t catch native SEH crashes, re-validate UObjects immediately before any ProcessEvent-routed call.

Tools relied on: UE4SS + BPModLoaderMod, retoc, UAssetGUI, the Altar SDK (UE 5.3.2). Built on community research documented throughout this wiki., CosmicBoogaloo / DreamEater Studio

9

The Last 1%: A C++ Crash-Guard for the Native Call

pcall can’t catch a native SEH · MC_SafeCall wraps it in __try/__except

Lesson 7 ends on an honest admission: even after skipping dead targets and re-validating components, the native SendMeleeHitOnPairedPawn can still fault on a same-frame death race (a component reachable from the target is freed mid-call). That fault is a CPU access violation (inc dword ptr [rax] on a garbage pointer), and Lua’s pcall cannot catch it. pcall catches Lua errors; a native SEH unwinds past Lua entirely. The only complete fix is a tiny C++ UE4SS mod that wraps the call in an OS structured-exception handler.

pcall Is the Wrong Tool for a Native FaultA Lua pcall only traps errors raised by Lua. An access violation from native game code is a hardware/OS exception (SEH) that blows straight through the Lua VM. No amount of Lua validation closes the window completely. You have to catch it in C++ with __try/__except.

SafeMeleeHit is that companion: a minimal C++ UE4SS mod (built from the CppUserModBase template). On Lua start it registers exactly one global Lua function, MC_SafeCall(fn), which invokes the passed Lua function inside a native __try { fn() } __except(…) { }. If fn triggers an access violation, the __except swallows it: the swing’s hit fizzles (no damage that frame) instead of taking the whole process down.

How the Lua side calls it (looked up fresh, so load order doesn’t matter)

local SafeCall = rawget(_G, "MC_SafeCall") -- nil if the C++ mod isn't installed
local function fireMelee()
 rider:SendMeleeHitOnPairedPawn(pawn, DAMAGE_AMOUNT, false)
end

local ok
if SafeCall then ok = SafeCall(fireMelee) -- SEH-protected by the C++ companion
else ok = pcall(fireMelee) -- Lua-only fallback, can still CTD on the race
end
if not ok then
 print("[MC] melee call faulted and was contained (no damage this swing)")
 return
end

It’s a separate, optional mod. If present, MC_SafeCall exists and Mounted Combat routes the dangerous call through it; if absent, it falls back to pcall and the load line prints companion: not found. That’s why it’s fetched with rawget(_G, "MC_SafeCall") on every call, neither the presence nor the load order of the two mods is ever assumed.

The General Pattern for Any Risky Native CallThis isn’t specific to melee. Any time you call a native UE4SS method that can fault on freed memory mid-call (combat, actor teardown, GC-window races), a one-function C++ SEH wrapper is the only complete fix. Register it once from a C++ companion, then guard every dangerous Lua->native call with it. It is the missing piece pcall can never provide.
10

The v10.10.x Reliability Pass: Detection, Friendly Fire, Two-Call Damage

Mount detection that works on any horse · stop hitting your own mount · damage needs both calls

Three bugs surfaced once other players (not just the dev’s own test horse) ran the mod. All three trace back to the same discipline: read class names and properties (safe pointer reads) and never call GetName/GetFullName/UFunctions on world NPC pawns (those SEH-crash).

1. Mount detection that works on any horse

The old detector scanned one hard-coded BP_Generic_Horse_C class, then probed five guessed rider-reference property names (Rider, RiderPawn, RiderActor, MountedActor, Driver), none ever confirmed to exist. It worked on the dev’s test horse and failed for everyone on any other horse. The rewrite uses the method already proven in Better Horse Control: while mounted, GetPlayer() (and the controller’s Pawn) is the horse pawn, and its PairedPawnMovementComponent class name contains "HorseMovement" for any horse, vanilla, unique, reskinned, or modded. No class guessing, no property probing.

local ok2, cls = pcall(function() return mc:GetClass():GetFName():ToString() end)
if ok2 and cls and cls:find("HorseMovement") then return pawn end -- it's the ridden horse

2. Stop damaging your own horse

You sit on the horse, horses are VPairedPawn subclasses, so the ridden horse shows up in the FindAllOf("VPairedPawn") swing scan as the closest target, and was taking the hit (the infamous “damage only hit the horse” report). Fix: exclude by class name containing "horse" (safe to read; class names don’t SEH like NPC GetFullName), applied both in target selection and as a hard final guard in SafeToHit right before the native call. Identity checks via GetAddress() are kept as belt-and-suspenders. Bonus: damaging the ridden horse, whose mounted paired-pawn state is itself unstable (it flaps ~2×/sec), was also a likely crash. One fix closed two bugs.

3. Damage needs BOTH calls, together

Neither call alone lands damage. SendMeleeHitOnPairedPawn registers the hit event; the actor-value write (SetFloatModifiedActorValue(10, hp)) sets the new HP, but the write only persists because the melee call created the hit event first. A mid-cycle build (v10.10.1) dropped the AVC write on a wrong “it’s read-only” assumption and damage silently vanished; v10.10.4 restored the pairing. Health is actor value index 10 on the ActorValuesPairingComponent.

The Common Thread: Safe-Read DisciplineEvery fix above is the same rule applied again: read class names and properties (raw pointer reads via :IsValid() / GetClass(), safe on any thread and on stale wrappers), and never call GetName/GetFullName or arbitrary UFunctions on a world NPC pawn. The authoritative mount flag g_mounted is a plain Lua bool written only on the game thread, so keybind and async contexts read it without ever touching a UObject, which is what killed the off-thread ProcessEvent crashes the old generation-counter machinery kept fighting.

Custom Standalone Wwise Audio (ZA WARUDO)

Press , to freeze time + play a brand-new custom sound, press again to resume. By CosmicBoogaloo / DreamEater · Altar build, UE 5.3.2 · Wwise 2023.1.8.8601.3258.

This documents getting a brand-new Wwise sound (not a vanilla replacement) to play in the packaged game. The community wiki called this a “research wall.” It is solvable. The bank was never the hard part, the post method was.

TL;DR, The One Thing That Cracked ItAfter a perfect bank, correct routing, correct mount, and a loaded event, every standard way to post the event failed (BP node crashes; PostEvent/PostOnActor/PostOnComponent hit a delegate wall). The winner: UAkGameplayStatics.PostEventAtLocation(event, location, rotation, world), the one exposed method with a signature UE4SS Lua can actually call (no blocking delegate). It returns a valid playing ID and plays the custom bank’s in-memory media. If you only read one thing, read that.
1

Wwise Integration From Scratch

Exact version · three Altar source fixes · build via Build.bat
Version Must Match EXACTLYUse Wwise 2023.1.8.8601.3258. Any other version produces bank-version mismatches the runtime rejects silently or with bank version X vs runtime 150. Install via the Audiokinetic Launcher.

Integrate into the Altar project

  1. In the Altar project, delete the existing Wwise and WwiseNiagara plugin folders.
  2. Audiokinetic Launcher → Integrate Wwise into Project.
  3. Fix engine source compile errors (Altar stubs reference outdated Wwise class names):
    • Source/Altar/Public/VAltarAkPortalComponent.h → change include to "AkAcousticPortal.h", parent class UAkPortalComponent.
    • Source/Altar/Public/VMusicPlayer.h → change include from "EAkCallbackType.h" to "AkGameplayTypes.h" (where enum class EAkCallbackType lives in 2023.1.8).
  4. Null-guard the listener crash in Plugins/Wwise/Source/AkAudio/Private/AkAudioDevice.cpp, AddDefaultListener() crashes writing IsListener=true on a null listener. At the top of the function add:
    if (in_pListener == nullptr) return;
    auto* SoundEngineCheck = IWwiseSoundEngineAPI::Get();
    if (SoundEngineCheck == nullptr) return;
    Also guard SetSpatialAudioListener with a SpatialAudioCheck.
  5. Build from the command line (the VS GUI threw “invalid pointer”):
    Build.bat AltarEditor Win64 Development -project="...\OblivionRemastered.uproject" -waitmutex
    Target name: AltarEditor; uproject: OblivionRemastered.uproject.
2

Authoring the Sound in Wwise

Remove the Motion device · in-memory media · output paths must agree

Project setup

  • Wwise project migrated to 2023.1.8. Two SFX sources (timestart, timestop) in the Actor-Mixer Hierarchy under Default Work Unit.
  • Two user-defined SoundBanks (TimeStart, TimeStop), plus the auto-defined Init bank.
  • Both sounds route to the default Master Audio Bus.
CRITICAL: Remove the Wwise Motion DeviceThe biggest hidden blocker. Bank generation was failing entirely with MissingPlugin: Bank 'Init' includes a 'Wwise Motion' Audio Device ... not installed. WwiseConsole failed with error 1. Because generation failed, banks never updated, every “regeneration” silently produced nothing usable, which is why the event’s media never populated.

Fix: delete all Motion references and save the project: Audio Devices → Default Work Unit → delete Default_Motion_Device; Master-Mixer Hierarchy → delete Motion Factory Bus; Actor-Mixer Hierarchy → delete the Factory Motion tree if present. The unsaved-changes asterisk means generation still reads the old on-disk state. After removal, generation completes: Process completed successfully.

In-memory media (self-contained banks)

Keep media in-memory (Stream unchecked). The bank then bakes the audio directly into its DATA chunk, no separate streamed .wem dependency at runtime. A correct bank parses like this:

BKHD  Bank Version: 150   Bank ID: 3886797186
DIDX  media id=32147691  size=478616
DATA  478616 bytes of embedded media
HIRC  Event -> Action -> Sound -> (media 32147691, bus 3803692087)

3803692087 is the FNV-1 hash of “Master Audio Bus”, the standard ID the vanilla game’s master bus also uses, so the sound routes correctly with no GUID spoofing needed.

Output paths (Wwise + UE must agree)

Set the Wwise Root Output Path and the UE Wwise Integration Root Output Path to the same root. Delete stale bank copies: old banks at the GeneratedSoundBanks\ root (v132) shadowed the fresh ones in GeneratedSoundBanks\Windows\ (v150), so UE read the stale copies → bank version 132 vs runtime 150 and Could not find suitable platform. Delete the loose root copies; keep the Windows\ subfolder.

3

Cooking & Packaging, Two Paks, Two Tools

retoc trio for uassets · UnrealPak filelist for loose audio

OBR uses IoStore, which forces a split:

ContentToolOutputWhy
Cooked uassets (ModActor, AkAudioEvents, InitBank.uasset)retoc to-zen.pak + .utoc + .ucas trioIoStore, mounts like vanilla mods
Loose .bnk + .wemUnrealPaksingle .pakretoc ignores loose files
A Lone UnrealPak Pak Does NOT Mount Cooked UassetsThat’s why early attempts left the mod totally invisible (ModActor present: false, 127 vanilla events load but yours load 0). Your working reference is your own MountedCombat mod, it’s a .pak+.utoc+.ucas trio. Match it.

retoc for the trio (cooked uassets)

Input directory = the folder containing OblivionRemastered so the mount path keeps the OblivionRemastered/Content/... prefix. Engine version UE5_3. Produces TimeStop.pak + .utoc + .ucas. retoc packs cooked uassets only (each .uasset needs its .uexp twin).

UnrealPak for the loose audio

Use a response-file (filelist) with explicit mount targets. Folder-based -create sets the mount to the wrong common root and your files end up not under OblivionRemastered/. The filelist forces correct relative paths:

"H:\...\Content\WwiseAudio\TimeStop.bnk"          "../../../OblivionRemastered/Content/WwiseAudio/TimeStop.bnk"
"H:\...\Content\WwiseAudio\TimeStart.bnk"         "../../../OblivionRemastered/Content/WwiseAudio/TimeStart.bnk"
"H:\...\Content\WwiseAudio\Media\32147691.wem"    "../../../OblivionRemastered/Content/WwiseAudio/Media/32147691.wem"
"H:\...\Content\WwiseAudio\Media\510089961.wem"   "../../../OblivionRemastered/Content/WwiseAudio/Media/510089961.wem"
UnrealPak.exe "H:\...\TimeStopAudio_P.pak" -create="H:\...\audio_filelist.txt" -compress
Two Path Rules That Bite.bnk paths are flat in WwiseAudio/ (matching SoundBanksInfo.json"Path": "TimeStop.bnk"), NOT in an Event/ subfolder. And .wem filenames must be the media ID from the metadata (510089961.wem, 32147691.wem), source wems live in ...\.cache\Windows\SFX\ and the hex in the cache name is NOT the media ID, so rename them.
DO NOT Ship an Init BankShipping your own Init.bnk (or InitBank.uasset) silences ALL game audio, your minimal init bank overwrites the vanilla one, wiping the game’s bus/routing config. The vanilla game already has its init bank loaded; your event routes through the vanilla Master Audio Bus. Remove Init.bnk from the audio pak and InitBank.uasset/.uexp from the trio. (Same mechanism is why calling LoadInitBank() from Lua at runtime kills all audio, never do it.)

Pak naming & placement

Name the trio without _P: TimeStop.pak/.utoc/.ucas. BPModLoader derives the mod folder from the pak name; TimeStop_P made it look for /Game/Mods/TimeStop_P/ModActor (wrong). Trio → LogicMods; audio pak → ~mods; no duplicate/stale paks anywhere.

4

Playing It From Lua, The Breakthrough

Why the standard posts fail · PostEventAtLocation

Why not the Blueprint / standard posts

  • The BP Post Event node crashes at runtime in the packaged build.
  • UAkGameplayStatics.PostEvent, AkAudioEvent.PostOnActor/PostOnComponent, AkComponent.PostAkEvent all require a PostEventCallback DelegateProperty, which UE4SS Lua cannot pass (Parameter 'Delegate' of type 'DelegateProperty' not supported).
  • There is no LoadBank/LoadBankByName in the exposed API, only LoadInitBank/UnloadInitBank/ClearSoundBanksAndMedia. The custom bank must auto-load via the event’s RequiredBank.

Discovering the working method

Dump every callable UFunction by scanning Function objects (ForEachFunction was unavailable in this build):

for _, fn in ipairs(FindAllOf("Function") or {}) do
    local full = fn:GetFullName()
    if full:find("AkGameplayStatics") then print(full) end
end

This revealed PostEventAtLocation, which takes (event, location, rotation, world) and no delegate. UE4SS can call it.

The working call

local function PlayEvent(path)
    local ev = StaticFindObject(path)
    if not (ev and ev:IsValid()) then LoadAsset(path); ev = StaticFindObject(path) end
    local player = UEHelpers.GetPlayer()
    local AkStatics = StaticFindObject("/Script/AkAudio.Default__AkGameplayStatics")
    local loc = player:K2_GetActorLocation()
    local rot = {Pitch = 0, Yaw = 0, Roll = 0}
    return AkStatics:PostEventAtLocation(ev, loc, rot, UEHelpers.GetWorld())
end

Returns a valid playing ID and plays the custom in-memory media. No BP, no ModActor needed for audio. The bank does not need an explicit load call, if the .bnk/.wem are correctly packed and the AkAudioEvent references them by SID, the Wwise runtime loads them automatically when the event is posted.

5

Final Working main.lua + What Ships

The complete script and the shipping file layout
local UEHelpers = require("UEHelpers")
local Kismet = StaticFindObject('/Script/Engine.Default__KismetSystemLibrary')
local frozen = false

local EVENT_FREEZE = "/Game/WwiseAudio/Events/TimeStop/TimeStop.TimeStop"
local EVENT_RESUME = "/Game/WwiseAudio/Events/TimeStop/TimeStart.TimeStart"

local function console(cmd)
    ExecuteInGameThread(function()
        local pc = UEHelpers.GetPlayerController()
        if pc and pc:IsValid() then
            Kismet:ExecuteConsoleCommand(pc.Player, cmd, pc, true)
        end
    end)
end

local function GetEvent(path)
    local ev = StaticFindObject(path)
    if not (ev and ev:IsValid()) then LoadAsset(path); ev = StaticFindObject(path) end
    return ev
end

local function PlayEvent(path)
    local ev = GetEvent(path)
    if not (ev and ev:IsValid()) then print("[Timestop] event not found: " .. path); return false end
    local player = UEHelpers.GetPlayer()
    if not (player and player:IsValid()) then return false end
    local AkStatics = StaticFindObject("/Script/AkAudio.Default__AkGameplayStatics")
    if not (AkStatics and AkStatics:IsValid()) then return false end
    local loc = player:K2_GetActorLocation()
    local rot = {Pitch = 0, Yaw = 0, Roll = 0}
    local ok, id = pcall(function()
        return AkStatics:PostEventAtLocation(ev, loc, rot, UEHelpers.GetWorld())
    end)
    if ok then print("[Timestop] played (id=" .. tostring(id) .. ")"); return true end
    print("[Timestop] post failed: " .. tostring(id)); return false
end

ExecuteInGameThread(function()
    GetEvent(EVENT_FREEZE); GetEvent(EVENT_RESUME)
    print("[Timestop] ready - press , to ZA WARUDO")
end)

RegisterKeyBind(Key.OEM_COMMA, function()
    ExecuteInGameThread(function()
        if not frozen then
            PlayEvent(EVENT_FREEZE); console("SloMo 0"); frozen = true
        else
            PlayEvent(EVENT_RESUME); console("SloMo 1"); frozen = false
        end
    end)
end)
Key ConstantKey.OEM_COMMA is the , key in UE4SS (OEM_PERIOD is .). Using Key.COMMA passes nil to RegisterKeyBind.

What ships in the final mod

LogicMods/
  TimeStop.pak / .utoc / .ucas      <- retoc trio: AkAudioEvent uassets (+ ModActor, optional)
                                       NO InitBank.uasset
~mods/
  TimeStopAudio_P.pak               <- UnrealPak: TimeStop.bnk, TimeStart.bnk (v150, flat),
                                       Media/32147691.wem, Media/510089961.wem
                                       NO Init.bnk
ue4ss/Mods/TimestopZaWarudo/
  scripts/main.lua                  <- the script above
  enabled.txt

The AkAudioEvent uassets (in the trio) + the loose banks/wems (in the audio pak) are both required: the event auto-loads its RequiredBank from the loose .bnk, and PostEventAtLocation plays it.

6

Debugging Checklist & Expensive Lessons

Symptom → cause → fix, and the realizations that cost the most
SymptomCauseFix
WwiseConsole failed with error 1 on generationMotion device referenced, plugin not installedDelete Motion device/bus, save project
bank version 132 vs runtime 150Stale banks at GeneratedSoundBanks\ root shadowing Windows\Delete loose root copies
Could not find suitable platform for WindowsUE reading stale/rootless banks; platform GUID mismatchAlign output paths, delete stale banks
Mod invisible, ModActor present: false, your events = 0 of NLone UnrealPak .pak doesn’t mount cooked uassets in IoStoreUse retoc trio for uassets
Content mounts at bare Content/ not OblivionRemastered/Content/Wrong pak root / missing mount targetsPoint input one level up; filelist with explicit ../../../OblivionRemastered/... targets
ModClass for 'X_P' is not validBPModLoader derives folder from pak name incl. _PDrop _P; pak name = content folder name
ALL game audio silent when pak presentShipping your own Init bank overrides vanilla routingRemove Init.bnk + InitBank.uasset
Event plays in-editor, crashes/silent in gamePosting via BP node or delegate-requiring APIPost via PostEventAtLocation from Lua
Parameter 'Delegate' ... not supportedUE4SS can’t pass DelegatePropertyUse PostEventAtLocation (no delegate)
AkAudioEvent “Contains Media” checked but Media array emptyNormal for in-memory banks, media lives in the bank, not the eventNot a bug; ignore
FModel won’t parse your .bnk (IndexOutOfRange)FModel’s .bnk parser is version-fragileNot a bug; verify via SoundBanksInfo.json instead

Key realizations (the expensive lessons)

  1. The bank was correct the entire time. Parsing the raw .bnk proved valid v150 + embedded media + correct bus routing. Don’t trust FModel’s parse failure, trust SoundBanksInfo.json and a manual chunk parse.
  2. The empty Media array is normal for in-memory audio, the media lives in the bank, not the event asset.
  3. The real blocker was the post method, not the bank. UE4SS can’t pass delegates, the BP node crashes, and there’s no LoadBank. PostEventAtLocation is the escape hatch.
  4. Never ship an Init bank and never call LoadInitBank() at runtime, both nuke all game audio.
  5. Two paks, two tools: retoc trio for cooked uassets (IoStore), UnrealPak for loose banks/wems.
  6. Dump the real API instead of guessing function names, scanning Function objects revealed PostEventAtLocation.

Custom standalone Wwise audio in Oblivion Remastered: solved. Press comma. ZA WARUDO., CosmicBoogaloo / DreamEater LLC


Better Horse Control

Live-tuning the ridden horse’s movement component from an in-game menu. By CosmicBoogaloo / DreamEater · evolved from HorseSpeedBoost.

OBR horses ride at one fixed feel. Better Horse Control writes the ridden horse’s VHorseMovementComponent live, speed, turn rate, acceleration, grip, braking, even an experimental snap-to-camera steering mode, and keeps those values applied as you swap between horses. No ESP, no asset work; it’s pure UE4SS Lua property writes.

End ResultWalk and gallop speed, turn rate, acceleration, ground grip and braking all adjustable while you ride from the CosmicMCM menu (F10), plus an optional instant “direct steering” mode, applied to whatever horse you’re currently on (vanilla, unique, or modded) and restored to vanilla on the horse you leave.
1

Finding the Ridden Horse (the reliable way)

Mounted, GetPlayer() IS the horse · filter on the movement component class name

This is the discovery that later fixed Mounted Combat too. While mounted, GetPlayer() (and the controller’s Pawn) returns the horse pawn itself, whose PairedPawnMovementComponent is the VHorseMovementComponent. On foot, GetPlayer() is the human, whose movement component is a plain VPairedPawnMovementComponent, so you filter on the class name containing "HorseMovement". Works for any horse, any number loaded, with zero rider/mount guessing.

local function horseMCfromPawn(pawn)
 if not SafeIsValid(pawn) then return nil end
 local ok, mc = pcall(function() return pawn.PairedPawnMovementComponent end)
 if not ok or not SafeIsValid(mc) then return nil end
 local ok2, cls = pcall(function() return mc:GetClass():GetFName():ToString() end)
 if ok2 and cls and cls:find("HorseMovement") then return mc end -- the ridden horse's move comp
 return nil
end

Primary lookup is GetPlayer(); the fallback is the controller’s possessed Pawn (also the horse when mounted).

2

The Vanilla Values and the “walk” Trap

Confirmed baselines, and why writing MoveWalkMax alone does nothing

All confirmed by in-game probe (these are the restore-to-vanilla targets):

PropertyVanillaControls
MoveWalkMax300walk gait base speed
TrotMultiplier4.5trot gait (the felt “walk”)
MoveRunMult7.5run gait
MoveSprintBaseMult4.4sprint gait
MaxAcceleration2048how fast it reaches target speed
GroundFriction8grip / cornering bite
BrakingDecelerationWalking2048stopping force
RotationRate.Yaw90turn rate (deg/sec)
The Felt “Walk” Speed Is Really the Trot GaitWriting MoveWalkMax alone did nothing visible, the speed you feel at low gait is the trot. So the slider WalkMult scales walk and trot together, and GallopMult scales run and sprint together, each from the vanilla base. Scaling all four gaits from the baseline (the HorseSpeedBoost approach) is what actually changes the feel.
3

Two Property Traps: Struct Write-Back & Owner-Side Steering

RotationRate is a struct copy · bUseControllerRotationYaw lives on the pawn

RotationRate is an FRotator struct. Setting mc.RotationRate.Yaw = x directly modifies a temporary copy that’s immediately discarded, the change never lands. You must read the struct, mutate it, and write the whole thing back:

local rr = mc.RotationRate
rr.Yaw = yaw
mc.RotationRate = rr -- write the whole struct back, or the change is lost

Direct steering lives on the pawn, not the movement component. bUseControllerRotationYaw is set on mc:GetOwner(). Off (default) = vanilla velocity-orient turning, where Turn Rate applies. On = the horse yaw snaps straight to the camera every frame, instant, zero drag, by setting the owner’s bUseControllerRotationYaw=true and disabling bOrientRotationToMovement.

4

Making Settings Stick Across Horse Swaps

Re-apply every poll · change-detection only resets the horse you left

The apply is deliberately not gated behind “is this a new horse?” anymore. Every poll it re-writes the current config to whatever horse is currently ridden. The old key-diff gate could misfire on a second horse and skip the apply, so your settings “fell off” after a swap. Change-detection is now used for one thing only: resetting the horse you just left back to vanilla, so you never leave modded values on a dismounted horse.

local function Tick()
 local mc = GetRiddenHorseMC()
 local key = MCKey(mc)
 local changed = (key ~= activeMCKey)
 if changed then
 if SafeIsValid(activeMC) then ResetControl(activeMC) end -- restore the one we left
 activeMC, activeMCKey = mc, key
 end
 if SafeIsValid(mc) then ApplyControl(mc) end -- always (re)apply to current
end

It runs on a throttled LoopAsync, plus a ClientRestart hook for an instant re-apply on the mount/dismount possession handoff.

5

Wiring It to the Menu (console-command bridge)

CosmicMCM fires a console command · the mod catches it with a handler

Settings come from CosmicMCM. Because UE4SS runs every mod in its own isolated Lua state, the menu can’t call into this mod directly, it fires a console command, and Better Horse Control catches it:

RegisterConsoleCommandHandler("BetterHorseControlSetMCM", function(fullCmd, params)
 if #params < 2 then return true end
 local key, val = params[1], params[2]
 if config[key] == nil then return true end -- ignore unknown keys
 config[key] = tonumber(val) or config[key]
 if SafeIsValid(activeMC) then ApplyControl(activeMC) end -- apply live to the ridden horse
 return true
end)

CosmicMCM sends BetterHorseControlSetMCM <key> <value> each time you nudge a slider; the mod updates its live config table and immediately re-applies to the horse you’re on. See the CosmicMCM writeup below for the other half of this bridge.

Better Horse Control: every gait, the turn rate, and the grip, tuned live on any horse from one menu. CosmicBoogaloo / DreamEater


CosmicMCM: One Settings Menu for Every Mod

A from-scratch in-game configuration menu (F10) that live-configures every DreamEater mod. By CosmicBoogaloo / DreamEater.

OBR has no mod-config framework, and the obvious way to build one, a real UMG menu with a widget per setting, hard-crashes on open. CosmicMCM is the workaround that actually ships: a single F10 menu with sliders, toggles and rebindable keys that configures multiple mods at once, persists to per-mod .ini files, and is built from three UMG widgets total.

End ResultOne F10 menu that live-configures ZA WARUDO, Better Horse Control, and anything else you register, sliders, toggles, and key rebinds, saved per-mod to .ini, pushed to each mod over a console-command bridge, and re-broadcast on startup, with almost no crash surface.
1

Why the Whole Menu Is One TextBlock

Hand-built UMG trees crash on open · three widgets, re-rendered as text
Hand-Building a Large UMG Tree Hard-CrashesConstructing many widgets (rows, boxes, a control per setting) via StaticConstructObject reliably crashes on menu open, a Slate/GC interaction. The more widgets you new up at runtime, the bigger the crash surface.

So the entire menu is one multi-line TextBlock inside a Border inside a UserWidget n/a three widgets, full stop. The menu is a formatted string (rows, a >> cursor, ON/OFF, slider values); navigating just rebuilds that string and calls SetText. Nothing is constructed or destroyed as you move around, so there’s nothing left to crash.

2

The Cross-Mod Bridge: Console Commands

Isolated Lua states can’t share globals · talk over the console channel

UE4SS runs every Lua mod in its own Lua state. Mods can’t share globals or call each other’s functions. The one reliable channel between them is the console: CosmicMCM executes <cmdPrefix> <key> <value> via KismetSystemLibrary, and the target mod catches it with RegisterConsoleCommandHandler.

local function sendValue(mi, key, value)
 local mod = MODS[mi]
 local cmd = string.format("%s %s %s", mod.cmdPrefix, key, tostring(value))
 ExecuteInGameThread(function() executeConsole(cmd) end)
end

Each managed mod is just a table: a cmdPrefix (e.g. BetterHorseControlSetMCM), its settings (headers, sliders, toggles, keybinds with min/max/step), and default values. Adding a mod to the menu is adding one entry to MODS and handling its command on the mod side.

3

Reading Keys Without a Focused Widget

Hook the controller’s AnyKey event · act only while open

A single TextBlock can’t take keyboard focus, so CosmicMCM reads input by hooking the player controller’s AnyKey input event (BP_AltarPlayerController…InpActEvt_AnyKey) and only acting while the menu is open. UP/DOWN navigate, LEFT/RIGHT adjust sliders, ENTER toggles / opens number-entry / starts a rebind, digits type an exact value, F11 switches mod, F10 opens/closes.

UE4SS Binds Don’t Consume the KeyA hooked/bound key still fires the normal game action too, bind a movement key and you get both the menu action and the in-game movement. That’s why rebindable hotkeys steer toward F-keys and punctuation.
4

Persistence & the Startup Broadcast

One .ini per mod · push every saved value once the world exists

Each mod gets one .ini in CosmicMCM/config/ ([settings], key=value), with the folder resolved from the running script path. UE4SS’s working directory is the game’s Binaries dir, not the mod folder, so you can’t use a relative path. Values save on every change.

The catch: at load time there’s no player yet, so ExecuteConsoleCommand has nothing to run against. CosmicMCM waits for the world via NotifyOnNewObject on the player character, then broadcasts every saved value to every managed mod, so settings (including a rebound Timestop key) apply on startup, not only when you next touch a slider.

NotifyOnNewObject("/Script/Altar.VOblivionPlayerCharacter", function()
 -- ... register the AnyKey hook ...
 ExecuteWithDelay(1500, function() broadcastAll() end) -- push saved config to all mods
end)

CosmicMCM: a whole settings framework from three widgets and a console channel. CosmicBoogaloo / DreamEater

Reference

Fixing Problems & Glossary

CTD index, status board, field notes, resource directory, and full glossary. Always open in another tab when something breaks.

Debugging Workflow

Opening the In-Game Console

The in-game console key is ` (backtick/tilde). This opens the Oblivion console. UE console commands are also available. The UE4SS GUI console window is separate, it opens alongside the game window when UE4SS is active with GuiConsoleVisible = 1 in UE4SS-settings.ini.

Rapid Testing Commands

tgm                    ; god mode for safe testing
tdetect                ; disable NPC detection so you can work undisturbed
set timescale to 0     ; freeze game time
coc "TestingHall"      ; jump to a vanilla test cell (or any cell EditorID)
coc "ICMarketDistrict" ; jump to a known working exterior
player.moveto 00000014 ; teleport to player start
ToggleDebugCamera      ; free-fly camera for scene inspection

Reading UE4SS Crash Logs

UE4SS logs are at: [GameDir]\Binaries\Win64\ue4ss\Logs\UE4SS.log

Key things to look for in crash logs:

  • Unhandled exception + stack trace, the actual crash location
  • LogScript: Error entries, OBScript evaluation errors
  • LogBlueprintUserMessages, Blueprint print nodes output here
  • ModuleManager errors, a DLL plugin failed to load

UE crash reports go to: C:\Users\YOU\AppData\Local\CrashReportClient\

Mod Compatibility & Conflicts

ESP Record Conflicts

  • When two mods modify the same record, the last one in load order wins. Neither mod’s change is truly merged, one overwrites the other.
  • To merge changes: open both ESPs in xEdit, copy the conflicting record into a new patch ESP, and manually combine both sets of changes. This is a compatibility patch.
  • xEdit shows conflicts in the right-click context menu and flags records with colored backgrounds. Red = conflict you must resolve.

PAK Conflicts

  • In ~mods, PAK files load in alphabetical order by filename with later entries overwriting earlier ones.
  • For your mod to win over another: prefix with a lower letter (000_) or instruct users to rename.
  • PAK conflicts are silent, no error, the later file just wins entirely for each asset.
  • SyncMap .ini files from multiple TSMI mods do NOT conflict, they are additive. Multiple INI files coexist fine.

Dirty Edits & ITM Records

When you open a record in xEdit and accidentally touch it without changing anything, xEdit may mark it as modified. These “Identical To Master” (ITM) records bloat your ESP and can cause unnecessary conflicts. Before releasing any mod: in xEdit, right-click your plugin → Other → Check for Errors and Other → Apply Filter for Cleaning. Remove ITM records.

Save Safety

Mod TypeSafe to Add Mid-Playthrough?Safe to Remove?
Texture/sound replacers (PAK only)YesYes, reverts to vanilla on removal
ESP stat edits (existing records)YesMostly yes, existing items revert to vanilla stats
ESP new items (new FormIDs)YesRisky, missing FormIDs in save cause issues
New interior cells (MagicLoader)YesNo, removing breaks saves that visited the cell
OBScript changesCareful, running scripts may have already modified save stateDepends, permanent ModAV changes stay in save
UE4SS Lua modsYesYes, no save state touched
ModAV is PermanentModActorValue in OBScript makes permanent changes to actor values stored in the save file. A player’s Health modified with ModAV cannot be “healed back” by normal means. Use SetActorValue for temporary changes or enchantment Fortify effects instead.

Distribution Checklist

Before Releasing a Mod

  1. Clean your ESP in xEdit (remove ITM records, check for errors).
  2. Test on a clean install without your development tools installed, users don’t have your UE project or xEdit scripts.
  3. Document what files go where in your mod description: pak in ~mods, ESP in Data, SyncMap ini in Data\SyncMap, etc.
  4. State your dependencies: OBSE64, UE4SS, TesSyncMapInjector, Address Library, any loader your mod needs.
  5. State game version: OBSE64 and compiled Blueprint paks are version-locked. Mention which game build you tested against.
  6. Include a folder structure diagram for anything beyond a simple pak replacement. Users get confused by nested install paths.
  7. Test removal: verify removing your mod on an existing save doesn’t corrupt it, or warn users clearly if it does.

What Files Users Actually Need

If your mod isUsers need
Texture/sound replacerModName_P.pak + .ucas + .utoc in ~mods
New item (existing model)ESP in Data + SyncMap .ini in Data\SyncMap
New item (custom model)ESP + SyncMap .ini + PAK in ~mods
New interior cellESP + SyncMap .ini + MagicLoader must be installed
Blueprint behavior modPAK in LogicMods\ModName + UE4SS installed
Lua scripting modLua mod folder in ue4ss\Mods\ + UE4SS installed

Performance Considerations

  • Draw calls matter more than polygon count. One complex merged mesh is better than 50 simple separate meshes in the same cell.
  • Cloth simulation is CPU-bound and scales with complexity. Use PhysicsProxy low-poly meshes to drive simulation and interpolate to the high-poly result.
  • Blueprint Tick cost: Event Tick in a Blueprint runs every frame. Use event-driven patterns (RegisterHook, NotifyOnNewObject) wherever possible instead of polling on tick.
  • ESP record count: practically unlimited, the vanilla game has thousands. Not a real bottleneck.
  • Lua mod load timing: many UE4SS mods loading simultaneously can cause CTDs due to race conditions. Use the deferred hook pattern and mods.txt/mods.json load ordering to sequence initialization.
  • PAK file size: no hard limit but very large PAKs (>1GB) can increase load time noticeably. Split by content type if needed.

Game Version & Patch Stability

  • Keep a backup of your working game version before any game update. Steam can be set to not auto-update under Properties → Updates.
  • OBSE64 breaks on every game binary update until offsets are manually updated by the OBSE team. Check the Nexus page changelog after patches.
  • Compiled Blueprint paks may become incompatible after patches that change class layouts. The game will usually crash silently on load or produce broken behavior rather than a useful error.
  • UAssetGUI edits are binary-specific, changes made to assets from one game version may not work on another. Always extract fresh assets from the current game version.
  • Check the Nexus early modding overview and Discord #announcements after every patch for community-reported breakage.

CTD & Issue Index

SymptomLikely CauseFix
New object invisible / CTD entering new interiorMissing SyncMap entry or wrong load orderRun Smart Mapper; place plugin above AltarDeluxe.esp
New item named [nl]SomethingAltar localization artifactInstall NL-Tag Remover (mods/473)
Won’t launch after adding a modScrambled load order or bad DLLRestore clean Plugins.txt; remove last-added mod; reinstall one at a time
OBSE “version not detected”Wrong build / wrong folder / base exe launchedMatch build; dll+exe in Binaries\Win64; always launch via OBSE loader
UE4SS not loading / startup crashOBSE conflict on first launchLaunch once WITHOUT OBSE first so UE4SS configures itself
Cooked assets won’t load in-gameWrong UE version used to cookCook in UE 5.3.2 specifically, not 5.5 or any other version
UAssetGUI “Failed to Parse X Exports”Referenced dependency files not extracted alongsideExtract all referenced assets too in the same directory tree, then retry
.pak ignored by gameMissing _P suffix or wrong load priorityAdd _P in ~mods; or prefix with 000_ for Paks folder alphabetical priority
BODY SETUP 0: BAD NAME INDEX crashCollision Presets = Block All, or wrong material slot namesSet Collision Presets to Custom; match slot names from vanilla FModel JSON
Whole world renders as UE grey gridStatic switch changed, or base material packedNever edit static switches; pak only the material instance, never the parent
Unversioned properties crash (hard to diagnose)Mod cooked with Shipping instead of DevelopmentAlways cook in Development configuration
BP_ form files crash after patch 1.2Old blueprint forms incompatible with new game versionExtract fresh vanilla BP_ forms from 1.2 PAKs; rebuild. Gauntlets and greaves most affected.
NPCs / creatures frozen in new cellNo navmesh data in new interiorsKnown limitation; no fix. Design rooms to not need NPC movement.
First-person weapon / glove / sleeve clippingWrong material parent for first-person gearUse FPSClippingFix material parent
Boots hide feet (bitmask = 0)0 means inherit from parent, not “show all”Use a non-zero value with relevant bits cleared
Inventory icon missingMissing T_ prefix in UE, or wrong xEdit pathPrefix PNG with T_ in UE only; keep .dds extension in xEdit Icon Image path
Textures blurry in-gameubulk split issueSet “Never Stream” on texture in UE; update retoc (issue #22 fixed)
Cloth mesh doubled (static + dynamic)Cloth mesh is the Root Skeletal Mesh ComponentCloth mesh must be a child of root, never root itself
Custom armor invisible when enchantedDynamic FormID not in TSMI SyncMapKnown bug; don’t change load order after enchanting; re-enchant if needed
Lua ExecuteConsole silently broken after patchAPI signature changedAdd fourth true parameter
JAA imports missing MIC layersJAA doesn’t import the MIC Layers tabEdit that material in UAssetGUI instead
Material un-assigns from chunk on project reopenAllow ChunkID Assignments not enabledEnable in Editor Preferences + set Cook Rule to Always Cook
Animation looks fine in editor, broken in-gameWrong bone index order from PSK skeletonImport SKEL_HumanoidSkeleton via JAA, not from PSK export
Notifies fire at wrong time in-gameWrong framerate (Blender default 24fps)Set Blender to 30fps; import UE at custom sample rate 60fps
RefreshAppearance crash with multiple modsMultiple mods calling it simultaneouslyLimit calls to only the aspect you’re changing
Enchantment VFX missing on custom weapon meshbAllowCPUAccess not set on the static meshSet Allow CPU Access = True in UAssetGUI on your SM_ asset

What Works, Status Board (mid-2026)

Mature & Stable
Texture/sound replacement · ESP property edits · vanilla-mesh standalone items via SyncMap · UI tweaks · OBScript enchantments / spells / AI via CS · UE4SS Lua runtime behaviors · Blueprint behavior mods.
Workable
Custom-mesh standalone items · animation replacement & retargeting · custom creatures via blueprint duplication · cloth capes via amulet slot workaround · body part hiding bitmask · MetaHuman facial animation (DNA files) · Wwise External Sources · new NPC creation · custom inventory icons · Blueprint UI mods · hair modding via data tables.
Partial
New interior cells, possible but no NPC pathfinding (NPCs freeze). Every placed object must be SyncMapped. Custom races, non-playable NPCs work; full CharGen sliders still in research. Lip sync on custom races, requires matching ABP_HeadPostProcess and voice type.
Not Yet Possible
UE-side terrain editing · new exterior cells & worldspaces · new custom Chaos Cloth Asset shapes (dataflows can’t be packed; vanilla CCAs can be duplicated with new materials) · fully custom voiced dialogue (WEM solved, BNK generation is the blocker) · custom magic effects (hardcoded) · stacking multiple enchantments on one item from the player enchanting UI (backend validation blocks enchanting already-enchanted gear; mod scripted workarounds exist) · navmesh editing · arbitrary body slot expansion (Altar enums fixed).

Field Notes

1.2 Update Mesh Fix Checklist

  1. Set Collision Presets to Custom on all static meshes (primary crash source: BODY SETUP 0: BAD NAME INDEX).
  2. Match Material Slot Names to vanilla values. Extract vanilla mesh properties JSON from FModel, search "MaterialSlotName". Mismatch = BAD NAME INDEX.
  3. Uncheck left checkbox on all Static Switch Parameters in every MIC, checked switches cause grey world or crash.
  4. For blueprint/form crashes after 1.2: extract fresh vanilla BP_ forms from 1.2 PAKs and rebuild. Gauntlets and greaves are most commonly affected; boots often fine; helmets sometimes affected.
  5. Use the 1.2 Updater Tool (mods/5281) to automate collision and slot name fixing.
The "Unversioned Properties" CrashHalf of unexplained 1.2 crashes are caused by unversioned properties, a UE serialization mode where property names are stripped for performance. The game ships with specific unversioned property tables. If a mod is packaged using Shipping configuration instead of Development, UE strips these names and the game can’t resolve them at runtime, causing crashes that look random. Fix: always cook with Development configuration. Also, mods that dummy parent classes in-editor may not resolve correctly when unversioned serialization shifts offsets in an update.
Cooking vs Packaging (1.2+ Preferred Workflow)Instead of using UE’s Package Project, try: cook in UE Editor, then find the cooked files in Saved\Cooked\Windows\OblivionRemastered\, copy only the assets you changed into a matching folder structure, and repack with Retoc. This gives you a single pak file containing both art and forms, is faster for iterative testing, and avoids the overhead of cooking everything.

Material Rules That Save Hours

  • Never change a static switch, even to the same value. Causes grey world-grid rendering.
  • Don’t pak the base material. Make a material instance of the base and pak only the instance.
  • All non-static parameters are fair game to override.
  • FPSClippingFix is the correct parent for any first-person-visible gear (weapons, gloves, cuirass sleeves).

Blender Socket Naming, The Double-Prefix Rule

When Blender exports a bone named Sockets_Something to UE, UE strips the Sockets_ prefix on import, so Sockets_Arrow becomes Arrow. To get a socket that arrives in UE as Socket_Arrow, name it Socket_Socket_Arrow in Blender. This applies to all weapon attachment sockets, quiver arrow sockets, and equipment slots. Credit: deathwrench.

pakchunk0, Safe to Delete

When packaging a Blueprint mod, UE often generates an unwanted pakchunk0.pak alongside your actual mod pak. This contains anything present in the UE project but not explicitly assigned to a chunk: the default map, base materials meant for instancing rather than overwriting, engine .ini files, etc. It is safe to delete as long as all your actual assets are correctly assigned to a named chunk. Alternatively, configure packaging settings so UE doesn’t generate it. Credit: cnnrduncan.

Gendered Mesh Array Slots (0-7)

Armor blueprints that support different meshes per race/gender use an array of 8 skeletal mesh slots. The index mapping confirmed by .yeah.nah.yeah.:

  • 0 = Human/Elf Male    1 = Human/Elf Female
  • 2 = Argonian Male     3 = Argonian Female
  • 4 = Khajiit Male       5 = Khajiit Female
  • 6 = Orc Male           7 = Orc Female

Even numbers = male, odd numbers = female. The vanilla function that automatically selects the correct mesh by race was broken in patch 1.2, see the Cloth callout in Lesson 9. Using the same mesh for male/female (or just index 0) is the practical workaround until a fix lands.

Custom Interior Map Loading (de_tylmarande, Aug 2025)

Standard interior cell creation via MagicLoader works well, but has a quirk: custom .umap files need to be validated by the game’s asset check stage or the game crashes before the loading screen finishes. Two approaches:

  1. VerticalSlice folder: Place your .umap at OblivionRemastered/Content/Maps/VerticalSlice/L_YourCellName.umap. The game searches this path before checking ST_CellsToMapPath, so it loads automatically. Caveat: all plugin-created cell locations also land here at runtime.
  2. Asset validation patch (C++ UE4SS DLL): Patch the asset validation subroutine so custom maps pass through as if they were shipped. Described by de_tylmarande; once applied, any custom umap loads like a native one regardless of location. The patch is available alongside de_tylmarande’s map cloning tool (check #resources channel).

MagicLoader v2’s CellsToMapPath DataTable link remains required for the Gamebryo cell → UE level mapping regardless of which approach you use.

Exterior World Partition Architecture

The exterior world uses a combination of systems (researched by .yeah.nah.yeah. and gourmet_games_dev_84729, Dec 2025). VLevelStreaming handles level streaming for sub-levels. VWorldPartition and related classes handle the exterior world. The world partition streaming policy is a custom Virtuous class (VWorldStreamingPolicy) that is stubbed out in the Altar SDK and requires building from source to fix. For reference: extract L_Tamriel’s JSON from FModel, it contains a complete picture of the exterior streaming setup. This is why new exterior cells/worldspaces remain blocked: the custom streaming policy can’t be replicated without source access.

Debug Console Quick Reference

ToggleDebugCamera         ; free camera (replaces old tfc)
SloMo 0                   ; freeze time completely
Altar.StandOutOblivionAsset 1          ; turn legacy assets pink (dev tool)
Altar.Cheat.AllowSetStage 1           ; enable SetStage in console
player.moveto 00000014    ; teleport to player start
ATM.SetWeatherSetup clear ; set clear weather
set timescale to 1        ; restore normal time
tgm                       ; toggle god mode
tai                       ; toggle AI
tdetect                   ; toggle enemy detection

Reducing Material Instance Count (UV Atlas / SimpleBake)

Having many separate material instances in one mod increases pak size and draw calls. To consolidate, bake all textures into a single UV atlas in Blender:

  • Manual method: In Blender UV Editor, put all UV islands on the same 0-1 UV space at reduced scale (e.g. 0.5 scale = 4 islands on one sheet). Background the target texture and position islands manually.
  • Automated method: Use the SimpleBake Blender add-on to automate multi-material baking onto a single atlas. A community-shared compiled build exists, check the Discord resources channel.

After baking: one texture sheet, one material instance. Credit: low.je, deathwrench.

Rebuilding Widget Blueprints (WBP), dicene’s WBP Reader

Widget blueprints can’t be imported from FModel directly, they must be rebuilt in UE. To make this tractable, dicene wrote a Python tool that parses the WBP’s JSON export and outputs a readable summary of all functions, properties, and nodes:

github.com/dicene/UETools, WBP_Reader-NoGui.py

Workflow: export WBP from FModel as JSON → run through WBP_Reader → use the output to systematically recreate the graph in UE. You can also feed the raw JSON to an AI assistant to walk through node-by-node reconstruction. You only need to recreate functions and properties that your mod actually interacts with, not the entire WBP. Names must match exactly; the game resolves them at runtime. Credit: dicene.

GhostEffect Shader on New NPCs

The NDAbGhostNPC ability spell applies a ghost/translucency effect shader. It works correctly on vanilla NPCs and items. On new SyncMap-created NPCs and creatures, applying it makes them fully invisible while alive and only correctly ghostly after death. Workaround: use a vanilla Effect Shader from an existing NPC record rather than the GhostNPC ability directly. Some creature types (Wraiths, which are already semi-translucent) partially work. Root cause is likely an extra implicit SyncMap dependency for effect shaders on new actors. Credit: urikslargda.devakm.

Deluxe Edition Content is UE-Side, Not in CS

The Deluxe Edition items and content are handled entirely on the UE side. They do not appear in the Construction Set under their expected names/IDs from the Steam store page. They are present in xEdit under different IDs. If you can’t find a DLC item in CS, check xEdit with the DLC’s ESP loaded. Credit: yofigi, masterlordflame.

Development-Only Nodes in Blueprint Mods

Some UE Blueprint nodes are marked “Development Only”, they execute in editor but are silently compiled out in packaged/shipped builds. If a branch of your BP logic never fires in-game but works fine in the editor, check all nodes for a “Development Only” badge. This is a common gotcha when adding debug prints or diagnostic logic. Credit: deathwrench.

Engine.ini Blueprint Variable Overrides

Set Blueprint variables without Lua by editing: C:\Users\YOU\Documents\My Games\Oblivion Remastered\Saved\Config\Windows\Engine.ini

[/Game/PathToBlueprint/BP_Name.BP_Name_C]
MyVariable=Value
MyStruct=(Prop1=Val1,Prop2=Val2)

Never include Content in the path. Use /Game/ or /PluginName/. [/Script/ is reserved for C++ classes.

Glossary

AltarProprietary translation layer (by Virtuous) bridging Gamebryo FormIDs to UE5 assets. Code is proprietary; extremely unlikely to be released.
Altar ESPsThe remaster’s own plugins (AltarESPMain, AltarDeluxe, AltarESPLocal). Convert all item names to LOC_FN localization keys. Never disable.
AVPairedPawnCentral UE class for player and NPCs. Most character data hangs off its components (WeaponsPairingComponent, OblivionActorStatePairingComponent, PhenotypeData, etc.).
BDPBody-part blueprint (BP_BDP_...) defining which body parts are hidden, mesh references, and material assignments for an armor piece.
CCAChaos Cloth Asset, UE5’s cloth system. Custom ones crash OBR because their dataflows cannot be packed into a pak.
ChunkingSplitting a UE project so only specific assets cook into separate paks. Never chunk skeletons. Blueprint paks go in LogicMods, art paks go in ~mods.
CUE4ParseCommunity C# library for reading UE4/5 assets. Used to extract DNA files which aren’t standard .uasset format.
DNA fileMetaHuman facial rig data file that controls how face bones deform. Not a standard .uasset, requires CUE4Parse to extract.
Dynamic FormIDRuntime-assigned FormID for enchanted or scripted items. Not covered by static TSMI SyncMap entries, causes invisible enchanted custom armor.
ESP / ESMBethesda plugin/master files, records and logic (the brain). Same format as 2006 Oblivion.
External SourcesWwise feature for loading custom .wem files at runtime without rebuilding soundbanks. Requires a packaged static mesh in the mod actor to function.
FPSClippingFixMaterial parent for first-person-visible gear (weapons, gloves, cuirass sleeves). Prevents geometry clipping in first-person view.
GamebryoThe original 2006 engine still running the game’s logic underneath UE5. Handles scripts, quests, stats, AI, saves.
GNDGround model, static mesh (SM_ prefix) shown when an item is dropped on the floor. Must have proper collision or it falls through terrain.
IO StoreUE5’s content packaging format. Three files per mod: .pak (loose), .ucas (content), .utoc (index). All three must be present.
j0.devLocal cloud server required by JAA 1.4.0+ for automatic batch asset imports from FModel JSON exports.
LOC_FNLocalization key prefix used by AltarESPMain to rename all vanilla items. Editing names without forwarding this causes the [nl] artifact.
MagicLoaderTool for new interior cells. Modifies the CellsToMapPath DataTable linking Gamebryo cells to UE map files.
MagickaMultiplierAn actor value accessible via Lua, confirmed by community research. Useful for custom magic scaling systems.
ModActorThe mandatory-named blueprint (must be named exactly ModActor) that UE4SS BPModLoaderMod auto-discovers and injects on game load.
NaniteUE5 virtualized geometry. Handles LODs automatically. FModel exports only the fallback mesh; use simple-nanite-parser for full detail.
NewWorldModelsUAssetGUI field in BP forms that declares which static mesh (GND) to display when an item is dropped.
NIFClassic Oblivion mesh format, dead for rendering in the remaster. BSA loading is attempted for compatibility but actual visuals come from PAK.
NNRMComposite texture format: Normal(RG) + Roughness(B) + Metallic(A). Import as BC7 non-sRGB. Used by most armor, weapon, and character materials.
Notification bridgeTechnique: OBScript sends a message notification → Lua reads via ConsumeNotification hook → hides it → executes UE-side logic. The key to “impossible” mods.
PAKUnreal content package (.utoc + .ucas + .pak). All three files must be present together for a mod to load.
PhysicsProxyLow-poly mesh duplicate used to calculate cloth physics; movements are then interpolated to the actual mesh. Should have no material assigned.
SkeletalVariantsProperty in generic armor BPs that forces CCA files to be mandatory and prevents cloth paint from working. Absent from hair/amulet BPs, why those work.
speech2faceUE procedural function handling mouth sync for voice lines. Runs independently from audio, audio and face animation are not synchronized frame-by-frame.
SyncMapTesSyncMapInjector’s .ini mapping file linking ESP form IDs to UE5 asset paths. Injected at game startup by a Lua mod.
TABPTemplate Animation Blueprint, the main ABP using linked layer instances for locomotion, combat, and idle animation categories.
ubulkUE5 extension for large textures split from the main .uasset. Fix blurry textures by enabling “Never Stream” on the texture in UE Editor.
uint32 limitationFormID values in UE are stored as uint32 (unsigned 32-bit int). Not natively accessible from UE Blueprints, requires C++ or UE4SS Lua.
VAudioHandlersIn-game Wwise audio subsystem. Accessible from Blueprints via BPF_PostEvent to post vanilla Wwise events without full Wwise integration.
_P suffixRequired on pak names placed in ~mods so UE loads them. All three files (.pak, .ucas, .utoc) must carry the _P suffix.
Root LockUE import option that zeroes out root bone offset on an animation. Required for all OBR animations since root motion is fully disabled in the game.
Linked LayerA sub-ABP instance within the TABP that handles a specific animation category (combat, locomotion, idle). Animation replacers must target the correct layer.
ESLLight Plugin format, NOT available in Oblivion Remastered. The 255 plugin limit from classic Oblivion applies (one-byte mod index, FF reserved for the save). The ~512 number is a separate file-handle cap covering plugins + BSAs.
Bendu OloThe default “base NPC” the player references in the CS. Do not delete or modify this record, it is used internally by the game as a reference actor.
Nine Divines ArmorThe vanilla quest most referenced as a scripting example for equipment-triggered scripts (OnEquip/OnUnequip with conditions). Study it in xEdit as a reference pattern.
WBPPairedPawnDebugInfo_CBuilt-in AI debug widget. Use FindFirstOf from Lua to access live NPC AI state data without building a custom debug overlay.
KismetDebuggerUE4SS tool for stepping through custom Blueprint mod logic in real time while the game is running.
TESSync.DynamicFormsA runtime TMap (~246 entries) in Lua that maps dynamic FormIDs to their live TESForm objects. The way to access enchanted or scripted items at runtime.

Reference

Every Tool & Folder

The complete modding workbench: where every file goes, how load order works, and every tool with its purpose.

The File Buckets

Every file you install or create belongs to exactly one destination. Learn to answer “which bucket?” by hand even when a mod manager handles it automatically.

BucketFile TypesLocationLayer
Data.esp .esm...\Content\Dev\ObvData\Data\brain
SyncMap.ini (TSMI mappings)...\ObvData\Data\SyncMap\bridge
Paks ~mods.pak + .ucas + .utoc...\Content\Paks\~mods\visual
LogicModsBlueprint paks...\Content\Paks\LogicMods\<ModName>\visual
UE4SS Mods (Lua)main.lua in Scripts\ subfolder...\Binaries\Win64\ue4ss\Mods\<ModName>\visual
UE4SS BPModLoaderModBlueprint paks (alternative path)...\Binaries\Win64\ue4ss\Mods\LogicMods\visual
OBSE / ASI Plugins.dll / .asi...\Binaries\Win64\extension
GameSettings.ini GMST overrides...\Binaries\Win64\GameSettings\brain

PAK Naming & Load Rules

  • _P suffix in ~mods: all three files must end in _P (e.g. MyMod_P.pak, MyMod_P.ucas, MyMod_P.utoc).
  • Alphabetical priority in Paks folder: placed directly in Paks, a mod sorts alphabetically and must sort below Oblivion.pak to win conflicts, use a prefix like 000_MyMod.pak.
  • Blueprint/logic mods go in the LogicMods subfolder.

ESP Load Order, Canonical Template

Oblivion.esm
DLCBattlehornCastle.esp · DLCFrostcrag.esp · DLCHorseArmor.esp
DLCMehrunesRazor.esp · DLCOrrery.esp · DLCShiveringIsles.esp
DLCSpellTomes.esp · DLCThievesDen.esp · DLCVileLair.esp
Knights.esp
AltarESPMain.esp
  ← YOUR NEW-OBJECT / NEW-CELL PLUGINS GO HERE
AltarDeluxe.esp
AltarESPLocal.esp
The New-Object Load Order RulePlugins that add new objects or new interior cells must sit above AltarDeluxe.esp. Below it, new objects render invisible and entering a new interior crashes, a TesSyncMapInjector ordering requirement.

Key Content Paths in the Game Files

/Game/Art/Character/{Race}/          ← Race textures and meshes
/Game/Art/Equipment/armor/{type}/    ← Armor skeletal meshes
/Game/Art/Equipment/weapons/         ← Weapon static meshes
/Game/Forms/items/armor/             ← Armor form blueprints (BP_BDP_*)
/Game/Forms/items/weapons/           ← Weapon form blueprints
/Game/Forms/actors/npc/              ← NPC blueprints
/Game/Forms/actors/race/             ← Race blueprints
/Game/Dev/Phenotypes/Hair/           ← Hair phenotype data tables
/Game/Materials/                     ← Master materials
/Game/Art/Animation/Humanoid/        ← Character animations
/Game/Art/Animation/Humanoid/Facial  ← Per-voice-line face animations
/Game/Art/UI/Icons/Dynamic_Icons/    ← Inventory icons

Loaders & Bridge Tools

ToolRoleNotes
OBSE64DLL plugin loader (not script extender yet)Steam only; match game build; dll+exe to Binaries\Win64
Address Library (mods/4475)Version-independent OBSE pluginsRequired by almost all OBSE plugins
UE4SS (mods/32, OR build v3.0.1)Lua scripting + Blueprint loader for UE5 sideLaunch once without OBSE to configure; use OR build not GitHub generic
TesSyncMapInjector (mods/1272)Links ESP FormIDs to UE5 assets at runtimeINIs in ObvData\Data\SyncMap; unzip to game root
Smart MapperxEdit script that auto-generates TSMI INI filesOutput lands in xEdit\SyncMap; copy to game Data\SyncMap
MagicLoader (mods/1966)Enables new interior cells via CellsToMapPath DataTableAlso requires MagicPatcher for string table entries
OBRConsole (mods/2205)Lets UE4SS Lua run Oblivion console commands⚠ Crashes if you tab out of the game while active
SML / Simple BP Mod Loader (mods/1172)Alternative Blueprint loader; better input action docsAlso provides SML Dev Resources for console output, UObject storage

ESP / Data Authoring

ToolRoleNotes
xEdit 4.1.5n+ (TES4R build)Primary ESP authoring and conflict detectionGet from xEdit Discord #xedit-builds; launch with -TES4R flag
Construction Set (2006) + CSEVisual editor for cells, containers, world objectsAltar ESPs need manual dependency loading via xEdit first to open in CS; saved esp in ObvData\Data\Data (move up one level)
LOOTAutomatic load-order sortingSort, then hand-fix the special cases above
NL-Tag Remover (mods/473)Strips [nl] artifact from new item namesRequired for any mod that adds new named items
Haphestia’s Fix and Port Script (fixmod; vibrantruin on Nexus)Fixes the 2 missing parameters CS doesn’t add for RemasterRun in xEdit with Altar files loaded
Game Settings Loader (mods/833)Load GMST overrides from config fileAlternative to setGS console command each session
Runtime EditorIDs (mods/1331)Surfaces EditorIDs in the in-game consoleQoL for testing; lets you spawn items by EditorID

UE5 Asset Pipeline

ToolRoleNotes
Blender + PSK/PSA pluginModel and animation authoringRename armature to “Armature”; if PSK breaks try Befzz plugin
Unreal Engine 5.3.2Cook art assets into paksUse 5.3.2 specifically. Install to C:/ not Program Files.
FModel + .usmap (mods/47)Browse and extract game assetsSet to GAME_UE5_3; regenerate .usmap after patches via UE4SS Ctrl+Numpad6
OR Mod Tools (mods/3918)retoc + UAssetGUI + oo2core bundleUse Nexus UAssetGUI version (PackageName bug fix)
retoc (github.com/trumank/retoc)Extract and repack IO Store paksto-legacy to extract; to-zen to repack; update for ubulk fix
UAssetGUI (Nexus version)Edit cooked .uasset/.uexp blueprint and form filesSet version to 5.3; needs both .uasset and .uexp in same folder
JsonAsAsset + j0.devImport materials from FModel JSON into UE projectUse C0bra5+Tectors fork; enable Stubs checkbox; MICs need manual parenting
C.A.F.E. (mods/4891)CosmicBoogaloo’s armor/weapon form path toolForm name must match asset path name exactly
NNRM Merge/Split Tool (mods/3051)Split and recombine NNRM channel-packed texturesAlso use c0bra5’s ffmpeg scripts for channel-precise splitting
OR SDK (Kein/Altar)UE project stub with Oblivion superclassesCompile with VS2022 MSVC v143 v14.38.33130; no engine-from-source needed
simple-nanite-parser (c0bra5)Extract full-detail Nanite meshesFModel only exports fallback mesh; this gets the real thing. Python, exports to GLTF.
Alpakit for UE 5.3.2One-click auto-deploy of Blueprint modsOBR-compatible fork by Wikt0r1us
sound2wemConvert audio files to Wwise .wem formatRequired for any audio replacement workflow
wwiserBrowse Wwise .bnk soundbanks to find .wem IDsFind CAkSound node SOURCE IDs for specific voice lines
Visual Studio 2022 (direct link)Compile Altar SDK project and OBSE64 C++ pluginsGame Dev C++ workload + MSVC v143 v14.38.33130 component
1.2 Updater Tool (mods/5281)Automates collision + material slot name fixing for 1.2Run on any mesh mod made before patch 1.2
Body Part Chart (mods/2583)nyyxn’s authoritative body-part bitmask referenceRequired reference for MaleBodySectionHidden values
Community DNA Files (mods/2592)MetaHuman DNA files for all base races and named NPCsRequired for custom head facial animation

Community

Credits & Contributors

This wiki exists because of hundreds of people who spent countless hours reverse-engineering a game with no official documentation, sharing everything they found for free, and building tools for everyone to use. This page is for them.

A Note to Virtuos & Bethesda “No official modding support?” That’s fine, we didn’t need it. In less than a year, the community reverse-engineered your dual-engine architecture, built a complete toolchain from nothing, figured out the Altar translation layer, and shipped thousands of mods. This wiki documents more about the technical internals of Oblivion Remastered than any official documentation ever would have. The community would still appreciate a Construction Kit though. Just saying., The OBR Modding Community

The Community Hub

Every discovery in this wiki came from the OBR Modding Discord. If you’re making mods, you need to be here. The #research-general, #research-modeling, #research-scripting-ue4ss, #research-animation, and #research-mapping channels are where the real work happens.

Research Contributors

These are the people who actually figured this stuff out. Every name in these lists did something real, wrote a tool, reversed a system, tested a workflow, documented a finding, or shared a crucial insight that saved everyone else hours of work.

Modeling, Textures & Asset Pipeline

c0bra5Nanite parser (simple-nanite-parser), material reverse-engineering (c0bra5 method), NNRM ffmpeg scripts, Nanite mesh extraction workflow. One of the most prolific technical contributors. github.com/C0bra5
deathwrenchPractical armor pipeline workflow, FBX export settings, collision setup, standalone item creation, confirmed Blender export settings. The workflow most people use traces back here.
tenebrisequitemCloth physics deep-dive research, established WHY custom CCA files crash, the dataflow limitation, and the SkeletalVariants culprit.
jack_laMorph targets, vertex colors, material research. Documented the PSK vs UEFormat tradeoffs and vertex color sRGB/linear issues.
qunaiBitmask research, BP form structure, TSMI enchanted item bug documentation. Body part hiding system mapping.
dotaxisPackaging workflow, deferred hook system for Lua mods, general toolchain contributions.
.astralusTexture pipeline research, engine config documentation.
izedev_55749MetaHuman DNA research, discovered the game uses the MetaHuman system for facial animation. Confirmed Audio2Face workflow.
MikhailTeslovNordic Muscles mod (mods/214), authored the pioneering tutorial for character mesh editing in OBR, establishing the linear vertex color rule that the entire mesh modding scene built on. mods/214
exicideBeast race armor compatibility research.
WSDogExtracted and publicly shared DNA files for all base races and named NPCs. mods/2592
_trungusEye color editing research.
nyyxnBody part vertex color chart, the authoritative reference for MaleBodySectionHidden bitmask values. mods/2583

Animation, Audio & ABP System

krasuepisacLocomotion blendspace TMap replacement discovery, the persistent animation replacement technique. StrideWarping documentation.
kei7855Animation notify research for combo chaining. Shortsword animation replacer (reference implementation). mods/2489
ryanhankAnimation research contributor.
michaelpstanichAnimation research contributor.
lunemodsAnimation research contributor.
strikshawAnimation research contributor.
miken1keCommunity standard Blender rig for OBR animation work. mods/2069
jakealaimoLive motion capture modded into Oblivion Remastered, reference implementation for advanced animation. YouTube
.yeah.nah.yeah.Wwise External Sources bug discovery (the static mesh workaround). Map border open space research. Animation & mapping contributor.

World Editing & Mapping

nafnaf_95New interior cells research, exterior cell limitations, CK-UE terrain height mismatch workaround, Black Marsh expansion proof-of-concept, navmesh limitations documentation.
khameli0nFixed the MagicLoader bug that overwrote FULL name fields. Critical tool fix.
Selene310187Player Home Problem Solving Guide and Master Tools/Tutorials List, two of the most important community reference documents. Forum guide
HaphestiaCreated MagicLoader, the tool that makes new interior cells possible. mods/1966 Also made the Fix and Port Script (fixmod), as vibrantruin on Nexus. mods/1132
diceneConsumeNotification method discovery (notification bridge foundation). Custom map loading research. ConsoleUtils C++ UE4SS mod. IDA-Scripts for reverse engineering. Wwise audio deep-dive research (AK function exposure, audio routing findings, custom audio workarounds). GitHub
de_tylmarandeCustom interior map loader patch, asset validation fix enabling custom umaps to load like shipped ones. Map cloning tool and ORSS (Oblivion Remastered Scripting Support) framework with global-reading and custom cell registration. C++ DLL modding research.
.yeah.nah.yeah.Wwise deep audio research (GUID problem, SID matching, UAkGameplayStatics API limits, audio routing, BPML_GenericFunctions usage). Exterior world partition architecture research (VWorldPartition, VLevelStreaming, L_Tamriel). Blender socket double-prefix rule. Gendered mesh slot array (0-7). Precompiled Altar project shared to community. Cloth physics system architecture (BPI_ClothPhysicsControl, BPC_ClothScalability). Forms-authoring plugin for UE editor. Forms authoring plugin video
tommnMCM ↔ Lua console command bridge pattern. Flame atronach transformation workaround (spawning actor attached to player). JoJo stand-style attached actor approach. mods/2321
cnnrduncan.umap layer structure documentation. Confirmed the layer-per-category architecture.
minutereadyGrass/tree placement system research, Mad’s rapid spray placement method, Blueprint/Lua spawn approach for vegetation.
narm_Mapping research contributor.
wxmichaelConfirmed .umap direct editing via UAssetGUI for object placement.
grimlock_artsHeightmap research contributor. Documented the “TES heightmap is just a flat base layer” finding.
agentlefoxHeightmap Extractor tool research.

Scripting, Lua & Cross-System

MadAborModdingCreated the Levitation mod, the reference implementation of the full OBScript→Lua notification bridge. The breakthrough that unlocked “impossible” mods. mods/3334
PuddlePumpkinKwaNotifications mod (Lua↔Blueprint communication pattern), ObvrWidgetLibrary, open-source BP mod examples. GitHub
zarrastroUE4SS scripting research, hook catalog contributor.
randombombombomUE4SS scripting research, hook catalog contributor.
veter_UE4SS scripting research, hook catalog contributor.
ubawesomeUE4SS scripting research, hook catalog contributor.
faeriemushroomUE4SS scripting research, hook catalog contributor.
crimsonSpellCastType snippet, UE4SS scripting research, hook catalog contributor.

Tools & Frameworks

KeinMaintained the Altar stub UE project (Kein/Altar fork), the single most important tool for Blueprint modding. Without this, everyone would need a 200GB UE source build. GitHub
nathtestUProjOblivionRemastered, Altar source headers reference. GitHub
alexander_preitCommunity armor/clothing toolkit, JAA quick setup guide. Made getting started dramatically easier.
Tripster
= .yeah.nah.yeah.
Pre-compiled Altar project and Materials Pack, removed the VS build requirement for most Blueprint modders. (“Tripster” is the same person as “.yeah.nah.yeah.”, credited above for Wwise audio, world-partition, and cloth physics research.)
wikt0r1usAlpakit for UE 5.3.2, OBR-compatible fork of the packaging tool. GitHub
tommnBloodlust (kill detection + OBScript), Custom Summoning mod (summon via hijacked NPC forms), MCM↔Lua bridge pattern, transformation via attached actor. mods/2321
yerawizardharrehShield on Back, open-source Blueprint uassets example. mods/2077

The Compiler & Curator

CosmicBoogaloo, DreamEater

The author of this wiki. I didn’t discover most of what’s on these pages, this community did. My job was to read every Discord export, every research channel, every tool readme, and every pinned message, then organize it into something a new modder can actually use without drowning in 9,655 messages of raw Discord history.

I also made C.A.F.E. (the armor/weapon form path tool), published 60+ mods with 15,000+ downloads, got covered by PC Gamer, GameSpot, and The Gamer, and received a personal compliment from Markus Persson. I’m building a full indie game engine and studio under the DreamEater name. Oblivion Remastered was the spark that started all of it.

A Note on Missing Credits This wiki was compiled from Discord exports covering hundreds of conversations. Some contributors may be missing from this page, or have outdated/missing Nexus links. If you contributed to any research documented here and want to be properly credited (or have a link added, corrected, or removed), please reach out. Every contribution matters.

Get In Touch

Found an error? Have new research to add? Want to be credited or update your info? Use the form below.

Contact CosmicBoogaloo

Thank You To every person who ever typed something into a Discord channel at midnight because they figured something out and wanted to share it: this is for you. The OBR modding community built something remarkable in under a year with no official support, no source code, and no documentation. That’s genuinely impressive. Keep going.
OBR Mod Creation Wiki, compiled by CosmicBoogaloo / DreamEater Living document · mid-2026 · always verify tool versions

Engine Internals

The Bridge & DataTables

How Oblivion Remastered fuses Gamebryo logic with a UE5 visual layer, and the Altar DataTables (SyncMap, CellsToMapPath) that TSMI and MagicLoader patch at runtime. The map of every content directory worth knowing.

The Three Layers

Every mod decision maps to one of three layers. The CK must be aware of all three simultaneously.

LayerRole
Gamebryo brainGame logic. ESP/ESM records, OBScript, AI packages, quests, items, dialogue text, FormIDs. Same format as 2006. Location: Content/Dev/ObvData/Data/
The Bridge bridgeAltar's translation layer. DataTables (CellsToMapPath, SyncMap, AllRaceModifications), FormID→UE asset mappings, VPairedPawn architecture, TESTopicInfo assets. This is what TSMI and MagicLoader patch at runtime.
Unreal Engine 5 visualRendering, audio, animation, Blueprint logic. PAK files, uassets, Wwise banks, AnimSequences, StateMachines. Location: Content/ tree.

Why TSMI and MagicLoader Exist (And Why They're Fragile)

Both tools are runtime DataTable patchers, they inject entries into Altar's bridge DataTables after the game loads. They exist because nobody has write access to those DataTables at cook time.

  • TSMI patches the SyncMap DataTable: FormID → UE Blueprint/mesh asset path. When Gamebryo wants to render a form, it looks up this table to find the UE asset. TSMI injects new entries for mod-added forms.
  • MagicLoader patches the CellsToMapPath DataTable: Gamebryo cell EditorID → UE .umap path. When the player enters a cell, Gamebryo tells UE which map to load via this table. MagicLoader injects new interior cell entries.
The CK Replaces This The CosmicBridge CK will generate correctly structured DataTable patch entries as part of the build pipeline, eliminating the need to run TSMI and MagicLoader separately. The runtime CosmicBridge mod will handle the injection using the same UE4SS hook mechanism both tools use, but unified and aware of all mod types simultaneously.

The FormID Bridge

Every Gamebryo form that has a visual representation owns a corresponding UE asset. The connection is the FormID. A form with ID 0x000479E1 maps to a UE Blueprint at a known path like /Game/Forms/actors/npc/SomeNPC.SomeNPC. The SyncMap DataTable holds these mappings. New mods add new FormIDs and need new SyncMap entries, that's TSMI's job.

Key Bridge DataTables (Confirmed)

DataTableKeyValueTool that patches it
SyncMapFormID stringUE Blueprint asset pathTSMI
CellsToMapPathCell EditorIDUE .umap pathMagicLoader
AllRaceModificationsRace asset pathBody/head mesh paths + phenotypeManual / CK target

SyncMap & DataTable Architecture bridge

SyncMapMain. The Master FormID→Blueprint Registry

SyncMapMain at /Game/Forms/SyncMapMain is type TESSync, the primary DataTable that maps every Gamebryo FormID to its UE Blueprint. When a mod adds a new item, NPC, or weapon, it needs an entry here.

Key Finding: Two SyncMap Files From PlaygoChunk_SyncMapMain: there are TWO SyncMap assets always cooked together: /Game/Forms/SyncMapMain.SyncMapMain and /Game/Forms/SyncMapLocal.SyncMapLocal. TSMI patches SyncMapMain. SyncMapLocal is likely for DLC/local additions. CosmicBridge should patch both.

VAltarBlueprintTable. The Alternative DataTable

DT_EquippedObjectsBlueprints and DT_UIBlueprints are VAltarBlueprintTable assets, a different DataTable type specifically for equipped objects and UI elements. Structure:

VAltarBlueprintTable {
 BlueprintsMap: [ {Key: "FormID_string", Value: {AssetPathName}} ]
 Rows: {
 "RowName": {
 FormID: int,
 Blueprint: {AssetPathName: "..."}
 }
 }
}

Both BlueprintsMap (the indexed fast-lookup) and Rows (the DataTable rows) must be populated for new entries to work. The row name can be anything (e.g. "NewRow_0", "NewRow").

DataTable Injection. CosmicBridge Implementation

CosmicBridge replaces TSMI and MagicLoader by reading config files that mods deposit and injecting into these DataTables at runtime:

-- CosmicBridge startup sequence
local function InjectSyncMap(configPath)
 -- Read mod's syncmap.json
 -- Find SyncMapMain DataTable in memory
 -- For each entry: inject row with FormID → Blueprint path
end

local function InjectCells(configPath) 
 -- Read mod's cells.json
 -- Find CellsToMapPath DataTable
 -- For each cell: inject row with EditorID → umap path
end

local function InjectEquippedObjects(configPath)
 -- Find DT_EquippedObjectsBlueprints
 -- Inject entries for weapons/items that need equipped Blueprint
end

Config File Formats (CosmicBridge Standard)

-- syncmap.json (replaces TSMI .ini files)
{
 "entries": [
 {
 "formId": 12345,
 "blueprint": "/Game/Forms/items/weapons/BP_MyWeapon.BP_MyWeapon",
 "blueprintClass": "/Game/Forms/items/weapons/BP_MyWeapon.BP_MyWeapon_C"
 }
 ]
}

-- cells.json (replaces MagicLoader)
{
 "cells": [
 {
 "editorId": "MyCustomCell",
 "mapPath": "/Game/Maps/MyMod/L_MyCell.L_MyCell"
 }
 ]
}

-- equipped.json (replaces DT_EquippedObjectsBlueprints manual editing)
{
 "entries": [
 {
 "formId": 12345,
 "blueprint": "/Game/Forms/items/weapons/BP_MyWeapon.BP_MyWeapon"
 }
 ]
}

Placeholder Blueprints. The TSMI Workaround

From the weapons folder: BP_WeaponBladeWithScabbardPlaceholder, BP_WeaponMeleePlaceholder, BP_WeaponBowPlaceholder, BP_WeaponQuiverPlaceholder, BP_WeaponShieldPlaceholder, BP_WeaponStaffPlaceholder. These are the empty Blueprint stubs that TSMI maps custom weapons to when no custom Blueprint exists, the weapon gets the correct weapon type behavior without a custom 3D mesh.

FModel Asset Map visual

Reference for every significant directory in the OBR content tree. Updated from live FModel session 2026-06-12.

Content/Dev/. Altar Development Assets

PathContentsMod relevance
Dev/AI/DetectionLighting (BlueprintLightProfile, SkylightIntensity), NavlinksAI behavior, reference for custom NPC AI
Dev/Animation/All ABP_, TABP_, blendspaces, interfacesAnimation replacers, custom ABPs
Dev/InteractibleObjects/All BP_VChest, BP_VDoor, BP_Flora variantsCell object placement
Dev/LevelSelectDoors/BP_LevelEntryDoorObv and variantsCell transition wiring
Dev/NPCs/BP_Generic_NPC, BP_NPC_SOUL, BPI_NPC_CustomFadeOutNPC templates
Dev/ObvData/Data/ESPs, ESMs, Plugins.txt, SyncMap/Gamebryo data, primary mod target
Dev/PairingActors/BP_ReferenceHolder, BPC_BoundEffect, BPC_StatusEffectActor pairing system
Dev/Phenotypes/PhenotypePreset_BASE, MorphSources, SkinParameterCollectionsCharacter appearance system
Dev/StateMachine/ASM_, AST_, PSM_, COND_ assets (~100+ files)All hookable state events
Dev/weapons/Weapon Blueprints and physics assetsCustom weapon authoring
Dev/Creatures/Creature Blueprints (Panther method templates)Custom creature base classes

Content/Forms/. Bridge Form Assets

PathContentsNotes
Forms/actors/creature/1,297 creature form assetsTSMI targets for creature FormIDs
Forms/actors/leveledcreature/705 leveled creature forms
Forms/actors/npc/5,921 NPC form assetsTSMI targets for NPC FormIDs
Forms/actors/race/15 race form assetsAllRaceModifications DataTable targets
Forms/miscellaneous/dialog/TESTopicInfo assets, all dialogue responsesTSMI dialogue mapping targets; template for custom dialogue
Forms/items/All item form assets by typeTSMI targets for item FormIDs

Content/WwiseAudio/. Audio Assets

PathContentsNotes
WwiseAudio/Events/Voice/oblivion/[race]/[m|f]/All voice line AkAudioEvent assetsTemplate for custom voice events; naming convention here
WwiseAudio/Event/English(US)/1,331 .bnk soundbank filesOne per voice event, loose files in Audio pak
WwiseAudio/Media/English(US)/94,453 .wem media filesNamed by MediaId (numeric hash), loose files in Audio pak
WwiseAudio/Character/Foley, Footstep, Vocal banksCharacter sound effects
WwiseAudio/Bus/Bus hierarchyRoute custom audio to correct bus

Content/Maps/. Level Files

PathContentsNotes
Maps/World/All exterior world umaps (6 variants per area)Naming: L_[AreaName]World[_Del/_Env/_Li/_SD/_VFX].umap
Maps/VerticalSlice/Interior cell umapsMagicLoader targets; template for custom interiors

Content/Art/. Visual Assets

PathContentsNotes
Art/Animation/Humanoid/Facial/All facial AnimSequences for lip syncNamed A_[questid]_[topic]_[formid]_[index]; one per voice line per race
Art/Animation/Humanoid/Full body animationsReplacer targets

Engine Internals

NPCs, Races, Faces & Hair

The full character pipeline: the TESNPC form, the bridge component stack on every humanoid, the 54-axis face morph system, race forms and FaceGen math, the dual hair asset trees, clothing/armor body-part slots, and the aging/vampire (senescence) system.

NPC Architecture bridge

Component Hierarchy (Confirmed)

Every humanoid NPC Blueprint (e.g. BP_Generic_NPC, BP_NPC_SOUL, specific named NPCs) inherits from VPairedCharacterVPairedPawnACharacter. The standard component set:

Component NameClassRoleAttachment
AudioComponentVAltarAkComponentVoice audio, only AkComponent per NPCCharacterMesh0
HeadComponentVHumanoidHeadComponentHead mesh, hair, facial pose, speech2face (native)CharacterMesh0
SoundPairingComponentVPawnSoundPairingComponentDialogue bank lifecycle managementRoot
CharacterMesh0USkeletalMeshComponentBody meshRoot

VPairedPawn. The Bridge Pawn

All characters (player and NPCs) are VPairedPawn subclasses. Key facts:

  • When mounted, UEHelpers.GetPlayer() returns the horse, not the rider
  • The rider is accessible as BP_OblivionPlayerCharacter_C
  • VPairedPawn:TryLinkConversationIdle, fires when NPC enters conversation idle state (registered but timing unreliable in tests)
  • VPairedCharacter:GetVoiceType and SetVoiceType, readable/writable voice type enum

BPI_NPC_CustomFadeOut Interface

NPCs implement BPI_NPC_CustomFadeOut_C with one function: CustomFadeOut(). This is the interface for triggering NPC fade-out/disappear sequences. Callable from Lua on any NPC that implements it.

UE4SS Access Patterns (Confirmed Safe vs Unsafe)

OperationStatusNotes
FindAllOf("BP_Generic_NPC_C")safeReturns array of NPC actors
actor:IsValid()safeUse before any call
actor:K2_GetActorLocation()safeConfirmed safe in all contexts
actor:GetName()crashesSEHs through pcall, never call on NPC actors
actor:GetFullName()crashesSame. SEH through pcall
actor.AudioComponentinvalid ptrIsValid=false despite property existing
GetComponentByClass(class)invalid ptrReturns object but GetName() SEHs
BPF_HasActiveEvents()safeOn retrieved AkComponent, safe call

NPC Form Deep Dive bridge

Source: 5 confirmed TESNPC dumps Jauffre, ImperialLegionForester, AlessiaCaro, AnvilGuardCastlePatrolDay01, TESTImperial, covering unique named, generic, female, and test NPCs.

TESNPC Asset Structure

Every NPC in Forms/actors/npc/ is a TESNPC UAsset containing four sub-objects:

Sub-objectTypeContains
BaseDataTESActorBaseDataLevel, ActorBaseFlag, faction data
[EditorID]TESNPCRace, sex, name, Blueprint path, hair/eyes, FaceGen data, FormID
PhenotypeVCharacterPhenotypeDataCustom head mesh + hair (unique NPCs only; generic = empty)
EnchantVEnchantSaveDataEnchantment state (usually empty)

Two NPC Tiers. Named vs Generic

There are two fundamentally different NPC types, differing in how they handle appearance:

Unique Named NPCs (Jauffre, AlessiaCaro, Anguilon)

  • bUseDefaultRaceAndSexPreset: false, overrides race default appearance
  • bUseProceduralHead: false, uses a custom sculpted head SkeletalMesh
  • Phenotype sub-object contains FaceBaseMesh pointing to a unique SK_[Name]_Head asset
  • Phenotype contains explicit Hair pointing to a VCharacterHairPiece_Hair asset
  • Example. Jauffre: SK_Jauffre_Head at /Game/Art/Character/Jauffre/, hair HP_Nord_HR_BaldPony

Generic / Procedural NPCs (Guards, Foresters, TESTImperial)

  • bUseDefaultRaceAndSexPreset not set (defaults to true), uses race's default phenotype preset
  • bUseProceduralHead not set (defaults to true), head generated from OblivionFaceGenDataOffset morph data
  • Phenotype sub-object is empty, appearance driven entirely by FaceGen float arrays
  • Hair/Eyes set in main TESNPC object, not Phenotype
  • The OblivionFaceGenDataOffset contains three float arrays: SymmetricalGeometryData (50 values), AsymmetricalGeometryData (30 values), TextureData (50 values)
CK Implication: Two Custom NPC Workflows For a quick generic NPC: set race, provide FaceGen float arrays (or zero them for race default), leave Phenotype empty. For a unique named character: sculpt a head in Blender, export as SK_[Name]_Head, create a VCharacterHairPiece, set bUseDefaultRaceAndSexPreset=false and bUseProceduralHead=false, populate Phenotype.

TESNPC Key Fields Reference

FieldTypeExample / Notes
InheritedRaceTESRace refTESRace'Imperial'/Game/Forms/actors/race/Imperial.0
SexECharacterSex enumOmit for Male (default), set ECharacterSex::FEMALE explicitly
FullNameStringDisplay name, shown in dialogue, crosshair
m_formIDInt (decimal)Decimal FormID, e.g. Jauffre = 145817 = 0x000239D9
m_formEditorIDStringEditorID, must be unique, no spaces
m_formTypeEnumAlways "FormID::NPC__ID" for NPCs
BlueprintAsset soft ref/Game/Forms/actors/npc/BP_[EditorID].BP_[EditorID]
BlueprintClassAsset soft ref/Game/Forms/actors/npc/BP_[EditorID].BP_[EditorID]_C
HairAsset soft ref/Game/Forms/miscellaneous/hair/[HairName].[HairName]
EyesAsset soft ref/Game/Forms/miscellaneous/eyes/[EyeName].[EyeName]
BSXFlagsIntAlways 7. Bethesda flags, don't change
MassFloatAlways 27.21554 for humanoids
ActorBaseFlagInt (bitmask)0 = no flags, 41106 = unique, 1048728 = respawning guard/generic

Complete NPC Blueprint Component List

Confirmed identical across all 4 NPC Blueprints, this is the canonical component set for every humanoid NPC:

Property NameComponent ClassInternal Name
MainSkeletalMeshComponent / MeshSkeletalMeshComponentBudgetedCharacterMesh0
AkAudioComponentVAltarAkComponentAudioComponent
HumanoidHeadComponentVHumanoidHeadComponentHead Component
PawnSoundPairingComponentVPawnSoundPairingComponentPawn Sound Pairing Component
CharacterBodyPairingComponentVCharacterBodyPairingComponentCharacter Body Pairing Component
CharacterAppearancePairingComponentVCharacterAppearancePairingComponentAppearance
AnimationPairingComponentVAnimationPairingComponentAnimation Pairing Component
TransformPairingComponentVTransformPairingComponentTransformPairingComponent
WeaponsPairingComponentVWeaponsPairingComponentWeapons Pairing Component
OblivionActorStatePairingComponentVCharacterStatePairingComponentOblivion Actor State Pairing Component
ActorValuesPairingComponentVActorValuesPairingComponentActorValuesPairingComponent
ActiveEffectsPairingComponentVActiveEffectsPairingComponentActiveEffectsPairingComponent
DockingPairingComponentVDockingPairingComponentDocking Pairing Component
StateMachineComponentVPairedPawnStateMachineComponentState Machine
PairedPawnMovementComponent / CharacterMovementVPairedPawnMovementComponentCharMoveComp
MergedMeshComponentVMergedSkeletalMeshComponentMerged Mesh Component
PhysicsControllerComponentVPhysicsControllerComponentPhysicsControllerComponent
PhysicalAnimationComponentVPhysicalAnimationComponentPhysicalAnimationComponent
HumanoidMotionWarpingComponentMotionWarpingComponentMotion Warping Component
TESRefComponentVTESObjectRefComponentTESRefComponent
CharacterFadeInOutComponentVCharacterFadeInOutComponentFade In/Out component
PhenotypeDataVCharacterPhenotypeDataPhenotype
CapsuleComponent / RootComponentCapsuleComponentCollisionCylinder
PhysicsBodyColliderCapsuleComponentPhysicsBodyCollider
WorldLimitDetectionBoxBoxComponentBorder Region Collider
FakeRootSceneComponentFakeRootComp
Key Pairing Components Explained The "Pairing" suffix = bridge component. VTransformPairingComponent syncs Gamebryo position to UE. VAnimationPairingComponent syncs Gamebryo animation state. VActorValuesPairingComponent exposes health/magicka/stamina (Health = actor value index 10). VCharacterStatePairingComponent bridges AI state. These are all read-only from Lua, they receive data from Gamebryo and push it to UE, not the other way.

Phenotype System visual

Face Morph Axes. Complete List

The FaceMorphsSource_Human asset defines 54 bilateral morph axes. Each axis has a negative morph target name and a positive one. The OblivionFaceGenDataOffset float arrays index into these. This is what drives procedural face generation for generic NPCs.

Axis LabelNegative MorphPositive Morph
Brow Ridge Low / HighBrowRidgeLowBrowRidgeHigh
Brow Ridge Inner Down / UpBrowRidgeInnerDBrowRidgeInnerU
Brow Ridge Outer Down / UpBrowRidgeOuterDBrowRidgeOuterU
Cheekbones Shallow / PronouncedCheekbonesShallCheekbonesProno
Cheekbones Thin / WideCheekbonesThinCheekbonesWide
Cheekbones Low / HighCheekbonesLowCheekbonesHigh
Cheeks Concave / ConvexCheeksConcaveCheeksConvex
Cheeks Gaunt / RoundCheeksGauntCheeksRound
Chin Backward / ForwardChinBackwardChinForward
Chin Recessed / PronouncedChinRecessedChinPronounced
Chin Retracted / JuttingChinRetractedChinJutting
Chin Shallow / DeepChinShallowChinDeep
Chin Small / LargeChinSmallChinLarge
Chin Short / TallChinShortChinTall
Chin Thin / WideChinThinChinWide
Eyes Down / UpEyeDownEyeUp
Eyes Small / LargeEyeSmallEyeLarge
Eyes Inward / OutwardEyeTiltInwardEyeTiltOutward
Eyes Apart / TogetherEyeApartEyeTogether
Face Light / HeavyFaceLightFaceHeavy
Face Gaunt / RoundFaceGauntFaceRound
Face Thin / WideFaceThinFaceWide
Forehead Small / LargeForeheadSmallForeheadLarge
Forehead Short / TallForeheadShortForeheadTall
Forehead Back / ForwardForeheadTiltBForeheadTiltF
Jaw Retracted / JuttingJawRetractedJawJutting
Jaw Thin / WideJawThinJawWide
Jaw Neck Slope Low / HighJawNeckSlopeLJawNeckSlopeH
Jaw Concave / ConvexJawlineConcaveJawlineConvex
Mouth Drawn / PursedMouthDrawnMouthPursed
Mouth Sad / HappyMouthSadMouthHappy
Mouth Low / HighMouthLowMouthHigh
Mouth Deflated / InflatedMouthDeflatedMouthInflated
Mouth Small / LargeMouthSmallMouthLarge
Mouth Retracted / PuckeredMouthRetractedMouthPuckered
Mouth Retracted / ProtrudingMouthRetractMouthProtruding
Mouth Tilt Down / UpMouthTiltDownMouthTiltUp
Mouth Underbite / OverbiteMouthUnderbiteMouthOverbite
Nose Bridge Shallow / DeepNoseBridgeShallNoseBridgeDeep
Nose Bridge Short / LongNoseBridgeShortNoseBridgeLong
Nose Down / UpNoseBridgeDownNoseBridgeUp
Nose Flat / PointedNoseFlatNosePointed
Nose Short / LongNoseShortNoseLong
Nose Tilt Down / UpNoseTiltDownNoseTiltUp
VampireVampireVampire
Hair LengthHairLengthShortHairLengthLong

Skin Parameter Slots (Confirmed)

The SkinParameterCollection_Humans asset defines material parameter slots by primitive index. Key ones for custom NPC skin/hair authoring:

ParameterSlotTypeNotes
Complexion0SimpleFace + body
Skin Tone1-3Color (3 slots)Face + body
Eyeliner Tone4-6Color
Eye socket Bruised/Bright7Simple
Eyebrows Color Intensity9Simple
Eyebrows Redness Intensity10Simple
Cheek Blush11Simple
Freckles17Simple
Salt And Pepper (hair)18SimpleHair greying
Root DyeColor21-23ColorDefault #0F0400
Tip DyeColor24-26ColorDefault #0F0602
Lipstick Color27-29ColorDefault #560319
Khajiit Pattern Number30SimpleHas Vampire/Sick modifiers
Root Color31-33SimpleHair root
Tip Color33SimpleHair tip
Eyelids Pale/Red35Simple

Face Material Slots

Slot NameIndex
Eye0
FaceSkin1

Race Form Architecture bridge

Source: Imperial.json, Nord.json, confirmed TESRace structure

TESRace Asset Structure

Race assets live at /Game/Forms/actors/race/[RaceName].[RaceName]. Each contains one TESRace object.

TESRace Key Fields

FieldImperial exampleNord exampleNotes
m_formID2311 (0x00000907)140541 (0x000224FD)Gamebryo FormID
m_formType"FormID::RACE_ID"Always this for races
Data.Height [M, F][1.0, 1.0][1.06, 1.06]Scale relative to Imperial baseline
MaleFullBodies[0].FullBodySkeletalMeshSK_Imperial_Body_mSK_Nord_Body_m/Game/Art/Character/[Race]/
FemaleFullBodies[0].FullBodySkeletalMeshSK_Imperial_Body_fSK_Nord_Body_f
MaleFullBodies[0].PhenotypePresetPhenotypePreset_Imperial_mPhenotypePreset_Nord_m/Game/Dev/Phenotypes/
FemaleFullBodies[0].PhenotypePresetPhenotypePreset_Imperial_fPhenotypePreset_Nord_f
Senescence DataTableDT_Senescence_Imperial_m/fDT_Senescence_Nord_m/f/Game/Dev/Phenotypes/Senescence/[Race]/

MeanOblivionFaceGenData. The Race Average Face

Each race has a MeanOblivionFaceGenData that defines the "average" face for that race. Individual NPC OblivionFaceGenDataOffset values are offsets from this mean.

  • Imperial mean: SymmetricalGeometryData = all zeros (perfectly average), TextureData = 50 non-zero values (specific skin texture biases)
  • Nord mean: SymmetricalGeometryData = 50 non-zero values (Nords have distinct facial geometry vs baseline), TextureData = 50 non-zero values
CK Implication: FaceGen Math To generate a custom NPC face: final_face = MeanFaceGenData + OblivionFaceGenDataOffset. Setting OblivionFaceGenDataOffset to all zeros gives you the race's average face. The CK character creator will expose sliders that map to the 54 morph axes and compute the offset arrays automatically.

Senescence / Aging System

Each race+sex combination has a DT_Senescence_[Race]_[m|f] DataTable at /Game/Dev/Phenotypes/Senescence/[Race]/. This controls age-related appearance changes. The EVSenescenceModifiers enum:

ValueEffect
HealthyNormal healthy appearance
SickYellow sclera, veins visible (eye params: ScleraTintU=1, VeinsPower=1.72)
Vampire_01/02/03Override eye material to vampire eyes (MIC_[Race]_Eyes_Vampire)
EVSenescenceModifiers_MAXFinal vampire stage

Nord only has Sick modifier defined, no vampire eye variants. Imperial has all four vampire stages pointing to MIC_Imperial_Eyes_Vampire.

All Playable Races. Asset Path Reference

RaceForm pathArt pathHeight
Imperial/Game/Forms/actors/race/Imperial/Game/Art/Character/Imperial/1.0 (baseline)
Nord/Game/Forms/actors/race/Nord/Game/Art/Character/Nord/1.06
Breton/Game/Forms/actors/race/Breton/Game/Art/Character/Breton/ (inferred)TBD
Redguard/Game/Forms/actors/race/Redguard/Game/Art/Character/Redguard/ (inferred)TBD
HighElf/Game/Forms/actors/race/HighElf/Game/Art/Character/Elf/ (inferred)TBD
WoodElf/Game/Forms/actors/race/WoodElf/Game/Art/Character/Elf/ (inferred)TBD
DarkElf/Game/Forms/actors/race/DarkElf/Game/Art/Character/Elf/ (inferred)TBD
Argonian/Game/Forms/actors/race/Argonian/Game/Art/Character/Argonian/ (inferred)TBD
Khajiit/Game/Forms/actors/race/Khajiit/Game/Art/Character/Khajiit/ (inferred)TBD
Orc/Game/Forms/actors/race/Orc/Game/Art/Character/Orc/ (inferred)TBD

Hair System visual

VCharacterHairPiece_Hair Architecture

Hair assets are VCharacterHairPiece_Hair objects at /Game/Dev/Phenotypes/Hair/. Each defines a hair style that works across multiple races and sexes. The system has two levels of mesh selection:

  1. Default mesh (HairSkeletalMeshes.MeshComponent), used for any race/sex combination not explicitly listed
  2. Race+Sex overrides (RaceAndSexSpecificHairMeshes[]), specific meshes for specific race/sex combos

Example: HP_Human_HR_Short

Default mesh: SK_Imperial_HR_Martin (male Imperial base)
 → Used for all male humans not in override list

Female override (Imperial/Breton/Redguard/Nord/HighElf/WoodElf/DarkElf):
 → SK_Imperial_HR_Martin_f (female-shaped version)

Male Elf override (HighElf/WoodElf/DarkElf):
 → SK_Elf_HR_Martin (elf-shaped male version)
Key Insight: One Hair Asset = Multiple Race Meshes A single HP_ asset handles race-appropriate hair geometry automatically. When the NPC editor assigns a hair piece, the game picks the right mesh based on the NPC's race+sex. Custom hair pieces need to provide at minimum the default mesh plus female overrides for the target races.

Complete Hair Asset Catalog (Confirmed from screenshot)

All assets at /Game/Dev/Phenotypes/Hair/:

Argonian Hair/Head Pieces

HP_Argonian_HR_BroadWebbedEars, HP_Argonian_HR_CentralSpineCluster_Jeweled, HP_Argonian_HR_CentralSpineCluster, HP_Argonian_HR_CentralSpineCrest_Jeweled, HP_Argonian_HR_CentralSpineCrest, HP_Argonian_HR_Curved, HP_Argonian_HR_DualForeheadHorns_Jeweled, HP_Argonian_HR_DualForeheadHorns, HP_Argonian_HR_Feathers, HP_Argonian_HR_Fins, HP_Argonian_HR_FinsJeweled, HP_Argonian_HR_Ridge, HP_Argonian_HR_SpikePaerialArray_Jeweled, HP_Argonian_HR_SpikePaerialArray, HP_Argonian_HR_Spikes_Jeweled, HP_Argonian_HR_Spikes, HP_Argonian_HR_SpikesDecorated, HP_Argonian_HR_Spines_Jeweled, HP_Argonian_HR_Spines, HP_Argonian_HR_Straigth, HP_Argonian_HR_WebbedSpikeCrest

Race-Specific Hair

HP_Breton_HR_Tonsure, HP_DarkElf_HR_Fringe, HP_DarkElf_HR_Mane, HP_DarkElf_HR_Mohawk, HP_Dremora_HR_Female, HP_Dremora_HR_Hair, HP_Dremora_HR_HairB, HP_Dremora_HR_HairLord, HP_Elf_HR_Braid, HP_Elf_HR_PonyTail, HP_HighElf_HR_Bun, HP_HighElf_HR_Classic, HP_HighElf_HR_Cone, HP_HighElf_HR_Peak, HP_HighElf_HR_Pony

General Human Hair (HP_HR_ prefix = race-agnostic)

HP_HR_Blindfold, HP_HR_Cropped, HP_HR_Loose, HP_HR_MediumLength, HP_HR_Ponytail, HP_HR_PonytailTwist

Human Hair (HP_Human_HR_ prefix)

HP_Human_HR_Fringes, HP_Human_HR_MediumMohawk, HP_Human_HR_MessyBalding, HP_Human_HR_Short, HP_Human_HR_ShortPulledBack, HP_Human_HR_ShortSlick

Imperial-Specific

HP_Imperial_HR_Bald, HP_Imperial_HR_Coil, HP_Imperial_HR_Headband

Khajiit

HP_Khajiit_HR_Braids, HP_Khajiit_HR_Common, HP_Khajiit_HR_Dreds, HP_Khajiit_HR_EarRings, HP_Khajiit_HR_Feathers, HP_Khajiit_HR_HeadBand, HP_Khajiit_HR_Jeweled, HP_Khajiit_HR_Mane, HP_Khajiit_HR_Wisps

New/DLC Hair (HP_New_HR_ prefix)

HP_New_HR_Bob, HP_New_HR_Buns, HP_New_HR_Casual, HP_New_HR_Long, HP_New_HR_LongMohawk, HP_New_HR_Messy, HP_New_HR_Receding, HP_New_HR_ShortPony, HP_New_HR_Stylish

Special

HP_Bald, the bald/shaved head option

Hair Form ESP Assets brain

Source: Forms/miscellaneous/Hair/ screenshot, complete catalog These are the Gamebryo-side hair form assets (what goes in the TESNPC.Hair field in the ESP). Distinct from the VCharacterHairPiece HP_ assets in Dev/Phenotypes/Hair/ which are the UE-side mesh holders.

The Two Hair Asset Systems

Hair in OBR has two parallel asset trees that must correspond:

SystemPathTypeUsed in
Gamebryo hair forms/Game/Forms/miscellaneous/Hair/TESHair formTESNPC.Hair field (ESP side)
UE hair pieces/Game/Dev/Phenotypes/Hair/VCharacterHairPiece_HairVCharacterPhenotypeData.Hair (UE side)

The TESNPC.Hair form references the Gamebryo hair record. The VCharacterPhenotypeData.Hair references the UE mesh asset. They map to each other, changing one without the other gives mismatched results.

Complete Hair Form Catalog (Forms/miscellaneous/Hair/)

Argonian

ArgonianDecoratedSpikes, ArgonianFins, ArgonianJeweledFins, ArgonianRidge, ArgonianSpikes, ArgonianSpines

General / Multi-race

Blindfold, Loose, MediumLength, Ponytail, PonytailTwist

Race-Specific

BretonTonsure, Cropped, DarkElfFringe, DarkElfMane, DarkElfMohawk, DarkElfTopknot, dremoraHair, DremoraHairB, DremoraHairLord, ElfBraid, ElfPonytail, HighElfBun, HighElfClassic, HighElfCone, HighElfPeak, HighElfpony, HumanFringes, ImperialBald, ImperialHeadband

Khajiit

KhajiitBraids, KhajiitCommon, KhajiitDreds, KhajiitEarrings, KhajiitFeathers, KhajiitHeadBand, KhajiitJeweled, KhajiitMane, KhajiitWisps

Nord

NordBaldPony

Orc

OrcBraids, OrcBun, OrcHeadband, OrcOneBraid, OrcPlaits, OrcRomantic, OrcStubs, OrcTopknot, OrcTwoBraids, OrcUpdo

Redguard

RedguardClassic, RedguardCoil, RedguardCornrows, RedguardDredz

Wood Elf

WoodElfFringes, WoodElfPony, WoodElfSpiky

Custom NPC Hair. Both Sides Required

When assigning custom hair to an NPC:

  1. Set TESNPC.Hair in the ESP to one of the Form paths above (e.g. /Game/Forms/miscellaneous/Hair/MediumLength.MediumLength)
  2. Set VCharacterPhenotypeData.Hair in the Blueprint to the corresponding HP_ asset (e.g. HP_HR_MediumLength), only needed for unique named NPCs with bUseProceduralHead=false
  3. Generic NPCs (procedural head) use the Hair form reference only, the HP_ is selected automatically by the race/sex system

Clothing System visual

Source: Dev/clothing/ screenshot

Body Part Definition Blueprints (BP_Generic_BDP_)

Located at /Game/Dev/clothing/. These define the body part slots that clothing and armor occupy:

AssetBody partNotes
BP_Generic_BDP_AmuletNeck/amulet slotUsed for cape-via-amulet workaround
BP_Generic_BDP_FeetBoots/feet
BP_Generic_BDP_HandsGloves/gauntlets
BP_Generic_BDP_LowerBodyPants/greaves
BP_Generic_BDP_RingRing slot
BP_Generic_BDP_RobeFull robe (overrides upper+lower)
BP_Generic_BDP_SkeletalHelmetHelmet with bones (animated)
BP_Generic_BDP_StaticHelmetRigid helmet
BP_Generic_BDP_UpperBodyCuirass/chest
BP_Generic_Static_ClothStatic cloth item
BP_Generic_Activable_ClothCloth with interaction

Head Morph Caping (Helmet Fit)

Two special Blueprint assets handle how helmets deform to fit different head shapes:

  • HeadMorphCaping_Generic_BDP_SkeletalHood, morphs skeletal (animated) hoods to fit the wearer's head morph
  • HeadMorphCaping_Generic_BDP_StaticHelmet, morphs rigid helmets to fit

Clothing System Components

AssetTypePurpose
BPC_Cloth_ScalabiltyBlueprint ComponentLOD/scalability for cloth physics
BPE_ClothAssetPriorityBlueprint EnumPriority when multiple cloth items overlap
BPE_ClothingInstanceTypeBlueprint EnumType of clothing instance (static, skeletal, etc.)
BPI_ClothPhysicsControlBlueprint InterfaceInterface for controlling cloth physics at runtime
BPS_ClothAssetAndMaterialSectionsBlueprint StructStruct grouping cloth mesh with its material sections

Senescence System (Aging & Vampire) visual

Architecture. Three Asset Types

The aging/senescence system uses a three-tier asset hierarchy per race+sex:

TESRace.MaleFullBodies[0].Senescence
 → DT_Senescence_[Race]_[m|f] (DataTable, 8 rows)
 Row 1..8 → DA_Senescence_[Race]_[m]_01..08 (VSenescenceLevel)
 DA_Senescence_[Race]_[M]_Body_01/08_00 (body textures)

DT_Senescence_Imperial_m. Confirmed Structure

8 rows, each representing an aging stage. Each row has:

FieldContent
HeadSenescencesArray of {Key, VSenescenceLevel asset path}, one head texture set per stage
BodySenescencesArray of {Key, VSenescenceLevel asset path}, body texture variant
HairSenescencesAlways empty in Imperial, hair aging not implemented

Rows 1-5 use body variant DA_Senescence_Imperial_M_Body_01_00. Rows 6-8 switch to DA_Senescence_Imperial_M_Body_08_00, the older body texture set. 8 aging stages total for Imperial male.

VSenescenceLevel (DA_). Per-Stage Textures

Each DA_Senescence_[Race]_[m]_XX asset defines what textures to use for each health/vampire state at that aging stage:

StateContents in DA_01
HealthyTextureEmpty, uses race default skin textures
SickTextureBaseColor Map: T_Imperial_Head_M_01_D, NNR Map: T_Imperial_Head_M_01_NNRS
VampireTexture[0] (Stage 1)T_WhiteHumanoid_Vampire_Head_M_01_Stage01_D, no phenotype override
VampireTexture[1] (Stage 2)T_WhiteHumanoid_Vampire_Head_M_01_Stage01_D, no phenotype override
VampireTexture[2] (Stage 3)T_WhiteHumanoid_Vampire_Head_M_01_Stage02_D, phenotype override → PhenotypePreset_Vampire
PhenotypePreset_Vampire Stage 3 vampirism overrides the entire head phenotype to PhenotypePreset_Vampire at /Game/Dev/Phenotypes/PhenotypePreset_Vampire. This is a separate phenotype preset that applies vampire-specific face morphs (gaunt cheeks, prominent brow etc). This asset exists and is referenced, worth dumping for the full vampire appearance pipeline.

Texture Naming Convention

Head textures: T_[Race]_Head_[M|F]_[Stage]_[D|NNRS]
Body textures: T_[Race]_Body_[M|F]_[Variant]_[Stage]_[suffix]
Vampire textures: T_WhiteHumanoid_Vampire_Head_[M|F]_[Stage]_[D]
Location: /Game/Art/Character/[Race]/

CK Implication: Custom Race Aging

For a custom race that reuses an existing race's body+head meshes, the simplest approach is to point its Senescence DataTable at an existing race's DA_ assets. The textures will be wrong (wrong race skin) but the system will function. For fully custom aging: create 8 DA_ assets per gender, each with the appropriate skin texture paths for your custom head mesh.

Engine Internals

Dialogue, Voice & Lip Sync

The dialogue system end to end: TESTopicInfo as the Rosetta Stone between Gamebryo dialogue and UE audio/animation, the working PostEventAtLocation audio path, and the resolution of the speech2face mystery (lip sync is a baked AnimSequence, not audio analysis).

Dialogue System Internals bridge

Fully Reverse-Engineered. June 2026 The complete dialogue pipeline has been mapped through live probe sessions and FModel dumps. Custom voiced NPCs with working lip sync are achievable with the correct asset pipeline.

The TESTopicInfo Asset. The Rosetta Stone

Every dialogue response in Oblivion Remastered is backed by a TESTopicInfo UE asset. This asset is the bridge between Gamebryo's dialogue records and UE's audio/animation systems. It lives in /Game/Forms/miscellaneous/dialog/.

A single response entry contains three things simultaneously:

{
 "ResponseID": 1,
 "Text": "I can manage with a weapon or with spells...",

 "AkAudioEvents": [ // ← Wwise event, keyed by Race+Sex+VoiceType
 {
 "Key": { "Sex": "MALE", "Race": "/Game/Forms/actors/race/Nord.Nord",
 "VoiceType": "EVVoiceType::LEGACY" },
 "Value": "/Game/WwiseAudio/Events/Voice/oblivion/nord/m/Play_nord_m_[topic]_[formid]_1"
 }
 ],

 "Animations": [ // ← Facial AnimSequence for lip sync
 {
 "Key": { "Sex": "MALE", "Race": "/Game/Forms/actors/race/Nord.Nord" },
 "Value": "/Game/Art/Animation/Humanoid/Facial/Iver/A_[topic]_[formid]_1"
 }
 ]
}
Lip Sync Solved. It's Just an AnimSequence speech2face is NOT involved in dialogue lip sync. The mouth animation for each voice line is a baked AnimSequence stored in /Game/Art/Animation/Humanoid/Facial/, referenced directly from the TESTopicInfo asset. For custom NPCs: generate the AnimSequence via Audio2Face (L11 of the modding wiki), point the TESTopicInfo at it, done.

Custom Dialogue Pipeline. Complete

  1. Author dialogue topic in xEdit / CK GUI → get FormID (e.g. XX000001)
  2. Record/generate voice line WAV → cook to Wwise event → get event UE asset path
  3. Run WAV through Audio2Face → export facial AnimSequence → cook to UE asset
  4. Create TESTopicInfo UAsset with text + Wwise event path + AnimSequence path
  5. Register via TSMI: GREETING_MyMod_XX000001=/Game/Forms/miscellaneous/dialog/MyTopic.MyTopic
  6. NPC's race + voice type determines which AkAudioEvent and Animation entry the game picks

Runtime Hook Architecture (CosmicBridge DialogueAPI)

For mods that intercept existing NPC dialogue rather than adding new topics, the hook chain is:

Confirmed Hook Firing Order (probe session 2026-06-12)
  1. 1. SetVDialogueMenuViewModel. UI binds (speaker/subtitle EMPTY at this point)
  2. 2. AST_Dialogue:OnEntered, camera state transitions to dialogue
  3. 3. AST_Dialogue:OnStateUpdate, fires every frame during dialogue
  4. 4. VDialogueMenuViewModel:GetSubtitle, fires when each line updates (non-empty check required)
  5. 5. AST_Dialogue:OnExited, dialogue ends
Timing Trap: SetVDialogueMenuViewModel This hook fires before the ViewModel has data. GetSpeakerName() and GetSubtitle() return empty strings here. Wait for OnStateUpdate + subtitle non-empty before firing audio or reading speaker identity.

Readable VM Properties (All Confirmed Working)

FunctionReturnsNotes
GetSpeakerName():ToString()String e.g. "Jauffre"FText, requires :ToString()
GetSubtitle():ToString()Current line textEmpty until line loads (~1 frame after OnEntered)
GetResponses()Table of response optionsPlayer dialogue choices
IsSubtitleVisible()BoolReflects user subtitle setting

Bank Residency. The Indoor/Outdoor Difference

Custom Wwise banks are not resident in interior cells. PostEventAtLocation returns playing_id=0 indoors consistently. Outdoors (tested with Chorrol Guard), playing_id is non-zero and audio fires correctly. Root cause: pak mounting differs between interior and exterior level loads. Fix pending, investigate pak mount triggers per level type.

speech2face. Confirmed Not Audio-Driven

speech2face lives in VHumanoidHeadComponent as pure native C++. It has zero exposed UFunctions for speech/lip sync control. It does not respond to PostEventAtLocation audio regardless of content. For custom dialogue, use the TESTopicInfo AnimSequence path instead, that's the intended system and it works.

Audio Architecture visual

The NPC Voice Component

Every humanoid NPC has exactly one voice component: VAltarAkComponent named AudioComponent, attached to CharacterMesh0. This is a custom Altar subclass of UAkComponent with only two additional UFunctions:

FunctionNotes
BPF_HasActiveEvents()Returns bool, whether audio is currently playing on this component. Safe to call.
ForceUpdateGameObjectPosition()Forces Wwise to update the game object's 3D position.
No Custom PostEvent Wrapper VAltarAkComponent does NOT have a custom delegate-free PostEvent method. The full AkComponent PostEvent requires a delegate parameter that UE4SS cannot satisfy. PostEventAtLocation (static, world position) is the only working post method from Lua.

PostEventAtLocation. The Working Method

local AkStatics = StaticFindObject("/Script/AkAudio.Default__AkGameplayStatics")
local ev = StaticFindObject("/Game/WwiseAudio/Events/YourEvent/YourEvent.YourEvent")
local loc = actor:K2_GetActorLocation()
local id = AkStatics:PostEventAtLocation(ev, loc, {Pitch=0,Yaw=0,Roll=0}, world)
-- id > 0 = success, id = 0 = bank not resident

Voice Event Asset Structure (Confirmed from FModel)

Each voice line gets its own dedicated bank. Not a shared bank, one bank per event.

FieldValue / Pattern
Event namingPlay_[race]_[gender]_[questid]_[topic]_[formid]_[index]
Bank locationEvent/English(US)/[EventName].bnk
Media locationMedia/English(US)/[MediaId].wem
bContainsMediafalse, media is always separate
bStreamingfalse
MaxAttenuationRadius3000.0
UE asset path/Game/WwiseAudio/Events/Voice/oblivion/[race]/[m|f]/[EventName]

Two-Pak Structure

PakToolContains
MyMod_P.pakretocCooked .uasset + .ubulk for AkAudioEvent assets
MyMod_Audio_P.pakUnrealPakLoose .bnk and .wem files at their full content paths

AkGameplayStatics. Full UFunction Surface

64 functions confirmed loaded at runtime. Key ones for mod use:

FunctionStatusNotes
PostEventAtLocationworkingPrimary audio post method from Lua
GetAkComponentbrokenHas Out-param bool (ComponentCreated). UE4SS can't satisfy
GetOrCreateAkComponentbrokenSame Out-param issue
SpawnAkComponentAtLocationuntestedExpects 8 params, signature not yet confirmed
SetRTPCValueuntestedCould set game parameters, useful for music/state
SetStateuntestedWwise states, music layer switching
StopActoruntestedStop all audio on an actor, potential voice mute method

ABP_HumanoidHead. The Head Animation Blueprint visual

Critical Finding: speech2face Architecture Confirmed ABP_HumanoidHead's AnimGraph structure reveals exactly how facial animation works. This resolves the speech2face open question.

Class Hierarchy

ABP_HumanoidHead_C
 extends: VHumanoidHeadCharacterAnimInstance
 extends: AnimInstance (UE native)

AnimGraph Node Structure. Confirmed

The AnimGraph uses named cached poses to layer animations in a specific order:

AnimGraph Output
 └── LayeredBoneBlend (BranchFilter mode)
 ├── BasePose: "BodyPose" (cached, copied from body skeletal mesh)
 └── BlendPose[0]: BlendListByBool
 ├── [False] ApplyAdditive
 │ ├── Base: "FacialAndEyes Animation" (cached)
 │ └── Additive: LocalRefPose
 └── [True] "EmotionPose" (cached)

"BodyPose" = AnimGraphNode_CopyPoseFromMesh (bUseAttachedParent=True)
"Facial Animation" = AnimGraphNode_Slot (SlotName='DefaultSlot') → sequence player
"FacialAndEyes Animation" = Facial Animation + BS_eyes_movement blendspace
"EmotionPose" = StateMachine (7 states, emotion transitions)

The Three Facial Layers

LayerCache nameSourcePurpose
Body syncBodyPoseCopyPoseFromMesh (parent body)Keeps head attached to body skeleton
Lip syncFacial AnimationAnimSequence played in DefaultSlotTHIS is the lip sync, plays the A_ AnimSequence from TESTopicInfo
Eye movementFacialAndEyes AnimationFacial + BS_eyes_movement blendspaceAdds eye tracking on top of facial anim
EmotionEmotionPoseStateMachine (7 emotion states)Ambient emotional expressions when not talking

Emotion StateMachine, 7 States

The emotion StateMachine drives ambient facial expressions. It has 7 sequence player states (AnimGraphNode_SequencePlayer through _6), each playing a different emotion AnimSequence. The Emotions array on the CDO contains the first confirmed entry:

Emotions[0]: AnimSequence'A_EmperorCut_Rig_02_neutral'
 → /Game/Art/Animation/Humanoid/Facial/Emotions/...

7 emotion states suggests: Neutral, Happy, Sad, Angry, Afraid, Disgusted, Surprised (standard FACS set) or similar.

The DefaultSlot. Lip Sync Entry Point

AnimGraphNode_Slot with SlotName='DefaultSlot' is the exact mechanism that plays facial AnimSequences from TESTopicInfo. When the dialogue system fires a voice line, it plays the corresponding A_[topic]_[formid]_1 AnimSequence into DefaultSlot on the head ABP. This is purely AnimMontage/Slot-based, no Wwise RTPC, no audio analysis, no speech2face native function involved in the mouth movement.

speech2face Mystery Resolved "speech2face" is simply the DefaultSlot AnimSequence playback system in ABP_HumanoidHead. It's not a Wwise lip sync plugin. It's not a native C++ audio analyzer. It's a standard UE AnimMontage slot that plays a baked facial AnimSequence. The reason speech2face doesn't respond to PostEventAtLocation audio is that it requires an AnimSequence to be played into the DefaultSlot by the dialogue system, audio alone does nothing. For custom dialogue: provide an A_ AnimSequence in TESTopicInfo.Animations, the dialogue system plays it into DefaultSlot, mouth moves correctly.

Eye Tracking

AimTarget is a Vector3 property on the ABP CDO (default 0,0,0). EyesBoneName: FACIAL_C_FacialRoot is the bone that gets aim-offset. EyeHeadingMultiplier: 0.5 controls how much the eyes follow targets. The BS_eyes_movement blendspace at /Game/Art/Animation/Humanoid/Eyes/ drives the eye directional movement.

UpdateBodyMesh Function

The ABP has an OnBodyMeshUpdated callback (taking NewBodyMesh: SkeletalMeshComponent) and an UpdateBodyMesh(BodyMesh) function. This is how the head ABP stays synced when the body mesh changes (e.g. armor swaps affecting the neck). Relevant for custom races with non-standard body meshes.

Key Functions (All Blueprint-callable)

FunctionParamsPurpose
AimHead(none confirmed)Updates head aim toward AimTarget
UpdateBodyMeshBodyMesh: SkeletalMeshComponentRelinks head to new body mesh
OnBodyMeshUpdatedNewBodyMesh: SkeletalMeshComponentEvent callback when body changes

Engine Internals

State Machine, Combat, Input, Effects & Mounts

The character state machine (every hookable AST_/COND_ asset), the player and AI controllers, the visual effects components, the Enhanced Input action map, and how mounts differ from NPCs.

Character State Machine Map visual

Source PSM_CharacterMain.json, ASM_Character.json, and StateMachine folder screenshot. All AST_ and COND_ assets confirmed present at Content/Dev/StateMachine/.

Top-Level Structure

ASM_Character is the root. Its initial state is PSM_CharacterMain, tagged State.Character. PSM_CharacterMain runs two parallel sub-machines simultaneously:

  • ASM_CharacterLocomotion, all movement states
  • ASM_CharacterAction, all combat/interaction states

Action States (All Hookable via RegisterHook)

Each AST_ is hookable with OnEntered, OnExited, OnStateUpdate. Pattern: /Game/Dev/StateMachine/AST_[Name].AST_[Name]_C:[Function]

State AssetTriggerCosmicBridge Event
AST_CharacterActionLightAttackLight attack beginsCombat.onLightAttack
AST_CharacterActionPowerAttackPower attack beginsCombat.onPowerAttack
AST_CharacterActionBlockBlock raisedCombat.onBlock
AST_CharacterActionShieldBashShield bashCombat.onShieldBash
AST_CharacterActionBowDrawShootBow drawn + shotCombat.onBowShoot
AST_CharacterActionSpellCastingSpell castCombat.onSpellCast
AST_CharacterActionDodgeDodgeCombat.onDodge
AST_CharacterActionGrabGrab objectCombat.onGrab
AST_CharacterDeadDeathActor.onDeath
AST_CharacterKnockDownKnocked downCombat.onKnockDown
AST_CharacterStaggeredStaggeredCombat.onStagger
AST_CharacterParalyzeParalyzedCombat.onParalyze
AST_CharacterStunnedStunnedCombat.onStun
AST_CharacterVampireFeedVampire feedingCombat.onVampireFeed
AST_CharacterDockingMounting/docking beginsActor.onMountBegin
AST_CharacterDockedMounted/dockedActor.onMounted
AST_CharacterUndockingDismountingActor.onDismount
AST_CharacterResurrectResurrectionActor.onResurrect
AST_CharacterGetUpGetting up from groundCombat.onGetUp
AST_Dialogue (OnEntered)Dialogue startsDialogue.onStart
AST_Dialogue (OnExited)Dialogue endsDialogue.onEnd

Condition Assets (State Query Flags)

COND_ assets are transition condition evaluators. They can be used to query current character state:

AssetTests
COND_IsDeadCharacter is dead
COND_IsRiddenCharacter is being ridden (mounted)
COND_IsWeaponDrawnWeapon is out
COND_IsCrouchCharacter is sneaking
COND_IsPlayerThis is the player character
COND_IsInAirAirborne
COND_IsRunningRunning
COND_IsSprintingSprinting
COND_IsSwimmingSwimming
COND_IsUnconsciousUnconscious
COND_IsDockedMounted/docked to a pawn
COND_IsInCustomizationMenuIn character creator
COND_HasShieldEquippedShield in offhand
COND_HasBowEquipedBow equipped
COND_HasFatigueLeftHas stamina remaining
COND_IsOverEncumberedOver carry weight
COND_ShouldPowerAttackPower attack threshold met
COND_ShouldSneakSneak mode active

Controllers & AI Architecture visual

BP_AltarPlayerController

Extends VEnhancedAltarPlayerController. This is the player controller, owns all input mapping contexts and references the player camera manager.

Key Input Actions (All hookable via RegisterHook)

ActionAsset path fragmentContext
AttackIA_Game_Combat_AttackIMC_Game_Combat
BlockIA_Game_Combat_BlockIMC_Game_Combat
Cast spellIA_Game_Combat_CastIMC_Game_Combat
GrabIA_Game_Actions_GrabIMC_Game_Actions
ActivateIA_Game_Actions_ActivateIMC_Game_Actions
Toggle POVIA_Game_Actions_TogglePOVIMC_Game_Actions
JumpIA_Game_Movement_JumpIMC_Game_Movement
CrouchIA_Game_Movement_CrouchIMC_Game_Movement
SprintIA_Game_Movement_SprintIMC_Game_Movement
Horse gallopIA_Game_Movement_GallopIMC_Game_Movement
Telekinesis PushIA_Game_Telekinesis_PushIMC_Game_Telekinesis
Telekinesis PullIA_Game_Telekinesis_PullIMC_Game_Telekinesis
Quick SaveIA_Game_Default_QuickSaveIMC_Game_Default

Key Tunable Properties (Engine.ini overrideable)

PropertyDefaultNotes
PowerAttackInputTime0.35sHold duration for power attack trigger
ViewSensitivity0.4Base camera sensitivity
GamepadSensitivityScale300.0
FirstPersonCameraVerticalSensitivityScale1.4
ThirdPersonCameraVerticalSensitivityScale1.4
CameraTrackingBaseSpeed90.0
PlayerCameraManagerClassBP_AltarPlayerCameraManager_CReplaceable for custom camera
CheatClassAltarCheatManager/Script/Altar native class

ControlledPlayerCharacter

The property ControlledPlayerCharacter (Edit | BlueprintVisible) holds a direct reference to the player character. This is accessible from Lua if the controller object can be retrieved, potentially more reliable than UEHelpers.GetPlayer() in edge cases.

BP_PairedPawnAIController

Extends VPairedPawnAIController. Controls all NPC AI. Key architecture:

  • VSteeringBehaviorsComponent, handles NPC movement/steering AI
  • VAltarPathFollowingComponent, custom path following
  • Navigation filter classes: NQF_Default_C, NQF_CannotSwim_C, NQF_CanOnlySwim_C, NQF_FollowPlayerCharacter_C
  • GetProcedureInfos(). Blueprint callable, returns AI procedure debug info
Navigation Filter Classes The four NQF_ (Navigation Query Filter) classes at /Game/Dev/Navigation/ determine which navmesh areas an NPC can traverse. Custom NPC AI packages should reference the appropriate filter, most humanoids use NQF_Default_C.

Effects System visual

BPC_BoundEffect. Invisibility & Fade

Component that handles NPC fade-in/out for bound/summoned creatures and invisibility effects. Extends ActorComponent.

FunctionNotes
FadeIn()Fades the actor in using Curve_BoundFadeIn over 0.3s
FadeFinished()Callback when fade completes

Key properties: PrimitiveComponents[], StaticMeshComponents[], FadeInCurve (CurveFloat at /Game/Dev/PairingActors/Curve_BoundFadeIn), FadeInDuration (default 0.3s).

BPC_StatusEffect. Magic Effect Visuals

The primary component for applying visual status effects (poison, fire, frost, etc.) to characters. Extends ActorComponent. Very complex, 38 child functions, 46 properties.

Key FunctionSignaturePurpose
Apply Status Effect on Component(form, VFXColors[], MeshIgnoreList[], StatusEffectValue)Apply visual effect to a mesh component
Remove Status Effect on Component(form, VFXColors[], MeshIgnoreList[], StatusEffectValue)Remove visual effect
Set Fade Value On All Material Switch MIDs(Value float)Control fade of material overlays
Create Invisibility Data For Component(MeshComponent)Set up invisibility effect on mesh
DisableBloodSplatterComponent(Disable bool)Toggle blood decal system

Key properties: Form (the effect form), PawnOwner, OverlayMaterialInterface, OverlayFadeDuration, Overlay_MinimumLifeTime (1.0s default), ApplyMaterialSwitch.

CosmicBridge EffectsAPI. Future Module BPC_StatusEffect's Apply/Remove functions are the hook points for a future EffectsAPI module. Bridge.Effects.applyTo(actor, effectForm, intensity). The component exists on all VPairedPawn actors. This is also how custom magic visual effects would work for new spell types.

Wwise Aux Bus. EXT_IC_General

Confirmed bus asset from WwiseAudio/Bus/. EXT_IC_General is an AkAuxBus with ID 1726809084. This is an environmental/reverb aux bus (IC = Interior/Exterior, General). Route custom audio to this bus for correct interior acoustic treatment. MaxAttenuationRadius: 0 = global/ambient routing, not spatialized.

Input Action Asset Map visual

All input actions confirmed from BP_AltarPlayerController CDO. Assets located at /Game/Dev/Input/GamePlay/InputActions/.

Input Mapping Contexts

ContextAssetActive when
IMC_Game_DefaultIMC_Game_DefaultAlways
IMC_Game_MovementIMC_Game_MovementIn-world movement
IMC_Game_CombatIMC_Game_CombatDuring combat readiness
IMC_Game_ActionsIMC_Game_ActionsWorld interaction
IMC_Game_TelekinesisIMC_Game_TelekinesisTelekinesis active
IMC_Game_QuickKeysIMC_Game_QuickKeysQuick item slots
IMC_Game_DebugIMC_Game_DebugDebug builds

Hookable Input Actions (CosmicBridge InputAPI. Future)

These IA_ assets can be hooked via UE4SS's Enhanced Input system hooks. Each can fire callbacks on press/release for custom mod behaviors without overriding game controls.

-- Future CosmicBridge InputAPI
Bridge.Input.onAction("IA_Game_Combat_Attack", function(pressed)
 if pressed then
 -- Custom attack hook
 end
end)

Mount Architecture visual

Source: BP_Horse_Black.json, confirmed horse Blueprint structure

Horse Blueprint Hierarchy

BP_Horse_Black_C
 extends: BP_Generic_HorseBlack_C (VModdableBlueprintGeneratedClass)
 extends: VPairedPawn (native, same as NPCs)

Key Differences vs NPC Blueprints

ComponentNPCHorse
CharacterMovementVPairedPawnMovementComponentVHorseMovementComponent
HumanoidHeadComponentYesNo
MountCameraSpringArmComponentNoYes, camera arm for mounted view
ReplicationDefaultbReplicates=false, RemoteRole=ROLE_None

The horse shares the same pairing component architecture as NPCs: VTransformPairingComponent, VAnimationPairingComponent, VActorValuesPairingComponent, VActiveEffectsPairingComponent, VWeaponsPairingComponent, VPawnSoundPairingComponent. This means horses have health (actor value index 10), can be affected by magic, and have weapon pairing (for the rider's weapon during mounted combat, confirmed from your mounted combat mod work).

Custom Mount Creation

For a custom mount, duplicate BP_Generic_HorseBlack_C (or whichever horse variant fits) and override the skeletal mesh. The VHorseMovementComponent handles all riding physics, no custom movement code needed for basic mounts.

Engine Internals

Weapons & Spells

The weapon Blueprint hierarchy (VModdableBlueprintGeneratedClass, type tags, hitboxes, physics), the TESObjectWEAP form, and the spell system (TESSpell + EffectSettings, and why custom magic routes through SEFF Script Effects).

Weapon System bridge

Fully Confirmed, weapon Blueprint hierarchy, form structure, hitbox, physics

The Critical Discovery: VModdableBlueprintGeneratedClass

All weapon Blueprints use VModdableBlueprintGeneratedClass instead of the standard BlueprintGeneratedClass. This is Altar's moddable Blueprint class, it supports runtime modification and is the class type TSMI patches. This is why weapons are modifiable at runtime in ways other assets aren't. Every weapon Blueprint you create must use this class.

Weapon Class Hierarchy. Complete

Three tiers: native C++ base → generic Blueprint template → specific weapon type child → individual weapon form

C++ Native (in /Script/Altar):
 VWeapon ← base for all weapons
 ├── VWeapon_Blade ← all blade weapons
 ├── VWeapon_Blunt ← blunt weapons 
 ├── VWeapon_Bow ← bows
 ├── VWeapon_Staff ← staves
 └── VShield ← shields
 VQuiver ← quiver/ammo holder

Generic Templates (Dev/weapons/):
 BP_Weap_GenericBlade → extends VWeapon_Blade
 BP_Weap_GenericBlunt → extends VWeapon_Blunt
 BP_Weap_GenericBow → extends VWeapon_Bow
 BP_Weap_GenericStaff → extends VWeapon_Staff
 BP_Weap_GenericShield → extends VShield
 BP_Weap_GenericQuiver → extends VQuiver

 Variants with scabbards:
 BP_Weap_GenericBlade_Scabbard → blade + scabbard mesh
 
Specific Type Children (Dev/weapons/GenericChild/):
 BP_Weap_GenericLongSword → extends BP_Weap_GenericBlade
 BP_Weap_GenericClaymore → extends BP_Weap_GenericBlade
 BP_Weap_GenericDagger → extends BP_Weap_GenericBlade
 BP_Weap_GenericShortSword → extends BP_Weap_GenericBlade
 BP_Weap_GenericAxe → extends BP_Weap_GenericBlade (likely)
 BP_Weap_GenericBattleAxe → extends BP_Weap_GenericBlade
 BP_Weap_GenericWarAxe → extends BP_Weap_GenericBlade
 BP_Weap_GenericHammer → extends BP_Weap_GenericBlunt
 BP_Weap_GenericMace → extends BP_Weap_GenericBlunt
 
 Scabbard variants:
 BP_Weap_GenericLongSwordWithScabbard
 BP_Weap_GenericDaggerWithScabbard
 BP_Weap_GenericShortSwordWithScabbard

Individual Weapon Forms (Forms/items/weapons/):
 BP_Iron_LongSword → extends BP_Weap_GenericLongSwordWithScabbard
 ... (one per weapon in the game)

WeaponTypeTag. The Key Identifier

Each weapon Blueprint has a WeaponTypeTag Gameplay Tag that drives animation, combat behavior, and loot generation. The complete confirmed tag set:

TagBlueprintLocomotion Tag
WeaponType.OneHanded.Blade.LongswordBP_Weap_GenericBladeActor.Locomotion.MoveSet.OneHanded
WeaponType.OneHanded.Blunt.MaceBP_Weap_GenericBluntActor.Locomotion.MoveSet.OneHanded
WeaponType.TwoHanded.Blade.ClaymoreBP_Weap_GenericClaymoreActor.Locomotion.MoveSet.TwoHanded
WeaponType.BowBP_Weap_GenericBowActor.Locomotion.MoveSet.Bow
WeaponType.StaffBP_Weap_GenericStaffActor.Locomotion.MoveSet.Staff
WeaponType.ShieldBP_Weap_GenericShield(none, shield has no locomotion tag)

VHitBoxComponent. Hit Detection

Bladed weapons use VHitBoxComponent for hit detection. The hitbox is a box collider whose dimensions define the blade's hit region:

WeaponBoxExtent (X,Y,Z)RelativeLocation YNotes
GenericBlade (base)0,0,0n/aZero default, children override
LongSword8, 50, 0.7-52.74Y = blade length offset
Claymore7, 55, 1.0-63.92Longer blade

The Y axis is the blade length. RelativeLocation.Y offsets from the hilt. Blunt weapons and bows do not have VHitBoxComponent, they use different hit detection (likely capsule/sphere from physics).

Mesh Types by Weapon Class

Weapon typeMesh typeNotes
Blades, Blunt, Staff, ShieldStaticMeshComponent"Main Mesh Component"
BowSkeletalMeshComponentNeeds ABP_Reverse_Bow animation Blueprint
Weapons with scabbardTwo StaticMeshComponents"Main Mesh Component" + "Scabbard Mesh Component"

VPhysicsControllerComponent. Weapon Physics

SettingBlade/Blunt/Staff/ShieldBow
PhysicsSimulationBehaviourWHEN_UNEQUIPPEDALWAYS
bIsGrabbabletruetrue
bIsTelekinesisTargetabletruetrue
DefaultSelfSurfaceTypeMetalMetal

Shield adds: bDoesUseCCD: true, bCanSnapToEnvironment: true, PhysicsLoadingBehaviour: LOAD_POSE_FROM_SAVE, BuoyancyCoefficient: 0.2

BPC_WeapBloodSplatter

All blade weapons inherit BPC_WeapBloodSplatter from BP_Weap_GenericBlade. Each specific weapon type overrides bloodSplatterTextureSize: LongSword = 80.0, Claymore = 100.0. Individual weapon Blueprints (like BP_Iron_LongSword) override MICsBloodParentReferences to point to their specific blood splatter material (e.g. MIC_Iron_LongSword_WeapBloodSplatter).

TESObjectWEAP Form. Complete Field Reference

From WeapIronLongsword confirmed:

FieldValue/ExampleNotes
FullName"Iron Longsword"Display name
TextureIcon/Game/ArtOriginal/textures/menus/icons/weapons/T_ironlongswordInventory icon, 2D texture
TypeEOblivionWeaponType::BLADE_ONE_HANDSee enum below
Speed1.0Attack speed multiplier
Mass4.5359235Weight in lbs (≈ 10 lbs)
m_formID3084Decimal FormID
m_formEditorID"WeapIronLongsword"EditorID
m_formType"FormID::WEAP_ID"Always this for weapons
Blueprint/Game/Forms/items/weapons/BP_Iron_LongSwordUE Blueprint asset path
BlueprintClass/Game/Forms/items/weapons/BP_Iron_LongSword_CThe _C class path
BSXFlags3Always 3 for weapons
EnchantSaveDataVEnchantSaveData sub-objectEnchantment state

EOblivionWeaponType Enum

ValueWeapon typeBlueprint base
BLADE_ONE_HANDOne-handed sword/daggerBP_Weap_GenericBlade or children
BLADE_TWO_HANDClaymore/battleaxeBP_Weap_GenericClaymore etc
BLUNT_ONE_HANDMace/axeBP_Weap_GenericBlunt
BLUNT_TWO_HANDWarhammerBP_Weap_GenericBlunt or child
BOWBowBP_Weap_GenericBow
STAFFStaffBP_Weap_GenericStaff

Individual Weapon Blueprint. Iron LongSword Pattern

The concrete weapon Blueprint (in Forms/items/weapons/) extends the specific generic type and sets the actual meshes:

BP_Iron_LongSword_C extends BP_Weap_GenericLongSwordWithScabbard_C

Overrides:
 Main Mesh Component.StaticMesh → SM_Iron_LongSword
 (/Game/Art/Equipment/weapons/Iron/SM_Iron_LongSword)
 Scabbard Mesh Component.StaticMesh → SM_Iron_LongSword_scabbard
 (/Game/Art/Equipment/weapons/Iron/SM_Iron_LongSword_scabbard)
 BPC_WeapBloodSplatter.MICsBloodParentReferences →
 [MIC_Iron_LongSword_WeapBloodSplatter]

Art assets at: /Game/Art/Equipment/weapons/[Material]/
 SM_[Material]_[WeaponType] ← weapon mesh
 SM_[Material]_[WeaponType]_scabbard ← scabbard mesh
 MIC_[Material]_[WeaponType]_WeapBloodSplatter ← blood MIC

CK Weapon Creator. Complete Pipeline

  1. Choose weapon type → selects correct Generic base class
  2. Assign static mesh → overrides "Main Mesh Component"
  3. Optionally assign scabbard mesh → overrides "Scabbard Mesh Component"
  4. Set VHitBox dimensions (X=blade width, Y=blade length, Z=blade thickness) and offset Y
  5. Assign blood splatter MIC
  6. Set TESObjectWEAP fields (name, type enum, speed, mass, icon)
  7. Generate: weapon Blueprint JSON + TESObjectWEAP form JSON + ESP record + SyncMap entry

Spell System bridge

Source: TestShock, SE05ShockSpell, SETestShockDamageArea. TESSpell confirmed

TESSpell Asset Structure

Spell form assets live at /Game/Forms/magic/. Each is a TESSpell object.

TESSpell Fields

FieldExampleNotes
FullName"Shocking Death"Display name
m_formID530235Decimal FormID
m_formEditorID"SE05ShockSpell"EditorID, no spaces
EffectSettingsArray of asset pathsReferences to magic effect setting forms at /Game/Forms/magic/effectsetting/

EffectSettings. The Magic Effect References

Each spell's power comes entirely from its EffectSettings array, references to EffectSetting form assets at /Game/Forms/magic/effectsetting/. Confirmed effect setting identifiers:

AssetEffect
SHDGShock Damage, the shock damage magic effect
SEFFScript Effect. OBScript-driven custom effect

SETestShockDamageArea combines both: SHDG (shock damage) + SEFF (script area effect). This is the pattern for custom magic effects that combine built-in damage with custom scripted behavior.

Custom Magic Effect Strategy You cannot create new native magic effect types (SHDG, FRDG, etc.), those are C++ hardcoded. But you CAN create new spells that combine existing effects + SEFF (Script Effect) for custom scripted behavior. SEFF lets OBScript drive arbitrary game logic when the spell hits. This is the correct path for custom magic in the CK.

Magic Effect Setting Path Pattern

/Game/Forms/magic/effectsetting/[4-CHAR-CODE].[4-CHAR-CODE]

Known effects (from dumps + wiki):
 SHDG = Shock Damage
 FRDG = Frost Damage 
 FIDG = Fire Damage
 SEFF = Script Effect
 HEAL = Restore Health
 DRAT = Drain Attribute
 FOAW = Fortify Attribute
 (etc, same 4-char codes as vanilla Oblivion)

TESSpell has no Blueprint reference

Unlike weapons and NPCs, TESSpell has no Blueprint or BlueprintClass field, the visual VFX for spells is driven by the EffectSetting assets themselves, not the spell form. This means spell forms are purely Gamebryo-side data; the UE side reads the effect type and selects the appropriate particle/VFX Blueprint automatically based on the effect code. Custom spell VFX requires modifying the EffectSetting assets, not the TESSpell.

Engine Internals

Cells & Interior Maps

The six-file map structure, cell transition doors, the ReferenceHolder pairing actor, and the interior streaming architecture (the base level is the streaming parent) that explains why MagicLoader cells get broken lighting and missing navmesh.

Cell & Map System bridge

Map File Structure

Each world area in Content/Maps/World/ consists of six files with a shared base name:

SuffixContentsRequired for custom cells
(none)Base level, actors, gameplay objectsYes
_DelDestructible objectsStub required
_EnvEnvironment / static geometryYes, primary visual content
_LiLighting and lightmassYes, controls baked lighting
_SDShadows and decalsStub required
_VFXParticle effects and ambient FXStub required
MagicLoader's Current Limitation MagicLoader only generates the base .umap entry in CellsToMapPath. It does not create stubs for the five variant files. This is why some interior lighting and FX behave incorrectly with ML-generated cells. The CK build pipeline will generate all six files.

LevelSelectDoors. Cell Transition Architecture

Cell transitions are handled by Blueprint actors in Content/Dev/LevelSelectDoors/:

BlueprintRole
BP_LevelEntryDoorObvGamebryo-synced door, the one to use for mod cells. Reads from CellsToMapPath DataTable.
BP_LevelEntryDoorUE-only door, no Gamebryo cell sync
BP_LevelDoorsDispatcherManages multiple doors in a level, central dispatch
BP_DoorToUnpairedDoor that leads to an area with no paired Gamebryo cell
BP_LevelEntryDoor_Market / _SewerSpecialized variants for specific area types

PairingActors

BP_ReferenceHolder is a hidden actor (tick interval 100s, bHidden=true) that holds an array of VPairedPawn class references. This is the lookup mechanism Altar uses to find UE actors from Gamebryo references at runtime. Custom cells need a ReferenceHolder instance if they contain NPCs.

InteractibleObjects. Placeable Types

The complete set of interactible object Blueprints available for cell authoring:

  • BP_VChest variants (1/2/3 rotations and translations), containers
  • BP_VDoor variants (1/2 rotations + translation), interior doors
  • BP_Flora_InteractibleObjects, harvestable plants
  • BP_TortureCage_Parent, scripted cage object
  • BP_VMisc_ConditionalStatic, static with conditional visibility
  • BPE_InteractibleObjectList, list/collection of interactibles

Interior Map Structure bridge

Source: L_BrumaMain all variants confirmed, complete architecture Six JSON dumps covering base + all 5 sub-levels. The streaming architecture is now fully understood.

The Base Level is the Streaming Parent

This is the critical architectural fact: the base L_[CellName].umap is not just one of many equal files, it is the parent level that streams in all variants via LevelStreamingAlwaysLoaded. The sub-levels are never loaded directly by MagicLoader or the player, they are loaded as streaming sub-levels of the base.

L_BrumaMain.umap ← BASE (parent, loaded by CellsToMapPath)
 StreamingLevels (all LevelStreamingAlwaysLoaded):
 → L_BrumaMain_Del.umap (color: #15FF00 green)
 → L_BrumaMain_Env.umap (color: #F1FF00 yellow)
 → L_BrumaMain_Li.umap (color: #FF00DE pink)
 → L_BrumaMain_VFX.umap (color: #0066FF blue)
 → L_BrumaMain_SD.umap (color: #FF007F red)
CK Cell Editor. Critical Architecture Fix MagicLoader only registers the base level in CellsToMapPath. But the base level must also contain the LevelStreamingAlwaysLoaded entries for all 5 sub-levels, otherwise they never load. The CK must generate ALL six files AND populate the base level's StreamingLevels array. This is why MagicLoader-generated cells often have broken lighting and missing VFX: the sub-levels aren't being streamed in.

What Each Sub-Level Actually Contains

Sub-levelActors in L_BrumaMainPurpose
Base (_none)AltarWorldSettings, InstancedFoliageActor, LevelInstance (LI_RVT_Interiors), NavMeshBoundsVolume, RecastNavMesh-HumanoidsGameplay actors, nav mesh, foliage, streaming parent
_DelAltarWorldSettings onlyDestructibles, empty stub in Bruma (no destructibles)
_EnvAltarWorldSettings onlyEnvironment geometry, empty in this dump (geometry is packed separately)
_LiAltarWorldSettings, SkyLight, PostProcessVolume (unbound), ExponentialHeightFogLighting, the most content-rich sub-level
_SDAltarWorldSettings, VAmbientSoundShadows/decals + ambient sound
_VFXAltarWorldSettings onlyParticle effects, empty stub in Bruma

Key Actors Confirmed in Each Sub-Level

Base Level. Navigation & Foliage

  • NavMeshBoundsVolume, defines the navmesh bake area for NPC pathfinding. Named RecastNavMesh-Humanoids. RuntimeGeneration: DynamicModifiersOnly. Without this, NPCs can't navigate the cell.
  • InstancedFoliageActor, holds all foliage/grass instances
  • LevelInstance pointing to LI_RVT_Interiors. Runtime Virtual Texture for interiors (referenced in PlaygoChunk_OtherMaps)
  • VAltarNavigationSystemModuleConfig. Altar's custom nav system config

_Li Sub-Level. Lighting Setup (Confirmed)

  • SkyLight, source type SLS_SpecifiedCubemap, cubemap: GrayDarkTextureCube (engine default dark), Intensity: 2.0, Mobility: Movable
  • PostProcessVolume, bUnbound=true (affects entire level), has film/bloom/exposure overrides
  • ExponentialHeightFog. FogDensity: 0.025, VolumetricFog enabled, ScatteringDistribution: 0.4, ExtinctionScale: 0.9

_SD Sub-Level. Ambient Sound Lives Here

VAmbientSound actor is in _SD, not _VFX as might be expected. The ambient sound for a cell is placed in the shadow/decal sub-level. Custom cells needing ambient audio should add VAmbientSound to their _SD sub-level.

LevelStreamingAlwaysLoaded. The Critical Asset

Each streaming entry in the base level is a LevelStreamingAlwaysLoaded object. Unlike LevelStreamingKismet, these load immediately and stay loaded for the lifetime of the parent. The sub-levels have distinct editor colors (purely visual in UE editor, not functional).

AltarWorldSettings. Custom World Settings

All levels use AltarWorldSettings (subclass of AWorldSettings) rather than standard UE WorldSettings. This contains Altar-specific configuration. BookmarkArray has 10 null slots (editor bookmarks). NavigationSystemConfig references the nav system config object.

Complete CK Cell Editor Output Spec (Updated)

Content/Maps/[ModName]/
 L_[CellName].umap ← BASE: streaming parent
 Must contain:
 - AltarWorldSettings
 - NavMeshBoundsVolume (for NPC pathfinding)
 - RecastNavMesh-Humanoids
 - LevelStreamingAlwaysLoaded × 5 (pointing to sub-levels)
 - LevelInstance → LI_RVT_Interiors (for RVT)
 - InstancedFoliageActor (if foliage needed)

 L_[CellName]_Del.umap ← AltarWorldSettings only (stub)
 
 L_[CellName]_Env.umap ← AltarWorldSettings + static mesh actors
 
 L_[CellName]_Li.umap ← AltarWorldSettings + SkyLight + PostProcessVolume + ExponentialHeightFog
 
 L_[CellName]_Li_BuiltData.uasset ← Generated by Lightmass bake (optional for dynamic lighting)
 
 L_[CellName]_SD.umap ← AltarWorldSettings + VAmbientSound (if ambient audio needed)
 
 L_[CellName]_VFX.umap ← AltarWorldSettings only (stub, add Niagara emitters for FX)

Interior Cell Path

Bruma's interiors are at /Game/Maps/Interiors/, not Maps/VerticalSlice/ as initially assumed from the VerticalSlice screenshot. VerticalSlice contains the tutorial/starting dungeon cells. City interiors live in Maps/Interiors/. The CK should let modders choose the output path.

Engine Internals

CosmicBridge Runtime & the CK

The plan: a persistent UE4SS Lua mod (CosmicBridge) exposing a clean API for every proven engine capability, plus a Rust/egui Construction Kit that unifies xEdit, the CS, TSMI and MagicLoader into one author-build-deploy pipeline.

The Goal Build the Oblivion Remastered Construction Kit independently. No Bethesda support, no Altar source access, no waiting. Everything the community needs to create cells, NPCs, voiced dialogue, custom quests, and entirely new gameplay, unified into a single authoring tool backed by a runtime Lua API that sits between both engines.

Why This Exists

Oblivion Remastered runs on two engines simultaneously. The original Gamebryo handles all game logic. AI, quests, items, scripting. Unreal Engine 5 handles all rendering, audio, and visual systems. Altar Studios (Virtuous) wrote a proprietary bridge layer that translates between them at runtime.

The modding community has reverse-engineered enough of that bridge to build tools, but those tools are fragmented: TSMI handles item mapping, MagicLoader handles cell loading, xEdit handles ESP authoring, retoc handles asset packing. None of them talk to each other. The CosmicBridge CK unifies all of them.

The Four Deliverables

LayerRole
CosmicBridge RuntimeUE4SS Lua mod. Persistent. Loaded first. Clean API for all other mods. Replaces raw UE4SS scripting for 95% of use cases.
CK Authoring GUIRust + egui desktop app. Replaces xEdit + CS + MagicLoader + TSMI for NPC, dialogue, item, and cell workflows. Generates all output files.
Build PipelineWraps retoc, UnrealPak, Wwise cook, and Audio2Face into one click. Author → Build → Deploy.
CK Bible (This Doc)The complete reverse-engineered spec. Everything confirmed by probe sessions, FModel dumps, and live runtime analysis.

CosmicBridge Architecture cosmicbridge

What CosmicBridge Is A persistent UE4SS Lua mod that loads first, exposes a clean modder-facing API for all proven engine capabilities, and serves as the platform every other mod builds on. require("CosmicBridge") and you have everything.

Module Structure

ue4ss/Mods/CosmicBridge/Scripts/
 main.lua ← entry point, loads all modules, exports global Bridge
 modules/
 DialogueAPI.lua
 AudioAPI.lua
 CombatAPI.lua
 WorldAPI.lua
 ActorAPI.lua
 LipSyncAPI.lua ← pending speech2face property dump
 utils/
 safe.lua ← pcall wrappers, IsValid guards
 find.lua ← FindNearestActor, FindByClass helpers

Usage Pattern for Dependent Mods

local Bridge = require("CosmicBridge")

-- Custom voiced NPC greeting
Bridge.Dialogue.onLine(function(speaker, subtitle)
 if speaker == "MyCustomNPC" then
 Bridge.Audio.playVoice(
 "/Game/WwiseAudio/Events/Voice/custom/MyNPC/Play_Greeting",
 speaker
 )
 end
end)

-- Custom combat reaction
Bridge.Combat.onLightAttack(function(attacker)
 if attacker == Bridge.Actor.getPlayer() then
 Bridge.Audio.playAt("/Game/WwiseAudio/Events/custom/SwingFX", attacker)
 end
end)

Implementation Status

ModuleStatusBlocking issue
DialogueAPIready to writeNone, all hooks proven
AudioAPIready to writeInterior bank residency (non-blocking)
CombatAPIready to writeNone. AST_ hooks proven
WorldAPIready to writeNone. TimeStop proven
ActorAPIpartialGetName/GetFullName SEH issue limits actor identity
LipSyncAPIpendingVHumanoidHeadComponent property dump needed

DialogueAPI cosmicbridge

FunctionStatusDescription
Bridge.Dialogue.onStart(fn)provenCalls fn(speaker: string) when dialogue opens. Hooks AST_Dialogue:OnEntered. Speaker name may be empty, use onLine for reliable speaker identity.
Bridge.Dialogue.onLine(fn)provenCalls fn(speaker: string, subtitle: string) each time a new line of dialogue starts. Hooks GetSubtitle post-return with non-empty guard. Fires once per line change.
Bridge.Dialogue.onEnd(fn)provenCalls fn(speaker: string) when dialogue ends. Hooks AST_Dialogue:OnExited.
Bridge.Dialogue.getSpeaker()provenReturns current speaker name string or nil if not in dialogue.
Bridge.Dialogue.getSubtitle()provenReturns current subtitle string or nil.
Bridge.Dialogue.isActive()provenReturns bool. Uses VConversationIdleAnimInstance:IsInDialogue().

AudioAPI cosmicbridge

FunctionStatusDescription
Bridge.Audio.playAt(eventPath, actor)provenFires a Wwise event at the actor's world position via PostEventAtLocation. Returns playing_id (0 = bank not resident). Calls LoadAsset before posting.
Bridge.Audio.playVoice(eventPath, speakerName)provenSame as playAt but looks up the nearest NPC matching speakerName and posts at their location. Designed for dialogue audio replacement.
Bridge.Audio.playAtLocation(eventPath, x, y, z)provenPostEventAtLocation at explicit world coordinates.
Bridge.Audio.playAtPlayer(eventPath)provenPosts at player location. Base behavior from ZA WARUDO mod.
Bridge.Audio.preload(eventPath)provenCalls LoadAsset + StaticFindObject to warm the event into the object registry before play time. Eliminates first-play hitch.

CombatAPI cosmicbridge

All combat events hook the corresponding AST_Character state machine states. Callbacks receive the actor that entered the state.

FunctionStatusDescription
Bridge.Combat.onLightAttack(fn)readyHooks AST_CharacterActionLightAttack:OnEntered. fn(actor).
Bridge.Combat.onPowerAttack(fn)readyHooks AST_CharacterActionPowerAttack:OnEntered. fn(actor).
Bridge.Combat.onHit(fn)provenUses SendMeleeHitOnPairedPawn hook (proven in Mounted Combat). fn(attacker, victim, damage).
Bridge.Combat.applyDamage(actor, amount)provenUses actor:SendMeleeHitOnPairedPawn(). The only working damage method. UGameplayStatics::ApplyDamage is a no-op in OBR.
Bridge.Combat.onDeath(fn)readyHooks AST_CharacterDead:OnEntered. fn(actor).
Bridge.Combat.onMounted(fn)provenHooks AST_CharacterDocked:OnEntered. fn(rider, mount). Proven in Mounted Combat work.

WorldAPI cosmicbridge

FunctionStatusDescription
Bridge.World.freeze()provenSloMo 0.001 + player CustomTimeDilation compensation. From ZA WARUDO.
Bridge.World.unfreeze()provenSloMo 1 + restore player dilation.
Bridge.World.setTimeDilation(factor)provenSet global time dilation. 1.0 = normal, 0.5 = half speed, 0.001 = near freeze.
Bridge.World.getPlayer()provenReturns player pawn. Note: returns horse when mounted.
Bridge.World.getPlayerLocation()provenReturns {X, Y, Z} table of player world position.
Bridge.World.console(cmd)provenExecute a console command via KismetSystemLibrary. From ZA WARUDO.

ActorAPI cosmicbridge

FunctionStatusDescription
Bridge.Actor.findNearest(className, maxDist)provenFindAllOf + distance sort using K2_GetActorLocation. Returns nearest valid actor. Safe, never calls GetName.
Bridge.Actor.getHealth(actor)provenReads actor value index 10 (Health). Confirmed in Mounted Combat work.
Bridge.Actor.setHealth(actor, value)provenSets actor value index 10. Use for guaranteed kill (set to 0) or healing.
Bridge.Actor.isMounted()provenScans for BP_Generic_Horse_C instances, checks if player pawn matches. From Mounted Combat.
Bridge.Actor.fadeOut(actor)readyCalls BPI_NPC_CustomFadeOut:CustomFadeOut() on NPC. Confirmed interface exists.

CK Tool Architecture construction kit

Implementation Stack Rust + egui desktop application. Single executable. Reads/writes ESP directly (commonlibob64 structs), generates UE assets as JSON for retoc, runs Wwise cook and Audio2Face as subprocesses, patches CosmicBridge config for runtime injection.

The Three Tool Layers

LayerRole
Authoring GUINPC Editor, Dialogue Editor, Cell Editor, Item Editor. Replaces xEdit + Construction Set + MagicLoader + TSMI for all common workflows. Written in Rust + egui.
Build PipelineOne-click: ESP → retoc (uassets) → UnrealPak (audio) → Wwise cook → Audio2Face (facial anims) → final pak structure. Configurable per-mod.
Runtime CosmicBridgeUE4SS Lua mod. Persistent. Handles TSMI/MagicLoader injection via the same DataTable patching mechanism, plus all modder-facing APIs.

What the CK Replaces

Current toolCK equivalentImprovement
xEdit (ESP authoring)CK NPC/Item/Dialogue editorsGUI with live validation, dual-engine awareness
Construction Set (cell layout)CK Cell EditorGenerates all 6 umap variants + DataTable entries simultaneously
TSMI (SyncMap injection)CosmicBridge runtimeUnified, no separate tool run, aware of all mod types
MagicLoader (cell loading)CosmicBridge runtimeGenerates all 6 map variants, not just base
retoc + UnrealPak (packing)CK Build PipelineOne click, correct structure guaranteed
Wwise + Audio2Face (voice)CK Build PipelineAutomated from WAV input to final pak

Dialogue Authoring Module construction kit

Inputs → Outputs

InputProcessOutput
Dialogue textESP record generation.esp with TESTopicInfo FormID
Voice WAV file (44100hz mono 16-bit)Wwise cook pipeline.bnk + .wem + AkAudioEvent.uasset
Voice WAV fileAudio2Face batchFacial AnimSequence.uasset
NPC race + sex + voice typeTESTopicInfo authoringKeyed AkAudioEvent + Animation entries
Dialogue FormIDTSMI mapping generationSyncMap patch entry

TESTopicInfo Asset Template

{
 "Type": "TESTopicInfo",
 "Name": "[TopicType]_[ModName]_[FormID]",
 "Properties": {
 "Responses": [{
 "ResponseID": 1,
 "Text": "[your dialogue text]",
 "AkAudioEvents": [{
 "Key": {
 "Sex": "ECharacterSex::[MALE|FEMALE]",
 "Race": "/Game/Forms/actors/race/[Race].[Race]",
 "VoiceType": "EVVoiceType::LEGACY"
 },
 "Value": {
 "AssetPathName": "/Game/WwiseAudio/Events/Voice/oblivion/[race]/[m|f]/[EventName].[EventName]"
 }
 }],
 "Animations": [{
 "Key": {
 "Sex": "ECharacterSex::[MALE|FEMALE]",
 "Race": "/Game/Forms/actors/race/[Race].[Race]"
 },
 "Value": {
 "AssetPathName": "/Game/Art/Animation/Humanoid/Facial/[VoiceSet]/[AnimName].[AnimName]"
 }
 }]
 }],
 "m_formID": [decimal FormID],
 "m_formEditorID": "[TopicEditorID]",
 "m_formType": "FormID::INFO_ID"
 }
}

Voice Line Naming Convention

Wwise event: Play_[race]_[gender]_[questid]_[topic]_[formid]_[index]
AnimSequence: A_[questid]_[topic]_[formid]_[index]
UE event path: /Game/WwiseAudio/Events/Voice/oblivion/[race]/[m|f]/[EventName]
UE anim path: /Game/Art/Animation/Humanoid/Facial/[VoiceSet]/[AnimName]
Bank path: Event/English(US)/[EventName].bnk
Media path: Media/English(US)/[MediaId].wem

Cell Editor Module construction kit

What a Complete Custom Interior Cell Requires

  1. Gamebryo side: Cell record in ESP with EditorID, region, lighting data
  2. Six UE map files: base + _Del + _Env + _Li + _SD + _VFX (currently MagicLoader only generates base)
  3. CellsToMapPath entry: EditorID → /Game/Maps/[YourMap]/[YourMap].umap
  4. BP_LevelEntryDoorObv instance in source cell pointing to new cell
  5. BP_ReferenceHolder instance in new cell (if NPCs are placed)

CK Cell Editor Output (per new cell)

Content/Maps/[ModName]/
 [CellName].umap ← actors + interactive objects
 [CellName]_Del.umap ← destructibles stub
 [CellName]_Env.umap ← static geometry
 [CellName]_Li.umap ← lighting
 [CellName]_SD.umap ← shadow/decal stub 
 [CellName]_VFX.umap ← VFX stub

Content/Dev/ObvData/Data/
 [ModName].esp ← CELL record with EditorID matching CellsToMapPath key

CosmicBridge/config/
 cells.json ← CellsToMapPath patch entries for runtime injection

NPC Editor Module construction kit

Custom NPC Checklist

LayerAssetTool
brainNPC record in ESP (race, class, factions, AI, voice type)CK NPC Editor → generates ESP record
bridgeSyncMap entry: FormID → Blueprint pathCosmicBridge runtime injection
visualBlueprint (duplicate BP_Generic_NPC, set phenotype)Altar UE project or CK stub generator
visualPhenotypePreset (face morphs, skin params)VCharacterPhenotypePreset asset. FaceMorphsSource_Human + SkinParameterCollection_Humans
bridgeAllRaceModifications entry (if custom race)CosmicBridge runtime injection

Phenotype System

Character appearance is controlled by VCharacterPhenotypePreset assets. The base preset (PhenotypePreset_BASE) references:

  • FaceMorphsSource_Human, at /Game/Dev/Phenotypes/MorphSources/, defines face shape morph targets
  • SkinParameterCollection_Humans, at /Game/Dev/Phenotypes/SkinParameterCollections/, skin tone, texture parameters

Custom NPCs need their own PhenotypePreset or can reference an existing racial preset.

Voice Type Enum (Confirmed)

EVVoiceType::LEGACY ← vanilla Oblivion voice types (altvoice, beggar, etc.)
ECharacterSex::MALE
ECharacterSex::FEMALE

Build Pipeline construction kit

Complete Build Flow

Author in CK GUI
 │
 ├── ESP records ──────────────────────→ [ModName].esp
 │
 ├── UAssets (TESTopicInfo, Blueprints) → retoc → [ModName]_P.pak
 │
 ├── Voice WAVs ──→ Wwise cook ─────→ .bnk + .wem
 │ └─ Audio2Face ────→ AnimSequence.uasset → retoc
 │ → [ModName]_Audio_P.pak (loose bnk/wem)
 │
 ├── Maps ─────────────────────────→ 6x .umap per cell → retoc → [ModName]_Maps_P.pak
 │
 └── CosmicBridge config ─────────→ cells.json + syncmap.json (runtime injection)

Pak Naming Rules

PakContentsTool
[Mod]_P.pakCooked uassets (TESTopicInfo, AkAudioEvent, Blueprints)retoc
[Mod]_Audio_P.pakLoose .bnk and .wem filesUnrealPak filelist
[Mod]_Maps_P.pakCooked .umap files (all 6 variants)retoc

Load Order in Plugins.txt

Oblivion.esm
AltarESPMain.esp
AltarDeluxe.esp
[YourMod].esp ← MUST be above AltarDeluxe.esp
AltarESPLocal.esp

Engine Internals

Probe Log & Dead Ends

Primary-source research output: confirmed findings from live UE4SS probe sessions and FModel dumps, the approaches that were fully investigated and do not work (so no one repeats them), and the open questions still being chased.

Probe Session Log research

Session: 2026-06-12. Dialogue & Lip Sync Research

Full UE4SS probe session using live game + FModel dumps. Jauffre and Chorrol Guard test NPCs.

Confirmed Findings

FindingMethodStatus
All four dialogue hooks register successfullyRegisterHook on AST_Dialogue + VDialogueMenuViewModelconfirmed
SetVDialogueMenuViewModel fires first, data emptyProbe v6 hook firing orderconfirmed
OnStateUpdate fires every frame during dialogueProbe v7 poll loopconfirmed
GetSpeakerName():ToString() returns "Jauffre"Probe v5 FText:ToString()confirmed
GetSubtitle():ToString() returns line textProbe v5confirmed
playing_id > 0 outdoors (Chorrol Guard)Probe v7, id=498,500,501,504,505,514confirmed
playing_id = 0 indoors (Jauffre)All probe versionsconfirmed
speech2face does NOT respond to PostEventAtLocationProbe v5 Attempt D, watched mouth, no movementconfirmed
VAltarAkComponent has no custom PostEvent wrapperProbe v2 UFunction scan, only 2 functionsconfirmed
VHumanoidHeadComponent has no speech UFunctionsProbe v2, 6 functions, none speech-relatedconfirmed
TESTopicInfo stores AnimSequence path for lip syncAbilities_000151f2.json FModel dumpconfirmed
Lip sync is baked AnimSequence, not speech2faceTESTopicInfo Animations arrayconfirmed

Confirmed Dead Ends research

Record These. Don't Repeat Them These are approaches that were fully investigated and confirmed not viable. Document the exact reason so no one wastes time on them again.
ApproachWhy it failsInvestigation cost
actor:GetName() on NPC actorsSEHs through pcall, native nullptr dereference. Never call on actors returned by FindAllOf.3 crash sessions
K2_GetComponentsByClass(nil)Passing nil to native UFunction SEHs through pcall1 crash session
Direct property access: npc.AudioComponentReturns userdata that IsValid()=false. Property exists but UE4SS can't resolve the instance ptr.2 probe sessions
GetComponentByClass → GetName()GetComponentByClass "finds" the component but returns a null ptr. GetName() SEHs1 probe session
AkComponent:PostEvent(event)Requires OnAkPostEventCallback delegate parameter. UE4SS cannot construct delegates1 probe session
AkGameplayStatics:GetAkComponentHas Out-param bool (ComponentCreated) UE4SS can't satisfy, "Tried storing reference to a Lua table for an Out parameter"1 probe session
speech2face via ambient audioConfirmed not audio-driven. No mouth movement on PostEventAtLocation regardless of audio content.Full probe session v1-v7
UGameplayStatics::ApplyDamageConfirmed no-op in OBR, returns immediately without applying damageMounted combat research
LoadAsset for new-path assetsSilent failure. LoadAsset only works for assets on known mount pathsMounted combat research

Open Research Questions research

QuestionWhy it mattersAttack vector
What drives speech2face lip sync?RESOLVEDABP_HumanoidHead AnimGraph confirmed: DefaultSlot AnimSequence playback. No Wwise RTPC, no audio analysis. Must provide A_ AnimSequence in TESTopicInfo.Animations. See Head ABP page.
Why does bank not load in interiors?Custom audio only works outdoorsCompare pak mount triggers between interior/exterior level loads. L_PersistentDungeon confirmed as Jauffre's level, may use different pak mount path.
VHumanoidHeadComponent properties during dialogue?May reveal speech2face input property, settable from LuaProperty dump probe mid-dialogue, compare silent vs speaking state
Can vanilla NPC voice be muted?Full audio replacement requires silencing originalAkGameplayStatics:StopActor on NPC, untested, needs probe
SpawnAkComponentAtLocation full signature?Could provide component-attached audio without GetAkComponentExpects 8 params, need to discover remaining 2 args
GetOrCreateAkComponent Out-param workaround?Would give direct component reference for audio postingUE4SS Out-param table passing, research UE4SS docs for Out-param syntax
CellsToMapPath DataTable direct write from Lua?Would allow runtime cell registration without MagicLoaderFind DataTable object via StaticFindObject, probe write methods
Multiplayer mod state sync compatibility?CosmicBridge should be the platform for multiplayer mods tooReach out to multiplayer mod team. CosmicBridge actor state APIs may already be what they need