Class PlayerNameIndex
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 ClassesModifier and TypeClassDescriptionstatic final recordOne recorded player: their current known name, UUID, and the epoch-ms they were last seen (0when never recorded — a row that predates last-online tracking). -
Constructor Summary
ConstructorsConstructorDescriptionPlayerNameIndex(StorageGateway gateway, org.slf4j.Logger logger, Telemetry telemetry, long suggestWindowMillis) -
Method Summary
Modifier and TypeMethodDescriptionCase-insensitive point lookup, O(log n) — used to resolve an already-typed name to a UUID.loadAll()Loads every known (uuid, username, last-seen) triple from the DB once.prefixMatches(String lowerPrefix, int limit) Returns up tolimitknown names starting withlowerPrefix(already lower-cased by the caller), ascending, skipping players last seen longer ago than the suggest window.voidRecords or updates one player's name and last-seen time.voidsetSuggestWindowMillis(long suggestWindowMillis) Re-appliescommands.suggest-offline-withinafter a reload.
-
Constructor Details
-
PlayerNameIndex
public PlayerNameIndex(StorageGateway gateway, org.slf4j.Logger logger, Telemetry telemetry, long suggestWindowMillis)
-
-
Method Details
-
setSuggestWindowMillis
public void setSuggestWindowMillis(long suggestWindowMillis) Re-appliescommands.suggest-offline-withinafter a reload.0= suggest everyone. -
loadAll
Loads every known (uuid, username, last-seen) triple from the DB once. Call exactly once, at plugin startup. -
put
Records or updates one player's name and last-seen time. Called byPlayerSettingsCache.markSeenAsyncalongside its DB write, so the index never drifts from whatfindUuidByNamewould 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 matchesSQL_NAME_FIND's existing behavior). -
findUuid
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
Returns up tolimitknown names starting withlowerPrefix(already lower-cased by the caller), ascending, skipping players last seen longer ago than the suggest window.subMapjumps 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
limitentries — still a lock-free in-memory walk of a few thousand nodes at worst, and typing one more character collapses the range.
-