From 2733fb7fe11388ee62185e36eb1b98e76f2fff82 Mon Sep 17 00:00:00 2001 From: Anatoly Kopyl Date: Fri, 10 Apr 2026 02:03:37 +0300 Subject: [PATCH] Lock dead players at death site; teleport to spawn when cooldown ends Persist a death-position snapshot in player attachment data, move spectators back each tick if they drift, and on rejoin during an active wait. When the respawn timer completes (or skip command), teleport to bed or world spawn then restore survival. Uses ServerPlayer.teleportTo with empty RelativeMovement set. --- .../net/respawnbackoff/DeathLockSnapshot.java | 38 ++++++++ .../respawnbackoff/RespawnBackoffData.java | 10 ++- .../net/respawnbackoff/RespawnBackoffMod.java | 87 ++++++++++++++++++- 3 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/main/java/net/respawnbackoff/DeathLockSnapshot.java diff --git a/src/main/java/net/respawnbackoff/DeathLockSnapshot.java b/src/main/java/net/respawnbackoff/DeathLockSnapshot.java new file mode 100644 index 0000000..2adcf9e --- /dev/null +++ b/src/main/java/net/respawnbackoff/DeathLockSnapshot.java @@ -0,0 +1,38 @@ +package net.respawnbackoff; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; + +public record DeathLockSnapshot( + ResourceKey dimension, + double x, + double y, + double z, + float yaw, + float pitch +) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceKey.codec(Registries.DIMENSION).fieldOf("dimension").forGetter(DeathLockSnapshot::dimension), + Codec.DOUBLE.fieldOf("x").forGetter(DeathLockSnapshot::x), + Codec.DOUBLE.fieldOf("y").forGetter(DeathLockSnapshot::y), + Codec.DOUBLE.fieldOf("z").forGetter(DeathLockSnapshot::z), + Codec.FLOAT.fieldOf("yaw").forGetter(DeathLockSnapshot::yaw), + Codec.FLOAT.fieldOf("pitch").forGetter(DeathLockSnapshot::pitch) + ).apply(instance, DeathLockSnapshot::new)); + + public static DeathLockSnapshot from(ServerPlayer player) { + return new DeathLockSnapshot( + player.level().dimension(), + player.getX(), + player.getY(), + player.getZ(), + player.getYRot(), + player.getXRot() + ); + } +} diff --git a/src/main/java/net/respawnbackoff/RespawnBackoffData.java b/src/main/java/net/respawnbackoff/RespawnBackoffData.java index 416dcb8..3dae382 100644 --- a/src/main/java/net/respawnbackoff/RespawnBackoffData.java +++ b/src/main/java/net/respawnbackoff/RespawnBackoffData.java @@ -1,5 +1,7 @@ package net.respawnbackoff; +import java.util.Optional; + import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -7,15 +9,17 @@ public record RespawnBackoffData( int exponent, long lastEpochDay, long pendingDurationMs, - long cooldownEndEpochMs + long cooldownEndEpochMs, + Optional deathLock ) { - public static final RespawnBackoffData DEFAULT = new RespawnBackoffData(0, 0L, 0L, 0L); + public static final RespawnBackoffData DEFAULT = new RespawnBackoffData(0, 0L, 0L, 0L, Optional.empty()); public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.INT.fieldOf("exponent").forGetter(RespawnBackoffData::exponent), Codec.LONG.fieldOf("last_epoch_day").forGetter(RespawnBackoffData::lastEpochDay), Codec.LONG.fieldOf("pending_duration_ms").forGetter(RespawnBackoffData::pendingDurationMs), - Codec.LONG.fieldOf("cooldown_end_ms").forGetter(RespawnBackoffData::cooldownEndEpochMs) + Codec.LONG.fieldOf("cooldown_end_ms").forGetter(RespawnBackoffData::cooldownEndEpochMs), + DeathLockSnapshot.CODEC.optionalFieldOf("death_lock").forGetter(RespawnBackoffData::deathLock) ).apply(instance, RespawnBackoffData::new)); public boolean hasActiveCooldown(long nowMs) { diff --git a/src/main/java/net/respawnbackoff/RespawnBackoffMod.java b/src/main/java/net/respawnbackoff/RespawnBackoffMod.java index 3e08f26..47eeb48 100644 --- a/src/main/java/net/respawnbackoff/RespawnBackoffMod.java +++ b/src/main/java/net/respawnbackoff/RespawnBackoffMod.java @@ -2,6 +2,9 @@ package net.respawnbackoff; import java.time.LocalDate; import java.time.ZoneOffset; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; @@ -11,8 +14,12 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.RelativeMovement; import net.minecraft.world.level.GameType; public class RespawnBackoffMod implements ModInitializer { @@ -52,6 +59,7 @@ public class RespawnBackoffMod implements ModInitializer { if (!player.isSpectator()) { player.setGameMode(GameType.SPECTATOR); } + data.deathLock().ifPresent(lock -> teleportToDeathLock(player, lock)); RespawnBackoffNetworking.sendCooldown(player, true, data.cooldownEndEpochMs()); } else { RespawnBackoffNetworking.sendCooldown(player, false, 0L); @@ -85,7 +93,13 @@ public class RespawnBackoffMod implements ModInitializer { long pendingMs = waitMinutes * 60_000L; int nextExponent = Math.min(exponent + 1, 6); - RespawnBackoffData next = new RespawnBackoffData(nextExponent, today, pendingMs, 0L); + RespawnBackoffData next = new RespawnBackoffData( + nextExponent, + today, + pendingMs, + 0L, + Optional.of(DeathLockSnapshot.from(player)) + ); player.setAttached(RESPAWN_BACKOFF, next); } @@ -101,15 +115,21 @@ public class RespawnBackoffMod implements ModInitializer { data.exponent(), data.lastEpochDay(), 0L, - end + end, + data.deathLock() ); player.setAttached(RESPAWN_BACKOFF, next); player.setGameMode(GameType.SPECTATOR); + data.deathLock().ifPresent(lock -> teleportToDeathLock(player, lock)); RespawnBackoffNetworking.sendCooldown(player, true, end); } private static void tickPlayer(ServerPlayer player, long nowMs) { RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT); + if (data.hasActiveCooldown(nowMs)) { + data.deathLock().ifPresent(lock -> enforceDeathLock(player, lock)); + } + if (!data.hasActiveCooldown(nowMs)) { return; } @@ -135,9 +155,11 @@ public class RespawnBackoffMod implements ModInitializer { data.exponent(), data.lastEpochDay(), 0L, - 0L + 0L, + Optional.empty() ); player.setAttached(RESPAWN_BACKOFF, cleared); + teleportToRespawnPoint(player); if (player.isSpectator()) { player.setGameMode(GameType.SURVIVAL); } @@ -155,7 +177,8 @@ public class RespawnBackoffMod implements ModInitializer { 0, today, 0L, - data.cooldownEndEpochMs() + data.cooldownEndEpochMs(), + data.deathLock() ); player.setAttached(RESPAWN_BACKOFF, next); } @@ -163,4 +186,60 @@ public class RespawnBackoffMod implements ModInitializer { public static boolean isPenaltyActive(ServerPlayer player, long nowMs) { return player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT).hasActiveCooldown(nowMs); } + + private static final double DEATH_LOCK_EPSILON_SQ = 1.0e-6; + + private static void teleportToDeathLock(ServerPlayer player, DeathLockSnapshot lock) { + MinecraftServer server = player.server; + ServerLevel level = server.getLevel(lock.dimension()); + if (level == null) { + level = server.overworld(); + BlockPos spawn = level.getSharedSpawnPos(); + teleportAbsolute(player, level, spawn.getX() + 0.5, spawn.getY(), spawn.getZ() + 0.5, level.getSharedSpawnAngle(), 0.0f); + return; + } + teleportAbsolute(player, level, lock.x(), lock.y(), lock.z(), lock.yaw(), lock.pitch()); + } + + private static void teleportAbsolute(ServerPlayer player, ServerLevel level, double x, double y, double z, float yaw, float pitch) { + Set relatives = Collections.emptySet(); + player.teleportTo(level, x, y, z, relatives, yaw, pitch); + } + + private static void enforceDeathLock(ServerPlayer player, DeathLockSnapshot lock) { + if (!player.level().dimension().equals(lock.dimension())) { + teleportToDeathLock(player, lock); + return; + } + double dx = player.getX() - lock.x(); + double dy = player.getY() - lock.y(); + double dz = player.getZ() - lock.z(); + if (dx * dx + dy * dy + dz * dz > DEATH_LOCK_EPSILON_SQ) { + teleportToDeathLock(player, lock); + } + } + + private static void teleportToRespawnPoint(ServerPlayer player) { + MinecraftServer server = player.server; + ServerLevel respawnLevel = server.getLevel(player.getRespawnDimension()); + if (respawnLevel == null) { + respawnLevel = server.overworld(); + } + BlockPos bed = player.getRespawnPosition(); + float yaw = player.getRespawnAngle(); + if (bed != null) { + teleportAbsolute(player, respawnLevel, bed.getX() + 0.5, bed.getY(), bed.getZ() + 0.5, yaw, 0.0f); + return; + } + BlockPos spawn = respawnLevel.getSharedSpawnPos(); + teleportAbsolute( + player, + respawnLevel, + spawn.getX() + 0.5, + spawn.getY(), + spawn.getZ() + 0.5, + respawnLevel.getSharedSpawnAngle(), + 0.0f + ); + } }