-
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.
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.
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.
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.
⚠️ 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.yml, thenetworkblock and the shared backends. - PlayerData & PDSections - the "pointer" half of the pattern.
- Accounts - the other resident of the network backend.
- 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