Table of Contents

Interface ICodePatcher

Namespace
SkyrimForge
Assembly
SkyrimForge.dll

Changes the machine code of the game, or of a native SKSE plugin, where it matches the bytes each patch expects.

public interface ICodePatcher

Remarks

IFunctionHooks redirects whole functions to a plugin's methods. This service changes code inside functions: it replaces bytes, inserts jumps, calls, and machine code, and runs a handler at one instruction through an InstructionHook.

A patch's expected bytes lie in one executable section of a loaded module. The code of Windows's modules, the .NET runtime's, managed assemblies, and SkyrimForge.Runtime.dll is not changed. An address in a function the module's exception directory lists is the start of one of its instructions.

A patch covers its expected bytes and remains until the process exits. A patch overlapping one recorded by this service or by IFunctionHooks throws, and the exception names the owner of the recorded patch or hook: the plugin that applied it, or SkyrimForge. A jump or a call whose target lies inside a recorded patch or hook past its first byte throws, as does a patch or detour containing such a target, whichever comes second. SkyrimForge.Runtime.log lists each patch's owner and its address as a module and an offset.

The bytes are written while no thread executes the bytes a patch overwrites, and no thread is inside a call made from them, since a thread returning from such a call continues at the call's end, which may then lie inside new bytes. For an instruction hook, SafetyHook moves a thread executing the hooked bytes during the write, and Apply(params ReadOnlySpan<CodePatch>) throws where a call is not the last instruction it moves. A plugin's Start is such a moment for most code.

On Special Edition and Anniversary Edition, Start runs while SKSE loads SkyrimForge, so the expected bytes are compared with code the SKSE plugins loaded after SkyrimForge have not yet changed. On Skyrim VR, Start runs once every SKSE plugin has loaded. Most SKSE plugins write code without comparing it first, so a plugin later in SKSE's order can overwrite a patch without failing. A patch applied from OnDataLoaded is compared with the code those plugins wrote while loading, though not with what a plugin later in SKSE's order writes at that same message.

After the game's data has loaded, SkyrimForge compares every patch and hook recorded by then with the code in place, a single time. SkyrimForge.Runtime.log records each one another module has changed, with its owner, its bytes as applied and as found, and the module that a jump or call written over it leads to.

Methods

AllocateCode(nint, int)

Allocates executable memory for machine code a JumpPatch or a CallPatch reaches.

nint AllocateCode(nint near, int length)

Parameters

near nint

An address within a 32-bit displacement of every allocated byte, such as the address of a patch reaching the memory.

length int

The number of bytes allocated.

Returns

nint

The address of the first allocated byte.

Remarks

The memory is readable, writable, and executable, and is never released. A jump or a call at near reaches every byte of it. SkyrimForge.Runtime.log lists the allocation with its owner. Code placed in it has no unwind data, so an exception or a stack walk passing through a frame there is not unwound correctly.

Exceptions

ArgumentOutOfRangeException

near is 0, or length is not positive.

InvalidOperationException

No free memory lies near enough to near.

Apply(params ReadOnlySpan<CodePatch>)

Applies every patch, or none where one cannot be applied.

void Apply(params ReadOnlySpan<CodePatch> patches)

Parameters

patches ReadOnlySpan<CodePatch>

The patches, whose expected bytes do not overlap one another.

Exceptions

ArgumentNullException

A patch is null.

ArgumentException

The expected bytes of two patches overlap. The target of a JumpPatch or a CallPatch lies inside the expected bytes of a patch past their first byte.

InvalidOperationException

A patch's expected bytes lie outside one executable section of a loaded module, lie in an excluded module, do not begin an instruction, do not match the code, overlap a recorded patch or hook, or contain the target of a recorded jump or call past their first byte. The code's memory protection could not be changed. The expected bytes of a jump, a call, or an insertion do not end on an instruction, or a direct branch elsewhere in their function lands inside them. The target of a jump or a call does not begin an instruction, or lies inside a recorded patch or hook. An insertion displaces an instruction with an operand relative to its own address, or no memory within reach could be allocated. An instruction hook's moved bytes extend past its expected bytes, cannot be relocated, contain the target of a recorded jump or call past their first byte, contain a call another moved instruction follows or a branch to a moved byte, or receive a direct branch from elsewhere in their function past their first byte. An instruction hook could not be written, or would exceed the limit of 1024 instruction hooks in a process. Nothing is written, except an earlier instruction hook of the group that could not be removed after a later one failed, which the message states and SkyrimForge.Runtime.log records.