Skip to content

Inline Stroage Backends for Plugins

Petrus Pradella edited this page Jul 30, 2026 · 4 revisions

Inline 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.

For the other need - data that must look the same from every server of the network - the door is ECNetworkStorage rather than a backend of your own: see The shared network backend below.


The inline backend shape

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: myplugin

Switching backend means replacing that one block with another type:

storage:
  groupedfile:
    path: plugins/MyPlugin/Data/groupedfile
    format: yaml

Valid types are the same as storage.yml: groupedfile | localfile | sql | postgresql | h2 | mongo | memory (see Storage Backends).


Seeding the block: writeInlineBackendTemplate

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 = compact

format 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.


Opening it: ECStorage.open

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.

Lifecycle: it's yours to close

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.


Reloads: register the callback, always

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.


The shared network backend

ECStorage is for data this plugin owns and this server reads. The opposite need - data that must look the same from every server of the network (a guild bank, a network leaderboard, a cross-server market) - has its own door: ECNetworkStorage, onto the one backend the admin declared under network.storage-backend-id, alongside the account registry and the network cooldowns.

import br.com.finalcraft.evernifecore.storage.ECNetworkStorage;

// onEnable - opens no connection and reads no config of your own; the backend is already up.
ECNetworkStorage network = ECNetworkStorage.of(getEcPluginData());
CachingManager<UUID, GuildBank> banks = network.manager(BANKS, CachePolicy.always());

// onDisable
network.release();

It is a facade, not a handle - and that is the whole point. An ECStorage captures the Storage it opened. The network storage belongs to the core and is rebuilt on every core reload, so an ECStorage over it kept in a static field would be a closed storage after the first /ecore reload. ECNetworkStorage resolves the live storage on every call and captures nothing, which is why it also memoizes no manager: a memoized one would belong to the registry the reload replaced. Call manager(...) on every access - the registry memoizes per type anyway.

Consequences worth knowing before you reach for it:

No close() The handle does not own the storage; the core does. What you own is your registrations - that is what release() gives back.
release() is mandatory Not a convenience. The core's cleanup runs off your PDSection/AccountSection registrations, so a plugin using only this facade is never swept: without the call it leaks its type registrations and, through them, keeps its classloader alive. It also frees the collection claims, so a plugin disabled and not re-enabled does not leave those names locked against everyone else. Idempotent, and it touches only what this handle registered - your PDSections keep resolving.
Claims are mandatory manager(...) / repository(...) claim the descriptor's collection first, so two plugins reaching for one name fail deterministically instead of quietly writing into the same table. The owner is derived from your plugin name, never the ECPluginData instance, so a reload re-claims instead of colliding with itself.
No storage.yml entry The descriptor and the cache policy are yours, and a knob the framework cannot honour is worse than none. /ecstorage status lists the claim with its owner - that is the collection's visibility.
One home per entity type A RefRegistry holds one manager per type, and this facade shares your plugin's registry with your PDSections and your ECStorage. A type already registered through your ECStorage cannot be registered here too.
Only after the storage boot of(...) throws StorageUnavailableException before the core's PlayerController has bootstrapped - reach for it from your onEnable, never a static initializer.

defaultCodec(Class) here is ref-aware by default (unlike ECStorage's, which is not): a Ref field in a network entity resolves against your plugin's registry, so it reaches your PDSections, your AccountSections and your ECStorage entities. Framework rows (Account, ServerCooldownRow) bind to the global registry instead, and resolution only walks upward - your entities reach theirs, never the other way around.

When the admin later moves the network family to a shared database with /ecstorage transfernetwork, your claimed collection travels with the framework's - nobody maintains a list.


The on-disk format comes from your codec

⚠️ Gotcha worth knowing. For a plugin-owned backend opened this way, the file format field in the inline block does not automatically decide how your entities are written to disk - the codec on your EntityDescriptor does. open hands you the storage, but it does not attach a codec to your descriptor for you; your descriptor's codec (e.g. a JacksonJsonCodec) is what serialises your entity. So if you want YAML files, give your descriptor a YAML codec - flipping format: yaml on the inline block alone will not change what your repository writes. The shortcut is STORAGE.defaultCodec(Type.class): it derives exactly the codec the core would use for that format (a file backend picks YAML or pretty JSON; every other backend uses compact JSON), so you can honour the configured format without hand-picking one.

Define your entity and its codec the EveryDatabase way - Defining Entities and Codecs.


See also

Clone this wiki locally