-
Notifications
You must be signed in to change notification settings - Fork 7
Accounts
The account layer is an opt-in identity system: it 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 layerOn = Accounts.isEnabled(); // is Multi-Platform Accounts turned on?PlayerData#getAccountId() gives the same canonical id for an already-resolved player.
The layer bootstraps only when it is enabled in storage.yml:
multi-platform-accounts:
enabled: true
storage-backend-id: "" # empty = the default-backend
# idle-grace-seconds: 3600 # absent = follow playerdata.default-idle-grace-secondsWhile disabled, every caller still goes through Accounts.get() (which returns a no-op facade resolving
each UUID to its own singleton), so account-scoped keying degrades cleanly to plain UUID keying;
mutating operations (link, unlink, …) fail with a clear error until it is enabled.
⚠️ On a real network the account 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).
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 'pd_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 multi-platform-accounts backend (no per-section routing), the
cache lifecycle is fixed (resident while any member is online, released after the last member quits -
multi-platform-accounts.idle-grace-seconds, which follows playerdata.default-idle-grace-seconds
when absent), and the row is always loaded at a member's login - so AccountSectionConfiguration is deliberately smaller
than a PDSectionConfiguration (collection name, discardDirtyOnReload() and schema migrations).
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