Skip to content

About

Loadouts encapsulate a group of packages (Mods, Tools etc.) and their configurations.

A Loadout is a way to organize and manage a specific set of packages and their associated configurations.

Think of it like your Create a Class loadout in Call of Booty

Loadouts allow you to experiment with different mod setups and configurations, with the ability to switch between them on the fly.

What a Loadout Contains

A Loadout consists of the following components

  • Package List: Historical metadata of packages used in the Loadout.
  • Package Configurations: History of configuration settings for each mod in the Loadout.
  • Mod Configuration Schemas: Files describing how mod configuration binary files are structured.

Event Sourcing

Loadouts in Reloaded3 use the concept of 'events' to track and manage changes over time.

Event sourcing is a design pattern that represents the state of a system as a sequence of events.

graph TD
    A[Initial State] -->|Add Package X| B(State 1)
    B -->|Edit Config of X| C(State 2)
    C -->|Remove Package Y| D(State 3)
    D -->|Add Package Z| E(State 4)

    B1[Event: Add Package X]
    C1[Event: Edit Config of X]
    D1[Event: Remove Package Y]
    E1[Event: Add Package Z]

    A --> B1
    B1 --> B
    B --> C1
    C1 --> C
    C --> D1
    D1 --> D
    D --> E1
    E1 --> E

    style B1 fill:#91ab3700,stroke:#333,stroke-width:2px
    style C1 fill:#91ab3700,stroke:#333,stroke-width:2px
    style D1 fill:#91ab3700,stroke:#333,stroke-width:2px
    style E1 fill:#91ab3700,stroke:#333,stroke-width:2px

Examples of events include:

  • Set initial state: Sets the initial state of a loadout.
  • Adding a package: This stores the package's metadata in the Loadout.
  • Removing a package: This removes the package's metadata from the Loadout.
  • Editing a package's configuration: This stores the new configuration settings for the package.

These events are stored in a sequential manner. This means that the Loadout can be reverted to any previous state by "replaying" the events in order.

Additional benefits include:

  • History: The event log is a complete history, enabling users to review past states and changes.
  • Efficiency: Incremental changes use very little disk space.
  • Reproducibility: You can reproduce the exact 1:1 state of the Loadout at any point in time.

Sharing and Syncing Loadouts

Loadouts in Reloaded3 are designed to be easily shared & synced across different devices or with other users.

Since Loadouts are represented as a sequence of events, they can be efficiently serialized and stored in a compact format.

Loadouts just like pretty much everything else in Reloaded3 are packages. This means they can be packed and downloaded by another user as a single file. The only caveat is they are stored outside of the main Packages directory.

Loadouts can be shared with other users with or without historical data. If shared without historical data, a 'snapshot' of the current state is created, and event history is trimmed to reduce the package size.

Sync Methods

Reloaded3 loadouts are intended to be share-able through the following methods

  • 1st Party Cloud Sync: Basically something I self host for my people.
  • Game Store SaveData Sync: e.g. Steam Cloud, GOG Galaxy SDK
  • Sharing as a Package: Can be uploaded to a modding site, like any regular mod.
    • Other users can then import it by downloading it like a normal mod.
  • Cloud Sync: GDrive, MEGA, Dropbox, etc.

The loadout packages must be FAST to pack and unpack.

In order to minimize time spent on syncing. This (unfortunately) means we must avoid many small files in case of hard drives or network storage being used.

Loadout File Format

This details the nature of how Reloaded3 implements Event Sourcing for Loadouts

Item Path Description
Header header.bin (Memory Mapped) Header with current loadout pointers. Facilitates 'transactions'.
Events events.bin List of all emitted events in the loadout.
Timestamps timestamps.bin Timestamps for each commit.
Commit Parameters commit-parameters.bin
+ commit-parameters-{x}.bin
List of commit message parameters for each event.
Configs config.bin
+ config-data.bin
Package Configurations.
Package Reference (IDs) package-reference-ids.bin Hashes of package IDs in this loadout.
Package Reference (Versions) package-reference-versions-len.bin
+ package-reference-versions.bin
String versions of package IDs in this loadout.
Store Manifests stores.bin
+ store-data.bin
Game store specific info to restore game to last version if possible.
Commandline Parameters commandline-parameter-data.bin Raw data for commandline parameters. Length specified in event.

These files are deliberately set up in such a way that making a change in a loadout means appending to the existing files. No data is overwritten. Rolling back in turn means truncating the files to the desired length.

In some cases, data is grouped to improve compression ratios by bundling similar data together when sharing.

And in other cases, we put cold data that is infrequently accessed, e.g. commit message params in a separate file as that information is rarely accessed.

Loadout Storage & Lifetime

