-
Notifications
You must be signed in to change notification settings - Fork 7
Inline Stroage Backends for Plugins
The core's storage.yml routes EverNifeCore's data (PlayerData, PDSections, accounts). Sometimes a
plugin wants to route its own data - a big payload that doesn't belong in a PDSection - to a backend
its own config declares. ECStorage.open(...) is that door: a plugin depending on
EverNifeCore opens a fresh, plugin-owned EveryDatabase
Storage from an inline single-backend block, using the same engine and the same drivers the core
already downloaded at boot.
This is the "store a pointer in PlayerData, the volume elsewhere" pattern: keep a small per-player
index in a PDSection (routed by the core's storage.yml) and put the fat
data on a backend the plugin's config points at.
An inline backend is a simplified form of a storage.yml backend: the single child key IS the backend
type, enabling is implicit (declaring it is enabling it), and exactly one is declared - there is
no enabled and no separate type field.
# in YOUR plugin's config.yml
storage:
mongo: # the child key names the type
url: "mongodb://localhost:27017"
db: mypluginSwitching backend means replacing that one block with another type:
storage:
groupedfile:
path: plugins/MyPlugin/Data/groupedfile
format: yamlValid types are the same as storage.yml: groupedfile | localfile | sql | postgresql | h2 | mongo | memory (see Storage Backends).
Rather than ship a static default, seed the block programmatically so it arrives commented and with your plugin's preferred default. It is idempotent: if a backend is already declared it returns untouched, keeping the admin's choice - safe to call on every load.
import br.com.finalcraft.evernifecore.storage.config.StorageYamlDefaults;
import br.com.finalcraft.everyconfig.config.section.ConfigSection;
ConfigSection storage = config.getConfigSection("storage");
// Convenience default: a groupedfile (YAML) backend, fully documented.
StorageYamlDefaults.writeInlineBackendTemplate(storage, "plugins/MyPlugin/Data");The full signature lets you pick the seeded type, the file format and a compact vs. full header:
StorageYamlDefaults.writeInlineBackendTemplate(
storage,
"plugins/MyPlugin/Data", // base folder; each file backend gets a /<type> subfolder
BackendType.MONGO, // the type to seed (null = groupedfile)
null, // FileFormat (null = YAML); ignored for non-file types
false); // false = full type catalog in the comment; true = compactformat is meaningful only for the file backends (groupedfile/localfile); on any other type it is
dropped (there is no file format to pick). Each file backend is rooted in its own
baseStoragePath/<type> subfolder, so switching type never makes two backends share a directory.
import br.com.finalcraft.evernifecore.storage.ECStorage;
import br.com.finalcraft.everydatabase.manager.CachingManager;
import br.com.finalcraft.everydatabase.manager.cache.CachePolicy;
// onEnable - the storage is created AND connected before this returns. Passing your ECPluginData
// seeds a groupedfile default under your data folder when the block is empty, and logs through you:
STORAGE = ECStorage.open(getEcPluginData(), config.getConfigSection("storage")).join();
CachingManager<UUID, Snapshot> snapshots = STORAGE.manager(MY_DESCRIPTOR, CachePolicy.always());
snapshots.saveAndCache(new Snapshot(...)).join();open parses the inline block, creates the storage and init()s (connects) it before completing. The
returned ECStorage is a self-contained handle: the live Storage (storage()), the definition it came
from (definition()), the RefRegistry its managers register in (refRegistry()), and the
defaultCodec(Class) shortcut. A backend that cannot be reached throws a StorageConfigException
directly - unwrapped from the CompletionException that join() would otherwise nest it in, so a plain
catch (StorageConfigException e) catches it and e.getCause() is the original connection failure. On
that failure the half-created storage is torn down for you, so a failed open leaves no orphaned pool.
Because the drivers are already on the classpath from the core's boot, whichever type the admin picks just
works - no per-plugin driver management.
Overloads let you pass the seed definition explicitly, an explicit StorageLogConfig, or skip the plugin
entirely (open(ConfigSection) / open(BackendDefinition) - advanced, and see the reload caveat below).
manager(...) is memoized per entity type, so calling it on every access is the intended style - do
not cache the result in a field. A field would survive a reload the manager behind it did not.
The handle is yours. It does not touch the core's registry, and you must close it when your plugin disables:
// onDisable
STORAGE.close().join();Open it only after the core's storage bootstrap has run - i.e. from your onEnable, not onLoad. Opening
earlier is not an error, but the handle falls back to a private RefRegistry, and a Ref between it
and your PDSections will not resolve; the core warns when it sees that.
A core reload builds fresh per-plugin RefRegistry instances. A handle opened before it keeps the old
one, so anything created through it becomes invisible to your PDSections - which were rebound to the new
registry. The core detects this and marks the handle detached: it then refuses manager(...),
repository(...) and defaultCodec(...) with a message naming the fix, while still allowing
flushManagers() and close() so you never lose what was dirty.
The fix is one registration, made once, on enable:
// onEnable, BEFORE the first open - the core warns about a handle nobody would re-open
PlayerController.onStorageReload(getEcPluginData(), this::openStorage);
private void openStorage() {
STORAGE = ECStorage.openOrReload(getEcPluginData(), config.getConfigSection("storage"), STORAGE).join();
}openOrReload covers all three entry points, because it tells them apart on its own:
| What changed | What it does |
|---|---|
| nothing (same block) | reuses the handle, wipes every cache (dirty cells discarded) |
| only the core's registry | rebinds onto the fresh one - the connection stays up |
| the target backend | closes the old handle and connects a new one |
Call flushManagers() first if the dirty data must survive the reload. In every case the managers are
dropped and you re-derive them with manager(...) - which is exactly why you should not hold one in a
field.
⚠️ Gotcha worth knowing. For a plugin-owned backend opened this way, the fileformatfield in the inline block does not automatically decide how your entities are written to disk - the codec on yourEntityDescriptordoes.openhands you the storage, but it does not attach a codec to your descriptor for you; your descriptor's codec (e.g. aJacksonJsonCodec) is what serialises your entity. So if you want YAML files, give your descriptor a YAML codec - flippingformat: yamlon the inline block alone will not change what your repository writes. The shortcut isSTORAGE.defaultCodec(Type.class): it derives exactly the codec the core would use for thatformat(a file backend picks YAML or pretty JSON; every other backend uses compact JSON), so you can honour the configuredformatwithout hand-picking one.
Define your entity and its codec the EveryDatabase way - Defining Entities and Codecs.
-
Storage Backends - the core's
storage.ymland the shared backends. - PlayerData & PDSections - the "pointer" half of the pattern.
- EveryDatabase: Quick Start - the describe → open → use → close lifecycle in full.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference