-
Notifications
You must be signed in to change notification settings - Fork 7
Accounts
The account layer links platform identities that belong to the same human - Minecraft ⇄ Hytale ⇄ external providers (Discord, a website) - into one account, and lets a plugin store data that is shared by every linked identity.
It is the counterpart to PlayerData & PDSections: a PDSection belongs to
one player (keyed by platform UUID); an AccountSection belongs to the account (keyed by a canonical
accountId).
Every platform UUID resolves to an Account. A UUID that has never been linked resolves to a
singleton account whose accountId == uuid. So until a link actually happens, an account-scoped
section keys by exactly the same value plain UUID keying would - account scoping only changes behavior
once identities are linked.
The first real link mints a brand-new random accountId (never a member's UUID), persists the
canonical account row, and writes one alias row per identity so every member stays resolvable by its own
key. accountIds are opaque - integrations must not derive meaning from them.
import br.com.finalcraft.evernifecore.playerdata.account.Accounts;
// Async resolve of the stored account (NEVER null - an unlinked uuid gets its singleton account):
Accounts.get().account(playerUuid).thenAccept(account -> {
UUID accountId = account.getAccountId();
account.getMembers().forEach(m -> { /* provider, providerUid, name */ });
});
boolean layerUp = Accounts.isEnabled(); // has the layer bootstrapped yet?PlayerData#getAccountId() gives the same canonical id for an already-resolved player.
The layer bootstraps on every boot; there is no switch for it in storage.yml. A flag would have
had nothing to control: without a link, every identity is its own singleton account and keys exactly as
plain UUID keying would - so "on with no links" and "off" were the same server. Linking is not a
setting, it is an operation: an account exists once an admin runs /ecaccount link.
Accounts.isEnabled() survives with the narrower meaning it always had in practice: whether the layer
has bootstrapped. It is false before the storage boot and in tests, where Accounts.get() returns a
no-op facade resolving each UUID to its own singleton (mutating operations on that facade fail with a
clear error). It is not an admin's answer to anything.
What is configured is where the family lives - the one backend under
network.storage-backend-id, shared with the network cooldowns:
network:
storage-backend-id: networkdata # REQUIRED - no implicit fallback
# idle-grace-seconds: 3600 # absent = follow playerdata.default-idle-grace-seconds
⚠️ On a real network that backend must be a database shared by every instance (MariaDB/MongoDB/PostgreSQL/…), never a local-file backend - the account family lives on that one backend and every server has to agree on it. This is a separate concern from multi-server cache-sync (same platform, shared DB); accounts is about linking different identities and is meaningful even on a single server (e.g. linking Discord).Note what the backend alone already buys you: two Minecraft servers pointed at one shared database share account data with no link at all, because they already agree on a player's UUID. Linking is what you need when the UUID itself differs - a Hytale server, a Discord identity. Moving the family to that shared database later is one command:
/ecstorage transfer network.
An AccountSection<T> is a section owned by the account rather than a player. It carries no player
reference, and two linked players online at once share the same live instance. Because it can be
written from several servers, consistency is eventual and convergence is driven by a merge(...)
method you implement.
import br.com.finalcraft.evernifecore.playerdata.AccountSection;
import java.util.List;
public class NetworkStatsSection extends AccountSection<NetworkStatsSection> {
public long globalKills;
/**
* Pure, associative, commutative. Return a NEW instance; never mutate this or the inputs.
* The framework tracks what was already merged, so it need NOT be idempotent.
*/
@Override
public NetworkStatsSection merge(List<NetworkStatsSection> others) {
NetworkStatsSection result = new NetworkStatsSection();
result.globalKills = this.globalKills;
for (NetworkStatsSection other : others) result.globalKills += other.globalKills;
return result;
}
}merge is called both when linked identities' rows are coalesced and when a concurrent-write
conflict is resolved - it is the section's convergence policy. Contrast this with a PDSection, which
resolves conflicts by ADOPT_WINNER (first-wins) rather than merging.
Register and read it through PlayerController:
// "netstats" is the stable section id, same contract as a PDSection's
PlayerController.registerAccountSectionCfg(ecPluginData, NetworkStatsSection.class, "netstats");
// on the database the collection will be 'acs_yourplugin_netstats'
// keyed by the player's canonical accountId:
PlayerController.getAccountSection(playerUuid, NetworkStatsSection.class); // CompletableFuture<T>
PlayerController.getAccountSectionByAccountId(accountId, NetworkStatsSection.class);// CompletableFuture<T>
NetworkStatsSection cached = PlayerController.getLoadedAccountSection(uuid, NetworkStatsSection.class);The whole account family lives on the ONE network backend (no per-section routing), the
cache lifecycle is fixed (resident while any member is online, released once none is - after the last
member quits, or after network.idle-grace-seconds of nobody using a row that was read
for an offline account), and the row is always loaded at a member's login - so
AccountSectionConfiguration is deliberately smaller than a PDSectionConfiguration (collection name,
description(), discardDirtyOnReload() and schema migrations).
The admin side is smaller too, and lives under accountsections
in storage.yml: the collection, and a cache policy. Reach for cache: { policy: TTL, ttlSeconds: N }
on a network without Redis - an account row is written by every instance, and a TTL is what bounds how
long this server may keep serving a version another server has already moved past.
Re-registering the class reloads it exactly like a PDSection: the rows are flushed, the cache is
dropped and the binding is rebuilt. discardDirtyOnReload() skips that flush - weigh it harder here
than on a PDSection, because an account row is shared by every linked identity and written from the
whole network, so what you discard may not even be this server's write.
Permission node evernifecore.command.account; the mutating subcommands additionally require
evernifecore.command.account.link.
| Subcommand | Usage | What it does |
|---|---|---|
info |
/ecaccount info <player> |
Shows the stored account a player belongs to and its linked members. |
link |
/ecaccount link <target> <source> |
Links two identities into one account. Data follows at each member's next login. |
unlink |
/ecaccount unlink <player> |
Removes a member: it stands alone and starts fresh next login; the account keeps the shared data. |
migrate |
/ecaccount migrate <player> |
Forces the account-data reconciliation of an offline player (it runs at login otherwise). |
Implications worth stating plainly:
-
Link is identity-only; data follows lazily. A link moves identity immediately but the account-wide
data a member accumulated as a singleton (under its own UUID) is absorbed into the canonical rows
at that member's next login, or eagerly via
/ecaccount migrate. The absorption is recorded in a per-row ledger, so an interrupted or repeated migration never double-applies a non-idempotentmerge. - Merging accounts is transitive. Linking two already-populated accounts unions their members; the ledger keeps convergence correct.
- Unlink is a fresh start for the member, not a data transfer - the account retains the shared row.
Beyond platform UUIDs, an identity from an external provider can be linked from code:
Accounts.get().linkExternal(playerUuid, "discord", discordUserId);
Accounts.get().findByExternal("discord", discordUserId); // CompletableFuture<Optional<Account>>
Accounts.get().unlinkExternal("discord", discordUserId);- PlayerData & PDSections - per-player sections and ADOPT_WINNER conflict.
-
Storage Backends - the shared account backend and
storage.yml. - Cooldowns - network cooldowns ride on an account section.
-
Command Framework - how
/ecaccountis declared.
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