Skip to content

Mod Support

Shmellyorc edited this page Aug 30, 2026 · 1 revision

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.


Overview

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.


How It Works

The Mount System

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 game

Loading Mods as Packs

Mods 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);

Loading Mods as Loose Files

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);

Multiple Mods

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 priority

Mod Structure

Pack File Structure

A 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)

Loose File Structure

The same structure applies to loose file mods.

MyMod/
├── textures/
│   └── player.png
├── sounds/
│   └── jump.wav
└── config/
│   └── mod_settings.json
└── MyMod.dll             ← Compiled code

Discoverable System

The discoverable system allows mods to register their own types and systems at runtime.

What Is Discoverable?

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.

Using DiscoverableAttribute

[Discoverable(Name = "MyMod", Category = "Mods", Priority = 10)]
public class MyMod : IMod
{
    public void Initialize()
    {
        Console.WriteLine("MyMod loaded!");
    }
}

Registering Custom Types

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
}

Finding Discoverable Types

// 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");

Instantiating Discoverable Types

// Find and instantiate all mods
var modTypes = DiscoverableHelper.FindAll<IMod>();
foreach (var modType in modTypes)
{
    var mod = InstanceHelper.CreateInstanceFromType<IMod>(modType, null);
    mod.Initialize();
}

Mod Assemblies

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

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

Loading Mod Assemblies

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
    }
}

Mod Assembly Structure

A mod with code follows this structure:

MyMod/
├── MyMod.dll          ← Compiled code
├── textures/
│   └── enemy.png      ← Assets
├── sounds/
│   └── roar.wav       ← Assets
└── config/
    └── settings.json   ← Assets

What Mods Can Do With Code

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

Example: A Complete Mod with Code

[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);
    }
}

Library Mods

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.

What Is a Library Mod?

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

LibraryModAttribute

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]

Example: A Library Mod

// 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
        }
    }
}

Example: A Content Mod Using a Library Mod

// 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() { }
}

Why Library Mods Matter

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

Mod Loading Order with Libraries

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
}

Mod Manager with Library Support

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();
        }
    }
}

Mod Dependencies

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
}

Why This Matters

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

Security Note

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.


Mod Development

Creating a Mod

Step 1: Create a new class library

dotnet new classlib -n MyMod -f net10.0
cd MyMod
dotnet add reference ../Void.Engine

Step 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 Release

Step 4: Package the mod

void-packer build -c bin/Release/net10.0/ -o Output/ -n MyMod

This creates MyMod.pack and MyMod.key files.

Testing a Mod

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);

Distributing a Mod

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

Mod Loading Example

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();
    }
}

Summary of Mod Types

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

Summary

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

Next Steps

Clone this wiki locally