Runtime Targeting - CharmedBaryon/CommonLibSSE-NG GitHub Wiki
This fork of CommonLibSSE can target multiple Skyrim runtimes with a single build, allowing for a single DLL to work in Skyrim SE, AE, and even VR. Multitargeting for all runtimes is the default behavior. When targeting multiple runtimes certain functionality may become inaccessible or only indirectly accessible. This is because there are some fundamental ABI incompatibilities between SE/AE and VR in particular. In much fewer cases, there may be differences between AE and SE (a tiny number of offsets remain which are not found in AE).
The largest difference caused by multi-targeting is found when enabling VR and SE, AE, or both. Some classes have different ABIs in VR. As a result only a portion, or even none of their member variables may be accessible, since the correct offset cannot be determined at compile time. To handle this, accesors have been provided which can return pointers structs which contain the member variables, which can be looked up at runtime. This accessor is usually called GetRuntimeData()
and returns a reference to a RUNTIME_DATA struct with the variables which are common to SE and VR. If build to exclude VR, or for only VR, then the member variables become accessible directly on the class since this problem is eliminated, in order to maintain compatibility with other branches of CommonLibSSE (although the accessor function still remains, and is recommended for the broadest compatibility). In some classes there are multiple accessors due to multiple variant memory offsets. Accessor functions that provide portable member access are usable in all builds of CommonLibSSE NG, but direct access to members are only available when all runtimes allow it.
In general, classes and data not common to all runtimes is always available, despite it not always being present. For example, the RE::PlayerCharacter
class always makes the VR data available via an accessor, GetVRNodeData()
, which returns a pointer to a struct with the data. If the current runtime is not Skyrim VR, this accessor returns a nullptr
. This behavior allows a plugin author to dynamically check for VR and add special handling for that case if found. Even classes specific to one runtime are always available (e.g. RE::BSOpenVRControllerDevice
for VR controllers), but when using multi-targeting their member variables and even member functions cannot be directly invoked without proxy accessors or invokers that handle the non-VR case gracefully.
Skyrim SE/VR and Skyrim AE versions of SKSE detect and load plugins differently. Skyrim SE/VR's SKSE will search for DLL files that have a SKSEPlugin_Query
function, and execute that function, expecting to get a true
result to determine that the DLL file is a valid plugin. The AE version will look for DLL files that declare a static, constinit
SKSEPlugin_Version
data structure with plugin metadata. All versions of SKSE will ignore the other's expected symbol, so a plugin must implement both of these to be portable. The recommended pattern implements SKSEPlugin_Query
as a minimal function that populates the required metadata to match the SKSEPlugin_Version
structure, and leaves all other functionality to SKSEPlugin_Load
. The simplest way to do this is with a declaration of SKSEPluginInfo
:
SKSEPluginInfo(
.Version = "1.0.0.0"_v,
.Name = "My Plugin"
)
This macro will declare SKSEPLugin_Version
as a SKSE::PluginDeclaration
(a CommonLibSSE NG class which syntactically and semantically improves upon SKSE::PluginVersionData
-- the content passed to the macro is the content of the declaration), and additionally defines an SKSEPlugin_Query
function which will match it. Note that Address Library use is the default with SKSE::PluginDeclaration
. The equivalent in a traditional project would be:
using namespace SKSE;
EXTERN_C [[maybe_unused]] __declspec(dllexport) constinit auto SKSEPlugin_Version = []() noexcept {
PluginVersionData v;
v.PluginName("PluginName");
v.PluginVersion({ 1, 0, 0, 0 });
v.UsesAddressLibrary(true);
return v;
}();
EXTERN_C [[maybe_unused]] __declspec(dllexport) bool SKSEAPI SKSEPlugin_Query(const QueryInterface*, PluginInfo* pluginInfo) {
pluginInfo->name = SKSEPlugin_Version.pluginName;
pluginInfo->infoVersion = PluginInfo::kVersion;
pluginInfo->version = SKSEPlugin_Version.pluginVersion;
return true;
}
To avoid different loading behavior between SE and AE, actions like checking for loading in the Creation Kit editor, initializing logging, etc. that were often done in SKSEPlugin_Query
in SE-era plugins should be done in SKSEPlugin_Load
. A more advanced example can be found in the CommonLibSSE Sample Plugin project.
To allow for direct access to more functionality (and more compatibility with other CommonLibSSE forks and the upstream), it is possible to build with a limited number of runtimes supported. There are three options that enable runtimes to be supported:
-
ENABLE_SKYRIM_SE
: For Skyrim SE versions prior to AE (1.5.x). -
ENABLE_SKYRIM_AE
: For Skyrim SE versions post-AE (1.6.x). -
ENABLE_SKYRIM_VR
: For Skyrim VR. By default, all three are enabled. Any runtime can be selectively disabled through CMake, by passing e.g.-DENABLE_SKYRIM_VR=off
. Any combination of runtime can be used, as long as at least one runtime is enabled. Accessors for multi-targeting are still usable in selectively targeted builds, but member functions become directly accessible again if using only VR or using only non-VR. If using only a single runtime the functions which do dynamic lookup of the current runtime will bypass the dynamic lookup.
The following vcpkg ports are provided for selective runtime builds:
-
commonlibsse-ng-se
: SE only. -
commonlibsse-ng-ae
: AE only. -
commonlibsse-ng-vr
: VR only. -
commonlibsse-ng-flatrim
: AE and SE, but not VR.
This macro will define SKSEPlugin_Version
and SKSEPlugin_Query
for you. The macro should receive the contents that will be put into SKSEPlugin_Version
, as declared using SKSE::PluginDeclaration
. SKSE::PluginDeclaration
is defined as a struct literal, so the contents must be ordered. For clarity, designated initialization is recommended, although in C++ this still requires ordering to be maintained.
SKSEPluginInfo(
.Version = "1.0.0.0_v",
.Name = "Version-Independent Plugin",
.Author = "John Doe",
.SupportEmail = "[email protected]",
.RuntimeCompatibility = VersionIndependence::AddressLibrary
)
SKSEPluginInfo(
.Version = "1.0.0.0_v",
.Name = "Version-Dependent Plugin",
.RuntimeCompatibility = { RUNTIME_SSE_1_5_97_0, RUNTIME_SSE_1_6_353_0 }
)
These functions allow checking if the current runtime module AE, SE, or VR. These functions are generally inline
but may be constexpr
on selective runtime builds, when the query can be determined at compile time. The GetRuntime()
call returns an enum of type REL::Module::Runtime
representing, SE, AE, or VR.
The REL::Relocate
function template takes two or three arguments of the same type, and returns the one which is intended for the currently in-use Skyrim runtime. If provided with two arguments, the first argument is the value to use for Skyrim SE and VR, and the second is to be used for AE. If three arguments are provided, the first argument is the value to use in SE, the second to be used with AE, and the third to be used in VR.
This function call is inline
when multiple runtimes are supported which require a choice to be made. If built for a subset of runtimes, such that the value to be returned can be determined at compile time based on the exclusion of arguments for the unsupported runtimes, then the function is constexpr
.
auto value = REL::Relocate(10, 20); // value is 10 on SE and VR, 20 on AE.
auto value2 = REL::Relocate("foo", "bar", "baz"); // value2 is "foo" on SE, "bar" on AE, and "baz" on VR.
Represents an address library ID which can vary between Skyrim runtimes. This class is similar to REL::ID
, but takes separate IDs for SE, AE, and VR, and determines the ID to use dynamically based on the runtime in current use. It can be constructed using two arguments, in which case the first argument is the ID to be used for SE and VR, and the second argument is the one to use for AE. If three arguments are used, the first ID is only used for SE and the third argument will be used for VR. Note that the VR address library uses the same IDs as the SE address library; the three-argument version is intended for cases where hooking in VR should target an entirely different function.
The RELOCATION_ID(a_seAndVR, a_ae)
macro is an alias for REL::RelocationID(a_seAndVR, a_ae)
, and supports compatibility with the form used in powerof3's CommonLibSSE fork. Note that in that version, the result of the macro is a REL::ID
but in CommonLibSSE NG it returns a REL::RelocationID
, which is a different class, albeit API-compatible with REL::ID
(therefore to maximize compatibility of your code with that fork, use auto
typing when saving the result to a variable).
REL::RelocationID
can be passed directly as the ID argument to REL::Relocation
.
constexpr auto PopulateHitDataID = RELOCATION_ID(42832, 44001); // Represents ID 42832 for SE or VR, and 44001 for AE.
REL::Relocation<int32_t*(Actor*, char*)> PopulateHitData(PopulateHitDataID, 0x42); // At time `REL::Relocation` is constructed, resolves the ID to use.
RELOCATION_ID
is recommended over the direct use of REL::RelocationID
when using the two-argument constructor, to preserve compatibility with powerof3's fork.
This ID type is similar to REL::RelocationID
, except that the first argument in the constructor is always an SE-only ID, and the second is an AE ID. The required third argument is an explicit offset to be used with Skyrim VR. This class is primarily intended for cases where VR support is desired, but adding a VR ID to the VR address library database is specifically not desired, e.g. for the full list of generated RTTI and vtable offsets. It should not be generally used for primary functionality in your plugin -- instead use REL::RelocationID
and submit a PR to the vr_address_tools project to add the necessary ID and offset for VR.
This class can be passed directly to REL::Relocation
in place of the regular REL::ID
.
// Uses address library ID 684588 for SE, ID 392214 for AE, and uses offset 0x1ed6cf8 for VR.
constexpr REL::VariantID RTTI_IFormFactory(684588, 392214, 0x1ed6cf8);
Similar to REL::Offset
, but with different offsets for SE, AE, and VR. This class is constructed with three arguments representing those three offsets. The first argument is the SE offset, the second is the AE offset, and the third is the VR offset. The correct offset is determined at the time the offset value is requested from the object.
This class can be passed directly to REL::Relocation
in place of the regular REL::Offset
, both when used as the sole argument or when used as the second argument following an ID. If passed as the first and only argument, the REL::Relocation
uses the address of the offset amount relative to the base memory address of the Skyrim module (same as when passing REL::Offset
, but with dynamic selection of the runtime's offset value). If passed as the second argument, along with an ID for the first argument, the address the REL::Relocation
uses is the offset relative to the memory address represented by the ID.
REL::Relocation<int(int)> CallGetSystemMetrics(REL::RelocationID(35548, 36547), REL::VariantOffset(0x8A, 0xA9, 0x9C));
A function which access a member variable or other member data of a class which may have varying offsets in VR. This function can be used to create an accessor function for member variables when a class has different memory layout in VR than it does in SE/AE. A common pattern in CommonLibSSE NG is to allow direct member variable access whenever possible. Therefore the vast majority of classes do not need any special handling since they do not differ in VR. When they do differ in VR, member variables can be directly accessed if and only if the build targets only VR or only non-VR runtimes. If the build targets at least one non-VR runtime in addition to VR, then member variables can no longer be directly accessed and an accessor function must be called, which uses REL::RelocateMember
to return a reference to the member. These accessor functions are always accessible, even when direct memory access is permitted.
REL::RelocateMember
takes its this
pointer as a first argument, followed by an offset to use within the class object for SE and AE as the second argument, and an offset for VR as the third argument. The template parameter determines the type the reference is interpreted as. The result is a reference to the member.
[[nodiscard]] RE::NiPoint3& GetPosition() noexcept
{
return REL::RelocateMember<RE::NiPoint3>(this, 0x18, 0x20); // Position member is located at offset 0x18 in the class on SE/AE, 0x20 in VR.
}
[[nodiscard]] const RE::NiPoint3& GetPosition() const noexcept
{
return REL::RelocateMember<RE::NiPoint3>(this, 0x18, 0x20);
}
This function can also be used for upcasting in classes with different memory layouts that have multiple inheritance, using the offset at which the content from the parent class is located, such as this example from RE::BookMenu
:
[[nodiscard]] SimpleAnimationGraphManagerHolder* AsSimpleAnimationGraphManagerHolder() noexcept
{
return &REL::RelocateMember<SimpleAnimationGraphManagerHolder>(this, 0x30, 0x40);
}
Invokes a virtual function on a polymorphic class which has different vtable layouts between SE/AE and VR. A small number of classes in Skyrim VR add a new virtual function, which causes subsequent virtual functions in the same vtable to be offset more than in SE and AE. This function can manually lookup the function pointer in the vtable to invoke the correct one, based on whether the current runtime is VR or not. This allows for virtual functions to be replaced by non-virtual functions which call REL::RelocateVirtual
, thus providing an API that is in most respects identical to the original class definition, while still dispatching dynamically to the correct vtable offset.
When calling this function, the template parameter should be the type of a virtual member function pointer matching the function signature of the virtual function being invoked. This is typically done by using decltype(&Class::Function)
. The first argument should be the offset within the vtable for SE/AE, and the second should be the offset in the vtable in VR. The remaining arguments will be the arguments to be passed to the virtual function, which should match the signature of the template argument (including an explicit this
argument).
Actor* Actor::SetUpTalkingActivatorActor(Actor* a_target, Actor*& a_activator)
{
return RelocateVirtual<decltype(&Actor::SetUpTalkingActivatorActor)>(0x0DB, 0x0DD, this, a_target, a_activator);
}
Note that this call will always use the vtable at offset 0 within the this
object. Currently in all CommonLibSSE NG cases that are known, the only variant vtables any class has are in its first vtable, so as of the time of this writing this is not a significant issue. If, however, you find a case where the variant vtable is in a non-primary vtable, you should static_cast
the this
argument to that type before passing it to REL::RelocateVirtual
, and use that parent type explicitly in the template parameter as the self type. This allows the static_cast
to handle the pointer conversion so that the expected vtable will then be at offset 0. If the class in question additionally has changes to its member variable layout in VR, you may need to use REL::RelocateMember
rather than static_cast
to perform this cast.
This macro is defined if the build of CommonLibSSE NG supports both Skyrim VR and at least one non-VR runtime. It allows for #if
macros to alter code where special handling is needed to cover functionality that varies between VR and non-VR editions of Skyrim.
These macros allow something to be marked constexpr
when using selective runtime targeting, if only a single runtime is being targeted. SKYRIM_REL
will be inline
if multiple runtimes are supported, or constexpr
when only one runtime is supported. This allows some functions to vary between inline
and constexpr
. SKYRIM_REL_CONSTEXPR
will be constexpr
when only one runtime is supported, or an empty string otherwise, allowing it to be used in cases where constexpr
can be used but inline
cannot (e.g. in an if
statement).
These macros function similarly to SKYRIM_REL
and SKYRIM_REL_CONSTEXPR
mentioned above, however resolve to constexpr
when not supporting both VR and non-VR runtimes (i.e. when SKYRIM_CROSS_VR
is undefined). This means that unlike SKYRIM_REL
and SKYRIM_REL_CONSTEXPR
, they are still constexpr
when both SE and AE are supported, but VR is not.
This macro resolves to virtual
when the build supports only VR, or only non-VR runtimes (i.e. if SKYRIM_CROSS_VR
is undefined), or to an empty string otherwise. This allows for virtual function declarations to easily be converted to non-virtual function declarations when supporting cross-VR runtimes, so that they can be replaced with a non-virtual implementation using REL::RelocateVirtual
calls.
A macro which is always set by CommonLibSSE NG. This macro can be used to detect when building against the NG fork or a compatible downstream that supports the classes, functions, and macros above. An example use case is Fully Dynamic Game Engine's Trueflame component's higher-level hooking primitives, where FunctionHook
, CallHook
, and BranchHook
classes come with support for REL::RelocationID
and the other similar classes if-and-only-if it is being used in a project with CommonLibSSE NG, but omits those constructors when used with a CommonLibSSE fork that does not have those types.
Currently, there is full coverage of all (known) VR and non-VR functionality in a combined runtime build, with the exception of the RE::PlayerCharacter
class. This class has major differences in VR and has not been reverse engineered in VR to the extent it has in non-VR runtimes. Therefore, mods which depend on member variables of this class should remain on a "flatrim" build (indeed, since the members are not RE'ed on VR, even doing a separate VR build is currently not an option, until reverse engineering on this class advances).
A useful way to both simplify your project, make it more maintainable, but also ensure your plugin is detected properly across multiple runtimes, is to use CMake to define your plugin metadata. You can do this by replacing add_library
with add_commonlibsse_plugin
in CMakeLists.txt
.
find_package(CommonLibSSE CONFIG REQUIRED)
add_commonlibsse_plugin(${PROJECT_NAME} SOURCES ${headers} ${sources})
This will automatically generate SKSEPlugin_Version
and SKSEPlugin_Query
and use your CMake project and target information to fill in the plugin name and version metadata. It will also default to declaring its compatibility mode is using Address Library. All of this can be overridden.
add_commonlibsse_plugin(<target>
# The plugin's name, defaults to target.
NAME <string>
# The plugin's author, empty by default.
AUTHOR <string>
# The support email address, empty by default.
EMAIL <string>
# The plugin version number, defaults to ${PROJECT_VERSION}.
VERSION <version number>
# Indicates the plugin is compatible with all runtimes via address library. This is the default if no
# other compatibilility mode is specified. Can be used with USE_SIGNATURE_SCANNING but not
# COMPATIBLE_RUNTIMES.
USE_ADDRESS_LIBRARY
# Indicates the plugin is compatible with all runtimes via signature scanning. Can be used with
# USE_ADDRESS_LIBRARY but not COMPATIBLE_RUNTIMES.
USE_SIGNATURE_SCANNING
# List of up to 16 Skyrim versions the plugin is compatible with. Cannot be used with
# USE_ADDRESS_LIBRARY or USE_SIGNATURE_SCANNING.
COMPATIBLE_RUNTIMES <version number> [<version number>...]
# The minimum SKSE version to support; defaults to 0, and recommended by SKSE project to be left
# 0.
MINIMUM_SKSE_VERSION <version number>
# Omit from all targets, same as used with add_library.
EXCLUDE_FROM_ALL
# List of the sources to include in the target, as would be the parameters to add_library.
SOURCES <path> [<path> ...]
)
When using this method, you can remove your use of SKSEPlugin_Version
, SKSEPlugin_Query
, and/or SKSEPluginInfo
from your source code. You also no longer need to link CommonLibSSE::CommonLibSSE
for that CMake target, as the CMake function configures that for you.
This method also obsoletes the common use of a plugin information header that is configured by CMake to inject the project name and version into C++. If you are using a Plugin.h.in
or PluginInfo.h.in
file to inject that information, you can now access that information through SKSE::PluginDeclaration::GetSingleton()
.
auto* plugin = SKSE::PluginDeclaration::GetSingleton();
auto name = plugin->GetName();
auto version = plugin->GetVersion();