Repository navigation
Ranking
What this page covers: RankedList<T>, the data half of every ordered list - where the entries
come from, how many are kept, in which order, which values each entry answers and how long one read
stays good for. The chat page (PageViewer), the placeholders (Placeholders) and any screen read
the same RankedSnapshot, so reading and ordering is paid once, whoever asks.
Package: br.com.finalcraft.evernifecore.ranking.
import br.com.finalcraft.evernifecore.ranking.CachePolicy;
import br.com.finalcraft.evernifecore.ranking.RankedList;
RankedList<FJPlayerData> top = RankedList.of(plugin, "miner-level", FJPlayerData.class)
.sourceAsync(() -> PlayerController.querySections(FJPlayerData.class, Query.all(),
QueryOptions.builder().descending("level").limit(100).build()))
.maxEntries(100)
.orderBy(FJPlayerData::getLevel).descending()
.cache(CachePolicy.ttl(Duration.ofSeconds(60)))
.addValue("player", data -> UUIDsController.getNameFromUUID(data.getUniqueId()))
.build();
PageViewer.of(top).setFormatLine("&7#${number} &e${player} &2LvL &a${value}").build(); // /ecpage finaljobs:miner-level 2
// %finaljobs_top_[miner-level]_1_name% once RankedPlaceholders.register(replacer, plugin) ranBuild the list once (on enable) and keep it.
| Factory | Id | Registered | Placeholders / /ecpage <id>
|
|---|---|---|---|
RankedList.of(owner, name, type) |
<owner>:<name> (owner = plugin name lower-cased, name [a-z0-9_-]{1,64}) |
yes, in RankedLists on build()
|
yes |
RankedList.anonymous(type) |
none | no | no - navigation falls back to a session |
A list that depends on a command argument (/clan top <clan>) is anonymous: an id has to mean the
same thing tomorrow. Building the same id again replaces the registration - that is what a plugin
reload looks like. RankedLists.find(id) (case-insensitive), ownedBy(owner) and
closeOwnedBy(owner) (run by the default onECPluginShutdownPre) are the registry.
of/anonymous -> a source -> a ceiling -> order, cache and values. The compiler enforces the
first three.
| Step | Thread | Use for |
|---|---|---|
source(Supplier<List<T>>) |
the calling thread | data already in memory (online players, a loaded map) |
sourceAsync(Supplier<CompletableFuture<List<T>>>) |
never blocks the caller | a backend query |
An async source keeps one read in flight: every reader arriving while it runs shares it. A read that fails completes the future exceptionally, keeps the previous snapshot and is not retried until the cache policy would have expired a snapshot built at the moment of the failure - a backend that is down is tried once per policy lifetime, not once per reader. A plain source that throws is a failed future too.
| Step | What it does |
|---|---|
maxEntries(int) |
Keeps at most that many entries. |
maxEntries(Function<List<T>, Integer>) |
Decides the cap at each read, from the raw list. |
unlimitedEntries() |
Every entry the source returned, however many. |
There is no default. What the ceiling cut is counted, not hidden: RankedSnapshot.getTotalCount()
is what the source returned, getShownCount() what was kept, isTruncated() whether they differ.
.orderBy(FPlayer::getName).ascending()
.orderBy(FEPlayerData::getMoney).descending()Without orderBy nothing is sorted. Numbers compare numerically whatever type they arrived in, enum
constants in declaration order, two values of the same Comparable type the way that type says,
anything else as case-insensitive text. What orderBy extracts is also ${value}.
.cache(CachePolicy.ttl(Duration.ofSeconds(5))) // the default
.cache(CachePolicy.none()) // read the source on every snapshot()
.cache(CachePolicy.manual()) // kept until invalidate()The TTL is measured on ECClock, so a test drives it with TestClock.
.addValue("kills", FJPlayerData::getKills) // ${kills}, %..._top_[list]_1_[kills]%Resolved once per entry per snapshot and shared by every reader, page and placeholder. Keys are
bare ("kills", never "${kills}") and case-insensitive. Every list also answers number (1-based
position), value (the order value, or the entry itself) and, when T is an FPlayer or an
IPlayerData, player (its name) - each a default your own addValue of the same key replaces.
list.valueOf(entry, key) reads one for a screen.
list.snapshot() // CompletableFuture<RankedSnapshot<T>>: complete at once while the policy vouches for the cache
list.snapshotIfPresent() // the cached snapshot, valid or not, or null - never reads, never blocks
list.refresh() // read now whatever the policy says (sharing a read in flight)
list.invalidate() // drop the cache; a read already running still answers, but cannot publishRankedSnapshot is immutable: getEntries() (RankedEntry: getObject(), getOrderValue(),
getPosition()), getShownCount(), getTotalCount(), isTruncated(), isEmpty(), getBuiltAt()
and positionOf(uuid) (null when that player is not on the kept list). There is no "loading"
snapshot: before the first read lands snapshotIfPresent() is null.
A /baltop ranks every player. Only the players this server touched are in memory, so the source
is one indexed query, ordered and cut by the backend, which loads nothing into the cache:
RankedList<FEPlayerData> baltop = RankedList.of(plugin, "balance", FEPlayerData.class)
.sourceAsync(() -> PlayerController.querySections(FEPlayerData.class, Query.all(),
QueryOptions.builder().descending("moneyIndex").limit(100).build()))
.maxEntries(100)
.orderBy(FEPlayerData::getMoneyIndex).descending()
.cache(CachePolicy.ttl(Duration.ofSeconds(30)))
.addValue("player", row -> UUIDsController.getNameFromUUID(row.getUniqueId()))
.build();Two consequences, and a list that ignores either is broken:
-
Every field the query touches is
@Indexedon the section, or the future fails withIllegalArgumentException. The index's widest number isdouble: keep adoublebeside aBigDecimalto order by. -
The rows come back detached - no
PlayerData, sogetName()/getPlayer()answernulland the built-in${player}is null; look the name up from the uuid as above.
A PRELOADED section is the exception: the whole collection is in memory by contract, so
.source(() -> new ArrayList<>(PlayerController.getAllSections(FEPlayerData.class))) is a plain source.
See PlayerData-and-PDSections.
RankedPlaceholders.register(replacer, plugin) serves every list of plugin by its name;
the core serves every list by full id under %evernifecore_top_[owner:name]_...% when PlaceholderAPI
is present. The table is on Placeholders.
- PageViewer - the chat renderer of a list
-
Placeholders -
top_[list]_... -
PlayerData-and-PDSections -
querySections,getAllSections,@Indexed
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Regions
- Groups
- Notifications
- NPCs
- Logic
- Quests
- Provenance
- Scheduler & Threading
- Items & NBT
- Screens
- Dialogues
- Menus
- Integrations
- Economy
- Boosters
- Deliveries
- Audit Log
- Dynamic Values
- Player Ignore
- Version Compatibility
Architecture & Reference