Interface EnderChestStorage

All Known Implementing Classes:
CachedStorage

public interface EnderChestStorage
All methods execute synchronously on the calling thread. No background threads, no queued writes — callers (the com.enhancedechest.service layer, via DbExecutor) are responsible for dispatching these onto an async executor and never blocking a region/main thread.

Ownership model: a player owns a chest iff a row exists for (player_uuid, chest_index). Chests are created explicitly (admin command, API, or the auto-bootstrap of the first chest).

The sole implementation is the lazy write-back CachedStorage (per-player load on first touch, dirty-row flush); the SQL side of the plugin implements the much narrower StorageBackend instead (per-player reads, batched flush, import/backup — no per-row writes).

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Interface
    Description
    static final record 
    Lightweight reference to an expired chest, returned by findExpired(long).
    static final record 
    One known player for the in-memory name index: their UUID, last recorded name, and last-seen time.
    static final record 
    static final record 
  • Method Summary

    Modifier and Type
    Method
    Description
    default void
    backup(Path target)
    Writes a consistent snapshot of the entire database to target (which must not already exist).
    void
    clearChestContents(UUID owner, int index)
    Empties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.
    void
    Clears the primary flag from all of the player's chests, leaving them with no main chest.
    void
    Closes all connections.
    void
    completeMigration(UUID owner, byte[] containerData, @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
    Commits a vanilla migration in one transaction: writes chest #1's contents and sets its migrated flag (one single-row UPDATE — chest #1's size is never changed), plus, when overflow is non-null, inserts a temp chest at the next free index carrying the vanilla items that did not fit.
    long
    Total number of rows in the enderchests table across every player and chest kind.
    default int
    createChest(UUID owner, int size)
    Creates a new permanent chest at the lowest free index, so a number freed by a delete is reused instead of the numbering climbing forever (the index is the number the player reads on the chest).
    int
    createChest(UUID owner, int size, @Nullable Long expiresAt)
    Creates a new NORMAL chest at the lowest free index.
    int
    createPermChest(UUID owner, int size)
    Creates a permission-granted chest (kind=PERM) at the lowest free index.
    void
    deleteChest(UUID owner, int index)
    Deletes a chest.
    void
    ensureChest(UUID owner, int index, int size)
    Creates a chest at a specific index if it does not already exist (used by migration).
    findExpired(long now)
    Returns every chest whose expiry is set and at or before now.
    @Nullable UUID
    Resolves a stored in-game name to its UUID, case-insensitively, or null if no player with that name has been recorded.
    int
    Reads the persisted applied_default_size baseline — the base-chest size currently dictated by the player's enhancedechest.default_size.<size> permission, or 0 when the base chest is not permission-managed.
    int
    Returns the index of the chest /ec opens: the primary if one is flagged, otherwise the lowest-indexed chest.
    int[]
    Bulk-inserts raw players and enderchests rows verbatim into this (fresh, active) backend, in a single transaction.
    void
    Creates schema and prepares connections.
    boolean
    Returns true if the player's chest #1 has its migrated flag set.
    Returns the player's chests ordered by index.
    Returns every recorded player with a non-null username, with the epoch-ms they were last seen (0 when never recorded).
    @Nullable EnderChestData
    loadChest(UUID owner, int index)
    Loads a single chest, or null if no such (owner, index) row exists.
    Loads the player's settings, or PlayerSettings.defaults() if they have no row yet.
    boolean
    reclaimTemp(UUID owner, int tempIndex, int targetIndex)
    Moves a temp chest's contents into an empty chest and deletes the temp row, in one transaction — the inverse of a spill, used when a player is granted a new chest that can hold a temp chest whole (see ChestSpillService#reclaimTempInto).
    void
    recordPlayerSeen(UUID owner, String name, long lastOnline)
    Records that a player was seen: their current in-game name against their UUID (so name→UUID resolution works for offline players from the plugin's own data, with no server usercache or Mojang lookup), and lastOnline as epoch-ms.
    void
    renameChest(UUID owner, int index, @Nullable String name)
    Sets or clears a chest's custom display name (null resets to the default numbered title).
    void
    resizeChest(UUID owner, int index, int size)
    Changes a chest's slot count.
    void
    saveChest(UUID owner, int index, byte[] containerData)
    Updates the container bytes of an existing chest.
    void
    saveSettings(UUID owner, PlayerSettings settings)
    Upserts the player's editMode/appliedDefaultSize (whole-object save): updates the existing row, or inserts one if none exists.
    void
    setAppliedDefaultSize(UUID owner, int size)
    Targeted single-field upsert of just the applied_default_size baseline (no preceding read), leaving every other setting untouched.
    void
    setEditMode(UUID owner, boolean editMode)
    Targeted single-field upsert of just the edit-mode preference.
    void
    setIcon(UUID owner, int index, @Nullable String icon)
    Sets or clears a chest's icon (material key, e.g.
    void
    setMigrated(UUID owner, boolean migrated)
    Updates the migrated flag on the player's chest #1.
    void
    setPrimary(UUID owner, int index)
    Makes the given chest the player's primary, clearing the flag from all others.
    void
    spillRemove(UUID owner, int index, @org.jetbrains.annotations.Nullable byte[] items, int tempSize, long tempExpiresAt)
    Deletes a chest, optionally spilling its items into a new temp chest, in one transaction.
    void
    spillShrink(UUID owner, int index, int newSize, byte[] visible, @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
    Shrinks a chest and spills any cut-off items into a new temp chest, in one transaction.
    default boolean
    True if this backend can produce a file snapshot via backup(Path).
    int
    transferChests(UUID from, UUID to, @Nullable Integer onlyIndex, Set<Integer> preserveDestIndices, long tempExpiresAt)
    Moves a player's NORMAL chests onto another player in a single transaction (the account-switch primitive behind /ee transfer).
  • Method Details

    • init

      void init()
      Creates schema and prepares connections. Must be called once before any other method.
    • close

      void close()
      Closes all connections. Safe to call even if init() was not completed.
    • supportsBackup

      default boolean supportsBackup()
      True if this backend can produce a file snapshot via backup(Path). Only the file-based SQLite backend supports it today; remote backends (MySQL/MariaDB/Postgres) return false and must be backed up with the database server's own tooling.
    • backup

      default void backup(Path target) throws Exception
      Writes a consistent snapshot of the entire database to target (which must not already exist). The snapshot is safe to take while players are saving — it does not interrupt writes. Only valid when supportsBackup() is true.
      Throws:
      Exception - if the snapshot fails; the caller logs it and leaves the live DB untouched.
    • listChests

      List<ChestSummary> listChests(UUID owner)
      Returns the player's chests ordered by index. Empty list if the player owns none.
    • countChests

      long countChests()
      Total number of rows in the enderchests table across every player and chest kind. Used by /ee import to refuse converting into a non-empty destination (import only into a fresh DB).
    • importRows

      Bulk-inserts raw players and enderchests rows verbatim into this (fresh, active) backend, in a single transaction. Every column is copied as-is — including the container_data bytes — so no item (de)serialization happens; this is the DB→DB conversion primitive behind /ee import.

      Inserts are batched per table (one reused PreparedStatement each, flushed in chunks) with autocommit off, so the whole copy commits once. A duplicate primary key (or any other failure) rolls the whole transaction back and throws — the caller guards that the destination is empty first, so a duplicate signals a real problem rather than silently merging.

      Returns:
      [playersInserted, chestsInserted]
    • getPrimaryIndex

      int getPrimaryIndex(UUID owner)
      Returns the index of the chest /ec opens: the primary if one is flagged, otherwise the lowest-indexed chest. Returns -1 if the player owns no chests.
    • loadChest

      @Nullable @Nullable EnderChestData loadChest(UUID owner, int index)
      Loads a single chest, or null if no such (owner, index) row exists.
    • saveChest

      void saveChest(UUID owner, int index, byte[] containerData)
      Updates the container bytes of an existing chest. No-op if the row was deleted while open. Size, name and primary flag are never touched here.
    • createChest

      default int createChest(UUID owner, int size)
      Creates a new permanent chest at the lowest free index, so a number freed by a delete is reused instead of the numbering climbing forever (the index is the number the player reads on the chest). No chest is ever auto-flagged primary; the main chest is set only via setPrimary(java.util.UUID, int).
      Returns:
      the index assigned to the new chest
    • createChest

      int createChest(UUID owner, int size, @Nullable @Nullable Long expiresAt)
      Creates a new NORMAL chest at the lowest free index. If expiresAt is non-null the chest expires at that epoch-millis instant (an expirable granted chest); null = never expires. No chest is ever auto-flagged primary; the main chest is set only via setPrimary(java.util.UUID, int).
      Returns:
      the index assigned to the new chest
    • createPermChest

      int createPermChest(UUID owner, int size)
      Creates a permission-granted chest (kind=PERM) at the lowest free index. PERM chests carry no expiry and are never auto-flagged primary; they are granted/removed by the reconcile of com.enhancedechest.service.PermissionChestService and are invisible to admin commands.
      Returns:
      the index assigned to the new chest
    • spillShrink

      void spillShrink(UUID owner, int index, int newSize, byte[] visible, @Nullable @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
      Shrinks a chest and spills any cut-off items into a new temp chest, in one transaction. The original row is updated to newSize with visible as its new contents; if overflow is non-null a temp chest (kind=TEMP, appended after the highest index so it sorts last, expiring at tempExpiresAt) is inserted holding it. Never leaves items in two rows at once.
      Parameters:
      tempSize - slot count of the temp chest created for the overflow (ignored if no overflow)
    • spillRemove

      void spillRemove(UUID owner, int index, @Nullable @org.jetbrains.annotations.Nullable byte[] items, int tempSize, long tempExpiresAt)
      Deletes a chest, optionally spilling its items into a new temp chest, in one transaction. If items is non-null a temp chest holding them is inserted before the original row is deleted. No survivor is promoted to primary (the main is an explicit player choice).
      Parameters:
      tempSize - slot count of the temp chest created for the items (ignored if items is null)
    • reclaimTemp

      boolean reclaimTemp(UUID owner, int tempIndex, int targetIndex)
      Moves a temp chest's contents into an empty chest and deletes the temp row, in one transaction — the inverse of a spill, used when a player is granted a new chest that can hold a temp chest whole (see ChestSpillService#reclaimTempInto).

      The stored bytes are copied verbatim: nothing is decoded, merged or re-packed, so every item keeps the exact slot it occupied in the temp chest. That is only sound while the target is at least as large as the temp chest and is itself empty, so both are re-checked here under the lock along with the two kinds — the caller picked the pair from a list it fetched earlier, and an expiry sweep or a concurrent op may have invalidated it since. Any failed check is a no-op returning false, never a partial move.

      Returns:
      true if the contents were moved and the temp row deleted, false if a check failed
    • findExpired

      List<EnderChestStorage.ExpiredRef> findExpired(long now)
      Returns every chest whose expiry is set and at or before now.
    • ensureChest

      void ensureChest(UUID owner, int index, int size)
      Creates a chest at a specific index if it does not already exist (used by migration). No-op if the row already exists. Never auto-flags primary.
    • resizeChest

      void resizeChest(UUID owner, int index, int size)
      Changes a chest's slot count. Caller validates size (multiple of 9, 9..54).
    • deleteChest

      void deleteChest(UUID owner, int index)
      Deletes a chest. No survivor is promoted: if the deleted chest was the main, the player simply has no main until they choose one again.
    • clearChestContents

      void clearChestContents(UUID owner, int index)
      Empties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.
    • transferChests

      int transferChests(UUID from, UUID to, @Nullable @Nullable Integer onlyIndex, Set<Integer> preserveDestIndices, long tempExpiresAt)
      Moves a player's NORMAL chests onto another player in a single transaction (the account-switch primitive behind /ee transfer). For each source NORMAL chest (all of them, or only onlyIndex when non-null) a copy is written to to at the same index, carrying its size, custom name, icon and contents (and, on a full transfer, the primary flag); the source row is then deleted, so the items live on exactly one account.

      The destination's pre-existing NORMAL chests in scope are removed first so the destination ends up with exactly the source's chest count and nothing stacked on top. Any destination index listed in preserveDestIndices is re-inserted as a TEMP chest (carrying its items and expiring at tempExpiresAt) before removal, so those items remain recoverable; indices not listed are discarded. Destination PERM/TEMP chests sitting on an index the source needs are relocated to a free index rather than dropped. The whole thing is one transaction, so a failure leaves both players untouched.

      Parameters:
      onlyIndex - a single source index to transfer, or null for every NORMAL chest
      preserveDestIndices - destination indices whose items must be kept (spilled to a temp chest); empty to discard every replaced destination chest
      tempExpiresAt - epoch-millis expiry stamped on any temp chest created for preserved items
      Returns:
      the number of chests transferred
    • renameChest

      void renameChest(UUID owner, int index, @Nullable @Nullable String name)
      Sets or clears a chest's custom display name (null resets to the default numbered title).
    • setIcon

      void setIcon(UUID owner, int index, @Nullable @Nullable String icon)
      Sets or clears a chest's icon (material key, e.g. minecraft:diamond; null resets to default).
    • setPrimary

      void setPrimary(UUID owner, int index)
      Makes the given chest the player's primary, clearing the flag from all others.
    • clearPrimary

      void clearPrimary(UUID owner)
      Clears the primary flag from all of the player's chests, leaving them with no main chest.
    • isMigrated

      boolean isMigrated(UUID owner)
      Returns true if the player's chest #1 has its migrated flag set.
    • setMigrated

      void setMigrated(UUID owner, boolean migrated)
      Updates the migrated flag on the player's chest #1. No-op if chest #1 does not exist.
    • completeMigration

      void completeMigration(UUID owner, byte[] containerData, @Nullable @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
      Commits a vanilla migration in one transaction: writes chest #1's contents and sets its migrated flag (one single-row UPDATE — chest #1's size is never changed), plus, when overflow is non-null, inserts a temp chest at the next free index carrying the vanilla items that did not fit. Used by the vanilla migration so no crash or concurrent runner can ever observe "contents written but not yet flagged" — the migration merge is not idempotent, so a re-run against an unflagged row would duplicate items. No-op on chest #1 if it does not exist.
      Parameters:
      overflow - encoded bytes of the vanilla items that did not fit, or null when all fit
      tempSize - slot count of the temp chest (ignored when overflow is null)
      tempExpiresAt - epoch-ms expiry of the temp chest (ignored when overflow is null)
    • loadSettings

      PlayerSettings loadSettings(UUID owner)
      Loads the player's settings, or PlayerSettings.defaults() if they have no row yet. Never returns null — an absent row is indistinguishable from an all-defaults one.
    • saveSettings

      void saveSettings(UUID owner, PlayerSettings settings)
      Upserts the player's editMode/appliedDefaultSize (whole-object save): updates the existing row, or inserts one if none exists. Does not touch username — that field is written only by recordPlayerSeen(java.util.UUID, java.lang.String, long), so a save built from a stale in-memory copy never clobbers a name recorded since it was loaded.
    • setEditMode

      void setEditMode(UUID owner, boolean editMode)
      Targeted single-field upsert of just the edit-mode preference. Cheaper than loadSettings(java.util.UUID) + saveSettings(java.util.UUID, com.enhancedechest.model.PlayerSettings) for a one-field toggle (no preceding read) and never clobbers other settings. Use saveSettings(java.util.UUID, com.enhancedechest.model.PlayerSettings) when persisting the whole object.
    • getAppliedDefaultSize

      int getAppliedDefaultSize(UUID owner)
      Reads the persisted applied_default_size baseline — the base-chest size currently dictated by the player's enhancedechest.default_size.<size> permission, or 0 when the base chest is not permission-managed. Returns 0 for a player with no settings row. Used by /ee resize to decide (even for an offline owner) whether the base chest is off-limits.
    • setAppliedDefaultSize

      void setAppliedDefaultSize(UUID owner, int size)
      Targeted single-field upsert of just the applied_default_size baseline (no preceding read), leaving every other setting untouched. Written by the default-size reconcile whenever the player's permission-derived base size changes. 0 records "not permission-managed".
    • recordPlayerSeen

      void recordPlayerSeen(UUID owner, String name, long lastOnline)
      Records that a player was seen: their current in-game name against their UUID (so name→UUID resolution works for offline players from the plugin's own data, with no server usercache or Mojang lookup), and lastOnline as epoch-ms. Called on join and quit, and from ChestOpener's open prelude when the name it already loaded differs.

      Cheap to call often: this only mutates the in-memory row and marks it dirty, so the SQL write rides the next batched flush rather than costing a statement per call.

    • findUuidByName

      @Nullable @Nullable UUID findUuidByName(String name)
      Resolves a stored in-game name to its UUID, case-insensitively, or null if no player with that name has been recorded. Never blocks on the network — this reads only the plugin's own table.
    • loadAllPlayerNames

      Returns every recorded player with a non-null username, with the epoch-ms they were last seen (0 when never recorded). Used once at startup to populate the in-memory PlayerNameIndex so offline-player tab-completion and resolution can answer from memory instead of hitting the DB on every keystroke.