From bac3192d07c159b57895b453efae4315843cbb38 Mon Sep 17 00:00:00 2001 From: Artemii Bigdan Date: Tue, 6 Oct 2026 20:13:27 +0200 Subject: [PATCH] fix: keep sub-level spawn on cancelled spawn set setRespawnPosition unconditionally removed Sable's tracked sub-level respawn point at HEAD, before the method body (and NeoForge's PlayerSetSpawnEvent hook) ran. Mods that cancel a spawn set - e.g. Traveller's Backpack treating a sleeping bag as a temporary bed - then left the tracking UUID gone while SpawnX/Y/Z still held sub-level-local coords, so the next death respawned out of world / crashed (sable#1370). Snapshot the respawn fields and tracked point at HEAD, and only drop the tracked point at RETURN when the call actually committed a non-sub-level spawn/reset. A no-op/cancelled call keeps the point; a freshly generated sub-level point is never clobbered. Loader-agnostic: no loader-specific API used. --- .../respawn_point/ServerPlayerMixin.java | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/respawn_point/ServerPlayerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/respawn_point/ServerPlayerMixin.java index 6eb602049..723f308c4 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/respawn_point/ServerPlayerMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/respawn_point/ServerPlayerMixin.java @@ -29,6 +29,7 @@ import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -53,6 +54,13 @@ public abstract class ServerPlayerMixin implements ServerPlayerRespawnExtension private UUID sable$respawnPoint = null; @Unique private Pair sable$queuedFreeze = null; + @Unique + @Nullable + private UUID sable$respawnPointBefore = null; + @Unique + private @Nullable BlockPos sable$respawnPosBefore = null; + @Unique + private @Nullable ResourceKey sable$respawnDimBefore = null; @Shadow public static Optional findRespawnAndUseSpawnBlock(final ServerLevel serverLevel, final BlockPos blockPos, final float f, final boolean bl, final boolean bl2) { @@ -72,36 +80,69 @@ public static Optional findRespawnAndUseSpawnBlock @Inject(method = "setRespawnPosition", at = @At("HEAD"), cancellable = true) private void sable$setRespawnPosition(final ResourceKey resourceKey, @Nullable final BlockPos blockPos, final float f, final boolean bl, final boolean sendMessage, final CallbackInfo ci) { - final ServerLevel level = this.serverLevel(); - final SubLevelTrackingPointSavedData data = SubLevelTrackingPointSavedData.getOrLoad(level); - - if (this.sable$respawnPoint != null) { - data.removeTrackingPoint(this.sable$respawnPoint); - this.sable$respawnPoint = null; + // Snapshot the state so a call that is ultimately a no-op (e.g. cancelled by another + // mod's PlayerSetSpawnEvent handler) does not destroy the tracked sub-level point. + this.sable$respawnPointBefore = this.sable$respawnPoint; + this.sable$respawnPosBefore = this.respawnPosition; + this.sable$respawnDimBefore = this.respawnDimension; + + if (blockPos == null) { + // Spawn is being reset; let vanilla/NeoForge handle it and reconcile at RETURN. + return; } - if (blockPos != null) { - final SubLevel trackingSubLevel = Sable.HELPER.getContaining(level, blockPos); + final ServerLevel level = this.serverLevel(); + final SubLevel trackingSubLevel = Sable.HELPER.getContaining(level, blockPos); - if (trackingSubLevel instanceof final ServerSubLevel serverSubLevel) { - this.sable$respawnPoint = data.generateTrackingPoint(Vec3.atCenterOf(blockPos), serverSubLevel); + if (trackingSubLevel instanceof final ServerSubLevel serverSubLevel) { + final SubLevelTrackingPointSavedData data = SubLevelTrackingPointSavedData.getOrLoad(level); - if (this.sable$respawnPoint != null) { - final boolean theSame = blockPos.equals(this.respawnPosition) && resourceKey.equals(this.respawnDimension); - if (sendMessage && !theSame) { - this.sendSystemMessage(Component.translatable("block.minecraft.set_spawn")); - } + if (this.sable$respawnPoint != null) { + data.removeTrackingPoint(this.sable$respawnPoint); + this.sable$respawnPoint = null; + } + + this.sable$respawnPoint = data.generateTrackingPoint(Vec3.atCenterOf(blockPos), serverSubLevel); - this.respawnPosition = blockPos; - this.respawnDimension = resourceKey; - this.respawnAngle = f; - this.respawnForced = bl; - ci.cancel(); + if (this.sable$respawnPoint != null) { + final boolean theSame = blockPos.equals(this.respawnPosition) && resourceKey.equals(this.respawnDimension); + if (sendMessage && !theSame) { + this.sendSystemMessage(Component.translatable("block.minecraft.set_spawn")); } + + this.respawnPosition = blockPos; + this.respawnDimension = resourceKey; + this.respawnAngle = f; + this.respawnForced = bl; + ci.cancel(); } } } + @Inject(method = "setRespawnPosition", at = @At("RETURN")) + private void sable$reconcileRespawnPoint(final ResourceKey resourceKey, @Nullable final BlockPos blockPos, final float f, final boolean bl, final boolean sendMessage, final CallbackInfo ci) { + final BlockPos posBefore = this.sable$respawnPosBefore; + final ResourceKey dimBefore = this.sable$respawnDimBefore; + this.sable$respawnPosBefore = null; + this.sable$respawnDimBefore = null; + + final boolean changed = !Objects.equals(this.respawnPosition, posBefore) + || !Objects.equals(this.respawnDimension, dimBefore); + + // Only drop the tracked point when this call actually committed a non-sub-level spawn (or + // a reset), and it is still the same point we entered the method with. A no-op call keeps + // the point intact; a freshly generated sub-level point is never clobbered. + if (changed + && this.sable$respawnPoint != null + && Objects.equals(this.sable$respawnPoint, this.sable$respawnPointBefore)) { + SubLevelTrackingPointSavedData.getOrLoad(this.serverLevel()) + .removeTrackingPoint(this.sable$respawnPoint); + this.sable$respawnPoint = null; + } + + this.sable$respawnPointBefore = null; + } + @Inject(method = "addAdditionalSaveData", at = @At("TAIL")) private void sable$addRespawnPoint(final CompoundTag compoundTag, final CallbackInfo ci) { if (this.sable$respawnPoint != null) {