Skip to content

UEVR Weapon Trace Fix Plugin

letmein edited this page Jun 18, 2026 · 2 revisions

UEVR Weapon Trace Fix Plugin — Complete Creation Guide

Documented from the Outer Worlds 2 plugin (OW2Plugin) created June 2026. Reference implementation: C:\Users\.gemini\antigravity-ide\scratch\OW2Plugin\ Prior art / template: D:\Downloads\ReturnalPlugin\ReturnalPlugin\


Overview

In VR, UEVR places the camera at the HMD position. This means weapon fire traces originate from the camera (eye position), not the physical gun barrel. The result is that bullets appear to fire from your face, not your hand. The fix is a UEVR plugin DLL that intercepts the game's "where does this bullet start" function and overrides its return value with the actual muzzle socket position from the weapon mesh.


Part 1: Understanding the Problem

What happens without the fix

  1. Player pulls trigger
  2. Game calls GetShotOrigin() on the FireMode object → returns eye/camera position
  3. Bullet trace starts at camera → wrong hit detection, especially at close range

What the fix does

  1. Hook GetShotOrigin() on the FireMode class (Blueprint function)
  2. In the pre-callback, navigate: Pawn → EquipmentComponent → Weapon → FPV Mesh
  3. Call GetSocketLocation("MuzzleFlashSocket") on the FPV weapon mesh
  4. Override the return value with the real muzzle world position
  5. Game uses the muzzle position for the bullet trace

Part 2: Prior Art — ReturnalPlugin

The template for this plugin is ReturnalPlugin by jbusfield. Study it first.

Location: D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\main.cpp

What ReturnalPlugin does

Hooks two Blueprint functions on Class /Script/Returnal.GunBase:

  • GetAimDir — overrides bullet direction with muzzle rotation
  • GetBulletOutLocation — overrides bullet spawn point with "muzzle" socket location

Key pattern

// The hook callback signature
bool my_hook_pre(UEVR_UFunctionHandle fn, UEVR_UObjectHandle obj, void* locals, void* result)
{
    // Navigate to mesh
    auto pawn = uevr::API::get()->get_local_pawn(0);
    auto gun = pawn->get_property<API::UObject*>(L"PropertyName");
    auto mesh = gun->get_property<API::UObject*>(L"MeshPropertyName");

    // Call GetSocketLocation
    struct {
        API::FName InSocketName{ L"muzzle" };
        glm::fvec3 ReturnValue{};   // UE4: float vectors
    } params;
    mesh->call_function(L"GetSocketLocation", &params);

    // Write result to hook output
    UEVR_Vector3f* out = (UEVR_Vector3f*)result;
    out->x = params.ReturnValue.x;
    // ...
    return false;  // false = don't skip original; true = skip original
}

// Registration
hook_bp_fn(L"Class /Script/Game.SomeClass", L"SomeBPFunction",
           my_hook_pre, nullptr, /*use_native=*/true);

UE4 vs UE5 critical difference

  • UE4 (Returnal): FVector = 3× float → use glm::fvec3, UEVR_Vector3f
  • UE5 (OW2, most modern games): FVector = 3× double → use glm::dvec3, UEVR_Vector3d

Part 3: Live Discovery with UEVR MCP

Full UEVR MCP reference: see uevr_mcp_discovery_process.md

The live discovery answers three questions:

  1. What is the muzzle socket name?
  2. What is the property path from pawn to weapon mesh?
  3. What is the function to hook?

Step 1: Find the pawn class name

$player = Invoke-RestMethod -Uri "http://localhost:8899/api/player"
$player.pawn.class   # → "IndianaPlayerCharacter_BP_C"

Step 2: Navigate pawn → weapon → mesh

# Pawn properties
$s = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/summary?address=$($player.pawn.address)"
$s.fields.PSObject.Properties | Where-Object { $_.Name -match "Equip|Weapon|Inventory" }
# Found: PlayerEquipmentComponent: ObjectProperty → PlayerEquipmentComponent @ 0x...

# Equipment component → weapon
$equipAddr = "0x..." # from above
$s2 = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/summary?address=$equipAddr"
$s2.fields.PSObject.Properties | Where-Object { $_.Name -match "Weapon|Equip" }
# Found: EquippedWeapon: ObjectProperty → Weapon_PI_Standard_Unique_C @ 0x...

