From 61788dfb5ae17109f76200041fee8022db34ac73 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:11:19 +0000 Subject: [PATCH 1/2] ProvinceSystem: wills and estates on Profile (stage 6b) Co-Authored-By: Claude Opus 5.5 --- projects/ProvinceSystem/docs/README.md | 1 + .../docs/characters/wills-estates.md | 167 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 projects/ProvinceSystem/docs/characters/wills-estates.md diff --git a/projects/ProvinceSystem/docs/README.md b/projects/ProvinceSystem/docs/README.md index 2f28494..41f391d 100644 --- a/projects/ProvinceSystem/docs/README.md +++ b/projects/ProvinceSystem/docs/README.md @@ -14,6 +14,7 @@ This section is the product and technical reference for **ProvinceSystem**. Tech - [cosmetics/skins.md](cosmetics/skins.md) - skins upload and review - [cosmetics/drinks.md](cosmetics/drinks.md) - BreweryX drink builder - [characters/creator.md](characters/creator.md) - web character creator + - [characters/wills-estates.md](characters/wills-estates.md) - wills on Profile, memorials and estates - [identity/tfmcweb.md](identity/tfmcweb.md) - Discord link, tokens, gate - [integrations/patreon.md](integrations/patreon.md) - Patreon supporter linking, entitlement and operations - [identity/auth-security.md](identity/auth-security.md) - Discord sign-in, feature codes and staff access diff --git a/projects/ProvinceSystem/docs/characters/wills-estates.md b/projects/ProvinceSystem/docs/characters/wills-estates.md new file mode 100644 index 0000000..f505c72 --- /dev/null +++ b/projects/ProvinceSystem/docs/characters/wills-estates.md @@ -0,0 +1,167 @@ +# Wills and estates on the website + +Profile shows each living character's will and lets its owner change it. Dead +and missing characters open a read-only memorial with the death date, the will +heir and where the estate went. RPCharacters owns wills and estates; the site +mirrors what it pushes and sends will changes back through a pull-and-ack +queue, like web creation ([creator.md](creator.md)). + +**Repos:** `ProvinceSystem` · `rpcharacters/` · `tfmcweb/` (gateway allow-list) + +## Profile + +- **Living cards** carry a strip under the card: "Heir: Kira" or "No will", + with *Change* and *Revoke*. *Change* opens a name search; picking a result + sends the change. Until RPCharacters answers, the strip shows "Pending: …" + in place of the buttons. A refused change shows why for seven days. +- **Heir search** lists living characters of the player's own realm whose name + contains the text: three letters or more, ten results, twenty searches a + minute per player. Each result shows name, race, the first eight characters + of its id, and "yours" for the player's own characters. Account names never + appear, and nothing shows who names whom. +- **Dead and missing cards** open `/profile?tab=characters&character=`, + which shows the death date ("Before records began" for legacy deaths with no + date), the heir captured at death, the estate's state, each part's recipient, + the money moved and what each part of the server moved. +- **Wardrobe and kits** refuse dead and missing characters: the pages say so and + the backend answers `403 Character is not alive` to every edit. The face and + figure pictures still load, for the cards and the memorial. + +## Data + +| Table | Key | Written by | +|---|---|---| +| `character_roster` (new columns `died_at`, `death_id`, `will_heir_id`, `will_heir_name`, `will_revision`) | `(player_uuid, realm_id, character_id)` | roster push; an applied will ack | +| `character_estates` | `(realm_id, death_id)` | roster push only; never deleted by a push | +| `will_requests` | `id` | Profile (`pending`), plugin ack (`acked`), a newer request (`replaced`) | + +The columns arrive through `_upgrade()` in `backend/src/skins/db.py`; the tables +are in `backend/src/skins/schema.sql`. Code: +[`characters/estates.py`](https://github.com/TF-Minecraft/ProvinceSystem/blob/main/backend/src/characters/estates.py), +[`characters/wills.py`](https://github.com/TF-Minecraft/ProvinceSystem/blob/main/backend/src/characters/wills.py). + +Estates are keyed by realm as well as death, because legacy death ids are +derived from the character id and Dev holds copies of Main's characters. + +## Roster push + +`PUT /characters/plugin/roster` accepts these optional keys on each character. +Times from the plugin are epoch milliseconds; the site returns ISO-8601 UTC. + +```json +{ + "died_at": 1760356800000, + "death_id": "uuid", + "will_heir_id": "uuid or null", + "will_heir_name": "Kira", + "will_revision": 3, + "estate": { + "death_id": "uuid", + "status": "OPEN | HELD | CANCELLED | DECIDED | REVERTED", + "due_at": 1760443200000, + "decided_at": 1760443260000, + "revision": 0, + "updated_at": 1760443260000, + "heir_name": "Kira", + "recipients": { + "money": {"kind": "CHARACTER", "name": "Kira"}, + "property": {"kind": "WAITING", "name": null}, + "guild_locks": {"kind": "CHARACTER", "name": "Guruth"}, + "letters": {"kind": "SENDER", "name": null} + }, + "money_moved_cents": 34025, + "participants": { + "thievery": {"outcome": "DONE", "revision": 0, "counts": {"locks": 297}} + } + } +} +``` + +- `died_at` and `death_id` are stored when either key is present; null clears + them. A push without them leaves the stored values. +- The will is written only when `will_revision` is present and not lower than + the stored revision; an equal revision replaces the names. +- An estate with a lower `revision` than the stored one is ignored. With the + same revision, it replaces the stored one only when its `updated_at` is not + older (an estate without `updated_at` cannot replace one that has it). An + estate already stored for another character is left alone. A bad estate + (unknown status, no death id, no revision) is dropped; the rest of the push + still applies. +- `estate.death_id` defaults to the character's `death_id`. Profile shows the + estate of the character's current death; with no `death_id`, its latest one. +- Recipient kinds `FROZEN`, `WAITING`, `SENDER` and `NONE` show as "Held by + staff", "Waiting for a guild leader", "Returned to senders" and "Nothing"; + others show their `name`. Counts are summed by label across participants and + shown as "297 locks, 1 vehicle", so labels are plural nouns players know. + +## Will requests + +### Profile + +`POST /characters/{character_id}/will` with a Profile session: + +```json +{"heir_character_id": "uuid or null", "base_revision": 3} +``` + +`null` revokes. `base_revision` is the `will.revision` the page showed. The +answer is `{"ok": true, "request": {…}}`, or: + +| Status | Detail | When | +|---|---|---| +| 403 | `Character not found` | not the caller's character in the session's realm | +| 409 | `character_not_alive` | the character is not ALIVE | +| 409 | `will_changed` | `base_revision` differs from the site's stored revision | +| 409 | `request_pending` | a request is already waiting and is under ten minutes old | +| 400 | `invalid_heir` | heir unknown in the realm, not ALIVE, or the testator | + +A pending request older than ten minutes is marked `replaced` by a new one, so +an old plugin that never pulls cannot block the will for good. + +`GET /characters/heirs?q=&character_id=` returns +`{"results": [{"id", "short_id", "name", "race", "own"}]}`; 400 under three +letters, 429 over the rate limit. + +`GET /characters` and `GET /profile` add, per roster character, `died_at` and +`death_id`; for ALIVE characters `will {heir_id, heir_name, revision}` and +`will_request` (the pending request, or the last refused one within seven +days); for DEAD and MISSING characters `estate` (or null). + +### Plugin + +Both routes use `X-Plugin-Key` and require a realm. Neither defaults to `main`. + +`GET /characters/plugin/wills/pending?realm_id=` → oldest first: + +```json +{"requests": [{"id": "uuid", "realm_id": "main", "character_id": "uuid", + "account_uuid": "uuid", "heir_character_id": "uuid or null", + "base_revision": 3, "created_at": "2026-10-13T12:00:00Z"}]} +``` + +`POST /characters/plugin/wills/applied`: + +```json +{"realm_id": "main", + "results": [{"id": "uuid", "result": "applied", "will_revision": 4, "heir_name": "Kira"}]} +``` + +`result` is one of `applied`, `stale`, `invalid heir`, `invalid`, +`character dead`. With `applied`, an optional `will_revision` updates the +roster row at once under the revision rule; the roster push follows. The +answer lists `acked` ids, `already` ids (answered or replaced before; a +replayed ack lands here and changes nothing) and `refused` entries with reason +`realm` (the request belongs to another realm), `result` or `unknown`. + +TFMCWeb's gateway adds `realm_id` to both routes as a second guard. + +## Tests + +`backend/src/characters/test_wills_estates.py` covers the push rules (revision +order, estates kept when a character leaves the push, realms kept apart), the +Profile and plugin routes (ownership, realm required and checked, replays, +replacement of stale pending requests), the heir search limits and the +wardrobe and kit refusals. Frontend tests: +`app/components/character/CharacterWill.test.tsx`, +`lib/characters/wills.test.ts`, and the memorial case in +`app/components/profile/ProfilePanel.test.tsx`. From 09d5e03a48f959d14f9dfb7e9e69add5132d7c40 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:42:30 +0000 Subject: [PATCH 2/2] ProvinceSystem: estate push rules and wardrobe read refusal from review Co-Authored-By: Claude Opus 5.5 --- .../docs/characters/wills-estates.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/projects/ProvinceSystem/docs/characters/wills-estates.md b/projects/ProvinceSystem/docs/characters/wills-estates.md index f505c72..06f315b 100644 --- a/projects/ProvinceSystem/docs/characters/wills-estates.md +++ b/projects/ProvinceSystem/docs/characters/wills-estates.md @@ -24,7 +24,9 @@ queue, like web creation ([creator.md](creator.md)). date), the heir captured at death, the estate's state, each part's recipient, the money moved and what each part of the server moved. - **Wardrobe and kits** refuse dead and missing characters: the pages say so and - the backend answers `403 Character is not alive` to every edit. The face and + the backend answers `403 Character is not alive` to every edit. The wardrobe + read (`GET /characters/{id}/wardrobe`) and the kit list refuse them too, + because the wardrobe read also trims slots to the rank's limit. The face and figure pictures still load, for the cards and the memorial. ## Data @@ -84,15 +86,19 @@ Times from the plugin are epoch milliseconds; the site returns ISO-8601 UTC. - An estate with a lower `revision` than the stored one is ignored. With the same revision, it replaces the stored one only when its `updated_at` is not older (an estate without `updated_at` cannot replace one that has it). An - estate already stored for another character is left alone. A bad estate - (unknown status, no death id, no revision) is dropped; the rest of the push - still applies. + estate already stored for another character is left alone. `updated_at` is + compared to the millisecond. +- Every estate push must carry `status`, `revision` and a death id (its own + `death_id` or the character's); without them the estate is dropped and the + rest of the push still applies. Send `updated_at` every time too. +- Negative counts and money read as zero. - `estate.death_id` defaults to the character's `death_id`. Profile shows the estate of the character's current death; with no `death_id`, its latest one. - Recipient kinds `FROZEN`, `WAITING`, `SENDER` and `NONE` show as "Held by staff", "Waiting for a guild leader", "Returned to senders" and "Nothing"; others show their `name`. Counts are summed by label across participants and - shown as "297 locks, 1 vehicle", so labels are plural nouns players know. + shown as "297 locks, 1 entity", so labels are plural nouns players know. + Money is shown only from `money_moved_cents`. ## Will requests @@ -148,7 +154,8 @@ Both routes use `X-Plugin-Key` and require a realm. Neither defaults to `main`. `result` is one of `applied`, `stale`, `invalid heir`, `invalid`, `character dead`. With `applied`, an optional `will_revision` updates the -roster row at once under the revision rule; the roster push follows. The +roster row at once when it is exactly `base_revision + 1` (and not older than +the stored will); any other value waits for the roster push that follows. The answer lists `acked` ids, `already` ids (answered or replaced before; a replayed ack lands here and changes nothing) and `refused` entries with reason `realm` (the request belongs to another realm), `result` or `unknown`.