Class ScyllaModuleManager
The concrete, sealed implementation of IScyllaModuleManager and the central orchestrator of SMUT (Scylla ModUlar Topology). Manages the complete lifecycle of all Scylla modules - from discovery and registration through dependency validation, ordered initialization, runtime start, and eventual shutdown - enforcing the framework's fail-fast philosophy at every stage.
Implements
Inherited Members
Namespace: Scylla.Core.Modules
Assembly: ScyllaCore.dll
Syntax
public sealed class ScyllaModuleManager : IScyllaModuleManager
Remarks
Lifecycle pipeline (normal startup):
- Modules self-register during
Awake()via RegisterModule(IScyllaModule). - ValidateAllDependencies() injects the manager into every module, validates each module's declared dependencies, and checks the hard-dependency graph for cycles.
- InitializeAllModules() builds a deterministic initialization order (topological sort of hard dependencies, tie-broken by InitPriority) and calls Initialize() on each module in that order.
- StartAllModules() calls StartModule() then EnableModule() on each module in initialization order.
Late / dynamic registration: If RegisterModule(IScyllaModule) is called after InitializeAllModules() has completed, the module is immediately run through the full inject -> validate -> initialize -> start -> enable pipeline. If any step fails the registration is rolled back and the module is removed from the registry.
Thread safety: All registry mutations and reads are serialized through an
internal lock(_lock). Lifecycle methods that call out to module code release the lock
first (snapshot pattern) to avoid deadlocks when module code calls back into the manager.
Zombie modules: Modules that have been shut down but not explicitly unregistered remain in the registry in the Shutdown state. They are filtered out by Modules, ModuleCount, and GetModule(string), but are visible to HasModule(string). Call CleanupZombieModules(bool) to permanently remove them and allow garbage collection.
Properties
ModuleCount
Gets the number of registered modules that are not in the Shutdown state.
Declaration
public int ModuleCount { get; }
Property Value
| Type | Description |
|---|---|
| int | A non-negative integer representing the count of active modules. Modules in Shutdown are excluded so this value accurately reflects the number of live modules currently managed by the framework. |
Remarks
Zombie (shutdown) modules that remain in the registry are not counted. Use CleanupZombieModules(bool) to remove them permanently.
Modules
Gets a read-only snapshot of all registered modules that are not in the Shutdown state.
Declaration
public IReadOnlyList<IScyllaModule> Modules { get; }
Property Value
| Type | Description |
|---|---|
| IReadOnlyList<IScyllaModule> | A new IReadOnlyList<T> containing every active module at the moment the property is read. Modules in Shutdown are excluded to prevent callers from accidentally interacting with zombie modules. |
Remarks
Each access allocates a new list (snapshot under lock). For hot-path code, prefer caching the result or using GetModulesInState(ScyllaModuleState) to narrow the query. Call CleanupZombieModules(bool) to permanently remove shutdown modules from the internal registry.
Methods
CleanupZombieModules(bool)
Scans the registry for modules in Shutdown state and permanently removes them, preventing memory leaks caused by modules that were shut down without an explicit call to UnregisterModule(string).
Declaration
public int CleanupZombieModules(bool suppressWarning = false)
Parameters
| Type | Name | Description |
|---|---|---|
| bool | suppressWarning | When |
Returns
| Type | Description |
|---|---|
| int | The number of zombie modules that were removed from the registry. Returns |
Remarks
A "zombie module" is a module that has been shut down (its State is Shutdown) but whose entry has not been removed from the Scylla.Core.Modules.ScyllaModuleManager._modules dictionary. This can occur when code calls Shutdown() directly rather than routing through UnregisterModule(string).
In addition to removing entries from Scylla.Core.Modules.ScyllaModuleManager._modules, this method also purges any lingering references from Scylla.Core.Modules.ScyllaModuleManager._initializationOrder to keep the shutdown sequence consistent.
The entire scan and removal is performed inside the lock to ensure consistency with concurrent late-registration activity.
See Also
GetModule(string)
Retrieves a registered module by its unique identifier, excluding modules in the Shutdown state.
Declaration
public IScyllaModule GetModule(string moduleID)
Parameters
| Type | Name | Description |
|---|---|---|
| string | moduleID | The unique identifier of the module to retrieve. Returns |
Returns
| Type | Description |
|---|---|
| IScyllaModule | The IScyllaModule instance associated with |
Remarks
If a module is found but is in Shutdown, a warning is
logged and null is returned to prevent callers from operating on a zombie module.
To check whether a module ID exists in the registry regardless of state (e.g. during
cleanup), use HasModule(string) instead.
See Also
GetModule<T>(string)
Retrieves a registered module by its unique identifier and casts it to the specified concrete or interface type.
Declaration
public T GetModule<T>(string moduleID) where T : class, IScyllaModule
Parameters
| Type | Name | Description |
|---|---|---|
| string | moduleID | The unique identifier of the module to retrieve. Passed directly to GetModule(string). |
Returns
| Type | Description |
|---|---|
| T | The module cast to |
Type Parameters
| Name | Description |
|---|---|
| T | The type to cast the retrieved module to. Must be a reference type that implements IScyllaModule (e.g., a concrete module class or a more specific module interface). |
Remarks
This overload is a convenience wrapper around GetModule(string) followed by
an as cast. All filtering and zombie-exclusion logic from the non-generic overload
applies here as well.
See Also
GetModulesInState(ScyllaModuleState)
Returns all registered modules that are currently in the specified lifecycle state.
Declaration
public List<IScyllaModule> GetModulesInState(ScyllaModuleState state)
Parameters
| Type | Name | Description |
|---|---|---|
| ScyllaModuleState | state | The ScyllaModuleState value to filter by. Pass Shutdown to enumerate zombie modules that have not yet been cleaned up. |
Returns
| Type | Description |
|---|---|
| List<IScyllaModule> | A new List<T> containing every module whose
State equals |
Remarks
Unlike Modules, this method includes modules in Shutdown when that state is explicitly requested. This is useful for diagnostics, cleanup checks, or framework tooling that needs a complete picture of the registry.
See Also
HasModule(string)
Determines whether a module with the specified ID is present in the registry, regardless of its current lifecycle state.
Declaration
public bool HasModule(string moduleID)
Parameters
| Type | Name | Description |
|---|---|---|
| string | moduleID | The unique identifier to look up. Returns |
Returns
| Type | Description |
|---|---|
| bool |
|
Remarks
Unlike GetModule(string), this method does not filter out shutdown (zombie) modules. This is intentional: callers performing cleanup checks need to know whether any entry - even a dead one - exists before deciding whether to call CleanupZombieModules(bool) or UnregisterModule(string). For a state-aware existence check, call GetModule(string) and test the return value for non-null.
See Also
InitializeAllModules()
Initializes all registered modules in deterministic dependency order. This is the third
step in the normal startup sequence, called after ValidateAllDependencies()
returns true.
Declaration
public void InitializeAllModules()
Remarks
The initialization order is computed once by Scylla.Core.Modules.ScyllaModuleManager.BuildInitializationOrder() and cached in Scylla.Core.Modules.ScyllaModuleManager._initializationOrder. The order respects hard-dependency constraints (dependencies always initialize before their dependents) and uses InitPriority to break ties between modules at the same dependency depth.
Only modules in Validated state are included in the computed order. Any module still in Discovered at this point was not validated and will be skipped.
If this method is called a second time after a successful initialization it logs a warning and returns without doing anything. Call ShutdownAllModules() first to reset the framework.
The entire method body executes inside lock(_lock) to prevent concurrent
late-registration from observing a partially initialized state.
Exceptions
| Type | Condition |
|---|---|
| ScyllaModuleException | Thrown when any module's Initialize() call throws. Before re-throwing, all successfully initialized modules are shut down in reverse order (rollback), leaving the framework in a clean, uninitialized state. The exception message identifies the failing module ID and wraps the original exception as the inner exception. |
See Also
RegisterModule(IScyllaModule)
Registers a module with the framework. If the framework has already been initialized (i.e., InitializeAllModules() has completed), the module is immediately run through the full late-registration pipeline: dependency injection, dependency validation, initialization, start, and enable. On any failure the module is removed from the registry and a ScyllaModuleException is thrown.
Declaration
public void RegisterModule(IScyllaModule module)
Parameters
| Type | Name | Description |
|---|---|---|
| IScyllaModule | module | The module instance to register. Must not be |
Remarks
During normal framework startup (before InitializeAllModules() is called), registration simply adds the module to the internal registry and logs a debug message. The full lifecycle pipeline is deferred to the explicit ValidateAllDependencies(), InitializeAllModules(), and StartAllModules() calls.
During late (dynamic) registration the registration itself is committed inside the lock
before the initialization pipeline begins. This ensures HasModule(string) returns
true for the duration of the pipeline. If the pipeline fails the registration is
rolled back while still holding the lock.
Modules typically call this method from their Awake() override via
base.Awake(), which routes through ScyllaModule.
Exceptions
| Type | Condition |
|---|---|
| ScyllaModuleException | Thrown in the following cases:
|
See Also
ShutdownAllModules()
Shuts down all modules in reverse initialization order, clears the initialization order list, resets the initialized flag, and removes all zombie modules from the registry.
Declaration
public void ShutdownAllModules()
Remarks
Shutdown is performed in the strict reverse of the order established by Scylla.Core.Modules.ScyllaModuleManager.BuildInitializationOrder(), ensuring that a module is always shut down before its dependencies. This mirrors the initialization contract.
If a module's Shutdown() call throws, the exception is caught, logged as an error, and the shutdown sequence continues. This guarantees that a single misbehaving module cannot prevent the rest of the framework from shutting down cleanly.
After all shutdown calls complete, CleanupZombieModules(bool) is invoked with
suppressWarning = true because shutdown modules at this point are expected, not
leaked.
See Also
StartAllModules()
Starts all initialized modules in initialization order and immediately enables each one after it starts. This is the fourth and final step in the normal startup sequence, called after InitializeAllModules() completes successfully.
Declaration
public void StartAllModules()
Remarks
For each module in Scylla.Core.Modules.ScyllaModuleManager._initializationOrder, this method calls StartModule() followed by EnableModule(). A module is only considered "successfully started" and added to the rollback list after both calls succeed.
The enable step honors the module's Inspector state. A module that is a Unity
component which is unchecked in the Inspector (or whose GameObject is
inactive) is started but not enabled, and rests in
Started until its component is enabled. Modules that
are not Unity components are always enabled. See AutoEnableAfterStart(IScyllaModule).
If InitializeAllModules() has not been called yet (i.e. Scylla.Core.Modules.ScyllaModuleManager._isInitialized
is false), the method logs an error and returns without action. Calling this
method before initialization is a programming error and should be corrected in the
startup sequence.
The lock is held only long enough to take a snapshot of the initialization order. The start and enable calls happen outside the lock to permit concurrent late registrations.
Exceptions
| Type | Condition |
|---|---|
| ScyllaModuleException | Thrown when any module's StartModule() or EnableModule() call throws. Before re-throwing, all successfully started modules are shut down in reverse order (rollback). The exception message identifies the failing module ID and wraps the original exception as the inner exception. |
See Also
UnregisterModule(string)
Unregisters a module from the framework, shutting it down first if it has not already been shut down.
Declaration
public bool UnregisterModule(string moduleID)
Parameters
| Type | Name | Description |
|---|---|---|
| string | moduleID | The unique identifier of the module to unregister. If |
Returns
| Type | Description |
|---|---|
| bool |
|
Remarks
If the module's current state is neither Shutdown nor Discovered, Shutdown() is called before removing it from the registry. Modules already in Discovered have never been initialized and do not require a shutdown call.
The module is also removed from Scylla.Core.Modules.ScyllaModuleManager._initializationOrder to keep the shutdown sequence consistent for any subsequent ShutdownAllModules() call.
Both the registry mutation and the shutdown call happen inside the lock, so concurrent registrations or queries will see a consistent state after this method returns.
See Also
ValidateAllDependencies()
Validates the dependency graph of all registered modules, preparing the framework for initialization. This is the second step in the normal startup sequence, called after all modules have registered themselves.
Declaration
public bool ValidateAllDependencies()
Returns
| Type | Description |
|---|---|
| bool |
|
Remarks
This method performs three sequential steps under a snapshot pattern (reads the module list under lock, then operates outside it):
-
Injection - calls InjectDependencies(IScyllaModuleManager)
on every module, passing
thisas the IScyllaModuleManager so modules can resolve their dependencies via GetModule<T>(string). - Per-module validation - calls ValidateDependencies() on every module. All modules are checked even if an earlier one fails, so the log will surface all validation errors at once.
- Cycle detection - calls Scylla.Core.Modules.ScyllaModuleManager.ValidateNoCycles(), which performs a DFS over hard dependencies and logs errors for every detected cycle. Soft-dependency cycles are also checked but produce warnings only and do not prevent startup.
When no modules are registered the method returns true immediately after logging
a notice.