# Weapon → FPV mesh
$weaponAddr = "0x..."
$r = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/method?address=$weaponAddr&methodName=GetFPVMeshComponent"
# Returns: { result: { address: "0x...", className: "SkeletalMeshComponent" } }

Step 3: Find socket names

# From FPV mesh → SkeletalMesh asset → Skeleton asset → Sockets array
$meshAddr = "0x..."
$s = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/summary?address=$meshAddr"
$skMeshAddr = ($s.fields.PSObject.Properties | Where-Object { $_.Name -eq "SkeletalMesh" }).Value
# Parse address from string like "ObjectProperty -> SkeletalMesh @ 0x..."

# Skeleton from SkeletalMesh
$sk = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/field?address=$skMeshAddr&fieldName=Skeleton"
$skelAddr = $sk.value.address

# Read sockets array
$arr = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/array?address=$skelAddr&fieldName=Sockets&limit=20"
foreach ($socket in $arr.elements) {
    $s = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/summary?address=$($socket.address)"
    $name = $s.fields.PSObject.Properties | Where-Object { $_.Name -eq "SocketName" }
    $bone = $s.fields.PSObject.Properties | Where-Object { $_.Name -eq "BoneName" }
    Write-Host "$($name.Value) | $($bone.Value)"
}
# Output:
# NameProperty = "MuzzleFlashSocket" | NameProperty = "barrel_JNT"    ← USE THIS
# NameProperty = "MuzzleFLashSocketSilencer" | NameProperty = "barrel_JNT"
# NameProperty = "Barrel_Socket" | ...

Step 4: Find the function to hook

# Get all methods on the FireMode base class
$typeName = [System.Uri]::EscapeDataString("BlueprintGeneratedClass /Game/Blueprints/WEAP/WeaponModes/FireMode_Base.FireMode_Base_C")
$result = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/type?typeName=$typeName"
$result.methods | Where-Object { $_.name -match "Shot|Origin|Aim|Fire" }
# Found: GetShotOrigin (owner: RangedMode, params: [], returnType: StructProperty) ← ZERO PARAMS, PERFECT

Step 5: Verify the socket position matches GetShotOrigin

# Call GetShotOrigin on a live FireMode instance
$fireModeAddr = "0x..."  # from objects_by_class on FireMode_Handgun_C
$r = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/method?address=$fireModeAddr&methodName=GetShotOrigin"
Write-Host "GetShotOrigin: $($r.result.x), $($r.result.y), $($r.result.z)"

# Call GetSocketLocation on the FPV mesh
$body = @{ address = $meshAddr; methodName = "GetSocketLocation"; args = @{ InSocketName = "MuzzleFlashSocket" } } | ConvertTo-Json -Compress
$r2 = Invoke-RestMethod -Uri "http://localhost:8899/api/explorer/method" -Method POST -Body $body -ContentType "application/json"
Write-Host "MuzzleSocket: $($r2.result.x), $($r2.result.y), $($r2.result.z)"
# Positions should be close (within ~30 Unreal Units) = confirmed correct socket

Part 4: OW2 Discovery Results

Item Value
Muzzle socket MuzzleFlashSocket (bone: barrel_JNT)
Silencer socket MuzzleFLashSocketSilencer (game has this typo)
Pawn → equipment pawn.PlayerEquipmentComponent (property)
Equipment → weapon equip.EquippedWeapon (property)
Weapon → mesh weapon.GetFPVMeshComponent() (method call)
Hook class path BlueprintGeneratedClass /Game/Blueprints/WEAP/WeaponModes/FireMode_Base.FireMode_Base_C
Hook function GetShotOrigin (zero params, returns StructProperty / FVector)
Secondary hook GetAimingDirection (returns direction vector)

Part 5: Project Structure

OW2Plugin/
├── OW2Plugin.sln           ← Open this in Visual Studio
├── OW2Plugin.vcxproj       ← Project file (x64 DLL, C++17)
└── src/
    ├── main.cpp            ← The entire plugin (single file)
    ├── uevr/               ← UEVR Plugin SDK headers (copied from ReturnalPlugin)
    │   ├── API.h
    │   ├── API.hpp
    │   └── Plugin.hpp
    └── glm/                ← GLM math library (header-only, copied from ReturnalPlugin)

How to copy the SDK headers for a new project

Copy-Item -Recurse "D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\uevr" ".\NewPlugin\src\uevr"
Copy-Item -Recurse "D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\glm"  ".\NewPlugin\src\glm"

