From 391a3dfa4624223c3778dcf4ab51c6e90ad6b86a Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:49:50 +0000 Subject: [PATCH 1/5] docs(thievery): document character lock owners, guard mode and the lock migration Co-Authored-By: Claude Opus 5.5 --- projects/Thievery/README.md | 1 + projects/Thievery/docs/LOCK-OWNERS.md | 186 ++++++++++++++++++++++++++ projects/Thievery/docs/SYSTEM.md | 34 ++--- projects/Thievery/docs/TEST_MATRIX.md | 8 ++ projects/Thievery/docs/testing.md | 14 +- 5 files changed, 224 insertions(+), 19 deletions(-) create mode 100644 projects/Thievery/docs/LOCK-OWNERS.md diff --git a/projects/Thievery/README.md b/projects/Thievery/README.md index d6215e2..396126d 100644 --- a/projects/Thievery/README.md +++ b/projects/Thievery/README.md @@ -7,6 +7,7 @@ Technical documentation is maintained here. Run commands from the source checkou TFMC runs Minecraft **1.21.10**. See the [shared platform and build baseline](../../PLATFORM.md) for runtime, build and validation conventions. - [Locks and theft systems](docs/SYSTEM.md) +- [Lock owners, guard mode and the lock migration](docs/LOCK-OWNERS.md) - [Automated tests and coverage](docs/testing.md) - [Manual test matrix](docs/TEST_MATRIX.md) diff --git a/projects/Thievery/docs/LOCK-OWNERS.md b/projects/Thievery/docs/LOCK-OWNERS.md new file mode 100644 index 0000000..da5c562 --- /dev/null +++ b/projects/Thievery/docs/LOCK-OWNERS.md @@ -0,0 +1,186 @@ +# Lock owners + +Every Thievery lock belongs to a character. Chests, barrels and other containers, +locked doors, armour stands, item frames and lockable furniture record the owning +character's ID, and only that character, played in a current session, counts as the +owner. Another character on the same account is a stranger: it cannot open a private +lock, rotate the lock state, clear clues or merge a chest onto it. Staff with +`thievery.admin` keep their override. + +## Who owns a lock + +Placing a container, display or lockable furniture piece gives it to the character +you are playing, with a PUBLIC lock. Opening an unowned container claims it for that +character, and a sneak toggle claims an unowned display. Locking a door with a +master key records the character as the door's owner; the key stays the credential +for opening, unlocking and breaking the door. + +Without a current character session, placing still works but leaves the block or +display unowned and writes no lock file, while claims, rotations and door locking +are refused with "Play a character to claim or change locks." Staff without a +current character are the exception for placement and door locking: the lock is +stamped as a **server** lock, starts PRIVATE and opens only through the staff +override. Server hoppers may move items between server containers, so staff +automation keeps working. Server locks cannot be picked. + +## Access + +PUBLIC and unowned locks are open to everyone. A PRIVATE lock opens for the owning +character only. A GUILD lock opens for characters in the guild the owner's locks +follow, compared by guild incarnation, so a disbanded and recreated guild with the +same name does not inherit access. A FACTION lock opens for characters whose guild +is in the same faction as that guild. + +The guild a lock follows comes from SimpleFactions' `lockGuild`: the owner's current +guild while they live, and the guild they belonged to when they died afterwards. A +dead owner's GUILD and FACTION locks therefore keep working for that guild. A +revived character lost their guild at death, so their guild has no access until they +rejoin. When SimpleFactions is not ready, or is older than 4.1.0 and has no +`lockGuild`, GUILD and FACTION locks deny everyone except staff. + +Both halves of a double chest are read together. Opening fills an unowned half from +its owned partner, and a wholly unowned double chest is claimed for the opener. When +the halves have two different owners, nothing is synced: opening still needs access +to both halves, nobody can rotate the chest (staff included), thieves cannot pick it, +and staff see it in the quarantine list until they set one owner with +`/thievery locks setowner`. + +## Automation + +A block hopper moves items only to and from containers with exactly its own owner, +checking both halves of a double chest. An unowned half goes with its owned +partner. Hoppers on another character's chest stop, including an alt's hopper under +its main's chest. + +A hopper minecart has no owner. Its moves are cancelled whenever the source or +destination is an owned container that is not PUBLIC, including either half of a +double chest. Owners cannot drain their own private storage with a hopper minecart; +use a block hopper, or make the chest PUBLIC. A block hopper that pushes into an +entity container, such as a chest minecart, is unaffected. + +## Picking and clues + +Thieves pick character locks as before. With `lockpicking.require-owner-online: true`, +a lock needs a member of its lock guild online, or, without a guild, the owning +character in play, or an open lockpick window. The messages never name the owner or +the guild: "Cannot lockpick - this lock's owners are away." A dead owner with no +lock guild is never online, so their lock can only be picked while a window is open. +Server locks, quarantined locks and unreadable records cannot be picked. + +Clue bookkeeping keys a lock's victim as `guild:`, otherwise +`player:`, and a pickpocket's victim by the character they are playing. +Clue keys written before character locks age out. + +Clue clearing on a PUBLIC or unowned container is open to anyone. Any other container +needs the owning character. On a door, the owning character or a member of its lock +guild may clear clues. + +## Staff commands + +Both commands need `thievery.admin` and are written to the server log and to +`plugins/Thievery/logs/locks-audit.log`. + +`/thievery locks quarantine` shows whether locks are running or frozen (and why), +then the quarantined records with a count for each stored owner, unreadable records, +files missing after a migration replay, and double chests with two owners. The full +list is in `plugins/Thievery/migrations/quarantine.json`. + +`/thievery locks setowner ` sets the owner of the container +(both halves of a double chest), locked door or lockable armour stand or item frame +you look at within 5 blocks. It keeps the lock state, except that an unreadable +record becomes PRIVATE. A UUID must be a known character ID, dead characters +included. A name must match exactly one living character; names need TLibs 2.5.0, +and without it use the UUID. A door whose record has lost its key cannot be +repaired this way: break it in Creative to remove the lock. + +## Storage + +Lock files keep the owner as a bare UUID and add `ownerKind`, which is `CHARACTER` +or `SERVER` (stored as the nil UUID): + +| Store | Path | Owner fields | +|---|---|---| +| Containers | `plugins/Thievery/data/_/__.json` | `owner`, `ownerKind` | +| Doors | `plugins/Thievery/door-data/_/__.json` | `ownerUUID`, `ownerKind` | +| Entities | `plugins/Thievery/entities/.json` | `owner`, `ownerKind` | +| Furniture | InteractibleFurniture variables | `thievery.owner`, `thievery.ownerKind` | + +Every write goes to a temporary file in the same directory and is then moved into +place atomically. A record is written only when it changed; owner, kind and state +changes are forced to disk, while thief cooldown changes in `accessMap` are not. +Thief cooldowns stay keyed by account. + +A record that cannot be read loads locked, with an unresolved owner: an empty, +truncated or `null` file, a bad UUID, an owned record with a missing or unknown lock +state, an unknown `ownerKind`, or garbage furniture variables. Such a lock denies +everyone except staff, cannot be picked, is never overwritten by ordinary play, and +is listed for staff. An error inside a lock listener cancels the event. + +A bare UUID without `ownerKind` is classified once whenever its record is loaded: a +known character ID is a character, an account with a reviewed heir is a legacy +owner, the nil UUID is the server, and anything else is unresolved. Legacy and +unresolved owners deny everyone except staff on non-PUBLIC locks and cannot be +picked. A classification made while character identity is unavailable is never +written back. + +Furniture is upgraded on first interaction instead of at startup: a bare character ID +gains its marker and a legacy account moves to its reviewed heir. Furniture values +written by older jars stay readable until stage 7. + +## Startup, guard mode and the migration + +Thievery registers its listeners in guard mode at the start of `onEnable`. In guard +mode the lock stores are frozen: the container, door and entity data managers save +nothing, and deletions (for example an item frame knocked off its wall) wait in a +queue. Players see "Locks are starting up. Try again shortly." when they try to +place or break a lockable, rotate or claim a lock, pick a lock, lock or unlock a +door, or clear clues. Owned, non-PUBLIC containers and displays stay shut to +players, and staff see a read-only copy of a container. PUBLIC and unowned +containers open normally without being claimed. Key holders can still open locked +doors. Block hoppers and hopper minecarts touching an owned container stop. +`resetcooldowns`, `setowner` and `testpick` on a container are refused. + +At the end of `onEnable` Thievery checks its gates in order. TLibs must offer the +reviewed-heir lookups (2.4.0 or later). Every store's `data-version` must be one this +jar understands. Character heirs must be ready, which is normal because RPCharacters +is a hard dependency and registers first. If any gate fails, locks stay in guard mode +until the server restarts, and the log says why with a SEVERE line. If everything +passes, the migration runs, the owner cache is cleared, queued deletions are applied +and locks switch to normal operation. + +The migration runs on the main thread before anyone can join. It scans every +container, door and entity file, classifies each distinct bare owner once and +rewrites the files it can settle: character IDs gain `ownerKind: CHARACTER`, the nil +UUID gains `SERVER`, and an account with a reviewed heir moves to that heir character. +Accounts without a heir, and values that cannot be classified yet, stay as they are +and go on the quarantine list, which every boot retries. A bare value written later, +for example by an older jar, goes through the same journalled pass on the next boot. +The log line `Lock migration: … in N ms` records the counts and duration. + +Each pass that rewrites files first archives every lock file it scanned to +`plugins/Thievery/migrations/backup/locks-NNNN.zip`, then writes +`migrations/journal-NNNN.json` with each file's original hash and the hash of the +exact bytes it will write. It rewrites the files by atomic move, forces every +directory it wrote into, marks the journal completed and writes `data-version` into +each store. If the server stops during the pass, or the pass throws (locks then stay +frozen until a restart), the next boot replays the unfinished journal: files already +at their new hash are done, files at their original hash are rewritten, a torn file +is taken from the archive and rewritten, and a missing file is listed for staff and +never restored. A journal is never archived twice, and a completed journal is never +replayed. + +## Rolling back + +Stop the server and roll back the whole stage 6a set together: + +1. For each `journal-NNNN.json`, newest first, restore a file from its archive only + if the file's current SHA-256 equals the entry's `newHash`. List changed and + missing files and leave them alone. +2. Delete the journals, `data-version` in `data/`, `door-data/` and `entities/`, and + `migrations/quarantine.json`. +3. Put back the previous jars. + +Under the old jars, lock files left unrestored, furniture upgraded since the +migration and server locks hold values the old code cannot match, so they deny +everyone except staff. That is expected and fails closed. Running an older Thievery +on migrated stores without this procedure is unsupported. diff --git a/projects/Thievery/docs/SYSTEM.md b/projects/Thievery/docs/SYSTEM.md index ccb0f4f..c575662 100644 --- a/projects/Thievery/docs/SYSTEM.md +++ b/projects/Thievery/docs/SYSTEM.md @@ -31,10 +31,11 @@ expire, with a message to the other player. An accepted robbery ends through its own quit handling. Pouch transfers still require both captured sessions to be current; switching characters never redirects the transfer to another pouch. -The build pins TLibs **2.4.0**, RPCharacters **2.16.0**, SimpleFactions **4.0.0** +The build pins TLibs **2.4.0**, RPCharacters **2.16.0**, SimpleFactions **4.1.0** and DenarEconomy **0.3.0**. RPCharacters remains required for clues, graves and -evil-RP rewards. Protected guild access and theft cooldown checks deny access -while factions or reviewed heirs are unavailable. +evil-RP rewards. Guild and faction locks need SimpleFactions 4.1.0 or later for +`lockGuild`; with an older SimpleFactions they deny everyone except staff. Theft +cooldown checks deny access while factions or reviewed heirs are unavailable. ## Character money and guild access @@ -52,15 +53,12 @@ before investigating a disputed payment. Startup does not issue another payment or resume a closed robbery. Pouch transfers move no physical coin items. Guild lookups use the active character. Guild cooldowns map character members -back to their account UUIDs and retain existing account-wide history. Offline -lock owner guild checks and `LockAccess` ownership checks use the account's -reviewed legacy heir; a different active character does not inherit those access -rights. Online guild checks require the relevant member character to be active. +back to their account UUIDs and retain existing account-wide history. Risk, thief +cooldowns and the container `accessMap` stay keyed by account, so switching +characters does not reset them. -Lock files, owner controls, risk and cooldown storage remain account-based until -stage 6. This includes owner toggles, shared-key and automation paths; this stage -does not claim full character isolation for physical locks. Existing guild target -keys retain their cooldown history. +Locks belong to characters: see [lock owners](LOCK-OWNERS.md) for ownership, +access, automation, staff commands, storage, guard mode and the lock migration. ## Responsibilities @@ -72,7 +70,9 @@ keys retain their cooldown history. ## Chest hopper automation -When a **block hopper** moves items (`InventoryMoveItemEvent`), allow pull/deposit only if the hopper and every involved block container (chest, barrel, hopper, etc.) share the same **non-null owner UUID**. Guild/PUBLIC player access does not apply to hoppers — only owner equality. Unowned containers block automation; hopper minecarts are not covered. Droppers and other initiators are unchanged. +When a **block hopper** moves items (`InventoryMoveItemEvent`), Thievery allows the pull or deposit only if every involved block container (chest, barrel, hopper and so on) has exactly the hopper's owner. Both halves of a double chest are checked, and an unowned half goes with its owned partner. Guild and PUBLIC access do not apply to block hoppers, and unowned hoppers move nothing. + +A **hopper minecart** has no owner. Its moves are cancelled when the source or destination is an owned container that is not PUBLIC, including either half of a double chest, so owners cannot drain their own private storage with one. Droppers and other initiators are unchanged. See [lock owners](LOCK-OWNERS.md#automation). Displays do not use the chest GUI. Doors and displays share one engine. @@ -153,9 +153,9 @@ Honor `min-lock-strength-ratio` against that display strength. ## Lock state -Reuse `LockState` (`PRIVATE` / `GUILD` / `FACTION` / `PUBLIC`) and the same access rules as `ContainerData.canAccess` (owner, same guild, same faction, public, `thievery.admin` bypass). `FACTION` is every guild in the owner's faction. Vassals are not included. Chest lockpick sessions scale budget, risk, critical chance, and break chance from `lockpicking.lock-types`. +Reuse `LockState` (`PRIVATE` / `GUILD` / `FACTION` / `PUBLIC`) and the same access rules as `ContainerData.canAccess` (owning character, the guild the owner's locks follow, that guild's faction, public, `thievery.admin` bypass). `FACTION` is every guild in that faction. Vassals are not included. Chest lockpick sessions scale budget, risk, critical chance, and break chance from `lockpicking.lock-types`. -Shift left-click is the toggle (same titles/sound as chests). The owner rotates state; staff with `thievery.admin` can also rotate a lock another player owns, and the placer stays the owner. Shift left-click **never** breaks a lockable display. +Shift left-click is the toggle (same titles/sound as chests). The owning character rotates state; staff with `thievery.admin` can also rotate a lock another character owns, and that character stays the owner. Shift left-click **never** breaks a lockable display. ### Lock change logging @@ -174,15 +174,15 @@ If the player cannot access a locked display, cancel: Owner / guild / public access still allows legitimate break and take. -Owner is set on place (`FurniturePlaceEvent`, `EntityPlaceEvent`). If missing, the first successful owner toggle may claim it. +Owner is set on place (`FurniturePlaceEvent`, `EntityPlaceEvent`) to the placer's current character, or the server for staff without one. If missing, the first toggle by a player with a current character claims it. ## Persistence (entity-capable) `ContainerDataManager` is block-file keyed. Do not reuse it for entities. -**Vanilla entities** (armor stand, item frame, glow item frame): `plugins/Thievery/entities/.json` (`owner`, `lockState`). Delete on entity remove. +**Vanilla entities** (armor stand, item frame, glow item frame): `plugins/Thievery/entities/.json` (`owner`, `ownerKind`, `lockState`). Delete on entity remove. -**IF furniture:** do not key only by `Furniture.getEntityId()`. Restore can respawn the ItemDisplay and change the UUID (`FurnitureRestoreHandler.ensureDisplay`). Store `thievery.owner` and `thievery.lockState` in `Furniture.getVariables()` and `persistFurniture`. The lock follows pickup, chunk reload, and respawn. +**IF furniture:** do not key only by `Furniture.getEntityId()`. Restore can respawn the ItemDisplay and change the UUID (`FurnitureRestoreHandler.ensureDisplay`). Store `thievery.owner`, `thievery.ownerKind` and `thievery.lockState` in `Furniture.getVariables()` and `persistFurniture`. The lock follows pickup, chunk reload, and respawn. Config: diff --git a/projects/Thievery/docs/TEST_MATRIX.md b/projects/Thievery/docs/TEST_MATRIX.md index 89a0ef8..e06043d 100644 --- a/projects/Thievery/docs/TEST_MATRIX.md +++ b/projects/Thievery/docs/TEST_MATRIX.md @@ -122,6 +122,14 @@ ItemsAdder must not grant inspection access. | L11 | Pick up and replace furniture: lock still on that piece | | L13 | Shift left-click on an owned chest, display and furniture: `/co inspect` or `/co lookup` shows ` set lock to .`; display and furniture entries sit on the supporting block | | L14 | Staff shift left-click on another player's lock changes state, keeps the owner, and logs with "(staff override)" | +| L15 | Your alt on the same account cannot open your PRIVATE chest, rotate it, clear its clues or merge a chest onto it | +| L16 | Placing a chest with no character selected leaves it unowned; staff with no character place a PRIVATE server chest only the staff override opens | +| L17 | A dead owner's GUILD chest still opens for the guild they died in; a revived owner's old guild has no access | +| L18 | A hopper minecart under an owned PRIVATE chest (either half of a double chest) takes nothing; under a PUBLIC or unowned chest it works | +| L19 | A block hopper under its owner's chest moves items; an alt's hopper under the same chest does not; server hoppers move between server chests | +| L20 | A double chest whose halves have two owners cannot be rotated or picked and appears in `/thievery locks quarantine` until `/thievery locks setowner` fixes it | +| L21 | Restart with RPCharacters failing to load: locks stay frozen ("Locks are starting up"), owned chests stay shut, key holders open doors, nothing in `plugins/Thievery/data` changes; the next good restart migrates | +| L22 | First boot on an old store: `Lock migration:` log line with counts and duration, `migrations/backup/locks-0001.zip`, a completed `journal-0001.json`, `data-version` in each store; a second boot changes nothing | ## Robbery diff --git a/projects/Thievery/docs/testing.md b/projects/Thievery/docs/testing.md index 576c6c3..4df8153 100644 --- a/projects/Thievery/docs/testing.md +++ b/projects/Thievery/docs/testing.md @@ -29,8 +29,18 @@ Pouch payment tests use real temporary SQLite files for balanced transfers, ledger reopen, a committed payment with a lost reply, insufficient funds and stale sessions. Handler tests repeat the same click, retry unavailable payments, and verify that unsavable intents consume no budget. Identity tests check -reviewed-heir ownership, another character on the same account, faction waiting, -and member-to-account cooldown mapping. +member-to-account cooldown mapping and faction waiting. + +Lock owner tests use temporary directories for real lock files. They cover the +migration (classification, exact-byte journal hashes, the archive, a second boot, +quarantine retry, stragglers, an exception mid-pass, every replay case and +`data-version`), the guard freeze in each data manager and handler, fail-closed +parsing, access for characters, alts, dead owners and server locks, double chests, +block hoppers and hopper minecarts, target keys and the staff lock commands. +`FactionFixture` gives each mocked player a character whose ID is their account +UUID; `plays(player, id)` switches it, and its `guilds` field stands in for +SimpleFactions' `lockGuild`. A JUnit extension starts every test with locks in +normal mode, no guild source and an empty quarantine list. Hands-up inspection tests exercise consent, independent item copies and live refresh, full-view click and drag cancellation, creative inventory events, From 621391953badb21b1d10e3e940217087d3bbc81b Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:25:18 +0000 Subject: [PATCH 2/5] docs(thievery): note setowner on furniture and record salvage Co-Authored-By: Claude Opus 5.5 --- projects/Thievery/docs/LOCK-OWNERS.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/projects/Thievery/docs/LOCK-OWNERS.md b/projects/Thievery/docs/LOCK-OWNERS.md index da5c562..016721b 100644 --- a/projects/Thievery/docs/LOCK-OWNERS.md +++ b/projects/Thievery/docs/LOCK-OWNERS.md @@ -16,7 +16,8 @@ master key records the character as the door's owner; the key stays the credenti for opening, unlocking and breaking the door. Without a current character session, placing still works but leaves the block or -display unowned and writes no lock file, while claims, rotations and door locking +display unowned (any stale record left at that spot by a container that was blown up +is removed), while claims, rotations and door locking are refused with "Play a character to claim or change locks." Staff without a current character are the exception for placement and door locking: the lock is stamped as a **server** lock, starts PRIVATE and opens only through the staff @@ -39,7 +40,8 @@ rejoin. When SimpleFactions is not ready, or is older than 4.1.0 and has no `lockGuild`, GUILD and FACTION locks deny everyone except staff. Both halves of a double chest are read together. Opening fills an unowned half from -its owned partner, and a wholly unowned double chest is claimed for the opener. When +its owned partner, and a wholly unowned double chest is claimed for the opener. An +unreadable half is never copied onto its partner; the chest is listed for staff. When the halves have two different owners, nothing is synced: opening still needs access to both halves, nobody can rotate the chest (staff included), thieves cannot pick it, and staff see it in the quarantine list until they set one owner with @@ -86,12 +88,14 @@ files missing after a migration replay, and double chests with two owners. The f list is in `plugins/Thievery/migrations/quarantine.json`. `/thievery locks setowner ` sets the owner of the container -(both halves of a double chest), locked door or lockable armour stand or item frame -you look at within 5 blocks. It keeps the lock state, except that an unreadable -record becomes PRIVATE. A UUID must be a known character ID, dead characters -included. A name must match exactly one living character; names need TLibs 2.5.0, -and without it use the UUID. A door whose record has lost its key cannot be -repaired this way: break it in Creative to remove the lock. +(both halves of a double chest), locked door, lockable armour stand or item frame, or +lockable furniture piece you look at within 5 blocks. It keeps the lock state, except +that an unreadable record becomes PRIVATE, and it keeps the thief cooldowns of an +unreadable container when they can still be read. A UUID must be a known character +ID, dead characters included. A name must match exactly one living character; names +need TLibs 2.5.0, and without it use the UUID. A door whose record has lost its key or +its strength cannot be repaired this way, so the lock is never weakened: break the +door in Creative to remove it. Fixed records leave the quarantine list straight away. ## Storage @@ -114,7 +118,8 @@ A record that cannot be read loads locked, with an unresolved owner: an empty, truncated or `null` file, a bad UUID, an owned record with a missing or unknown lock state, an unknown `ownerKind`, or garbage furniture variables. Such a lock denies everyone except staff, cannot be picked, is never overwritten by ordinary play, and -is listed for staff. An error inside a lock listener cancels the event. +is listed for staff. An error inside a lock listener cancels the event, including a +linkage error from a changed SimpleFactions or TLibs API. A bare UUID without `ownerKind` is classified once whenever its record is loaded: a known character ID is a character, an account with a reviewed heir is a legacy From be9ee794195a7ddba6c9bc0930b55a7089e74b96 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:28:18 +0000 Subject: [PATCH 3/5] docs(thievery): guild and faction members clear clues on locks that admit them Co-Authored-By: Claude Opus 5.5 --- projects/Thievery/docs/LOCK-OWNERS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/Thievery/docs/LOCK-OWNERS.md b/projects/Thievery/docs/LOCK-OWNERS.md index 016721b..683a525 100644 --- a/projects/Thievery/docs/LOCK-OWNERS.md +++ b/projects/Thievery/docs/LOCK-OWNERS.md @@ -4,7 +4,7 @@ Every Thievery lock belongs to a character. Chests, barrels and other containers locked doors, armour stands, item frames and lockable furniture record the owning character's ID, and only that character, played in a current session, counts as the owner. Another character on the same account is a stranger: it cannot open a private -lock, rotate the lock state, clear clues or merge a chest onto it. Staff with +lock, rotate the lock state, clear a private lock's clues or merge a chest onto it. Staff with `thievery.admin` keep their override. ## Who owns a lock @@ -73,9 +73,9 @@ Clue bookkeeping keys a lock's victim as `guild:`, otherwise `player:`, and a pickpocket's victim by the character they are playing. Clue keys written before character locks age out. -Clue clearing on a PUBLIC or unowned container is open to anyone. Any other container -needs the owning character. On a door, the owning character or a member of its lock -guild may clear clues. +Clue clearing on a PUBLIC or unowned container is open to anyone. A PRIVATE container +needs the owning character, and a GUILD or FACTION container anyone that lock lets in. +On a door, the owning character or a member of its lock guild may clear clues. ## Staff commands From bbed18f5135905b5ede7d4af5f6f6dc4b05c3d93 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:26:29 +0000 Subject: [PATCH 4/5] docs(thievery): server-owned locks become private; document the rollback tool Co-Authored-By: Claude Opus 5.5 --- projects/Thievery/docs/LOCK-OWNERS.md | 34 +++++++++++++++++++-------- projects/Thievery/docs/testing.md | 6 +++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/projects/Thievery/docs/LOCK-OWNERS.md b/projects/Thievery/docs/LOCK-OWNERS.md index 683a525..6190aee 100644 --- a/projects/Thievery/docs/LOCK-OWNERS.md +++ b/projects/Thievery/docs/LOCK-OWNERS.md @@ -90,8 +90,10 @@ list is in `plugins/Thievery/migrations/quarantine.json`. `/thievery locks setowner ` sets the owner of the container (both halves of a double chest), locked door, lockable armour stand or item frame, or lockable furniture piece you look at within 5 blocks. It keeps the lock state, except -that an unreadable record becomes PRIVATE, and it keeps the thief cooldowns of an -unreadable container when they can still be read. A UUID must be a known character +that an unreadable record becomes PRIVATE and a lock given to `server` always becomes +PRIVATE, as server placements are; the reply says so. Staff without a character cannot +enter the world when character bodies are on, so this is how they make a server lock. +The thief cooldowns of an unreadable container are kept when they can still be read. A UUID must be a known character ID, dead characters included. A name must match exactly one living character; names need TLibs 2.5.0, and without it use the UUID. A door whose record has lost its key or its strength cannot be repaired this way, so the lock is never weakened: break the @@ -176,14 +178,26 @@ replayed. ## Rolling back -Stop the server and roll back the whole stage 6a set together: - -1. For each `journal-NNNN.json`, newest first, restore a file from its archive only - if the file's current SHA-256 equals the entry's `newHash`. List changed and - missing files and leave them alone. -2. Delete the journals, `data-version` in `data/`, `door-data/` and `entities/`, and - `migrations/quarantine.json`. -3. Put back the previous jars. +Stop the server and roll back the whole stage 6a set together. Thievery's part is the +offline tool [`tools/thievery_rollback.py`](https://github.com/TF-Minecraft/Thievery/blob/main/tools/thievery_rollback.py) +in the source checkout. It needs only Python 3 and runs against the plugin folder: + +```sh +python3 tools/thievery_rollback.py /path/to/plugins/Thievery +python3 tools/thievery_rollback.py /path/to/plugins/Thievery --apply --report rollback.json +``` + +Without `--apply` it is a dry run that writes nothing. It processes every +`migrations/journal-NNNN.json` newest first and restores a file from that journal's +archive only if the file's current SHA-256 still equals the journal's `newHash` and +the archived copy matches `originalHash`. Files changed or deleted since the migration, +and archive copies that do not match, are listed and left alone. With `--apply` it then +deletes the journals, `data-version` in `data/`, `door-data/` and `entities/`, and +`migrations/quarantine.json`, so a later upgrade starts clean; the archives are kept. +If a journal or archive cannot be read it stops with exit code 2 and writes nothing. +`--report` saves the per-journal result as JSON. + +Then put back the previous jars. Under the old jars, lock files left unrestored, furniture upgraded since the migration and server locks hold values the old code cannot match, so they deny diff --git a/projects/Thievery/docs/testing.md b/projects/Thievery/docs/testing.md index 4df8153..01d7757 100644 --- a/projects/Thievery/docs/testing.md +++ b/projects/Thievery/docs/testing.md @@ -42,6 +42,12 @@ UUID; `plays(player, id)` switches it, and its `guilds` field stands in for SimpleFactions' `lockGuild`. A JUnit extension starts every test with locks in normal mode, no guild source and an empty quarantine list. +The offline rollback tool has Python tests in `tools/test_thievery_rollback.py`, run +with `python3 -m unittest discover -s tools -p 'test_*.py'` and in CI after the Maven +build. They build a fixture laid out as the migration writes it, with two journals, +and cover the dry run, newest-first restores across both passes, changed, missing and +mismatched files, marker removal, and journals or archives that cannot be trusted. + Hands-up inspection tests exercise consent, independent item copies and live refresh, full-view click and drag cancellation, creative inventory events, robbery/pickpocket event ordering, range and visibility checks, cancelled opens, From 5f35254c0760551e4b2d4d55a5c7d92e020b8e9e Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:44:18 +0000 Subject: [PATCH 5/5] docs(thievery): doors given to the server keep their key Co-Authored-By: Claude Opus 5.5 --- projects/Thievery/docs/LOCK-OWNERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/Thievery/docs/LOCK-OWNERS.md b/projects/Thievery/docs/LOCK-OWNERS.md index 6190aee..8ab5bb6 100644 --- a/projects/Thievery/docs/LOCK-OWNERS.md +++ b/projects/Thievery/docs/LOCK-OWNERS.md @@ -91,7 +91,8 @@ list is in `plugins/Thievery/migrations/quarantine.json`. (both halves of a double chest), locked door, lockable armour stand or item frame, or lockable furniture piece you look at within 5 blocks. It keeps the lock state, except that an unreadable record becomes PRIVATE and a lock given to `server` always becomes -PRIVATE, as server placements are; the reply says so. Staff without a character cannot +PRIVATE, as server placements are; the reply says so. A door keeps its key, which +still opens it after it is given to `server`. Staff without a character cannot enter the world when character bodies are on, so this is how they make a server lock. The thief cooldowns of an unreadable container are kept when they can still be read. A UUID must be a known character ID, dead characters included. A name must match exactly one living character; names