From 5d4a632cb12a206e42df37aaa15aebbc653007bd Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:39:01 +0900 Subject: [PATCH 1/5] Add Waystones sub-level compatibility Waystones treats a waystone's block position as a stable world position, but a waystone placed on an assembled sub-level (e.g. an aircraft) is stored at the hidden plot-yard coordinate ~20.48M blocks out. Two problems followed: - Teleporting to such a waystone froze the server. Waystones' same-dimension warp calls ServerGamePacketListenerImpl#teleport directly, bypassing Sable's ServerPlayer#teleportTo projection, so the player was dropped into the plot-yard. Add a compatibility mixin that projects the teleport target out of the sub-level before the entity is moved. - Distance and XP cost were wrong while the sub-level was unloaded: with no live pose to project against, the raw plot-yard coordinate leaked through. Track a per-plot last-known pose in SubLevelContainer (set on allocate, refreshed on unload), persist it alongside plot occupancy, and have projectOutOfSubLevel fall back to it when no loaded sub-level is present. Server-side distance checks (cost, is_within_distance, /waystones list) now resolve an unloaded sub-level's approximate position load-free. Known limitation: the client GUI distance text still shows the raw distance for sub-levels never loaded on the client; fixing that needs last-known poses synced to clients. --- .../ryanhcode/sable/ActiveSableCompanion.java | 33 +++++++++- .../sable/api/sublevel/SubLevelContainer.java | 63 +++++++++++++++++++ .../WaystoneTeleportManagerMixin.java | 41 ++++++++++++ .../storage/SubLevelOccupancySavedData.java | 27 ++++++++ common/src/main/resources/sable.mixins.json | 1 + 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java diff --git a/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java b/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java index ddc8c23a..d9ed9969 100644 --- a/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java +++ b/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java @@ -171,7 +171,13 @@ public Iterable getAllIntersecting(final Level level, final BoundingBo public Vector3d projectOutOfSubLevel(final Level level, final Vector3dc pos, final Vector3d dest) { final SubLevel subLevel = this.getContaining(level, pos); - if (subLevel == null) return dest.set(pos); + if (subLevel == null) { + final Pose3dc lastPose = this.lastKnownContainingPose(level, pos.x(), pos.z()); + if (lastPose != null) { + return lastPose.transformPosition(pos, dest); + } + return dest.set(pos); + } final Pose3dc pose; if (level instanceof final LevelPoseProviderExtension extension) { @@ -192,7 +198,13 @@ public Vec3 projectOutOfSubLevel(final Level level, final Vec3 pos) { public Vec3 projectOutOfSubLevel(final Level level, final Position pos) { final SubLevel subLevel = this.getContaining(level, pos); - if (subLevel == null) return pos instanceof final Vec3 vec ? vec : new Vec3(pos.x(), pos.y(), pos.z()); + if (subLevel == null) { + final Pose3dc lastPose = this.lastKnownContainingPose(level, pos.x(), pos.z()); + if (lastPose != null) { + return JOMLConversion.toMojang(lastPose.transformPosition(JOMLConversion.toJOML(pos))); + } + return pos instanceof final Vec3 vec ? vec : new Vec3(pos.x(), pos.y(), pos.z()); + } final Pose3dc pose; if (level instanceof final LevelPoseProviderExtension extension) { @@ -204,6 +216,23 @@ public Vec3 projectOutOfSubLevel(final Level level, final Position pos) { return JOMLConversion.toMojang(pose.transformPosition(JOMLConversion.toJOML(pos))); } + /** + * Resolves the last-known pose of a held (unloaded) sub-level whose reserved plot contains the + * given world position, so distance/cost queries can project an unloaded sub-level's coordinate + * to its approximate world location without loading it back in. Returns {@code null} when the + * position is not inside any reserved plot (e.g. ordinary world positions), in which case the + * caller leaves the coordinate untouched. + */ + private @Nullable Pose3dc lastKnownContainingPose(final Level level, final double blockX, final double blockZ) { + final SubLevelContainer container = SubLevelContainer.getContainer(level); + if (container == null) { + return null; + } + final int chunkX = Mth.floor(blockX) >> SectionPos.SECTION_BITS; + final int chunkZ = Mth.floor(blockZ) >> SectionPos.SECTION_BITS; + return container.getLastKnownPose(chunkX, chunkZ); + } + @Override public @Nullable T runIncludingSubLevels(final Level level, final Vec3 origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction converter) { return this.runIncludingSubLevels(level, (Position) origin, shouldCheckOrigin, subLevel, converter); diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java index 70168e2e..a2117237 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java @@ -58,6 +58,13 @@ public abstract class SubLevelContainer { * The occupancy of the plotgrid, including loaded and unloaded plots */ private final BitSet occupancy; + /** + * The last-known pose of each plot, indexed identically to {@link #subLevels}. Set while a + * sub-level is allocated and refreshed when it unloads, so that distance and cost queries can + * resolve a held (unloaded) sub-level's approximate world position without loading it back in. + * {@code null} for unoccupied plots. + */ + private final Pose3d[] lastKnownPoses; /** * All observers/listeners for the plotgrid */ @@ -135,6 +142,7 @@ public SubLevelContainer(final Level level, final int logSideLength, final int l this.originZ = originZ; this.subLevels = new SubLevel[(1 << logSideLength) * (1 << logSideLength)]; this.occupancy = new BitSet(this.subLevels.length); + this.lastKnownPoses = new Pose3d[this.subLevels.length]; } /** @@ -261,6 +269,7 @@ public SubLevel allocateSubLevel(final UUID uuid, final int x, final int z, fina final int index = this.getIndex(x, z); this.subLevels[index] = subLevel; this.getOccupancy().set(index); + this.lastKnownPoses[index] = new Pose3d(pose); this.allSubLevels.add(subLevel); this.subLevelsByUUID.put(subLevel.getUniqueId(), subLevel); this.observers.forEach(observer -> observer.onSubLevelAdded(subLevel)); @@ -487,7 +496,61 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason if (reason == SubLevelRemovalReason.REMOVED) { this.getOccupancy().clear(index); + this.lastKnownPoses[index] = null; + } else { + // Still held, just unloaded: remember where it was so distance/cost queries can resolve + // its approximate world position without loading the sub-level back in. + this.lastKnownPoses[index] = new Pose3d(subLevel.logicalPose()); + } + + if (this.level instanceof final ServerLevel serverLevel) { + SubLevelOccupancySavedData.getOrLoad(serverLevel).setDirty(); + } + } + + /** + * Returns the last-known pose of the (possibly unloaded) sub-level whose plot contains the given + * global chunk position. While the sub-level is loaded this is its allocation/last-unload pose, + * which is stale - callers that need the live pose should use {@link #getContaining} first and + * fall back to this only when no loaded sub-level is present. + * + * @return the last-known pose, or {@code null} if no plot is reserved at that position + */ + public @Nullable Pose3d getLastKnownPose(final int chunkX, final int chunkZ) { + final int plotX = (chunkX >> this.logPlotSize) - this.originX; + final int plotZ = (chunkZ >> this.logPlotSize) - this.originZ; + final int sideLength = 1 << this.logSideLength; + if (plotX < 0 || plotX >= sideLength || plotZ < 0 || plotZ >= sideLength) { + return null; + } + return this.lastKnownPoses[this.getIndex(plotX, plotZ)]; + } + + /** + * @return the pose to persist for the plot at the given index: the live pose when the sub-level + * is currently loaded, otherwise the last-known pose captured when it was unloaded. + */ + @ApiStatus.Internal + public @Nullable Pose3d getPersistablePose(final int index) { + if (index < 0 || index >= this.subLevels.length) { + return null; + } + final SubLevel loaded = this.subLevels[index]; + if (loaded != null) { + return new Pose3d(loaded.logicalPose()); + } + return this.lastKnownPoses[index]; + } + + /** + * Restores a persisted last-known pose for the plot at the given index. + */ + @ApiStatus.Internal + public void setLastKnownPose(final int index, final Pose3d pose) { + if (index < 0 || index >= this.lastKnownPoses.length) { + return; } + this.lastKnownPoses[index] = pose; } /** diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java new file mode 100644 index 00000000..c8d7cc76 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java @@ -0,0 +1,41 @@ +package dev.ryanhcode.sable.mixin.compatibility.waystones; + +import com.llamalad7.mixinextras.sugar.Local; +import dev.ryanhcode.sable.Sable; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +/** + * Waystones stores a waystone's location as the raw block position of its block entity. For a waystone + * placed on an assembled Sable sub-level (e.g. an aircraft) that position is the hidden plot-yard + * coordinate (~20.48M blocks out), not the contraption's rendered location. + * + *

