ArcanumServices
World-scoped service registry for ArcanumLib. Each world load activates an ArcanumRuntime that owns an ArcanumServiceRegistry instance. ModSystems register their services during StartServerSide / StartClientSide; the runtime is disposed on world unload, preventing static state from leaking between saves.
Architecture
ArcanumRuntime is the instance-based root. It owns:
ArcanumServiceRegistry Services— the per-world service registry.- Lifecycle coordination via
ArcanumLifecycle.
ArcanumServices (static) and ArcanumLifecycle (static) are thin facades that delegate to ArcanumRuntime.Current. This preserves backward compatibility while enabling instance-based access for new code and tests.
What is it for?
- Avoiding static
CurrentorInstancefields that survive world reloads. - Letting static public facades resolve an instance that was created for the current world.
- Enabling cross-mod access to shared services (
PityTracker,CategorizedLogger,ActionExecutorService,StatusEffectService) without hardcoded references. - Keeping client, server, and world-scoped services separate so singleplayer does not accidentally overwrite one side with the other.
- Allowing tests to create and dispose runtime instances for isolation.
Scopes
| Scope | Meaning |
|---|---|
Global | Shared across client and server. |
Client | Belongs to the client side. |
Server | Belongs to the server side. |
World | Tied to the currently loaded world. |
ArcanumLibModSystem automatically registers ICoreClientAPI and ICoreAPI under Client, and ICoreServerAPI and ICoreAPI under Server.
Quick example
Instance-based access (preferred for new code)
using ArcanumLib.Core;
var registry = ArcanumRuntime.Current?.Services;
var sapi = registry?.Get<ICoreServerAPI>(ArcanumServiceScope.Server);
Static facade (backward compatible)
using ArcanumLib.Core;
using ArcanumLib.Persistence;
var tracker = new PityTracker(sapi, "old:pity");
ArcanumServices.Register(tracker, ArcanumServiceScope.Server);
Consume a service
var tracker = ArcanumServices.Get<IPityTracker>(); // searches all scopes
// or explicitly server-side:
var tracker = ArcanumServices.Get<IPityTracker>(ArcanumServiceScope.Server);
tracker?.RecordOpen(playerUid, "my:milestone", rolledTier);
Resolve the current API by side
var sapi = ArcanumServices.Get<ICoreServerAPI>(ArcanumServiceScope.Server);
var capi = ArcanumServices.Get<ICoreClientAPI>(ArcanumServiceScope.Client);
Ensure a service is initialized
var tracker = ArcanumServices.EnsureInitialized(() => new PityTracker(sapi), ArcanumServiceScope.Server);
Shut down
// ArcanumLibModSystem.Dispose already disposes the runtime on world unload.
// For manual cleanup:
ArcanumRuntime.Current?.Dispose();
API
ArcanumRuntime
| Member | Description |
|---|---|
Current | The active runtime, or null if no world is loaded. |
Activate() | Creates a new runtime, sets it as Current, and returns it. Disposes any prior runtime. |
Services | The ArcanumServiceRegistry for this runtime. |
Api | The core API for the current side. |
Side | The application side (Client or Server). |
Initialize() | Marks the runtime as initialized and runs lifecycle init handlers. |
Dispose() | Runs lifecycle disposal, shuts down all services, clears Current. |
ArcanumServices (static facade)
| Method | Description |
|---|---|
Register<T>(T service, ArcanumServiceScope scope = Global) | Registers or replaces a service of type T in the given scope. Throws if no runtime is active. |
Unregister<T>(ArcanumServiceScope scope = Global) | Removes the registered T from the given scope. No-op if no runtime is active. |
Get<T>(ArcanumServiceScope? scope = null) | Returns the registered T or null. Returns null if no runtime is active. |
TryGet<T>(out T? service, ArcanumServiceScope? scope = null) | Returns true and the service if one is registered. |
EnsureInitialized<T>(Func<T> factory, ArcanumServiceScope scope = Global) | Returns the existing T or creates and registers one. Throws if no runtime is active. |
Get(Type type, ArcanumServiceScope? scope = null) | Non-generic get by Type. Returns null if no runtime is active. |
Shutdown(ArcanumServiceScope? scope = null) | Clears all registered services, or only the given scope. No-op if no runtime is active. |
Notes
- Registration is thread-safe.
- Use
Tas the service contract. For example, registerPityTrackerand resolve it viaArcanumServices.Get<IPityTracker>(). - Services that implement
IDisposableare disposed byShutdownandUnregister. - Prefer explicit
Client/Serverscopes for side-specific services to avoid singleplayer conflicts. Get<T>andGet(Type)return null when no runtime is active, making them safe for logging paths.RegisterandEnsureInitializedthrow when no runtime is active, since registration requires an active world.