Skip to content

Ranking

Pétrus Pradella edited this page Oct 25, 2026 · 1 revision

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.


The 30-second version

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

Build the list once (on enable) and keep it.


Identity: named or anonymous

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.


The builder is a staircase

of/anonymous -> a source -> a ceiling -> order, cache and values. The compiler enforces the first three.

The source

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.

The ceiling

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.

The order

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

The cache

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

The values

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


Reading

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 publish

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


Ranking the whole player base

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 @Indexed on the section, or the future fails with IllegalArgumentException. The index's widest number is double: keep a double beside a BigDecimal to order by.
  • The rows come back detached - no PlayerData, so getName()/getPlayer() answer null and 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.


Placeholders

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.


See also

Clone this wiki locally