Part 6: The Plugin Code — Explained

Full source: C:\Users\xaero\.gemini\antigravity-ide\scratch\OW2Plugin\src\main.cpp

Core structure

// 1. Include SDK
#include "uevr/Plugin.hpp"
#include "glm/glm.hpp"
using namespace uevr;

// 2. hook_bp_fn helper — same in every plugin
bool hook_bp_fn(wstring_view class_name, wstring_view fn_name,
                UEVR_UFunction_NativePreFn pre, UEVR_UFunction_NativePostFn post,
                bool use_native)
{
    auto cls = (API::UClass*)API::get()->find_uobject(class_name);
    auto fn = cls->find_function(fn_name);
    uint32_t flags = fn->get_function_flags();
    if (use_native) flags |= 0x400;  // FUNC_Native — makes pre-callback fire
    fn->set_function_flags(flags);
    return API::get()->param()->sdk->ufunction->hook_ptr((UEVR_UFunctionHandle)fn, pre, post);
}

// 3. Navigation helper
bool get_muzzle_location(glm::dvec3& out)
{
    auto pawn = API::get()->get_local_pawn(0);
    auto equip = pawn->get_property<API::UObject*>(L"PlayerEquipmentComponent");
    auto weapon = equip->get_property<API::UObject*>(L"EquippedWeapon");
    if (!weapon) return false;  // unarmed

    struct { API::UObject* ReturnValue = nullptr; } fpv;
    weapon->call_function(L"GetFPVMeshComponent", &fpv);
    if (!fpv.ReturnValue) return false;

    // UE5: FVector is double, FName in params struct, then return value
    struct {
        API::FName InSocketName{ L"MuzzleFlashSocket" };
        glm::dvec3 ReturnValue{};
    } params;
    fpv.ReturnValue->call_function(L"GetSocketLocation", &params);

    out = params.ReturnValue;
    return true;
}

// 4. Hook callback
bool on_get_shot_origin_pre(UEVR_UFunctionHandle fn, UEVR_UObjectHandle obj,
                             void* locals, void* result)
{
    if (!result) return false;
    glm::dvec3 muzzle{};
    if (!get_muzzle_location(muzzle)) return false;

    auto out = reinterpret_cast<UEVR_Vector3d*>(result);  // UE5 = double
    out->x = muzzle.x; out->y = muzzle.y; out->z = muzzle.z;
    return true;  // true = skip original function body
}

// 5. Plugin class
class OW2TraceFix : public uevr::Plugin {
public:
    void on_initialize() override {
        hook_bp_fn(
            L"BlueprintGeneratedClass /Game/Blueprints/WEAP/WeaponModes/FireMode_Base.FireMode_Base_C",
            L"GetShotOrigin",
            on_get_shot_origin_pre, nullptr, true);
    }
};

// 6. Global singleton — self-registers on DLL load
std::unique_ptr<OW2TraceFix> g_plugin{ new OW2TraceFix() };

Critical details

call_function params struct layout:

  • Fields in the struct map directly to the UFunction parameter list in order
  • Input params first, then return value last
  • Must match exact UE types: API::FName for NameProperty, glm::dvec3 for FVector (UE5)

use_native = true in hook_bp_fn:

  • Sets FUNC_Native flag (0x400) on the UFunction
  • Without this, the pre-callback doesn't fire before the Blueprint body executes
  • Always true for hooks where you want to override the return value

