From 50c0a1175710aa9858987da06b579b25a00a3ad8 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Fri, 14 Aug 2026 22:54:45 +0200 Subject: [PATCH] fix(player): switch to a virtual border --- .../cygnus/player/CygnusPlayer.java | 43 ++++++++----- .../player/CygnusPlayerHeartbeatTest.java | 62 +++++++++++++++++++ 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java b/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java index ba411e81..cf60254f 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java +++ b/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java @@ -6,7 +6,7 @@ import net.minestom.server.entity.attribute.AttributeModifier; import net.minestom.server.entity.attribute.AttributeOperation; import net.minestom.server.network.packet.server.play.EntityAttributesPacket; -import net.minestom.server.network.packet.server.play.WorldBorderWarningReachPacket; +import net.minestom.server.network.packet.server.play.InitializeWorldBorderPacket; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.sound.SoundEvent; @@ -17,11 +17,21 @@ @SuppressWarnings("java:S3252") public final class CygnusPlayer extends InstanceSwitchChunkPlayer { + static final int PORTAL_TELEPORT_BOUNDARY = 29_999_984; + + /** + * Radius (in blocks) of the virtual, per-player world border used to fake the heartbeat + * vignette. It is recentered on the player every tick, so the real client-side distance to + * its edge is always exactly this value, independent of the instance's actual world border. + */ + static final double FAKE_BORDER_RADIUS = 50.0; + static final double FAKE_BORDER_DIAMETER = FAKE_BORDER_RADIUS * 2.0; + static final AttributeModifier SPEED_MODIFIER_SPRINTING = - new AttributeModifier(Key.key("cygnus:sprinting"), 0.25, AttributeOperation.ADD_MULTIPLIED_TOTAL); + new AttributeModifier(Key.key("cygnus","sprinting"), 0.25, AttributeOperation.ADD_MULTIPLIED_TOTAL); static final AttributeModifier DISABLED_SPRINT_MODIFIER = - new AttributeModifier(Key.key("cygnus:sprinting"), 0.0, AttributeOperation.ADD_MULTIPLIED_TOTAL); + new AttributeModifier(Key.key("cygnus", "sprinting"), 0.0, AttributeOperation.ADD_MULTIPLIED_TOTAL); private static final float HEALTH_THRESHOLD = 6.0f; // 3 hearts private static final int MAX_INTERVAL_TICKS = 36; // Every 1.8s (slow, subtle pulse at start) @@ -107,21 +117,17 @@ public void tickHeartbeat() { float intensity = Math.clamp(1.0f - (health / HEALTH_THRESHOLD), 0.0f, 1.0f); - double distanceToBorder = 29_999_984.0; - var instance = getInstance(); - if (instance != null && instance.getWorldBorder() != null) { - var border = instance.getWorldBorder(); - double radius = border.diameter() / 2.0; - double dx = Math.abs(getPosition().x() - border.centerX()); - double dz = Math.abs(getPosition().z() - border.centerZ()); - distanceToBorder = Math.max(1.0, radius - Math.max(dx, dz)); - } - // Non-linear visual curve makes the red border vignette stronger earlier and very intense at low HP float visualIntensity = (float) Math.pow(intensity, 0.6); float clampedIntensity = Math.min(visualIntensity, 0.995f); - int warningBlocks = (int) (distanceToBorder / (1.0f - clampedIntensity)); - sendPacket(new WorldBorderWarningReachPacket(warningBlocks)); + int warningBlocks = (int) (FAKE_BORDER_RADIUS / (1.0f - clampedIntensity)); + + var position = getPosition(); + sendPacket(new InitializeWorldBorderPacket( + position.x(), position.z(), + FAKE_BORDER_DIAMETER, FAKE_BORDER_DIAMETER, 0L, + PORTAL_TELEPORT_BOUNDARY, 0, warningBlocks + )); float intervalFactor = (float) Math.pow(intensity, 0.85); int targetInterval = (int) (MAX_INTERVAL_TICKS - (intervalFactor * (MAX_INTERVAL_TICKS - MIN_INTERVAL_TICKS))); @@ -151,7 +157,9 @@ private void playHeartbeatSound(float intensity) { private void resetHeartbeat() { heartbeatActive = false; heartbeatTicks = 0; - sendPacket(new WorldBorderWarningReachPacket(0)); + // Restores the real (instance-wide) world border after the per-tick fake one used for + // the vignette, so the client stops seeing the small virtual border we sent it. + sendPacket(getInstance().createInitializeWorldBorderPacket()); } /** @@ -163,6 +171,9 @@ public boolean isHeartbeatActive() { return heartbeatActive; } + /** + * {@inheritDoc} + */ @Override public EntityAttributesPacket getPropertiesPacket() { return super.getPropertiesPacket(); diff --git a/game/src/test/java/net/onelitefeather/cygnus/player/CygnusPlayerHeartbeatTest.java b/game/src/test/java/net/onelitefeather/cygnus/player/CygnusPlayerHeartbeatTest.java index d8fcaabc..6f49a8fe 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/player/CygnusPlayerHeartbeatTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/player/CygnusPlayerHeartbeatTest.java @@ -1,7 +1,10 @@ package net.onelitefeather.cygnus.player; +import net.minestom.server.coordinate.Pos; import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.server.play.InitializeWorldBorderPacket; import net.minestom.testing.Env; +import net.minestom.testing.TestConnection; import net.onelitefeather.cygnus.CygnusPlayerTestBase; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -23,6 +26,65 @@ void testHeartbeatInactiveAboveThreshold(@NotNull Env env) { env.destroyInstance(instance, true); } + @Test + void testHeartbeatSendsVirtualBorderCenteredOnPlayer(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + TestConnection connection = env.createConnection(); + Pos spawnPos = new Pos(120.5, 40, -75.25); + CygnusPlayer player = (CygnusPlayer) connection.connect(instance, spawnPos); + + player.setHealth(4.0f); + var collector = connection.trackIncoming(InitializeWorldBorderPacket.class); + player.tickHeartbeat(); + + collector.assertSingle(packet -> { + assertEquals(spawnPos.x(), packet.x(), 0.001); + assertEquals(spawnPos.z(), packet.z(), 0.001); + assertEquals(100.0, packet.newDiameter(), 0.001); + }); + + env.destroyInstance(instance, true); + } + + @Test + void testHeartbeatWarningBlocksStayWithinIntRangeNearDeath(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + TestConnection connection = env.createConnection(); + CygnusPlayer player = (CygnusPlayer) connection.connect(instance, Pos.ZERO); + + player.setHealth(0.5f); + var collector = connection.trackIncoming(InitializeWorldBorderPacket.class); + player.tickHeartbeat(); + + collector.assertSingle(packet -> assertTrue(packet.warningBlocks() <= 10_000, + "warningBlocks should stay in a sane range, was " + packet.warningBlocks())); + + env.destroyInstance(instance, true); + } + + @Test + void testHeartbeatResetRestoresRealBorder(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + TestConnection connection = env.createConnection(); + CygnusPlayer player = (CygnusPlayer) connection.connect(instance, Pos.ZERO); + + player.setHealth(4.0f); + player.tickHeartbeat(); + assertTrue(player.isHeartbeatActive()); + + player.setHealth(20.0f); + var collector = connection.trackIncoming(InitializeWorldBorderPacket.class); + player.tickHeartbeat(); + + collector.assertSingle(packet -> { + assertEquals(instance.getWorldBorder().centerX(), packet.x(), 0.001); + assertEquals(instance.getWorldBorder().centerZ(), packet.z(), 0.001); + assertEquals(instance.getWorldBorder().diameter(), packet.newDiameter(), 0.001); + }); + + env.destroyInstance(instance, true); + } + @Test void testHeartbeatActiveBelowThreshold(@NotNull Env env) { Instance instance = env.createFlatInstance();