docs: add plugin setup guide

This commit is contained in:
2026-05-30 18:33:24 +12:00
parent f3db41c402
commit b61043a3fe
1776 changed files with 122386 additions and 2 deletions
+282
View File
@@ -0,0 +1,282 @@
#include "F4SE/API.h"
#include "F4SE/Interfaces.h"
#include "REL/FHookStore.h"
#include "REL/Trampoline.h"
#include "REX/TSingleton.h"
#include "REX/W32/OLE32.h"
#include "REX/W32/SHELL32.h"
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/msvc_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/spdlog.h>
namespace F4SE
{
namespace Impl
{
struct API :
public REX::TSingleton<API>
{
void Init(InitInfo, const F4SE::QueryInterface* a_intfc);
void InitLog();
void InitTrampoline();
void InitHook(REL::EHookStep a_step);
InitInfo info;
std::string pluginName{};
std::string pluginAuthor{};
REL::Version pluginVersion{};
REL::Version f4seVersion{};
PluginHandle pluginHandle{ static_cast<PluginHandle>(-1) };
std::uint32_t releaseIndex{ 0 };
std::function<const void*(const char*)> pluginInfoAccessor;
std::string_view saveFolderName{};
MessagingInterface* messagingInterface{ nullptr };
ScaleformInterface* scaleformInterface{ nullptr };
PapyrusInterface* papyrusInterface{ nullptr };
SerializationInterface* serializationInterface{ nullptr };
TaskInterface* taskInterface{ nullptr };
ObjectInterface* objectInterface{ nullptr };
TrampolineInterface* trampolineInterface{ nullptr };
};
void API::Init(InitInfo a_info, const F4SE::QueryInterface* a_intfc)
{
info = a_info;
static std::once_flag once;
std::call_once(once, [&]() {
if (const auto data = PluginVersionData::GetSingleton()) {
pluginName = data->GetPluginName();
pluginAuthor = data->GetAuthorName();
pluginVersion = data->GetPluginVersion();
} else {
std::vector<char> buf(REX::W32::MAX_PATH, '\0');
const auto size = REX::W32::GetModuleFileNameA(REX::W32::GetCurrentModule(), buf.data(), REX::W32::MAX_PATH);
if (size) {
std::filesystem::path p(buf.begin(), buf.begin() + size);
pluginName = p.stem().string();
}
}
f4seVersion = a_intfc->F4SEVersion();
pluginHandle = a_intfc->GetPluginHandle();
releaseIndex = a_intfc->GetReleaseIndex();
pluginInfoAccessor = reinterpret_cast<const Impl::F4SEInterface*>(a_intfc)->GetPluginInfo;
saveFolderName = a_intfc->GetSaveFolderName();
});
}
void API::InitLog()
{
if (info.log) {
static std::once_flag once;
std::call_once(once, [&]() {
if (saveFolderName.empty())
return;
wchar_t* knownBuffer{ nullptr };
const auto knownResult = REX::W32::SHGetKnownFolderPath(REX::W32::FOLDERID_Documents, REX::W32::KF_FLAG_DEFAULT, nullptr, std::addressof(knownBuffer));
std::unique_ptr<wchar_t[], decltype(&REX::W32::CoTaskMemFree)> knownPath(knownBuffer, REX::W32::CoTaskMemFree);
if (!knownPath || knownResult != 0) {
REX::ERROR("failed to get known folder path");
return;
}
std::filesystem::path path = knownPath.get();
path /= std::format("My Games/{}/F4SE/{}.log", GetSaveFolderName(), info.logName ? info.logName : GetPluginName());
std::vector<spdlog::sink_ptr> sinks{
std::make_shared<spdlog::sinks::msvc_sink_mt>()
};
if (info.logRotate > 0) {
constexpr auto maxSize = std::numeric_limits<std::size_t>::max();
sinks.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>(path.string(), maxSize, info.logRotate, true));
} else {
sinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.string(), true));
}
auto logger = std::make_shared<spdlog::logger>("global", sinks.begin(), sinks.end());
logger->set_level(static_cast<spdlog::level::level_enum>(info.logLevel));
logger->flush_on(static_cast<spdlog::level::level_enum>(info.logLevel));
spdlog::set_default_logger(std::move(logger));
spdlog::set_pattern(info.logPattern ? info.logPattern : "[%T.%e] [%=5t] [%L] %v");
REX::INFO("{} v{}", GetPluginName(), GetPluginVersion());
});
}
}
void API::InitTrampoline()
{
if (info.trampoline) {
static std::once_flag once;
std::call_once(once, [&]() {
if (!info.trampolineSize) {
const auto hookStore = REL::FHookStore::GetSingleton();
info.trampolineSize += hookStore->GetSizeTrampoline();
}
auto& trampoline = REL::GetTrampoline();
if (const auto intfc = GetTrampolineInterface()) {
if (const auto mem = intfc->AllocateFromBranchPool(info.trampolineSize))
trampoline.set_trampoline(mem, info.trampolineSize);
else
trampoline.create(info.trampolineSize);
}
});
}
}
void API::InitHook(REL::EHookStep a_step)
{
if (info.hook) {
const auto hookStore = REL::FHookStore::GetSingleton();
hookStore->Init();
hookStore->Enable(a_step);
}
}
}
void Init(const PreLoadInterface* a_intfc, InitInfo a_info) noexcept
{
static std::once_flag once;
std::call_once(once, [&]() {
auto api = Impl::API::GetSingleton();
api->Init(a_info, a_intfc);
api->InitLog();
api->trampolineInterface = a_intfc->QueryInterface<TrampolineInterface>(PreLoadInterface::kTrampoline);
api->InitTrampoline();
api->InitHook(REL::EHookStep::PreLoad);
});
}
void Init(const LoadInterface* a_intfc, InitInfo a_info) noexcept
{
static std::once_flag once;
std::call_once(once, [&]() {
auto api = Impl::API::GetSingleton();
api->Init(a_info, a_intfc);
api->InitLog();
api->messagingInterface = a_intfc->QueryInterface<MessagingInterface>(LoadInterface::kMessaging);
api->scaleformInterface = a_intfc->QueryInterface<ScaleformInterface>(LoadInterface::kScaleform);
api->papyrusInterface = a_intfc->QueryInterface<PapyrusInterface>(LoadInterface::kPapyrus);
api->serializationInterface = a_intfc->QueryInterface<SerializationInterface>(LoadInterface::kSerialization);
api->taskInterface = a_intfc->QueryInterface<TaskInterface>(LoadInterface::kTask);
api->objectInterface = a_intfc->QueryInterface<ObjectInterface>(LoadInterface::kObject);
api->trampolineInterface = a_intfc->QueryInterface<TrampolineInterface>(LoadInterface::kTrampoline);
api->InitTrampoline();
api->InitHook(REL::EHookStep::Load);
});
}
REL::Version GetF4SEVersion() noexcept
{
return Impl::API::GetSingleton()->f4seVersion;
}
std::string_view GetPluginName() noexcept
{
return Impl::API::GetSingleton()->pluginName;
}
std::string_view GetPluginAuthor() noexcept
{
return Impl::API::GetSingleton()->pluginAuthor;
}
REL::Version GetPluginVersion() noexcept
{
return Impl::API::GetSingleton()->pluginVersion;
}
PluginHandle GetPluginHandle() noexcept
{
return Impl::API::GetSingleton()->pluginHandle;
}
const PluginInfo* GetPluginInfo(std::string_view a_plugin) noexcept
{
if (const auto& accessor = Impl::API::GetSingleton()->pluginInfoAccessor) {
if (const auto result = accessor(a_plugin.data())) {
return static_cast<const PluginInfo*>(result);
}
}
REX::ERROR("failed to get plugin info for {}", a_plugin);
return nullptr;
}
std::uint32_t GetReleaseIndex() noexcept
{
return Impl::API::GetSingleton()->releaseIndex;
}
std::string_view GetSaveFolderName() noexcept
{
return Impl::API::GetSingleton()->saveFolderName;
}
const MessagingInterface* GetMessagingInterface() noexcept
{
return Impl::API::GetSingleton()->messagingInterface;
}
const ScaleformInterface* GetScaleformInterface() noexcept
{
return Impl::API::GetSingleton()->scaleformInterface;
}
const PapyrusInterface* GetPapyrusInterface() noexcept
{
return Impl::API::GetSingleton()->papyrusInterface;
}
const SerializationInterface* GetSerializationInterface() noexcept
{
return Impl::API::GetSingleton()->serializationInterface;
}
const TaskInterface* GetTaskInterface() noexcept
{
return Impl::API::GetSingleton()->taskInterface;
}
const ObjectInterface* GetObjectInterface() noexcept
{
return Impl::API::GetSingleton()->objectInterface;
}
const TrampolineInterface* GetTrampolineInterface() noexcept
{
return Impl::API::GetSingleton()->trampolineInterface;
}
}
namespace F4SE
{
void Init(const LoadInterface* a_intfc, const bool a_log) noexcept
{
Init(a_intfc, { .log = a_log });
}
void AllocTrampoline(std::size_t a_size) noexcept
{
auto api = Impl::API::GetSingleton();
api->info.trampoline = true;
api->info.trampolineSize = a_size;
api->InitTrampoline();
}
}
@@ -0,0 +1 @@
#include "F4SE/Impl/PCH.h"
@@ -0,0 +1,193 @@
#include "F4SE/InputMap.h"
#include "RE/C/ControlMap.h"
#include "RE/P/PC_GAMEPAD_TYPE.h"
#include "REX/PS4/SCEPAD.h"
#include "REX/W32/DINPUT.h"
#include "REX/W32/USER32.h"
#include "REX/W32/XINPUT.h"
namespace F4SE
{
std::uint32_t InputMap::XInputToScePadOffset(std::uint32_t keyMask)
{
switch (keyMask) {
case REX::W32::XINPUT_GAMEPAD_DPAD_UP:
return REX::PS4::SCE_PAD_BUTTON_UP;
case REX::W32::XINPUT_GAMEPAD_DPAD_DOWN:
return REX::PS4::SCE_PAD_BUTTON_DOWN;
case REX::W32::XINPUT_GAMEPAD_DPAD_LEFT:
return REX::PS4::SCE_PAD_BUTTON_LEFT;
case REX::W32::XINPUT_GAMEPAD_DPAD_RIGHT:
return REX::PS4::SCE_PAD_BUTTON_RIGHT;
case REX::W32::XINPUT_GAMEPAD_START:
return REX::PS4::SCE_PAD_BUTTON_OPTIONS;
case REX::W32::XINPUT_GAMEPAD_BACK:
return REX::PS4::SCE_PAD_BUTTON_TOUCH_PAD;
case REX::W32::XINPUT_GAMEPAD_LEFT_THUMB:
return REX::PS4::SCE_PAD_BUTTON_L3;
case REX::W32::XINPUT_GAMEPAD_RIGHT_THUMB:
return REX::PS4::SCE_PAD_BUTTON_R3;
case REX::W32::XINPUT_GAMEPAD_LEFT_SHOULDER:
return REX::PS4::SCE_PAD_BUTTON_L1;
case REX::W32::XINPUT_GAMEPAD_RIGHT_SHOULDER:
return REX::PS4::SCE_PAD_BUTTON_R1;
case REX::W32::XINPUT_GAMEPAD_A:
return REX::PS4::SCE_PAD_BUTTON_CROSS;
case REX::W32::XINPUT_GAMEPAD_B:
return REX::PS4::SCE_PAD_BUTTON_CIRCLE;
case REX::W32::XINPUT_GAMEPAD_X:
return REX::PS4::SCE_PAD_BUTTON_SQUARE;
case REX::W32::XINPUT_GAMEPAD_Y:
return REX::PS4::SCE_PAD_BUTTON_TRIANGLE;
default:
return keyMask;
}
}
std::uint32_t InputMap::ScePadOffsetToXInput(std::uint32_t keyMask)
{
switch (keyMask) {
case REX::PS4::SCE_PAD_BUTTON_UP:
return REX::W32::XINPUT_GAMEPAD_DPAD_UP;
case REX::PS4::SCE_PAD_BUTTON_DOWN:
return REX::W32::XINPUT_GAMEPAD_DPAD_DOWN;
case REX::PS4::SCE_PAD_BUTTON_LEFT:
return REX::W32::XINPUT_GAMEPAD_DPAD_LEFT;
case REX::PS4::SCE_PAD_BUTTON_RIGHT:
return REX::W32::XINPUT_GAMEPAD_DPAD_RIGHT;
case REX::PS4::SCE_PAD_BUTTON_OPTIONS:
return REX::W32::XINPUT_GAMEPAD_START;
case REX::PS4::SCE_PAD_BUTTON_TOUCH_PAD:
return REX::W32::XINPUT_GAMEPAD_BACK;
case REX::PS4::SCE_PAD_BUTTON_L3:
return REX::W32::XINPUT_GAMEPAD_LEFT_THUMB;
case REX::PS4::SCE_PAD_BUTTON_R3:
return REX::W32::XINPUT_GAMEPAD_RIGHT_THUMB;
case REX::PS4::SCE_PAD_BUTTON_L1:
return REX::W32::XINPUT_GAMEPAD_LEFT_SHOULDER;
case REX::PS4::SCE_PAD_BUTTON_R1:
return REX::W32::XINPUT_GAMEPAD_RIGHT_SHOULDER;
case REX::PS4::SCE_PAD_BUTTON_CROSS:
return REX::W32::XINPUT_GAMEPAD_A;
case REX::PS4::SCE_PAD_BUTTON_CIRCLE:
return REX::W32::XINPUT_GAMEPAD_B;
case REX::PS4::SCE_PAD_BUTTON_SQUARE:
return REX::W32::XINPUT_GAMEPAD_X;
case REX::PS4::SCE_PAD_BUTTON_TRIANGLE:
return REX::W32::XINPUT_GAMEPAD_Y;
default:
return keyMask;
}
}
std::uint32_t InputMap::GamepadMaskToKeycode(std::uint32_t keyMask)
{
if (RE::ControlMap::GetSingleton()->pcGamePadMapType == RE::PC_GAMEPAD_TYPE::kOrbis) {
keyMask = ScePadOffsetToXInput(keyMask);
}
switch (keyMask) {
case REX::W32::XINPUT_GAMEPAD_DPAD_UP:
return kGamepadButtonOffset_DPAD_UP;
case REX::W32::XINPUT_GAMEPAD_DPAD_DOWN:
return kGamepadButtonOffset_DPAD_DOWN;
case REX::W32::XINPUT_GAMEPAD_DPAD_LEFT:
return kGamepadButtonOffset_DPAD_LEFT;
case REX::W32::XINPUT_GAMEPAD_DPAD_RIGHT:
return kGamepadButtonOffset_DPAD_RIGHT;
case REX::W32::XINPUT_GAMEPAD_START:
return kGamepadButtonOffset_START;
case REX::W32::XINPUT_GAMEPAD_BACK:
return kGamepadButtonOffset_BACK;
case REX::W32::XINPUT_GAMEPAD_LEFT_THUMB:
return kGamepadButtonOffset_LEFT_THUMB;
case REX::W32::XINPUT_GAMEPAD_RIGHT_THUMB:
return kGamepadButtonOffset_RIGHT_THUMB;
case REX::W32::XINPUT_GAMEPAD_LEFT_SHOULDER:
return kGamepadButtonOffset_LEFT_SHOULDER;
case REX::W32::XINPUT_GAMEPAD_RIGHT_SHOULDER:
return kGamepadButtonOffset_RIGHT_SHOULDER;
case REX::W32::XINPUT_GAMEPAD_A:
return kGamepadButtonOffset_A;
case REX::W32::XINPUT_GAMEPAD_B:
return kGamepadButtonOffset_B;
case REX::W32::XINPUT_GAMEPAD_X:
return kGamepadButtonOffset_X;
case REX::W32::XINPUT_GAMEPAD_Y:
return kGamepadButtonOffset_Y;
case 0x9: // Left Trigger game-defined ID
return kGamepadButtonOffset_LT;
case 0xA: // Right Trigger game-defined ID
return kGamepadButtonOffset_RT;
default:
return kMaxMacros; // Invalid
}
}
std::uint32_t InputMap::GamepadKeycodeToMask(std::uint32_t keyCode)
{
std::uint32_t keyMask;
switch (keyCode) {
case kGamepadButtonOffset_DPAD_UP:
keyMask = REX::W32::XINPUT_GAMEPAD_DPAD_UP;
break;
case kGamepadButtonOffset_DPAD_DOWN:
keyMask = REX::W32::XINPUT_GAMEPAD_DPAD_DOWN;
break;
case kGamepadButtonOffset_DPAD_LEFT:
keyMask = REX::W32::XINPUT_GAMEPAD_DPAD_LEFT;
break;
case kGamepadButtonOffset_DPAD_RIGHT:
keyMask = REX::W32::XINPUT_GAMEPAD_DPAD_RIGHT;
break;
case kGamepadButtonOffset_START:
keyMask = REX::W32::XINPUT_GAMEPAD_START;
break;
case kGamepadButtonOffset_BACK:
keyMask = REX::W32::XINPUT_GAMEPAD_BACK;
break;
case kGamepadButtonOffset_LEFT_THUMB:
keyMask = REX::W32::XINPUT_GAMEPAD_LEFT_THUMB;
break;
case kGamepadButtonOffset_RIGHT_THUMB:
keyMask = REX::W32::XINPUT_GAMEPAD_RIGHT_THUMB;
break;
case kGamepadButtonOffset_LEFT_SHOULDER:
keyMask = REX::W32::XINPUT_GAMEPAD_LEFT_SHOULDER;
break;
case kGamepadButtonOffset_RIGHT_SHOULDER:
keyMask = REX::W32::XINPUT_GAMEPAD_RIGHT_SHOULDER;
break;
case kGamepadButtonOffset_A:
keyMask = REX::W32::XINPUT_GAMEPAD_A;
break;
case kGamepadButtonOffset_B:
keyMask = REX::W32::XINPUT_GAMEPAD_B;
break;
case kGamepadButtonOffset_X:
keyMask = REX::W32::XINPUT_GAMEPAD_X;
break;
case kGamepadButtonOffset_Y:
keyMask = REX::W32::XINPUT_GAMEPAD_Y;
break;
case kGamepadButtonOffset_LT:
keyMask = 0x9; // Left Trigger game-defined ID
break;
case kGamepadButtonOffset_RT:
keyMask = 0xA; // Right Trigger game-defined ID
break;
default:
keyMask = 0xFF; // Invalid
break;
}
if (RE::ControlMap::GetSingleton()->pcGamePadMapType == RE::PC_GAMEPAD_TYPE::kOrbis) {
keyMask = XInputToScePadOffset(keyMask);
}
return keyMask;
}
}
@@ -0,0 +1,133 @@
#include "F4SE/Interfaces.h"
#include "F4SE/API.h"
#include "REX/W32/KERNEL32.h"
namespace F4SE
{
bool MessagingInterface::RegisterListener(EventCallback* a_handler, std::string_view a_sender) const
{
const auto success = GetProxy().RegisterListener(GetPluginHandle(), a_sender.data(), reinterpret_cast<void*>(a_handler));
if (!success) {
REX::ERROR("failed to register listener for {}", a_sender);
}
return success;
}
bool MessagingInterface::Dispatch(std::uint32_t a_messageType, void* a_data, std::uint32_t a_dataLen, const char* a_receiver) const
{
const auto success = GetProxy().Dispatch(GetPluginHandle(), a_messageType, a_data, a_dataLen, a_receiver);
if (!success) {
REX::ERROR("failed to dispatch to {}", (a_receiver ? a_receiver : "all listeners"));
}
return success;
}
bool ScaleformInterface::Register(std::string_view a_name, RegisterCallback* a_callback) const
{
const auto success = GetProxy().Register(a_name.data(), reinterpret_cast<void*>(a_callback));
if (!success) {
REX::ERROR("failed to register {}", a_name);
}
return success;
}
void SerializationInterface::SetUniqueID(std::uint32_t a_uid) const
{
GetProxy().SetUniqueID(GetPluginHandle(), a_uid);
}
void SerializationInterface::SetRevertCallback(EventCallback* a_callback) const
{
GetProxy().SetRevertCallback(GetPluginHandle(), reinterpret_cast<void*>(a_callback));
}
void SerializationInterface::SetSaveCallback(EventCallback* a_callback) const
{
GetProxy().SetSaveCallback(GetPluginHandle(), reinterpret_cast<void*>(a_callback));
}
void SerializationInterface::SetLoadCallback(EventCallback* a_callback) const
{
GetProxy().SetLoadCallback(GetPluginHandle(), reinterpret_cast<void*>(a_callback));
}
void SerializationInterface::SetFormDeleteCallback(FormDeleteCallback* a_callback) const
{
GetProxy().SetFormDeleteCallback(GetPluginHandle(), reinterpret_cast<void*>(a_callback));
}
bool SerializationInterface::WriteRecord(std::uint32_t a_type, std::uint32_t a_version, const void* a_buf, std::uint32_t a_length) const
{
const auto success = GetProxy().WriteRecord(a_type, a_version, a_buf, a_length);
if (!success) {
REX::ERROR("failed to write record");
}
return success;
}
bool SerializationInterface::OpenRecord(std::uint32_t a_type, std::uint32_t a_version) const
{
const auto success = GetProxy().OpenRecord(a_type, a_version);
if (!success) {
REX::ERROR("failed to open record");
}
return success;
}
bool SerializationInterface::WriteRecordData(const void* a_buf, std::uint32_t a_length) const
{
const auto success = GetProxy().WriteRecordData(a_buf, a_length);
if (!success) {
REX::ERROR("failed to write record data");
}
return success;
}
bool SerializationInterface::GetNextRecordInfo(std::uint32_t& a_type, std::uint32_t& a_version, std::uint32_t& a_length) const
{
return GetProxy().GetNextRecordInfo(std::addressof(a_type), std::addressof(a_version), std::addressof(a_length));
}
std::uint32_t SerializationInterface::ReadRecordData(void* a_buf, std::uint32_t a_length) const
{
const auto read = GetProxy().ReadRecordData(a_buf, a_length);
if (read != a_length) {
REX::ERROR("failed to read full record data {}B of {}B", read, a_length);
}
return read;
}
bool PapyrusInterface::Register(RegisterFunctions* a_callback) const
{
const auto success = GetProxy().Register(reinterpret_cast<void*>(a_callback));
if (!success) {
REX::ERROR("failed to register callback");
}
return success;
}
void* TrampolineInterface::AllocateFromBranchPool(std::size_t a_size) const
{
const auto mem = GetProxy().AllocateFromBranchPool(GetPluginHandle(), a_size);
if (!mem) {
REX::ERROR("failed to allocate from branch pool");
}
return mem;
}
void* TrampolineInterface::AllocateFromLocalPool(std::size_t a_size) const
{
const auto mem = GetProxy().AllocateFromLocalPool(GetPluginHandle(), a_size);
if (!mem) {
REX::ERROR("failed to allocate from local pool");
}
return mem;
}
const PluginVersionData* PluginVersionData::GetSingleton() noexcept
{
return reinterpret_cast<const PluginVersionData*>(REX::W32::GetProcAddress(REX::W32::GetCurrentModule(), "F4SEPlugin_Version"));
}
}