-
Notifications
You must be signed in to change notification settings - Fork 1
Mod Support
Void Engine has built-in mod support through its mount-based virtual file system and discoverable type system. This allows players to modify your game without touching the original files.
Mod support in Void is built on two core systems:
| System | What It Does |
|---|---|
| Mount System | Allows mods to override or add assets |
| Discoverable System | Allows mods to register new types and systems |
These systems work together to enable full game modification.
Mounts are virtual file systems that the AssetManager searches for files. When you request an asset, the AssetManager checks each mount in order until it finds the file.
[Request] textures/player.png
|
|
v
[Mount 1: Mod]
|
|
+-- textures/player.png found!
|
v
[Return: Mod Version]
|
|
v
[Done]
[Mount 2: Base Game] -- Never checked
Mount Priority:
Mounts added to the front have higher priority. This means mods can override base game assets by adding their mount to the front.
// Base game mount (lowest priority)
AssetManager.Instance.AddMountToStart(new VirtualFileSystemMount());
// Mod mount (highest priority)
var modPack = AssetManager.Instance.LoadPack("Mods/MyMod.pack");
AssetManager.Instance.AddMountToStart(modPack); // Overrides base gameMods can be distributed as encrypted pack files, just like the base game assets.
// Load a mod pack
var modPack = AssetManager.Instance.LoadPack("Mods/MyMod.pack", "Mods/MyMod.key");
// Add it to the front so it overrides base game assets
AssetManager.Instance.AddMountToStart(modPack);Mods can also be loaded as loose files for development or simpler distribution.
// Add a loose file mod
var modMount = new VirtualFileSystemMount("Mods/MyMod/");
AssetManager.Instance.AddMountToStart(modMount);When loading multiple mods, the order matters. The last mod added to the front has the highest priority.
// Load mods in order of priority (highest first)
AssetManager.Instance.AddMountToStart(modPack3); // Highest priority
AssetManager.Instance.AddMountToStart(modPack2); // Medium priority
AssetManager.Instance.AddMountToStart(modPack1); // Lowest priorityA mod pack follows the same virtual path structure as the base game.
MyMod.pack
├── textures/
│ ├── player.png ← Overrides base game player texture
│ └── enemies/
│ └── goblin.png ← Overrides base game goblin
├── sounds/
│ └── jump.wav ← Overrides base game jump sound
├── levels/
│ └── level_01.ldtk ← Overrides base game level
└── config/
└── mod_settings.json ← New file (not in base game)
The same structure applies to loose file mods.
MyMod/
├── textures/
│ └── player.png
├── sounds/
│ └── jump.wav
└── config/
│ └── mod_settings.json
└── MyMod.dll ← Compiled code
The discoverable system allows mods to register their own types and systems at runtime.
DiscoverableAttribute marks a type that should be discoverable by the engine. The engine scans assemblies for types with this attribute and makes them available for use.
[Discoverable(Name = "MyMod", Category = "Mods", Priority = 10)]
public class MyMod : IMod
{
public void Initialize()
{
Console.WriteLine("MyMod loaded!");
}
}Mods can register their own types that the engine will discover automatically.
[Discoverable(Name = "CustomEntity", Category = "Entities")]
public class CustomEntity : MapEntity
{
// Custom entity implementation
}
[Discoverable(Name = "CustomSystem", Category = "Systems")]
public class CustomSystem : ISystem
{
// Custom system implementation
}// Find all mods
var modTypes = DiscoverableHelper.FindAll<IMod>();
// Find all entity types
var entityTypes = DiscoverableHelper.FindAll<MapEntity>();
// Find a specific mod by name
var myModType = DiscoverableHelper.FindSingleByName<IMod>("MyMod");
// Find mods in a specific category
var modsInCategory = DiscoverableHelper.FindManyByCategory<IMod>("Mods");// Find and instantiate all mods
var modTypes = DiscoverableHelper.FindAll<IMod>();
foreach (var modType in modTypes)
{
var mod = InstanceHelper.CreateInstanceFromType<IMod>(modType, null);
mod.Initialize();
}Mods are not limited to assets. They can also include compiled .NET assemblies (DLLs) that add new gameplay systems, entities, behaviors, and more.
| What Mods Can Include | How It Works |
|---|---|
| Assets | Textures, sounds, levels, config files through the mount system |
| Code | Compiled .NET assemblies loaded at runtime through the discoverable system |
| Systems | Custom game systems that integrate with the engine |
| Entities | New entity types that can be placed in levels |
| Behaviors | Custom behaviors and interactions |
| UI | Custom UI elements and screens |
| Anything else | Any .NET code that can be compiled into a DLL |
When a mod is loaded, the engine scans all assemblies in the mod pack or folder for types marked with DiscoverableAttribute. These types are then registered and available for use.
// The engine automatically scans mod assemblies
// Any type with [Discoverable] is registered
[Discoverable(Name = "NewEnemy", Category = "Entities")]
public class NewEnemy : MapEntity
{
public override void OnUpdate(FrameTime time)
{
// Custom enemy behavior
}
}
[Discoverable(Name = "NewSystem", Category = "Systems")]
public class NewSystem : ISystem
{
public void Initialize()
{
// Custom system initialization
}
}A mod with code follows this structure:
MyMod/
├── MyMod.dll ← Compiled code
├── textures/
│ └── enemy.png ← Assets
├── sounds/
│ └── roar.wav ← Assets
└── config/
└── settings.json ← Assets
| Capability | Description |
|---|---|
| Add new entities | Create new enemy types, NPCs, interactive objects |
| Add new systems | Add custom gameplay systems (weather, economy, etc.) |
| Override existing behavior | Replace or extend base game logic |
| Add new UI | Create custom menus, HUD elements, dialogs |
| Add new mechanics | Introduce new gameplay mechanics |
| Add new content types | Define new asset types beyond what the engine supports |
| Hook into events | Subscribe to beacons and respond to game events |
| Add new commands | Add console commands or debug tools |
[Discoverable(Name = "WeatherSystem", Category = "Systems")]
public class WeatherSystem : ISystem
{
private float _timer;
private Weather _currentWeather;
public void Initialize()
{
_currentWeather = Weather.Sunny;
BeaconManager.Instance.Subscribe(GameBeacons.WeatherChanged, OnWeatherChanged);
}
public void Update(FrameTime time)
{
_timer += time.DeltaTime;
if (_timer > 60f) // Change weather every minute
{
_timer = 0f;
ChangeWeather();
}
}
private void ChangeWeather()
{
var weathers = Enum.GetValues<Weather>();
_currentWeather = weathers[FastRandom.Shared.Next(weathers.Length)];
BeaconManager.Instance.Publish(GameBeacons.WeatherChanged, _currentWeather);
}
private void OnWeatherChanged(BeaconHandle handle)
{
if (handle.TryGet<Weather>(0, out var weather))
{
// Handle weather change
}
}
public void Unload()
{
BeaconManager.Instance.Unsubscribe(GameBeacons.WeatherChanged, OnWeatherChanged);
}
}Some mods are not meant to be used directly by players. They provide infrastructure, APIs, or shared functionality that other mods depend on. These are called library mods.
A library mod is a mod that does not add content directly. Instead, it provides code, systems, or APIs that other mods can use.
| Type | Description | Example |
|---|---|---|
| Content Mod | Adds content directly (items, enemies, levels) | A mod that adds new weapons |
| Library Mod | Provides infrastructure for other mods | A mod that adds a new API for other mods to use |
The LibraryModAttribute marks an assembly as a library mod. This tells the engine that the mod is not meant to be loaded directly by players.
[assembly: LibraryMod]// MyLibraryMod.cs
using Void.Engine;
using Void.Engine.Helpers;
[assembly: LibraryMod]
namespace MyLibraryMod
{
[Discoverable(Name = "MyLibrary", Category = "Libraries")]
public class MyLibrary : IMod
{
private static MyLibrary _instance;
public static MyLibrary Instance => _instance;
public void Initialize()
{
_instance = this;
// Initialize library systems
}
public void Load() { }
public void Unload() { }
// Public API for other mods to use
public void DoSomething()
{
// Library functionality
}
}
}// MyContentMod.cs
using Void.Engine;
using Void.Engine.Helpers;
[Discoverable(Name = "MyContentMod", Category = "Mods", Priority = 5)]
public class MyContentMod : IMod
{
public void Initialize()
{
// Use the library mod
var library = MyLibraryMod.MyLibrary.Instance;
library.DoSomething();
Console.WriteLine("MyContentMod loaded!");
}
public void Load() { }
public void Unload() { }
}| Without Library Mods | With Library Mods |
|---|---|
| Each mod must implement its own systems | Shared systems are in one place |
| Inconsistent behavior across mods | Consistent behavior across mods |
| No standard API for modders | Standard API for modders to build on |
| Duplicated code across mods | Code is shared and maintained in one place |
| Hard to build a modding community | Easier to build a modding community |
Library mods should be loaded before content mods that depend on them.
[Discoverable(Name = "MyLibrary", Category = "Libraries", Priority = 100)]
public class MyLibrary : IMod
{
// High priority = loaded first
}
[Discoverable(Name = "MyContentMod", Category = "Mods", Priority = 10)]
public class MyContentMod : IMod
{
// Lower priority = loaded after libraries
}public class ModManager
{
private List<IMod> _libraryMods = new List<IMod>();
private List<IMod> _contentMods = new List<IMod>();
public void LoadMods()
{
// Load all mod assemblies
var allModTypes = DiscoverableHelper.FindAll<IMod>();
foreach (var modType in allModTypes)
{
var mod = InstanceHelper.CreateInstanceFromType<IMod>(modType, null);
if (mod == null) continue;
// Check if this is a library mod
var assembly = modType.Assembly;
var isLibrary = assembly.GetCustomAttribute<LibraryModAttribute>() != null;
if (isLibrary)
{
_libraryMods.Add(mod);
}
else
{
_contentMods.Add(mod);
}
}
// Initialize library mods first
foreach (var mod in _libraryMods)
{
mod.Initialize();
}
// Then content mods
foreach (var mod in _contentMods)
{
mod.Initialize();
}
}
}Mods can depend on other mods. The discoverable system supports priority ordering, so you can control the load order.
[Discoverable(Name = "MyMod", Category = "Mods", Priority = 10)]
public class MyMod : IMod
{
// Higher priority = loaded first
}
[Discoverable(Name = "MyModAddon", Category = "Mods", Priority = 20)]
public class MyModAddon : IMod
{
// Lower priority = loaded after MyMod
}| Without Code Support | With Code Support |
|---|---|
| Mods can only change assets | Mods can change gameplay |
| Limited to texture and sound replacements | Full game modifications possible |
| No new mechanics | New mechanics can be added |
| Static content only | Dynamic content and behaviors |
| Simple mods | Complex mods with new systems |
Because mods can include compiled code, they have the same capabilities as the game itself. This is the tradeoff for full mod support. It is the same approach used by games like RimWorld, Cities: Skylines, and Factorio.
Step 1: Create a new class library
dotnet new classlib -n MyMod -f net10.0
cd MyMod
dotnet add reference ../Void.EngineStep 2: Implement the mod interface
using Void.Engine;
using Void.Engine.Helpers;
[Discoverable(Name = "MyMod", Category = "Mods", Priority = 10)]
public class MyMod : IMod
{
public void Initialize()
{
Console.WriteLine("MyMod initialized!");
// Add custom code here
}
public void Load()
{
// Load mod resources
}
public void Unload()
{
// Unload mod resources
}
}Step 3: Build the mod
dotnet build -c ReleaseStep 4: Package the mod
void-packer build -c bin/Release/net10.0/ -o Output/ -n MyModThis creates MyMod.pack and MyMod.key files.
Method 1: As a pack file
var modPack = AssetManager.Instance.LoadPack("Mods/MyMod.pack", "Mods/MyMod.key");
AssetManager.Instance.AddMountToStart(modPack);Method 2: As loose files
var modMount = new VirtualFileSystemMount("Mods/MyMod/");
AssetManager.Instance.AddMountToStart(modMount);Distribute the .pack and .key files. Users place them in the mods folder and the game loads them.
Game/
├── Game.exe
├── GameAssets.pack
├── GameAssets.key
└── Mods/
├── MyMod.pack
└── MyMod.key
public class ModManager
{
private List<IMod> _mods = new List<IMod>();
public void LoadMods()
{
// Load all mod packs from the Mods folder
var modPacks = AssetManager.Instance.LoadAllPacks("Mods/");
foreach (var modPack in modPacks)
{
AssetManager.Instance.AddMountToStart(modPack);
}
// Discover and initialize mod types
var modTypes = DiscoverableHelper.FindAll<IMod>();
foreach (var modType in modTypes)
{
var mod = InstanceHelper.CreateInstanceFromType<IMod>(modType, null);
if (mod != null)
{
mod.Initialize();
_mods.Add(mod);
}
}
}
public void UnloadMods()
{
foreach (var mod in _mods)
{
mod.Unload();
}
_mods.Clear();
}
}| Type | Attribute | Purpose | Load Order |
|---|---|---|---|
| Content Mod | [Discoverable] |
Adds content to the game | After libraries |
| Library Mod |
[Discoverable] + [assembly: LibraryMod]
|
Provides infrastructure for other mods | First |
| Feature | Description |
|---|---|
| Mount System | Virtual file systems that allow asset overriding |
| Discoverable Types | Automatic type discovery and registration |
| Mod Assemblies | Compiled .NET code loaded at runtime |
| Library Mods | Mods that provide infrastructure for other mods |
| Pack Loading | Load encrypted mod packs with keys |
| Loose File Loading | Load mods as loose files for development |
| Priority System | Control which mods override others |
| Mod Interface | Standard interface for mods to implement |