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.

Current Version

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

CallSuccessFailure
RegisterAnimationUpdateListenerReturns a non-zero ListenerHandle.Returns 0 when the callback pointer is null.
UnregisterAnimationUpdateListenerReturns 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.

FieldTypeMeaning
apiVersionuint32_tEvent API version. Version 1 events use AccuratePenetration::API::kVersion.
sizeuint32_tSize of the event struct for compatibility checks.
receiverRE::ActorHandleThe actor this update is about.
positionuint8_tThe receiver's scene position as reported by the active animation framework.
contextSceneContextBit flags for scene classification, such as Vaginal, Anal, Oral, Handjob, or Masturbation.
selfInteractionInteractionPartner*Optional self interaction data for scenes where the receiver is interacting with themself.
actorsInteractionPartner*Temporary array of partner interactions.
actorCountuint32_tNumber of entries in actors.
anusOpeningfloatCurrent normalized anus opening value for the receiver.
vaginalOpeningfloatCurrent normalized vaginal opening value for the receiver.
endingboolTrue for the final cleanup notification for this receiver's scene.

Interaction Partner

FieldTypeMeaning
actorRE::ActorHandleThe partner actor handle.
sitePenetrationSiteTarget site: Mouth, Anus, Vagina, Both, HandL, HandR, or Hands.
positionuint8_tThe partner's scene position.
penetrationDepthfloatCurrent depth reported by the physics solver.
penisSizefloatMeasured partner penis length.
penisGirthfloatMeasured 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, and actors are temporary. Copy any data you need before returning from the callback.
  • Store the returned ListenerHandle and pass it to UnregisterAnimationUpdateListener when your plugin no longer wants updates.
  • Do not assume the callback will receive resolved actor pointers. The API intentionally passes RE::ActorHandle.
Callback Work

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;
}