diff --git a/superpowers/plans/2026-08-27-paper-scene-editor.md b/superpowers/plans/2026-08-27-paper-scene-editor.md new file mode 100644 index 0000000..5c5d85f --- /dev/null +++ b/superpowers/plans/2026-08-27-paper-scene-editor.md @@ -0,0 +1,667 @@ +# Paper Scene Editor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task by task. Use `superpowers:test-driven-development` for every behavior change and `superpowers:verification-before-completion` before each merge or release. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the first Paper scene-editor slice so builders can author canonical, catalog-valid props and NPCs in a build world, save root `scene.json` safely, and receive a dirty-session guard before `/map push`. + +**Architecture:** First publish a non-empty resourcepacks 0.6.0 catalog. Then create the standalone `plugin-scene-editor` repository with a Bukkit-free `common` module and a Paper adapter. Keep documents immutable, persist through `scene-format` only, and integrate GroundsMaps through the editor's small `SceneEditStatus` service API. + +**Tech Stack:** Kotlin, Java 25, Gradle, Grounds conventions, `scene-format` 0.1.0, `scene-testkit` 0.1.0, `resourcepacks-catalog` 0.6.0, Paper 26.1.2, Adventure, MockBukkit, JUnit 5 + +## Global constraints + +- Use isolated worktrees for `resourcepacks`, `plugin-scene-editor`, `buildsystem`, and later deployment changes. Preserve every unrelated dirty checkout. +- Do not wait for `arc-linux`, Kubernetes, service-maps Stage acceptance, or cluster recovery. Record those as external release/deployment gates. +- Never create a second scene JSON model or catalog JSON codec. Use released public APIs from `scene-format` and `GroundsAssetCatalog`. +- Write a failing focused test before each production change. Confirm that it fails for the expected reason before implementing. +- Keep all common state free of Bukkit types. Paper objects enter only through adapter interfaces. +- Do not claim a saved scene is published. `/scene save` reports local file state only. +- Do not merge a consumer pin until its exact producer artifact is published, except for locally verified branches explicitly held behind that gate. +- Use `GRADLE_USER_HOME=/tmp/` when the default Gradle cache is unavailable. +- Update the Confluence masterplan only after repository evidence exists; never mark deployment or Stage acceptance complete from local tests. + +--- + +### Task 1: Add declarative bootstrap assets to resourcepacks + +**Repository:** `/home/lukas/grounds/resourcepacks` + +**Files:** +- Modify: `resourcepacks-catalog/build.gradle.kts` +- Modify: `resourcepacks-catalog/src/main/kotlin/gg/grounds/resourcepacks/catalog/GroundsAssets.kt` +- Modify: `resourcepacks-catalog/src/test/kotlin/gg/grounds/resourcepacks/catalog/CatalogApiTest.kt` +- Modify: `resourcepacks-catalog/src/test/java/gg/grounds/resourcepacks/catalog/CatalogJavaApiTest.java` +- Modify: `resourcepacks-product/src/test/kotlin/gg/grounds/resourcepacks/product/CatalogParityValidatorTest.kt` + +**Contract:** +- `grounds:editor/marker` is a `PROP` with `LocalBounds(center=(0,0.5,0), size=(1,1,1))`. +- `grounds:editor/guide` is an `NPC_BODY` with `LocalBounds(center=(0,0.9,0), size=(0.6,1.8,0.6))`. +- Both have no animations and empty editor metadata. +- `GroundsAssetCatalog.catalog.assets` is immutable and deterministically ordered. + +- [x] **Step 1: Create an isolated feature worktree** + +```bash +git fetch origin +git worktree add ../.worktrees/resourcepacks-scene-bootstrap -b feat/scene-bootstrap-assets origin/main +``` + +- [x] **Step 2: Write failing catalog API tests** + +Replace the empty-catalog assertions with exact key, kind, bounds, ordering, version-reference, Java-access, and mutation-rejection assertions. Extend the parity fixture to require both projected model paths. + +Run: + +```bash +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew --no-build-cache :resourcepacks-catalog:test :resourcepacks-product:test --tests '*CatalogParityValidatorTest' +``` + +Expected: failure because the generated catalog is still empty. + +- [x] **Step 3: Generate the catalog from one declarative source** + +Add one ordered private definition map in `GroundsAssets.kt` and retain the public Java-facing `GroundsAssets.all: Set` as its immutable key set. Keep the existing public owners, types, and Java ABI unchanged. The selected Gradle build version must reach the runtime catalog for both releases and exact edge builds. + +- [x] **Step 4: Run the focused tests** + +```bash +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew --no-build-cache :resourcepacks-catalog:check :resourcepacks-product:test --tests '*CatalogParityValidatorTest' +git diff --check +``` + +Expected: catalog API tests and the parity fixture pass against the two exact projected model paths. Full pack composition remains red until Task 2 supplies the source models. + +- [x] **Step 5: Commit the catalog declaration** + +```bash +git add resourcepacks-catalog +git commit -m "feat(catalog): declare scene editor bootstrap assets" +``` + +--- + +### Task 2: Package matching bootstrap models + +**Repository:** resourcepacks feature worktree from Task 1 + +**Files:** +- Create: `art/content/models/editor/marker.json` +- Create: `art/content/models/npc_bodies/editor/guide.json` +- Modify: `art/content/LICENSE` +- Modify: `resourcepacks-product/src/main/kotlin/gg/grounds/resourcepacks/product/ContentContribution.kt` +- Modify: `resourcepacks-product/src/main/kotlin/gg/grounds/resourcepacks/product/ProductGraph.kt` +- Modify: `resourcepacks-product/src/test/kotlin/gg/grounds/resourcepacks/product/PackComposerTest.kt` +- Modify: `resourcepacks-catalog/src/test/kotlin/gg/grounds/resourcepacks/catalog/PackArtworkLicenseContractTest.kt` + +- [x] **Step 1: Write failing composition and license tests** + +Require these exact content-pack entries: + +```text +assets/grounds/models/editor/marker.json +assets/grounds/models/npc_bodies/editor/guide.json +``` + +Require both source JSON files to be covered by the repository's artwork/content provenance contract. + +- [x] **Step 2: Add deliberately authored minimal models** + +Add deterministic JSON models using Minecraft parent/texture references and no copied binary artwork. Record them as Grounds-authored source in `art/content/LICENSE` while preserving the existing vendor/MCModels attribution text and tests. The marker is a one-block editor marker; the guide is a simple bootstrap NPC-body model. Do not add image files or vendor assets. + +- [x] **Step 3: Capture and compose the files safely** + +Extend `ProductGraph` to capture both sources with `HeldSourceFile`, bounded size, and pinned bytes. Feed byte-backed entries through `ContentContribution`; do not pass mutable paths into pack composition. Preserve no-follow behavior and exact release-input capture. + +- [x] **Step 4: Verify model/catalog parity and the built pack** + +```bash +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew --no-build-cache :resourcepacks-catalog:check :resourcepacks-product:test +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew --no-build-cache clean check -PpackSetVersion="$(tr -d '\n' < version.txt)" +git diff --check +``` + +Build the current branch's release-shaped PackSet into a fresh absent output path and inspect the ZIP. The feature branch must use the exact value still present in `version.txt`; Release Please changes it to 0.6.0 later: + +```bash +scene_pack_version="$(tr -d '\n' < version.txt)" +scene_pack_commit="$(git rev-parse HEAD)" +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew :resourcepacks-product:buildPackSet -PpackSetVersion="$scene_pack_version" -PprovenanceCommit="$scene_pack_commit" -PpublicationType=release -PpublicationId="v$scene_pack_version" -PreleaseOutput=/tmp/scene-packset-bootstrap +unzip -l "/tmp/scene-packset-bootstrap/grounds-content-pack-v${scene_pack_version}.zip" +``` + +Expected: both exact model paths exist and parity validation passes. + +- [x] **Step 5: Commit the pack content** + +```bash +git add art/content resourcepacks-product resourcepacks-catalog/src/test +git commit -m "feat(pack): add scene editor bootstrap models" +``` + +--- + +### Task 3: Merge and release resourcepacks 0.6.0 + +**Repository:** resourcepacks feature worktree + +- [x] **Step 1: Run full verification and request review** + +```bash +GRADLE_USER_HOME=/tmp/scene-resourcepacks-gradle ./gradlew --no-build-cache clean check -PpackSetVersion="$(tr -d '\n' < version.txt)" +git diff --check origin/main...HEAD +git status --short +``` + +Use `superpowers:requesting-code-review`; resolve every blocking finding and rerun the commands. + +- [x] **Step 2: Push, create the PR, and merge after green checks** + +Do not wait on `arc-linux` indefinitely. If required hosted checks cannot start, keep the verified branch and record the release gate rather than weakening branch protection. + +Delivered by resourcepacks PR #26. CI run `33076082670` passed and the PR was squash-merged as `bc69e647c6111edfa8e6efaf9c2f79715f6d51d6`. + +- [ ] **Step 3: Complete Release Please for 0.6.0** + +Let Release Please update `version.txt`, changelog, and release metadata. On that release branch, rerun `clean check` with the exact new `version.txt` value. Merge its release PR, verify tag `v0.6.0`, Maven package `gg.grounds:resourcepacks-catalog:0.6.0`, and immutable PackSet artifacts. Do not activate a cluster channel while the cluster is down. + +Release PR #24 and its clean verification run `33077019256` were merged as `a1ccb3d4981c268253744f88142d2c849082006c`; tag `v0.6.0` exists. Immutable release run `33077618746` built and locally verified all four artifacts, then failed at the catalog Maven upload with GitHub Packages `402 Payment Required`. Keep this step open until Maven, CDN/release assets, and Stable advancement are complete. + +- [ ] **Step 4: Record exact producer evidence** + +Capture merge SHA, tag URL, Maven coordinate, PackSet checksums, and any queued workflow IDs. These values gate Task 4 production dependency resolution. + +--- + +### Task 4: Create and scaffold `plugin-scene-editor` + +**Repository:** new `/home/lukas/grounds/plugin-scene-editor` + +**Files:** +- Create: `settings.gradle.kts`, `build.gradle.kts`, `gradle.properties` +- Create: `common/build.gradle.kts`, `paper/build.gradle.kts` +- Create: `paper/src/main/resources/plugin.yml` +- Create: `README.md`, `CHANGELOG.md`, `LICENSE` +- Create: `release-please-config.json`, `.release-please-manifest.json` +- Create: `.github/dependabot.yml` +- Create: `.github/workflows/ci.yml`, `.github/workflows/release-please.yml`, `.github/workflows/release.yml` +- Create: Gradle wrapper files +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/SceneEditStatus.kt` +- Create: packaging and Java-consumer tests under `common/src/test` and `paper/src/test` + +- [ ] **Step 1: Create the repository locally and on GitHub** + +Create the GitHub repository with the same visibility and merge settings as sibling Grounds plugin repositories. Initialize `main`, then create an isolated `feat/scene-editor-mvp` worktree. Do not reuse the dirty docs or plugin template checkouts. + +- [x] **Step 2: Copy only conventions, not product code** + +Use `plugin-grounds-platform` and `plugin-permissions` as structure exemplars. Configure exact released inputs: + +```text +root project: plugin-scene-editor +modules: common, paper +base conventions: 0.8.0 +Kotlin/JDK: Kotlin 2.2.20 conventions and Java 25 +Paper: paper-conventions / Paper 26.1.2 +scene-format: 0.1.0 +scene-testkit: 0.1.0 (test only) +resourcepacks-catalog: 0.6.0 +paper compile-only API: de.eintosti:buildsystem-api:4.0.0 +``` + +Set plugin name `GroundsSceneEditor`, main class `gg.grounds.scene.editor.paper.GroundsSceneEditorPlugin`, API version matching the current Paper convention, hard `depend: [BuildSystem]`, root `/scene`, and the permissions from the approved design. Expand `${VERSION}` explicitly with `ProcessResources`; `paper-conventions` does not do this itself. + +- [x] **Step 3: Add the public service boundary first** + +Create the Java-friendly contract: + +```kotlin +fun interface SceneEditStatus { + fun hasUnsavedChanges(worldId: UUID): Boolean +} +``` + +Add a Java compilation test proving callers see `boolean hasUnsavedChanges(UUID)` without Kotlin-specific types. + +- [x] **Step 4: Add packaging tests** + +Prove the deployable Paper shadow JAR contains `SceneEditStatus`, `scene-format`, and the pinned catalog exactly once, leaves `SceneEditStatus` unrelocated, expands `${VERSION}`, and excludes test libraries. Configure Paper's Maven publication to replace the disabled/empty standard JAR with `shadowJar`, following `plugin-grounds-runtime/paper/build.gradle.kts`; publish both `common` and the runnable Paper artifact. Use the default Grounds artifact naming unless deployment tooling proves a fixed versioned filename is required. + +- [x] **Step 5: Verify and commit the scaffold** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache clean check +git diff --check +git add . +git commit -m "build: scaffold Paper scene editor" +``` + +Local commit `30ee898` passed a serial clean build and both Maven publications against an isolated Maven repository populated from the exact `v0.6.0` tag. The public GitHub repository remains intentionally uncreated until repository visibility is explicitly approved; the remote package outage is not represented as a successful release. + +--- + +### Task 5: Implement catalog binding, scene creation, and typed mutations + +**Repository:** plugin-scene-editor feature worktree + +**Files:** +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBinding.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/catalog/CatalogStatus.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutation.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutationResult.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutations.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/validation/SceneValidationState.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/validation/SaveEligibility.kt` +- Create matching tests under `common/src/test/kotlin/...` + +- [x] **Step 1: Write failing catalog and creation-default tests** + +Cover exact catalog references, wrong asset kind, missing NPC bounds, duplicate IDs, player-position/yaw placement, zero pitch/roll, unit scale, null group/animation/label, `(0,2.25,0)` label offset, fixed look, null proximity, empty bindings, visible state, and automatic activation. + +- [x] **Step 2: Implement catalog binding and scene creation** + +Production binding reads `GroundsAssetCatalog.catalog` and constructs immutable `grounds:actions@1` with no actions. Tests inject catalogs. New documents pin these exact references. + +- [x] **Step 3: Write failing mutation tests** + +Cover create, select-independent replace, position set/here/add, rotation set/add with canonical angles, uniform positive scale, clone, label set, and remove for props/NPCs. Rejections must preserve the exact original document. + +Also cover a decoded document containing `ApplicationAction`: unrelated prop/NPC mutations preserve it byte-semantically, the empty action catalog makes it read-only and catalog-unverified, and no common mutation can create, replace, or silently remove it without an exact catalog definition. + +- [x] **Step 4: Implement typed immutable mutations** + +Use scene DTO constructors/copies only. Return structured rejection values instead of throwing user-input failures. Run intrinsic validation after every candidate mutation and catalog validation for save eligibility. + +- [x] **Step 5: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :common:test +git diff --check +git add common +git commit -m "feat(common): add scene mutations and validation" +``` + +Delivered locally as `d9c3770`. TDD and review covered exact catalog pins/defaults, every first-slice prop/NPC mutation family, canonical transforms, exact-original rejection identity, unsupported composite rejection, non-finite input, pure intrinsic/catalog status separation, SaveEligibility, and read-only preservation of unverified application actions. Final forced `:common:spotlessCheck :common:test` rerun passed against the exact locally staged v0.6.0 catalog. + +--- + +### Task 6: Implement shared sessions, history, selections, and leases + +**Files:** +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/session/EditorSession.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/session/EditorSessionService.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/session/SessionState.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/history/SceneHistory.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/lease/ElementLease.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/lease/ElementLeaseRegistry.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/session/EditorSelection.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/SceneEditorEvents.kt` +- Create matching tests + +- [x] **Step 1: Write failing state-machine tests** + +Cover one session per world, shared document/per-player selection, serialized mutation order, one event per success, no event/history change on rejection, canonical-byte dirty comparison, and `SceneEditStatus` across absent/clean/dirty sessions. + +- [x] **Step 2: Implement bounded history** + +Store complete immutable snapshots, cap at 100, clear redo after a new edit, keep history after save, and make multi-step undo/redo atomic session operations. + +- [x] **Step 3: Implement leases with an injected clock** + +Cover acquire, refusal with owner, renewal, 120-second expiry, explicit override, disconnect/world-change/deselect/delete release, and document-level operations without global element leases. + +- [x] **Step 4: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :common:test +git diff --check +git add common +git commit -m "feat(common): add editor sessions and leases" +``` + +Delivered locally as `a40f2ff`. TDD and two review rounds covered one shared session per +world, independent player selections, serialized mutations, FIFO post-commit events with +listener-failure isolation, canonical-byte dirty state, generation-bound save snapshots, +100-entry immutable history, atomic multi-step undo/redo, 120-second element leases, +renewal/expiry/override/release behavior, and controlled no-session/input edge cases. The final +forced `:common:spotlessCheck :common:test --rerun-tasks` run passed with all 11 Gradle tasks +executed, and the final read-only review reported no P1/P2 findings. + +--- + +### Task 7: Implement atomic world-root persistence and recovery + +**Files:** +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/SceneRepository.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/WorldSceneRepository.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/AtomicSceneFileStore.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/SceneFingerprint.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/SceneLoadResult.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/repository/SceneSaveResult.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/recovery/RecoveryService.kt` +- Create matching tests + +- [x] **Step 1: Write failing load and path-security tests** + +Test absent, valid, and invalid root `scene.json`; exact-byte fingerprinting; normalized world root; symlink escape rejection; size bounds; and preservation of invalid bytes. + +- [x] **Step 2: Write failing atomic-save tests** + +Inject file operations and prove sibling temp creation, flush, `ATOMIC_MOVE`, per-world serialization, concurrent fingerprint rejection, stale async generation rejection, and retention of the old file when atomic replacement is unsupported. + +- [x] **Step 3: Implement canonical persistence only** + +Accept/return scene-format results. Never expose a second serializer. Update the session base fingerprint and dirty comparison point only after successful replacement. + +- [x] **Step 4: Implement explicit recovery** + +Cover byte-identical timestamped sibling backup-and-create, generated diagnostic exports below plugin data, clean reload, dirty reload refusal, and literal-confirm discard-and-reload with audit details. + +- [x] **Step 5: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :common:test +git diff --check +git add common +git commit -m "feat(common): persist scenes atomically" +``` + +Delivered locally as `7ce3a1e`. TDD and three independent review rounds covered bounded +no-follow reads, exact-byte SHA-256 fingerprints with distinct absence, canonical `SceneJson` +encoding, eligibility-bound save reservations, coordinated conditional `ATOMIC_MOVE`, concurrent +disk conflicts, atomic-only failure retention, defensive byte boundaries, invalid-file backup and +create, diagnostic exports, clean/dirty reload rules, literal-confirm discard audit, JVM capability +forgery, fatal-error reservation cleanup, and best-effort temp cleanup. The final forced +`:common:spotlessCheck :common:test --rerun-tasks` run passed with all 11 Gradle tasks executed, +and the final read-only review reported no P1/P2 findings under the documented cooperative-writer +coordination contract. + +--- + +### Task 8: Compose the Paper plugin and command surface + +**Files:** +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/GroundsSceneEditorPlugin.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/PaperSceneEditorRuntime.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/PaperSessionResolver.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommand.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleter.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizer.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/AdventureSceneFeedback.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/PaperScheduler.kt` +- Create matching MockBukkit tests + +- [x] **Step 1: Write failing lifecycle/service tests** + +Prove enable composition, catalog initialization failure, command registration, `SceneEditStatus` registration, reverse-order close/unregister, and no mutable global singleton. + +- [x] **Step 2: Implement lifecycle composition** + +Keep `JavaPlugin` limited to dependency construction and registration. Run Bukkit work on the server thread and file work asynchronously with session-generation checks. + +- [x] **Step 3: Write failing command/permission/completion tests** + +Cover every first-slice path from the approved design, exact permissions, console-only catalog status exception, finite-number parsing, asset-kind completion, pagination, partial-path completion, and read-only display of preserved application actions. + +`PaperSessionResolver` must query the published BuildSystem API for the player's current `BuildWorld`; ordinary loaded Bukkit worlds are rejected. MockBukkit tests cover a missing BuildWorld, a valid build world, world change, and BuildSystem service loss. The hard plugin dependency makes a completely absent BuildSystem an enable-time failure rather than an ambiguous editor mode. + +- [x] **Step 4: Implement commands as adapters over common operations** + +Commands must not reconstruct DTOs independently. Use Adventure components and deterministic problem ordering. `/scene save` says saved, never published. + +- [x] **Step 5: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :paper:test +git diff --check +git add paper +git commit -m "feat(paper): expose scene editor commands" +``` + +Delivered locally as `2cf36d6`, with supporting Common hardening in `e66a808`, `c80ebae`, +`880193a`, and `db4a59f`. TDD covers transactional plugin lifecycle, BuildSystem service and exact +world resolution, scheduler shutdown races, lazy valid/absent/invalid scene bootstrap, atomic +first-command recovery, generation-bound reload/discard, all first-slice prop/NPC mutations, +owner-bound leases and administrative takeover, exact leaf permissions, kind- and +session-aware completion, pagination, finite-number rejection, preserved read-only application +actions, deterministic diagnostics, and local-only save wording. The final forced +`:common:spotlessCheck :common:test :paper:spotlessCheck :paper:test --rerun-tasks` verification +passed, and three independent review rounds ended with no P1/P2 findings. + +--- + +### Task 9: Add previews and lifecycle cleanup + +**Files:** +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/preview/PaperPreviewAdapter.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/preview/PreviewRegistry.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/PaperWorldLifecycleListener.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/PaperPlayerLifecycleListener.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/tool/RaySelection.kt` +- Create matching tests + +- [x] **Step 1: Write failing pure selection and adapter tests** + +Cover deterministic nearest-hit selection, generic prop/NPC placeholders, labels, per-viewer selected outline/axes/ID, non-persistent entities, plugin PDC keys, and a visible fallback when rendering fails. + +- [x] **Step 2: Implement server-thread preview reconciliation** + +Reconcile from immutable document snapshots. Never mutate documents from preview state. Discard stale async results by generation. + +- [x] **Step 3: Implement cleanup listeners** + +Quit/world change releases player selection and leases. World unload/plugin disable removes all preview entities, cancels tasks, releases leases, logs dirty state, and notifies online editors. + +- [x] **Step 4: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :common:test :paper:test +git diff --check +git add common paper +git commit -m "feat(paper): preview scene documents safely" +``` + +Delivered locally as `cd3f1c1`. Pure ray selection uses normalized world-unit distances and a +deterministic ID tie-breaker. Paper previews reconcile immutable generation snapshots into +non-persistent, plugin-tagged, viewer-only Display entities with generic prop/NPC bodies, labels, +selected outline/axes/ID, and a visible text fallback. Registry ownership is transactional, +viewer/world scoped, and retains failed removals for scoped retry. Quit, world-change, unload, and +disable cleanup release selections/leases, cancel preview work, remove entities, revoke active-save +capabilities before atomic replacement, log dirty sessions, and notify every online participant. +The final forced Common/Paper test and format run executed 25 of 26 tasks successfully; two +independent review rounds ended with no remaining P1/P2 findings. + +--- + +### Task 10: Add the editor tool + +**Files:** +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/tool/TransformComponent.kt` +- Create: `common/src/main/kotlin/gg/grounds/scene/editor/tool/TransformMath.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/tool/EditorTool.kt` +- Create: `paper/src/main/kotlin/gg/grounds/scene/editor/paper/tool/EditorToolListener.kt` +- Create matching tests + +- [x] **Step 1: Write failing transform-math tests** + +Cover X/Y/Z, yaw/pitch/roll, uniform scale, signed fine/normal/coarse steps, canonical rotation, positive scale rejection, and exact mutation equivalence with commands. + +- [x] **Step 2: Write failing listener tests** + +Cover exact PDC-tagged item identity, permissions/session/selection/lease gates, ray-select left click, component cycling, accepted wheel cancellation, and normal click/hotbar behavior outside active tool state. + +- [x] **Step 3: Implement the tool and action bar** + +Show element ID, component, value, step, dirty state, and lease time. Cancel `PlayerItemHeldEvent` only after a valid editor mutation is accepted. + +- [x] **Step 4: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-gradle ./gradlew --no-build-cache :common:test :paper:test +git diff --check +git add common paper +git commit -m "feat(paper): add scene editor tool" +``` + +Delivered locally as `73e9a4b`. The exact plugin-tagged Blaze Rod is available without inventing +a scene, while all input remains gated by BuildSystem world, permission, session, selection, and +owner lease. Main-hand ray selection uses rotated/scaled world hit bounds; right click cycles the +seven transform components; adjacent wheel events apply signed fine/normal/coarse command-equivalent +mutations and cancel only after acceptance. The action bar reports element, component/value, step, +dirty state, and lease time. Quit/world-change cleanup removes component state. The final forced +Common/Paper test and format run executed 25 of 26 tasks successfully, and two independent reviews +reported no remaining P1/P2 findings. + +--- + +### Task 11: Verify, merge, and release the editor API and Paper plugin + +**Repository:** plugin-scene-editor feature worktree + +- [x] **Step 1: Add the canonical end-to-end fixture** + +Create a scene through common mutations using both bootstrap assets, save it through the atomic repository, decode it with released `scene-format` 0.1.0, reload it into a fresh session, and assert byte-canonical equality. Package the Paper shadow JAR and rerun its contents contract. + +- [x] **Step 2: Run clean verification and review** + +```bash +GRADLE_USER_HOME=/tmp/scene-editor-release ./gradlew --no-build-cache clean check +git diff --check origin/main...HEAD +git status --short +``` + +Use `superpowers:requesting-code-review`; resolve all blocking findings and rerun verification. + +Local release preparation is complete through Step 2. Commit `af5fa84` adds the canonical fixture +against the exact production bootstrap catalog; signed commit `22ceb60` normalizes the generated +Windows wrapper so `git diff --check main...HEAD` is clean. A split clean-then-versioned-check run +completed all Common/Paper tests, format gates, and the Paper Shadow JAR for `0.1.0`. Both Maven +publications were installed locally and inspected: Common JAR/sources/Javadoc/POM and the +classifierless Paper Shadow JAR are present; `plugin.yml` reports `0.1.0`; no Bukkit, Paper, or +BuildSystem API classes are bundled. The Paper JAR and local Paper Maven artifact share SHA-256 +`50be2507ddee8fa896abd1571c202cdc63e5aa3a8f905e2546da85576579ef17`; the Common JAR SHA-256 is +`b86dc7f3bf589ab3eab990038c660c9d7bea6ae86e7562060cd08ebc1493f069`. Two independent whole-branch +reviews found no remaining P1/P2 after the wrapper normalization. + +- [x] **Step 3: Push, create the PR, and merge with the explicitly authorized pipeline exception** + +Keep any runner outage visible as an external gate. Do not replace missing protected checks with an unverifiable manual claim. + +The repository was created private and then explicitly changed to public. The fully locally verified +implementation was pushed and [plugin-scene-editor#1](https://github.com/groundsgg/plugin-scene-editor/pull/1) +was merged as `d66bbb0f27007d41ac0802d1f95ae90fca33697b`. The remote Java build was not used as +release evidence: the user explicitly authorized merging without waiting for the runner, while the +fresh local release check executed all 29 requested tasks successfully and `git diff --check` was +clean. Release Please bootstrap corrections were merged through +[plugin-scene-editor#6](https://github.com/groundsgg/plugin-scene-editor/pull/6) and +[plugin-scene-editor#7](https://github.com/groundsgg/plugin-scene-editor/pull/7). + +- [ ] **Step 4: Release the first editor version** + +Merge the Release Please PR and verify the release tag, the published `common` Maven coordinate, and the runnable Paper shadow artifact. Download both from the release/package service and rerun the Java API and JAR-contents smoke checks against the published files. + +Release Please merged [plugin-scene-editor#5](https://github.com/groundsgg/plugin-scene-editor/pull/5) +as `bef5ee9eef23c0fda85bab86149e5cb1f06e2177` and created +[v0.1.0](https://github.com/groundsgg/plugin-scene-editor/releases/tag/v0.1.0) as `grounds-bot`. +Keep this step open: automatic publish run +[33105049938](https://github.com/groundsgg/plugin-scene-editor/actions/runs/33105049938) failed in +the build before publishing because it could not resolve the still-unpublished +`gg.grounds:resourcepacks-catalog:0.6.0`. No editor Maven coordinate or remote runnable Paper +artifact may be claimed until the upstream GitHub Packages `402 Payment Required` gate is resolved +and Release Please's tag-triggered publish workflow is rerun successfully. + +- [ ] **Step 5: Record exact consumer pins** + +Capture editor version, merge SHA, release URL, common coordinate/checksum, Paper artifact URL/checksum, and workflow IDs. Task 12 must use these exact released values. + +--- + +### Task 12: Guard dirty scenes before GroundsMaps push + +**Repository:** `/home/lukas/grounds/buildsystem` using a new worktree based on the branch containing async map publication + +**Files:** +- Modify: `settings.gradle.kts` +- Modify: `buildsystem-grounds/build.gradle.kts` +- Modify: `buildsystem-grounds/src/main/java/gg/grounds/buildsystem/command/MapCommand.java` +- Create: `buildsystem-grounds/src/main/java/gg/grounds/buildsystem/command/SceneEditorPushGuard.java` +- Create: `buildsystem-grounds/src/test/java/gg/grounds/buildsystem/command/SceneEditorPushGuardTest.java` +- Modify: archive contract test that owns `WorldArchive` + +- [ ] **Step 1: Add dependency and descriptor contract tests** + +Add the Grounds GitHub Packages repository to `dependencyResolutionManagement`, using `github.user`/`github.token` Gradle properties with `GITHUB_ACTOR`/`GITHUB_TOKEN` CI fallback and never logging either value. Prove the exact released common POM/JAR resolves before changing production code. + +Pin that released common API as `compileOnly` and `testImplementation`. Add `GroundsSceneEditor` to the generated GroundsMaps `softDepend`. Test that the shadow JAR does not bundle `SceneEditStatus` and that `plugin.yml` contains the soft dependency. + +- [ ] **Step 2: Write failing guard tests** + +Cover absent editor plugin/API, absent service, clean session, dirty session, and a provider failure. A dirty session must stop before Bukkit world save, archive creation, link creation, or registry calls. A provider failure must log and fail closed, because silently publishing potentially dirty work is unsafe. + +- [ ] **Step 3: Implement a linkage-safe optional adapter** + +Keep typed `SceneEditStatus` access in `SceneEditorPushGuard`, and load/invoke that adapter only after `PluginManager` confirms `GroundsSceneEditor` is enabled. The no-editor test must prove `MapCommand` loads and pushes without `NoClassDefFoundError` when the compile-only API is absent. Query on the server thread. + +- [ ] **Step 4: Wire the guard at the start of push** + +Call it at the beginning of the private push path, before all side effects. Send: `You have unsaved scene edits. Run /scene save before /map push.` for dirty state. + +- [ ] **Step 5: Retain the archive-root contract** + +Extend the existing archive test to prove `scene.json` remains a direct archive-root entry and its bytes are unchanged. + +- [ ] **Step 6: Verify and commit** + +```bash +GRADLE_USER_HOME=/tmp/scene-buildsystem-gradle ./gradlew --no-build-cache :buildsystem-grounds:test :buildsystem-grounds:shadowJar +unzip -p build/libs/GroundsMaps-*.jar plugin.yml | rg 'softdepend|GroundsSceneEditor' +jar tf build/libs/GroundsMaps-*.jar | rg 'SceneEditStatus' && exit 1 || true +git diff --check +git add buildsystem-grounds +git commit -m "feat(maps): block pushes with dirty scenes" +``` + +--- + +### Task 13: Run final cross-repository acceptance and prepare the buildserver + +**Repositories:** plugin-scene-editor, resourcepacks, buildsystem, and the service-maps feature worktree when its derivation implementation exists + +**Buildserver files after acceptance:** +- Modify: `containers/buildserver/Dockerfile` +- Modify: `containers/buildserver/README.md` +- Create: `containers/scripts/test-buildserver-scene-editor-pin.sh` +- Modify: `containers/.github/workflows/ci.yml` +- Modify, only when deployment resumes: `deploy/environments/stage/components/buildserver/values.yaml` + +- [ ] **Step 1: Verify archive and published artifact contracts together** + +Run the canonical editor fixture against the published 0.6.0 catalog and released editor artifacts. Archive its world with BuildSystem and assert that root `scene.json` is byte-identical and still decodes with released `scene-format` 0.1.0. + +- [ ] **Step 2: Exercise downstream contracts locally** + +If the independent service-maps scene-derivation branch has landed production `SceneDeriver`, feed the bytes through it using the exact 0.6.0 asset catalog and empty `grounds:actions@1`, then assert representative derived output without contacting the cluster. If that implementation has not landed, record this as a non-blocking downstream check; released scene-format decoding and BuildSystem archive-root verification remain mandatory. + +- [ ] **Step 3: Run full clean builds** + +```bash +GRADLE_USER_HOME=/tmp/scene-resourcepacks-final ./gradlew --no-build-cache clean check +GRADLE_USER_HOME=/tmp/scene-editor-final ./gradlew --no-build-cache clean check +GRADLE_USER_HOME=/tmp/scene-buildsystem-final ./gradlew --no-build-cache clean check +``` + +Run each command in its repository. Also run `git diff --check`, inspect artifact contents, and request final code review. + +- [ ] **Step 4: Merge and publish the remaining consumers in dependency order** + +1. Confirm the already released resourcepacks 0.6.0 and editor artifacts. +2. Merge and publish BuildSystem/GroundsMaps with the exact editor-common pin. +3. In a fresh containers worktree, pin the new BuildSystem base image by immutable digest and add exact `SCENE_EDITOR_URL`/`SCENE_EDITOR_SHA256` build arguments. Download `GroundsSceneEditor.jar` in the existing verified plugin stage and copy it to `/app/plugins`. +4. Add and wire `scripts/test-buildserver-scene-editor-pin.sh`; assert the Dockerfile/README pins agree and the built image contains exactly one editor JAR alongside BuildSystem and GroundsMaps. +5. Merge containers and publish the derived `ghcr.io/groundsgg/buildserver` image by immutable digest. +6. Only after cluster work resumes, update `deploy/environments/stage/components/buildserver/values.yaml` to that exact derived image tag/digest and retain its parser/Helm tests. + +If pipelines remain unavailable, merge only changes whose protected checks can complete, retain verified local branches for downstream pins, and do not fabricate artifact coordinates. + +- [ ] **Step 5: Update the masterplan with evidence** + +Mark Phase 6 items complete only for merged code, published artifacts, and verified local cross-repo contracts. Leave cluster deployment and Stage acceptance explicitly blocked by the external cluster/runner outage. Include exact PRs, SHAs, tags, artifact digests, and queued workflow IDs. diff --git a/superpowers/specs/2026-08-27-paper-scene-editor-design.md b/superpowers/specs/2026-08-27-paper-scene-editor-design.md new file mode 100644 index 0000000..9b95cab --- /dev/null +++ b/superpowers/specs/2026-08-27-paper-scene-editor-design.md @@ -0,0 +1,413 @@ +--- +title: "Paper scene editor design" +description: "Architecture and behavioral contract for authoring validated scene.json files on Grounds build servers." +--- + +# Paper scene editor design + +## Status + +Approved for implementation planning on 2026-08-27. + +This specification defines Phase 6 of the Scene, NPC, and Resource Pack platform. It introduces a Paper-only `plugin-scene-editor` product that lets builders create, preview, validate, and save the optional `scene.json` belonging to a build world. Cluster deployment and Phase 5 Stage acceptance are independent and may remain unavailable while this plugin is developed and tested locally. + +## Outcome + +A permitted builder can enter a loaded build world, create or open its scene, add props and NPCs, select and transform them, inspect validation problems, undo or redo changes, and atomically save canonical `scene.json` at the world root. The existing `/map push` flow later archives that file and submits it to service-maps for authoritative derivation. + +The first vertical slice ends when a scene containing props and NPCs can be created, transformed, saved, decoded again, and proven byte-canonical and catalog-compatible. Trigger-chain authoring and composite-rig authoring build on the same architecture in later slices. + +## Repository and modules + +Create a dedicated `groundsgg/plugin-scene-editor` repository with two modules: + +| Module | Responsibility | +| --- | --- | +| `common` | Immutable editor sessions, mutations, bounded history, leases, repository abstraction, catalog binding, validation, and save eligibility. It has no Bukkit dependency. | +| `paper` | JavaPlugin lifecycle, world/session discovery, commands, permissions, tab completion, Paper previews, editor-tool input, Adventure feedback, and main-thread scheduling. | + +The repository uses `gg.grounds.base-conventions` at the root and `gg.grounds.paper-conventions` for the Paper artifact. It targets JDK 25, Kotlin, Paper 26.1.2, `library-scene` 0.1.0, and the exact catalog artifact prepared below. The initial product is Paper-only; no Velocity or Minestom adapter is created. + +The Paper module produces the deployable shadow JAR. It uses `plugin.yml`, `${VERSION}` resource expansion, and the existing Grounds GitHub Packages/release conventions. + +## Dependencies and ownership + +`library-scene:scene-format` is the only scene schema, codec, and validation authority. The editor never creates a second JSON model and never parses or writes scene JSON through another serializer. + +The editor consumes these public operations: + +- `SceneJson.decode(ByteArray)` +- `SceneJson.encode(SceneDocument)` +- `SceneValidation.validateIntrinsic(SceneDocument)` +- `SceneValidation.validateCatalogs(SceneDocument, AssetCatalog, ActionCatalog)` + +The plugin treats all scene-format DTOs as immutable. Each edit replaces the affected element and constructs a new `SceneDocument`. It does not mutate collection instances retained by existing documents. + +The editor owns authoring state and Paper previews. BuildSystem owns build worlds and `/map push`. The Paper module consumes published `de.eintosti:buildsystem-api:4.0.0`, declares a hard `BuildSystem` plugin dependency, and opens sessions only for worlds resolved as `BuildWorld`. service-maps owns authoritative derivation, publication, pins, and rollback. The editor never calls R2, Kubernetes, or service-maps directly in the first release. + +## Scene file contract + +The file path for a loaded world is exactly: + +```text +/scene.json +``` + +The editor never stores the file below `grounds/` or a plugin-data directory. The existing `WorldArchive` packs the world folder and therefore preserves `scene.json` at archive root, which is the only location the derive worker recognizes. + +The repository applies these load rules: + +1. If `scene.json` is absent, the session starts without a document. The builder must explicitly create a scene before element commands become available. +2. If decoding succeeds, the canonical document becomes the session base snapshot. +3. If decoding fails, the original bytes remain untouched. The session exposes ordered `SceneProblem` diagnostics and enters read-only recovery state. +4. Recovery requires an explicit command that moves the invalid file to a timestamped sibling backup before creating a new document. The plugin never silently overwrites invalid authored content. + +Saving uses `SceneJson.encode`. A successful save writes the returned canonical bytes to a sibling temporary file, flushes the file, and replaces `scene.json` atomically where the filesystem supports atomic moves. If an atomic move is unsupported, the operation fails and retains the previous file; it does not fall back to a non-atomic overwrite. + +The file repository serializes saves per world. It refuses a save if the on-disk file changed since the session loaded or last saved it. The comparison uses the SHA-256 of the exact loaded/saved bytes, with absence represented separately from an empty file. + +The editor repository is the sole supported writer while an editor save or recovery operation is +running. Repository instances in the same server process coordinate through one normalized-path +commit lock and recheck the exact fingerprint inside the conditional atomic-move boundary. Java +NIO does not provide a portable compare-and-swap primitive for file contents: a non-cooperating +external process can still race in the final interval between that recheck and `ATOMIC_MOVE`. +Manual or third-party writes during an active editor operation are therefore outside the supported +coordination contract; changes present before the conditional commit are rejected and preserved. + +## Catalog model + +The first release uses versioned catalog snapshots from its own classpath. It does not invent a catalog-file codec or add a catalog-discovery API. + +The currently released `gg.grounds:resourcepacks-catalog:0.5.1` contains no assets and cannot satisfy the vertical slice. Phase 6 therefore begins with a resourcepacks prerequisite: make catalog entries declarative, add matching pack models for one bootstrap `PROP` (`grounds:editor/marker`) and one bootstrap `NPC_BODY` (`grounds:editor/guide`), and release them together as resourcepacks 0.6.0. Both entries have explicit default bounds, and product validation proves that each catalog key has the model path required by its asset kind. + +The deployable plugin then pins `gg.grounds:resourcepacks-catalog:0.6.0` and obtains the asset snapshot from `GroundsAssetCatalog.catalog`. Its exact catalog reference becomes the asset-catalog reference of every new `SceneDocument`. Updating the asset catalog requires updating this dependency and releasing a new plugin build, so the editor and resource-pack declarations cannot drift at runtime. Unit tests may inject richer in-memory catalogs, but production commands never expose test-only assets. + +Until an action-catalog artifact or discovery contract exists, the plugin constructs one local immutable `ActionCatalog(CatalogId("grounds:actions"), "1", emptyMap())`. Its reference becomes the action-catalog reference of every new document. The plugin exposes catalog status but has no runtime catalog reload command in this release. + +The current Grounds action catalog is `grounds:actions@1` and contains no application actions. The editor therefore: + +- preserves decodable application actions already present in a document; +- displays them read-only; +- rejects creation or mutation of an application action unless the active action catalog defines its ID and argument contract; +- never silently removes an unknown action. + +Catalog compatibility controls save eligibility. A scene remains editable when its pins do not match the classpath catalogs, but the editor labels it `catalog-unverified` and refuses normal save. A recovery export may write canonical intrinsic-valid bytes below the plugin's diagnostic-export directory, never to the live world `scene.json`. If the pinned asset catalog cannot initialize, plugin enable fails instead of starting without its required validation authority. + +The first release has no catalog-pin migration command. A mismatched document is inspectable, editable in memory, and exportable, but remains unsavable until an editor build carrying its exact catalog is installed. Explicit catalog migration is deferred because it needs asset-by-asset compatibility rules, not a blind reference rewrite. + +## Session model + +There is at most one `EditorSession` per loaded Bukkit world. A session contains: + +- world identity and exact world-root path; +- base document and current document; +- base file fingerprint; +- selected element per participating player; +- active transform component per participating player; +- bounded undo and redo history; +- element edit leases; +- validation state; +- dirty state and last successful save time. + +Selections are per player. Documents, history, validation, and leases are shared per world. + +The session service creates sessions lazily when a permitted player runs a scene command in a loaded build world. It closes a session when the world unloads or the plugin disables. Closing removes every preview entity, cancels tasks, releases leases, and discards unsaved memory only after logging and notifying online editors that unsaved changes existed. + +## Mutations and history + +Every authoring operation is a typed `SceneMutation` with: + +- a stable mutation name for audit and feedback; +- the acting player UUID; +- the target element ID when applicable; +- an `apply(SceneDocument)` operation returning either a replacement document or structured rejection; +- no Bukkit or filesystem dependency. + +The session performs a mutation in this order: + +1. Verify permission, session state, selection, and lease. +2. Apply the mutation to the current immutable document. +3. Run intrinsic validation. +4. Record the previous document in history only when the mutation succeeds. +5. Clear redo history. +6. Recompute catalog validation and save eligibility. +7. Publish one document-changed event for the Paper preview adapter. + +History stores complete immutable `SceneDocument` snapshots. It is capped at 100 entries per world. Undo and redo are themselves serialized session operations and do not create additional history entries. A save does not clear history; it updates the base fingerprint and dirty comparison point. + +## Edit leases + +A lease protects one scene element from concurrent edits. It contains the element ID, owning player UUID, acquisition time, and last activity time. + +- Selecting an unleased element acquires its lease. +- Selecting an element already leased by another player shows the owner and refuses mutation. +- Every successful mutation renews the lease. +- A lease expires after 120 seconds without activity. +- Deselect, world change, disconnect, plugin disable, or element deletion releases the lease. +- Administrative permission `grounds.scene.lease.override` allows an explicit lease takeover command; normal selection never steals a lease. + +Document-level operations such as save, undo, redo, scene creation, and recovery are serialized by the session and do not require leasing every element. + +## Commands + +The root command is `/scene`. Commands use explicit hierarchical paths, declare matching Bukkit permissions, and provide context-aware tab completion. Partial paths list valid next subcommands rather than returning an empty response. + +The first vertical slice provides: + +```text +/scene create +/scene info +/scene validate +/scene save +/scene reload +/scene history +/scene undo [steps] +/scene redo [steps] +/scene recovery backup-and-create +/scene recovery export +/scene recovery discard-and-reload confirm +/scene tool give + +/scene prop list +/scene prop create +/scene prop select +/scene prop position set +/scene prop position here +/scene prop position add +/scene prop rotation set +/scene prop rotation add +/scene prop scale set +/scene prop clone +/scene prop remove + +/scene npc list +/scene npc create +/scene npc select +/scene npc position set +/scene npc position here +/scene npc position add +/scene npc rotation set +/scene npc rotation add +/scene npc scale set +/scene npc label set +/scene npc clone +/scene npc remove + +/scene lease status +/scene lease release +/scene catalogs status +``` + +Commands execute only for players in loaded build worlds except catalog status, which authorized console senders may use. Coordinates and transforms reject non-finite values before constructing scene DTOs. Asset completion lists only catalog entries of the required kind. + +`/scene reload` refuses to discard a dirty document. After a concurrent disk change, the builder may first preserve the current intrinsic-valid bytes with `/scene recovery export`, then explicitly replace the in-memory document from disk with `/scene recovery discard-and-reload confirm`. The literal `confirm` argument is required and the command reports the discarded scene ID and fingerprint. Diagnostic exports use generated collision-resistant names; commands never accept arbitrary output paths. + +Create mutations use deterministic defaults. Both prop and NPC creation place the element at the executing player's current position with the player's yaw, zero pitch and roll, unit scale, no group, visible state, automatic activation, and no initial animation. A prop needs no additional defaults. An NPC starts with no label, label offset `(0, 2.25, 0)`, fixed look behavior, no proximity sensor, and no trigger bindings. Its interaction bounds come from the selected `NPC_BODY` catalog entry; NPC creation fails if that entry has no default bounds. + +Permissions mirror paths beneath `grounds.scene`, with broader convenience nodes such as `grounds.scene.edit`, `grounds.scene.save`, `grounds.scene.recovery`, `grounds.scene.catalogs.status`, and `grounds.scene.lease.override`. Server-side permission checks remain authoritative even when tab completion hides inaccessible paths. + +Trigger-chain and CompositeProp-rig commands are excluded from the first vertical slice. Their future command roots are reserved as `/scene npc trigger ...` and `/scene composite ...`. + +## Paper preview + +The Paper adapter renders a non-authoritative preview from the current document. Preview entities are marked non-persistent, tagged with plugin-owned persistent-data keys for identification while alive, and never saved into the world. + +The first slice renders: + +- props and NPC bodies as deterministic generic display-entity or bounding-box placeholders keyed by asset kind; +- labels as text displays; +- the selected element with an outline, axis markers, and an ID label visible only to the selecting player where Paper permits per-viewer visibility. + +Preview rendering never changes the document. Asset IDs remain visible so a placeholder can be correlated with its catalog entry. Faithful model previews wait for a standardized `editorMetadata` key contract; the first release does not interpret unspecified metadata keys. + +All Bukkit entity creation, mutation, and removal runs on the server thread. File and catalog I/O runs asynchronously, returning immutable results that the server thread applies only if the session generation still matches. + +## Editor tool + +The plugin gives an authorized builder a named editor tool through an explicit command. Tool behavior is active only when all of these are true: + +- the player holds that exact plugin-tagged item; +- the player has an active session and selection; +- the selected element's lease belongs to the player; +- the player has edit permission. + +Input mapping: + +| Input | Result | +| --- | --- | +| Left click | Ray-select the nearest preview element. | +| Right click | Select the next transform component. | +| Sneak + right click | Select the previous transform component. | +| Mouse wheel | Apply one signed step and cancel the held-slot change. | +| Sneak + mouse wheel | Apply a fine step. | +| Sprint + mouse wheel | Apply a coarse step. | + +Step sizes: + +| Value | Fine | Normal | Coarse | +| --- | ---: | ---: | ---: | +| Position | 0.01 | 0.1 | 1 block | +| Rotation | 1 degree | 5 degrees | 15 degrees | +| Scale | 0.01 | 0.05 | 0.25 | + +Position cycles through X, Y, and Z. Rotation cycles through yaw, pitch, and roll. Scale is uniform in the first slice. The action bar shows element ID, property, current value, step size, dirty state, and lease time remaining. + +Outside active tool state, clicks and hotbar selection behave normally. The listener cancels `PlayerItemHeldEvent` only after it has accepted a valid editor step. + +## Validation and save eligibility + +The session maintains separate intrinsic and catalog validation results. + +`save` succeeds only when: + +- a document exists; +- the session is not in invalid-file recovery state; +- intrinsic validation has no problems; +- active catalogs exactly match the document's references; +- catalog validation has no problems; +- the disk fingerprint still matches the session base; +- no save is already running. + +Validation output groups problems by element and field path, preserves deterministic ordering, and paginates long output. `validate` never mutates the document. + +Dirty state means the current document differs from the last successfully saved document. Canonical encoded bytes are the comparison authority; object identity is not. + +## BuildSystem publish integration + +The editor does not replace or wrap `/map push` in the first release. Builders save first and then use the existing command. + +The editor's `common` module publishes a Java-friendly read-only integration interface: + +```java +public interface SceneEditStatus { + boolean hasUnsavedChanges(UUID worldId); +} +``` + +The Paper plugin is named `GroundsSceneEditor`, embeds `common` in its deployable JAR without relocating its public API, and registers one implementation through Bukkit's `ServicesManager`. The `buildsystem-grounds` module adds the published `plugin-scene-editor:common` artifact as `compileOnly`, adds `GroundsSceneEditor` to its generated `plugin.yml` `softdepend`, and queries the service in `MapCommand` before `/map push` starts world-save, archive, or upload work. It does not bundle its compile-only copy, so Paper resolves the shared API class from the declared plugin dependency. + +If the service reports a dirty session, `MapCommand` rejects the push with an actionable message naming `/scene save`. An absent plugin or service preserves today's push behavior. The interface exposes no mutation, save, or publication operation, and service-maps remains uninvolved. + +After save, `/map push` continues to: + +1. save the Bukkit world; +2. archive the exact world folder; +3. include root `scene.json`; +4. upload the source archive; +5. commit a version with `derive=true`; +6. poll the exact version status. + +The editor reports only local save state. It never displays “published” based on a successful file write. + +## Failure behavior + +| Failure | Required behavior | +| --- | --- | +| Invalid existing JSON | Keep original bytes, enter recovery state, show ordered decode problems. | +| Document pins mismatch classpath catalogs | Allow editing, mark catalog-unverified, reject normal save. | +| Required asset catalog fails to initialize | Fail plugin enable; do not expose an unvalidated editor session. | +| Invalid mutation | Leave document, history, preview, and dirty state unchanged. | +| Preview failure | Keep document, show placeholder/problem, permit later refresh. | +| Concurrent disk change | Reject save; require optional export followed by confirmed discard-and-reload. | +| Atomic replace unsupported | Reject save and retain the previous file. | +| Async result from stale session | Discard it without touching the current session or preview. | +| Player disconnect | Release selection and leases; keep shared document session alive. | +| World unload or plugin disable | Remove previews, cancel tasks, release leases, report unsaved state. | + +Failures use Adventure components with concise summaries and expandable command-driven detail. Logs include world, scene, element, and problem code where available, but never dump entire scene files or catalog payloads. + +## Security boundaries + +- The plugin represents declarative scene data only; it cannot encode arbitrary commands or scripts. +- Application-action authoring requires an exact active action-catalog definition. +- Scene paths are derived from operator-owned loaded worlds and normalized before access. Diagnostic exports remain below the plugin data directory. +- The repository never follows a symlink that would place `scene.json` outside the exact world root. +- Recovery backups remain siblings of `scene.json` and use collision-resistant timestamps. +- Commands recheck permissions at execution time. +- Preview entities carry no executable behavior and never survive plugin shutdown. +- The editor does not receive service credentials, R2 credentials, Kubernetes tokens, or presigned URLs. + +## Lifecycle + +`onEnable` performs only composition and registration: + +1. Construct catalog providers from the pinned classpath asset catalog and local empty action catalog. +2. Construct common services. +3. Register `SceneEditStatus` through Bukkit's ServicesManager. +4. Register command executor, tab completer, and listeners. +5. Schedule lease expiry and preview maintenance through one shared task. + +`onDisable` closes registrations and services in reverse order. Runtime registrations return `AutoCloseable` handles where practical. No global singleton exposes mutable editor state. + +## Testing strategy + +### Common unit tests + +- Create, replace, clone, and remove prop/NPC mutations. +- Reject duplicate IDs, non-finite transforms, invalid scale, and wrong asset kind. +- Apply the exact prop/NPC creation defaults and reject an NPC body without default bounds. +- Preserve unchanged documents after rejected mutations. +- Bound history at 100 snapshots and prove deterministic undo/redo. +- Acquire, renew, expire, release, and administratively take over leases. +- Separate per-player selection from shared document state. +- Compute dirty state from canonical bytes. +- Preserve invalid source bytes and require explicit recovery. + +### File tests + +- Load absent, valid, and invalid `scene.json` from temporary world roots. +- Prove canonical encode/decode round trips with `scene-testkit`. +- Prove save uses the exact root path. +- Prove atomic replacement retains the old file on injected failure. +- Reject symlink escape and concurrent file modification. +- Prove recovery creates a byte-identical backup before a new document. + +### Paper tests + +Use MockBukkit where it supports the required Paper behavior: + +- plugin lifecycle and service registration; +- command permissions and hierarchical completion; +- player/world session resolution; +- selection and lease cleanup on quit/world change; +- editor-tool identity and inactive hotbar behavior; +- accepted wheel input cancellation and correct step sizes; +- main-thread preview application and stale async-result rejection. + +Use focused adapter tests with mocked Paper interfaces when MockBukkit does not implement display entities or `PlayerItemHeldEvent` precisely enough. Pure ray selection and transform math stay in `common`. + +### Cross-repository contracts + +- Build a world archive and assert `scene.json` remains at archive root. +- Build resourcepacks 0.6.0 and prove both bootstrap catalog entries have their required pack model paths. +- Decode the editor output through released `scene-format` 0.1.0. +- Run representative output through service-maps `SceneDeriver` with fake catalog resolution. +- Prove the editor does not claim publish success and blocks dirty-session push through the optional integration hook. + +Cluster, Keycloak, presigned upload, R2 promotion, Kubernetes Job, and asynchronous service acceptance remain Phase 5 Stage tests and are not prerequisites for merging local editor logic. + +## Acceptance criteria + +- A builder can create a scene in a loaded build world without editing JSON. +- A builder can create, select, move, rotate, scale, clone, and delete one prop and one NPC. +- Command and editor-tool edits produce the same canonical document mutations. +- Two builders can work in one scene without editing the same leased element concurrently. +- Undo and redo restore exact earlier canonical documents. +- Invalid existing files and concurrent external edits cannot be overwritten silently. +- Normal save succeeds only with intrinsic-valid, catalog-compatible content. +- Saved bytes decode canonically through `scene-format` 0.1.0. +- Saved `scene.json` resides at the exact world root and enters the existing map archive unchanged. +- Plugin shutdown removes all previews, tasks, selections, and leases. +- The product requires no cluster, R2, or service credential to run its local editor functions. + +## Deferred slices + +- Trigger-chain authoring for safe scene actions and catalog-defined application actions. +- CompositeProp creation, part hierarchy, local transforms, and rig preview. +- Animation timeline editing. +- A centralized asset/action catalog discovery API. +- Explicit catalog-pin migration backed by asset compatibility rules. +- Standardized editor-preview metadata and faithful resource-pack model previews. +- Automatic invocation or orchestration of `/map push`. +- Displaying authoritative service-maps derive and publication status in the editor. +- Phase 7 Minestom runtime behavior and Phase 8 portal status UI.