collision.ship-to-ship-enabled: false in config.yml--only filter to bot test runnersShips now physically collide with each other. A global ShipCollisionCoordinator singleton runs once per server tick before individual ship ticks, com✨g 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:
The narrow phase uses axis-aligned binning for performance:
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:
# 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):
ships:
smallship:
collision:
mass: 100.0 # heavier = harder to push
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:
sounds:
# Volume multiplier for ship-to-ship collision sounds
# (0.0 = disabled, 1.0 = normal)
ship-collision-volume: 0.15
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 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.
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.
Post-launch review caught several issues in ShipCollisionCoordinator:
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.
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.
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.
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.
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.
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.
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).
Increased particle spread and count for both ship-to-ship collision dust and ship damage smoke effects to make them more visible.
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.
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.
/blockships highlightcolliders debug commandProtocolLib 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.
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.
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.
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 com✨g 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.
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:
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.
Ref: https://github.com/def9a2a4/BlockShips/issues/15
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.
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
Better ship collider handling
/blockships dismount Command
blockships.dismount (default: true)Health Regeneration Enabled by Default
Performance & Stability Improvements
Bug Fixes
Sneak-to-Dismount for Shulker Seats
Ship Entity Persistence on Player Disconnect
PlayerQuitEvent and PlayerKickEvent handlersDismount Re-mount Prevention
updateCollisionPositions() now checks occupiedSeatIndices before re-mountingfreeSeat() removes seat from occupied set, preventing re-mountInput State Cleanup on Driver Exit
freeSeat() now clears ALL input flags when driver exitscurrentYVelocity = 0; water ships snap to neutral buoyancyPassenger Relationship Verification
Collision Shulker Spawn Error Handling
Attribute Compatibility Fixes
Pre-1.21.9 Display Rotation Fix
spawnYaw tracking for display rotation compatibilityRemoved Non-functional Deck Physics
applyDeckPhysics() and pushPlayerOutOfShulker() methods
build ships or airships out of blocks and sail or fly them smoothly - without client side mods or resource packs!