diff --git a/android/PLAN.md b/android/PLAN.md index 2ab48ca..567c0ed 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1619,6 +1619,21 @@ push, and Play (M6–M8) follow the designed app. Excluded: editing the schedule, which is operator configuration. +20. **M19 — chat titles on a Rust leaderboard** (post-v1; planned 2026-09-25, + `feat/rust-phase-17-titles` into `edge`). `module-rust` phase 17. **Design of record: + [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §33**, D135–D137. The phase is mostly + in-game BetterChat work. The app's part is one field. + + - **`titles: [{ text, color }]`** on each row of `GET /servers/:id/leaderboard`. These are the + titles the game shows in chat, after the server's mode (first, all or up to N). The operator + sets the rules. + - **Each title is a chip beside the player's name** on the leaderboard, coloured from `color`, + with its text colour chosen for contrast. + - **Against an older module the field is absent**, so the DTO defaults it to empty and nothing + is drawn. + + Excluded: the rules, the mode and group styles, which are all operator configuration. + ### Deferred (not a milestone) - **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 7e5146c..3ea6382 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -213,6 +213,13 @@ What the plugin genuinely adds is **alliances and clan/alliance chat** — riche roster data. That is what phase 17's adapter surfaces, and it sits beside the Team provider rather than under it. +> **Corrected again, and the adapter dropped, by phase 17 (§33, 2026-09-25).** *"There is no read API +> whatsoever"* above is the second grep artefact in this section. `Clans` 0.2.10 has `GetClan`, +> `GetAllClans`, `GetClanOf`, `GetClanMembers`, `GetClanAlliances` and more, as plain private methods +> reached by name, which a `[HookMethod]` search cannot see (§33.1). The org lead then dropped the +> adapter (D134). A per-server *clan source*, with the plugin as a server's Teams source, is a candidate +> future phase that would reverse this decision. It is built only if an operator asks. + One route is deliberately not taken: the plugin persists to its own files under `oxide/data/`, and a determined integration could read those directly. **That is reaching into another plugin's private storage, not using an API** — it breaks without warning on any upstream refactor and is not a @@ -224,6 +231,11 @@ upstream pull request adding a read method, not a file reader. > **Amended 2026-09-23 by D47 (§24):** the uMod **Clans** plugin is no longer required. It moved to > phase 17's optional tier, because it shares no data with the game's own clans, which are what > Teams are built from. The required set is Kits, PopupNotifications and ZoneManager. +> +> **Amended again 2026-09-25 by D141 (§33):** PopupNotifications is optional too. Nothing called it +> once D105 sent `rust.announce` through `Server.Broadcast`, and phase 17 made popups a delivery +> choice that refuses with a reason when the plugin is absent. The required set is Kits and +> ZoneManager. **Named 2026-09-15 (org lead), extended the same day by R17.** All four are MIT; three are k1lly0u's and BetterChat's author differs only in the optional tier. All fetched at plan time: @@ -303,6 +315,10 @@ not move the provider off first-party, but it does mean phase 17's adapter can o leadership* on servers that run the plugin, over a snapshot baseline on servers that do not. Design phase 9's snapshot so phase 17 can sharpen it rather than replace it. +> **Withdrawn by phase 17 (§33.1).** The plugin's `OnClanUpdate` is a promotion in *its own* clans, +> and D47 established that it never touches the game's. It cannot sharpen first-party leadership, +> which stays snapshot-driven (D54). + ### R7 — the notifications and engagement set ships in v1 **Decided 2026-09-15 (org lead).** `registerNotificationStreams`, `registerEventTriggers`, @@ -652,6 +668,11 @@ naturally with R2: the site already authors permission groups, so a site-authore chat colour and tag. That is a push and would need the same drift posture R2 has; it is a phase-17 decision, not a given. +> **Decided by phase 17 (§33, D134–D143).** All three: titles from operator rules (D135, D136), +> all twelve fields on a site group with R2's drift posture (D138), and that style as the voice of +> news and event lines (D140). `API_SetGroupField` does not save, so a pushed style is saved through +> BetterChat's own console command (D143). + **The tier is open-ended by design.** Other integrations get added as they prove useful, and the bar for each is the one this plan applies everywhere: it must fulfil the contract — declare honestly, degrade to absent, and never make the module's own surfaces depend on something that may not be @@ -1009,7 +1030,7 @@ Each phase ends with its findings written down, as every workstream here does. | 14 | **The live map** (R9). ✅ **Built and walked 2026-09-25 (§30.7), protocol 11, D119–D120 (§30.6); the person-parts are the map walk in `PLAYER_WALK.md`.** 📐 **Plan of record in §30 (D109–D118), 2026-09-24.** A rig probe found that the `.map` file is terrain, not a picture, and that Rust+ already caches the rendered JPEG; the game's own render stalls the main thread 8.5 s (§30.0). Protocol 11. *As first written:* the map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it | | 15 | **Android leg D** (R10). Map and events. ✅ **Built and walked on an emulator 2026-09-25 (§31.5); the counted tally and a real player are in `PLAYER_WALK.md`.** 📐 **Plan of record in §31 (D121–D125), 2026-09-25.** The map is a fifth tab drawn natively in Compose over the module's projected answer, so the app has no gate of its own to get wrong. "Events" is a site-event marker opening the app's own event page, plus M13 walked against a Rust run. That needs one field on core's public calendar (D125) | Android-app + website + docs | The map renders on a phone with the same layer gates | | 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral. ✅ **Built and walked 2026-09-25 — plan in §32 (D126–D132), as built and findings in §32.5–32.7 (D133).** Five commands. Any answer narrower than public goes to the caller alone (D127), which is the reverse of the case the criterion names and the one core does not cover. Plus an operator-set **next wipe** (a rule and a one-off date) shown on the web, in the app and by `/wipe` | Module-Rust + Android-app + docs | A refusal does not go public in the channel, **and neither does a permitted answer narrower than public** | -| 17 | **Optional mod integrations** (R15). **BetterChat** first — leaderboard titles through `API_RegisterThirdPartyTitle`, a pull with no drift — then the uMod **Clans** adapter (alliances and clan chat, beside the provider rather than under it, R5), then others as they prove useful | Rust-Plugins + Module-Rust + docs | A server missing every optional mod still runs the module, Teams included | +| 17 | **Optional mod integrations** (R15). 📝 **Planned 2026-09-25 — plan in §33 (D134–D143).** **BetterChat** three ways: leaderboard titles through `API_RegisterThirdPartyTitle` from operator rules, shown on the web and in the app too; all twelve chat fields on a site-authored group with R2's drift posture; and that style as the voice of news and event lines. **PopupNotifications** becomes optional and a delivery choice. Protocol 12. ~~The uMod **Clans** adapter~~ is dropped (D134): its read API exists after all, and a per-server clan source is a candidate future phase | Rust-Plugins + Rust-Link + Module-Rust + Android-app + docs | A server missing every optional mod still runs the module, Teams included | | 18 | **The installer** (R4) **and the Pterodactyl egg** (R20) — the two halves of "how the shard side reaches an operator", built together so one story is not told twice. `--game servuo\|rust`, the bundle payload as a variant, a **framework** prerequisite check in `doctor` (which one, not whether Oxide — R19), the protocol pairing refusal carried over; the egg derived from "Rust Autowipe" with the sidecar inside the game container, a second allocation for `[web].bind`, **the sidecar configured from egg variables** (R22), the sidecar store held out of `REMOVE_FILES`, and its install script fetching the same pinned pair the installer resolves | installer + Rust-Link + docs | An operator sets a Rust server up with the released binary and nothing hand-copied; **and** a second operator imports the egg, deploys, and reaches the same place — on either framework | | 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change, and Pterodactyl is a hosting claim the site can now make); the Integration-kit question R2 raised; and whether the kit owes a reader anything about **supporting two mod frameworks at once** (R19) — a shape it has no chapter for either | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way | @@ -6038,6 +6059,302 @@ identity. while its header still says *wiped Sep 15*. The header reads the state row's `saveCreatedAt`. This phase does not touch it. +## 33. Phase 17 — optional mod integrations (plan of record, 2026-09-25) + +R15's phase. The row named two members: **BetterChat** leaderboard titles first, then an adapter +for the **uMod Clans** plugin. Reading the plugins' sources again changed both halves. + +- **The Clans adapter is dropped** (D134). The plan was wrong about that plugin a second time: it + *does* have a read API (§33.1). But a read-only list of a second clan system, beside Teams and + not part of them, adds little the game does not already show its players. What would add value + is letting an operator make the plugin a server's Teams source. That reverses R5 and D47, so it is + recorded as a candidate future phase, to build if an operator running uMod Clans asks. +- **BetterChat grows from one use to three.** Leaderboard titles in chat (D135, D136), a chat style + on a site-authored permission group (D138, D139, D143), and that style as the **voice** of + news and event lines in game chat (D140). +- **PopupNotifications joins the phase**, as D62 promised. It leaves the required set, and popups + become a delivery choice for `rust.announce` and for the news leg (D141, D142). + +Titles appear on the web's leaderboard and in the app's (D137), so **Android-app joins this phase.** + +Five repositories: [Rust-Plugins][rp], [Rust-Link][rl], [Module-Rust][mr] and [Android-app][aa] +into `edge`, and this document into `main`. **The protocol moves to 12** (§33.2). **Core does not +change and `MODULE_API` does not move.** Everything here rides on surfaces the module already +registers: the action registry, the announce leg and its own routes. + +### 33.0 The decisions this phase needed + +All from the org lead, 2026-09-25. + +| # | Decision | +|---|---| +| **D134** | **The phase is BetterChat (titles, group styles, the voice) and popups. The uMod Clans adapter is dropped.** A per-server *clan source*, which would let the plugin feed Teams on a server that chooses it, is recorded as a candidate future phase, to build only if an operator asks. It would reverse R5 and D47. Rejected: the read-only uMod clan list and roster the row implied, which is a lesser second clan surface beside Teams. | +| **D135** | **Titles come from operator rules, per server.** A rule names a stat (`kills`, `npckills` or `playtime`), a top N (1–10), the title's text and its colour. It ranks the **current wipe**. With no rules, a server shows no titles. Rejected: a fixed built-in set with one switch, and rules that may also rank all-time. | +| **D136** | **How many titles a player shows is a per-server setting:** the first rule they meet, all of them, or up to N. The operator's rule order decides precedence. | +| **D137** | **Titles show on the web and in the app as well as in chat.** They appear on leaderboard rows. Android joins the phase for it. | +| **D138** | **A site-authored permission group can carry all twelve BetterChat fields.** A hand edit made in game follows R2: it is reported as drift, never silently overwritten. | +| **D139** | **Deleting a styled group removes its BetterChat group** with BetterChat's own `chat group remove`, run as the server console. BetterChat has no remove API, and each time it loads it re-creates an Oxide group for every chat group it holds. A failure is reported, not swallowed. Rejected: blanking the fields and leaving the group, which would bring the Oxide group back on every BetterChat reload. | +| **D140** | **A styled group can be the voice of news and of `rust.announce`.** The module composes the line in BetterChat's markup and the plugin broadcasts it. There is no stand-in sender, and it works whether or not BetterChat is loaded. Rejected: `API_SendMessage`, which needs a player as the sender and would show every announcement as coming from that player. | +| **D141** | **PopupNotifications leaves `requires_plugins`**, which amends R6 a second time. `rust.announce` gains a delivery choice, `chat` or `popup`. A popup on a server without the plugin refuses with a reason (R3). Rejected: removing it entirely, and leaving it required and unused. | +| **D142** | **The news leg gets the same choice**, as a per-server setting beside `announce_news`. | +| **D143** | **A pushed style is set through BetterChat's API and then saved through its console command.** `API_SetGroupField` changes a field in memory and never saves it (§33.1). The plugin sets each field through the API, for its typed answers. Then, once per group it touched, it runs `chat group set` with a value that has not changed, because that path writes BetterChat's whole file. Rejected: writing every field through the console, which gives no typed answer. Also rejected: re-applying after every reload, which would silently overwrite a hand edit that R2 says must be reported. | + +### 33.1 Facts the phase rests on + +**From the plugins' sources**, fetched fresh from uMod on 2026-09-25: BetterChat 5.2.15, Clans +0.2.10, PopupNotifications 0.2.1. + +- **BetterChat keeps a third-party title as a callback.** `API_RegisterThirdPartyTitle(plugin, + Func)` stores it in a dictionary. + - `PrepareMessage` calls every callback on each chat line, on the main thread. It calls them + again whenever another plugin asks `API_GetMessageData` or `API_GetFormattedMessage`. + - A callback's exception is caught and logged per line. + - Third-party titles are appended **after** BetterChat cuts its own titles to `MaxTitles`, so + that setting does not limit ours. + - The string is used as given. **Its colour markup is ours to write**, in BetterChat's + `[#hex]…[/#]` / `[+size]…[/+]` form, which `covalence.FormatText` turns into the game's rich + text. +- **A registration does not survive BetterChat reloading.** The dictionary belongs to the + instance, and `OnPluginUnloaded` drops ours when *we* unload. The Clans plugin shows the pattern: + register at init, and again from `OnPluginLoaded` when the loaded plugin's title is + `"Better Chat"`. +- **The Clans plugin registers a title itself** (§12.3). On a server running both, ours is the + second. +- **Group styles, the field list:** a `ChatGroup` has twelve settable fields: `Priority`, `Title`, + `TitleColor`, `TitleSize`, `TitleHidden`, `TitleHiddenIfNotPrimary`, `UsernameColor`, + `UsernameSize`, `MessageColor`, `MessageSize`, `ChatFormat` and `ConsoleFormat`. +- **Group styles, the API:** + - `API_AddGroup` saves. + - `API_SetGroupField` answers `Success`, `InvalidField` or `InvalidValue`, or `null` for a group + that does not exist. + - `API_GetGroupFields` reads a group back. + - **`API_SetGroupField` never calls `SaveData`** (D143). Only `API_AddGroup`, `Loaded()` and the + `chat` command's own edits do. +- **Group styles, removal:** + - There is **no remove API**. The only removal is `chat group remove `. That command is + `[Command("chat"), Permission("betterchat.admin")]` and has a console branch + (`player.LastCommand == CommandType.Console`). + - `Loaded()` creates an Oxide group for every chat group that lacks one. So a chat group the site + forgot brings its Oxide group back on every BetterChat load (D139). +- **Group names:** `ChatGroup.Find` matches a group's name exactly, and the `chat` command + lowercases what it is given. Oxide's own groups are case-insensitive. +- **`API_SendMessage` needs a player.** The message dictionary's `Player` is an `IPlayer`, its id + is written into the line, and Rust shows that player as the sender (D140). +- **PopupNotifications has one call,** `[HookMethod("CreatePopupNotification")] (string message, + BasePlayer player = null, float duration = 0f)`. + - A null player means everyone *connected*. + - A duration of 0 means the plugin's configured default. + - Nothing queues for a player who is offline. +- **The Clans plugin has a read API, and R5 says it has none.** `#region API` holds plain private + methods, reached by name exactly as ZoneManager's are (§12.4): + - `GetClan(tag)` returns a `JObject` of `tag`, `description`, `owner`, `moderators`, `members`, + `allies` and `invitedallies`; + - also `GetAllClans`, `GetClanOf`, `GetClanMembers`, `GetClanAlliances`, `IsClanMember`, + `IsMemberOrAlly` and `HasFriend`. + + A search for `[HookMethod]` finds none of them. It is the three-conventions trap of §12.4 again, + and the second grep artefact in R5 (the first was §12.3). It no longer decides anything, because + D134 drops the adapter. But it would decide the clan-source phase: that phase needs no upstream + pull request and no file reader. +- **D54's promised sharpening does not exist.** The Clans plugin's `OnClanUpdate` fires when a + member is promoted *in that plugin's clans*. D47 established that the plugin never touches the + game's clans, which are the ones Teams are built from. Leadership stays snapshot-driven. + +**From our own code** (`edge` in each repo): + +- **The plugin holds minute tallies, not standings.** `_tallies` is flushed as `player.tally` + every 60 s (PROTOCOL.md §8.6), and the standings live in `rust_player_wipe_stats`. So a title is + computed by the module and **pushed**. The plugin's callback reads what was pushed. +- **`perm.sync` is one verb with a whole desired set** (D32). + - The pushed ledger (`rust_perm_pushed`) is what tells a retirement from a hand edit (D31). + - A drift row is `(kind, subject, object)` and holds **no value**. A changed field needs one + (§33.2). +- **`chat.say`:** + - It flattens the message, bounds it at 256 characters (`EventsMaxChat` may lower that), + remembers keys for ten minutes and says the line with `Server.Broadcast`. + - `rust.announce` and the news leg (`rust.chat`, the servers with `announce_news` on) are its two + callers. +- **The sidecar has one route per command** (`web.rs`). + - `/chat` forwards its body as an object and adds nothing. New fields on `chat.say` need no + sidecar code. + - `titles.set` needs a route. +- **PopupNotifications is required and unused.** `overlay.toml` lists it, but nothing calls it: + D105 sent `rust.announce` through `Server.Broadcast`. +- **There is no public player page.** The leaderboard is where a title can show (D137). + +### 33.2 What the plan builds + +**Protocol 12** (PROTOCOL.md §18). `PROTOCOL_VERSION`, `overlay.toml`'s `protocol` and +`module.json`'s protocol move to 12 in the same change. + +- **`titles.set`** (module → plugin, through a new `POST /titles`). + - Body: `{ setId, titles: [{ steamId, text }] }`, where `text` is already in BetterChat markup. + - It **replaces** what the plugin holds. It is not a stream of edits, and it is not persisted: + the module re-sends it on every boot id or wipe id it has not sent to, as it does for + permissions. + - Answered `titles.ok { count, betterChat }`, where `betterChat` says whether a callback is + registered right now. + - Refused with `titles.error`: `malformed`, or `too-large` past 500 entries. +- **`perm.sync`, the groups:** a group may carry `chat: { : { value, expect } }`. + - `expect` is the value this site last pushed, or `null` for a field it has never pushed. + - The plugin writes a field only when the game's value still equals `expect`, or is BetterChat's + default and `expect` is `null`. A different value is a hand edit: it is not written, and it is + reported (D138). + - A retired group that carried a style is removed with `chat group remove` (D139). +- **`perm.sync`, the report:** it gains `chat`: + - `loaded`, `version`, `applied` and `saved` (D143); + - `drift: [{ group, field, game }]`; + - `removed: [group]`; + - `failed: [{ group, field?, reason }]`. + + With BetterChat absent, the answer is `loaded: false` and nothing else. The permissions + themselves sync exactly as before. +- **`chat.say`:** it gains `delivery`, `format` and a new refusal. + - `delivery` is `chat` (the default, and what every protocol-11 caller sends by leaving it out) + or `popup`. + - `format` is optional: BetterChat markup with one `{message}` in it, at most 512 characters. + The plugin substitutes the flattened message *after* bounding it, so the 256-character limit + is still the text's, never the markup's. Then it runs `covalence.FormatText`. + - A popup with PopupNotifications absent is refused `chat.error { code: 'popup-unavailable' }` + with a sentence. + - A popup ignores `format`: its panel is not a chat line. +- **`server.hello` and `server.status`** gain `integrations: { betterChat, popupNotifications }`, + each `{ loaded, version }`, so Admin → Rust can show what each server has. The clan board's + `umodClans` flag stays where D47 put it. +- **Console `rg.titles [steamId]`** prints how many titles are held and whether the callback is + registered. Given a Steam id, it also prints what BetterChat would render for that player, from + `API_GetFormattedMessage` over `covalence.Players.FindPlayerById`. That is how the walk proves a + title without a person typing in chat (§33.3). + +**Rust-Plugins** (`feat/phase-17-integrations`, from `edge`): + +- **`[PluginReference] BetterChat` and `PopupNotifications`,** both optional. Each call checks for + null and answers with a reason. +- **The title callback.** + - The map is one `Dictionary`, **swapped whole** on each `titles.set`, never + edited in place. The callback can be reached from another plugin's thread through BetterChat's + API, and a swapped reference is safe to read where a mutated dictionary is not. + - The callback does one lookup and returns `null` for a player with no title. No socket, no file, + no allocation beyond the lookup (R15's trap). + - It registers at `OnServerInitialized`, and again on `OnPluginLoaded` when the plugin's title is + `"Better Chat"`. +- **Group styles inside `HandlePermSync`,** after the groups exist: + - `API_AddGroup` for a styled group BetterChat lacks. + - Compare-and-set for each field, through `API_GetGroupFields` and then `API_SetGroupField`. + - One `chat group set` per touched group, to save (D143). + - `chat group remove` for a retired styled group. + - Every console command goes through `ConsoleSystem.Run(ConsoleSystem.Option.Server, …)` with + its arguments quoted. The result is read back through `API_GroupExists` / `API_GetGroupFields`, + never inferred from the command having run. +- **`chat.say` delivery and `format`,** as above. +- **`overlay.toml`:** `protocol = 12` and `requires_plugins = ["Kits", "ZoneManager"]` (D141). + +**Rust-Link** (`feat/phase-17-integrations`, from `edge`): `POST /titles` → `titles.set` through +`forward_object`, `PROTOCOL_VERSION = 12`, and the README's route table. + +**Module-Rust** (`feat/phase-17-integrations`, from `edge`): + +- **Schema:** + - `rust_title_rules (id, server_id, position, stat, top_n, text, color)`; + - `rust_servers.title_mode` (`first`, `all` or `upto`, default `first`) and `title_max`; + - `rust_servers.news_delivery` (`chat` or `popup`, default `chat`); + - `rust_perm_group_chat (group_name, field, value)`, with a cascade from the group; + - `rust_perm_drift.detail` (nullable), for the value a changed field holds in game; + - the fleet setting `announce.voice` in `rust_settings`: a group name, or empty for plain chat. +- **`titleSync.js`**, a loop on the same tick as `permSync.js`. + - Per enabled server, it evaluates the rules against the current wipe's standings, applies the + mode, composes each player's text, digests the set, and sends `titles.set` when the digest, + the boot id or the wipe id moved. + - A pure `model/titles/titles.js` does the evaluation, so the rules, ties and modes are unit + tests. +- **The leaderboard** (`GET /servers/:id/leaderboard`): each row gains `titles: [{ text, color }]`. + These are the same titles, after the same mode, that the game shows. The array is empty for a + player with none. +- **`permSync`:** + - Each group's `chat` goes into the desired set, with `expect` from the pushed ledger, which gains + one row per field pushed (`kind` `chat-field`). + - The report's `chat.drift` becomes drift rows with `detail`. + - **Adopt** copies the game's value into the site's group. + - **Revoke** re-pushes the site's value with `expect` set to what the game holds. That is a + person choosing to overwrite, which R2 allows. +- **Actions:** + - `rust.announce` gains `delivery` (`chat` or `popup`, from a new `rust.options.delivery` option + source, since core has no enum type) and applies the voice. + - The news leg reads each server's `news_delivery` and applies the voice. + - `rust.announce` **stays version 1.** `delivery` is optional, defaults to `chat` and carries an + `example`. A bump would stop every existing step from dispatching until someone re-saved it + (EVENTS.md: a step pinned to an older `action_version` warns instead of running). +- **Admin:** + - **Admin → Rust servers**, per server: a *Chat titles* section (the rules, ordered; the mode and + N), the news delivery, and what the server reports under `integrations`. A section whose plugin + is not loaded says so and still saves, because titles are held until BetterChat arrives. + - **Admin → Rust permissions**, per group: a *Chat style (BetterChat)* section with the twelve + fields, and its drift rows beside the permission drift. + - The fleet voice is chosen on the servers page. +- **Web:** titles as small coloured tags beside the name on leaderboard rows. +- **The module's own docs:** the swagger fragment, `routes.manifest.json` if a shape moved, and + the README's plugin list, with BetterChat and PopupNotifications under *optional*. + +**Android-app** (`feat/rust-phase-17-titles`, from `edge`): `RustLeaderboardRowDto.titles`, +defaulting to empty so an older module sends nothing and nothing is drawn. Each title is a chip +beside the name, coloured from `color`, with text contrast checked. `android/PLAN.md` M19. + +### 33.3 The walk + +On **both rigs**, because every piece here is framework code. BetterChat is Universal, and it must +be proven on Carbon as well as Oxide. Real core on a walk database, BetterChat 5.2.15 installed in +each rig's plugins folder. The steps that need no person: + +1. **Nothing optional installed.** BetterChat and PopupNotifications are unloaded on both rigs. + - The module runs. Teams sync. `titles.set` is answered with `betterChat: false`. + - A permission sync reports `chat.loaded: false` and still syncs. + - `rust.announce` with `popup` refuses with the sentence, and with `chat` it says the line. + - **This is the row's criterion.** +2. **Titles.** Rules on one server: #1 kills, top 3 playtime. The mode is walked through all three + settings. + - `rg.titles ` prints the rendered line for players who have stats on the rig. Those + are the phase-13 walk players, known to `covalence.Players`. + - The same titles appear in the API and on the web, and later on the phone. + - Reloading BetterChat, then `rg.titles` again, proves the re-registration. +3. **A style.** + - A site group gets a title and colours. After the sync, `chat group list` / `API_GetGroupFields` + show them. + - `oxide.reload BetterChat` leaves them in place, which proves D143. + - A hand edit (`chat group set TitleColor #ff0000`) becomes a drift row with its + `detail`. Adopting it and revoking it each converge. +4. **A delete.** The styled group is deleted on the site. After the sync, BetterChat has no such + group, and `oxide.reload BetterChat` does not bring the Oxide group back (D139). +5. **The voice and delivery.** + - With the voice set, `rust.announce` (`chat`) and a news post each say a line whose rich text + the server console log shows. + - With PopupNotifications loaded, `popup` is answered `said`, and the news leg honours a server + set to `popup`. +6. **Android**, on the emulator: leaderboard chips, and nothing against a module without the field. + +**The steps that need a person** are added to `PLAYER_WALK.md`: seeing the title and the voice in a +real chat line, and a popup on a real screen. + +### 33.4 Readings nobody was asked about + +Each is taken as written unless review says otherwise. + +1. **A rule counts only a stat above zero.** A fresh wipe gives no titles, rather than giving #1 + kills to somebody with none. +2. **Ties follow the leaderboard's own order,** so a rule's top N is the first N rows the web shows + for that stat. The tie-break is the one D126's `/top` already uses. +3. **A title's text is at most 24 characters and its colour a `#rrggbb`.** The module strips `[`, + `]`, `<` and `>` from the text, so an operator's title cannot carry markup of its own. +4. **Titles are pushed even when BetterChat is absent.** They are small, and a server that adds + BetterChat shows them at the next chat line, not at the next sync. +5. **The voice is one fleet setting.** News and `rust.announce` share it, and it is a group's + `Title`, `TitleColor`, `TitleSize`, `MessageColor`, `MessageSize` and `ChatFormat`. `{Username}` + renders as nothing, because a server line has no sender. A group with no style is not offered. +6. **Group names go to BetterChat lowercased.** Its `chat` command lowercases, and `Find` is exact, + so a mixed-case name would be a group the command can never remove. +7. **`ChatFormat` and `ConsoleFormat` are free text** (D138 chose all twelve). Each is checked on + save for at most 128 characters and for containing `{Message}`, because a format without it + swallows every line that group's members type. + --- [uo46]: https://gitea.whitlocktech.com/RunicGateway/Module-uo/issues/46