The Module System and Assembly Loading
Unturned's module system is the code-level extension mechanism that allows the game to load external assemblies at runtime, resolve their dependencies, initialize them through a standardized lifecycle interface, and shut them down cleanly on exit. It is the same system used to load the game's own core assemblies — UnturnedNexus and FrameworkNexus are discovered and initialized through exactly the same IModuleNexus interface that third-party plugin frameworks use.
This article documents every component of the module system: the .module configuration file format, the ModuleConfig / ModuleAssembly data model, the Module wrapper class, the ModuleHook MonoBehaviour that orchestrates discovery and initialization, the dependency resolution algorithm, the custom AssemblyResolve pipeline, and the IModuleNexus lifecycle interface.
Source code location: Framework/Modules/*.cs
Architecture Overview
The module system lives entirely in the SDG.Framework.Modules namespace. It is designed as a framework-level subsystem that runs before any game-specific code. The core types form a three-layer architecture:
| Layer | Class | Responsibility |
|---|---|---|
| Config | ModuleConfig | Data model for .module JSON file |
| Assembly | ModuleAssembly | Single assembly entry within a module config |
| Wrapper | Module | Runtime wrapper managing loading, initialization, shutdown |
| Discovery | ModuleHook | MonoBehaviour that orchestrates the full pipeline |
| Interface | IModuleNexus | Lifecycle contract for entry-point classes |
The .module Configuration File
Each module is defined by a .module file on disk. The files are JSON-formatted and parsed by Newtonsoft.Json via IOUtility.jsonDeserializer. The search path is Modules/ relative to the game's root directory, with recursion into subdirectories.
A minimal module file:
json
{
"Name": "MyModule",
"Version": "1.0.0.0",
"IsEnabled": true,
"Assemblies": [
{
"Path": "MyModule.dll",
"Role": "Both"
}
],
"Dependencies": []
}ModuleConfig Fields
| Field | Type | JSON Type | Default | Purpose |
|---|---|---|---|---|
IsEnabled | bool | boolean | true | If false, the module's assemblies are not loaded |
DirectoryPath | string | (set by loader) | — | Absolute path to the directory containing the .module file. Set by ModuleHook during loading. |
FilePath | string | (set by loader) | — | Absolute path to the .module file |
Name | string | string | "" | Module name used for dependency matching |
Version | string | string | "1.0.0.0" | Display version string |
Version_Internal | uint | (computed) | — | Version converted to a uint32 via Parser.getUInt32FromIP (IP-format version encoding) |
Dependencies | array | ModuleDependency[] | [] | Modules that must be loaded before this one |
Assemblies | array | ModuleAssembly[] | [] | Relative paths to .dll files to load |
ModuleDependency
csharp
public class ModuleDependency
{
public string Name; // Module name to depend on
public string Version; // Minimum required version
public uint Version_Internal; // Computed from Version
}Dependencies are checked during sortModules. If a dependency module is not present in the sorted list at a version equal to or higher than the required version, the dependent module is discarded with a warning logged.
ModuleAssembly
csharp
public class ModuleAssembly
{
public string Path; // Relative path to .dll file
public EModuleRole Role; // Client, Server, or Both_Required
public bool Load_As_Byte_Array; // Workaround for self-updating plugins
}The Role enumeration controls which platform loads the assembly:
| Enum value | Client | Dedicated Server |
|---|---|---|
None | Loaded | Loaded |
Client | Loaded | Removed from assembly list |
Server | Removed from assembly list | Loaded |
Both_Required | Loaded | Loaded; also discoverable via getRequiredModules |
The Load_As_Byte_Array flag exists because Assembly.LoadFile locks the file on disk, preventing self-updating plugin frameworks from replacing the DLL while the game is running. When true, Assembly.Load(byte[]) is used instead, which reads the bytes into memory and releases the file handle.
Module Discovery and Loading
Module discovery is orchestrated by ModuleHook, a MonoBehaviour that goes through three phases:
Phase 1: awake() — Discovery
ModuleHook.awake()
│
├─ AppDomain.CurrentDomain.AssemblyResolve += handleAssemblyResolve
├─ AppDomain.CurrentDomain.TypeResolve += OnTypeResolve
│
├─ coreAssembly = Assembly.GetExecutingAssembly()
├─ coreTypes = coreAssembly.GetTypes() // with ReflectionTypeLoadException catch
│
└─ loadModules()
│
├─ DiscoverAssemblies()
│ └─ Recursively scan Modules/ for *.dll files
│ Record AssemblyName→path mapping in discoveredNameToPath
│
├─ findModules()
│ └─ Recursively scan Modules/ for *.module files
│ Deserialize each .module → ModuleConfig
│ Set DirectoryPath, FilePath, Version_Internal
│ Remove stale "Framework" and "Unturned" dependencies
│
├─ sortModules(configs)
│ ├─ Sort by dependency using ModuleComparer
│ ├─ Remove configs missing assemblies or dependencies
│ └─ Remove assemblies with role mismatches
│
└─ For each config:
module = new Module(config) ← registers assembly paths
modules.Add(module)The DiscoverAssemblies() method scans all *.dll files in the Modules/ directory tree and stores their AssemblyName objects. This pre-discovery step enables the custom AssemblyResolve handler to find dependency assemblies that are not explicitly listed in any .module file's assembly list — a critical feature for plugin frameworks that need to resolve their own transitive dependencies.
Phase 2: start() — Core Nexus and Module Initialization
ModuleHook.start()
│
├─ coreNexii = new List<IModuleNexus>()
├─ For each type in coreTypes:
│ └─ if (type.IsAssignableFrom(IModuleNexus) && !type.IsAbstract)
│ nexus = Activator.CreateInstance(type)
│ nexus.initialize()
│ coreNexii.Add(nexus)
│
└─ initializeModules()
└─ For each module in modules:
├─ Check IsEnabled
├─ Check dependency enablement
├─ Check command-line disable flag
└─ module.isEnabled = true
├─ module.load() → resolve assemblies, get types
└─ module.initialize() → find IModuleNexus, call initialize()The core assembly scan in ModuleHook.start() is how UnturnedNexus and FrameworkNexus are discovered. They are not loaded from .module files — they live in the game's main Assembly-CSharp.dll and are found via Assembly.GetExecutingAssembly().GetTypes().
After core nexii are initialized, initializeModules() iterates through the discovered modules in order. Each module's isEnabled setter triggers load() followed by initialize().
Phase 3: OnDestroy() — Shutdown
ModuleHook.OnDestroy()
│
└─ shutdownModules()
└─ For each module in reverse order:
module.isEnabled = false
└─ module.shutdown()
└─ Call shutdown() on each IModuleNexus
└─ For each coreNexus:
nexus.shutdown()
└─ AppDomain.CurrentDomain.AssemblyResolve -= handleAssemblyResolve
└─ AppDomain.CurrentDomain.TypeResolve -= OnTypeResolveShutdown happens in reverse dependency order (modules that depend on others are shut down first). Core nexii are shut down after all module nexii. The AssemblyResolve handler is unregistered last.
The Module Class
Module wraps a ModuleConfig and manages the runtime lifecycle of its assemblies. The key fields are:
csharp
public class Module
{
public bool isEnabled; // Config enabled AND dependencies enabled
public ModuleConfig config; // The parsed .module data
public Assembly[] assemblies; // Loaded assembly objects
public Type[] types; // All types from all assemblies
public EModuleStatus status; // None → Initialized → Shutdown
private List<IModuleNexus> nexii; // Holds references to prevent GC
}Module.register()
Called in the constructor. Iterates over config.Assemblies and calls ModuleHook.registerAssemblyPath(path, loadAsByteArray) for each one. This step populates the global assembly path registry that the AssemblyResolve handler uses.
Module.load()
Called when isEnabled is set to true. For each assembly in the config, it calls ModuleHook.resolveAssemblyPath() which returns a loaded Assembly object. It collects all types across all assemblies (catching ReflectionTypeLoadException to handle assemblies with missing dependencies), and fires the onModuleLoaded event.
Module.initialize()
Searches all types in the module for classes that implement IModuleNexus and are not abstract. For each matching type, it instantiates via Activator.CreateInstance and calls nexus.initialize(). The nexus instances are stored in the nexii list to prevent garbage collection. Sets status = EModuleStatus.Initialized.
Module.shutdown()
Iterates backward through the nexii list, calling shutdown() on each. Clears the list and sets status = EModuleStatus.Shutdown.
The Custom AssemblyResolve Pipeline
The ModuleHook.handleAssemblyResolve method is registered with AppDomain.CurrentDomain.AssemblyResolve and intercepts all assembly resolution requests during the game's lifetime. The pipeline has six stages, with three plugin-settable events for third-party frameworks:
handleAssemblyResolve(args)
│
├─ Stage 1: PreVanillaAssemblyResolve
│ └─ Plugin framework override (e.g., Rocket handles its own deps)
│
├─ Stage 2: Backward-compatible redirects
│ ├─ "Assembly-CSharp-firstpass" → com.rlabrecque.steamworks.net
│ └─ "Steamworks.NET" → com.rlabrecque.steamworks.net
│
├─ Stage 3: PreVanillaAssemblyResolvePostRedirects
│ └─ Second plugin override (after redirects but before vanilla search)
│
├─ Stage 4: resolveAssemblyName(name)
│ └─ Look up pre-registered assemblies by full name
│
├─ Stage 5: LoadAssemblyFromDiscoveredPaths (if enabled)
│ └─ Search Modules/ for matching DLLs by name + version
│
└─ Stage 6: PostVanillaAssemblyResolve
└─ Final plugin overrideThe pipeline is designed to give plugin frameworks like RocketMod the ability to intercept resolution at any stage. If PreVanillaAssemblyResolve returns an assembly, the vanilla pipeline is skipped entirely. The resolveAssemblyName method checks the nameToAssembly dictionary (already-loaded assemblies) and then the nameToPath dictionary (registered assembly file paths). For file lookups, it reads the bytes with File.ReadAllBytes and loads via Assembly.Load(byte[]) if loadAsByteArray is set, or Assembly.LoadFile otherwise.
The LoadAssemblyFromDiscoveredPaths method is the fallback that searches all *.dll files discovered during DiscoverAssemblies(). It matches by assembly name and minimum version. This step is enabled by default but can be disabled with the -NoVanillaAssemblySearch command-line flag (useful when a third-party framework has its own resolution and the vanilla search conflicts with it).
The IModuleNexus Interface
csharp
public interface IModuleNexus
{
void initialize();
void shutdown();
}Despite its simplicity, this interface is the entire extension surface for Unturned's module system. Any class in any loaded assembly that implements IModuleNexus will have its initialize() called when the module is enabled, and shutdown() called when the module is disabled or the game exits.
The UnturnedNexus class is the primary implementation. Its initialize() method registers every built-in asset type and useable type:
csharp
public void initialize()
{
Assets.assetTypes.addType("Hat", typeof(ItemHatAsset));
Assets.assetTypes.addType("Gun", typeof(ItemGunAsset));
// ... 50+ more asset type registrations
Assets.assetTypes.addType("Road", typeof(RoadAsset));
Assets.useableTypes.addType("Gun", typeof(UseableGun));
Assets.useableTypes.addType("Melee", typeof(UseableMelee));
// ... 20+ more useable type registrations
}The FrameworkNexus class is the second implementation. Its initialize() and shutdown() are currently empty — it exists as a placeholder for framework-level initialization that may be needed in future versions.
Dependency Resolution Algorithm
The sortModules method in ModuleHook implements a topological sort over module dependencies. The algorithm has three stages:
Stage 1: Sort by ModuleComparer
— Uses custom comparer that orders by dependency depth
— Produces initial ordering
Stage 2: Validate assemblies
For each config in sorted order:
For each assembly in config.Assemblies:
— Remove if Role mismatch (Client assembly on server, etc.)
— Check for directory escape attacks (".." in path)
— Check that the .dll file exists on disk
If any assembly fails or list becomes empty:
— Remove config from list
— Log "discard module ... because it has no assemblies"
Stage 3: Validate dependencies
For each dependency in config.Dependencies:
— Search earlier configs (lower index) for matching Name
— If found, check that Version_Internal >= dependency.Version_Internal
— If not found or version insufficient:
— Remove config from list
— Log "discard module ... because dependency ... wasn't met"The dependency check uses Version_Internal, which is computed by Parser.getUInt32FromIP. This converts a dotted version string (e.g., "1.2.3.4") into a single uint32 that can be compared numerically. The conversion treats each octet as a byte in a uint32:
"1.2.3.4" → 0x01020304 → 16909060 (uint32)Only earlier-indexed configs are searched for dependencies, which enforces a strict partial ordering: a module cannot depend on a module that appears later in the sorted list. The sort order is not guaranteed to be unique for cyclic dependencies — the ModuleComparer does not detect cycles.
Module Enablement Cascading
When a module is toggled on or off at runtime (via the in-game mod menu or the toggleModuleEnabled API), the system cascades the change to dependent modules:
toggleModuleEnabled(index, enable=true)
│
└─ modules[index].isEnabled = true
│
└─ For each module after index:
└─ If module depends on enabled module:
└─ updateModuleEnabled(moduleIndex, true)
toggleModuleEnabled(index, enable=false)
│
└─ For each module after index in reverse:
└─ If module depends on disabled module:
└─ updateModuleEnabled(moduleIndex, false)
│
└─ modules[index].isEnabled = falseThe cascading ensures that turning off a utility module automatically turns off all modules that depend on it, and turning one on re-enables any dependents whose other dependencies are also satisfied. The areModuleDependenciesEnabled check validates that all dependencies are still enabled before enabling a dependent.
Command-Line Module Control
Modules can be disabled at launch time without editing the .module file using the command-line flag format:
-disableModule/ModuleNameThe isModuleDisabledByCommandLine method searches the command line string for the module name preceded by -disableModule/. This allows server operators to selectively disable modules without filesystem write access to the Modules directory.
Module Path Override
The -ModulesPath command-line flag redirects module discovery to a different directory:
-ModulesPath D:\CustomModulesWhen set, module discovery uses the specified path instead of the default Unturned/Modules/. This is used by plugin frameworks that maintain their own module directories.
Architecture Diagram
Modules/
└── MyMod/
├── MyMod.module
└── MyMod.dll
ModuleHook (MonoBehaviour)
│
├── awake()
│ ├── DiscoverAssemblies() — scan for all *.dll
│ ├── findModules() — scan for *.module
│ ├── sortModules() — topological sort + validation
│ └── For each config → new Module(config)
│ └── Module.register() → registerAssemblyPath()
│
├── start()
│ ├── Find IModuleNexus in core assembly
│ │ ├── UnturnedNexus.initialize() — register asset types
│ │ └── FrameworkNexus.initialize() — (empty)
│ │
│ └── initializeModules()
│ └── For each Module:
│ └── isEnabled = true
│ ├── Module.load() — resolve assemblies
│ │ ├── ModuleHook.resolveAssemblyPath()
│ │ └── Assembly.Load / Assembly.LoadFile
│ │
│ ├── Module.initialize()
│ │ └── Find IModuleNexus in types
│ │ └── Activator.CreateInstance
│ │ └── nexus.initialize()
│ │
│ └── onModuleInitialized event
│
└── OnDestroy()
└── shutdownModules() — reverse order
└── Module.isEnabled = false
└── Module.shutdown()
└── nexus.shutdown() for each nexusIModuleNexus Lifecycle Summary
┌──────────────────────────────┐
│ AppDomain.CurrentDomain │
│ AssemblyResolve │
│ + TypeResolve │
│ │
│ ModuleHook registers these │
│ in awake() │
└──────────┬───────────────────┘
│
┌──────────────────────┴──────────────────────┐
│ ModuleHook.start() │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Core assembly │ │ Module modules[] │ │
│ │ IModuleNexus │ │ │ │
│ │ scan │ │ Sorted by dep │ │
│ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ nexus.initialize() module.isEnabled=true │
│ │ │
│ Module.load() │
│ Module.initialize() │
│ │ │
│ module.nexus │
│ .initialize() │
└──────────────────────────────────────────────┘
│
┌──────────────────────┴──────────────────────┐
│ ModuleHook.OnDestroy() │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Modules reverse │ │ Core nexus │ │
│ │ shutdown │ │ shutdown │ │
│ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ nexus.shutdown() nexus.shutdown() │
│ │ │
│ Unhook AssemblyResolve │
│ Unhook TypeResolve │
└──────────────────────────────────────────────┘Plugin Framework Integration
The module system provides three deliberate extension points for plugin frameworks:
Assembly resolution events (
PreVanillaAssemblyResolve,PreVanillaAssemblyResolvePostRedirects,PostVanillaAssemblyResolve) — allow frameworks to intercept dependency resolution before or after the vanilla pipeline.Assembly discovery — the
DiscoveredAssemblies()step can be supplemented with the-NoVanillaAssemblySearchflag if a framework wants complete control over DLL resolution.The IModuleNexus interface — any framework can define its own
IModuleNexusclass in a module assembly and use itsinitialize()to set up framework-level hooks.
The Load_As_Byte_Array flag on ModuleAssembly exists specifically to support self-updating plugin frameworks. When a framework distributes updates via workshop content, the old DLL must be replaceable while the game is running. Byte-array loading releases the file handle, allowing the update to overwrite the file.
Module System Thread Safety
The module system is not thread-safe. All module loading, initialization, and shutdown occurs on the main Unity thread. The ModuleHook.Awake, ModuleHook.Start, and ModuleHook.OnDestroy callbacks are all called by Unity on the main thread. The Module.isEnabled setter, which triggers assembly loading and nexus initialization, is also designed for main-thread use.
The AssemblyResolve callback, however, can be called on any thread. The resolve handler is thread-safe because:
nameToPathis populated duringModule.Register()(main thread, before any module loading)nameToAssemblyis populated duringresolveAssemblyName(called fromAssemblyResolvewhich can be any thread)discoveredNameToPathis populated duringDiscoverAssemblies()(main thread, before any resolution requests)
Module Architecture Best Practices
When designing a module for Unturned, the following patterns are recommended based on the SDK source:
Single IModuleNexus per module: Each module should have exactly one class implementing
IModuleNexus. Having multiple entry points makes the initialization order unpredictable.Explicit assembly list: List all required DLLs in the
Assembliesarray. The auto-discovery fallback (DiscoveredAssemblies) is intended for framework transitive dependencies, not for primary module assemblies.Load_As_Byte_Arrayfor updatable modules: If the module or its dependencies may need to be updated while the game is running (workshop content updates), setLoad_As_Byte_Array: trueto avoid file locking.Version your dependencies: Specify minimum version numbers in
Dependenciesentries. The version comparison usesVersion_Internalwhich compares numerically, not as strings.Use
ServerorClientrole for platform-specific assemblies: A module with a server-only assembly and a client-only assembly should use two separateModuleAssemblyentries with different roles.Dependency order: Modules are sorted by their dependency graph before initialization. If module A depends on module B, B is guaranteed to be initialized before A and shut down after A.
Module Version Encoding
Module versions are encoded using IP-format conversion:
csharp
config.Version_Internal = Parser.getUInt32FromIP(config.Version);This converts a dotted decimal string (e.g., "1.2.3.4") into a uint32 by treating each octet as a byte:
"0.0.0.0" → 0x00000000 → 0
"1.0.0.0" → 0x01000000 → 16777216
"1.2.0.0" → 0x01020000 → 16908288
"1.2.3.0" → 0x01020300 → 16909056
"1.2.3.4" → 0x01020304 → 16909060
"255.255.255.255" → 0xFFFFFFFF → 4294967295The dependency version check uses >= comparison on the uint32 values, meaning version 1.2.3.4 satisfies a dependency requirement of 1.2.3.0 but not 1.2.4.0. Version 2.0.0.0 satisfies 1.9.9.9 because 0x02000000 ≥ 0x01090909.
The conversion only handles four-octet version strings. Modules using non-standard version strings (e.g., "1.0-beta") will fail to parse and result in Version_Internal = 0, which means they will not satisfy any minimum version dependency.
Module Directory Structure Conventions
The standard directory structure for a module:
Modules/
└── MyModule/
├── MyModule.module ← Required: Module configuration
├── MyModule.dll ← Required: Module assembly
├── MyModule.pdb ← Optional: Debug symbols
└── Dependencies/
└── SomeLib.dll ← Optional: Dependency assembliesThe .module file path references are relative to the directory containing the .module file:
json
{
"Name": "MyModule",
"Version": "1.0.0.0",
"IsEnabled": true,
"Assemblies": [
{
"Path": "MyModule.dll",
"Role": "Both"
}
],
"Dependencies": [
{
"Name": "SomeOtherModule",
"Version": "2.0.0.0"
}
]
}The ModuleConfig.DirectoryPath is set by ModuleHook.findModules() to the directory containing the .module file. Assembly paths are concatenated with this directory: config.DirectoryPath + assembly.Path.
The path validation in sortModules checks for .. directory escape attacks:
csharp
bool escapeDirectory = false;
for (int charIndex = 1; charIndex < assembly.Path.Length; charIndex++)
{
if (assembly.Path[charIndex] == '.' && assembly.Path[charIndex - 1] == '.')
{
escapeDirectory = true;
break;
}
}
if (escapeDirectory)
{
hasAssemblies = false; // Discard module
}This check prevents modules from loading assemblies outside their directory tree.
Debugging Module Loading Problems
When a module fails to load or initialize, the following diagnostic information is available:
Module file parse errors — Caught in
findModules():csharpcatch (Exception exception) { UnturnedLog.exception(exception, $"Caught exception parsing .module file: {moduleFile}"); }Assembly load errors — Caught in
Module.load():csharpcatch (ReflectionTypeLoadException exception) { assemblyTypes = exception.Types; // Partial type list }Missing dependency errors — Logged in
sortModules():csharpUnturnedLog.warn($"Discard module \"{config.Name}\" because dependency \"{dependency.Name}\" wasn't met");Missing assembly errors — Logged in
sortModules():csharpUnturnedLog.warn($"Module \"{config.Name}\" missing assembly: {assemblyPath}");Nexus initialization errors — Caught in
Module.initialize():csharpcatch (Exception ex) { UnturnedLog.error($"Caught exception while initializing module \"{config.Name}\" entry point \"{type.Name}\":"); UnturnedLog.exception(ex); }
All diagnostic output goes through UnturnedLog, which writes to both the Unity console and the game's log file.
FAQ
Can a module load assemblies from outside the Modules directory?
Yes, but only through the -ModulesPath command-line flag, which redirects the entire module discovery root to a different directory. There is no per-module mechanism for loading from arbitrary paths. The AssemblyResolve pipeline can resolve individual DLLs by name from any discovered path.
What happens if two modules have the same name?
Module names are used as unique identifiers in the dependency system. If two modules have the same Name field, only the first one encountered during discovery is kept. The duplicate is discarded with a log warning.
Can a module be enabled/disabled without restarting the game?
Yes. The ModuleHook.toggleModuleEnabled(int index) method toggles the IsEnabled flag in the .module file, then calls updateModuleEnabled which sets module.isEnabled = true/false. The enablement cascades to dependent modules. This is used by the in-game mod configuration UI.
How does the module system interact with third-party anti-cheat?
When third-party anti-cheat is active on the client, shouldLoadModules returns false, which prevents any module assemblies from being loaded. The ModuleHook.loadModules method checks this flag and skips all module loading if anti-cheat is enabled. On the dedicated server, modules are always loaded regardless of anti-cheat status.
Can a module reference the core game assembly?
Yes. The core assembly (Assembly-CSharp.dll) is already loaded when modules are initialized. Module assemblies can reference any public type in the core assembly. The AssemblyResolve handler does not need to resolve the core assembly because it is already in the AppDomain.
What happens if a module's IModuleNexus.initialize() throws an exception?
The exception is caught by Module.initialize():
csharp
catch (Exception ex)
{
UnturnedLog.error($"Caught exception while initializing module \"{config.Name}\" entry point \"{type.Name}\":");
UnturnedLog.exception(ex);
}The module continues loading (other nexus classes in the same assembly are still initialized). The failed nexus is not added to the nexii list, so shutdown() will not be called on it.
Does the module system support hot-reloading?
No. Modules are loaded once during startup and shut down during game exit. There is no mechanism to unload and reload assemblies at runtime without restarting the game. The Load_As_Byte_Array flag exists for self-updating plugin frameworks that replace DLLs between restarts, not for hot-reloading within a session.
How do I debug module loading issues?
Use these command-line flags:
| Flag | Information logged |
|---|---|
-LogAssemblyResolve | Every assembly resolution request and its result |
-NoVanillaAssemblySearch | Disable auto-discovery to test explicit assembly lists |
The module system logs all discovery, sorting, and initialization steps via UnturnedLog.info. Check the game's log file for messages starting with:
- "Looking for module files in:"
- "Discovered assembly..."
- "Found N module(s):"
- "Discard module ... because ..."
- "Initialized module ..."
Appendix: Module System Class Reference
| Class | Namespace | Source file | Role |
|---|---|---|---|
ModuleConfig | SDG.Framework.Modules | ModuleConfig.cs | Parsed .module config data |
ModuleAssembly | SDG.Framework.Modules | ModuleAssembly.cs | Single assembly entry |
ModuleDependency | SDG.Framework.Modules | ModuleDependency.cs | Dependency requirement |
Module | SDG.Framework.Modules | Module.cs | Runtime module wrapper |
ModuleHook | SDG.Framework.Modules | ModuleHook.cs | MonoBehaviour orchestrator |
ModuleComparer | SDG.Framework.Modules | ModuleComparer.cs | Dependency-based sorter |
IModuleNexus | SDG.Framework.Modules | IModuleNexus.cs | Lifecycle interface |
EModuleRole | SDG.Framework.Modules | EModuleRole.cs | Assembly role enum |
EModuleStatus | SDG.Framework.Modules | EModuleStatus.cs | Module lifecycle state |
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Module system architecture, assembly resolution, dependency ordering, IModuleNexus lifecycle. |
