Repository navigation
UEVR Weapon Trace Fix Plugin
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\
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.
- Player pulls trigger
- Game calls
GetShotOrigin()on theFireModeobject → returns eye/camera position - Bullet trace starts at camera → wrong hit detection, especially at close range
- Hook
GetShotOrigin()on the FireMode class (Blueprint function) - In the pre-callback, navigate:
Pawn → EquipmentComponent → Weapon → FPV Mesh - Call
GetSocketLocation("MuzzleFlashSocket")on the FPV weapon mesh - Override the return value with the real muzzle world position
- Game uses the muzzle position for the bullet trace
The template for this plugin is ReturnalPlugin by jbusfield. Study it first.
Location: D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\main.cpp
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
// 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", ¶ms);
// 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 (Returnal): FVector = 3×
float→ useglm::fvec3,UEVR_Vector3f -
UE5 (OW2, most modern games): FVector = 3×
double→ useglm::dvec3,UEVR_Vector3d
Full UEVR MCP reference: see
uevr_mcp_discovery_process.md
The live discovery answers three questions:
- What is the muzzle socket name?
- What is the property path from pawn to weapon mesh?
- What is the function to hook?
$player = Invoke-RestMethod -Uri "http://localhost:8899/api/player"
$player.pawn.class # → "IndianaPlayerCharacter_BP_C"# 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" } }# 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" | ...# 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# 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| 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) |
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)
Copy-Item -Recurse "D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\uevr" ".\NewPlugin\src\uevr"
Copy-Item -Recurse "D:\Downloads\ReturnalPlugin\ReturnalPlugin\src\glm" ".\NewPlugin\src\glm"Full source: C:\Users\xaero\.gemini\antigravity-ide\scratch\OW2Plugin\src\main.cpp
// 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", ¶ms);
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() };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::FNamefor NameProperty,glm::dvec3for FVector (UE5)
use_native = true in hook_bp_fn:
- Sets
FUNC_Nativeflag (0x400) on the UFunction - Without this, the pre-callback doesn't fire before the Blueprint body executes
- Always
truefor 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
nullptrbefore writing (some code paths don't provide a return slot)
<ConfigurationType>DynamicLibrary</ConfigurationType> <!-- Must be DLL -->
<PlatformToolset>v142</PlatformToolset> <!-- VS 2019+ toolset -->
<LanguageStandard>stdcpp17</LanguageStandard> <!-- C++17 required -->
<AdditionalIncludeDirectories>$(ProjectDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>- Open
OW2Plugin.sln - Right-click solution → Retarget Projects (upgrades from v142 to current toolset) → OK
- Set configuration to Release | x64
- Build → Build OW2Plugin (Ctrl+B)
- Output:
x64\Release\OW2Plugin.dll
| 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 |
# 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" -ForceFor Outer Worlds 2:
Copy-Item "x64\Release\OW2Plugin.dll" `
"$env:APPDATA\UnrealVRMod\TheOuterWorlds2-WinGDK-Shipping\plugins\OW2Plugin.dll" -ForceIn the UEVR log (or DebugView): look for:
OW2Plugin: on_initialize
OW2Plugin: hooked BlueprintGeneratedClass...::GetShotOrigin -> OK
OW2Plugin: hooks installed
Use UEVR MCP to find the zero-parameter function that returns the bullet origin. Common names across games:
-
GetBulletOutLocation(Returnal) -
GetShotOrigin(Outer Worlds 2) GetAimingTraceStartLocationGetFireLocation-
GetMuzzleLocation(but often has params — skip to one that's parameterless)
Walk from pawn to weapon mesh using UEVR MCP summary + field exploration.
Common property names: CurrentGun, EquippedWeapon, WeaponMesh, GunMesh, FPVMesh
Read the Skeleton's Sockets array. Common socket names:
-
muzzle(Returnal — lowercase) -
MuzzleFlashSocket(OW2 — note casing) -
Muzzle_Socket,MuzzleSocket,barrel_end
- 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
// 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", ¶ms)
// 4. Socket name
API::FName InSocketName{ L"YourSocketName" };Add to the pre-callback:
static int count = 0;
if (++count <= 5) API::get()->log_info("OW2Plugin: GetShotOrigin hook fired #%d", count);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);
}
}- Verify the class path is exactly right (check via UEVR MCP
/api/explorer/type) - Try
use_native = false— some already-native functions don't need the flag set - Try hooking a post-callback instead of pre
- You may be hooking the wrong function — find the one that actually changes hit position
- Check if the weapon class inherits from a C++ base — the hook must be on the class that owns the function
- Verify
return truein the pre-callback (notreturn false)
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.