SKSE Plugin API
Register a C++ listener from another SKSE plugin and receive Accurate Penetration animation updates on Skyrim's main thread.
Overview
The API is exported from AccuratePenetration.dll as AccuratePenetration_GetAPI_V1. Version 1 exposes listener registration for per-receiver animation updates.
Consumers should include AccuratePenetrationAPI.h, look up the exported function at runtime, validate the returned interface, and unregister listeners before their plugin shuts down.
AccuratePenetration::API::kVersion is 1. Check both InterfaceV1::version and InterfaceV1::size before using function pointers.
Getting The Interface
Load the export after Accurate Penetration is present in the process. The header provides both the DLL name and exported function name.
#include "AccuratePenetrationAPI.h"
#include <Windows.h>
using GetAPIFn = const AccuratePenetration::API::InterfaceV1*(__cdecl*)();
auto module = GetModuleHandleW(AccuratePenetration::API::kPluginDLL);
auto getAPI = module ?
reinterpret_cast<GetAPIFn>(GetProcAddress(module, AccuratePenetration::API::kGetAPIFunctionNameV1)) :
nullptr;
const auto* api = getAPI ? getAPI() : nullptr;
if (!api ||
api->version != AccuratePenetration::API::kVersion ||
api->size < sizeof(AccuratePenetration::API::InterfaceV1) ||
!api->RegisterAnimationUpdateListener ||
!api->UnregisterAnimationUpdateListener) {
return;
}
The exported interface is optional from the consumer's point of view. If the DLL or export is missing, skip integration and continue loading your plugin normally.
Registration Results
| Call | Success | Failure |
|---|---|---|
| RegisterAnimationUpdateListener | Returns a non-zero ListenerHandle. | Returns 0 when the callback pointer is null. |
| UnregisterAnimationUpdateListener | Returns true when the handle was found and removed. | Returns false for handle 0 or an unknown handle. |
Animation Events
AnimationUpdateEvent describes one receiver actor and the currently known interaction partners for that receiver.
| Field | Type | Meaning |
|---|---|---|
| apiVersion | uint32_t | Event API version. Version 1 events use AccuratePenetration::API::kVersion. |
| size | uint32_t | Size of the event struct for compatibility checks. |
| receiver | RE::ActorHandle | The actor this update is about. |
| position | uint8_t | The receiver's scene position as reported by the active animation framework. |
| context | SceneContext | Bit flags for scene classification, such as Vaginal, Anal, Oral, Handjob, or Masturbation. |
| selfInteraction | InteractionPartner* | Optional self interaction data for scenes where the receiver is interacting with themself. |
| actors | InteractionPartner* | Temporary array of partner interactions. |
| actorCount | uint32_t | Number of entries in actors. |
| anusOpening | float | Current normalized anus opening value for the receiver. |
| vaginalOpening | float | Current normalized vaginal opening value for the receiver. |
| ending | bool | True for the final cleanup notification for this receiver's scene. |
Interaction Partner
| Field | Type | Meaning |
|---|---|---|
| actor | RE::ActorHandle | The partner actor handle. |
| site | PenetrationSite | Target site: Mouth, Anus, Vagina, Both, HandL, HandR, or Hands. |
| position | uint8_t | The partner's scene position. |
| penetrationDepth | float | Current depth reported by the physics solver. |
| penisSize | float | Measured partner penis length. |
| penisGirth | float | Measured partner penis girth. |
Threading And Lifetime
- Register and unregister listeners on Skyrim's main thread.
- Callbacks run during Accurate Penetration's main-thread update and cleanup.
AnimationUpdateEvent,selfInteraction, andactorsare temporary. Copy any data you need before returning from the callback.- Store the returned
ListenerHandleand pass it toUnregisterAnimationUpdateListenerwhen your plugin no longer wants updates. - Do not assume the callback will receive resolved actor pointers. The API intentionally passes
RE::ActorHandle.
Keep listener callbacks short. If your integration needs expensive work, copy the event data you need and process it later from your own update path.
Example Listener
#include "AccuratePenetrationAPI.h"
namespace {
const AccuratePenetration::API::InterfaceV1* g_api = nullptr;
AccuratePenetration::API::ListenerHandle g_listener = 0;
void __cdecl OnPPAUpdate(const AccuratePenetration::API::AnimationUpdateEvent* a_event, void* a_userData)
{
if (!a_event || a_event->apiVersion != AccuratePenetration::API::kVersion) {
return;
}
const bool isOral = AccuratePenetration::API::HasContext(
a_event->context,
AccuratePenetration::API::SceneContext::Oral);
if (a_event->ending) {
// Clear any state tracked for a_event->receiver.
return;
}
for (std::uint32_t i = 0; i < a_event->actorCount; ++i) {
const auto& partner = a_event->actors[i];
// Copy values you need here. Do not keep pointers into a_event.
}
}
}
void RegisterPPAListener(const AccuratePenetration::API::InterfaceV1* a_api)
{
if (!a_api || !a_api->RegisterAnimationUpdateListener) {
return;
}
g_api = a_api;
g_listener = g_api->RegisterAnimationUpdateListener(OnPPAUpdate, nullptr);
}
void UnregisterPPAListener()
{
if (g_api && g_listener && g_api->UnregisterAnimationUpdateListener) {
g_api->UnregisterAnimationUpdateListener(g_listener);
}
g_listener = 0;
g_api = nullptr;
}
Copy AccuratePenetrationAPI.h
Copy this header into your SKSE plugin project and include it from your integration code.
#pragma once
#include <cstdint>
#include "RE/B/BSPointerHandle.h"
#ifndef ACCURATEPENETRATION_API
#define ACCURATEPENETRATION_API
#endif
namespace AccuratePenetration::API {
inline constexpr auto kPluginDLL = L"AccuratePenetration.dll";
inline constexpr auto kGetAPIFunctionNameV1 = "AccuratePenetration_GetAPI_V1";
inline constexpr std::uint32_t kVersion = 1;
enum class SceneContext : std::uint32_t {
None = 0,
Vaginal = 1 << 0,
Anal = 1 << 1,
Oral = 1 << 2,
Aggressive = 1 << 3,
FemDom = 1 << 4,
Loving = 1 << 5,
Dirty = 1 << 6,
Boobjob = 1 << 7,
Handjob = 1 << 8,
Footjob = 1 << 9,
Masturbation = 1 << 10,
};
[[nodiscard]] constexpr SceneContext operator|(SceneContext a_lhs, SceneContext a_rhs) noexcept { return static_cast<SceneContext>(static_cast<std::uint32_t>(a_lhs) | static_cast<std::uint32_t>(a_rhs)); }
[[nodiscard]] constexpr SceneContext operator&(SceneContext a_lhs, SceneContext a_rhs) noexcept { return static_cast<SceneContext>(static_cast<std::uint32_t>(a_lhs) & static_cast<std::uint32_t>(a_rhs)); }
[[nodiscard]] constexpr bool HasContext(SceneContext a_flags, SceneContext a_check) noexcept { return (a_flags & a_check) == a_check; }
enum class PenetrationSite : std::uint8_t {
None = 0,
Mouth,
Anus,
Vagina,
Both,
HandL,
HandR,
Hands,
};
struct InteractionPartner {
RE::ActorHandle actor;
PenetrationSite site = PenetrationSite::None;
std::uint8_t position = 0;
float penetrationDepth = 0.0f;
float penisSize = 0.0f;
float penisGirth = 0.0f;
};
struct AnimationUpdateEvent {
std::uint32_t apiVersion = kVersion;
std::uint32_t size = sizeof(AnimationUpdateEvent);
RE::ActorHandle receiver;
std::uint8_t position = 1;
SceneContext context = SceneContext::None;
const InteractionPartner* selfInteraction = nullptr;
const InteractionPartner* actors = nullptr;
std::uint32_t actorCount = 0;
float anusOpening = 0.0f;
float vaginalOpening = 0.0f;
// True for the final cleanup notification for this receiver's scene.
bool ending = false;
};
using ListenerHandle = std::uint64_t;
using AnimationUpdateCallback = void(__cdecl*)(const AnimationUpdateEvent* a_event, void* a_userData);
using RegisterAnimationUpdateListenerFn = ListenerHandle(__cdecl*)(AnimationUpdateCallback a_callback, void* a_userData);
using UnregisterAnimationUpdateListenerFn = bool(__cdecl*)(ListenerHandle a_handle);
struct InterfaceV1 {
std::uint32_t version = kVersion;
std::uint32_t size = sizeof(InterfaceV1);
RegisterAnimationUpdateListenerFn RegisterAnimationUpdateListener = nullptr;
UnregisterAnimationUpdateListenerFn UnregisterAnimationUpdateListener = nullptr;
};
// Main-thread only. Register/unregister on Skyrim's main thread. Callbacks run during this plugin's main-thread update and cleanup.
// AnimationUpdateEvent and nested pointers are temporary; copy any data you need before returning from the callback.
} // namespace AccuratePenetration::API
extern "C" ACCURATEPENETRATION_API const AccuratePenetration::API::InterfaceV1* __cdecl AccuratePenetration_GetAPI_V1();