Skip to main content

Runtime and safety contract

RPS wraps an unsafe native engine boundary; it cannot turn arbitrary game memory into an ordinary application API. The framework makes the conditions for a safe call explicit and fails closed when it cannot prove them.

Exact runtime gate

RPS::Runtime::RuntimeModule::detect() checks the current process for:

  • the expected Fallout4VR.exe module;
  • a valid 64-bit PE image;
  • the exact supported file version, 1.2.72.0;
  • a usable module base and image extent.

The returned object is a value. Keep it in your integration service and pass it to subsystem APIs. resolve() and resolveFunction() return zero/null unless the module is ready and the symbol belongs to the supported image.

const auto module = RPS::Runtime::RuntimeModule::detect();
if (!module) {
switch (module.status()) {
case RPS::Runtime::ModuleStatus::ModuleUnavailable:
case RPS::Runtime::ModuleStatus::WrongExecutable:
case RPS::Runtime::ModuleStatus::InvalidPeImage:
case RPS::Runtime::ModuleStatus::UnsupportedRuntime:
// Disable the RPS-backed feature and report once.
break;
default:
break;
}
return;
}

Version metadata alone is not proof that a hook site or live object generation is safe. High-risk wrapper paths additionally validate executable targets, memory ranges, vtables, expected entry bytes, complete native records, or postconditions as appropriate.

Borrowed means synchronous

Most void* arguments are borrowed engine objects. RPS does not retain them unless the returned type explicitly says it owns a reference. The caller must keep the source object and its containing subsystem stable for the complete call.

Addresses copied into a result are diagnostics and identity witnesses. They do not grant permission to dereference the address later.

Common invalidation events include:

  • load, save, cell, or world transitions;
  • actor unload or process replacement;
  • graph/skeleton/controller reconstruction;
  • scene detach or object destruction;
  • hknp body removal and slot reuse;
  • renderer or audio-manager replacement;
  • plugin shutdown.

Thread and callback domains

Each API belongs to one of these execution domains:

DomainTypical APIsRule
Physics read/write epochPhysics::Api, body snapshots, shape casts, constraintsSupply the exact WorldReadGuard or WorldWriteGuard.
Physics callbackGenerator output and some world guardsKeep work bounded; never retain callback storage.
Game/frame thread outside physicsscene hierarchy, actor state/pathing, audio, lightsThe wrapper rejects an active or unreadable physics-step context.
Pure/value codecollision filters, transform math, pose blend helpersNo engine pointer is required; finite/bounds rules still apply.
Consumer-owned hookHooks and collision pair policyThe consumer owns thread quiescence, original-call chaining, and shutdown.

Do not move a call between domains just because it compiles. In particular, never call deliverPhysicsHit() from a contact callback: it constructs native hit data and dispatches Actor::HitMe synchronously on the owning game thread.

Units are part of the type contract

FO4VR physics uses both game units and Havok units. RPS names unit-bearing fields and functions where the boundary matters:

  • buildSphereGame() converts a game-unit radius;
  • buildSphereHavok() accepts a Havok-unit radius;
  • RayRequest and ShapeCastRequest use game-space positions/distances;
  • PrismaticConstraintInfo::minimumRelativeDistanceHavok and maximumRelativeDistanceHavok are already Havok units;
  • PhysicsImpactContact stores point and velocity in Havok space;
  • preparePhysicsImpactGeometry() converts them using an explicit scale.

Use readScaleSnapshot() when a feature needs current game/Havok conversion. Its fallback values are marked; do not silently treat a fallback as proof that VR-specific scale-dependent behavior is ready.

Finite and complete data

Native functions are not a substitute for input validation. RPS rejects:

  • NaN and infinite transforms, vectors, angles, forces, and time steps;
  • invalid body IDs, high-water marks, motion slots, and free records;
  • incomplete pointer chains or native arrays;
  • degenerate axes, scales, rotations, or constraint limits;
  • output buffers smaller than the native count;
  • generator tracks with invalid headers, blob ranges, palettes, or access mode;
  • changed object generations between preflight and postcondition.

Value-initialize request structures with {} and keep reserved fields zero.

Failure is not always reversible

Some native operations can cross an irreversible boundary before cleanup fails. Structured results preserve this distinction.

Examples:

  • PhysicsHitResult::applied may be true even if later HitData destruction or collision-reference release reports an error.
  • point-light unregister can fault after renderer ownership becomes unknown; RPS does not guess whether a second unregister is safe.
  • hierarchy mutation can change the observed parent before a later reference release fails.

Do not reduce these results to “false means nothing happened.” Inspect the status and side-effect fields, rate-limit diagnostics, and avoid speculative retry.

No hidden product policy

RPS exposes mechanics, not ROCK/SCISSORS behavior state machines. It will not decide when an actor should ragdoll, which mod owns movement authority, which collision pairs should be suppressed, how a grab should behave, or when an audio/light effect should exist. The consuming mod must own that policy and must leave a deterministic final state.

Next: ownership and physics epochs.