Sable already projects {@link net.minecraft.server.level.ServerPlayer#teleportTo} out of the + * sub-level, but Waystones' same-dimension warp path teleports via + * {@code connection.teleport(...)} directly, bypassing that projection. The player is dropped into the + * plot-yard, which desyncs the sub-level stream ("Received a sub-level movement packet for a + * non-existent sub-level") and freezes the server thread. + * + *

Project the teleport target out of the sub-level before the entity is moved. This affects only + * the entity placement (the warp-plate / modifier block-entity lookups still use the original plot + * position via {@code destination.location()}). + * {@link dev.ryanhcode.sable.api.SubLevelHelper#projectOutOfSubLevel} is a no-op for positions that + * are not inside a sub-level, so this is safe for ordinary warps and idempotent with the + * cross-dimension {@code teleportTo} path that Sable already handles. + */ +@Mixin(targets = "net.blay09.mods.waystones.core.WaystoneTeleportManager", remap = false) +public class WaystoneTeleportManagerMixin { + + @ModifyVariable( + method = "teleportEntity(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/Direction;)Lnet/minecraft/world/entity/Entity;", + at = @At("HEAD"), + argsOnly = true, + index = 2 + ) + private static Vec3 sable$projectTeleportTargetOutOfSubLevel(final Vec3 targetPos3d, @Local(argsOnly = true) final ServerLevel targetWorld) { + return Sable.HELPER.projectOutOfSubLevel(targetWorld, targetPos3d); + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java index eb1bec96..a85dca45 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java @@ -1,8 +1,12 @@ package dev.ryanhcode.sable.sublevel.storage; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; +import dev.ryanhcode.sable.companion.math.Pose3d; +import dev.ryanhcode.sable.util.SableNBTUtils; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.world.level.saveddata.SavedData; @@ -45,6 +49,14 @@ private static SubLevelOccupancySavedData load(final ServerLevel level, final Co final BitSet occupancy = container.getOccupancy(); occupancy.clear(); occupancy.or(occupancyData); + + // restore the last-known pose of each reserved (possibly unloaded) plot + final ListTag poses = tag.getList("last_known_poses", Tag.TAG_COMPOUND); + for (int i = 0; i < poses.size(); i++) { + final CompoundTag entry = poses.getCompound(i); + final Pose3d pose = SableNBTUtils.readPose3d(entry.getCompound("pose")); + container.setLastKnownPose(entry.getInt("index"), pose); + } } return data; @@ -61,6 +73,21 @@ public CompoundTag save(final CompoundTag compoundTag, final HolderLookup.Provid compoundTag.putLongArray("sub_level_occupancy", longArray); + // persist each reserved plot's last-known pose so distance/cost queries stay accurate for + // sub-levels that are still unloaded after a restart + final ListTag poses = new ListTag(); + for (int index = occupancy.nextSetBit(0); index >= 0; index = occupancy.nextSetBit(index + 1)) { + final Pose3d pose = container.getPersistablePose(index); + if (pose == null) { + continue; + } + final CompoundTag entry = new CompoundTag(); + entry.putInt("index", index); + entry.put("pose", SableNBTUtils.writePose3d(pose)); + poses.add(entry); + } + compoundTag.put("last_known_poses", poses); + return compoundTag; } } \ No newline at end of file diff --git a/common/src/main/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index faf5ee1b..85fce64f 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -110,6 +110,7 @@ "compatibility.vista.LODMixin", "compatibility.vista.ViewFinderAccessMixin", "compatibility.vista.ViewFinderControllerMixin", + "compatibility.waystones.WaystoneTeleportManagerMixin", "death_message.CombatTrackerMixin", "death_message.EntityMixin", "enchanting_table.EnchantingTableBlockEntityMixin", From d89c52694382698011453d7ab018b4ef79e2d9b3 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:13:51 +0900 Subject: [PATCH 2/5] Teleport Waystones warps onto unloaded sub-levels Building on the last-known pose work, allow warping to a waystone whose contraption is currently unloaded, reusing the same machinery Sable uses for bed respawns onto sub-levels. - Track the sub-level UUID per plot alongside the last-known pose (SubLevelContainer), persisted with plot occupancy, so a held sub-level can be identified without loading it. - WaystoneImplMixin: a waystone on a reserved-but-unloaded plot is treated as a valid teleport target instead of "currently being moved or has gone missing". - WaystoneTeleportManagerMixin: after projecting the target to the sub-level's last-known world position (which re-activates the held sub-level by loading the world chunk there), freeze the player to (subLevelId, localAnchor). The freeze places the player precisely on the deck once the sub-level goes live, then releases - the same path as respawn. Known limitations: the warp trusts that a reserved plot still holds the waystone (no disk-read validation), and relies on the re-activation landing within the freeze window. --- .../sable/api/sublevel/SubLevelContainer.java | 47 +++++++++ .../waystones/WaystoneImplMixin.java | 46 +++++++++ .../WaystoneTeleportManagerMixin.java | 95 +++++++++++++++---- .../storage/SubLevelOccupancySavedData.java | 11 ++- common/src/main/resources/sable.mixins.json | 1 + 5 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java index a2117237..030a9b5a 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java @@ -65,6 +65,12 @@ public abstract class SubLevelContainer { * {@code null} for unoccupied plots. */ private final Pose3d[] lastKnownPoses; + /** + * The UUID of the sub-level occupying each plot, indexed identically to {@link #subLevels}. Kept + * for held (unloaded) plots so callers can identify - and freeze a player to - a sub-level + * without loading it back in. {@code null} for unoccupied plots. + */ + private final UUID[] lastKnownUuids; /** * All observers/listeners for the plotgrid */ @@ -143,6 +149,7 @@ public SubLevelContainer(final Level level, final int logSideLength, final int l this.subLevels = new SubLevel[(1 << logSideLength) * (1 << logSideLength)]; this.occupancy = new BitSet(this.subLevels.length); this.lastKnownPoses = new Pose3d[this.subLevels.length]; + this.lastKnownUuids = new UUID[this.subLevels.length]; } /** @@ -270,6 +277,7 @@ public SubLevel allocateSubLevel(final UUID uuid, final int x, final int z, fina this.subLevels[index] = subLevel; this.getOccupancy().set(index); this.lastKnownPoses[index] = new Pose3d(pose); + this.lastKnownUuids[index] = uuid; this.allSubLevels.add(subLevel); this.subLevelsByUUID.put(subLevel.getUniqueId(), subLevel); this.observers.forEach(observer -> observer.onSubLevelAdded(subLevel)); @@ -497,6 +505,7 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason if (reason == SubLevelRemovalReason.REMOVED) { this.getOccupancy().clear(index); this.lastKnownPoses[index] = null; + this.lastKnownUuids[index] = null; } else { // Still held, just unloaded: remember where it was so distance/cost queries can resolve // its approximate world position without loading the sub-level back in. @@ -526,6 +535,44 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason return this.lastKnownPoses[this.getIndex(plotX, plotZ)]; } + /** + * Returns the UUID of the (possibly unloaded) sub-level whose plot contains the given global + * chunk position, so a held sub-level can be identified - and frozen to - without loading it. + * + * @return the sub-level UUID, or {@code null} if no plot is reserved at that position + */ + public @Nullable UUID getLastKnownUuid(final int chunkX, final int chunkZ) { + final int plotX = (chunkX >> this.logPlotSize) - this.originX; + final int plotZ = (chunkZ >> this.logPlotSize) - this.originZ; + final int sideLength = 1 << this.logSideLength; + if (plotX < 0 || plotX >= sideLength || plotZ < 0 || plotZ >= sideLength) { + return null; + } + return this.lastKnownUuids[this.getIndex(plotX, plotZ)]; + } + + /** + * @return the persisted sub-level UUID for the plot at the given index, or {@code null} if none. + */ + @ApiStatus.Internal + public @Nullable UUID getLastKnownUuid(final int index) { + if (index < 0 || index >= this.lastKnownUuids.length) { + return null; + } + return this.lastKnownUuids[index]; + } + + /** + * Restores a persisted sub-level UUID for the plot at the given index. + */ + @ApiStatus.Internal + public void setLastKnownUuid(final int index, final UUID uuid) { + if (index < 0 || index >= this.lastKnownUuids.length) { + return; + } + this.lastKnownUuids[index] = uuid; + } + /** * @return the pose to persist for the plot at the given index: the live pose when the sub-level * is currently loaded, otherwise the last-known pose captured when it was unloaded. diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java new file mode 100644 index 00000000..03c249b4 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java @@ -0,0 +1,46 @@ +package dev.ryanhcode.sable.mixin.compatibility.waystones; + +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Keeps a waystone on an unloaded sub-level valid as a teleport target. + * + *

{@code WaystoneImpl#isValidInLevel} checks {@code level.getBlockState(pos)} at the stored + * position, which for a waystone on an assembled sub-level is the plot-yard coordinate. While the + * contraption is unloaded its block isn't in the world, so the check fails and Waystones refuses with + * "currently being moved or has gone missing". If the position belongs to a reserved-but-unloaded + * sub-level plot, treat the waystone as valid - the warp re-activates the sub-level and the player is + * frozen onto it (see {@link WaystoneTeleportManagerMixin}). + */ +@Mixin(targets = "net.blay09.mods.waystones.core.WaystoneImpl", remap = false) +public abstract class WaystoneImplMixin { + + @Shadow + private BlockPos pos; + + @Inject(method = "isValidInLevel", at = @At("HEAD"), cancellable = true) + private void sable$validOnHeldSubLevel(final ServerLevel level, final CallbackInfoReturnable cir) { + final SubLevelContainer container = SubLevelContainer.getContainer(level); + if (container == null) { + return; + } + + final int chunkX = this.pos.getX() >> SectionPos.SECTION_BITS; + final int chunkZ = this.pos.getZ() >> SectionPos.SECTION_BITS; + + // Reserved plot with no loaded sub-level == the waystone is on a held (unloaded) contraption. + // When loaded, fall through to the vanilla block check. + if (container.getLastKnownPose(chunkX, chunkZ) != null && Sable.HELPER.getContaining(level, chunkX, chunkZ) == null) { + cir.setReturnValue(true); + } + } +} diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java index c8d7cc76..4f7e7302 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java @@ -1,41 +1,96 @@ package dev.ryanhcode.sable.mixin.compatibility.waystones; import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; +import dev.ryanhcode.sable.mixinterface.player_freezing.PlayerFreezeExtension; +import dev.ryanhcode.sable.network.packets.tcp.ClientboundFreezePlayerPacket; +import it.unimi.dsi.fastutil.Pair; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.UUID; /** - * Waystones stores a waystone's location as the raw block position of its block entity. For a waystone - * placed on an assembled Sable sub-level (e.g. an aircraft) that position is the hidden plot-yard - * coordinate (~20.48M blocks out), not the contraption's rendered location. + * Lets Waystones teleport onto Sable sub-levels (e.g. an aircraft). + * + *

A waystone placed on an assembled sub-level is stored at the hidden plot-yard coordinate. Two + * things need to happen for a warp onto it to work: * - *

Sable already projects {@link net.minecraft.server.level.ServerPlayer#teleportTo} out of the - * sub-level, but Waystones' same-dimension warp path teleports via - * {@code connection.teleport(...)} directly, bypassing that projection. The player is dropped into the - * plot-yard, which desyncs the sub-level stream ("Received a sub-level movement packet for a - * non-existent sub-level") and freezes the server thread. + *

    + *
  • The teleport target is projected out of the sub-level to the contraption's real (or, while + * unloaded, last-known) world position. Waystones' same-dimension warp uses + * {@code connection.teleport(...)} directly, bypassing Sable's {@code ServerPlayer#teleportTo} + * projection, so without this the player is dropped into the plot-yard and the server freezes. + *
  • If the contraption is currently unloaded, the player is frozen to the sub-level after the + * teleport. Teleporting to the last-known position re-activates the held sub-level (its holding + * chunk is keyed by that world position); the freeze then places the player precisely on the + * deck once it is live - the same mechanism Sable uses for bed respawns. + *
* - *

Project the teleport target out of the sub-level before the entity is moved. This affects only - * the entity placement (the warp-plate / modifier block-entity lookups still use the original plot - * position via {@code destination.location()}). * {@link dev.ryanhcode.sable.api.SubLevelHelper#projectOutOfSubLevel} is a no-op for positions that - * are not inside a sub-level, so this is safe for ordinary warps and idempotent with the - * cross-dimension {@code teleportTo} path that Sable already handles. + * are not inside a sub-level, so ordinary warps are unaffected. */ @Mixin(targets = "net.blay09.mods.waystones.core.WaystoneTeleportManager", remap = false) public class WaystoneTeleportManagerMixin { - @ModifyVariable( - method = "teleportEntity(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/Direction;)Lnet/minecraft/world/entity/Entity;", - at = @At("HEAD"), - argsOnly = true, - index = 2 - ) - private static Vec3 sable$projectTeleportTargetOutOfSubLevel(final Vec3 targetPos3d, @Local(argsOnly = true) final ServerLevel targetWorld) { + private static final String TELEPORT_ENTITY = "teleportEntity(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/Direction;)Lnet/minecraft/world/entity/Entity;"; + + @ModifyVariable(method = TELEPORT_ENTITY, at = @At("HEAD"), argsOnly = true, index = 2) + private static Vec3 sable$projectTeleportTarget(final Vec3 targetPos3d, @Local(argsOnly = true) final ServerLevel targetWorld, @Share("sableHeldFreeze") final LocalRef> freezeRef) { + // If the waystone is on a currently-unloaded sub-level, remember which sub-level (and the + // local anchor) so the player can be frozen to it after the teleport. + final UUID heldUuid = sable$heldSubLevelUuid(targetWorld, targetPos3d); + if (heldUuid != null) { + freezeRef.set(Pair.of(heldUuid, new Vector3d(targetPos3d.x, targetPos3d.y, targetPos3d.z))); + } + return Sable.HELPER.projectOutOfSubLevel(targetWorld, targetPos3d); } + + @Inject(method = TELEPORT_ENTITY, at = @At("RETURN")) + private static void sable$freezeOntoSubLevel(final Entity entity, final ServerLevel targetWorld, final Vec3 targetPos3d, final Direction direction, final CallbackInfoReturnable cir, @Share("sableHeldFreeze") final LocalRef> freezeRef) { + final Pair freeze = freezeRef.get(); + if (freeze == null) { + return; + } + + if (cir.getReturnValue() instanceof final ServerPlayer player) { + ((PlayerFreezeExtension) player).sable$freezeTo(freeze.first(), freeze.second()); + player.connection.send(new ClientboundCustomPayloadPacket(new ClientboundFreezePlayerPacket(freeze.first(), freeze.second()))); + } + } + + /** + * @return the UUID of the held (unloaded) sub-level whose plot contains the given target, or + * {@code null} if the target is in open world or on an already-loaded sub-level (in which case the + * ordinary teleport plus entity-sticking is enough). + */ + private static @Nullable UUID sable$heldSubLevelUuid(final ServerLevel level, final Vec3 pos) { + if (Sable.HELPER.getContaining(level, pos.x, pos.z) != null) { + return null; + } + final SubLevelContainer container = SubLevelContainer.getContainer(level); + if (container == null) { + return null; + } + final int chunkX = Mth.floor(pos.x) >> SectionPos.SECTION_BITS; + final int chunkZ = Mth.floor(pos.z) >> SectionPos.SECTION_BITS; + return container.getLastKnownUuid(chunkX, chunkZ); + } } diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java index a85dca45..20f059a2 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java @@ -12,6 +12,7 @@ import net.minecraft.world.level.saveddata.SavedData; import java.util.BitSet; +import java.util.UUID; /** * Stores the map for which plots are occupied @@ -54,8 +55,12 @@ private static SubLevelOccupancySavedData load(final ServerLevel level, final Co final ListTag poses = tag.getList("last_known_poses", Tag.TAG_COMPOUND); for (int i = 0; i < poses.size(); i++) { final CompoundTag entry = poses.getCompound(i); + final int index = entry.getInt("index"); final Pose3d pose = SableNBTUtils.readPose3d(entry.getCompound("pose")); - container.setLastKnownPose(entry.getInt("index"), pose); + container.setLastKnownPose(index, pose); + if (entry.hasUUID("uuid")) { + container.setLastKnownUuid(index, entry.getUUID("uuid")); + } } } @@ -84,6 +89,10 @@ public CompoundTag save(final CompoundTag compoundTag, final HolderLookup.Provid final CompoundTag entry = new CompoundTag(); entry.putInt("index", index); entry.put("pose", SableNBTUtils.writePose3d(pose)); + final UUID uuid = container.getLastKnownUuid(index); + if (uuid != null) { + entry.putUUID("uuid", uuid); + } poses.add(entry); } compoundTag.put("last_known_poses", poses); diff --git a/common/src/main/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index 85fce64f..7b792a33 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -110,6 +110,7 @@ "compatibility.vista.LODMixin", "compatibility.vista.ViewFinderAccessMixin", "compatibility.vista.ViewFinderControllerMixin", + "compatibility.waystones.WaystoneImplMixin", "compatibility.waystones.WaystoneTeleportManagerMixin", "death_message.CombatTrackerMixin", "death_message.EntityMixin", From c88bc2a537a152b488f63e5e8d687040aa14400e Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:58:30 +0900 Subject: [PATCH 3/5] Hide misleading waystone distance for unloaded sub-levels (client) A waystone on a sub-level stores the plot-yard coordinate. When the contraption is loaded the client projects it via Sable's distanceToSqr overwrite and the distance is accurate, but while it is unloaded here the client has no pose to project against and the distance renders as a meaningless ~20,000 km. Recovering the real distance would require the server to stream last-known poses to clients. Instead, detect that the waystone sits in a reserved plot-yard position that isn't loaded on this client and omit the distance - reusing the existing path Waystones already uses for cross-dimension waystones, which show no distance either. --- .../waystones/WaystoneButtonMixin.java | 59 +++++++++++++++++++ common/src/main/resources/sable.mixins.json | 1 + 2 files changed, 60 insertions(+) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java new file mode 100644 index 00000000..b089f152 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java @@ -0,0 +1,59 @@ +package dev.ryanhcode.sable.mixin.compatibility.waystones; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Hides the misleading distance for a waystone whose sub-level isn't loaded on this client. + * + *

A waystone on a sub-level stores the far-away plot-yard coordinate. While the contraption is + * loaded, Sable's {@code Entity#distanceToSqr} overwrite projects it to the real location and the + * distance is accurate. While it is unloaded here the client has no pose to project against, so the + * distance would render as a meaningless ~20,000 km. The client can't recover the real distance + * without the server streaming last-known poses, so instead we omit the distance entirely - the same + * thing Waystones already does for cross-dimension waystones. + */ +@Mixin(targets = "net.blay09.mods.waystones.client.gui.widget.WaystoneButton", remap = false) +public abstract class WaystoneButtonMixin { + + // Owner is intentionally omitted: `player` is statically a LocalPlayer here, so the call's owner + // is not Entity. An ownerless target matches the inherited distanceToSqr regardless. + @WrapOperation(method = "renderWidget", at = @At(value = "INVOKE", target = "distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D")) + private double sable$detectUnloadedSubLevel(final Entity player, final Vec3 waystonePos, final Operation original, @Share("sableUnknownDistance") final LocalBooleanRef unknown) { + unknown.set(sable$isOnUnloadedSubLevel(player.level(), waystonePos)); + return original.call(player, waystonePos); + } + + @WrapOperation(method = "renderWidget", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Ljava/lang/String;III)I")) + private int sable$hideUnloadedSubLevelDistance(final GuiGraphics graphics, final Font font, final String text, final int x, final int y, final int color, final Operation original, @Share("sableUnknownDistance") final LocalBooleanRef unknown) { + if (unknown.get()) { + return 0; + } + return original.call(graphics, font, text, x, y, color); + } + + @Unique + private static boolean sable$isOnUnloadedSubLevel(final Level level, final Vec3 pos) { + // Loaded here: the projected distance is accurate, leave it alone. + if (Sable.HELPER.getContaining(level, pos.x, pos.z) != null) { + return false; + } + // Otherwise, only treat it as unknown when the position is actually a reserved plot-yard + // coordinate - a genuinely far-away waystone keeps its real (large) distance. + final SubLevelContainer container = SubLevelContainer.getContainer(level); + return container != null && container.inBounds(BlockPos.containing(pos)); + } +} diff --git a/common/src/main/resources/sable.mixins.json b/common/src/main/resources/sable.mixins.json index 7b792a33..c302d6e6 100644 --- a/common/src/main/resources/sable.mixins.json +++ b/common/src/main/resources/sable.mixins.json @@ -17,6 +17,7 @@ "clip_overwrite.ClientLevelMixin", "clip_overwrite.GameRendererMixin", "compatibility.iris.ExtendedShaderMixin", + "compatibility.waystones.WaystoneButtonMixin", "config.GameRendererAccessor", "debug_render.DebugRendererMixin", "debug_render.DebugScreenOverlayMixin", From 4c40634505d2984ce88d0386a7d225457160ff39 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:16:54 +0900 Subject: [PATCH 4/5] Fix WaystoneButtonMixin @WrapOperation receiver type MixinExtras requires the wrapped operation's receiver parameter to match the call's exact owner type. The distanceToSqr call in WaystoneButton#renderWidget has a LocalPlayer receiver (the static type of Minecraft#player), not Entity, so the handler must take LocalPlayer or mixin application fails with "unexpected argument type ... at index 0" and the screen crashes on open. --- .../compatibility/waystones/WaystoneButtonMixin.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java index b089f152..9f242889 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java @@ -8,8 +8,8 @@ import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; -import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.spongepowered.asm.mixin.Mixin; @@ -29,10 +29,10 @@ @Mixin(targets = "net.blay09.mods.waystones.client.gui.widget.WaystoneButton", remap = false) public abstract class WaystoneButtonMixin { - // Owner is intentionally omitted: `player` is statically a LocalPlayer here, so the call's owner - // is not Entity. An ownerless target matches the inherited distanceToSqr regardless. + // `player` is statically a LocalPlayer at the call site, so the receiver parameter must be typed + // as LocalPlayer (MixinExtras requires the exact owner type, not a supertype). @WrapOperation(method = "renderWidget", at = @At(value = "INVOKE", target = "distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D")) - private double sable$detectUnloadedSubLevel(final Entity player, final Vec3 waystonePos, final Operation original, @Share("sableUnknownDistance") final LocalBooleanRef unknown) { + private double sable$detectUnloadedSubLevel(final LocalPlayer player, final Vec3 waystonePos, final Operation original, @Share("sableUnknownDistance") final LocalBooleanRef unknown) { unknown.set(sable$isOnUnloadedSubLevel(player.level(), waystonePos)); return original.call(player, waystonePos); } From fcb1ba172622d266af8a1f56dfcb38a6cb8ec180 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:59:21 +0900 Subject: [PATCH 5/5] Trim Waystones-compat docs to match surrounding style --- .../ryanhcode/sable/ActiveSableCompanion.java | 6 +--- .../sable/api/sublevel/SubLevelContainer.java | 30 +++++-------------- .../waystones/WaystoneButtonMixin.java | 19 ++++-------- .../waystones/WaystoneImplMixin.java | 15 ++++------ .../WaystoneTeleportManagerMixin.java | 30 +++++-------------- .../storage/SubLevelOccupancySavedData.java | 3 +- 6 files changed, 28 insertions(+), 75 deletions(-) diff --git a/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java b/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java index d9ed9969..dd82d4be 100644 --- a/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java +++ b/common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java @@ -217,11 +217,7 @@ public Vec3 projectOutOfSubLevel(final Level level, final Position pos) { } /** - * Resolves the last-known pose of a held (unloaded) sub-level whose reserved plot contains the - * given world position, so distance/cost queries can project an unloaded sub-level's coordinate - * to its approximate world location without loading it back in. Returns {@code null} when the - * position is not inside any reserved plot (e.g. ordinary world positions), in which case the - * caller leaves the coordinate untouched. + * @return the last-known pose of the held sub-level whose reserved plot contains the position, or {@code null} if none */ private @Nullable Pose3dc lastKnownContainingPose(final Level level, final double blockX, final double blockZ) { final SubLevelContainer container = SubLevelContainer.getContainer(level); diff --git a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java index 030a9b5a..88bda640 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/sublevel/SubLevelContainer.java @@ -59,16 +59,11 @@ public abstract class SubLevelContainer { */ private final BitSet occupancy; /** - * The last-known pose of each plot, indexed identically to {@link #subLevels}. Set while a - * sub-level is allocated and refreshed when it unloads, so that distance and cost queries can - * resolve a held (unloaded) sub-level's approximate world position without loading it back in. - * {@code null} for unoccupied plots. + * The last-known pose of each plot, kept while held so unloaded sub-levels can be located. {@code null} if unoccupied. */ private final Pose3d[] lastKnownPoses; /** - * The UUID of the sub-level occupying each plot, indexed identically to {@link #subLevels}. Kept - * for held (unloaded) plots so callers can identify - and freeze a player to - a sub-level - * without loading it back in. {@code null} for unoccupied plots. + * The sub-level UUID of each plot, kept while held. {@code null} if unoccupied. */ private final UUID[] lastKnownUuids; /** @@ -507,8 +502,7 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason this.lastKnownPoses[index] = null; this.lastKnownUuids[index] = null; } else { - // Still held, just unloaded: remember where it was so distance/cost queries can resolve - // its approximate world position without loading the sub-level back in. + // Held, not removed: remember its pose so it can be located while unloaded. this.lastKnownPoses[index] = new Pose3d(subLevel.logicalPose()); } @@ -518,12 +512,8 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason } /** - * Returns the last-known pose of the (possibly unloaded) sub-level whose plot contains the given - * global chunk position. While the sub-level is loaded this is its allocation/last-unload pose, - * which is stale - callers that need the live pose should use {@link #getContaining} first and - * fall back to this only when no loaded sub-level is present. - * - * @return the last-known pose, or {@code null} if no plot is reserved at that position + * @return the last-known pose of the (possibly unloaded) sub-level at the given chunk, or {@code null} if none. + * Stale while loaded; prefer {@link #getContaining} when a live pose is required */ public @Nullable Pose3d getLastKnownPose(final int chunkX, final int chunkZ) { final int plotX = (chunkX >> this.logPlotSize) - this.originX; @@ -536,10 +526,7 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason } /** - * Returns the UUID of the (possibly unloaded) sub-level whose plot contains the given global - * chunk position, so a held sub-level can be identified - and frozen to - without loading it. - * - * @return the sub-level UUID, or {@code null} if no plot is reserved at that position + * @return the UUID of the (possibly unloaded) sub-level at the given chunk, or {@code null} if none */ public @Nullable UUID getLastKnownUuid(final int chunkX, final int chunkZ) { final int plotX = (chunkX >> this.logPlotSize) - this.originX; @@ -552,7 +539,7 @@ public void removeSubLevel(final int x, final int z, final SubLevelRemovalReason } /** - * @return the persisted sub-level UUID for the plot at the given index, or {@code null} if none. + * @return the sub-level UUID stored for the plot index, or {@code null} if none */ @ApiStatus.Internal public @Nullable UUID getLastKnownUuid(final int index) { @@ -574,8 +561,7 @@ public void setLastKnownUuid(final int index, final UUID uuid) { } /** - * @return the pose to persist for the plot at the given index: the live pose when the sub-level - * is currently loaded, otherwise the last-known pose captured when it was unloaded. + * @return the live pose if the plot's sub-level is loaded, otherwise its last-known pose */ @ApiStatus.Internal public @Nullable Pose3d getPersistablePose(final int index) { diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java index 9f242889..c44618a6 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneButtonMixin.java @@ -17,20 +17,14 @@ import org.spongepowered.asm.mixin.injection.At; /** - * Hides the misleading distance for a waystone whose sub-level isn't loaded on this client. - * - *

A waystone on a sub-level stores the far-away plot-yard coordinate. While the contraption is - * loaded, Sable's {@code Entity#distanceToSqr} overwrite projects it to the real location and the - * distance is accurate. While it is unloaded here the client has no pose to project against, so the - * distance would render as a meaningless ~20,000 km. The client can't recover the real distance - * without the server streaming last-known poses, so instead we omit the distance entirely - the same - * thing Waystones already does for cross-dimension waystones. + * Hides the distance for a waystone whose sub-level isn't loaded on this client. Its stored plot-yard + * coordinate can't be projected without a pose here, so the distance would show a meaningless + * ~20,000 km; omit it instead, as Waystones already does for cross-dimension waystones. */ @Mixin(targets = "net.blay09.mods.waystones.client.gui.widget.WaystoneButton", remap = false) public abstract class WaystoneButtonMixin { - // `player` is statically a LocalPlayer at the call site, so the receiver parameter must be typed - // as LocalPlayer (MixinExtras requires the exact owner type, not a supertype). + // Receiver must be LocalPlayer (its static type at the call site); WrapOperation needs the exact owner. @WrapOperation(method = "renderWidget", at = @At(value = "INVOKE", target = "distanceToSqr(Lnet/minecraft/world/phys/Vec3;)D")) private double sable$detectUnloadedSubLevel(final LocalPlayer player, final Vec3 waystonePos, final Operation original, @Share("sableUnknownDistance") final LocalBooleanRef unknown) { unknown.set(sable$isOnUnloadedSubLevel(player.level(), waystonePos)); @@ -47,12 +41,11 @@ public abstract class WaystoneButtonMixin { @Unique private static boolean sable$isOnUnloadedSubLevel(final Level level, final Vec3 pos) { - // Loaded here: the projected distance is accurate, leave it alone. + // Loaded here: the projected distance is accurate. if (Sable.HELPER.getContaining(level, pos.x, pos.z) != null) { return false; } - // Otherwise, only treat it as unknown when the position is actually a reserved plot-yard - // coordinate - a genuinely far-away waystone keeps its real (large) distance. + // Only unknown when it's a plot-yard coordinate; a genuinely far waystone keeps its real distance. final SubLevelContainer container = SubLevelContainer.getContainer(level); return container != null && container.inBounds(BlockPos.containing(pos)); } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java index 03c249b4..c7d70149 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneImplMixin.java @@ -12,14 +12,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** - * Keeps a waystone on an unloaded sub-level valid as a teleport target. - * - *

{@code WaystoneImpl#isValidInLevel} checks {@code level.getBlockState(pos)} at the stored - * position, which for a waystone on an assembled sub-level is the plot-yard coordinate. While the - * contraption is unloaded its block isn't in the world, so the check fails and Waystones refuses with - * "currently being moved or has gone missing". If the position belongs to a reserved-but-unloaded - * sub-level plot, treat the waystone as valid - the warp re-activates the sub-level and the player is - * frozen onto it (see {@link WaystoneTeleportManagerMixin}). + * Keeps a waystone on a held (unloaded) sub-level valid. {@code isValidInLevel} checks the block at + * the stored plot-yard position, which is empty while the contraption is unloaded; treat a + * reserved-but-unloaded sub-level plot as valid so the warp ({@link WaystoneTeleportManagerMixin}) + * can re-activate it. */ @Mixin(targets = "net.blay09.mods.waystones.core.WaystoneImpl", remap = false) public abstract class WaystoneImplMixin { @@ -37,8 +33,7 @@ public abstract class WaystoneImplMixin { final int chunkX = this.pos.getX() >> SectionPos.SECTION_BITS; final int chunkZ = this.pos.getZ() >> SectionPos.SECTION_BITS; - // Reserved plot with no loaded sub-level == the waystone is on a held (unloaded) contraption. - // When loaded, fall through to the vanilla block check. + // Reserved plot, no loaded sub-level = held contraption; when loaded, let the vanilla block check run. if (container.getLastKnownPose(chunkX, chunkZ) != null && Sable.HELPER.getContaining(level, chunkX, chunkZ) == null) { cir.setReturnValue(true); } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java index 4f7e7302..9fd566e2 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/compatibility/waystones/WaystoneTeleportManagerMixin.java @@ -27,24 +27,11 @@ import java.util.UUID; /** - * Lets Waystones teleport onto Sable sub-levels (e.g. an aircraft). - * - *

A waystone placed on an assembled sub-level is stored at the hidden plot-yard coordinate. Two - * things need to happen for a warp onto it to work: - * - *

    - *
  • The teleport target is projected out of the sub-level to the contraption's real (or, while - * unloaded, last-known) world position. Waystones' same-dimension warp uses - * {@code connection.teleport(...)} directly, bypassing Sable's {@code ServerPlayer#teleportTo} - * projection, so without this the player is dropped into the plot-yard and the server freezes. - *
  • If the contraption is currently unloaded, the player is frozen to the sub-level after the - * teleport. Teleporting to the last-known position re-activates the held sub-level (its holding - * chunk is keyed by that world position); the freeze then places the player precisely on the - * deck once it is live - the same mechanism Sable uses for bed respawns. - *
- * - * {@link dev.ryanhcode.sable.api.SubLevelHelper#projectOutOfSubLevel} is a no-op for positions that - * are not inside a sub-level, so ordinary warps are unaffected. + * Lets Waystones warp onto sub-levels. A waystone on a sub-level is stored at its plot-yard + * coordinate, so the target is projected out to the contraption's real (or last-known) position - + * Waystones' same-dimension warp uses {@code connection.teleport} directly and would otherwise drop + * the player into the plot-yard. If the contraption is unloaded, the player is frozen to it after the + * teleport so they land on the deck once it re-activates, as bed respawns do. */ @Mixin(targets = "net.blay09.mods.waystones.core.WaystoneTeleportManager", remap = false) public class WaystoneTeleportManagerMixin { @@ -53,8 +40,7 @@ public class WaystoneTeleportManagerMixin { @ModifyVariable(method = TELEPORT_ENTITY, at = @At("HEAD"), argsOnly = true, index = 2) private static Vec3 sable$projectTeleportTarget(final Vec3 targetPos3d, @Local(argsOnly = true) final ServerLevel targetWorld, @Share("sableHeldFreeze") final LocalRef> freezeRef) { - // If the waystone is on a currently-unloaded sub-level, remember which sub-level (and the - // local anchor) so the player can be frozen to it after the teleport. + // On an unloaded sub-level, remember it (and the local anchor) to freeze the player afterwards. final UUID heldUuid = sable$heldSubLevelUuid(targetWorld, targetPos3d); if (heldUuid != null) { freezeRef.set(Pair.of(heldUuid, new Vector3d(targetPos3d.x, targetPos3d.y, targetPos3d.z))); @@ -77,9 +63,7 @@ public class WaystoneTeleportManagerMixin { } /** - * @return the UUID of the held (unloaded) sub-level whose plot contains the given target, or - * {@code null} if the target is in open world or on an already-loaded sub-level (in which case the - * ordinary teleport plus entity-sticking is enough). + * @return the UUID of the held (unloaded) sub-level at the target, or {@code null} if it is open world or already loaded */ private static @Nullable UUID sable$heldSubLevelUuid(final ServerLevel level, final Vec3 pos) { if (Sable.HELPER.getContaining(level, pos.x, pos.z) != null) { diff --git a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java index 20f059a2..2925ba33 100644 --- a/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java +++ b/common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelOccupancySavedData.java @@ -78,8 +78,7 @@ public CompoundTag save(final CompoundTag compoundTag, final HolderLookup.Provid compoundTag.putLongArray("sub_level_occupancy", longArray); - // persist each reserved plot's last-known pose so distance/cost queries stay accurate for - // sub-levels that are still unloaded after a restart + // persist each reserved plot's last-known pose & uuid for sub-levels still unloaded after a restart final ListTag poses = new ListTag(); for (int index = occupancy.nextSetBit(0); index >= 0; index = occupancy.nextSetBit(index + 1)) { final Pose3d pose = container.getPersistablePose(index);