Fantasy Stick
Development in progress. Manual character crafting is registered; fantasy sessions and the full RPG are not ready for deployment.
Fantasy Stick is the proposed transferable-character RPG plugin for Social Minecraft. Its approved design uses Paper character sheets, native equipment storage and a book/basic-stick/seven-ability hotbar. Crafting is now assembled; fantasy-mode activation and most gameplay integrations remain planned. The server-independent progression core now tests initial stats/points, stat purchases, CON's equal maximum/current-health gain, capped XP and MAX presentation. YAML persistence, backend death/revival transitions and a native signed-book factory are also present. Saved-sheet crafting is wired; anvil-name persistence now observes native accepted takes; health, session and spell interactions remain incomplete.
Build and verify
- Java 25 toolchain (Gradle can resolve it), Gradle wrapper 9.6.1.
- Purpur API/runtime
26.2.build.2618-stable;plugin.ymldeclares API26.2. - Node.js 20 or later and npm for the TypeScript release-tool tests.
./gradlew clean check jar
./gradlew check jar -PreleaseVersion=0.1.0
check runs Java packaging tests, TypeScript release-client/credential-helper tests, and a native compatibility test against the checksum-verified Purpur runtime. Native preparation downloads and patches the declared runtime and loads vanilla registries/components without starting a listening server or creating a gameplay world. The item tests decode signed written books with Minecraft's real codec, without a plugin instance or YAML lookup. A clean build repeats that preparation. Dependencies and runtime downloads require network access.
Build output is build/libs/purpur-fantasy-stick-<version>.jar. Server APIs, tests, credentials, and Node tooling are not bundled in the plugin. Runtime identity is FantasyStick, entrypoint games.dmg.fantasystick.FantasyStickPlugin, data folder FantasyStick, namespace fantasystick. Startup registers the manual crafting controller and automated-Crafter guard before the character-sheet recipe. Commands, permissions, fantasy sessions and spells remain absent.
Character storage and keepsakes
YamlCharacterRepository stores schema-version-1 records at <plugin data directory>/characters/<character UUID>.yml. Character identity, name, progress, seven-slot loadout, ability cooldowns, explicit dead flag and latest death ID/time are preserved; the record has no holder-owned equipment or ordinary-health/nutrition snapshot. Integer values and decimal health retain exact precision, and timestamps use UTC RFC 3339 strings. Unknown learned ability IDs and unlearned cooldowns survive reload.
The file adapter uses safe YAML construction, duplicate-key rejection, bounded reads (1 MiB per record), typed validation, UUID filenames and no symlinked record reads. Replacement writes a complete temporary file, flushes it and atomically renames it; expected revisions and transient OS file locks reject duplicate/stale writers. Empty .lock files are coordination files, not administrator-unlock flags. Unsupported atomic replacement fails rather than falling back to a partial overwrite. Malformed files are retained, not reset.
CharacterStorage queues disk operations on its persistence worker and acknowledges successful writes only after commit. Its backend restoration checks the saved dead flag, matching current death ID and death age strictly under 24 hours at the request/selection time. It preserves progress/cooldowns and restores full character health; old books do not become eligible after a later death. This method is not a complete Revive spell: the engine must still validate the caster and native dialog, spend its cooldown, recheck item possession and perform physical conversion safely.
TailBooks creates vanilla WRITTEN_BOOK items with static pages. The full character name remains in display/pages; the native title is bounded to vanilla's 32 UTF-16 units without splitting a surrogate pair. fantasystick:tail_version, character_id and death_id identify the saved record. Pages/title are never read as authoritative stats or revival eligibility. Plugin absence does not remove the vanilla written-book contents. The factory does not yet deliver books from a death event.
File-level atomic replacement is not atomicity with Minecraft inventory/player saves. This checkpoint does not implement cross-save reconciliation, an administrator lock, automatic overflow drops or player/session persistence.
Craft preparation and conservative cleanup
CharacterStorage.prepareCraft accepts an immutable batch of distinct fresh character records and returns a process-local CraftPreparation handle. Allocation/configured starting points remain the caller's responsibility. Retain this handle until delivery or abandonment: ready() returns an observation copy, and cancelling/timing out that future does not cancel the owned operation. Use abandon() to invalidate the attempt, including while writes are queued/in progress. Writes acknowledge the complete prepared batch; claimForDelivery() exposes it once, immediately before native delivery. Claiming permanently rules out cleanup, even if later delivery throws.
Abandonment skips remaining queued writes and schedules cleanup for acknowledged, never-issued creations. Cleanup checks the original file identity/creation and modification attributes and exact bytes under the repository lock. Changed, replaced, corrupt, symlinked, busy or unidentifiable files are retained. Partial-save failure cleans only acknowledged creations and reports uncertain targets without deleting them. Shutdown rejection preserves records rather than inventing successful cleanup. Repeated cleanup cannot delete a later file. Restart never reconstructs cleanup authority from revision zero or scans records for automatic deletion/reissue.
Thirteen craft-persistence tests cover these backend boundaries, including worker timing, partial/post-rename failures, ownership changes, restart, shutdown, claim/abandon races, and mutated/cancelled observer futures. This backend alone is not native inventory delivery: the assembled controller separately contains vanilla consumption, validates current view/lifecycle/content and commits actual items. Filesystem locks coordinate cooperating writers; concurrent external filesystem mutation must be excluded/coordinated. File flush/atomic rename does not establish directory power-loss durability or Minecraft/plugin-save atomicity. All create/update/cleanup mutations now share one fail-fast characters/.records.lock coordination file, so abandoning new characters does not accumulate one lock file per ID. Read-only atomic-file loads remain unlocked. The file stays present but its OS lock is held only for an operation, not an administrator-unlock flag. Never unlink it while writers may use it. Old per-ID lock files are left intact; no new ones are created.
Upgrade boundary: stop all older per-ID-protocol writers before opening the same data directory with this shared-lock implementation. Mixing versions or hot-reloading while old worker writes remain queued is unsafe. YAML format and record filenames are unchanged; no deployment or existing-server data migration has been performed. Contention between cooperating repository instances fails without mutation and can be retried, rather than blocking a server tick or deleting coordination files.
Paper sheet and recipe definition
CharacterSheets renders one Paper sheet from a saved live record, with its current name and the exact offhand instruction. fantasystick:sheet_version (integer 1) and character_id (UUID string) identify the character, with no owner lock or duplicated progress snapshot. Identification is only a reference: it does not authorize entry or replace saved-state/session validation. Rendering from a dead record is refused.
CharacterFactory allocates a fresh UUID and randomized two-part character name when explicitly called. It reuses the progression initializer for zero stats, level 1/zero XP, configured unspent points and full base health, with empty abilities/loadout/cooldowns and no prior death. Name collisions are allowed; identity is independent. Construction and recipe previews do not invoke allocation. Unit tests check fresh defaults/configured arbitrary-precision points, request-time allocation, seeded name variation and same-name/different-ID characters. A native composition test prepares a planned batch through YAML and verifies individually named sheets/IDs after native serialization without consuming or delivering inventory items. The assembled craft-event controller now invokes this allocator only for accepted current attempts.
The fantasystick:character_sheet shapeless recipe definition contains one stick, paper, feather and ink sac. Character creation is manual player crafting only (inventory grid or crafting table). ManualCraftingGuard cancels this recipe's automated CrafterCraftEvent before native consumption/ejection, without touching other recipes or reversing cancellations. Startup installs this guard and the manual controller before exposing the recipe. Its preview has no character metadata and allocates no identity/name. Manual completion, bulk crafting, cancellation/save handling and accepted anvil-name persistence are registered. Presentation edits preserving metadata are tested, not a claim that complete anvil transactions work.
Manual craft planning (not delivery)
ManualCraftPlan reads real crafting inventories without mutation. It calculates one result for cursor/number/offhand clicks, distinct empty destination slots for shift-click, and single/bulk intent for Q/Ctrl-Q. It bounds quantity by ingredients and storage capacity, preserves native reverse storage order, rejects cancelled/non-craft/wrong-recipe/malformed input, and snapshots full item data rather than retaining mutable native stack mirrors. Returned leftovers are detached copies; matches detects changed inputs, result, recipe, cursor or planned destinations without overwriting intervening items.
This is not a transaction commit. Eleven native planner/composition tests use actual 2×2/3×3 crafting containers, player storage, menu-carried stacks and recipe holders, with the absent player/network boundary stubbed. They do not execute packet dispatch or vanilla result consumption. The assembled controller separately supplies one-use lifecycle tokens (a native menu may be reused after close/reopen), allocation/persistence wiring, reentrant update handling and delivery. Drop intent is not proof that a cancellable native drop succeeds. No recipe/listener registration, recovery policy or cross-save atomicity is added by this planner.
Fixed-destination craft delivery
ManualCraftDelivery can apply a validated plan and exact-sized saved batch to the cursor, selected hotbar/offhand slot or bulk storage destinations. It rechecks current contents before claiming delivery, constructs all sheets first, then debits fresh native input mirrors and places all outputs before triggering one matrix recomputation. This deliberately relies on the pinned Purpur 2618 mirror/count/setter behavior. No further adapter-planned writes occur after the prepare-callback boundary. Existing native setters may send packets; this is not packet-silent or exception/crash-atomic.
Nine native delivery tests cover normal/right-click, bulk and exact-slot delivery, partial ingredient metadata, empty first matrix cells, stale context refusal, mismatched/abandoned/reused batches, reentrancy, callback item changes, new-menu cursor safety and post-callback exceptions. The fixture uses actual containers/cursor stacks and intercepts slotsChanged; it does not execute a listening server's recipe manager or event dispatch. A mismatch refuses before consuming cleanup authority; an exception after claiming delivery conservatively retains records rather than replaying or rolling back.
The caller still must contain vanilla consumption, validate a one-use lifecycle/attempt token, schedule on the server thread, pass the player's actual current view and synchronize the appropriate client menu afterward. World-drop plans return UNSUPPORTED_ROUTE unchanged in this component; that is not a registered gameplay policy. The registered controller uses this component only for fixed destinations and routes Q/Ctrl-Q separately.
Q/Ctrl-Q craft delivery
Q/Ctrl-Q remain supported scope; no shortcut restriction was accepted or enabled. ManualDropDelivery executes saved drop plans one unit at a time, checking original contents minus completed debits before every next unit. PlayerSheetDrops creates an unspawned item, dispatches PlayerDropItemEvent, revalidates, and uses native Item.spawnAt for the cancellable ItemSpawnEvent path. It never uses inventory-source dropping or cancellation's return-to-inventory fallback. A successful unit consumes one ingredient set, then recomputes; cancellation stops the suffix without charging it.
Thirteen additional tests cover sequential/bulk/cancelled drops, exact remaining-input validation, stale context, changed/removed entities, same-batch reentry and exceptions. Tests combine real native matrices, serialized sheets, YAML and Paper's event dispatcher; world insertion itself remains an explicit API boundary double, supplemented by pinned-runtime bytecode inspection. No live world or packet dispatch is claimed.
After public callbacks, records are conservatively retained—including unused batch records. Repeated late cancellations can therefore accumulate records. A spawn callback can change inventory after an output escapes but before a safe debit; UNCONFIRMED means stop/report without speculative charging, deletion, rollback or replay. This is not arbitrary-plugin/cross-save atomicity. The controller must hold a per-attempt guard across callbacks (including against newly started attempts), revalidate actual current view/lifecycle, and synchronize the client. The registered controller supplies these guards and has native lifecycle coverage as described below.
Manual craft controller
ManualCraftingController contains vanilla handling before deferred work, allocates/saves accepted plans, returns delivery to an always-deferred server-thread callback and synchronizes the player's actual current inventory afterward. It uses attempt-object identity plus a delivery reservation that survives close/reopen and remains held through client-sync callbacks. It invalidates on close, accepted open, quit and disable; stale callbacks cannot clear a newer attempt, and rejected scheduling abandons owned unissued records without worker-side player access.
Twelve native controller tests cover saved deferred delivery, Q/Ctrl-Q routing with full storage, replay/reentrancy, menu reuse, client-sync reentry, pre-existing cancellation/unrelated recipes, rejected intents, allocation/save/scheduler failures, postclaim exceptions and retained-file reporting. Review reproduced a planning exception escaping before vanilla cancellation; capture now always cancels in finally. Async diagnostics are collected and asserted on the test thread rather than lost inside discarded futures. These tests invoke controller callbacks with real native grids/items/YAML and explicit player/scheduler/world boundaries, not a listening server. The controller checkpoint passed all 153 tests, audit and static analysis. The newer registration assembly is described below.
Scheduler injection must always enqueue (never run inline) or throw on rejection. Diagnostic callbacks must be nonthrowing, thread-safe and avoid native player access. The existing cancellation boolean cannot distinguish this handler's containment from a later plugin's independent veto; prior cancellations are preserved. No new Q/Ctrl-Q restriction, global inventory lock or automatic uncertain-delivery replay is introduced.
Startup, configuration and shutdown
The packaged FantasyStickPlugin now wires storage, allocation, deferred scheduling, native drop-event dispatch and logger diagnostics. Both protective listeners are installed before fantasystick:character_sheet is exposed. Recipe-key collisions and partial registration failures clean up this enable attempt without replacing an existing recipe. Shutdown invalidates attempts before closing storage, unregisters its listeners and removes its recipe; removal exceptions are reported while remaining cleanup continues.
An optional <plugin data>/config.yml setting controls new characters' initial points:
starting-points: 40
The default is 40; nonnegative arbitrary-precision integers are supported. Invalid values fail before gameplay registration. Existing saved characters are not rewritten. Configuration is read on enable; no reload command or automatic data migration is supplied.
Nine native lifecycle tests load the actual packaged entrypoint through a test configured classloader and native JavaPlugin.setEnabled, then register/dispatch through Paper. They cover registration ordering, configured points, all craft routes, distinct persisted sheets, installed Crafter cancellation, failed startup and shutdown/queued-callback cleanup. Server registry/scheduler/player/world insertion are explicit test ports; this is not a listening world/client test. The assembled checkpoint passes clean/incremental packaging, all 162 tests, audit and static analysis; remote publication is tracked separately. Fantasy-mode entry and the remaining RPG are not implemented.
Accepted anvil names
CharacterAnvilNames observes Purpur's non-cancellable accepted-take event, not anvil previews. Matching input/result character identities and changed displayed text are captured on the server thread; CharacterStorage.renameCharacter then changes only name/revision on the persistence worker against the latest live record. It preserves stats, XP, levels, points, health, loadout, cooldowns and historical death identity. Same-name requests do not rewrite; missing/dead records cannot be created or revived by renamed items. Clearing the custom name uses the native item-factory name, including an explicit item-name component where present.
Vanilla retains responsibility for item movement, XP charging and anvil damage. The inspected event fires at the start of native accepted-take processing, before that continuation; the observer does not make native completion contingent on YAML acknowledgment. Failure/crash may leave item presentation ahead of the saved name. The original saved progress is retained and captured IDs are logged; there is no automatic item/XP rollback, reissue or guaranteed retry.
Six backend tests, seven native observer tests and an added packaged-entrypoint registration test cover preservation, sequencing, failures, shutdown, preview/take separation, name fallbacks and later serialized sheet/Tail rendering. Native XP/damage execution and live clients are not exercised by these fixtures. Active overhead/chat/tab presentation and fantasy sessions remain incomplete. This slice passes 176 tests, clean/incremental packaging, three additional native-suite repeats, audit and static analysis. A controller test's leftover retry was explicitly drained before fixture reuse (12 focused stress runs pass); production shutdown remains nonblocking. Remote publication is tracked separately.
Native equipment adapter
EquipmentStorage preflights and moves nine hotbar/four armor slots into an actual named shulker in one free main-inventory slot. Item counts, metadata/damage/enchantments and legal nested contents remain native item data, accessible without this plugin. Eligibility is tested against the real vanilla shulker slot for every nonlegacy native item material; a main-inventory shulker does not prevent entry. Oversized or ineligible equipment and missing space produce unchanged refusals.
Version-1 box metadata uses fantasystick:storage_version, storage_owner_id, storage_session_id and storage_slots. Box positions 0–8 contain original hotbar slots 0–8; positions 9–12 contain boots, leggings, chest and head from player slots 36–39. These metadata fields are for identity/restoration, not the only copy of the items. Restoration searches main slots 9–35, so sorting relocation does not lose the box. Repeated requests, wrong identity, duplicate/malformed boxes, unexpected extra contents or occupied restoration slots do not overwrite/delete items. These are adapter error results, not a persistent administrator lock or completed recovery policy.
The caller must remove authenticated fantasy controls before restoration. One new head stack uses the slot freed by removing the box, even with a full main inventory; there is no drop fallback. The adapter does not yet route sheet/cursor events, emit entry warnings, manage character/player snapshots or register a playable session. Native tests use real player-slot storage with only the absent network recipient stubbed; client packets and live gameplay remain unverified.
CI and publication
Gitea CI verifies pushes/PRs; main pushes run semantic-release and can publish immutable versioned JARs. Source checkout is anonymous from the public repository; external action revisions are pinned to full commit SHAs. CI and actual release publication must both be verified for the exact source commit before any deployment preparation. The operator has provisioned RELEASE_TOKEN and the bootstrap is pushed. Do not infer successful CI, publication or credential validity from the secret name alone; record actual exact-commit workflow and artifact results in the canonical wiki.
The release credential must be a repository-write credential authorized for dmg/purpur-fantasy-stick. Provision it through the approved protected-file/stdin workflow; never paste it into chat, commit it, or supply it as a command argument.
- Git receives the credential through
git-credential.shusing Git's pipe-based credential protocol, restricted to HTTPS,git.garvis.dev, this exact repository path and usernamedmg. The helper ignores store/erase requests and never writes the token. Recognized semantic-release token environment names are unset so they cannot become credential-bearing Git URLs. HTTP redirects are disabled. This works even when semantic-release overridesGIT_ASKPASS. - API upload uses an in-process TypeScript client, a fixed repository URL, timeouts, redirect refusal and sanitized failures. Credentials are read from the release step's environment, never arguments or generated request files.
- Publication creates a draft, uploads/checks the expected asset name and size, then publishes. Failed calls are not automatically retried, existing releases/assets are not overwritten, and raw errors/response bodies are not logged.
- A failed run may leave a tag or draft. Inspect server-side state before proposing an approved recovery; rerunning does not imply a new release and must not overwrite an immutable asset. Verify downloaded bytes/checksum and embedded JAR version separately after publication.
- Release-client tests use synthetic credentials and stubbed HTTP responses. They do not prove a real token works or a release exists.
Canonical knowledge and delivery
Stories, design, open decisions and evidence are in the private SoMC wiki project. See AGENTS.md for the shared process. Source builds do not need wiki access.
The documented crafting/cleanup boundaries are automated-test evidence, not cross-save atomicity, complete RPG balance, live multiplayer or production-compatibility verification. Production rollout and GitOps changes remain separately approved; the development cycle never pushes GitOps.