# Aeshnidae.Leaderboard

Who is highest, richest, deadliest and most enlightened. Seventeen boards, built
from numbers the server already keeps plus the few it did not, shown in four places:
`/top` in game, a page in the Aeshnidae Codex, `leaderboard.json` beside the dll for
the website's Leaderboards page, and a daily post to Discord.

```
/top                     the boards, with who leads each
/top <board> [n]         the top n of one board (default 10, up to MaxRows)
/top me                  where you stand on every board
/top <board> me          where you stand on one
/top refresh             admin: rebuild now
/top post                admin: post to Discord now
/top hooks               admin: last refresh, Discord result, which sibling hooks are bound
/top-reload              admin: re-read Settings.json
```

## The boards

| Key | Title | Where the number comes from |
| --- | --- | --- |
| `level` | Level | `Level` (25), with enlightenments beside it |
| `enlightenment` | Enlightenments | `Enlightenment` (390) |
| `xp` | Lifetime experience | `TotalExperience` (int64 1) |
| `radiance` | Radiance banked | `aeshnidae_bank`, **per account** |
| `resonance` | Resonance banked | `aeshnidae_bank`, **per account** |
| `luminance` | Luminance | `AvailableLuminance` (int64 6) on hand + the account's banked luminance |
| `radiance-earned` | Radiance earned | counted here, since install |
| `radiance-spent` | Radiance spent on mastery | counted here, since install |
| `resonance-earned` | Resonance earned | counted here, since install |
| `resonance-spent` | Resonance spent on auras | counted here, since install |
| `luminance-earned` | Luminance earned | counted here, since install |
| `luminance-spent` | Luminance spent | counted here, since install (deposits into the bank are not spending) |
| `kills` | Creature kills | `CreatureKills` (207) - ACE counts these already |
| `deaths` | Deaths | `NumDeaths` (43), with kills per death beside it |
| `pk` | Player kills | `PlayerKillsPk` (208) |
| `mastery` | Mastery points | sum of `aeshnidae_skill_mastery` ranks |
| `auras` | Aura ranks | sum of `aeshnidae_resonance_auras` ranks |

A per-account board has one row per account, named for its most recently played
character (or the one online), with "and N other characters" beside it - the bank
belongs to the account and there is no honest way to split it. Ties share a rank.
A zero is not on the board.

**Who is on them.** Every character that is not deleted, not on `ExcludeNames`, and -
with `ExcludeStaff` on - not on an account whose access level is above Player. The
auth database is joined for that; a level-999 admin is not a leaderboard entry.

## How it works

**A refresh** (every `RefreshMinutes`) is one read of the shard database - the
character list, the seven properties, the three sibling tables, this mod's own
counters - and then, for every character in the world, the live value off the
`Player` object instead. ACE writes player biotas to the database on a timer, so
the database is minutes behind for anyone online; the live read closes that gap.
Everyone else is exactly as they logged out. The snapshot is what every readout
serves; nothing a player types touches the database.

**The counters** are the flows nothing else keeps. Luminance earned and spent are
hooked in ACE (`Player.AddLuminance`, the clamped change in the balance - or the whole
award when Aeshnidae.Bank has diverted it to the bank; `Player.SpendLuminance`, except
inside `BankService.Deposit`). Radiance and Resonance earned come from Bank's
`Earning.Award`; spent from `MasteryDb.TryDebitRadiance` and `AuraDb.TryDebitResonance`.
Those three are in sibling mods, in their own load contexts, so they are bound by
name at runtime through Harmony's imperative API - the AdminAudit pattern, with its
caveats: a rename upstream drops a hook silently, `/mod find` unbinds them. `/top hooks`
shows the state and every refresh retries anything unbound. A balance is per account;
a flow is credited to the character of that account who is in the world at the time.

Counters are buffered in memory and flushed every `FlushSeconds` to
`aeshnidae_stats` (character, stat, value); a crash loses at most that much counting.
They start at zero on install - the boards say "since 16 Sep 2026".

**`leaderboard.json`** is rewritten beside the dll on every refresh (atomically):
`generated`, `characters`, and every board with its top `MaxRows` rows. The website's
Leaderboards page is built from it; see `C:\Aeshnidae\ops\publish-leaderboard.sh`.

**Discord.** Once a day at `Discord.PostHourUtc`, the top `Discord.Rows` of each board
in `Discord.Boards` (empty means all), split under 2,000 characters on board
boundaries. The day of the last post is kept in `lastpost.txt` so a restart in that
hour does not post twice. The webhook URL goes into the deployed `Settings.json` with
`set-webhook.sh`, never into source.

## Settings

- `RefreshMinutes` (5), `Rows` (10), `MaxRows` (50), `FlushSeconds` (15)
- `ExcludeStaff` (true), `ExcludeNames` ([])
- `WriteJson` (true)
- `Discord`: `WebhookUrl` (""), `Username`, `PostHourUtc` (12; -1 never), `Rows` (5), `Boards`

| File | |
| --- | --- |
| `Boards.cs` | The seventeen boards, the snapshot, the refresh, the JSON. |
| `Hooks.cs` | The counters: ACE's luminance hooks and the sibling binds. |
| `Db.cs` | The counters' table and buffer; the one big read. |
| `Discord.cs` | The daily post. |
| `Commands.cs` | `/top`, `/top-reload`. |
| `CodexPage.cs` | The Codex pages. |
