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.
This commit is contained in:
2026-04-10 02:03:37 +03:00
parent b9a7ccf4be
commit 2733fb7fe1
3 changed files with 128 additions and 7 deletions

View File

@@ -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<Level> dimension,
double x,
double y,
double z,
float yaw,
float pitch
) {
public static final Codec<DeathLockSnapshot> 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()
);
}
}

View File

@@ -1,5 +1,7 @@
package net.respawnbackoff; package net.respawnbackoff;
import java.util.Optional;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
@@ -7,15 +9,17 @@ public record RespawnBackoffData(
int exponent, int exponent,
long lastEpochDay, long lastEpochDay,
long pendingDurationMs, long pendingDurationMs,
long cooldownEndEpochMs long cooldownEndEpochMs,
Optional<DeathLockSnapshot> 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<RespawnBackoffData> CODEC = RecordCodecBuilder.create(instance -> instance.group( public static final Codec<RespawnBackoffData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.fieldOf("exponent").forGetter(RespawnBackoffData::exponent), Codec.INT.fieldOf("exponent").forGetter(RespawnBackoffData::exponent),
Codec.LONG.fieldOf("last_epoch_day").forGetter(RespawnBackoffData::lastEpochDay), Codec.LONG.fieldOf("last_epoch_day").forGetter(RespawnBackoffData::lastEpochDay),
Codec.LONG.fieldOf("pending_duration_ms").forGetter(RespawnBackoffData::pendingDurationMs), 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)); ).apply(instance, RespawnBackoffData::new));
public boolean hasActiveCooldown(long nowMs) { public boolean hasActiveCooldown(long nowMs) {

View File

@@ -2,6 +2,9 @@ package net.respawnbackoff;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; 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.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation; 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.server.level.ServerPlayer;
import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.level.GameType; import net.minecraft.world.level.GameType;
public class RespawnBackoffMod implements ModInitializer { public class RespawnBackoffMod implements ModInitializer {
@@ -52,6 +59,7 @@ public class RespawnBackoffMod implements ModInitializer {
if (!player.isSpectator()) { if (!player.isSpectator()) {
player.setGameMode(GameType.SPECTATOR); player.setGameMode(GameType.SPECTATOR);
} }
data.deathLock().ifPresent(lock -> teleportToDeathLock(player, lock));
RespawnBackoffNetworking.sendCooldown(player, true, data.cooldownEndEpochMs()); RespawnBackoffNetworking.sendCooldown(player, true, data.cooldownEndEpochMs());
} else { } else {
RespawnBackoffNetworking.sendCooldown(player, false, 0L); RespawnBackoffNetworking.sendCooldown(player, false, 0L);
@@ -85,7 +93,13 @@ public class RespawnBackoffMod implements ModInitializer {
long pendingMs = waitMinutes * 60_000L; long pendingMs = waitMinutes * 60_000L;
int nextExponent = Math.min(exponent + 1, 6); 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); player.setAttached(RESPAWN_BACKOFF, next);
} }
@@ -101,15 +115,21 @@ public class RespawnBackoffMod implements ModInitializer {
data.exponent(), data.exponent(),
data.lastEpochDay(), data.lastEpochDay(),
0L, 0L,
end end,
data.deathLock()
); );
player.setAttached(RESPAWN_BACKOFF, next); player.setAttached(RESPAWN_BACKOFF, next);
player.setGameMode(GameType.SPECTATOR); player.setGameMode(GameType.SPECTATOR);
data.deathLock().ifPresent(lock -> teleportToDeathLock(player, lock));
RespawnBackoffNetworking.sendCooldown(player, true, end); RespawnBackoffNetworking.sendCooldown(player, true, end);
} }
private static void tickPlayer(ServerPlayer player, long nowMs) { private static void tickPlayer(ServerPlayer player, long nowMs) {
RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT); RespawnBackoffData data = player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT);
if (data.hasActiveCooldown(nowMs)) {
data.deathLock().ifPresent(lock -> enforceDeathLock(player, lock));
}
if (!data.hasActiveCooldown(nowMs)) { if (!data.hasActiveCooldown(nowMs)) {
return; return;
} }
@@ -135,9 +155,11 @@ public class RespawnBackoffMod implements ModInitializer {
data.exponent(), data.exponent(),
data.lastEpochDay(), data.lastEpochDay(),
0L, 0L,
0L 0L,
Optional.empty()
); );
player.setAttached(RESPAWN_BACKOFF, cleared); player.setAttached(RESPAWN_BACKOFF, cleared);
teleportToRespawnPoint(player);
if (player.isSpectator()) { if (player.isSpectator()) {
player.setGameMode(GameType.SURVIVAL); player.setGameMode(GameType.SURVIVAL);
} }
@@ -155,7 +177,8 @@ public class RespawnBackoffMod implements ModInitializer {
0, 0,
today, today,
0L, 0L,
data.cooldownEndEpochMs() data.cooldownEndEpochMs(),
data.deathLock()
); );
player.setAttached(RESPAWN_BACKOFF, next); player.setAttached(RESPAWN_BACKOFF, next);
} }
@@ -163,4 +186,60 @@ public class RespawnBackoffMod implements ModInitializer {
public static boolean isPenaltyActive(ServerPlayer player, long nowMs) { public static boolean isPenaltyActive(ServerPlayer player, long nowMs) {
return player.getAttachedOrElse(RESPAWN_BACKOFF, RespawnBackoffData.DEFAULT).hasActiveCooldown(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<RelativeMovement> 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
);
}
} }