Skip to content

Accounts

Petrus Pradella edited this page Jul 23, 2026 · 5 revisions

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


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


Enabling the layer

The layer bootstraps only when it is enabled in storage.yml:

multi-platform-accounts:
  enabled: true
  storage-backend-id: ""   # empty = the default-backend

While 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).


Account sections

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:

PlayerController.registerAccountSectionCfg(ecPluginData, NetworkStatsSection.class);

// 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), and hot-load always happens on login - so AccountSectionConfiguration is deliberately smaller than a PDSectionConfiguration (collection name + schema migrations only).


The /ecaccount command

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-idempotent merge.
  • 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.

External identities

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);

See also

Clone this wiki locally