
BlockShips
build ships or airships out of blocks and sail or fly them smoothly - without client side mods or resource packs!
BlockShips 0.0.15 - ship collision update
Список изменений
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: falsein 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
--onlyfilter to bot test runners - issue is bot inputs persist after it exists the ship
- Added version-based test skips and
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, 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:
- 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:
# 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
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:
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:
-
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: 0in config,totalMasswould be 0, producing NaN forces that propagate through the physics system. Both masses now clamped to minimum 1.0. -
Allocation reduction:
shipsByWorldlists are now cleared in-place instead of dropped and re-created every tick. Removed a shared mutableZERO_FORCEstatic constant that could be corrupted by future callers. -
Midpoint origin precision:
(float)(a + b) * 0.5fcast 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.