Loadouts are stored as compressed Reloaded3 packages when inactive.

When a loadout is loaded, it is unpacked and memory mapped onto disk.

More specifically, we create memory mapped files and decompress the package contents into them.

This way, we can load loadouts with the latency of a memory mapped file while still maintaining the ability to handle faults and crashes.

Rolling Back a Loadout

To roll back a Loadout to an earlier state, we replay the events from the start

We 'replay' up to the event with the desired index. As most events involve generating or unpacking a file, we can batch these operations, for example if:

  • Event 1 is Add Package X
  • Event 2 is Add Package Y
  • Event 3 is Remove Package X

We can just extract the data for Package Y as Event 2 cancels itself out. Processing the steps to be done from 0 to get to an earlier state is therefore a matter of milliseconds.

After that, we simply truncate the files. As every file works using the Append mechanism, we can just truncate the file(s) to the required length. To do that, we keep track of entries that would normally be written to each file as we replay the events, then truncate everything after where we ended up.

Fault Handling

How we handle crashes/errors.

In the event of a crash or power loss, we compare the packed version of the loadout with the unpacked version. If the unpacked version contains invalid data, we discard it and reload the packed version.

If there was a power loss mid-write, there is a possibility the various files can contain data that is unaccounted for. For example, a half persisted mod config. To handle this, we check the header.bin file. If any of the content files have extra data, the file is trimmed to the expected length.

Snapshots

Reloaded3 will not use snapshots.

It's expected an entire loadout could be replayed in milliseconds and there's also no good reason to change the starting point of the history.

Loadouts shared with other people can simply have the whole history stripped if needed.

Loadout Files

This lists the binary file formats used by an unpacked loadout.

All values are in little endian unless specified otherwise. They are shown in lowest to highest bit order.

So an order like u8, and u24 means 0:8 bits, then 8:32 bits.

header.bin

This is a master file which tracks the state of other files in the loadout.

This stores the version of the loadout and structure counts for remainder of the loadout files.

In the event of an unexpected crash, this file is used to determine the last state of the Loadout before performing a cleanup of unused data (by truncating remaining files).

Format:

Data Type Name Description
u16 Version Version of the loadout format.
u16 Reserved
u32 NumEvents Total number of events and timestamps in this loadout.
u32 NumMetadata Total number of package metadata files in this loadout.
u32 NumConfigs Total number of package configuration files in this loadout.
u32 NumGameVersions Total number of game versions.

Backwards compatibility is supported but not forwards.

If you're loading a Version that is newer than what you support, you should reject the file to avoid errors.

events.bin

This file contains all of the events that occurred in this loadout.

Each event has a 1:1 mapping to a timestamp in timestamps.bin. The number of events stored here is stored in header.bin.

The event format is documented in the Event List Page.

As a summary. Each event is composed of an u8 EventType and 0, 8, 24 or 56 bits of InlineData (depending on EventType). Events are laid out such that they align with 8 byte boundaries.

Any data that doesn't fit in the InlineData field is stored in another file and loaded by index. Details of that can be seen on each individual event entry.

Optimizing Events

Sometimes events can be optimized.

For example, if a package is added and then immediately enabled, we can cancel out the events.

As the nature of the events is such that they are always appended, we don't do this during normal operation. However, when we pack the loadout we will run certain clever optimizations like this to reduce clutter and save space.

Situations where optimizations are applied at pack stage will be noted in the event's description.

timestamps.bin

This contains the timestamp for each event.

Each timestamp here corresponds to an event in events.bin.

This is an array of 32-bit timestamps (R3TimeStamp[]). The number of items is defined in header.bin.

commit-parameters.bin

This file contains the parameters for any event that requires additional info in its commit message.

The commit messages can usually be derived directly from the event.
However, in some cases, additional information may be desireable to embed.

For example, modifying a package configuration requires a description of the changes made.
Because, we may not have the actual mod configuration metadata to determine what has changed.

This file stores parameters for these rare cases.

A timestamp is shown beside each event, it does not need to be embedded into description.

The Parameter struct is defined as:

Data Type Name Description
u8 ParameterType Type of the parameter.
u24 ParameterLength Length of the parameter in bytes.

ParameterType is defined as:

Type Data Type Example Description
0 UTF-8 Char Array Hello, World! UTF-8 characters.
1 u32 (R3TimeStamp) 1st of January 2024 Renders as human readable time.
2 u32 (R3TimeStamp) 5 minutes ago Renders as relative time.
3 u0 1st of January 2024 Human readable timestamp. Time sourced from message timestamp.
4 u0 5 minutes ago Relative time. Time sourced from message timestamp.
5 BackReference 10 entries ago Reference to a previous item. See backreferences.
6 List See Parameter Lists Defines the start of a list. See Parameter Lists.