return true vs return false in pre-callback:

  • return true = skip the original function (you've handled it entirely)
  • return false = let the original run after your callback

result pointer:

  • Points to the return value slot in the UFunction's params buffer
  • Cast to the exact UE type: UEVR_Vector3f* for UE4, UEVR_Vector3d* for UE5
  • Check for nullptr before writing (some code paths don't provide a return slot)

Part 7: Build Setup

vcxproj key settings

<ConfigurationType>DynamicLibrary</ConfigurationType>   <!-- Must be DLL -->
<PlatformToolset>v142</PlatformToolset>                 <!-- VS 2019+ toolset -->
<LanguageStandard>stdcpp17</LanguageStandard>           <!-- C++17 required -->
<AdditionalIncludeDirectories>$(ProjectDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

Building with Visual Studio 2026 (current)

  1. Open OW2Plugin.sln
  2. Right-click solution → Retarget Projects (upgrades from v142 to current toolset) → OK
  3. Set configuration to Release | x64
  4. Build → Build OW2Plugin (Ctrl+B)
  5. Output: x64\Release\OW2Plugin.dll

Common build errors

Error Fix
MSB8020: build tools for v142 not found Right-click solution → Retarget Projects
C3668: override did not override any base class Remove that method — it doesn't exist in your SDK version
C4244: conversion from UEVR_TrackedDeviceIndex to float Harmless warning in API.hpp — ignore

Part 8: Installation

# Create plugins folder and copy DLL
$dest = "$env:APPDATA\UnrealVRMod\<GameExeName>\plugins"
New-Item -ItemType Directory -Force -Path $dest
Copy-Item "x64\Release\OW2Plugin.dll" "$dest\OW2Plugin.dll" -Force

For Outer Worlds 2:

Copy-Item "x64\Release\OW2Plugin.dll" `
  "$env:APPDATA\UnrealVRMod\TheOuterWorlds2-WinGDK-Shipping\plugins\OW2Plugin.dll" -Force

Verifying it loaded

In the UEVR log (or DebugView): look for:

OW2Plugin: on_initialize
OW2Plugin: hooked BlueprintGeneratedClass...::GetShotOrigin -> OK
OW2Plugin: hooks installed

Part 9: Adapting This to Another Game

Step 1: Find the hook target

Use UEVR MCP to find the zero-parameter function that returns the bullet origin. Common names across games:

  • GetBulletOutLocation (Returnal)
  • GetShotOrigin (Outer Worlds 2)
  • GetAimingTraceStartLocation
  • GetFireLocation
  • GetMuzzleLocation (but often has params — skip to one that's parameterless)

Step 2: Find the navigation path

Walk from pawn to weapon mesh using UEVR MCP summary + field exploration. Common property names: CurrentGun, EquippedWeapon, WeaponMesh, GunMesh, FPVMesh

Step 3: Find the socket name

Read the Skeleton's Sockets array. Common socket names:

  • muzzle (Returnal — lowercase)
  • MuzzleFlashSocket (OW2 — note casing)
  • Muzzle_Socket, MuzzleSocket, barrel_end

Step 4: Check UE version

  • UE4: UEVR_Vector3f, glm::fvec3 — 32-bit floats
  • UE5: UEVR_Vector3d, glm::dvec3 — 64-bit doubles
  • Check by looking at what the MCP returns: if coordinates are ~10000+ and precise they're likely doubles

Step 5: Update the three strings in main.cpp

// 1. Class path for hook registration
L"BlueprintGeneratedClass /Game/Path/To/YourClass.YourClass_C"
// Or for C++ native classes:
L"Class /Script/GameModule.ClassName"

// 2. Function name to hook
L"GetBulletOriginOrWhateverItsCalled"

// 3. Property names in navigation helper
pawn->get_property<API::UObject*>(L"YourWeaponPropertyName")
weapon->get_property<API::UObject*>(L"YourMeshPropertyName")
// or use call_function for methods:
weapon->call_function(L"GetWeaponMesh", &params)

// 4. Socket name
API::FName InSocketName{ L"YourSocketName" };

Part 10: Debugging Tips

Check if hook fires

Add to the pre-callback:

static int count = 0;
if (++count <= 5) API::get()->log_info("OW2Plugin: GetShotOrigin hook fired #%d", count);

Check if muzzle location is valid

glm::dvec3 muzzle{};
if (get_muzzle_location(muzzle)) {
    static bool logged = false;
    if (!logged) { logged = true;
        API::get()->log_info("Muzzle: %.1f, %.1f, %.1f", muzzle.x, muzzle.y, muzzle.z);
    }
}

If hook never fires (no log messages)

  1. Verify the class path is exactly right (check via UEVR MCP /api/explorer/type)
  2. Try use_native = false — some already-native functions don't need the flag set
  3. Try hooking a post-callback instead of pre

If bullets still fire from camera

  1. You may be hooking the wrong function — find the one that actually changes hit position
  2. Check if the weapon class inherits from a C++ base — the hook must be on the class that owns the function
  3. Verify return true in the pre-callback (not return false)

If GetFPVMeshComponent() returns null

The weapon may not have a visible FPV mesh in certain game states (e.g. third person, cutscenes). Add a null check — silently skip. The TPV mesh may also work as a fallback.