Conversation
…d/rotation scaling Ships now derive their speed, acceleration, and rotation from a power-to-mass ratio based on sail composition. Wool blocks (3 pts) and banners (7 pts) provide sail power; every ship gets 2 free base points. The ratio linearly interpolates between absolute floors (1 block/sec speed, 30s/revolution rotation) and caps (1.5x default stats), with ratio 0.7 mapping to current defaults. Sails cap at ratio 0.8 — engines (not yet implemented) will be needed to push past it. Airship vertical speed scales with density magnitude rather than the horizontal ratio. Also adds ship_engine custom item definition (8 copper + blast furnace recipe) and updates the ship info display to show wool/banner counts, power ratio, and effective speed percentage.
Engine system: tagged blast furnace crafted from 8 copper + blast furnace. BlockPlaceEvent listener transfers PDC tag to TileState, vanilla smelting suppressed on engine blocks. Scanner and detection count engines via PDC check. Engine power (30 pts/engine) wired into the ratio calculation for both horizontal and airship vertical stats. Ship info display and detection chat messages now show engine count and engine-adjusted power ratio. Also extracts hardcoded help book content from ShipWheelMenu into HelpBookContent.java + help_book.yml, loaded once at plugin startup.
Adds the full engine fuel system on top of the engine detection foundation. Engines now require fuel to contribute power to the ship's ratio. Includes: - EngineMenuGUI: 3 fuel slots per engine, opened by right-clicking the engine block on an assembled ship. Validates fuel-only items, saves on close. - Fuel consumption: burns 1 tick per game tick while W held. Auto-consumes next fuel item when current burns out. Recomputes effective stats on change. - Smoke particles: CAMPFIRE_SIGNAL_SMOKE at fueled engine positions every 5 ticks while ship has a driver. - Fuel state persistence: per-engine fuel slots and burn ticks serialized to ship_wheels.yml via Base64-encoded ItemStack bytes. - Engine block indices and local positions tracked in ShipModel for click detection and particle spawning. - ShipInstance now holds a wheelData reference (set during assembly) for fuel state access in the physics loop. Note: DisplayShip.java and config.yml include unrelated changes from a concurrent Captain's Manual fix branch (shapeless recipes, help lore).
- Reload help book content on /blockships reload - Support shapeless recipes in ItemUtil.registerItemRecipe() - Replace verbose help lore (full book dump) with terse controls summary - Remove unused getHelpSections() from ShipWheelMenu - Fix speed percentage display: divide by sail cap (0.8) not default (0.7) so 100% means "max sails" rather than "default speed" - Bump floor-acceleration from 0.005 to 0.015 (less sluggish minimum)
…ne glint - Simplify ship info book hover to show only speed % (detailed breakdown moved to a new Ship Stats banner item at slot 20) - Speed % now uses sail cap (0.8) as 100% baseline instead of default ratio (0.7) — over 100% means engines are contributing - Stats banner shows wool/banner/engine counts, sail power with "capped at 80%" indicator when applicable, mass, power ratio, and speed % - Add config-driven enchantment glint support to CustomItem (enchant-glint field); ship_engine gets glint by default - Fix floor acceleration default mismatch (config says 0.015, code fallback said 0.005)
Previously only ship_wheel and ship kits were giveable. Now supports: - captains_manual (written help book) - any custom-items entry (ship_engine, balloon, etc.) Also adds tab completion for all giveable items and extracts item listing into shared helpers to avoid duplication.
New config keys added in plugin updates (like ship_engine, stats section) were invisible to existing servers because Bukkit's saveDefaultConfig() never overwrites an existing config.yml. Users had to manually delete their config to get new entries. migrateConfig() now runs at startup: loads the jar's bundled config, walks all leaf keys, and adds any that are missing from the user's config. Never overwrites existing values — customizations are preserved. Controlled by `auto-migrate-config: true` (opt-out by setting false).
Speed % now color-coded: red (<50%), gold (50-74%), yellow (75-99%), green (100-124%), aqua (125%+). Ships below 50% speed show a hint to add sails. Density line now combines the numeric value with a colored float status label (e.g., "1.33 (Floats well)") instead of separate lines. Removed surface offset from hover (redundant). Applied consistently across ship info hover, stats banner, and detection chat messages.
…er ref - Engine blocks now drop the custom ship engine item (with PDC + glint) instead of a vanilla blast furnace when broken - Add totalPositiveWeight field to ShipModel so assembled ships report correct positive weight instead of passing clamped maxHealth - Guard computeStat() against divide-by-zero when defaultRatio >= 1.0 - Use local plugin reference for engine PDC check in scanner instead of redundant global lookup
Rename totalPositiveWeight → mass (sum of max(0, weight) per block). This is the correct denominator for the power-to-mass ratio: it represents how much solid material sails need to push, excluding negative-weight floatation blocks. Fixes airships getting zero sail benefit — getSailRatio() previously returned 0 for negative totalWeight. Now uses mass, so airships with sails correctly get horizontal speed benefit. Also fixes: - engineBlockIndices changed from Set to List so iteration order matches engineLocalPositions (fixes smoke at wrong engine / IOOBE) - Lazy resolveWheelData() for chunk-recovered ships — looks up via ShipWheelManager.getWheelByShipUUID() on first access so fuel state is correctly loaded instead of assuming all engines fueled - Shift-click non-fuel into engine GUI now blocked - Dried kelp burn time 4001 → 4000 - YAML key renamed to "mass" with backwards-compat read of old "total_positive_weight" key
…er ref Engine PDC preserved on disassembly (is_engine flag in rawYaml, restored in placeBlocks). Ship info uses countFueledEngines() for ratio and shows fueled/unfueled engines separately. Smoke particles now spawn at engine shulker position instead of manual coordinate transform. Placed (unassembled) engine blocks open the custom fuel GUI instead of vanilla furnace UI. Shift-click non-fuel into engine GUI blocked. Smoke changed to CAMPFIRE_COSY_SMOKE for shorter duration. Known issues still unfixed: - countFueledEngines() doesn't know total engine set (uses computeIfAbsent side effect, phantom map entries); needs engine indices parameter - getShipInfo() uses stale lastDetected* fields for assembled ships instead of live ShipInstance data — mass can be 0/stale, effective power = mass - Engine fuel state and detection data are disconnected systems - Chat detection messages don't match lore format (missing mass, ratio) - Unassembled ships show engines at full potential points, not as unfueled
…back Root cause: assembleShip() never called setLastHealth() or setLastDetectedStats(), so the ship wheel menu fell into the "unassembled" display branch (lastMaxHealth=0) even for assembled ships. This showed all engines at full potential points instead of the fueled/unfueled breakdown. Fixes: - Set lastHealth + detection stats during assembly so menu has correct data immediately - getShipInfo() now reads live ShipInstance data (model + fuel state) for assembled ships instead of stale lastDetected* fields - countFueledEngines() takes engine block indices list so it checks ALL engines, not just those with map entries - getEngineFuelSlots() no longer uses computeIfAbsent (was creating phantom entries in the fuel map) - Physics fallback changed from engineCount (all fueled) to 0 when wheelData is null - tickEngineFuel skips engines with no fuel map entry - Engine display always shows fueled/unfueled breakdown regardless of assembled state - Engine GUI status shows Running/Ready/Idle based on burn ticks AND fuel items in slots
…tion deceleration scaling Previously wool power (3) and banner power (7) were hardcoded across multiple files. Fuel burn duration had no multiplier. Rotation deceleration used raw config values with no ratio-based scaling. Add wool-power, banner-power, fuel-burn-multiplier, floor-rotation-deceleration, and cap-rotation-deceleration to config.yml and ShipConfig. Consumers updated in subsequent commits.
…tation decel - Defer computeEffectiveStats() from constructor to after wheelData is linked. Previously the first stat computation always saw 0 fueled engines because wheelData wasn't set yet. Now recomputeStats() is called explicitly after assembly and lazily on recovery. - Guard minMovementThreshold so it only zeros speed when W/S aren't held — prevents low-ratio ships from being unable to move. - Return empty bucket when lava bucket fuel is consumed (vanilla parity). - Burn fuel on any movement input (A/D/Space/Sprint), not just W. - Apply fuel-burn-multiplier from config when consuming fuel items. - Scale rotation deceleration by power-to-mass ratio so heavy ships retain rotation momentum longer.
Engine fuel:
- Remap GUI fuel slots {1,2,3} → {0,1,2} for direct 1:1 mapping with
blast furnace container indices. Status slot moves from 5 → 4.
- Transfer pre-assembly fuel from blast furnace containers into
wheelData on assembly (was silently lost).
- Write wheelData fuel back to containers on disassembly (reverse gap).
- Clear stale fuel/burn entries on disassembly.
- Stop clearing entire blast furnace on save — targeted slot writes only.
- Crash-safe fuel deserialization with per-item try-catch.
- Add click-to-refresh on engine status item.
Stats display:
- Use weighted block count for density (matches physics, was using total
block count which included weightless blocks like trapdoors).
- Use config values instead of hardcoded 0.8 sail cap, 2 base power,
3 wool power, 7 banner power across all display paths.
- Standardize chat terminology: "power" → "pts".
Detection chat:
- Add chat output for assembled ship detection (was completely silent).
- Show live fuel state (fueled/unfueled engine breakdown) for assembled.
… dead data Engine GUI validation: - Block double-click collect and number-key hotbar swaps with non-fuel items. Add InventoryDragEvent handler to prevent drag-placing non-fuel. - Recompute ship stats on engine GUI close so fuel changes take effect immediately without requiring movement. Engine explosions: - Handle EntityExplodeEvent and BlockExplodeEvent for engine blocks — drop custom ship engine item instead of vanilla blast furnace. Null safety: - Guard null/invalid shulkers in camera distance update loop. ShipModel cleanup: - Remove dead engineLocalPositions field (populated but never read; smoke particles use collision shulker positions instead). - Accept woolPower/bannerPower as constructor params so sail power calculation uses config values. Old YAML with engine_local_positions key is silently ignored on load.
computeEffectiveStats() previously called ship.resolveWheelData(), which could trigger recomputeStats() → computeEffectiveStats() again when wheelData was first lazily resolved. The null guard prevented infinite recursion but the double computation was wasteful and fragile. Read ship.wheelData directly instead — all callers of recomputeStats() (assembly, recovery, GUI close) guarantee wheelData is set beforehand.
…orms (1/3) Root cause: the entity tracker sends vehicle rotation at byte precision (~1.4° quantization) every 3 ticks, conflicting with float-precision position sync packets sent every tick — creating periodic jitter. Fix: freeze the vehicle's yaw at spawnYaw and track rotation internally via physics.currentYaw. All visual rotation is applied through display entity transformation matrices, bypassing the entity tracker entirely. Changes: - ShipPhysics: add currentYaw field, rotation updates it instead of teleporting the vehicle, forward direction and snap methods use it - ShipInstance: all vehicle.getYaw() → physics.currentYaw, removed version-specific display rotation branch (always apply delta rotation via transformation), added idle yaw sync and setInterpolationDelay(0) - ShipCollision: collision forward direction uses physics.currentYaw Known issues to fix in follow-up commits: - idle yaw sync causes double rotation when ship stops (display transformation not updated on idle tick but vehicle yaw jumps) - setInterpolationDelay(0) causes item display Y jitter (interpolation duration > update interval creates cascading positional lag) - DisplayShip dismount velocity uses frozen vehicle yaw (wrong direction) - ShipPersistence saves frozen vehicle yaw (loses rotation on chunk save) - currentYaw can grow unbounded without normalization
Issues observed during in-game testing of the internal yaw tracking: - idle yaw sync caused double rotation on stop: when the ship went idle, the vehicle yaw was teleported from spawnYaw to currentYaw, but the display transformation (still carrying deltaYaw) was not updated on idle ticks — resulting in visual = inheritedYaw + deltaYaw = 2x rotation. Fix: remove idle yaw sync entirely, vehicle yaw stays frozen forever. - setInterpolationDelay(0) caused item display Y jitter: with interpolation_duration=2 but updates every 1 tick, each interpolation was interrupted before completion, creating cascading positional lag visible as massive downward jitter on item displays. Fix: remove setInterpolationDelay(0), let transforms snap at 20 Hz (still 3x smoother than the original 6.67 Hz byte-precision tracker). - dismount velocity used frozen vehicle yaw: players ejected from a rotated ship would fly in the original spawn direction. Fix: use physics.currentYaw in DisplayShip dismount calculation. - persistence saved frozen vehicle yaw: chunk unload would lose the ship's actual rotation. Fix: save physics.currentYaw in ShipPersistence.fromInstance(). - snapToFineGrid changed vehicle yaw: driver exit would unfreeze the vehicle yaw, re-introducing entity tracker byte-precision packets. Fix: only snap position, keep vehicle yaw frozen. Remaining: currentYaw can grow unbounded without normalization.
Custom ships lost their rotation on chunk reload because recoverEntities() set spawnYaw to model.initialRotation.x (assembly yaw), but the idle sync had already reset spawnYaw to currentYaw. This mismatch created a non-zero deltaYaw on recovery, causing double rotation (inherited vehicle yaw + transformation delta both contributing rotation). Fix: always set spawnYaw = vehicle.getYaw() on recovery (for all ship types), matching the idle sync's invariant that spawnYaw == currentYaw. deltaYaw is 0 on recovery, display shows R(initial), and the vehicle's inherited yaw provides the actual rotation. Also: - Extract updateDisplayTransforms() method from tick() so the idle yaw sync can call it after resetting spawnYaw (ensures display matches the synced state immediately, preventing one-tick visual glitch) - Add idle yaw sync: when ship stops, sync vehicle yaw to currentYaw for NBT persistence, reset spawnYaw to currentYaw, update displays - Normalize currentYaw to [0,360) after each rotation update - Use normalizeAngle() for position sync yaw delta to handle wrapping - Normalize spawnYaw/currentYaw at all init sites
…oGrid (4/3) The idle yaw sync teleported the vehicle on the first idle tick to update its NBT yaw for chunk persistence. This caused a 1-frame visual pop: the ENTITY_TELEPORT packet (changing vehicle yaw) and the display entity metadata update (resetting transform delta to 0) arrive at the client as separate packets, momentarily showing double-rotation. Root fix: save physics.currentYaw in per-world YAML metadata (ShipWorldData) so chunk recovery no longer depends on vehicle entity NBT yaw. The idle sync teleport is then unnecessary and removed entirely. Vehicle yaw now stays frozen at spawnYaw for the entity's entire lifetime. Metadata uses config.contains() to distinguish legacy files (no current_yaw key → Float.NaN sentinel → fall back to vehicle NBT) from files where the ship genuinely faces yaw=0 (north). Also fixes alignToGrid(): the vehicle teleport was passing snappedYaw, unfreezing the vehicle yaw. Now preserves loc.getYaw() (frozen) and resets spawnYaw + refreshes display transforms in the ShipInstance wrapper. ShipWheelManager.disassembleShip() reads physics.currentYaw instead of the frozen vehicle yaw for block placement rotation.
BACKGROUND The rotation system (82bda96) freezes the vehicle entity's yaw at spawnYaw for its entire lifetime. All visual rotation is applied through display entity transformation matrices using: deltaYaw = physics.currentYaw - spawnYaw On 1.21.9+, Minecraft clients inherit the parent vehicle's yaw when rendering display entity passengers. This means the client renders: visual rotation = vehicle.getYaw() + deltaYaw + initialModelRotation = spawnYaw + (currentYaw - spawnYaw) + initial = currentYaw + initial ✓ This is only correct when the invariant holds: spawnYaw == vehicle.getYaw() If spawnYaw is reset to a value that differs from vehicle.getYaw(), the client's inherited vehicle rotation no longer cancels out and the ship appears at the wrong angle. WHAT 1648bb5 BROKE 1648bb5 replaced the idle yaw sync with per-world metadata persistence (saving physics.currentYaw to ShipWorldData YAML). The goal was correct — the idle sync caused a 1-frame double-rotation pop because the ENTITY_TELEPORT packet (updating vehicle yaw) and the display metadata packet (resetting deltaYaw to 0) arrived at the client in separate frames. Metadata persistence avoids this by never changing vehicle yaw at all. However, 1648bb5 introduced two bugs that break the invariant: BUG 1 — alignToGrid() froze vehicle yaw while resetting spawnYaw alignToGrid() in ShipPhysics was changed to pass loc.getYaw() (the frozen vehicle yaw) in the teleport Location instead of snappedYaw. The ShipInstance wrapper then does: spawnYaw = physics.currentYaw; // = snappedYaw This left vehicle.getYaw() at the old frozen value while spawnYaw was updated to snappedYaw — breaking the invariant. After align (before fix): vehicle.getYaw() = oldFrozenYaw (e.g. 47°) spawnYaw = 90° (reset in wrapper) deltaYaw = 0° (currentYaw - spawnYaw) client renders = 47° + 0° = 47° ← wrong In-game effect: after using the align command, the ship's display blocks would visually snap to their spawn orientation (the old frozen yaw), while the collision boxes remained at the correct snapped angle. Invisible walls next to visible blocks. BUG 2 — chunk recovery set spawnYaw from metadata instead of vehicle NBT recoverEntities() was changed to set BOTH spawnYaw and currentYaw from the metadata yaw: spawnYaw = metadataYaw (e.g. 90°) currentYaw = metadataYaw (e.g. 90°) deltaYaw = 0° But the vehicle entity loads from chunk NBT with its original frozen yaw (e.g. 0°), which the client sees. So: vehicle.getYaw() = 0° (from NBT — was never updated) spawnYaw = 90° (from metadata) deltaYaw = 0° client renders = 0° + 0° = 0° ← should be 90° In-game effect: every ship that had been rotated from its spawn angle would appear at the wrong rotation after chunk unload/reload. Walking away from a ship and returning would show it "unrotated" back to its spawn angle, while collisions remained at the correct rotated position. FIXES Fix 1 — revert alignToGrid() to update vehicle yaw (ShipPhysics.java) Restore snappedYaw in the teleport Location for alignToGrid(). The ship is stationary after align, so there is no active position-sync packet to conflict with the rotation packet — no byte-precision jitter. Cardinal angles (90° multiples) also map exactly to byte encoding (90/360 x 256 = 64), so there is zero quantization error. After align with fix: vehicle.getYaw() = 90° (updated by teleport) spawnYaw = 90° (reset in wrapper) currentYaw = 90° deltaYaw = 0° client renders = 90° + 0° = 90° ✓ Invariant maintained. The ShipWheelManager change reading ship.physics.currentYaw for block placement remains correct (same value as vehicle.getYaw() post-align, but more explicit). Fix 2 — recovery sets spawnYaw from vehicle NBT, currentYaw from metadata (ShipInstance.java recoverEntities) spawnYaw must equal vehicle.getYaw() (what the client inherits from NBT). The metadata yaw goes into currentYaw, making deltaYaw provide the compensation: spawnYaw = normalizeYaw(vehicle.getYaw()) // e.g. 0° (NBT) currentYaw = normalizeYaw(metadataYaw) // e.g. 90° deltaYaw = 90° - 0° = 90° client = 0° + 90° = 90° ✓ Legacy metadata without current_yaw falls back to spawnYaw, giving deltaYaw = 0 — identical to pre-metadata behaviour. Fix 3 — replace R_initial-only init display loop with updateDisplayTransforms() (ShipInstance.java recoverEntities) The previous init loop applied only R_initial (no delta). With fix 2 making spawnYaw != currentYaw, this would cause a 1-tick flash at the wrong angle before the first tick applied the correct transform. Replacing the 18-line manual loop with updateDisplayTransforms() applies the full deltaYaw immediately on recovery, eliminating the flash. The call is safe at this point: vehicle, displays, spawnYaw, currentYaw, cachedR_initial, and all work matrices are fully initialized. WHAT REMAINS CORRECT FROM 1648bb5 - ShipWorldData saving physics.currentYaw and loading via config.contains() (correct sentinel, handles yaw=0 case) - Idle sync removal (safe — spawnYaw is never reset during normal ticking, so the invariant holds without it) - alignToGrid() wrapper (spawnYaw reset + updateDisplayTransforms) - ShipWheelManager reading physics.currentYaw for block placement
Adds `custom-ships.destruction-mode` to config.yml with two values: - `disassemble` (default): current behavior — blocks placed back into the world, wheel block broken and dropped, explosions spawned. - `destroy`: ship is permanently lost. Stored inventory contents and engine fuel drop as loose items at the ship's location; blocks and the wheel item are not recovered. Explosions still spawn. Implementation: - ShipConfig: adds `destroyOnDeath` boolean field loaded from `custom-ships.destruction-mode` (true iff value == "destroy"). - ShipInstance.destroyAndDropItem(): when destroyOnDeath is set, drops storages and fuel then calls destroyWithCleanup() instead of routing through disassembleShip(). Prefab ships are unaffected (branch is inside the "custom".equals(shipType) guard). - ConfigValidator.migrateConfig() picks up the new key automatically via its full-key scan of the bundled config. Three bugs are fixed in the following commit (found during review): - destroyWithCleanup() called without null-guarding getDisplayShip() - wheel block left in-world and placedWheels map not cleaned up on destroy, causing the wheel to reappear after server restart - lead items silently lost when ship shulkers are removed; players lose leads with no feedback or item drop
Master switch for the power-to-mass ratio system. When false, custom ships bypass ratio scaling and use their fixed default stats (speed/rotation/vertical), ignoring sails, engines, and mass — restoring pre-stats behavior. Engines also stop consuming fuel in this mode. statsEnabled is plumbed through ShipConfig and defaults to true, so existing configs are unaffected. Both computeEffectiveStats() and the tickEngineFuel() call in ShipPhysics are gated on it.
The Ship Info panel and detection chat always reported 0 fueled engines for a parked ship, even when its engine furnaces held fuel, understating the projected speed/ratio. Pre-assembly the fuel lives in the blast furnace block inventory rather than in wheelData, so countFueledEngines correctly returned 0 and the unassembled UI branch hardcoded 0. Detection now inspects each engine furnace's contents during the structure scan (new hasFuel helper, counting items with a burn time), stores the result as lastDetectedFueledEngineCount, and reads it in the unassembled branch. The count is folded into setLastDetectedStats so the whole detection snapshot stays atomic; the two assembled-path callers pass the live countFueledEngines value. The detection chat is updated to split fueled vs unfueled engines (as the assembled path already did) and fold the engine bonus into the previewed speed %, so the message no longer falsely claims "unfueled" when fuel is present. Assembled ships were already correct and physics is unaffected.
Document the two changes that landed without their changelog entries: - custom-ships.stats.enabled: note the off switch in the intro bullets and the Ship Stats System section, and add `enabled: true` to the config block. Covers what disabling does (fixed default stats, engines stop burning fuel), the safe default, and that it applies to newly assembled ships / after restart. - Add a Bug Fixes entry for unassembled previews now counting fuel held in engine furnaces instead of always reporting 0 fueled engines.
Wooden shelves (*_shelf) had collider: true and display_rotation: true, both copied from the chiseled_bookshelf entry in 63bc339. - collider: false — shelves are thin wall-mounted blocks and should not have a solid collision box. - drop display_rotation: true — unlike chiseled bookshelf (whose facing BlockDisplay ignores), the 1.21.9 shelf is a normal directional block whose facing BlockDisplay renders natively, like furnace. The manual display_yaw rotation was additive, double-applying facing and rendering shelves at the wrong orientation during flight.
Capture review/planning notes ahead of the v0.0.16 release. No code changes — documentation and project-instruction updates only. todo-v0.0.16-fixes.md (new): findings A–N from three review passes over the 0.0.16 release (stats-toggle review, a five-agent deep bug review, and a third pass over previously-unaudited subsystems). Each finding is re-verified and adversarially critiqued, with severity, root cause, fix, and end-to-end verification steps. Priority order: F (immobile after every reload, prefab + sail-only) and E (destroy-mode fuel dup) are CRITICAL; A/G/G2/K HIGH; the rest MEDIUM/LOW. Records C as an accepted "won't fix" (live /blockships reload for flying ships) so the limitation stays documented. docs/todo/TODO-persistence.md (new): design for eliminating plugin-written YAML by moving runtime ship/wheel state into Minecraft-native storage (entity & block PersistentDataContainer + scoreboard-tag scanning), with chunk-load discovery, a one-version migration shim, risks/tradeoffs, and verification. CLAUDE.md: convert to a bullet list and add two working conventions — keep temporary files in .temp/ (not system /tmp/), and skip decorative fluff around bash commands.
Two CRITICAL fixes from the v0.0.16 fix-audit re-review. F: recoverEntities() never called recomputeStats(), so ships rebuilt from persistence came back with effective stats at 0 -- immovable, can't turn, airships unable to ascend/descend -- on every chunk reload and server restart. Prefab and sail-only custom ships stayed frozen; only engine'd ships self-healed via the per-tick fuel path. Fix: resolveWheelData() + physics.recomputeStats() at the end of recoverEntities(), mirroring the spawn path (idempotent, null-guarded). E: a destroy-mode death dropped engine fuel twice -- once from the stale assembly-time storages snapshot (pre-burn amount), once from wheelData. Skip engine block indices in the destroy-mode storages drop loop so wheelData is the sole authoritative source. Also corrects ships saved before the fix. Also document both in CHANGELOG-v0.0.16.md and record the re-review findings in todo-v0.0.16-fixes.md (refute G/O1/M1/N1 as false positives, add O2/O3/P1-P3, revise the G2/J1/K fix approaches). WIP: hoppers, engines, and engine inventory/drops are still in flux on this branch.
Flip custom-ships.stats.enabled default from true to false, pending a rework of the power-to-mass ratio system in a future update. Fresh installs and configs missing the key now fall back to fixed default stats (sails/engines cosmetic); existing configs that set the key are unaffected. Changed in all three source-of-truth spots: config.yml, the ShipConfig config-read fallback, and the Builder field initializer. Updated the v0.0.16 changelog to reflect the new default.
Engines only do anything while the power-to-mass stats system is on, and that system now ships disabled by default. Skip registering the ship_engine recipe when custom-ships.stats.enabled is false, so engines can't be crafted in the default config. This goes through registerAllRecipes, so it re-applies on /blockships reload. Also correct the stale "default: true" comment on ShipConfig.statsEnabled, and add a stats-relevance triage to the 0.0.16 fix-audit doc, sorting the findings into three bins: not-stats-related (fix regardless of the toggle), stats-related game-breaking, and stats-related minor - to scope the release now that stats/engines ship off by default pending a refactor.
Assembling a custom ship containing a hopper threw IllegalArgumentException because HOPPER's 5 slots isn't a multiple of 9, which the size-based Bukkit.createInventory overload requires. Because scanStructure has already serialized+cleared the ship's containers by then, the failed assembly also permanently deleted their contents. Add a nullable InventoryType to StorageType (HOPPER -> InventoryType.HOPPER) and a createStorageInventory helper that uses the type-based createInventory overload (no 9-multiple restriction) when invType is set. Route both storage creation sites (recovery load + assembly) through it. Also cap the two setContents restore calls to the inventory size so a token/slot drift can't throw and fail a ship load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mbly can't destroy items scanStructure serialized a ship's container contents and then immediately emptied the world containers. Because the ShipInstance constructor runs (and can throw) after that clear but before removeBlocks, any assembly failure permanently deleted the contents of every container on the ship. The hopper crash was the deterministic trigger, but any throw did it. Keep the serialize in scan but remove the clear+update from both the Container and TileStateInventoryHolder branches. Empty the containers instead in a new guarded Pass 0 in removeBlocks, just before the setType passes. A throw before removeBlocks now leaves the world containers full, so the assembly is a clean, retryable no-op instead of item loss. Pass 0 iterates solidBlocks only (no inventory-holding block is attachable) and guards each clear per-block so one failure can't leave a half-removed ship; a clear that fails at worst drops that container's items rather than deleting them. This relies on scan and removeBlocks staying synchronous within one tick (no hopper transfer between the deferred clear and removal) - documented on removeBlocks. Also removes a stale TODO whose setType(AIR, true) change is already in place.
…y ghost entities Three stats-independent release blockers (findings G2, K, S). G2 (+U): a plain furnace/smoker on a ship opened a 27-slot chest GUI, but disassembly restored into the real 3-slot block and silently destroyed anything in the extra slots. deserializeInventory now collects items whose slot is beyond the real inventory, and the two restore callers drop them at the block instead of discarding — covering both new and already-saved ships, since overflow is measured against the runtime block size. Also read the slot index via Number to tolerate a Long/Double from a hand-edited model (U). K: the Captain's Manual shapeless recipe only matched on base material (any PLAYER_HEAD), so a mob head / decorative head / anvil-renamed head / balloon could craft the manual and be silently consumed. The shapeless branch now re-validates the grid against the registered ingredient pool, matching custom items by their custom_item_id PDC (rename-proof) rather than display name; a wrong or extra ingredient sets the result to null. Refactored isShipWheel onto a shared matchesCustomItemId helper. S: if ship assembly threw partway (e.g. a bad block), the half-built ShipInstance was discarded with no handle to its already-spawned entities, leaking invisible ghosts that multiplied on each retry. Wrapped the constructor spawn sequence in try/catch that tears everything down and rethrows. destroy() now also removes the displays list directly (child displays are parented a tick later, so on a mid-assembly throw they are not yet passengers of parent) and null-guards the vehicle removal.
…ed (B) P1 — runaway ghost ship: acceleration, drag, the stop threshold, and rotation keyed only off the input flags, which are cleared solely by freeSeat() <- VehicleExitEvent. When a driver leaves the seat without that event (death while seated, forced cross-world teleport) the flags stay stuck true and the ship flies/turns forever unmanned. Gate accel, drag, and stop on a new throttling = hasDriver && (fwd||back) so a driverless ship falls into the drag branch and coasts to a stop; gate only the active-turn rotation arms on hasDriver (decay else stays always-on so a spinning ghost winds down); and add hasDriver to the engine fuel-burn guard. B — stats UI leaked while disabled: 0.0.16 ships the power-to-mass system off by default, but the wheel menu and detect chat still showed power ratio / speed % / points / 'add sails'. Thread statsEnabled into ShipInfo and, when off, show a 'Stats system disabled — fixed speed' note instead in createInfoItem/createStatsItem and both detect-chat blocks, keeping the composition-independent lines (blocks, weight, health, density, mass, seats).
…k copy Ships built from newer blocks (e.g. spruce_shelf and other *_shelf variants) were rejected on assembly for servers upgraded from an older version. The plugin extracted blocks.yml to the data folder on first run via saveResource(..., false) and then only ever read that disk copy, never merging new defaults — so blocks added in later versions stayed missing from the on-disk config. The same stale-copy pattern affected items.yml and prefab ship models. Stop keeping editable disk copies of bundled content. New ConfigResources.load reads config/<path> if an admin override exists, else straight from the jar, so every update ships current defaults automatically. Route blocks.yml (BlockConfigManager), items.yml (ItemTextureManager), and prefab models (ShipModel.fromFile) through it, and drop DisplayShip.extractDefaultModelFiles. ConfigValidator now warns when a legacy copy still sits at the folder root (no longer read — move to config/ or delete) and when a config/blocks.yml override is missing entries the bundled default has. config.yml and the ships/custom-items readers are unchanged; getConfig() already layers jar defaults so it does not suffer this staleness. Splitting config.yml is a deferred follow-up.
Document the stale-copy fix (662292f) in the v0.0.16 changelog: a summary bullet noting new blocks work on upgraded servers without resetting configs, and a Bug Fixes entry explaining the root cause (blocks.yml/items.yml/prefab models extracted once and never updated), the jar-first loading with optional config/ overrides, and the reworked ConfigValidator.
Batch of stats-independent fixes ahead of 0.0.16: - U: the assembly-path container restore cast the slot index with (Integer), which throws on a migrated/hand-edited model whose slot is a Long/Double. Read it via Number.intValue() (matches the already-fixed deserializeInventory). - T: placeBlocks restored each block's metadata (banner/sign color, sign text, container items, skull) unguarded, so one bad value threw out of the loop and left the ship half-placed but still registered. Wrap the per-block loop body in try/catch that logs and skips only that block's metadata; the block itself is already placed, so the ship still fully disassembles and deregisters. - O3: recoverEntities threw IllegalStateException when a persisted collider index no longer fit the model, and neither the startup sweep nor the async chunk-load recovery loop caught it, so one stale ship aborted recovery of the rest of the batch. Skip the out-of-range collider with a warning instead of throwing, and add a per-ship catch to both recovery loops as a backstop. - L: /blockships give cleanup. Dedupe the giveable list (ship_wheel and captains_manual were listed twice, once hardcoded and once from custom-items) via a LinkedHashSet; add a giveOrDrop helper so a full inventory drops the item at the player's feet instead of silently eating it; guard the Material.valueOf in ItemFactory.loadCustomItem so a typo'd base-material degrades to null instead of throwing (this path also runs during recipe registration at startup, so it previously broke plugin enable entirely). - Furnace GUI: a plain furnace/smoker/blast-furnace now opens a real 3-slot furnace GUI in flight instead of a 27-slot chest, via a new StorageType.FURNACE(3, InventoryType.FURNACE). Exact size match to the real block means zero overflow on disassembly. The special engine (blast furnace with its own EngineMenuGUI) is unaffected — its storage type is inert at runtime. Applies to newly assembled ships; existing ships keep their persisted CHEST type, which the overflow-drop backstop keeps non-destructive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chunk_steering / chunk_steering_airship only checked total displacement > 1.0 after recovery, which the dismount deck/seat offset satisfies even when the recovered ship never moved — a false negative that let the F "immobile after reload" regression pass green across CI. Assert >=2 blocks westward (negative X, the steered direction) instead, mirroring the fresh-spawn directional check in test-bot.js.
The previous strengthening (c23a7a0) asserted >=2 blocks westward, but the recovered ship faces and drives north — verified in-game: the smallship moves ~18 blocks -Z and the airship ~18 north + ~20 up. The westward check only saw the ~1-block dismount offset (dX=-1.0) and failed a working ship. Assert >=10 blocks north (negative Z) instead, and log all three axes so the heading stays visible. (The main test's westward result comes from its A/D turn sequence; the chunk test never steers left/right, so the ship travels straight north.)
countFueledEngines treated any non-AIR item in an engine's furnace slots as "fueled", but tickEngineFuel only consumes items with getBurnTime > 0. A non-burnable item (raw iron, cobblestone) sneaked into a ship engine's slots (via a hopper/dispenser into the underlying blast furnace, or the offhand-swap GUI gap) therefore counted as fuel forever and granted permanent free engine thrust that never depleted. Require EngineMenuGUI.getBurnTime > 0 in the two "is this engine fueled?" scans: countFueledEngines and EngineMenuGUI.createStatusItem's status readout. The item-moving scans (assembly copy, serialize/deserialize, drop, consume) are left untouched, so no items are filtered or lost. VALID_FUELS is exactly the getBurnTime>0 set, so no real fuel ever reads as unfueled. Inert when custom-ships.stats.enabled is false (the default) since the count is only read by the power calc, which returns early when stats are off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sassemble
A block's custom name (anvil rename) is tile-entity NBT that blockdata
can't carry, so a renamed chest/barrel/furnace/hopper/dropper/dispenser or
banner lost its name through a custom-ship assemble->disassemble cycle, and
while assembled its storage GUI showed a generic label ("Ship Chest").
Both Container (via LockableTileState) and Banner implement Nameable, so a
single generic path covers them:
- scanStructure serializes any Nameable block's customName() as a GSON
component string into rawYaml["custom_name"] (null for un-renamed blocks,
so nothing is stored and the generic label is preserved).
- createStorageInventory takes the name and uses it as the in-flight GUI
title with full color/format fidelity, falling back to the generic label
when absent or on malformed JSON. Both call sites (assembly, chunk
recovery) pass it, guarding the cast so a wrong-type value degrades
gracefully instead of throwing.
- placeBlocks reapplies the name to the placed Nameable block on
disassembly, in a separate generic pass (fires even for a named block
with no items/patterns) inside the existing per-block try/catch.
Rides the existing rawYaml persistence round-trip, so reload/recovery is
covered with no schema change. Signs aren't Nameable (no block name);
lectern books already round-trip via the inventory path (only the open
page number, cosmetic, is unaffected).
- Leash: transferLeadsFromShip placed the LeashHitch at shipLoc + rotatedPos via Location.getBlock(), which floors, while placeBlocks positions the restored fence with Math.round() of the same rotated offset (to cancel cos(90°)≈6e-17 error). On a rotated ship (90/180/270°) a coordinate that landed just under an integer floored one block low, so the hitch spawned off the fence, in air, and popped off — breaking the lead. Round the components to match placeBlocks so the hitch lands on the actual fence. - Sign: the scan read signSide.getColor().name() unguarded, but SignSide.getColor() is @nullable, so a colorless sign would NPE and abort the assembly scan. Default to BLACK (the vanilla default; the restore side already tolerates a null/absent color).
…masked NPE Assembly (ShipWheelManager.assembleShip) built the ShipInstance and transferred leads with no error handling, and the block removal happens only afterward. If construction threw (e.g. the root armor stand spawn is denied by another plugin, or bad block metadata), the exception propagated raw to the caller: the player got no feedback, and the ShipInstance constructor's own cleanup called ShipRegistry.unregister on an instance whose vehicle was never assigned, throwing an NPE that masked the real cause. - ShipRegistry.unregister: null-guard the vehicle deref so destroy() is safe to call on a partially-built, never-registered instance. - assembleShip: wrap construction + lead transfer in try/catch (removeBlocks stays outside, so a failure never removes the world blocks). On failure, destroy a built-but-unregistered ship (guarded so cleanup can't mask the cause), log the full stack trace, tell the player it failed with the exception detail, and point them to the issue tracker. Returns false.
Enforce ASCII for punctuation/text and improve diagnostics on unexpected failures. Emoji (warning/health/airship) and section-sign color codes are kept intentionally. - Replace non-ASCII "funky" characters in comments and messages with ASCII: em-dash -> '-', arrow -> '->', degree -> ' deg', '*'/'~' for multiplication/approximation. Applies to code comments and a few player/log strings. - Replace all bare printStackTrace() calls with contextual logging. Genuinely-unexpected sites (item/profile deserialization, armor stand/shulker/collider setup, steering packet handlers) now log the operation + identifier via getLogger().log(..., throwable) so the trace is preserved, and point users at the issue tracker. Config-parse failures keep a warning with the offending key. - Add BlockShipsPlugin.ISSUES_URL constant and route the two inline issue-tracker URLs through it. - Give clearer reasons in the spawndrowned failure messages.
…metadata round-trip) Guards the v0.0.16 assembly/disassembly fixes the bot suite didn't cover: the hopper 5-slot inventory crash (#7), container item dup/loss (G2), and guarded metadata restore (T). The new weird_ship test builds a ship whose deck is packed with odd-sized containers (hopper/dropper/dispenser/barrel/trapped_chest), furnace-family blocks, and metadata blocks (banner, sign), preloads the hopper with coal, then assembles -> drives -> disassembles and asserts the round-trip is clean: - full entity spawn (>=12 shulkers; a crash destroys the ship -> ~0) - hopper coal round-trips exactly, incl. the 5th slot, with no stray dropped items - no [BlockShips] errors logged during the run Disassembly uses the real player path (sneak + right-click a ship shulker opens the wheel menu -> Disassemble), since assembly removes the wheel block. Adds the WEIRD_SHIP config, blockCharWorldPos, disassembleViaWheelMenu, and server-log scan helpers. Widens the test-server-ci server.log grep to catch WARNING-level metadata-skip and printStackTrace signatures as a CI backstop.
… metadata - onCommand: replace the silent fall-through (which showed the raw, hand-maintained plugin.yml usage) with an explicit "Unknown subcommand" message + sendHelp(), making the in-game help the single source of truth for the command list. - Prefix all command guard/permission-denied/error messages with the red color code so failures read consistently; success messages unchanged. - plugin.yml: set author to def9a2a4 (was the "You" placeholder) and add the missing highlightseats and spawndrowned entries so usage matches the actual 11 subcommands.
Follow-up cleanup to 662292f. Remove the now-unused YamlConfiguration and java.io.File imports from BlockConfigManager (loadConfig switched to ConfigResources.load, which returns a FileConfiguration), and hoist the duplicated config.getMapList("blocks") in ShipModel.fromFile into a local so the model block-list is parsed once instead of twice. No behavior change.
Mob heads (zombie/skeleton/creeper/piglin/dragon skulls) fell through to the BlockDisplay branch, which renders only the baked block model and never runs the skull block-entity renderer — so floor variants lost their rotation and wall variants were wrong. Route every head (player and mob) through the ItemDisplay + HEAD transform path that player heads already use: - Scanner now captures skull_rotation/skull_facing for any Skull block, not just PLAYER_HEAD; skull_profile stays player-only. - ShipInstance routes on a new hasHead flag (skull_profile | skull_rotation | skull_facing), resolves the head item material from the block name (mapping wall variants to their floor item form), and applies a skin profile only when present. Material.valueOf is guarded so assembly never aborts. Also extract the floor/wall skull transform into a shared applySkullTransform helper used by the spawn, per-tick, and chunk-recovery paths. The recovery path previously applied floor-only math to wall heads, so even player wall heads jumped on chunk reload; the three sites can no longer drift. Wall heads (player + mob, except dragon's full block) now get a 0.5 shulker collider, shifted toward the wall and up via a facing-derived per-block offset so the hitbox lines up with the rendered head.
The weird_ship hopper preload never landed, so the round-trip assertion always saw 0 coal and the test failed in CI. Two bugs: - The /item replace block command omitted the required `with` keyword, so the server silently rejected it and the hopper stayed empty. - buildCustomShipWithWheel leaves a ship wheel in the bot's hand (creative place doesn't consume it), so right-clicking the hopper hit the wheel interaction handler instead of opening the container. Add `with`, clear the bot's inventory right after the wheel is placed, and verify the preload actually landed (open the world hopper and count coal) so a bad preload fails as "preload failed" rather than as an ambiguous round-trip mismatch later. Verified passing locally (coal=5).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.0.16: Ship Stats, Sails & Engines, more block types, wider compatibility
custom-ships.destruction-mode: destroydestroys blocks on death instead of disassembling (drops inventory, fuel, and leads). Default staysdisassemble. Closes destruction - complete destruction, no block placement #27PlayerInputEvent; ProtocolLib is only needed pre-1.21.2. Builds against and supports up to 1.21.11. Maybe helps with potential protocollib issue? #28 ? Partially inspired from discussion in Similar plugin -- trade notes / join forces? jemcdevitt/SimpleShips#1/blockships givesupports all custom itemsspecial-drownedspawn chance lowered 5% → 2%. Closes drowned with shipwheels as heads spawning #29Notes
TODO.mddocs/changelogs/CHANGELOG-v0.0.16.md