The parameter data is split into multiple files to aid compression:

  • Text is expected to be mostly (English) ASCII and thus be mostly limited to a certain character set.
  • Timestamps are expected to mostly be increasing.
  • Other/Misc integers go in a separate file.
  • Other/Misc floats go in a separate file.

Here is a listing of which parameter types go where:

Type Data Type File
0 UTF-8 Char Array commit-parameters-text.bin
1 u32 (R3TimeStamp) commit-parameters-timestamps.bin
2 u32 (R3TimeStamp) commit-parameters-timestamps.bin

Message parameters can be deduplicated.

The writer maintains a hash of all parameters so far and reuses the same parameter index if the parameter ends up being a duplicate.

Back References

Back References are a Special Type of Parameter that references a previous item.

This improves loadout sizes by reducing existing previous data.

Data Type Name Description
u8 ParameterType Type of the parameter.
u24 ParameterOffset Negative offset to referenced parameter.

A ParameterOffset of 1 means 'the previous parameter'. 2 means 'the one before that' etc.

Parameter Lists

This primitive is used when you have an unknown number of items.

Imagine you have a message which says:

Changes were made, here they are:

{ChangeList}

And you want ChangeList to have multiple items.

These items need to be localizable, for example a single Change item could be:

- Value **{Name}** changed to **{NewValue}**

This is where Parameter Lists come in.

A Parameter List is defined as:

Data Type Name Description
u8 ParameterType Type of the parameter.
u4 Version [Event Specific] version of the list.
u20 NumParameters Number of parameters.

For the example above, we can treat each Change as 2 parameters. In which case, if we had 2 changes, we would set NumParameters to 4.

The individual parameters for Name and NewValue would then follow as regular parameters in commit-parameters.bin.

Why is there a Version field?

Sometimes it may be desireable to change the structure. Suppose you wanted to change Change item to also have the previous value:

- Value **{Name}** changed from **{OldValue}** to **{NewValue}**

In order to perform this change, you would set the Version field to 1. So when you read loadouts you can interpret both the old and new format side by side.

Message Template List

Find the full list of templates on the Commit Messages Page.

config.bin

This stores all historical mod configurations for any point in time.

This is the array of file sizes, each being:

Data Type Name Description
u16 FileSize Size of the configuration file.

Every new config is appended to config-data.bin as it is added.

Each unique config has an index, a.k.a. ConfigIdx, which is an incrementing value from 0 every time a config is added. Emitted events refer to this index.

How do you use this data?

When loading a loadout, calculate the offsets of each config in memory, by iterating through the FileSize field(s).

  • First config is at 0
  • Second is at 0 + FileSize(first)
  • Third is at second + FileSize(second).

etc. As you do this also, hash the configs. (AHash recommended)

When a new config is created, hash it and check if it's a duplicate, if it isn't, add it to the config list.

config-data.bin

This is a buffer of raw, unmodified configuration files.

You can get the file size and offsets from the config.bin file.

Package References

A 'package reference' consists of a XXH3(PackageID) and Version.

From Package.toml.

This is the minimum amount of data required to uniquely identify a package.

Packages are referred to by an index known as MetadataIdx in the events.

So a MetadataIdx == 1 means fetch the entry at index 1 of package-reference-ids.bin and package-reference-versions.bin.

As for how to use the data, it is similar to config.bin, essentially we deduplicate entries by in-memory hash. So an event can always refer to a MetadataIdx created in an earlier event to save space.

Launcher MUST ensure each published mod has valid update/download data.

Otherwise this system could fail, as a hash of packageID is not useful.

package-reference-ids.bin

This is a buffer of XXH3(PackageID)

Each entry is 8 bytes long.

Using a 64-bit hash, we need around 5 billion hashes until we reach a 50% chance of collision, that's quite plenty!

System can still always fail, we just pray it won't.

Some Numbers

Nexus Mods alone hosts 815999 mods as of 30th of May 2024 (obtained via GraphQL API).

The probability of a hash collision on whole mod set is roughly the following:

>>> r=815999
>>> N=2**64
>>> ratio = 2*N / r**2
>>> ratio
55407743.67551148
>>> 1-math.exp(-1/ratio)
1.8048018635141716e-08

That ends up being ~0.0000018% I'll be damned if R3 comes anywhere close to that.

Anyway, assuming a more modest '100000' mods will be made in R3's lifetime, we can expect a probability of 0.0000000027%, or more than 1 in 3.7 trillion.

If I'm ever that successful, I'd probably be funded enough that I could extend this to 128-bit hash, and at that point a meteor is more likely to land on your house (no joke).

package-reference-versions-len.bin

Contains the lengths of entries in package-reference-versions.bin.

