3 Commits
Author SHA1 Message Date
dmg bf7f01a39e chore(knowledge): move canonical docs to shared SoMC wiki
CI / build (push) Successful in 1m54s
Release / release (push) Successful in 2m28s
2026-09-09 23:18:25 -04:00
dmg 3bd2dc7d14 feat(quests): support requests for all inventory items
Release / release (push) Successful in 2m46s
CI / build (push) Successful in 1m19s
2026-09-09 21:50:16 -04:00
dmg de8ff83c3c feat(notifications): add login quest counts and claim reminders
Release / release (push) Successful in 2m40s
CI / build (push) Successful in 1m17s
2026-09-06 21:34:24 -04:00
34 changed files with 252 additions and 595 deletions
+5 -36
View File
@@ -1,40 +1,9 @@
# Spigot Quest Board Agent Guide
# spigot-quest-board agent entrypoint
## Project
The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
Spigot Quest Board is a Java 25 plugin for Purpur 26.2. It provides shared physical quest boards where players escrow rewards, deliver requested block items, and claim delivered or returned items.
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/spigot-quest-board/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/spigot-quest-board/` stories. Also follow the parent workspace `AGENTS.md` when present.
## Canonical knowledge
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-quest-board/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
- `knowledge/` is the canonical OKF v0.1 knowledge bundle.
- Read `knowledge/index.md` and the relevant documents in `knowledge/user-stories/` before changing behavior.
- Every feature or behavior change requires an approved user story with task-list acceptance criteria.
- Preserve unknown YAML frontmatter fields when editing knowledge documents.
- Keep `knowledge/index.md`, `knowledge/log.md`, and `knowledge/user-stories/index.md` current.
## Development
- Compile against the Purpur API version declared in `build.gradle.kts`.
- Use Java 25 and enable strict compiler linting.
- Prefer small, server-independent domain objects and services that can be unit tested.
- Use test-driven development where practical: add a failing test, implement the minimum behavior, then refactor.
- Treat escrow, completion, cancellation, expiry, and claiming as atomic state transitions. Never duplicate or silently discard player items.
- Key players by UUID, not display name.
- Persist board, quest, escrow, and claim state safely across restarts using atomic file replacement where supported.
- Use UTC and RFC 3339 timestamps for persisted or documented date-times.
- Keep player-facing command autocomplete permission-aware and context-sensitive.
- Use Purpur's supported Paper dialog API for native dialog interfaces.
## Verification
Run before completing a story:
```bash
./gradlew clean check jar
```
Mark acceptance criteria complete only after implementation and verification.
## Git
Use conventional commits in the form `type(scope): description`, with summaries under 72 characters.
Development follows [Development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
+4 -4
View File
@@ -1,12 +1,12 @@
# Spigot Quest Board
A Purpur 26.2 plugin for shared physical quest boards. Players can escrow rewards for requested block items, deliver those blocks at any quest board, and claim delivered or returned items safely.
A Purpur 26.2 plugin for shared physical quest boards. Players can escrow rewards for requested items, deliver those items at any quest board, and claim delivered or returned items safely.
The approved behavior is specified in the [OKF knowledge bundle](knowledge/index.md).
The approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-quest-board/index.md).
## Status
Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with glowing centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request a block and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested blocks, rewards, issuers, and time remaining; custom reward names are shown together with their material type. Players can complete quests at any board or with `/quests complete <quest>` by delivering the required blocks. Exact escrowed rewards are granted immediately, and delivered blocks are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel <quest>`. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet.
Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with glowing centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request any inventory item material and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested items, rewards, issuers, and time remaining; custom reward names are shown together with their material type. Players can complete quests at any board or with `/quests complete <quest>` by delivering the required items. Exact escrowed rewards are granted immediately, and delivered items are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel <quest>`. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet.
## Requirements
@@ -23,7 +23,7 @@ The plugin JAR is written to `build/libs/`.
## Player commands
Player commands are disabled by default and can be enabled persistently by an administrator. When disabled, players within five blocks of a registered board can still use `/quests create <block> <quantity>` with material and quantity autocomplete; other player command forms remain disabled.
Player commands are disabled by default and can be enabled persistently by an administrator. When disabled, players within five blocks of a registered board can still use `/quests create <item> <quantity>` with material and quantity autocomplete; other player command forms remain disabled.
```text
/quests
-15
View File
@@ -1,15 +0,0 @@
---
type: Index
title: Spigot Quest Board Knowledge
description: Entry point for the Spigot Quest Board OKF knowledge bundle.
okf_version: "0.1"
---
# Spigot Quest Board Knowledge
This bundle specifies shared physical quest boards, reward escrow, block deliveries, item claims, player commands, administration, persistence, and plugin delivery requirements.
## Explore
- [User stories](user-stories/index.md)
- [Knowledge log](log.md)
-121
View File
@@ -1,121 +0,0 @@
---
type: Log
title: Spigot Quest Board Knowledge Log
description: Chronological record of material decisions affecting Spigot Quest Board.
---
# Spigot Quest Board Knowledge Log
## 2026-09-05 — Initial quest-board design
- Administrators create physical quest boards, and every board exposes the same global quests and claims.
- Players create quests by selecting a requested block material and quantity and escrowing reward items from their inventory.
- Completing players deliver the requested block items at a board and immediately receive the escrowed reward.
- Delivered blocks remain held by the quest system until the issuer claims them at a board.
- Cancelled and seven-day-expired quests return escrowed rewards through the same claim system rather than direct inventory delivery.
- Inventory overflow from rewards or claims is dropped at the receiving player's feet.
- Player `/quests` commands are disabled by default so administrators can require physical-board interaction.
- The board interface follows Spigot Base by using Purpur's supported native dialog API.
## 2026-09-05 — Foundation started
- Approved work begins with an OKF knowledge bundle, Java 25/Purpur 26.2 Gradle foundation, automated verification, and a public Gitea repository.
## 2026-09-05 — Foundation completed
- Published the project to the public `dmg/spigot-quest-board` Gitea repository.
- Added strict Java 25/Purpur 26.2 Gradle builds, plugin metadata, a metadata regression test, and CI/release workflows modeled on Spigot Base.
- Verified the plugin foundation and JAR with `./gradlew clean check jar`.
## 2026-09-05 — Shared physical quest boards
- Added durable world-UUID and block-coordinate board registration through `/questadmin createboard`.
- Registered main-hand interactions open one shared native Purpur dialog while unregistered blocks remain untouched.
- Persistence publishes a board only after an atomic YAML save succeeds and rejects duplicate locations.
- Verified 11 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Quest creation and reward escrow
- Added board-dialog and `/quests create <block> <quantity>` creation using a valid block material, positive quantity, and the exact held reward stack.
- Escrow preserves full Bukkit item metadata in durable Base64-backed YAML and rolls inventory removal back if persistence fails.
- Quests receive UUID identifiers and UTC creation and seven-day expiration timestamps.
- Verified 26 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Shared quest browsing
- Added globally shared active-quest listings to every board and to `/quests` and `/quests list`.
- Listings include requested blocks, exact reward summaries, issuer names, and boundary-safe remaining durations.
- Added persisted lifecycle status with backward-compatible active defaults and filtered stale quest IDs from contextual completion and cancellation suggestions.
- Verified 37 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Block delivery and quest completion
- Added board actions and `/quests complete <quest>` for exact requested-material delivery from player inventories.
- Completion serializes state transitions, holds delivered blocks in durable issuer claims, and grants exact escrowed rewards with owner-protected overflow drops.
- Added durable online and next-login issuer notifications and rollback before persistence succeeds.
- Verified 49 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Issuer quest cancellation
- Added issuer-only board actions and `/quests cancel <quest>` with ownership-filtered autocomplete.
- Cancellation atomically moves exact reward escrow into an issuer claim without directly changing inventory.
- Serialized cancellation against completion and retained active state when persistence fails.
- Verified 58 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Quest expiration and held-item claims
- Added boundary-safe scheduled expiration that moves exact rewards into durable issuer claims.
- Added claim collection at every board and through `/quests claim`, distinguishing delivered blocks from cancelled or expired reward returns.
- Inventory overflow drops at the claimant's feet with ownership protection, and failed claim acknowledgement rolls inventory and drops back before retry.
- Added persisted online and next-login expiry notifications.
- Verified 73 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Administrative player-command control
- Disabled all player `/quests` command forms and autocomplete by default while preserving equivalent physical-board actions.
- Added persistent `/questadmin commands enable|disable` control with permission-aware autocomplete and failure-safe updates.
- Kept administrative board creation available independently of the player-command setting.
- Verified 82 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Screen-fitting quest-board dialogs
- Replaced the clipped 800-pixel combined board screen with a compact 420-pixel dashboard.
- Split browsing, creation, and claims into dedicated native dialog screens with Back navigation.
- Presented active quests as individual detail dialogs while retaining completion and issuer cancellation actions.
- Verified 88 tests, including a 250-quest navigation case, and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Generated physical quest boards
- Added `/questadmin createboard physical` to generate a five-wide, four-tall board above the targeted ground anchor facing the administrator.
- Used oak-log pillars, an oak-plank face, and three oak wall signs with obfuscated decorative text.
- Registered all visible planks and signs in one durable operation and added obstruction checks plus block rollback when construction or persistence fails.
- Preserved existing single-block board creation and persisted board compatibility.
- Verified 103 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Readable physical-board signage
- Added centered `Quest Board` and right-click instruction signs on the two layers above each generated board's three decorative signs.
- Registered all five signs and the plank face as shared-board interaction locations.
- Added exact legacy-structure detection and failure-safe startup upgrades for previously generated three-sign boards without touching custom or altered structures.
- Verified 111 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Glowing readable board signs
- Enabled glowing front-side text on generated title and instruction signs while retaining non-glowing decorative signs.
- Added exact full-structure detection to refresh existing generated boards once without modifying custom or altered signs.
- Added two-sign snapshot rollback when a refresh cannot complete safely.
- Verified 116 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Nearby command-based quest creation
- Allowed `/quests create <block> <quantity>` and its material and quantity autocomplete within five blocks of any registered board location even while global player commands are disabled.
- Kept listing, completion, cancellation, and claiming commands disabled under that setting while preserving global command behavior when enabled.
- Enforced same-world Euclidean proximity with an inclusive five-block boundary for custom and generated boards.
- Verified 121 tests and the plugin JAR with `./gradlew clean check jar`.
## 2026-09-05 — Named reward identification
- Displayed custom reward names together with their material type in shared board details and command listings, such as `1 × Shopping List (PAPER)`.
- Stored safe plain-text custom names alongside unchanged exact item metadata and inferred names from valid historical escrow data when possible.
- Kept malformed or unavailable metadata from blocking quest browsing by falling back to material descriptions.
- Verified 128 tests and the plugin JAR with `./gradlew clean check jar`.
-21
View File
@@ -1,21 +0,0 @@
---
type: Index
title: Spigot Quest Board User Stories
description: Catalog of user stories for the Spigot Quest Board plugin.
---
# Spigot Quest Board User Stories
1. [US-001: Build and release the plugin](us-001-build-and-release-plugin.md)
2. [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
3. [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
4. [US-004: Browse available quests](us-004-browse-available-quests.md)
5. [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md)
6. [US-006: Cancel an owned quest](us-006-cancel-an-owned-quest.md)
7. [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
8. [US-008: Control player quest commands](us-008-control-player-quest-commands.md)
9. [US-009: Use a screen-fitting quest-board interface](us-009-use-a-screen-fitting-quest-board-interface.md)
10. [US-010: Generate a physical quest-board structure](us-010-generate-a-physical-quest-board.md)
11. [US-011: Add readable physical-board signage](us-011-add-readable-physical-board-signage.md)
12. [US-012: Create quests by command near a board](us-012-create-quests-near-a-board.md)
13. [US-013: Show custom reward item names](us-013-show-custom-reward-item-names.md)
@@ -1,28 +0,0 @@
---
type: User Story
title: "US-001: Build and release the plugin"
description: Give maintainers repeatable Purpur builds, automated verification, and versioned public Gitea releases.
status: done
---
# US-001: Build and release the plugin
As a **plugin maintainer**, I want automated builds and releases modeled on Spigot Base so that tested, correctly versioned artifacts can be distributed consistently.
## Acceptance criteria
- [x] The Gradle project compiles against Purpur API `26.2.build.2618-stable` using a Java 25 toolchain.
- [x] Compiler lint warnings fail the build.
- [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
- [x] Plugin metadata declares the entrypoint, player and admin commands, and permissions.
- [x] Pushes and pull requests build and test the plugin in Gitea Actions.
- [x] Pull requests validate conventional commit messages.
- [x] CI stores a development JAR as a workflow artifact.
- [x] Main-branch conventional commits drive semantic versioning.
- [x] A successful release builds a versioned JAR and attaches it to the corresponding Gitea release.
- [x] The project is published in the public `dmg/spigot-quest-board` Gitea repository.
- [x] Build files, Gradle wrapper, workflows, and release behavior follow `../spigot-base/` where applicable while using Spigot Quest Board names and identifiers.
## Related
- [User-story catalog](index.md)
@@ -1,27 +0,0 @@
---
type: User Story
title: "US-002: Create and use shared quest boards"
description: Let administrators establish persistent physical boards that expose one shared quest system.
status: done
---
# US-002: Create and use shared quest boards
As an **administrator**, I want to turn a targeted block into a quest board so that players can interact with quests at controlled physical locations.
## Acceptance criteria
- [x] An authorized administrator can use `/questadmin createboard` to register the block they are targeting.
- [x] Creation rejects a missing, invalid, or already registered target without changing state.
- [x] Registered boards persist across server restarts with their world and block coordinates.
- [x] Interacting with any registered board opens the quest-board interface.
- [x] Every registered board exposes the same global quests and claimable items.
- [x] The interface uses Purpur's supported native dialog API and follows the interaction style of Spigot Base.
- [x] Ordinary block interaction is not intercepted at unregistered locations.
- [x] Administrative actions require the `spigotquestboard.admin` permission, granted to server operators by default.
- [x] Automated tests verify board registration, persistence, shared visibility, authorization, and interaction routing.
## Related
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-004: Browse available quests](us-004-browse-available-quests.md)
@@ -1,28 +0,0 @@
---
type: User Story
title: "US-003: Create a block-delivery quest"
description: Let a player request block items while safely escrowing an inventory-funded reward.
status: done
---
# US-003: Create a block-delivery quest
As a **player**, I want to request a block material and quantity in exchange for items I already own so that another player can fulfill my request safely.
## Acceptance criteria
- [x] A player can create a quest through a registered quest board by selecting a valid block material, a positive quantity, and reward items from their inventory.
- [x] Reward items must exist in the issuer's inventory and are removed only when quest creation succeeds.
- [x] Removed rewards are held durably in escrow by the quest system.
- [x] A created quest records a unique identifier, issuer UUID and current name, requested material and quantity, exact reward items, creation time, and expiration time.
- [x] Each quest expires exactly seven days after successful creation.
- [x] Invalid input, insufficient reward items, or persistence failure leaves the issuer's inventory unchanged and creates no quest.
- [x] When player commands are enabled, `/quests create ...` provides equivalent validated creation behavior with contextual autocomplete.
- [x] Quest state and item metadata survive server restarts without duplication or loss.
- [x] Automated tests verify validation, escrow, rollback, timestamps, command routing, and persistence.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md)
- [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
@@ -1,26 +0,0 @@
---
type: User Story
title: "US-004: Browse available quests"
description: Let players inspect globally available quests and their essential terms.
status: done
---
# US-004: Browse available quests
As a **player**, I want to browse current quests so that I can decide which block deliveries to fulfill.
## Acceptance criteria
- [x] Every registered board lists the same active quests.
- [x] Each listing shows the requested block material, quantity, reward, issuer, and time remaining.
- [x] Remaining durations are clear and never display an expired quest as active.
- [x] Completed, cancelled, and expired quests are excluded from the active listing.
- [x] When player commands are enabled, `/quests` and `/quests list` provide the same active quest information in chat.
- [x] Listing and autocomplete do not expose stale quest identifiers as completable or cancellable.
- [x] Automated tests verify filtering, displayed fields, duration boundaries, and command aliases.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md)
- [US-013: Show custom reward item names](us-013-show-custom-reward-item-names.md)
@@ -1,29 +0,0 @@
---
type: User Story
title: "US-005: Deliver blocks and complete a quest"
description: Let a player deliver requested blocks at a quest board and receive the escrowed reward.
status: done
---
# US-005: Deliver blocks and complete a quest
As a **player**, I want to deliver requested blocks to a quest board so that I receive the promised reward and the issuer can claim my delivery.
## Acceptance criteria
- [x] An active quest offers a Complete Quest action through every registered board.
- [x] Completion requires the delivering player to possess the full requested quantity of the exact block material.
- [x] Successful completion removes the requested blocks from the delivering player's inventory and stores them as a claim for the issuer.
- [x] Successful completion gives the exact escrowed reward items to the delivering player.
- [x] Reward items that do not fit are dropped safely at the delivering player's feet with a clear message.
- [x] The issuer is notified immediately when online or on their next login when offline that delivered blocks can be claimed at a quest board.
- [x] When player commands are enabled, `/quests complete <quest>` provides equivalent behavior with contextual autocomplete.
- [x] Completion is atomic and exactly one player can complete a quest, including simultaneous attempts.
- [x] Validation or persistence failure does not consume delivered blocks, release rewards, or change quest state.
- [x] Completed quests are no longer shown as active or completable.
- [x] Automated tests verify inventory validation, settlement, overflow, notifications, rollback, and concurrent completion protection.
## Related
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
@@ -1,27 +0,0 @@
---
type: User Story
title: "US-006: Cancel an owned quest"
description: Let an issuer cancel an active quest and reclaim its escrowed reward safely.
status: done
---
# US-006: Cancel an owned quest
As a **quest issuer**, I want to cancel my active quest so that I can reclaim the reward when I no longer need the requested blocks.
## Acceptance criteria
- [x] An issuer can cancel their own active quest through any registered board.
- [x] A player cannot cancel a quest issued by another player.
- [x] Completed, cancelled, and expired quests cannot be cancelled.
- [x] Cancellation removes the quest from active listings and converts its exact escrowed reward into a claim for the issuer.
- [x] The reward is not inserted directly into the issuer's inventory during cancellation.
- [x] When player commands are enabled, `/quests cancel <quest>` provides equivalent behavior and only autocompletes the player's cancellable quest identifiers.
- [x] Cancellation and simultaneous completion are serialized so items cannot be duplicated or lost.
- [x] Persistence failure leaves the quest active and its reward escrowed.
- [x] Automated tests verify ownership, state validation, claim creation, autocomplete, rollback, and completion races.
## Related
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
@@ -1,30 +0,0 @@
---
type: User Story
title: "US-007: Expire quests and claim held items"
description: Hold delivered blocks and returned rewards until their owner safely claims them.
status: done
---
# US-007: Expire quests and claim held items
As a **quest issuer**, I want delivered blocks and returned rewards held at the shared quest board so that I can retrieve them safely.
## Acceptance criteria
- [x] An uncompleted quest expires seven days after its creation time.
- [x] Expiration removes the quest from active listings and converts its exact escrowed reward into a claim for the issuer.
- [x] Completed-quest claims contain the exact block items delivered by the completing player.
- [x] Claims from completion, cancellation, and expiration can be collected through any registered board.
- [x] The board clearly distinguishes delivered blocks from returned rewards.
- [x] Online issuers receive a real-time message when items become claimable.
- [x] Offline issuers receive a message on their next login.
- [x] When player commands are enabled, `/quests claim` collects the same pending claims.
- [x] Items are inserted into the claimant's inventory where space permits; overflow is dropped safely at their feet with a clear message.
- [x] A claim is removed only after all of its items have been inserted or dropped successfully.
- [x] Pending claims and notification state survive logout and server restart without duplication or loss.
- [x] Automated tests verify expiry boundaries, each claim source, notifications, overflow, failure recovery, and persistence.
## Related
- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md)
- [US-006: Cancel an owned quest](us-006-cancel-an-owned-quest.md)
@@ -1,28 +0,0 @@
---
type: User Story
title: "US-008: Control player quest commands"
description: Let administrators require physical-board interaction by controlling access to player quest commands.
status: done
---
# US-008: Control player quest commands
As an **administrator**, I want to enable or disable player quest commands so that I can require players to use physical quest boards.
## Acceptance criteria
- [x] Player `/quests` commands are disabled by default.
- [x] An authorized administrator can use `/questadmin commands enable|disable` with contextual autocomplete.
- [x] The command setting persists across server restarts.
- [x] When disabled, `/quests`, `list`, `complete`, `cancel`, and `claim` reject execution with a clear message directing the player to a quest board; nearby creation follows [US-012](us-012-create-quests-near-a-board.md).
- [x] Disabling commands does not prevent any equivalent action through a registered board.
- [x] `/questadmin` remains available to authorized administrators regardless of the player-command setting.
- [x] Unauthorized users cannot change the setting.
- [x] Automated tests verify the default, persistence, authorization, every gated subcommand, and autocomplete.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
- [US-012: Create quests by command near a board](us-012-create-quests-near-a-board.md)
@@ -1,30 +0,0 @@
---
type: User Story
title: "US-009: Use a screen-fitting quest-board interface"
description: Split the oversized quest-board dialog into compact navigable screens that fit the player's display.
status: done
---
# US-009: Use a screen-fitting quest-board interface
As a **player**, I want the quest-board interface to fit on screen so that I can read quest information and reach every action.
## Acceptance criteria
- [x] The oversized combined dialog is replaced by a compact dashboard.
- [x] Dialog message bodies use a maximum width of 420 pixels.
- [x] Browsing, quest creation, and pending claims use dedicated screens rather than one combined body.
- [x] Active quests are presented as individual navigable entries instead of one unbounded text listing.
- [x] Each quest detail shows its issuer, requested material and quantity, reward, and time remaining.
- [x] Completion, issuer-only cancellation, creation, and claim collection remain available through the board.
- [x] Nested screens provide Back controls that return toward the quest-board dashboard.
- [x] Large active-quest collections remain navigable without producing an unbounded single dialog.
- [x] Automated tests verify width limits, screen separation, navigation, displayed content, and action routing.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-004: Browse available quests](us-004-browse-available-quests.md)
- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md)
- [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md)
@@ -1,29 +0,0 @@
---
type: User Story
title: "US-010: Generate a physical quest-board structure"
description: Let an administrator generate and register a decorative oak quest board at a targeted ground anchor.
status: done
---
# US-010: Generate a physical quest-board structure
As an **administrator**, I want quest-board creation to optionally construct a recognizable physical board so that I do not need to build each board manually.
## Acceptance criteria
- [x] `/questadmin createboard physical` generates and registers a physical quest board while the existing `/questadmin createboard` behavior remains unchanged.
- [x] The targeted block is treated as the ground anchor and remains unchanged.
- [x] The generated board faces the administrator and is five blocks wide and four blocks tall.
- [x] The outer columns use oak-log pillars and the center uses oak planks.
- [x] Oak wall signs on the front display decorative obfuscated or gibberish text.
- [x] Clicking generated visible planks or signs opens the same globally shared quest-board interface.
- [x] Generation requires empty space and refuses to overwrite an existing structure.
- [x] Structure generation and interaction-location registration form one failure-safe transaction; failure restores changed blocks and registers nothing.
- [x] Generated interaction locations persist across restarts and remain compatible with existing registered boards.
- [x] The `physical` argument is offered through permission-aware command autocomplete.
- [x] Automated tests verify geometry, facing direction, obstruction handling, rollback, registration, persistence, and autocomplete.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-009: Use a screen-fitting quest-board interface](us-009-use-a-screen-fitting-quest-board-interface.md)
@@ -1,28 +0,0 @@
---
type: User Story
title: "US-011: Add readable physical-board signage"
description: Give generated quest boards a readable title and browsing instruction above their decorative signs.
status: done
---
# US-011: Add readable physical-board signage
As a **player**, I want readable signs on a generated quest board so that I know what it is and how to open it.
## Acceptance criteria
- [x] A generated physical board has a centered top sign reading `Quest Board` across its sign lines.
- [x] A centered sign immediately below instructs the player to right-click a sign below to browse quests.
- [x] The existing lower row retains three decorative signs with obfuscated or gibberish text.
- [x] All five signs and the visible oak planks open the globally shared quest-board interface.
- [x] New physical boards reserve and validate space for all five signs without overwriting blocks.
- [x] Existing generated boards with the prior three-sign layout are detected from their exact registered oak structure and upgraded with the two centered readable signs.
- [x] Existing custom single-block boards and unrelated structures are not modified.
- [x] Existing-board upgrades never overwrite occupied sign locations and persist new interaction locations failure-safely.
- [x] Automated tests verify sign placement, readable text, decorative text, interaction registration, obstruction handling, and prior-layout migration.
- [x] The title and instruction signs use glowing front-side text for readability while decorative signs remain non-glowing.
- [x] New boards receive glowing readable signs, and exact existing generated five-sign boards are refreshed on startup without modifying custom signs.
## Related
- [US-010: Generate a physical quest-board structure](us-010-generate-a-physical-quest-board.md)
@@ -1,29 +0,0 @@
---
type: User Story
title: "US-012: Create quests by command near a board"
description: Allow command-based quest creation and material autocomplete near a physical board even when global player commands are disabled.
status: done
---
# US-012: Create quests by command near a board
As a **player**, I want to use the quest-creation command near a quest board so that I can use material autocomplete while still interacting at the physical board.
## Acceptance criteria
- [x] When global player quest commands are disabled, `/quests create <block> <quantity>` remains available to a player within five blocks of a registered board interaction location.
- [x] Material and quantity autocomplete remain available for nearby quest creation.
- [x] A player farther than five blocks from every registered board is directed to move closer and no quest or escrow change occurs.
- [x] Board proximity requires the player and registered location to be in the same world.
- [x] A distance of exactly five blocks is accepted and a greater distance is rejected.
- [x] Root listing, `list`, `complete`, `cancel`, and `claim` remain disabled while global player commands are disabled.
- [x] When global player quest commands are enabled, all command behavior, including creation, remains available regardless of board proximity.
- [x] Custom single-block boards and every registered interaction location on generated boards satisfy the proximity requirement.
- [x] Board-dialog creation remains available regardless of command settings or command proximity rules.
- [x] Automated tests verify boundaries, cross-world behavior, autocomplete, command settings, disabled subcommands, and generated-board proximity.
## Related
- [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md)
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-008: Control player quest commands](us-008-control-player-quest-commands.md)
@@ -1,26 +0,0 @@
---
type: User Story
title: "US-013: Show custom reward item names"
description: Display a reward item's custom name together with its material type when browsing quests.
status: done
---
# US-013: Show custom reward item names
As a **player**, I want named reward items identified by both custom name and material so that I understand exactly what a quest will award.
## Acceptance criteria
- [x] A reward with a custom display name is shown as amount, readable custom name, and material type, such as `1 × Shopping List (PAPER)`.
- [x] An unnamed reward continues to show its amount and material type, such as `3 × DIAMOND`.
- [x] Styled custom names are converted to safe readable plain text without changing the escrowed item's exact metadata.
- [x] Quest-board detail dialogs and `/quests` listings use the same reward representation.
- [x] Newly escrowed rewards persist the readable custom name alongside their exact serialized item data.
- [x] Existing persisted named rewards derive their custom name from valid stored item metadata when possible.
- [x] Missing, malformed, or unreadable item metadata safely falls back to the material type without preventing quest browsing.
- [x] Automated tests verify named, unnamed, styled, persisted, historical, and malformed reward metadata.
## Related
- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md)
- [US-004: Browse available quests](us-004-browse-available-quests.md)
@@ -1,6 +1,7 @@
package games.dmg.spigotquestboard;
import java.io.IOException;
import java.time.Clock;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
@@ -15,11 +16,17 @@ final class BukkitIssuerNotifier implements IssuerNotifier, Listener {
private final QuestService quests;
private final Server server;
private final Logger logger;
private final Clock clock;
BukkitIssuerNotifier(QuestService quests, Server server, Logger logger) {
this(quests, server, logger, Clock.systemUTC());
}
BukkitIssuerNotifier(QuestService quests, Server server, Logger logger, Clock clock) {
this.quests = Objects.requireNonNull(quests, "quests");
this.server = Objects.requireNonNull(server, "server");
this.logger = Objects.requireNonNull(logger, "logger");
this.clock = Objects.requireNonNull(clock, "clock");
}
@Override
@@ -32,7 +39,20 @@ final class BukkitIssuerNotifier implements IssuerNotifier, Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
deliver(event.getPlayer());
Player player = event.getPlayer();
int count = quests.activeQuests(clock.instant()).size();
String summary = switch (count) {
case 0 -> "No quests are currently available.";
case 1 -> "1 quest is currently available.";
default -> count + " quests are currently available.";
};
player.sendMessage(summary + " Visit the quest board to accept or create a quest.");
if (!quests.claimsFor(player.getUniqueId()).isEmpty()) {
player.sendMessage(
"You have items waiting to be claimed. Visit the quest board to collect them."
);
}
deliver(player);
}
void deliver(Player player) {
@@ -6,11 +6,11 @@ import java.util.Locale;
import java.util.Optional;
import org.bukkit.Material;
final class BukkitBlockMaterialCatalog implements BlockMaterialCatalog {
final class BukkitItemMaterialCatalog implements ItemMaterialCatalog {
@Override
public Optional<String> normalizeBlock(String input) {
public Optional<String> normalizeItem(String input) {
Material material = Material.matchMaterial(input == null ? "" : input);
if (material == null || material.isAir() || !material.isBlock()) {
if (material == null || material.isAir() || !material.isItem()) {
return Optional.empty();
}
return Optional.of(material.name());
@@ -20,7 +20,7 @@ final class BukkitBlockMaterialCatalog implements BlockMaterialCatalog {
public List<String> suggest(String prefix) {
String normalizedPrefix = prefix.toUpperCase(Locale.ROOT);
return Arrays.stream(Material.values())
.filter(material -> material.isBlock() && !material.isAir())
.filter(material -> material.isItem() && !material.isAir())
.map(Material::name)
.filter(name -> name.startsWith(normalizedPrefix))
.sorted()
@@ -3,7 +3,7 @@ package games.dmg.spigotquestboard;
import java.util.List;
import java.util.Optional;
interface BlockMaterialCatalog {
Optional<String> normalizeBlock(String input);
interface ItemMaterialCatalog {
Optional<String> normalizeItem(String input);
List<String> suggest(String prefix);
}
@@ -113,7 +113,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
+ "metadata, is escrowed only if the quest saves successfully.",
List.of(action("Create quest", QuestBoardDialogSpec.Route.SUBMIT_CREATE)),
List.of(
new QuestBoardDialogSpec.Input("requested_material", "Requested block", "", 64),
new QuestBoardDialogSpec.Input("requested_material", "Requested item", "", 64),
new QuestBoardDialogSpec.Input("requested_quantity", "Quantity", "64", 10)
),
true
@@ -221,7 +221,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
}
ActionButton collect = callbackButton(
"Collect pending claims",
"Collect delivered blocks and returned rewards",
"Collect delivered items and returned rewards",
250,
respondingPlayer -> submitClaim(respondingPlayer)
);
@@ -247,7 +247,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
private ActionButton renderQuestAction(QuestBoardDialogSpec.Action action) {
return switch (action.route()) {
case COMPLETE -> callbackButton(
action.label(), "Deliver the requested blocks", 180,
action.label(), "Deliver the requested items", 180,
player -> submitCompletion(player, action.questId())
);
case CANCEL -> callbackButton(
@@ -328,7 +328,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
}
return claims.stream().map(claim -> {
String kind = claim.type() == QuestClaimType.DELIVERED_BLOCKS
? "DELIVERED BLOCKS" : "RETURNED REWARD (" + claim.source().name() + ")";
? "DELIVERED ITEMS" : "RETURNED REWARD (" + claim.source().name() + ")";
String items = claim.items().stream()
.map(item -> item.amount() + " " + item.material())
.reduce((left, right) -> left + ", " + right)
@@ -382,7 +382,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
player.sendMessage(exception.getMessage());
} catch (IOException exception) {
player.sendMessage(
"The quest could not be saved. Your delivered blocks were restored."
"The quest could not be saved. Your delivered items were restored."
);
}
}
@@ -55,7 +55,7 @@ final class QuestClaimController implements QuestClaimGateway {
private static String description(QuestClaim claim) {
if (claim.type() == QuestClaimType.DELIVERED_BLOCKS) {
return "Collected delivered blocks from completed quest " + claim.questId() + ".";
return "Collected delivered items from completed quest " + claim.questId() + ".";
}
String reason = claim.source() == QuestClaimSource.EXPIRATION
? "expired" : "cancelled";
@@ -148,7 +148,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
sender.sendMessage(exception.getMessage());
} catch (IOException exception) {
sender.sendMessage(
"The quest could not be saved. Your delivered blocks were restored."
"The quest could not be saved. Your delivered items were restored."
);
}
return true;
@@ -223,7 +223,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
);
}
if (arguments.length == 2 && "create".equalsIgnoreCase(arguments[0])) {
return creator.suggestBlockMaterials(arguments[1]);
return creator.suggestItemMaterials(arguments[1]);
}
if (arguments.length == 3 && "create".equalsIgnoreCase(arguments[0])) {
return startsWith(QUANTITIES, arguments[2]);
@@ -267,7 +267,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
private static void usage(CommandSender sender) {
sender.sendMessage(
"Usage: /quests [list] | /quests create <block> <quantity> | "
"Usage: /quests [list] | /quests create <item> <quantity> | "
+ "/quests complete <quest> | /quests cancel <quest> | /quests claim"
);
sender.sendMessage("Hold the entire reward stack in your main hand; its exact metadata will be escrowed.");
@@ -8,12 +8,12 @@ import org.bukkit.entity.Player;
final class QuestCreationController implements QuestCreationGateway {
private final QuestService quests;
private final BlockMaterialCatalog materials;
private final ItemMaterialCatalog materials;
private final HeldRewardInventory rewards;
QuestCreationController(
QuestService quests,
BlockMaterialCatalog materials,
ItemMaterialCatalog materials,
HeldRewardInventory rewards
) {
this.quests = Objects.requireNonNull(quests, "quests");
@@ -29,9 +29,9 @@ final class QuestCreationController implements QuestCreationGateway {
if (requestedAmount <= 0) {
throw new IllegalArgumentException("Requested quantity must be positive");
}
String material = materials.normalizeBlock(requestedMaterial)
String material = materials.normalizeItem(requestedMaterial)
.orElseThrow(() -> new IllegalArgumentException(
"Requested material must be a valid block"
"Requested material must be a valid item"
));
HeldRewardInventory.RemovedReward removed = rewards.remove(player);
try {
@@ -46,7 +46,7 @@ final class QuestCreationController implements QuestCreationGateway {
}
@Override
public List<String> suggestBlockMaterials(String prefix) {
public List<String> suggestItemMaterials(String prefix) {
return materials.suggest(prefix == null ? "" : prefix);
}
}
@@ -9,5 +9,5 @@ interface QuestCreationGateway {
Quest create(Player player, String requestedMaterial, int requestedAmount, Instant createdAt)
throws IOException;
List<String> suggestBlockMaterials(String prefix);
List<String> suggestItemMaterials(String prefix);
}
@@ -33,7 +33,7 @@ final class QuestService implements QuestBrowser {
String material = Objects.requireNonNull(requestedMaterial, "requestedMaterial")
.trim().toUpperCase(Locale.ROOT);
if (material.isEmpty() || "AIR".equals(material)) {
throw new IllegalArgumentException("Requested material must be a block");
throw new IllegalArgumentException("Requested material must be a valid item");
}
Quest quest = new Quest(
UUID.randomUUID(), issuerId, issuerName, material, requestedAmount,
@@ -90,7 +90,7 @@ final class QuestService implements QuestBrowser {
IssuerNotification notification = new IssuerNotification(
UUID.randomUUID(), active.id(), active.issuerId(),
"Quest " + active.id() + " was completed. Your delivered "
+ active.requestedMaterial() + " blocks can be claimed at a quest board.",
+ active.requestedMaterial() + " items can be claimed at a quest board.",
completedAt
);
@@ -37,11 +37,11 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
}
QuestCreationGateway creator = new QuestCreationController(
quests, new BukkitBlockMaterialCatalog(), new BukkitHeldRewardInventory()
quests, new BukkitItemMaterialCatalog(), new BukkitHeldRewardInventory()
);
Clock clock = Clock.systemUTC();
BukkitIssuerNotifier notifier = new BukkitIssuerNotifier(
quests, getServer(), getLogger()
quests, getServer(), getLogger(), clock
);
QuestCompletionGateway completer = new QuestCompletionController(
quests, new BukkitQuestCompletionInventory(), notifier
@@ -4,15 +4,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent;
import org.junit.jupiter.api.Test;
final class BukkitIssuerNotifierTest {
@@ -82,6 +88,90 @@ final class BukkitIssuerNotifierTest {
assertTrue(service.state().notifications().isEmpty());
}
@Test
void everyLoginReportsAvailableQuestsWithNaturalWordingAndBoardHint() throws Exception {
QuestService service = new QuestService(new Repository());
Instant now = Instant.EPOCH.plusSeconds(604800);
BukkitIssuerNotifier notifier = new BukkitIssuerNotifier(
service, mock(Server.class), Logger.getAnonymousLogger(),
Clock.fixed(now, ZoneOffset.UTC)
);
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(player);
String hint = " Visit the quest board to accept or create a quest.";
// Expired at exactly login time, even before the expiry task runs.
createQuest(service, Instant.EPOCH);
Quest completed = createQuest(service, now);
service.complete(completed.id(), new EscrowItem("STONE", 1, null), now);
Quest cancelled = createQuest(service, now);
service.cancel(cancelled.id(), cancelled.issuerId(), now);
notifier.onPlayerJoin(event);
verify(player).sendMessage("No quests are currently available." + hint);
createQuest(service, now);
notifier.onPlayerJoin(event);
verify(player).sendMessage("1 quest is currently available." + hint);
createQuest(service, now);
notifier.onPlayerJoin(event);
notifier.onPlayerJoin(event);
verify(player, times(2)).sendMessage("2 quests are currently available." + hint);
verify(player, never()).sendMessage(contains("items waiting"));
}
@Test
void claimsAreRemindedOnEveryLoginAcrossReloadsUntilAllCollected() throws Exception {
Repository repository = new Repository();
QuestService service = completedService(repository);
UUID issuer = service.state().notifications().values().iterator().next().recipientId();
Quest cancelled = service.create(
issuer, "Issuer", "STONE", 1,
List.of(new EscrowItem("DIAMOND", 1, null)), Instant.EPOCH
);
service.cancel(cancelled.id(), issuer, Instant.EPOCH.plusSeconds(1));
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(issuer);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(player);
String reminder = "You have items waiting to be claimed. "
+ "Visit the quest board to collect them.";
BukkitIssuerNotifier notifier = new BukkitIssuerNotifier(
service, mock(Server.class), Logger.getAnonymousLogger()
);
notifier.onPlayerJoin(event);
notifier.onPlayerJoin(event);
verify(player, times(2)).sendMessage(reminder);
assertTrue(service.pendingNotifications(issuer).isEmpty());
assertEquals(2, service.claimsFor(issuer).size());
service = new QuestService(repository);
notifier = new BukkitIssuerNotifier(service, mock(Server.class), Logger.getAnonymousLogger());
notifier.onPlayerJoin(event);
verify(player, times(3)).sendMessage(reminder);
service.acknowledgeClaim(issuer, service.claimsFor(issuer).getFirst().id());
notifier.onPlayerJoin(event);
verify(player, times(4)).sendMessage(reminder);
clearInvocations(player);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
notifier.onPlayerJoin(event);
verify(player, never()).sendMessage(reminder);
when(player.getUniqueId()).thenReturn(issuer);
service.acknowledgeClaim(issuer, service.claimsFor(issuer).getFirst().id());
notifier.onPlayerJoin(event);
verify(player, never()).sendMessage(reminder);
}
private static Quest createQuest(QuestService service, Instant now) throws Exception {
return service.create(
UUID.randomUUID(), "Issuer", "STONE", 1,
List.of(new EscrowItem("DIAMOND", 1, null)), now
);
}
private static QuestService completedService(Repository repository) throws Exception {
QuestService service = new QuestService(repository);
Quest quest = service.create(
@@ -0,0 +1,100 @@
package games.dmg.spigotquestboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.bukkit.Material;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
final class BukkitItemMaterialCatalogTest {
private final BukkitItemMaterialCatalog catalog = new BukkitItemMaterialCatalog();
private MockedStatic<Material> materials;
@BeforeEach
void provideServerMaterialProperties() {
// Purpur delegates material properties to server-only registries.
String[] names = {"SHULKER_SHELL", "SHULKER_BOX", "STONE", "AIR", "CAVE_AIR", "VOID_AIR", "WATER", "FIRE"};
Material[] fixtures = new Material[names.length];
for (int i = 0; i < names.length; i++) {
Material material = Mockito.mock(Material.class);
Mockito.when(material.name()).thenReturn(names[i]);
Mockito.when(material.isItem()).thenReturn(i <= 3);
Mockito.when(material.isBlock()).thenReturn(i != 0);
Mockito.when(material.isAir()).thenReturn(i >= 3 && i <= 5);
fixtures[i] = material;
}
materials = Mockito.mockStatic(Material.class);
materials.when(Material::values).thenReturn(fixtures);
for (int i = 0; i < names.length; i++) {
String name = names[i];
Material material = fixtures[i];
materials.when(() -> Material.matchMaterial(name)).thenReturn(material);
materials.when(() -> Material.matchMaterial(name.toLowerCase(java.util.Locale.ROOT)))
.thenReturn(material);
}
}
@AfterEach
void closeMaterialFixtures() {
materials.close();
}
@Test
void acceptsShellsAndExistingBlockItems() {
assertEquals(Optional.of("SHULKER_SHELL"), catalog.normalizeItem("shulker_shell"));
assertEquals(Optional.of("SHULKER_BOX"), catalog.normalizeItem("shulker_box"));
assertEquals(Optional.of("STONE"), catalog.normalizeItem("stone"));
}
@Test
void rejectsAirInvalidAndNonItemMaterials() {
for (String input : new String[] {"AIR", "CAVE_AIR", "VOID_AIR", "WATER", "FIRE", "invalid", ""}) {
assertTrue(catalog.normalizeItem(input).isEmpty(), input);
}
assertTrue(catalog.normalizeItem(null).isEmpty());
}
@Test
void createsPersistsAndSettlesShellRequest(@org.junit.jupiter.api.io.TempDir java.nio.file.Path directory)
throws Exception {
QuestRepository repository = new YamlQuestRepository(directory.resolve("quests.yml"));
QuestService service = new QuestService(repository);
org.bukkit.entity.Player player = Mockito.mock(org.bukkit.entity.Player.class);
java.util.UUID issuer = java.util.UUID.randomUUID();
Mockito.when(player.getUniqueId()).thenReturn(issuer);
Mockito.when(player.getName()).thenReturn("Issuer");
EscrowItem reward = new EscrowItem("DIAMOND", 1, null);
HeldRewardInventory inventory = ignored -> new HeldRewardInventory.RemovedReward() {
@Override public EscrowItem item() { return reward; }
@Override public void rollback() { throw new AssertionError("Unexpected rollback"); }
};
QuestCreationController controller = new QuestCreationController(service, catalog, inventory);
assertTrue(controller.suggestItemMaterials("shulker_").contains("SHULKER_SHELL"));
Quest quest = controller.create(player, "shulker_shell", 16, java.time.Instant.EPOCH);
service = new QuestService(repository);
EscrowItem delivery = new EscrowItem("SHULKER_SHELL", 16, null);
QuestCompletion completion = service.complete(
quest.id(), delivery, java.time.Instant.EPOCH.plusSeconds(1)
);
assertEquals(java.util.List.of(reward), completion.reward());
service = new QuestService(repository);
QuestClaim claim = service.claimsFor(issuer).getFirst();
assertEquals(java.util.List.of(delivery), claim.items());
assertEquals(QuestStatus.COMPLETED, service.state().quests().get(quest.id()).status());
service.acknowledgeClaim(issuer, claim.id());
assertTrue(new QuestService(repository).claimsFor(issuer).isEmpty());
}
@Test
void suggestsShellsAndBoxesButNotNonItems() {
assertTrue(catalog.suggest("shulker_").contains("SHULKER_SHELL"));
assertTrue(catalog.suggest("shulker_").contains("SHULKER_BOX"));
assertTrue(catalog.suggest("WATER").stream().noneMatch("WATER"::equals));
}
}
@@ -267,7 +267,7 @@ final class QuestBoardDialogUiTest {
}
@Test
void boardListingDistinguishesDeliveredBlocksFromReturnedRewards() {
void boardListingDistinguishesDeliveredItemsFromReturnedRewards() {
RecordingCreator creator = new RecordingCreator(false);
Player player = mock(Player.class);
UUID owner = UUID.randomUUID();
@@ -301,7 +301,7 @@ final class QuestBoardDialogUiTest {
String listing = ui.claimListingText(player);
org.junit.jupiter.api.Assertions.assertTrue(listing.contains("DELIVERED BLOCKS — 2 STONE"));
org.junit.jupiter.api.Assertions.assertTrue(listing.contains("DELIVERED ITEMS — 2 STONE"));
org.junit.jupiter.api.Assertions.assertTrue(
listing.contains("RETURNED REWARD (EXPIRATION) — 1 DIAMOND")
);
@@ -412,7 +412,7 @@ final class QuestBoardDialogUiTest {
}
@Override
public List<String> suggestBlockMaterials(String prefix) {
public List<String> suggestItemMaterials(String prefix) {
return List.of();
}
}
@@ -29,7 +29,7 @@ final class QuestClaimControllerTest {
assertEquals(new ClaimCollectionResult(3, 6), result);
assertTrue(service.claimsFor(issuer).isEmpty());
verify(player).sendMessage(contains("delivered blocks from completed quest"));
verify(player).sendMessage(contains("delivered items from completed quest"));
verify(player).sendMessage(contains("returned reward from cancelled quest"));
verify(player).sendMessage(contains("returned reward from expired quest"));
verify(player).sendMessage(contains("dropped at your feet, protected for you"));
@@ -417,7 +417,7 @@ final class QuestCommandTest {
}
@Override
public List<String> suggestBlockMaterials(String prefix) {
public List<String> suggestItemMaterials(String prefix) {
suggestionPrefix = prefix;
return List.of("STONE", "STONE_BRICKS");
}
@@ -71,9 +71,9 @@ final class QuestCreationControllerTest {
private static QuestCreationController controller(
MemoryQuestRepository repository, HeldRewardInventory inventory
) throws IOException {
BlockMaterialCatalog catalog = new BlockMaterialCatalog() {
ItemMaterialCatalog catalog = new ItemMaterialCatalog() {
@Override
public Optional<String> normalizeBlock(String input) {
public Optional<String> normalizeItem(String input) {
return "stone".equalsIgnoreCase(input) ? Optional.of("STONE") : Optional.empty();
}