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 + ); + } }