Class PlayerNameIndex

java.lang.Object
com.enhancedechest.service.PlayerNameIndex

public final class PlayerNameIndex extends Object
In-memory index of every player name the plugin has ever recorded (the players.username column — see PlayerSettingsCache#markSeenAsync), used to answer offline-player tab-completion and name resolution from memory instead of hitting the DB on every keystroke.

Unlike PlayerSettingsCache (bounded to online players, evicted on quit), this index holds every known player for the lifetime of the server — it is loaded once in full via loadAll() at startup and kept in sync afterward by a single put(java.util.UUID, java.lang.String, long) call per name change, so it never needs a DB round-trip on the command/suggestion hot path.

The database is the only name source. Nothing here — and nothing that reads this index — may touch the playerdata folder. Bukkit.getOfflinePlayers() looks like the obvious way to fill the gaps, but it builds one OfflinePlayer per file in that folder and getName() on any whose profile is not in the usercache loads and decompresses that player's .dat file. On a server with a long history that is tens of thousands of NBT reads and their transient buffers: it pinned a server thread on disk I/O when the suggestion providers called it per keystroke, and doing it in one startup sweep instead only concentrates the same memory pressure into a burst that can cost the server an OOM kill. Names that are not in the DB are simply not completable; the admin types them in full and the command still resolves them.

Coverage therefore comes from writes, not scans: PlayerSettingsCache.preloadSettings records a joining player's name (reusing the settings row the join already loads, so a returning player whose name is unchanged costs no write), and ChestOpener's open prelude does the same. Every player who joins is in the DB from then on.

Keyed by lower-cased name in a ConcurrentSkipListMap (not a plain hash map) specifically so prefix search — the shape every tab-completion query actually needs — is a subMap range lookup, O(log n + k) for k matches, rather than an O(n) scan of the whole roster. Lock-free reads make it safe to call from Brigadier suggestion callbacks (main thread) while an async name update is landing concurrently.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final record 
    One recorded player: their current known name, UUID, and the epoch-ms they were last seen (0 when never recorded — a row that predates last-online tracking).
  • Constructor Summary

    Constructors
    Constructor
    Description
    PlayerNameIndex(StorageGateway gateway, org.slf4j.Logger logger, Telemetry telemetry, long suggestWindowMillis)
     
  • Method Summary

    Modifier and Type
    Method
    Description
    Case-insensitive point lookup, O(log n) — used to resolve an already-typed name to a UUID.
    Loads every known (uuid, username, last-seen) triple from the DB once.
    prefixMatches(String lowerPrefix, int limit)
    Returns up to limit known names starting with lowerPrefix (already lower-cased by the caller), ascending, skipping players last seen longer ago than the suggest window.
    void
    put(UUID uuid, String username, long lastOnline)
    Records or updates one player's name and last-seen time.
    void
    setSuggestWindowMillis(long suggestWindowMillis)
    Re-applies commands.suggest-offline-within after a reload.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Constructor Details

    • PlayerNameIndex

      public PlayerNameIndex(StorageGateway gateway, org.slf4j.Logger logger, Telemetry telemetry, long suggestWindowMillis)
  • Method Details

    • setSuggestWindowMillis

      public void setSuggestWindowMillis(long suggestWindowMillis)
      Re-applies commands.suggest-offline-within after a reload. 0 = suggest everyone.
    • loadAll

      public CompletableFuture<Void> loadAll()
      Loads every known (uuid, username, last-seen) triple from the DB once. Call exactly once, at plugin startup.
    • put

      public void put(UUID uuid, String username, long lastOnline)
      Records or updates one player's name and last-seen time. Called by PlayerSettingsCache.markSeenAsync alongside its DB write, so the index never drifts from what findUuidByName would answer. A rename simply adds a new key; the old lower-cased name is left pointing at the same UUID (the DB itself has no rename history either — this matches SQL_NAME_FIND's existing behavior).
    • findUuid

      public UUID findUuid(String name)
      Case-insensitive point lookup, O(log n) — used to resolve an already-typed name to a UUID.

      Not filtered by the suggest window: that window only decides whose name is offered while typing. A name typed in full must always resolve, however long its owner has been away — otherwise an admin could no longer reach the chests of a player who stopped logging in, which is exactly when they most often need to.

    • prefixMatches

      public List<PlayerNameIndex.NameEntry> prefixMatches(String lowerPrefix, int limit)
      Returns up to limit known names starting with lowerPrefix (already lower-cased by the caller), ascending, skipping players last seen longer ago than the suggest window. subMap jumps straight to the matching range instead of scanning every entry, so this stays cheap even once the roster is in the tens of thousands.

      Filtered-out entries still cost a step of the walk, so with a narrow window and a broad prefix this can examine many more than limit entries — still a lock-free in-memory walk of a few thousand nodes at worst, and typing one more character collapses the range.