Data Type Name Description
u8 VersionLength Size of the Version string.

This data compresses extremely well.

Most versions are of form X.Y.Z so there is a lot of repetition of 05.

package-reference-versions.bin

This is a buffer consisting of package versions, whose length is defined in package-reference.bin

These versions are stored as UTF-8 strings. No null terminator.

This data compresses extremely well.

Because the randomness (entropy) of values is low, the version components are super commonly 1s and 0s, and almost always all first two numbers 0-1 and dot .

Restoring Actual Package Files

We follow a multi step process in order to reliably try restore Reloaded3 packages.

First we attempt to obtain full package metadata from Central Server.

But what if Central Server is down?

We will query the Static CDN API. That contains a dump of the latest package update info.

stores.bin

This stores all game store specific info.

Why do we store this info?

This info can be used to identify the game when you share the loadout with a friend, and the game isn't known by the Community Repository.

Or in the event that you cloud sync a game (between your machines) that's not known by the Community Repository.

It can also be used to identify when game updates have taken place when auditing the log.

Data Type Name Description
u8 (StoreType) StoreType The store from which the game came from.
u16 FileSize Size of the configuration file.
u8 Currently Unused

The offsets can be derived from file sizes.

Basically this contains data specific to game stores such as GOG, Steam, Epic etc. that can be used to revert the game to an older version.

Reverting to earlier versions is not possible in all game stores.

store-data.bin

When values, e.g. strings are not available, they are encoded as 0 length strings, i.e. constant 00.

  • String8 is assumed to be a 1 byte length prefixed UTF-8 string.
  • String16 is assumed to be a 2 byte length prefixed UTF-8 string.
CommmonData Struct

This struct is shared between all store entries.

i.e. This game was manually added.

Data Type Name Description
u64 ExeHash The hash of the game executable (using (XXH3))
String16 ExePath The path to the game executable
String8 AppId The application ID of the game

We store this for every game, regardless of store.

Unknown
Data Type Name Description
u8 Version The version of the structure
CommonData CommonData The common data structure
Steam
Data Type Name Description
u8 Version The version of the structure
CommonData CommonData The common data structure
u64 AppId The Steam application ID
u64 DepotId The Steam depot ID
u64 ManifestId The Steam manifest ID
String8 Branch The Steam branch name
String8 BranchPassword The password for the Steam branch (if password-protected)

To perform rollback, will maintain basic minimal change fork of DepotDownloader, no need to reinvent wheel. Manifest contains SHA checksums and all file paths, we might be able to only do partial downloads.

To determine current version, check the App's .acf file in steamapps. The InstalledDepots will give you the current Depot and Manifest ID. Steam does not unfortunately have user friendly version names.

To determine downloadable manifests, we'll probably have to use SteamKit2. Use DepotDownloader code for inspiration.

GOG

Extended details in Stores: GOG.

We can get the info from the registry at HKEY_LOCAL_MACHINE\Software\GOG.com\Games\{GameId}

Data Type Name Description
u8 Version The version of the structure
CommonData CommonData The common data structure
u64 GameId The unique identifier for the game on GOG
u64 BuildId The unique identifier for the build
String8 VersionName The user-friendly version name for display purposes

The VersionName is also copied into the commit message on each update.

To identify the version reliably, it seems we will need to compare the hashes against the ones in the different depots.

This will also allow us to support e.g. Heroic on Linux.

Heroic & Playnite

These are 3rd party launchers that support GOG

They need to be supported, because there's no official Linux launcher.

To be determined.

Epic

Version downgrade with Epic isn't possible.

We will store the minimal amount of data required to identify the game in the hopes it is one day.

With Epic we can nip this data from C:\Program Data\Epic\EpicGamesLauncher\Data\Manifests. We want the following:

Data Type Name Description
u8 Version The version of the structure
CommonData CommonData The common data structure
u128 CatalogItemId The MD5 hash identifier for the game on Epic
String8 AppVersionString The version string of the game on Epic

These values are directly extracted from the manifest file.

Microsoft

Version downgrade with Microsoft isn't possible.

We will store the minimal amount of data required to identify the game in the hopes it is one day.

We're interested in AppXManifest.xml in this case.

Data Type Name Description
u8 Version The version of the structure
CommonData CommonData The common data structure
String8 PackageFamilyName The unique identifier for the game on the Microsoft Store. {Identity.Name}_{hash(Identity.Publisher)}
String8 PackageVersion The version of the game package on the Microsoft Store, from Identity field.

The PackageVersion is actually a four part version, but is stored as string, so just in case an invalid version exists in some manifest, we will string it.

commandline-parameter-data.bin

This file contains the raw strings for commandline parameters. The lengths of the parameters are specified in the UpdateCommandline event.