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.exemodule; - 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:
| Domain | Typical APIs | Rule |
|---|---|---|
| Physics read/write epoch | Physics::Api, body snapshots, shape casts, constraints | Supply the exact WorldReadGuard or WorldWriteGuard. |
| Physics callback | Generator output and some world guards | Keep work bounded; never retain callback storage. |
| Game/frame thread outside physics | scene hierarchy, actor state/pathing, audio, lights | The wrapper rejects an active or unreadable physics-step context. |
| Pure/value code | collision filters, transform math, pose blend helpers | No engine pointer is required; finite/bounds rules still apply. |
| Consumer-owned hook | Hooks and collision pair policy | The 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;RayRequestandShapeCastRequestuse game-space positions/distances;PrismaticConstraintInfo::minimumRelativeDistanceHavokandmaximumRelativeDistanceHavokare already Havok units;PhysicsImpactContactstores 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::appliedmay 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.