Adapters are lightweight bridge files that wire two modules together without coupling them to each other. They live in addons/ascendere/Adapters/ and are the only place in the codebase that is allowed to depend on multiple domain modules simultaneously.
Ascendere modules are deliberately isolated — Inventory knows nothing about SaveLoad, and Currency knows nothing about WorldFlags. This keeps modules independently installable and testable.
When cross-module behaviour is needed (e.g. saving inventory slots), an Adapter holds both sides:
Modules/Inventory ←──┐
│ Adapters/Inventory_SaveLoad
Modules/SaveLoad ←──┘
You install only the adapters that match your installed modules. If you don’t use SaveLoad, simply don’t include the Inventory_SaveLoad adapter.
addons/ascendere/Adapters/
<Provider>_<Consumer>/
<Provider>SaveContributor.cs (or other bridge class)
| Segment | Meaning |
|---|---|
Provider |
Module that owns the data |
Consumer |
Module that uses/stores the data |
Examples:
| Folder | Wires together |
|---|---|
Inventory_SaveLoad/ |
Inventory data ↔ SaveLoad slots |
Currency_SaveLoad/ |
Currency balances ↔ SaveLoad slots |
WorldFlags_SaveLoad/ |
World flags ↔ SaveLoad slots |
Module manifests use two adapter-related fields:
{
"optionalDeps": ["saveload_module"],
"hasAdapters": true
}
| Field | Type | Description |
|---|---|---|
optionalDeps |
string[] |
Module IDs that adapters in this module depend on (install if you want integration) |
hasAdapters |
bool |
true when the module ships adapters in Adapters/<ModuleName>_*/ |
| Adapter | Provider | Consumer | Auto-registration |
|---|---|---|---|
Inventory_SaveLoad/ |
Inventory | SaveLoad | [Saveable(Order = 20)] |
Currency_SaveLoad/ |
Currency | SaveLoad | [Saveable(Order = 30)] |
WorldFlags_SaveLoad/ |
WorldFlags | SaveLoad | [Saveable(Order = 10)] |
Planned:
Loot_Inventory,Loot_Currency,Quest_Inventory
Every SaveLoad adapter implements ISaveContributor and is tagged [Saveable]. The SaveLoadModule reflects all assemblies during Initialize() and auto-registers any class carrying that attribute — no bootstrapping code required.
[Saveable(Order = 20)] // auto-registered, lower order = saved first
internal sealed class InventorySaveContributor : ISaveContributor
{
public string ContributorId => "Inventory"; // unique key in the save file
public int SaveVersion => 1;
public Dictionary OnSave() { /* snapshot state */ }
public void OnLoad(Dictionary data) { /* restore state */ }
}
Follow these five steps to integrate two modules without coupling them.
Decide which module owns the data (Provider) and which consumes it (Consumer). You’ll import both interfaces.
addons/ascendere/Adapters/<Provider>_<Consumer>/
For SaveLoad integration, implement ISaveContributor:
using Ascendere.SaveLoad;
using Ascendere.YourModule.Interfaces; // Provider interface
using Godot;
using Godot.Collections;
namespace Ascendere.Adapters.YourModule_SaveLoad;
[Saveable(Order = 40)] // choose an order unique among your contributors
internal sealed class YourModuleSaveContributor : ISaveContributor
{
public string ContributorId => "YourModule";
public int SaveVersion => 1;
public Dictionary OnSave()
{
var svc = ServiceLocator.GetOrDefault<IYourModuleService>();
if (svc == null)
return new Dictionary();
// Snapshot your data into a Godot Dictionary.
var root = new Dictionary();
// ... populate root ...
return root;
}
public void OnLoad(Dictionary data)
{
var svc = ServiceLocator.GetOrDefault<IYourModuleService>();
if (svc == null)
{
GD.PrintErr("[YourModule] Service not available during load.");
return;
}
// Restore data from the Dictionary.
}
}
Add the consumer to optionalDeps and set hasAdapters: true:
{
"optionalDeps": ["saveload_module"],
"hasAdapters": true
}
The [Saveable] attribute is all you need. Rebuild the project; SaveLoadModule.Initialize() will discover and register your contributor automatically.
[Saveable])If you prefer explicit control, skip the attribute and register manually in your module’s Initialize():
public override void Initialize()
{
var saveLoad = ServiceLocator.GetOrDefault<ISaveLoadService>();
saveLoad?.RegisterContributor(new YourModuleSaveContributor());
}
public override void Shutdown()
{
var saveLoad = ServiceLocator.GetOrDefault<ISaveLoadService>();
saveLoad?.UnregisterContributor("YourModule");
}
| ✅ Do | ❌ Don’t |
|---|---|
Keep adapters in Adapters/<Provider>_<Consumer>/ |
Put cross-module code inside a domain module |
| Import only the interfaces of both modules | Depend on concrete implementations |
Use ServiceLocator.GetOrDefault<>() and null-check before using |
Assume services are always available |
Handle OnLoad failures gracefully (log + skip, don’t throw) |
Call GD.PushError and crash on missing data |
Keep SaveVersion at 1 until a breaking format change requires migration |
Bump the version for purely additive changes |