-
Notifications
You must be signed in to change notification settings - Fork 33
Unit Testing
Due to the ease with which it is possible to trigger loading (or attempting to load) the Skyrim executable module to handle Address Library IDs and memory offsets, it is important to be careful with how any REL::Relocation
is used or when resolving addresses and offsets of REL::ID
, REL::Offset
, and other similar classes. A good practice is to avoid static declarations at the namespace or class level, and instead access any of these through functions.
namespace MyPlugin
{
REL::Relocation<void()> AFunction(REL::ID(123)); // Bad, forces loading REL::Module immediately during initialization, cannot be run without SkyrimSE.exe.
// Better, REL::Module is not initialized unless and until this function is run.
[[nodiscard]] inline const REL::Relocation<void()>& GetAFunction() noexcept
{
static REL::Relocation<void()> function(REL::ID(123));
return function;
}
}
CommonLibSSE NG makes it possible to manually configure REL::Module
, rather than tying it to a Skyrim executable module that created the current process. A prerequisite of this is that your test code must run to perform this configuration before any automatic initialization would take place. Automatic initialization of REL::Module
happens when resolving an address, e.g. through construction of a REL::Relocation
, without any prior manual initialization.
Manual initialization can occur in two ways. To manually initialize a generic REL::Module
, you can call REL::Module::inject()
. The result of this call ensures the module instance is largely a blank, default value. This can be used if you expect access to REL::Module
incidentally, but information from it will not be used during testing. Where a fully-realized REL::Module
is needed, it is possible to force load a specific executable by calling REL::Module::inject(pathToFile)
. For example, REL::Module::inject(R"(C:\Program Files (x86)\Steam\steamapps\common\Skyrim Special Edition\SkyrimSE.exe)")
. If the file is found, it will be loaded into the current process (but not run!), and the REL::Module
information will be initialized based on that executable appropriately. As an alternative, you can pass in a REL::Module::Runtime
value to indicate the type of Skyrim module you want. If Skyrim is installed, the installed path to the executable will be discovered by querying the Windows registry. Both functions return a bool
to indicate success or failure.
Calls to REL::Module::inject()
can be done multiple times, which will reinitialize the module to its new state. The module that was injected will be unloaded, and a new one loaded in its place. Calling reset()
will remove all injected values entirely, and a subsequent get()
call without further injection will cause the module to load normally, as it would when first accessed in a Skyrim process.
Whenever a module is injected, the existing ID database is cleared as well. This can be done explicitly by calling REL::IDDatabase::reset()
. This causes the next access to the ID database to freshly load the Address Library based on the current REL::Module
state. It is possible to forcibly load an Address Library file as well, by calling REL::IDDatabase::load()
. This function takes a path to the Address Library file as well as a format enum (REL::IDDatabase::Format
) and a version which should match the version found in the database file.
Certain functions or data from Skyrim that would be accessed through the relocation system may be accessible, but this will be limited. Actual interaction with the Skyrim executable should be considered a form of integration testing rather than unit testing. No state in Skyrim that comes from starting the engine, or loading game data, will be available. Functionality will be equivalent to the ways you can interact with the engine from SKSEPlugin_Load
.
Due to runtime optimizations made when using selective runtime targeting, REL::Module
should not be mocked or injected for a runtime that CommonLibSSE NG is not compiled to support. Therefore it works best when run with support for multi-targeting all runtimes.
Any REL::Relocation
which is cached from a previous injection of REL::Module
will remain in its old state. These should not be reused after injecting a new module.