Your first integration
Create one durable framework service after your plugin reaches a safe runtime
lifecycle point. Detect the executable once, retain the value-only
RuntimeModule, and keep world/object discovery separate from the checked
operation that uses those borrowed objects.
Minimal service
#pragma once
#include <RPS/Runtime/PhysicsApi.h>
#include <RPS/Runtime/RuntimeModule.h>
#include <RPS/Runtime/WorldAccess.h>
class RpsPhysicsService final
{
public:
[[nodiscard]] bool start() noexcept;
void stop() noexcept;
[[nodiscard]] bool setBodyVelocity(
void* hknpWorld,
RPS::Runtime::Physics::BodyId body,
const RPS::Runtime::Physics::Vector4& linear,
const RPS::Runtime::Physics::Vector4& angular) const noexcept;
private:
RPS::Runtime::RuntimeModule module_{};
bool ready_{};
};
#include "RpsPhysicsService.h"
using namespace RPS::Runtime;
using namespace RPS::Runtime::Physics;
bool RpsPhysicsService::start() noexcept
{
module_ = RuntimeModule::detect();
ready_ = static_cast<bool>(module_);
return ready_;
}
void RpsPhysicsService::stop() noexcept
{
// Retire owned constraints, bodies, sounds, and scene objects before this.
ready_ = false;
module_ = {};
}
bool RpsPhysicsService::setBodyVelocity(
void* hknpWorld,
BodyId body,
const Vector4& linear,
const Vector4& angular) const noexcept
{
if (!ready_ || !hknpWorld || !body.valid() ||
!linear.finite() || !angular.finite()) {
return false;
}
WorldWriteGuard write{module_, hknpWorld};
if (!write.active()) {
return false;
}
Api physics{module_, hknpWorld};
return physics.setVelocityDeferred(write, body, linear, angular);
}
This example does four important things:
- It rejects every unsupported executable before resolving native calls.
- It does not store the borrowed
hknpWorldor body ID as immortal state. - It acquires an explicit write epoch for the exact world being mutated.
- It treats a failed wrapper result as no authority to continue.
Call it only with current engine identity
RPS does not invent a world, actor, scene node, graph driver, collision object, or body ID. Obtain those from a lifecycle-safe source in your plugin and pass them for one synchronous operation. Re-resolve or invalidate them when the game loads, unloads, changes world, rebuilds a skeleton, replaces a controller, or destroys the owning scene object.
Do not cache BodyId alone as permanent identity. hknp slots can be recycled.
Pair it with the world/object generation already owned by your plugin, or
re-resolve it through resolveCollisionObjectBody() using both expected scene
owner and expected world witnesses.
Keep the F4SE gate separate
RuntimeModule::detect() verifies the actual PE image and executable file
version. It does not replace your F4SE Query/Load contract.
In F4SEVR 0.6.21, QueryInterface::RuntimeVersion() is a flat-compatibility
value, not the Fallout4VR.exe file version. Do not compare that query value
with RUNTIME_VR_1_2_72. Confirm VR identity and exact executable version in
the runtime/module domain before installing hooks or using layout-dependent
features. RPS performs its own exact runtime check again at its boundary.
Read before you mutate
When an operation depends on current body state, acquire a read guard and copy a snapshot:
WorldReadGuard read{module_, hknpWorld};
if (!read.active()) {
return;
}
Api physics{module_, hknpWorld};
const BodySnapshot snapshot = physics.snapshot(read, bodyId);
if (!snapshot.valid) {
return;
}
const auto motionIndex = snapshot.body.motionIndex;
const auto filterInfo = snapshot.body.collisionFilterInfo;
The result contains copied values. Do not derive a long-lived pointer from an address field in a snapshot.
Result handling rule
Boolean wrappers report only complete success. Structured results preserve more truth: native invocation, accepted/rejected status, postcondition, rollback, cleanup, or ownership uncertainty. Log the status once and stop the operation. Do not blindly retry a native call whose result says ownership or registration state is unknown.
Before adding more subsystems, read the runtime and safety contract and ownership and epoch rules.