build ships or airships out of blocks and sail or fly them smoothly - without client side mods or resource packs! Uses display entities to allow non-grid-aligned blocks
A Minecraft plugin that lets players create rideable, physics-enabled ships. Build custom ships from blocks or spawn pre-built vessels, then sail the seas or take to the skies. All of this without any client side mods or resource packs!
DynLight for dynamic lighting -- without this, light emitting blocks on custom ships won't work when the ship is assembled.
the prefab small spruce ship
A small custom ship next to the prefab small spruce ship
WARNING: THIS PROJECT IS IN ALPHA. EXPECT BUGS, MISSING FEATURES, AND BREAKING CHANGES. USE AT YOUR OWN RISK, AND BACKUP YOUR WORLD OFTEN. THE DEVELOPER IS NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY THIS PLUGIN.
Features
Build a ship out of allowed blocks, place a Ship's wheel on it and right click, and click "assemble" in the menu. Your blocks are now a mobile ship! Depending on the density (fully configurable) of the blocks in your ship, your ship might float lower or higher in the water. Use enough "lighter than air" blocks like glowstone, and your ship will be a flying airship.
Ships can include:
Functional blocks - Crafting tables, anvils, enchanting tables work as normal. (furnaces/brewing stands don't fully yet work)
Seats - any stair becomes a passenger seat you can sit on.
Lead points - Anything leashed to a fence will stay tied to the ship. You can lead things to the ship while its moving. Prefab ships have a single lead point.
Cannons - a dispenser with a block of obsidian behind it will shoot its contents. fire all through the ship menu, or right click on the obsidian to fire.
Health & damage - Ships have health, take damage from collisions/attacks, and regenerate over time. When they run out of health, ships will attempt to place themselves back in the world.
Sounds & effects - Movement and damage come with visual effects and audio.
IMPORTANT: Download ProtocolLib if using 1.21.2 or below! ProtocolLib is no longer required as of BlockShips 0.0.16
Place both jars in your server's plugins folder
Restart the server
Configuration
config.yml
Main plugin settings including:
Ship physics (speed, acceleration, drag)
Buoyancy values (density, strength)
Collision settings (mass, response)
Crafting recipes
blocks.yml
Configure which blocks can be used in custom ships:
Weight scale - Affects buoyancy
Collider - Custom collision shapes
Seat/storage - Special block behaviors
Inspiration
This plugin is inspired by mods which also implement rideable ships, as well as plugins which have attempted similar functionality. I made this plugin because I realized that with the addition of display entities, it might be possible to create a better ship plugin than previously possible, but without requiring any client-side mods. No code from any of other project has been used. In particular, this plugin was inspired by:
Although this plugin predates them, you should also check out the extremely cool Create Aeronautics Mod, as well as the similar SimpleShips plugin!
License
You are free to use this plugin only for non-commercial projects and servers. For commercial use, please contact the author for a license. For more details, see the LICENSE.txt file.
Developing
You will need java and gradle installed. Running make build will compile the plugin and create a jar file in bin/.
# v0.0.17 - tweaks and fixes
- **Cheaper craftable recipes:** the Ship Balloon now uses a glowstone block (was a
phantom membrane) and the Ship Wheel uses a lead (was a blast furnace). Custom heads
(balloon, wheel, ship kits) also render correctly in the recipe book again.
- **WorldGuard region protection is now respected:** ships that disassemble or crash-land
inside a WorldGuard-protected region no longer write blocks into it past the region's
build permission. Optional, soft-dependency — no effect unless WorldGuard is installed.
Enabled by default (`plugins.worldguard.enabled: true`).
- **Destructive admin commands are now failure-aware:** `forcedisassembleall` and
`killentities` no longer silently swallow persistence/cleanup errors, and
`/blockships info` gained a real loaded-vs-unloaded ship/wheel breakdown.
- **New permission nodes:** `blockships.info` (default true) and `blockships.highlight`
(default op). If you manage permissions explicitly, grant these.
---
## New Features
### WorldGuard Region Build Permissions (80ba661)
BlockShips edits the world directly (it never fires a `BlockPlaceEvent`), so until now a
ship disassembling or crash-landing inside a WorldGuard region wrote its blocks straight
in, ignoring the region's build flag. A new optional integration makes every plugin
block-edit respect the acting player's build permission.
Active only when WorldGuard is installed **and** `plugins.worldguard.enabled: true`
(default). Behavior:
- **Disassembly:** cells landing in a region the player can't build in become conflicts.
The player is warned and can still Force Disassemble; on force those blocks **drop as
items** instead of being placed — preserving engine/wheel custom-item identity and
container contents. A protected wheel anchor drops the wheel item and deregisters
instead of planting a head.
- **Assembly:** denied if any scanned cell (across the whole flood-fill) is protected —
closing a block-laundering dupe.
- **Wheel placement** and **breaking a wheel/engine** are denied inside a protected region
(these paths edit blocks programmatically or past WorldGuard's own cancel).
- **System paths** (crash disassemble, `forcedisassembleall`) act as a non-member, so
unattended ships drop items rather than griefing the region. Because a system path has no
player identity, this applies even inside the ship's own owner's claim or a spawn region —
admins who prefer the old behavior can set `system-disassembly-in-region: place-anyway`
(default `drop-items`) to have unattended disassembly write blocks back instead of dropping
them. Player-driven Force Disassemble is unaffected and always respects the player's own
build permission.
Implementation notes: new `integration` package — `WorldGuardHook` (static holder, no-op
default), `NoOpWorldGuardHook`, `WorldGuardHookImpl`. The query uses the built-in `BUILD`
flag via `RegionQuery`, treats any result `!= ALLOW` as denied, uses a `NON_MEMBER`
associable for the player-less path, checks bypass explicitly, null-guards the region
lookup, and **fails open with throttled logging**. `BlockStructureScanner` gains a
`protectedCount` conflict category, player-aware validate/place overloads gated by an
O(1) "world has regions" check, and a `dropPartAsItems` helper. WorldGuard is
`compileOnly` (never shaded), declared `softdepend` in `plugin.yml`; the hook is
(re)installed on enable and reload and only touches WorldGuard classes behind a
`Class.forName` guard, so the plugin loads and behaves identically without WorldGuard.
New config values:
```yaml
plugins:
worldguard:
enabled: true
system-disassembly-in-region: drop-items # or place-anyway
```
### `/blockships info` Stats Rework (07df6a5)
`/blockships info` now reports a loaded-vs-unloaded breakdown of prefab/custom/all ships,
sourced from the in-memory registry (loaded) and the chunk index (persisted), plus
unassembled wheels, orphaned wheel links, and a duplicate-wheel-link warning. A shared
`classifyWheels()` feeds both the stats view and the `forcedisassembleall` confirmation so
their counts agree by construction; destructive commands now print a stats + entity
preview before the confirm step.
## Hardening & Fixes
### Destructive Admin Commands Now Surface Failures (07df6a5)
`forcedisassembleall` and `killentities` used to ignore persistence and cleanup errors.
Now:
- `ShipWorldData.removeShip` and `saveAllChunkIndices` return a boolean and log on failure
(failed delete / `chunks.yml` write), so callers can detect and report the failure.
- `ShipWheelManager.saveAll` returns a boolean; `disassembleShip` gains a 4-arg overload
with a `DisassembleOutcome` holder, so a post-disassembly save failure is reported
separately from disassembly success.
- `forcedisassembleall` / `killentities` isolate per-ship / per-entity failures in
try/catch, count them, and report `destroyFailed` / `saveErrors` / `cleanupFailed` to
the admin. `killentities` force-unregisters a ship whose `destroy()` throws, so no
phantom entry lingers in the registry.
### Command Surface Cleanup (07df6a5)
- New permission nodes: **`blockships.info`** (default true) and **`blockships.highlight`**
(default op), gating the info and highlight handlers and their help + tab-complete
listings. `blockships.give` now also covers `spawndrowned`.
- `reload` re-registers/unregisters the special-drowned listener to match the toggled
config, instead of requiring a full restart.
- The dead multi-line `plugin.yml` usage block collapses to `/blockships help` (the
in-game help is the single source of truth).
- `highlightcolliders` guarded against empty/invalid colliders; denial wording and
success-message coloring unified.
### WorldGuard Drop-Path Data Fidelity & Robustness (cb47a01, 2a388d8)
Follow-up review passes on the WorldGuard integration, tightening the drop path and the
assembly gate:
- **Decorated blocks keep their identity when they drop.** Player-head textures, banner
patterns, and custom (anvil-renamed) names are copied onto items dropped into a protected
region, on top of the existing engine/wheel custom-item and container-content preservation.
(Sign text still can't ride a vanilla item and is not preserved.) Decoration transfer is
wrapped so corrupted/hand-edited model data yields an undecorated, logged drop rather than
losing the whole block.
- **No more duplicated or vanished multi-cell blocks.** Door/tall-plant upper halves and bed
heads no longer drop a second item; upside-down stairs and top-hung trapdoors (single-cell
blocks that merely *look* like a stacked top half) are explicitly kept, so they no longer
silently disappear on a protected force-disassembly.
- **The assembly gate fails closed on a WorldGuard fault.** A transient query error during
the assembly scan can no longer reopen the block-laundering exploit; destructive/drop paths
still fail open. The gate also distinguishes a world with region support *disabled*
(assembly proceeds normally) from region data that *failed to load* (fails closed), instead
of blocking assembly in every regions-disabled world.
- **Breaking a wheel/engine respects other protection plugins.** The break handlers run at
`HIGHEST` and bail if the event is already cancelled, instead of manually breaking and
dropping after another plugin has denied it.
- **Misconfiguration no longer renders as garbage.** A `sail-cap-ratio` of `0` no longer
displays an `Infinity`/`2147483647%` speed (it falls back to the honest percentage), and an
unrecognized `system-disassembly-in-region` value logs a warning and uses `drop-items`.
- `/blockships give ship_engine` is gated on `custom-ships.stats.enabled` (matching the recipe
gate) and hidden from give help + tab-complete when stats are off. Documented that a
`__global__` region is required for WorldGuard's world-wide build-deny to be honored.
### Waterlogging Preserved on Disassembly (54eb636)
Ships float, so hull cells sit in water source blocks. A waterloggable block (fence, stairs, slab,
wall, ...) disassembling back over a water source was previously placed dry, overwriting the water
and leaving an unnatural air pocket underwater. The destination cell is now authoritative:
`placeBlocks` clears waterlogged on every waterloggable block and re-sets it only when the target is
a water **source** (level 0 — transient flowing water is excluded); `scanStructure` clears
waterlogged at capture so stored ship models never carry water. Because the clear is unconditional,
this also **self-heals** ships already saved with `waterlogged=true` baked into their block data,
which would otherwise re-waterlog on dry land forever.
## Other
- **Recipe tweaks (23c8562):** Ship Balloon now crafts with a glowstone block instead of a
phantom membrane (a mob drop that's awkward to farm); Ship Wheel now takes a lead instead of
a blast furnace — cheaper for a single wheel and a better nautical fit alongside the compass.
- **Recipe-book head textures (9a5d013):** custom heads (balloon, ship wheel, ship kits) were
showing as blank Steve heads in the recipe book because the result icon never resolved its
skin through Mojang's session server. The raw texture is now embedded directly on the
profile (on a deterministic, texture-derived UUID so identical heads stack), so these render
client-side without a lookup. The same fix now extends to **ingredient slots**: a custom-item
ingredient (e.g. the balloons in the airship recipe) is matched with an `ExactChoice` over its
craftable variants — every dye colour for wool-sourced items — so the recipe book shows the real
textured item instead of a blank head, while still matching by full item meta.
- README now links to the related [SimpleShips](https://github.com/jemcdevitt/SimpleShips)
plugin (4de59a1).
alpha
BlockShips 0.0.16 -- perf/bugfix/compat, wip ship stats system
# v0.0.16 - compatibility, bugfixes, performance, and in-dev ship stats system
# You probably want to reset/check your configs!
- **ProtocolLib is now optional on Paper 1.21.2+:** Ship controls use Minecraft's native input event on newer servers, so you no longer need to install ProtocolLib there. Pre-1.21.2 servers still require it. Now builds against and supports up to 1.21.11 ([#28](https://github.com/def9a2a4/BlockShips/issues/28))
- **Ship rotation is now smooth and jitter-free!**
- a recent performance update made this worse, now it's fixed!
- **More blocks usable in custom ships:** Chiseled bookshelves, shelves (1.21.9+), and signs can now be part of a ship, and their contents, sign text, and custom (anvil) names are preserved across assembly and disassembly ([#23](https://github.com/def9a2a4/BlockShips/issues/23))
- **Ship assembly is now crash- and data-safe:** hoppers no longer crash assembly, a failed assembly can no longer empty your containers, and plain furnaces/smokers no longer lose items stored in them on disassembly
- **Alternate (optional) destruction mode:** Ships can be configured to permanently destroy on death instead of disassembling ([#27](https://github.com/def9a2a4/BlockShips/issues/27))
- Set `custom-ships.destruction-mode: destroy` in config.yml
- Blocks are lost, but inventory contents, engine fuel, and mob leads are dropped
- default is still to disassemble them into blocks on destruction
- **Towny compatibility:** ship entities now set an empty custom name to prevent Towny's mob removal timer from deleting them ([#17](https://github.com/def9a2a4/BlockShips/issues/17))
- Previously, Towny would remove ship shulkers (collision/seat entities) in town chunks, ejecting players and ghosting the ship
- Workaround config options for Towny are documented in the issue thread
- **Drowned captains are rarer:** the default `special-drowned` spawn chance is lowered from 5% to 2% so they don't overwhelm biomes where drowned spawn heavily ([#29](https://github.com/def9a2a4/BlockShips/issues/29))
- **New blocks now work without resetting configs:** `blocks.yml`, `items.yml`, and prefab ship models are read from the jar instead of a stale on-disk copy, so newly-supported blocks (like shelves) work on upgraded servers automatically. To customize, drop an edited copy under a new `config/` subfolder. **If you modified these, you should manually merge the configs!**
- **[DISABLED BY DEFAULT] Ship speed now dependent on engines and sails:** Ships now have a power-to-mass ratio system that scales speed and rotation based on block composition ([#18](https://github.com/def9a2a4/BlockShips/issues/18))
- Wool blocks act as sails (3 power), banners as upgraded sails (7 power), engines provide 30 power when fueled
- Heavier ships are slower unless you add more power sources
- Sail-only power is capped — engines required to push ships past 80% of max ratio
- Engines use fuel, emit smoke when working
- Ship info UI shows power ratio, effective speed percentage, and color-coded stats
- Server admins who prefer the old fixed-speed behavior can turn the whole system off with `custom-ships.stats.enabled: false` — custom ships then use fixed default speeds and ignore sails/engines/mass
- **The stats system is disabled by default** (`custom-ships.stats.enabled: false`) pending a rework in a future update. Set it to `true` to opt in
- rework will probably be via integration with [Mechanism](https://modrinth.com/plugin/mechanism) plugin
- other various bugfixes and improvements
---
## New Features
### Ship Stats System (5da91fd) — [#18](https://github.com/def9a2a4/BlockShips/issues/18)
Ships now have a power-to-mass ratio that scales their speed and rotation rate. Every block contributes mass; specific blocks contribute power. The ratio between total power and total mass determines how fast the ship can move and turn.
Power sources:
- Base power: 2 free points per ship (ensures minimum performance)
- Wool blocks: 3 power points each (sails)
- Banner blocks: 7 power points each (upgraded sails)
- Engines: 30 power points each (when fueled)
The ratio maps linearly between a floor (minimum speed/rotation) and a cap (maximum multiplier over defaults). A ratio of 0.7 reproduces the previous default behavior. Sail-only power is capped at ratio 0.8 — engines are required to exceed it.
Airship vertical speed scales separately with density magnitude (total mass / block count), independent of the horizontal power ratio.
The system can be disabled entirely with `custom-ships.stats.enabled: false`: custom ships then fall back to fixed default stats and block composition no longer affects performance (engines also stop consuming fuel). The key **defaults to `false`** pending a rework of the stats system in a future update — set it to `true` to opt in. It takes effect on newly assembled ships and after a server restart, like the other `stats` settings.
Ship info UI updated to show wool count, banner count, engine count, power ratio, and effective speed as a percentage (f542112, 8c7c2e7). Ship info hover simplified to show only speed %; detailed breakdown moved to a new Ship Stats banner item at slot 20 in the menu. Speed % uses sail cap (0.8) as 100% baseline — over 100% means engines are contributing. Speed and density values are color-coded in the UI (8c7c2e7). Floor acceleration default corrected from 0.005 to 0.015 (f9ed87f, f542112).
New config values:
```yaml
stats:
enabled: false # you must enable the ship stats system here; it's disabled by default
base-power: 2
engine-power: 30
wool-power: 3
banner-power: 7
sail-cap-ratio: 0.8
default-ratio: 0.7
max-ratio-multiplier: 1.5
```
### Configurable Stats (2a9757e)
Previously hardcoded stat values are now exposed as config options, enabling tuning without code changes.
New config values:
```yaml
stats:
wool-power: 3
banner-power: 7
fuel-burn-multiplier: 1.0
floor-rotation-deceleration: 0.05
cap-rotation-deceleration: -1
```
### Engine System (16e7a07, 8dd07a7)
New custom item: Ship Engine. Crafted from 8 copper ingots surrounding a blast furnace (shaped recipe). Appears as a blast furnace with enchant glint (f542112 — new `enchant-glint` config field for custom items). Because the ship stats system is disabled by default, the engine recipe isn't registered unless you enable it (`custom-ships.stats.enabled: true`) — so engines are uncraftable in the default config.
When a player places an engine, a BlockPlaceEvent listener transfers PDC tags to the block's TileState. The BlockStructureScanner detects tagged blast furnaces as engines and includes them in stats calculations. Vanilla smelting is suppressed on engine blocks.
**Fuel system:** Right-clicking an engine block on an assembled ship opens the EngineMenuGUI with 3 fuel slots. Only vanilla furnace fuels are accepted (validated on click). Fuel burns 1 item per tick while any movement input is held (W/A/D/Space/Sprint, not just W — d001055). When the current fuel item is exhausted, the next slot is auto-consumed. Lava bucket fuel returns an empty bucket (vanilla parity — d001055). Ship stats are recomputed whenever fuel state changes.
Pre-assembly fuel in blast furnace containers is transferred into wheelData on assembly; fuel is written back to containers on disassembly (c3e0705 — previously silently lost). Fuel deserialization is crash-safe with per-item try-catch. Click-to-refresh on engine status item shows live fuel state.
CAMPFIRE_COSY_SMOKE particles are emitted at fueled engine positions every 5 ticks while the ship has a driver.
Per-engine fuel slots and burn ticks are serialized to `ship_wheels.yml` as Base64-encoded ItemStack bytes. Engine block indices and local positions are tracked in ShipModel for click detection on assembled ships.
Breaking an engine block drops the custom ship engine item (with PDC tag and glint) instead of a vanilla blast furnace (d4f4ee7). Explosions (EntityExplodeEvent, BlockExplodeEvent) also drop the custom item (232aac0).
### Smooth Ship Rotation (82bda96, 55a6cc6, 59dcb85, 1648bb5, 3995b4d)
Complete rewrite of ship rotation across 5 commits to eliminate visible jitter.
**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. This created a visible snap every 3 ticks.
**Solution:** The vehicle's yaw is frozen at its spawn rotation. Actual rotation is tracked internally as `ShipPhysics.currentYaw` (float precision). All visual rotation is applied via display entity transformation matrices with `setInterpolationDelay(0)` for smooth client-side interpolation over 2 ticks.
All consumers updated to use `physics.currentYaw` instead of `vehicle.getYaw()`:
- Forward direction calculations in ShipPhysics
- Collision direction in ShipCollision
- Snap-to-grid and snap-to-cardinal methods
- Display rotation matrices (now use delta yaw: currentYaw - spawnYaw)
`currentYaw` is persisted in ship metadata for chunk recovery. Yaw normalized to [0, 360) to prevent drift.
Persisting `currentYaw` across chunk recovery also addresses the chunk-reload symptom from [#7](https://github.com/def9a2a4/BlockShips/issues/7), where a parked ship would snap to north (collider rotates, display stays put) after its chunk unloaded and reloaded. This is a partial fix — the more severe collider/skin desync some users reported on older versions may still persist.
### Destruction Mode (3507fa0, dc0dfc6, 6288eed) — closes [#27](https://github.com/def9a2a4/BlockShips/issues/27)
Ships can now be configured to permanently destroy on death instead of disassembling.
New config value:
```yaml
custom-ships:
destruction-mode: disassemble # or "destroy"
```
**Disassemble mode (default):** Blocks placed back into world at original positions. Wheel block broken and dropped. All stored items returned. Engine fuel dropped.
**Destroy mode:** Ship blocks permanently lost. Wheel block removed from world and tracking map (new method `ShipWheelManager.destroyWheelBlock(Location)` — removes without dropping the item). Stored inventory contents dropped as loose items. Engine fuel items dropped as loose items.
Lead preservation: Before destroying collision shulkers, iterates all leashable entities within 12-block radius. Any entity leashed to a ship shulker gets one LEAD item dropped, matching vanilla fence-post-broken behavior. Uses Paper's `Leashable` interface instead of `Mob` to also catch leashable non-mob entities like boats in 1.21.2+ (6288eed). Leash is detached after dropping to prevent `tickLeash` from dropping a duplicate lead when the shulker holder is removed by `destroy()`.
NPE guard added: `getDisplayShip()` null-checked before `destroyWithCleanup()`, with fallback to bare `destroy()`.
### Config System Cleanup (1c8155e)
An earlier dev build (31855a0) added `ConfigValidator.migrateConfig()` which attempted to detect missing keys in the user's config.yml and write them in from the bundled default. This was removed before release for several reasons:
1. **It was fundamentally broken.** It used `diskConfig.contains(key)` to check for missing keys, but Bukkit's `contains()` also checks in-memory defaults loaded by `saveDefaultConfig()`. Every key appeared to already exist, so the migration never wrote anything.
2. **Even if fixed, `saveConfig()` strips all YAML comments.** The first successful migration would destroy every inline comment in the user's config file — all the documentation explaining what each setting does.
3. **It's unnecessary.** Bukkit's defaults system already makes new keys available via `getConfig()` / `getConfigurationSection()`. Code reading config values gets the bundled default automatically when a key isn't in the user's file. Recipe registration discovers new items (ship_engine, captains_manual) through defaults without needing them on disk.
The `auto-migrate-config` setting was also removed.
Scoped the outdated-file check to only resource files (blocks.yml, items.yml, prefab ships) where it's useful. Previously, any config.yml customization triggered a misleading "delete your config" warning. Softened warning message for the remaining files.
Recipe registration hardened (1c8155e):
- Ingredient parse errors bail out immediately (no half-built recipes)
- `Bukkit.addRecipe()` wrapped in try-catch so one bad recipe can't cascade-fail all subsequent registrations
- Shaped recipes validate that every non-space pattern character has a corresponding ingredient defined before attempting registration
### /blockships give Expansion (2b53f05)
The `/blockships give` command now supports all custom items:
- `captains_manual` (written help book)
- Any `custom-items` entry from config (ship_engine, balloon, etc.)
- Ship wheels and ship kits (existing)
Tab completion includes all giveable item IDs. Item listing extracted into shared helpers to avoid duplication.
### Help Book Extraction (16e7a07, f9ed87f)
Captain's manual content moved from hardcoded Java in ShipWheelMenu into `help_book.yml`. Content can be updated without recompiling. Help book content reloads on `/blockships reload` (f9ed87f). Shapeless recipe support added to ItemUtil.registerItemRecipe() (f9ed87f). Various content fixes and stats tuning applied.
The Captain's Manual is now a craftable item — a shapeless recipe combining a Ship's Wheel with a book yields the written guide. It can also be obtained via `/blockships give captains_manual`.
### Towny Compatibility (856419d) — [#17](https://github.com/def9a2a4/BlockShips/issues/17)
Towny's mob removal timer periodically scans town chunks and removes hostile mobs. Shulkers are on the default removal list, and BlockShips uses shulker entities as invisible collision boxes and seats. When the timer fires, ship collision entities get deleted, players get ejected, and the ship becomes a ghost (display entities survive since they're not mobs).
Fix: All ship entities (shulkers, armor stands) now have their custom name set to `Component.empty()` and `customNameVisible` set to false. Towny's removal logic skips named entities, so this prevents ship entities from being culled while remaining invisible to players.
Applied to:
- ShipWheelManager: collision shulkers spawned during ship detection
- ShipInstance: root armor stand vehicle, carrier armor stands, and seat shulkers spawned during assembly and chunk recovery
Server admins running Towny can also configure workarounds on the Towny side — see [#17](https://github.com/def9a2a4/BlockShips/issues/17) for details.
### Optional ProtocolLib & Wider Version Support (e15f794, 13e84cb, 7da6074) — [#28](https://github.com/def9a2a4/BlockShips/issues/28)
ProtocolLib is no longer a hard dependency. It is now declared as a `softdepend` in `plugin.yml` (and `compileOnly` in the build), so the plugin loads with or without it.
Ship steering picks an input path based on the server:
- **Paper 1.21.2+:** uses Minecraft's native `PlayerInputEvent` via the new `PaperInputListener`. This runs on the main thread and requires **no ProtocolLib**.
- **Pre-1.21.2:** falls back to the existing ProtocolLib packet interception (`ShipSteeringListener`). ProtocolLib is still required here.
This also fixes the **1.21.3–1.21.8** window where ProtocolLib's reflection on the player input record had stopped working (breaking WASD controls), and removes a race condition where input booleans were written from the netty thread while physics read them on the main thread.
The plugin now builds against `paper-api:1.21.11`, so the effective supported range is roughly **1.21.2–1.21.11** on Paper without ProtocolLib (older versions remain supported with ProtocolLib installed).
### More Custom-Ship Blocks: Shelves, Bookshelves, Signs (e9b6757) — partially addresses [#23](https://github.com/def9a2a4/BlockShips/issues/23)
Three new block types can now be part of a custom ship, with their state preserved across assembly and disassembly:
- **Chiseled bookshelves** (6 book slots)
- **Shelves** (1.21.9+, 3 item slots)
- **Signs** (standing, wall, and hanging) — front and back text, text color, glow state, and waxed flag are all retained
Shelf and chiseled bookshelf contents are serialized through a new `TileStateInventoryHolder` path in `BlockStructureScanner` (these store items in the tile entity rather than block data). `blocks.yml` gains `chiseled_bookshelf` and `*_shelf` entries so they can be part of a ship and render correctly in flight.
Note: due to a `BlockDisplay` limitation, shelf/bookshelf items and sign text are not visible while the ship is flying — they reappear when the ship is disassembled. This is a partial fix for [#23](https://github.com/def9a2a4/BlockShips/issues/23): decorated pots, other data blocks, and wall-mounted heads are still unsupported, and in-flight sign-text rendering is not yet implemented.
## Bug Fixes
### Hopper Ships Crashed; A Failed Assembly Destroyed Container Contents (8ea5b92, 587e90a, 30f269c, 12045ef)
A cluster of assembly-lifecycle bugs:
- A ship containing a **hopper** threw on assembly. The in-flight storage GUI is built with `Bukkit.createInventory(size)`, which requires a multiple of 9 slots, and a hopper has 5. Odd-sized storage now uses the `InventoryType` overload, which has no such restriction (8ea5b92).
- The scan serialized and **cleared every container** before the ship object was built, so *any* failure during assembly permanently emptied every chest/barrel/furnace on the ship. The clear is now deferred until the blocks are actually removed, so a failed assembly leaves the world untouched (587e90a).
- A mid-assembly failure also left already-spawned display/collision entities as invisible orphans that multiplied on each retry; they're now torn down on failure (30f269c).
- A failed assembly now shows the player a clear error with a report link and logs the real cause, instead of failing silently (12045ef).
### Furnace/Smoker Items Lost on Disassembly (30f269c, bef8292)
A plain furnace, smoker, or blast furnace on a ship opened a 27-slot chest GUI in flight (they have no dedicated storage type), but on disassembly only the real block's 3 slots were restored — anything in the other slots was silently destroyed. Overflow items are now dropped at the block instead of deleted (30f269c), and plain furnaces now open a real 3-slot furnace GUI in flight, so newly assembled ships have no mismatch at all (bef8292). Note: a furnace ship saved *before* this update may drop those overflow items twice (a one-time dupe) the first time it's disassembled; re-assembling it clears the mismatch for good.
### Assembly & Disassembly Hardened Against Bad Block Data (bef8292, 699a394)
Restoring a block's metadata on disassembly (banner/sign colors, sign text, container items, skull profiles) was unguarded, so one bad value threw out of the loop and left the ship half-placed but still registered as alive. Each block's metadata restore is now isolated — a failure skips only that block's decoration, and the ship still fully disassembles. A sign with no dye color also threw during the assembly scan (the color getter is nullable) and now defaults to black (699a394). And a hand-edited or migrated save whose stored slot index isn't a plain integer no longer aborts the storage restore.
### Leads Broken When Disassembling a Rotated Ship (699a394)
Disassembling a ship rotated 90/180/270° could drop the leads on leashed animals. The restored fence is positioned with rounding (to cancel a tiny floating-point error from the rotation), but the lead-hitch was placed with truncation, so it could land one block off the fence, in the air, and pop off. The hitch now rounds to match the fence.
### Renamed Containers and Banners Lost Their Names (a760209)
A block's custom (anvil) name lives in tile-entity data that block data can't carry, so a renamed chest, barrel, furnace, hopper, dropper, dispenser, or banner lost its name across an assemble/disassemble cycle and showed a generic "Ship Chest" label on its in-flight GUI. Custom names now travel with the ship and are reapplied on disassembly, and the in-flight GUI shows the real name with full color and formatting.
### Mob Heads and Wall-Mounted Heads (799ca40)
Mob heads (zombie, skeleton, creeper, piglin, dragon) fell through to the generic block-display path, which renders only the baked model — so floor mob heads lost their rotation and wall heads rendered wrong. Every head (player and mob) now uses the same item-display path player heads already use: the scanner captures rotation/facing for any skull, wall variants map to their floor item form, and a shared transform helper is 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 reload). Wall heads (player and mob) now get a collider too. Resolves [#20](https://github.com/def9a2a4/BlockShips/issues/20).
### One Bad Ship Aborted Recovery of All Others (bef8292)
When a ship's saved data no longer matched its model (the definition changed between save and load), recovery threw — and because the startup sweep had no per-ship error handling, it aborted recovery of every remaining ship across all chunks and worlds. The stale block is now skipped with a warning, and both recovery loops catch per-ship failures.
### Unmanned Ships Could Fly Forever (47b7cb3)
A ship's motion was driven by the last input flags, which clear only on a normal dismount. If a driver left the seat without one (died while seated, or was teleported to another world), the flags stuck and the ship kept accelerating and turning with nobody aboard. Horizontal movement, rotation, and fuel burn are now gated on the ship having a real driver (vertical already was), so an abandoned ship coasts to a stop.
### Stats Shown Even When the System Was Off (47b7cb3)
With the stats system disabled (the default), physics uses fixed speeds, but the wheel menu and "ship detected" chat still printed power ratio, speed %, sail/engine points, and "add sails!" hints — making it look like the toggle did nothing. Those are now suppressed with a brief "stats disabled" note; block count, weight, mass, and health still show.
### Engine Exploit, Recipe, and /blockships give Fixes (535a4eb, 30f269c, bef8292)
- **Free engine fuel:** an engine counted as fueled if it held *any* item, but only burnable items are consumed — so a non-burnable item (raw iron, cobblestone) granted permanent thrust that never depleted. Only burnable fuel now counts (535a4eb). *(Only relevant with `stats.enabled: true`.)*
- **Captain's Manual recipe:** the shapeless recipe matched on base material only, so any player head (mob head, decorative head, balloon) + a book crafted the manual and silently consumed the wrong item. It now validates the real ship-wheel tag (30f269c).
- **/blockships give:** the give list and tab-complete listed `ship_wheel` and `captains_manual` twice; a full inventory ate the item instead of dropping it; and a misconfigured `base-material` threw a raw error, which also broke plugin startup. All three fixed (bef8292).
### Command Help & Error Consistency (e869ed5)
An unknown `/blockships` subcommand used to print the raw plugin.yml usage string; it now shows a clear "Unknown subcommand" message plus the in-game help, which is the single source of truth for the command list. All error and permission-denied messages now read consistently in red. The `plugin.yml` metadata was also corrected (author, and the missing `highlightseats`/`spawndrowned` entries).
### Circular resolveWheelData Call (65112f3)
`computeEffectiveStats` called `resolveWheelData` which called `computeEffectiveStats` again, creating infinite recursion in certain code paths. Broken by restructuring the call chain to avoid the cycle.
### Engine Stats Display and Fuel Lifecycle (cab8a4f, c3e0705)
Ship info display showed stale stats data and incorrect fuel counts. Stats display referenced wrong fuel state, and detection chat messages did not reflect engine-adjusted power ratios. Fixed stats consumers to read live fuel state and recompute on change.
Engine fuel lifecycle fixes (c3e0705):
- GUI fuel slots remapped {1,2,3} → {0,1,2} for direct 1:1 mapping with blast furnace container indices
- Pre-assembly fuel from blast furnace containers now transferred into wheelData on assembly (was silently lost)
- Fuel written back to containers on disassembly (reverse direction)
- Stale fuel/burn entries cleared on disassembly
- Stop clearing entire blast furnace on save — targeted slot writes only
- Crash-safe fuel deserialization with per-item try-catch
- Click-to-refresh on engine status item
- Detection chat now shows output for assembled ship detection (was completely silent) with live fuel state (fueled/unfueled engine breakdown)
- Density display uses weighted block count (matches physics — was using total block count which included weightless blocks like trapdoors)
- Standardized chat terminology: "power" → "pts"
### Unassembled Preview Showed Engines as Unfueled
The Ship Info panel and the detection chat message always reported **0 fueled engines** for a parked (unassembled) ship, even when its engines' blast furnaces held fuel — so the projected speed/ratio understated a fueled ship. Detection now reads each engine furnace's contents during the structure scan and counts engines holding burnable fuel, so the preview panel and detect message show the real fueled count and an engine-adjusted speed estimate. (Assembled ships were already correct; physics was never affected.)
### Ship Physics Timing (d001055)
Multiple physics issues fixed in one pass:
- Stats timing: `computeEffectiveStats()` was called in constructor before wheelData was linked, so first computation always saw 0 fueled engines. Now deferred until after assembly, with lazy recomputation on recovery
- Movement threshold: low-ratio ships were unable to move because `minMovementThreshold` zeroed speed even when W/S were held. Now only applies when no movement input is active
- Fuel burn: lava bucket fuel returned nothing when consumed — now returns empty bucket (vanilla parity)
- Fuel burn: only triggered on W key — now triggers on any movement input (A/D/Space/Sprint)
- Fuel burn multiplier from config was not being applied
- Rotation deceleration: did not scale with power ratio — heavy ships now retain rotation momentum longer
### Mass/Weight Semantics (c76e5f6)
Renamed `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 fixed:
- `engineBlockIndices` changed from Set to List so iteration order matches engine positions (fixes smoke particles appearing 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 items into engine GUI now blocked
- Dried kelp burn time 4001 → 4000 ticks
- YAML key renamed to "mass" with backwards-compat read of old "total_positive_weight" key
### Stats Bugs (1015cea, d4f4ee7)
Multiple stats system bugs fixed:
- Engine blocks dropped as vanilla blast furnace on break — now drop custom ship engine item with PDC tag and glint
- `totalPositiveWeight` field added to ShipModel so assembled ships report correct positive weight instead of passing clamped maxHealth
- `computeStat()` divide-by-zero when `defaultRatio >= 1.0` — added guard
- Scanner reference not set early enough in ship assembly
### Fuel Validation and Explosion Drops (232aac0)
Engine GUI exploit prevention:
- Double-click item collect with non-fuel items now blocked
- Number-key hotbar swaps with non-fuel items now blocked
- InventoryDragEvent handler added to prevent drag-placing non-fuel
- Stats recomputed on engine GUI close so fuel changes take effect immediately without requiring movement
Engine explosion handling:
- EntityExplodeEvent and BlockExplodeEvent handlers added 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: removed dead `engineLocalPositions` field (populated but never read; smoke particles use collision shulker positions). `woolPower`/`bannerPower` now accepted as constructor params so sail power calculation uses config values.
### Prefab Ships Unable to Move (c8ef29c)
Commit 5da91fd moved `computeEffectiveStats()` out of the ShipPhysics constructor to avoid a circular call with wheelData (needed for custom ship fuel state). But `recomputeStats()` was only called from custom-ship paths (assembly, wheelData resolution, engine menu close). Prefab ships never hit any of those paths, so `effectiveMaxSpeed`, `effectiveAcceleration`, and all other effective stat fields stayed at 0.0f. Ships could spawn and be mounted but couldn't move at all.
Fix: call `physics.recomputeStats()` in the ShipInstance constructor after delegates are initialized. For prefab ships this sets the final config-based values. For custom ships it produces a conservative initial ratio (0 fueled engines), which gets recomputed after wheelData is linked.
### Ships Immovable After Chunk Recovery
A follow-on to the fix above. Deferring `computeEffectiveStats()` out of the constructor (5da91fd) and restoring it on the spawn path (c8ef29c) both missed the chunk-**recovery** path: `recoverEntities()` never called `recomputeStats()`, so any ship rebuilt from persistence came back with all effective stat fields at 0.0f. Only engine'd custom ships self-healed (via the per-tick fuel/smoke path, which links wheelData); prefab ships and sail-only custom ships stayed frozen — immovable, unable to turn, airships unable to ascend or descend. This triggered on every chunk reload and every server restart, regardless of `stats.enabled`.
Fix: call `resolveWheelData()` then `physics.recomputeStats()` at the end of `recoverEntities()`, mirroring the spawn path. `computeEffectiveStats` reads only config/model/wheelData (null-guarded) and is idempotent, so it is safe to run once per recovery.
### Rotation Bugs (55a6cc6, 59dcb85, 1648bb5, 3995b4d)
Series of fixes following the smooth rotation rewrite:
- Chunk recovery did not restore `currentYaw`, causing ships to snap to spawn yaw on chunk reload
- Yaw values could drift outside [0, 360) over time, causing display glitches
- `alignToGrid` used vehicle yaw instead of internal yaw, snapping to wrong position
- Idle sync teleport caused unnecessary jitter — removed
- Two rotation invariant bugs where forward direction disagreed with visual rotation in edge cases
### Container Item Duplication on Assembly (19e0bc7)
Inventory blocks duplicated their contents when a ship was assembled. The block's items were serialized into the ship data, but the snapshot inventory was never cleared before the block was set to air — and Bukkit drops a block's remaining items to the world on `setType(AIR)`. So every chest/furnace/hopper (and shelf/chiseled bookshelf) effectively spilled a second copy of its contents onto the ground.
Fixed by clearing the snapshot inventory and pushing the empty state to the world (`getSnapshotInventory().clear()` + `update()`) before removing the block, on both the `Container` and `TileStateInventoryHolder` paths.
### Engine Fuel Duplicated on Destruction (destroy mode)
A crafted engine is a blast furnace — a `Container` — so at assembly its fuel was serialized into `container_items` and rebuilt into the in-memory `storages` map, *and* separately transferred into the wheel's engine slots. On a **destroy-mode** death the drop path iterated both `storages` and `wheelData`, so engine fuel dropped twice. Worse, the `storages` copy was the stale assembly-time snapshot, so it ignored fuel burned while sailing and could drop more fuel than the player ever had. (Disassemble mode was unaffected — it overwrites `container_items` from the wheel before placing.)
Fixed by skipping engine block indices in the destroy-mode `storages` drop loop, leaving `wheelData` as the single authoritative source of dropped engine fuel. This also corrects ships saved before the fix, whose engine `container_items` are rebuilt into `storages` at load.
### Shelf/Bookshelf Rotation & Destroy Drops (63bc339)
Two issues with the newly supported shelf and chiseled bookshelf blocks:
- They rendered without their correct facing in flight; their `blocks.yml` entries were fixed so they now face the right direction, like chests and barrels.
- Their contents were not dropped when a ship was destroyed. Unlike containers, these blocks store items in `container_items` rather than the `storages` map, so they were missed by the destroy path. They now drop their items at the ship's death location.
### Newly-Added Blocks Missing on Upgraded Servers (662292f)
On servers upgraded from an earlier version, newly-supported blocks — most visibly shelves (1.21.9+) — were rejected during ship assembly. `blocks.yml` was extracted to the plugin data folder on first run and then only ever read from that disk copy, never merging new defaults. A server that generated `blocks.yml` before shelves existed kept a stale copy without the `*_shelf` entry: the bundled default had it, but the on-disk file hid it. `items.yml` and the prefab ship models had the same stale-copy pattern.
These content files are no longer written to disk. They're read straight from the jar (via a new `ConfigResources` helper), so every update ships current defaults automatically. Admins who want to customize a file drop an edited copy under a new `config/` subfolder (`config/blocks.yml`, `config/items.yml`, `config/prefab_ships/*.yml`), which is read in preference to the bundled copy but never overwritten by the plugin.
`ConfigValidator` was reworked to match: it now warns when a legacy copy still sits at the data-folder root (that location is no longer read — move it to `config/` or delete it), and when a `config/blocks.yml` override is missing entries the bundled default has (an override going stale). `config.yml` is unchanged — Bukkit already layers bundled defaults under the user's file, so it never suffered this problem.
### Controls Warning Clarity (6236198)
The startup warning shown when WASD controls cannot be initialized (no Paper input event and no ProtocolLib) was reworded to lead with the impact before the cause:
```
WASD ship controls will not work!
Paper PlayerInputEvent not available, ProtocolLib not found.
```
### Rarer Drowned Captains — [#29](https://github.com/def9a2a4/BlockShips/issues/29)
The default `special-drowned.spawn-chance` is lowered from `0.05` (5%) to `0.02` (2%). In biomes where drowned spawn heavily, the special ship-wheel-dropping drowned were appearing in overwhelming numbers. The feature can still be disabled entirely with `special-drowned.enabled: false`, or the rate tuned back up.
## Known Issues / Not Yet Addressed
These are tracked but not resolved in v0.0.16:
- **Proper tile entity support ([#23](https://github.com/def9a2a4/BlockShips/issues/23)):** decorated pots and other data-bearing blocks still aren't supported, and sign text is preserved but not rendered while a ship is in flight.
- **Partial destruction ([#24](https://github.com/def9a2a4/BlockShips/issues/24)):** only whole-ship destroy/disassemble (#27) is implemented; per-block / progressive destruction is not.
- **Older-version visual desync ([#7](https://github.com/def9a2a4/BlockShips/issues/7)):** the chunk-reload rotation snap is fixed, but the more severe collider/skin desync reported on legacy versions may still occur.
- **ProtocolLib reports ([#28](https://github.com/def9a2a4/BlockShips/issues/28)):** the underlying cause should be resolved by the optional-ProtocolLib path on 1.21.2+, but the issue remains open pending reporter confirmation.
# v0.0.15 - Ship-to-Ship Collision
- Ships now physically collide with each other!
- This used to kind of work, but was disabled for performance reasons. now it's back!
- We did some clever math to make it fast: collision detection uses axis-aligned binning, designed to handle ships with 1000+ colliders without impacting server performance
- Force is proportional to mass: heavy ships barely move, light ships bounce off
- Parked ships (no driver) resist being pushed but aren't immovable
- Grey dust particles and a quiet wooden impact sound play at the collision contact point
- Players seated on either ship hear a quieter version of the sound so large-ship riders don't miss collisions happening far away. also fixed for terrain collision sounds.
- Ship-to-ship collision can be disabled entirely for performance by setting `collision.ship-to-ship-enabled: false` in config.yml
- Reduced visual pop-in when ships spawn or load from chunks — display entities now appear closer to their final position
- still WIP, initial wrong rotation still flashes
- Fixed player desync when disconnecting while riding a ship — players could reconnect and steer a ghost ship they weren't visually on
- Fixed CI test flakiness (by skipping certain tests on old versions)
- Added version-based test skips and `--only` filter to bot test runners
- issue is bot inputs persist after it exists the ship
---
## New Features
### Ship-to-Ship Collision Detection (7042ebd, a0ddad9)
Ships now physically collide with each other. A global ShipCollisionCoordinator singleton runs once per server tick before individual ship ticks, computing all ship-ship forces centrally. Each ship reads its pre-computed force during its own collision detection pass.
This architecture eliminates three problems that plagued the previous (disabled) implementation:
- No entity scanning: uses ShipRegistry directly instead of getNearbyEntities + scoreboard tag parsing
- No double-counting: each ship pair is processed exactly once
- No tick-ordering bugs: forces are pre-computed, not mutated cross-ship during detection
The narrow phase uses axis-aligned binning for performance:
- Broad phase groups ships by world and checks vehicle distance vs sum of collision radii for each unique pair
- Narrow phase projects all colliders onto the ship-to-ship axis, drops them into 2.0-block-wide bins via O(n) counting sort, then only checks cross-ship pairs in matching/adjacent bins
- Early exit after a configurable max collisions per pair (default 20)
All per-tick data structures are reusable flat arrays with zero steady-state allocation. Designed to handle 1000+ colliders per ship.
The force model is simple push-apart: total penetration depth summed across overlapping collider pairs, pushed along the ship-to-ship axis, split by inverse mass ratio. Heavy ships barely move, light ships get pushed more. Equal mass ships each get half.
Parked ships (no driver) receive 30% of the collision force, so they resist being pushed but aren't completely immovable.
New config values:
```yaml
# Ship-to-ship collision settings (global)
collision:
# Enable/disable ship-to-ship collision detection
# (disable for performance on busy servers)
ship-to-ship-enabled: true
# Maximum number of collider overlaps to process per ship pair
# (early exit for performance)
ship-to-ship-max-collisions: 20
```
Per-ship mass is derived from blocks for custom ships, or configured under each prefab ship type's collision section (already existed):
```yaml
ships:
smallship:
collision:
mass: 100.0 # heavier = harder to push
```
### Collision Effects (b4e3be7)
Ship-to-ship collisions now produce visual and audio feedback.
Grey dust particles (3-6, scaled by penetration depth) spawn at the midpoint of the first overlapping collider pair — the actual contact point, not the midpoint between vehicles.
The impact sound (ENTITY_ZOMBIE_ATTACK_WOODEN_DOOR, same as ship damage but much quieter) plays at the contact point with a per-pair cooldown of 15 ticks (0.75s) to prevent spam during sustained collisions.
For large ships where the contact point may be far from seated players, the sound also plays at each seated player's location via player.playSound() at half volume. Only the target player hears this — no sound pollution for bystanders.
New config value:
```yaml
sounds:
# Volume multiplier for ship-to-ship collision sounds
# (0.0 = disabled, 1.0 = normal)
ship-collision-volume: 0.15
```
## Bug Fixes
### Stationary Ships Ignoring Collision Forces (a0ddad9)
A stationary ship being rammed by another ship would not react. ShipCollision.detect() had an early return when the ship wasn't moving and had no previous collision force, which bailed out before reading the coordinator's pre-computed force.
Solution: Restructure detect() so the terrain collision loop is gated behind the movement check (expensive, per-collider) but the coordinator force lookup always runs (single HashMap get).
### Display Entity Spawn Pop-In (a59e611)
Display entities spawned at the vehicle's ground-level position, then jumped up when mounted as passengers 1 tick later, causing a visible pop-in flash when ships spawn or load from chunks.
Solution: Spawn display entities 2.5 blocks higher (approximate passenger attachment height) and pre-multiply their transformation matrices with the ship's rotation and position offset so they appear closer to their final position during the brief pre-mount window.
### CI Test: Airship Driver Seat Mounting (72cac94, 6ab1dbd)
Bot-based integration tests for airships intermittently failed because mountShip() picks the nearest shulker, which after the v0.0.14 spawn-at-final-position change is the passenger seat for airships, not the driver seat.
Solution: Teleport the bot 1 block in -Z before mounting so the driver seat is the closest shulker. Applied to both smallairship and chunk-test airship test suites.
### Collision Coordinator Bugs (e646b27, 3c3900d)
Post-launch review caught several issues in ShipCollisionCoordinator:
1. **Force mutation**: `getShipCollisionForce()` returned a direct reference to the pooled vector in the force map. When ShipCollision.detect() called `.mul(PARKED_SHIP_FORCE_DAMPING)` on a parked ship, it permanently corrupted the stored force. Now returns a defensive copy.
2. **Sound cooldown off-by-one**: `Map.Entry.setValue()` returns the old value, not the new one. The removeIf lambda compared the pre-decrement value, making every cooldown last 16 ticks instead of 15.
3. **Zero mass NaN**: If both ships had `shipMass: 0` in config, `totalMass` would be 0, producing NaN forces that propagate through the physics system. Both masses now clamped to minimum 1.0.
4. **Allocation reduction**: `shipsByWorld` lists are now cleared in-place instead of dropped and re-created every tick. Removed a shared mutable `ZERO_FORCE` static constant that could be corrupted by future callers.
5. **Midpoint origin precision**: `(float)(a + b) * 0.5f` cast to float before multiplying, losing precision on the sum. Now `(float)((a + b) * 0.5)` keeps the multiply in double precision.
### CI Test: Version-Based Test Skips (8dffcff, 8aab3ec)
Added VERSION_SKIPS map to skip tests on specific Minecraft versions where protocol differences cause false failures (e.g. mineflayer hardcodes jump:0x01 in steer_vehicle packets on pre-1.21.3, causing airship drift). Both test-bot.js and chunk-test.js now support `--only=<substring>` to run specific tests.
### Player Desync on Disconnect While Riding (8e0b5be)
When a player disconnects while driving or riding a ship, the dismount handler relied on `player.getVehicle()` to find the seat shulker. On some Bukkit/Paper versions, the server ejects passengers before `PlayerQuitEvent` fires, making `getVehicle()` return null. No cleanup happened: `occupiedSeatIndices` still contained the seat, and steering input flags stayed set. On reconnect, the re-mount logic in `updateCollisionPositions()` would force-remount the player onto the shulker — the server thinks they're riding, but the client shows them standing on the ground. The player could steer the ship but didn't move with it.
Solution: Add `dismountPlayerFromAnyShip()` which tries the fast `getVehicle()` path first, then falls back to scanning all ships' `seatShulkers` for the disconnecting player. The fallback calls `removePassenger()` + `freeSeat()` directly, bypassing `VehicleExitEvent` (which would try to teleport a disconnecting player).
### Collision and Damage Particle Tuning (f948850)
Increased particle spread and count for both ship-to-ship collision dust and ship damage smoke effects to make them more visible.
### CI Test: Bot Facing Direction (3c0af42)
On 1.21.4, `/tp` without explicit yaw leaves the bot facing south instead of west. The smallship test spawns a ship that inherits the bot's yaw, so it moved in -Z instead of -X, failing the westward movement assertion. Bigship passed by luck because the bot's yaw happened to end up pointing west after the prior test's control sequence.
Solution: Add `yaw=90 pitch=0` to all `/tp` commands in test-bot.js and chunk-test.js so the bot always faces west regardless of server version.
### CI Test: Skip Smallship on 1.21.4 (9652c46)
Smallship test (first to run) consistently fails on 1.21.4 in CI with `dX=0.00` (ship moved nowhere) but passes locally and all subsequent tests pass. Likely a timing issue where the bot or ship isn't fully ready on CI's slower hardware before controls start. Skipped on 1.21.4 until root cause is identified.
# v0.0.14 - Bugfix & Performance Update
- Fixed moving ship choppiness by sending extra position sync packets via ProtocolLib (can be disabled in config.yml)
- Fixed players falling through the ship on dismount, assembly, and
disassembly
- Fixed players getting launched off the ship when assembling (collision shulkers are now created at their final positions instead of being teleported from ship root)
- Fixed deck jitter when walking on moving ships
- Fixed ships with trapdoor or top-slab bottoms appearing to float
(buoyancy now accounts for slab half-height and ignores trapdoors)
- Lots of general performance improvements and optimizations
- Paper 26.1.2 support
- New `/blockships highlightcolliders` debug command
----------------------------------------------------------------------
# NEW FEATURES
------------
ProtocolLib Vehicle Position Sync (1c57ae3, 113c3b3)
Ship display entities now receive vehicle position packets every tick
via ProtocolLib, significantly reducing perceived display lag and
rubber-banding for riders and nearby players.
- Position sync packets are gated on speed and rotation thresholds to
avoid unnecessary network traffic when a ship is stationary.
- Thresholds are configurable in config.yml. The entire feature can be
disabled if it causes issues with other plugins.
Highlight Colliders Debug Command (1850bd1)
New /blockships highlightcolliders command toggles a glowing outline
on all collision shulker entities belonging to a ship. Useful for
debugging collision geometry and verifying collider placement.
Per-Tick Performance Improvements (7281ceb, 4d33f3d, 67e6d86, 659d90d)
Broad sweep of per-tick optimizations across the ship movement and
collision pipeline.
- Cache vehicle location per tick to avoid redundant getLocation()
clones.
- Reduce object allocations in the terrain collision loop.
- Cache rotation matrix per tick instead of recomputing per block.
- Guard setHealth calls to skip when health is already at max.
- Throttle passenger-check frequency.
- Move ship persistence writes to async tasks.
- Deduplicate tag extraction, buoyancy math, yaw normalization, and
location reuse across ship subsystems.
Paper 26.1.2 Support (8ab89f8, a86f761, 9f0cd89)
Added Paper 26.1.2 to the CI test matrix. Bot-based integration tests
are skipped on 26.x until minecraft-protocol adds support for the new
protocol version.
# BUG FIXES
---------
Ship Movement Choppiness on Water Ships (e5a0f1a)
Water ships exhibited choppy movement and walking-on-deck bugs caused
by stale position state being fed into the buoyancy and movement
pipeline.
Root cause: Vehicle location was not refreshed between movement phases
within the same tick, so buoyancy and player-walk corrections operated
on outdated positions.
Player Clipping on Dismount, Assembly, and Disassembly (eebd894, e94f7a7)
Players dismounting a ship could be teleported inside nearby solid
blocks. The previous fix used a blind Y-offset that was unreliable
for multi-deck ships and ships near terrain.
Solution: Calculate a safe dismount position by scanning nearby
colliders and finding an unobstructed location. Also fixed clipping
on assembly and disassembly where players were not repositioned
relative to the new block layout.
Player Launch on Ship Assembly (63c1941, bae9102)
Assembling a ship could launch nearby players into the air because
collision carrier entities were spawned at the ship root and then
teleported to their final positions. The moving shulkers pushed
players as they traveled.
Solution: Spawn collision carriers directly at their final world
positions instead of teleporting them from the ship root. Collider
world-position math extracted into computeColliderWorldPos for reuse.
Deck Jitter on Moving Ships (c61fbb6, ce17c97, fbb6961, 0e3a942, 35fa46a)
Players standing on a moving ship's deck experienced rapid ~0.1 block
Y oscillation, especially on ships with buoyancy.
Root cause: Minecraft's entity tracker sends relative-move packets at
1/4096 block resolution. Over time, carrier ArmorStand positions on
the client accumulate precision drift, putting shulker collision boxes
at Y values where the client's gravity-collision cycle doesn't
converge. The jitter was purely client-side — it persisted even during
/tick freeze and only cleared after ~10 seconds or a relog.
Solution: When the ship stops moving, call hideEntity + showEntity on
each carrier for all tracked players. This forces the client to
discard stale entity state and receive fresh SPAWN_ENTITY packets
with exact server-side positions. For airships, the refresh also
triggers when vertical velocity zeroes out.
Display Entity Teleport Smoothing (9ebe9a6)
Display entities lagged behind the ship during movement because
teleport smoothing was not correctly applied to BlockDisplay entities.
Additionally, the dismount height filter incorrectly rejected valid
dismount positions above or below the ship center.
Solution: Apply proper interpolation to display entity teleports and
widen the dismount height filter to accept positions across the full
ship height.
Stale Vehicle Location in Terrain Collision (4d33f3d)
The terrain collision loop used a cached vehicle location that could
become stale mid-tick after buoyancy or gravity adjustments moved the
ship.
Solution: Refresh the cached location after each movement phase within
the collision loop.
Sync/Async Save Race in Chunk Unload (9ce4177)
Ship data could be corrupted or lost when a chunk unload triggered a
synchronous save while an async save from the wheel manager was
already in flight.
Solution: Serialize save operations so only one write is active at a
time, preventing partial overwrites.
Ship Floats Above Water With Trapdoor/Top-Slab Hull (76b3993)
Ships whose bottom row was made of top slabs or trapdoors appeared to
float above the waterline because the buoyancy calculation used the
full block Y as the ship's bottom edge.
Solution: Account for top-slab half-height when computing the ship's
lowest extent, and exclude trapdoors from the bottom-edge calculation
entirely since they do not displace water.
Seat Shulker Health Overflow (8b2b878)
Calling setHealth on seat shulkers could throw an IllegalArgument
exception when the health value exceeded the entity's max health
attribute.
Solution: Clamp health to the entity's max health before calling
setHealth.
- light sources on custom ships will work now, thanks to integration with https://github.com/def9a2a4/DynLight
- double chest related duplication bug fixed
- TNT can now be fired from ship cannons (disabled by default, see config)
- lingering potions from cannons fixed
NEW FEATURES
------------
DynLight Integration for Light-Emitting Ship Blocks (c5ad222, d10feb3)
Ship blocks that emit light (glowstone, lanterns, torches, etc.) now
produce dynamic lighting via DynLight plugin integration. BlockDisplay
entities are tagged with dynlight:<level> scoreboard tags so DynLight
can render real-time light around moving ships.
- Configurable via ship-lights toggle in config.yml.
- No compile-time dependency — tags are inert if DynLight is absent.
- Dynlight tags are applied to collision shulkers (not BlockDisplay
entities) so DynLight can track their world position accurately.
- Blocks without colliders (torches, etc.) delegate their light to the
nearest neighbor shulker (below first, then sides, then above).
- Blocks fully surrounded by opaque blocks are skipped via occlusion
culling to avoid wasted light sources.
Configurable TNT Cannon Projectile Support (183284f)
TNT loaded into ship dispensers now spawns as primed TNT instead of
dropping as an inert item. This enables functional TNT cannons on
ships.
- Controlled by cannons.tnt-enabled (default: false) and
cannons.tnt-fuse-ticks (default: 80) in config.yml.
- Disabled by default to prevent griefing on public servers.
Ref: https://github.com/def9a2a4/BlockShips/issues/15
BUG FIXES
---------
Double Chest Item Duplication on Ship Assembly (5b882c2)
Fixed double chests duplicating their entire 54-slot shared inventory
into each half during ship assembly, resulting in doubled items on
disassembly.
Root cause: DoubleChestInventory was being serialized for both halves,
so each half stored all 54 slots instead of its own 27.
Solution: Force double chests to single chests during block scanning
so each half only serializes its own 27-slot inventory. BlockData is
also stored as type=single to prevent auto-merging on disassembly.
Fixes: https://github.com/def9a2a4/BlockShips/issues/12
Lingering Potion Visual When Fired From Ship Cannons (fe03538)
Lingering potions fired from ship dispensers appeared as generic
thrown potions on the client side because the potion item data was
set after the entity entered the world.
Solution: Set potion item before entity enters the world by using
lambda-based spawn, so clients see the correct lingering potion
particle and bottle texture.
Fixes: https://github.com/def9a2a4/BlockShips/issues/15
Lingering Potion Using Splash Behavior on Impact (d994684)
Lingering potions fired from ship cannons were splashing on impact
instead of creating an area effect cloud, because they were spawned
as ThrownPotion entities.
Solution: Use LingeringPotion entity class so lingering potions
correctly create an area effect cloud on impact.
Ref: https://github.com/def9a2a4/BlockShips/issues/15
NEW FEATURES
------------
Seat Highlighting & Third-Person Camera Distance (517a89c)
- `/blockships highlightseats` command: Look at a ship and run this
command to highlight all seats with colored particles. Orange
particles indicate passenger seats, red particles indicate driver
seat. Particles remain visible for 5 seconds.
- Seats button in ship wheel menu: Shows seat count and current
occupancy.
- Camera distance customization: New camera-distance config option
for prefab ships. Per-ship camera distance setting for custom ships
via +/- buttons in menu. Auto-calculates sensible defaults based on
ship block count. Range: 4-32 blocks (Minecraft 1.21.6+ via
GENERIC_CAMERA_DISTANCE attribute).
Ship Health HUD Display (b84f36d)
When players ride seat shulkers, the Minecraft health bar HUD now
shows the ship's health (similar to riding a horse). Health syncs
after melee damage, projectile damage, and health regeneration.
Ships with 40 HP or less display directly (1:1 mapping). Ships with
more than 40 HP scale proportionally to 20 hearts max.
Ship Wheel Info Message (fef8bb1)
Right-clicking a ship with a ship wheel now shows a helpful message.
For custom ships: explains wheels cannot be added to assembled ships
and to use sneak+right-click for menu. For prefab ships: explains
wheels are for custom builds and that prefabs come from ship kits.
Also fixes PlayerInteractEntityEvent firing twice by filtering to
main hand only. Motivated by user confusion in issues:
#1
#3
BigShip Seats (e9f5c4c)
Added 3 new seats to the big ship prefab: two side passenger seats
and one crow's nest seat.
BUG FIXES
---------
Banner Rendering Fix (e164326)
Fixed multiple banner display issues on ships:
- Plain banners now detected: Previously only patterned banners
worked. Now checks for banner_rotation and banner_facing keys in
addition to banner_patterns, so any banner item works.
- Floor banner rotation corrected: Banners now face the correct
direction.
- Wall banner positioning fixed: Correct Y offset and Z offset
toward wall.
- Code refactor: Extracted calculateBannerTransform() helper to
eliminate duplication across 3 locations.
Ladder Duplication Bug Fix (dfd105b)
Fixed blocks that need support (ladders, torches, etc.) dropping as
items during ship assembly.
Root cause: isAttachable() was incomplete, causing supported blocks
to be removed after their support blocks.
Solution: Refactored to use precomputed EnumSet for O(1) lookups.
Added missing blocks: ladder, lantern, bell, candle, repeater,
comparator, tripwire, rail, redstone wire. Added copper torches and
copper lanterns to allowed blocks list.
Chunk Test Improvements (c62495b)
Increased CHUNK_UNLOAD_WAIT_MS from 5s to 20s for more reliable
chunk unloading in tests. Added /forceload query verification after forceload
removal.
**New Features**
**Wider Minecraft Version Support**
- New compatibility layer supports Minecraft 1.21.1 through 1.21.11
- purpur support in addition to existing paper support
- Pre-1.21.2: Uses S+Space for airship descent (sprint unavailable in packet)
**Better ship collider handling**
- standing on ship decks works more reliably, no longer bugs out as much
**/blockships dismount Command**
- New command allows players to force-dismount from ships when normal methods fail
- Permission: `blockships.dismount` (default: true)
**Health Regeneration Enabled by Default**
- All ship types now regenerate 1.0 HP/second by default (was 0.0)
- Applies to: galley, airship, skiff, and custom ships
**Performance & Stability Improvements**
- Reduced GC pressure via object pooling (33+ reusable work objects)
- Async I/O for ship recovery prevents main thread blocking during chunk loads
- Thread-safe steering packet handling with cached reflection methods
- Early termination in terrain collision detection
---
**Bug Fixes**
**Sneak-to-Dismount for Shulker Seats**
- Fixed sneak (shift) dismount for Shulker seats across all Minecraft versions
- Version-specific packet handling for 1.21.2+ and 1.21.3+ formats
- Applies to all ship passengers, not just the driver
**Ship Entity Persistence on Player Disconnect**
- New `PlayerQuitEvent` and `PlayerKickEvent` handlers
- Ejects player from ship seat before disconnect completes
- Prevents vanilla Minecraft from removing ridden entities
- Properly frees seat for other players
**Dismount Re-mount Prevention**
- Fixed: Players being forced back into seats after intentional dismount
- `updateCollisionPositions()` now checks `occupiedSeatIndices` before re-mounting
- `freeSeat()` removes seat from occupied set, preventing re-mount
**Input State Cleanup on Driver Exit**
- `freeSeat()` now clears ALL input flags when driver exits
- Prevents ships from continuing movement with stale input state
- Airships get `currentYVelocity = 0`; water ships snap to neutral buoyancy
**Passenger Relationship Verification**
- Added every-tick check that shulker is still passenger of carrier
- Fixes broken relationships on chunk reload
- Re-adds passenger if relationship breaks (even on stationary ships)
**Collision Shulker Spawn Error Handling**
- Wrapped collider spawn in try-catch blocks
- Cleans up dangling carriers/shulkers on failure
- Prevents resource leaks and NPEs during configuration
**Attribute Compatibility Fixes**
- MAX_HEALTH and SCALE access wrapped with null checks and version comatibilities
- Health regeneration wrapped in try-catch to prevent tick crashes
**Pre-1.21.9 Display Rotation Fix**
- Added `spawnYaw` tracking for display rotation compatibility
- Prevents double-rotation bug on older versions
- Display rotation uses delta from spawn instead of absolute yaw
**Removed Non-functional Deck Physics**
- Deleted `applyDeckPhysics()` and `pushPlayerOutOfShulker()` methods
- These caused buggy movements on ships, and were non-functional, removed them
## Ship Damage Feedback & Audio System
- Added visual & audio damage feedback
- Added ship movement sounds
- Added chat message alerts to riders when their ship is destroyed
- Added configurable volume controls in `config.yml` for damage, movement, cannon, and lead break sounds
## Interactive Help System
- Info button in ship wheel menu with question mark player head
- Paginated help book covering controls, getting started, riding, menu, cannons, weight/buoyancy
## Drowned Ship Captains
- Rare drowned mobs that naturally spawn holding ship wheels (5% default chance)
- Guaranteed ship wheel drop on death
- New command: `/blockships spawndrowned`
- can be disabled in `config.yml`
## Ship Recovery System Improvements
Major architectural improvements for ships spanning multiple chunks, but it's still buggy. Try not to have ships spanning more than 2-3 chunks.
## Misc
- **Configuration validation** - New `ConfigValidator` class for detecting config mismatches
- **New blocks** - Added hay bales, bells, flower pots, candles, cauldron variants to `blocks.yml`
- **Buoyancy fix** - Fixed kelp and seagrass detection (they aren't actually waterlogged)
- **Collision tuning** - Reduced terrain collision feedback on prefab small ship
### New Features
- **Recipe Unlock System**: BlockShips recipes now unlock when players complete a configurable advancement (default: `smelt_iron`)
- **Automatic Recipe Granting**: Players who already have the required advancement will receive recipes when they join the server
- **New Config Option**: Added `recipe-unlock-advancement` setting to customize which advancement unlocks BlockShips recipes
### Bug Fixes
- **Fixed Ship Persistence**: Ships now properly persist across server restarts. Previously, ships were being destroyed during shutdown before Minecraft could save them
- **Fixed `destroyall` Command**: The command now properly cleans up YAML storage files when removing ships
- **ProtocolLib Warning**: Added a warning message when running BlockShips commands if ProtocolLib is not installed
### Technical Changes
- Added `ShipRegistry.clear()` method that clears the in-memory registry without destroying entities, allowing proper persistence
- Improved shutdown handling to preserve entities for natural Minecraft persistence