Skip to content
John Stewart edited this page Jun 5, 2022 · 12 revisions

stability-stable

Running Without Skyrim

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

Injecting REL::Module and REL::IDDatabase

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.

Limitations of Unit Testing

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.