Skip to content

Implementation Rules

Implementation Rules

Danger

Please do not implement hacks for things such as hotswapping files at runtime by serving different data on future loads; or writing to buffers passed by the application. Not only are these hard to debug but emulators should be as application agnostic as possible.

APIs to enable those features can be provided for other mods to use (e.g. via Dependency Injection), but must not be enabled by default.

This framework prioritises performance and compatibility first.

Always Stream if Possible

Implement your emulator as a Type A whenever possible.

While Type B may be easier, since you can potentially simply use existing libraries with MemoryManagerStream, it will have a noticeable impact on first load time; and the increased memory usage may lead to increased swapping to/from pagefile. The pagefile also has limits.

Memory Usage

Warning

Use memory mapped files for small files only. It is suggested to write bigger files (>100MB) out to disk directly.

If using using Type B emulation, use memory mapped files (MemoryManager & MemoryManagerStream) when possible. Failure to do so risks virtual address space starvation in 32-bit processes.

When using memory mapped files, only sections that are currently mapped/viewed use up the address space, in the case of MemoryManager, this means only AllocationGranularity is used.

Warning

For files smaller than AllocationGranularity use a MemoryStream instead to avoid wasting address space.

Use Lazy Loading & Immutability

Implementations should only produce/initialize emulated files when they are first requested by the application; i.e. when a handle is opened.

Once produced, the file emulator should always serve the same file on subsequent requests/handle openings. i.e. generated files persist for application lifetime.

Always Read All Requested Bytes

Info

A common programmer error is to issue a Read() command on a file stream and assume that all bytes requested will be given back.

This is not often the case and even I have been guilty of this mistake for a very long time. If possible, DO NOT return less than the number of bytes requested (when possible) in order to shield against buggy software implementations.

While this may sound more complicated than it should for e.g. archives, it really should not be. If you have some code for an archive emulator's ReadData that looks something like:

// If getting header in Type-A emulator
if (isHeaderRead)
{
    // We are reading the file header, let's give the program the false header.
    var fakeHeaderSpan = new Span<byte>(afsFile.HeaderPtr, afsFile.Header.Length);
    var endOfHeader = offset + length;
    if (endOfHeader > fakeHeaderSpan.Length)
        length -= (uint)(endOfHeader - fakeHeaderSpan.Length);

    var slice = fakeHeaderSpan.Slice((int)offset, (int)length);
    slice.CopyTo(bufferSpan);

    numReadBytes = slice.Length;
    return true;
}

// Else we are reading a file, let's pass a new file to the buffer.
if (afsFile.TryFindFile((int)offset, (int)length, out var virtualFile))
{
    numReadBytes = virtualFile.GetData(bufferSpan);
    return true;
}

Then you can just invoke this function multiple times until the requested amount of bytes have been filled.

Tip

A recommended way of building stream (Type A) based emulators emulators is MultiStream. With MultiStream you can avoid this issue entirely, and usually implement ReadData in ~5 lines.

Hooks Always Enabled

Don't deactivate your hooks at any point. All hooks should always be enabled to allow for recursive use of the emulators.

Data Access Patterns

Tip

Assume data can be accessed in any order, and reads may begin from any offset and/or length.