Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4c8e61c
Implement C++ Mod Config.
Sora-yx Sep 15, 2026
8bbcc06
Fix typo in the doc (ended dropping crossplatform)
Sora-yx Sep 15, 2026
7fb4acf
ModConfig: Fix missing 32 bits mod template reference.
Sora-yx Sep 15, 2026
00d437e
Syntax moment
Sora-yx Sep 15, 2026
a1658e0
Handle null case for when user config directory is null (+ rework mig…
Sora-yx Sep 16, 2026
a423fff
Validate filename helper + add missing "Values" for enum
Sora-yx Sep 16, 2026
e924738
Inlined enum stuff through helper functions
Sora-yx Sep 16, 2026
7308d68
Invoke and Migrate improvement stuff.
Sora-yx Sep 16, 2026
9671e70
Reloaded Mod Config header: Add mutex for thread safe + API struct
Sora-yx Sep 16, 2026
8db149d
Documentation fixes: formatting adjust + rework CMake to use env vari…
Sora-yx Sep 16, 2026
ef58379
wrapper to the loader API
Sora-yx Sep 18, 2026
3b0573e
Changed: Improve/shorten NativeMods docs
Sewer56 Sep 20, 2026
da26497
Changed: Shorten native mod template README
Sewer56 Sep 20, 2026
9055250
Changed: Remove unused usings from native config tests
Sewer56 Sep 20, 2026
98c866e
Changed: Polish native config docs and clear new-code warnings
Sewer56 Sep 20, 2026
756a7a8
Changed: Group native mod config types under Native subnamespace
Sewer56 Sep 20, 2026
1f8bd02
Changed: Split native mod config schema models into Schema subnamespace
Sewer56 Sep 20, 2026
405b14c
Changed: Link native schema docs to C# interface types
Sewer56 Sep 20, 2026
14c1c83
Update: Doc cleanup of ConfigurableBase.cs
Sewer56 Sep 20, 2026
1dfb6cb
Style: ModConfigSchema added newline
Sewer56 Sep 20, 2026
a15949f
Style: Fix initializer alignment in picker schema parsers
Sewer56 Sep 20, 2026
eadcbad
Fixed: Remove duplicate blank line in ConfigureModCommand
Sewer56 Sep 20, 2026
7b9e43e
Changed: Link plain doc mentions to code members
Sewer56 Sep 20, 2026
0fa2850
Changed: Dedupe native and managed configurator setup
Sewer56 Sep 20, 2026
e799103
Changed: Add Arrange/Act/Assert sections to native mod tests
Sewer56 Sep 20, 2026
cc6ff21
Changed: Removed redundant comment
Sewer56 Sep 20, 2026
0aa172d
Native Mod Config: swap to Enum + add missing json exception + valida…
Sora-yx Sep 21, 2026
86c1e4d
Updated documentation to add enum property and InitialFolderPath
Sora-yx Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 157 additions & 3 deletions docs/NativeMods.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ To generate the config file, create a new mod from within the launcher.

Reloaded tries to start mods by using the following entry points in order:

- [ReloadedStartEx][native-header] - `void fn(const ReloadedStartInfo* info)`
- ReloadedStart
- InitializeASI
- Init

If none of these entry points is found, the mod will not be loaded.
The exported methods should have no parameters and return `void`.

In the case of `ReloadedStartInfo`, it provides a wrapper around the API that's
usually provided to .NET mods (`IModLoader`).

After calling any API that returns strings, you will need to call `free_string`
afterwards.

**Suspend, Resume, Unload:**

Expand All @@ -47,5 +53,153 @@ Specifically, you will need to use a good hooking/detouring library that fully r

Here is an example of how such a hooking library may be implemented: [Reloaded.Hooks](https://github.com/Reloaded-Project/Reloaded.Hooks/issues/2).

## CoreRT/NativeAOT?
Yes you can; mad scientist.
## Languages

### C/C++

#### Setup

You need a C++17 compiler and CMake to build native mods:

- Visual Studio 2022 (or newer) with the *Desktop development with C++* workload,
using the MSVC or Clang toolset.
- CMake 3.15 or newer, bundled with Visual Studio (or from [cmake.org](https://cmake.org)).

Start from the template (`dotnet new reloaded-native`) or copy the files from
the [native mod template][native-template].

Build the DLL for your game's architecture:

```text
cmake -B build -A x64 (64-bit game)
cmake -B build -A Win32 (32-bit game)
cmake --build build --config Release
```

Upon building, the mod will automatically be copied to the right location
and show up in Reloaded-II.

## Mod Configuration

### User Settings (Config Dialog)

The Reloaded-II launcher exposes a *Configure* dialog for native mods if the
`ConfigSchema.json` file exists next to `ModConfig.json`.

The declarative schema file supports all features supported by the .NET equivalent.

Example:

```json
{
"Configurations": [
{
"FileName": "Config.json",
"DisplayName": "Default Config",
"Properties": [
{
"Name": "EnableThing",
"Type": "bool",
"DisplayName": "Enable Thing",
"Description": "Turns the thing on or off.",
"Category": "General",
"Order": 0,
"DefaultValue": true
},
{
"Name": "Volume",
"Type": "int",
"DefaultValue": 75,
"Slider": {
"Minimum": 0.0, "Maximum": 100.0,
"SmallChange": 1.0, "LargeChange": 10.0,
"TickFrequency": 10, "ShowTextField": true
}
},
{ "Name": "Brightness", "Type": "float", "DefaultValue": 1.5 },
{
"Name": "Quality",
"Type": "enum",
"DefaultValue": "High",
"Values": [
"Low",
{ "Name": "High", "DisplayName": "High Quality" }
]
},
{
"Name": "CustomFile",
"Type": "string",
"FilePicker": { "Title": "Choose a File" }
}
]
}
]
}
```

- `Type` is `bool`, `int`, `float`, `double`, `string`, or an enum.
Enums list their values inline under `Values`, or under a shared `Enums`
array when the same enum is used by several properties.
- Property and enum value `Name`s must only contain letters, digits and
underscores. Use `DisplayName` for freeform text.
- `DisplayName`, `Description`, `Category`, `Order` and `DefaultValue` mirror
the attributes used by the C# mod template.
- `Slider`, `FilePicker` and `FolderPicker` mirror the `SliderControlParams`,
`FilePickerParams` and `FolderPickerParams` attributes, all fields are
optional. `InitialFolderPath` is one of .NET's

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`InitialFolderPath` is one of .NET's
  `Environment.SpecialFolder` names, e.g. `Desktop`, `MyDocuments`,
  `ProgramFiles`, etc.

Needs hyperlink to MSDN.
And separate bullet; to avoid complexity making things hard for user to read.

`Environment.SpecialFolder` names, e.g. `Desktop`, `MyDocuments`,
`ProgramFiles`, etc.
- Each entry in `Configurations` becomes one page of the dialog, saved to its
own file (`FileName`) inside the mod's user config folder
(`User/Mods/<ModId>`). Values missing from the file fall back to
`DefaultValue`.

The values are saved as a flat JSON file such as:

```json
{
"EnableThing": false,
"Volume": 10,
"Brightness": 0.25,
"Quality": "Low"
}
```

### Reading the Settings

#### C++

Using `ReloadedModConfig.h` from the [native mod template][native-template],
define `RELOADED_MOD_CONFIG_IMPL(your_start_function)` in
exactly one source file:

```cpp
#include "ReloadedModConfig.h"

static void my_start()
{
auto& config = reloaded::config();
bool enabled = config.get_bool("EnableThing", true);
long long volume = config.get_int("Volume", 75);
double brightness = config.get_float("Brightness", 1.5);
std::wstring file = config.get_wstring("CustomFile", L"");

static const char* quality[] = { "Low", "High" };
int qualityIndex = config.get_enum("Quality", quality, 2, 1);
}

RELOADED_MOD_CONFIG_IMPL(my_start)
```

Missing values fall back to the schema defaults, then to the fallback
argument. `config.watch(callback)` reloads the settings when the user changes
them while the game is running.

`reloaded::log(text)` writes to the Reloaded log through the loader API;
`reloaded::log_async(text)` queues the write instead, prefer it from hot paths
such as game hooks. The text is UTF-8; the helper header already asks MSVC to
encode narrow literals as UTF-8, and building with `/utf-8` does the same for
the whole project.

[native-template]: https://github.com/Reloaded-Project/Reloaded-II/tree/master/source/Reloaded.Mod.Template/templates/native
[native-header]: https://github.com/Reloaded-Project/Reloaded-II/blob/master/source/Reloaded.Mod.Template/templates/native/ReloadedModConfig.h
112 changes: 97 additions & 15 deletions source/Reloaded.Mod.Launcher.Lib/Commands/Mod/ConfigureModCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Native = Reloaded.Mod.Launcher.Lib.Models.Model.Configuration.Native;

namespace Reloaded.Mod.Launcher.Lib.Commands.Mod;

/// <summary>
Expand All @@ -19,6 +21,13 @@ public ConfigureModCommand(PathTuple<ModConfig>? modTuple, PathTuple<ModUserConf
_applicationTuple = applicationTuple;
}

/// <summary>
/// Full path of the mod's user config folder; null when the mod has none.
/// </summary>
private string? ExistingUserConfigFolder => _modUserConfigTuple != null
? Path.GetFullPath(Path.GetDirectoryName(_modUserConfigTuple.Path)!)
: null;

/* ICommand */

// Disallowed inlining to ensure nothing from library can be kept alive by stack references etc.
Expand Down Expand Up @@ -65,12 +74,57 @@ private bool TryGetConfiguratorDisposing()
// Disallowed inlining to ensure nothing from library can be kept alive by stack references etc.
[MethodImpl(MethodImplOptions.NoInlining)]
private bool TryGetConfigurator(out IConfiguratorV1? configurator, out PluginLoader? loader)
{
var modDirectory = Path.GetFullPath(Path.GetDirectoryName(_modTuple!.Path)!);

// Native (non .NET) mods describe their settings in a schema file, no managed code required.
if (Native.ModConfigSchema.ExistsInFolder(modDirectory))
{
loader = null;
configurator = CreateNativeConfigurator(modDirectory);
return true;
}

return TryGetManagedConfigurator(modDirectory, out configurator, out loader);
}

/// <summary>
/// Creates the configurator for a native mod.
/// </summary>
/// <remarks>
/// Throws when the settings schema is broken or the settings cannot move
/// to the user config folder.
/// </remarks>
private IConfiguratorV1 CreateNativeConfigurator(string modDirectory)
{
// Validate upfront, a broken schema disables the button instead of failing later.
Native.ModConfigSchema.Load(modDirectory);

// Native settings always live in the user config folder, creating the
// standard one when the mod has none yet.
string configDirectory = ExistingUserConfigFolder
?? ModUserConfig.GetUserConfigFolderForMod(_modTuple!.Config.ModId);

Directory.CreateDirectory(configDirectory);

var nativeConfigurator = new Native.ModConfigurator(modDirectory);
ConfigureConfigurator(nativeConfigurator, modDirectory, configDirectory);

return nativeConfigurator;
}

/// <summary>
/// Loads the configurator from the mod's .NET DLL, returning false when the
/// DLL is missing or holds no configurator.
/// </summary>
private bool TryGetManagedConfigurator(string modDirectory, out IConfiguratorV1? configurator, out PluginLoader? loader)
{
var config = _modTuple!.Config;
string dllPath = config.GetManagedDllPath(_modTuple.Path);
configurator = null;
loader = null;

string dllPath = config.GetManagedDllPath(_modTuple.Path);

if (!File.Exists(dllPath))
return false;

Expand All @@ -84,34 +138,62 @@ private bool TryGetConfigurator(out IConfiguratorV1? configurator, out PluginLoa
var assembly = loader.LoadDefaultAssembly();
var types = assembly.GetTypes();
var entryPoint = types.FirstOrDefault(t => typeof(IConfiguratorV1).IsAssignableFrom(t) && !t.IsAbstract);
if (entryPoint == null)

if (entryPoint == null)
return false;

configurator = (IConfiguratorV1)Activator.CreateInstance(entryPoint)!;
var modDirectory = Path.GetFullPath(Path.GetDirectoryName(_modTuple.Path)!);
ConfigureConfigurator(configurator, modDirectory, ExistingUserConfigFolder);

return true;
}

/// <summary>
/// Sets up a freshly created configurator with its mod directory, user
/// config location and application context.
/// </summary>
/// <param name="configurator">The configurator to set up.</param>
/// <param name="modDirectory">Full path to the mod's folder.</param>
/// <param name="configDirectory">Full path to the mod's user config
/// folder; null skips migration and leaves the location untouched.</param>
private void ConfigureConfigurator(IConfiguratorV1 configurator, string modDirectory, string? configDirectory)
{
configurator.SetModDirectory(modDirectory);

if (configurator is IConfiguratorV2 versionTwo && _modUserConfigTuple != null)
if (configurator is IConfiguratorV2 versionTwo && configDirectory != null)
{
var configDirectory = Path.GetFullPath(Path.GetDirectoryName(_modUserConfigTuple.Path)!);
versionTwo.Migrate(modDirectory, configDirectory);
MigrateConfigurator(versionTwo, modDirectory, configDirectory);
versionTwo.SetConfigDirectory(configDirectory);
}

if (configurator is IConfiguratorV3 versionThree)
versionThree.SetContext(CreateContext());
}

/// <summary>
/// Moves a configurator's config files to a new folder.
/// </summary>
private void MigrateConfigurator(IConfiguratorV2 configurator, string modDirectory, string configDirectory)
{
if (configurator is Native.ModConfigurator native)
{
versionThree.SetContext(new ConfiguratorContext()
{
Application = _applicationTuple.Config,
ModConfigPath = _modTuple.Path,
ApplicationConfigPath = _applicationTuple.Path
});
if (!native.TryMigrate(modDirectory, configDirectory))
throw new InvalidOperationException($"Could not move the settings of '{_modTuple!.Config.ModName}' from '{modDirectory}' to '{configDirectory}'.", native.MigrationError);
}
else
{
configurator.Migrate(modDirectory, configDirectory);
}

return true;
}

/// Builds the application/mod context handed to V3 configurators.
private ConfiguratorContext CreateContext() => new ConfiguratorContext()
{
Application = _applicationTuple.Config,
ModConfigPath = _modTuple!.Path,
ApplicationConfigPath = _applicationTuple.Path
};

[MethodImpl(MethodImplOptions.NoInlining)]
private void Execute_Internal()
{
Expand Down
Loading
Loading