Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions common/src/main/java/dev/ryanhcode/sable/ActiveSableCompanion.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,13 @@ public Iterable<SubLevel> 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) {
Expand All @@ -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) {
Expand All @@ -204,6 +216,19 @@ public Vec3 projectOutOfSubLevel(final Level level, final Position pos) {
return JOMLConversion.toMojang(pose.transformPosition(JOMLConversion.toJOML(pos)));
}

/**
* @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);
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, S extends SubLevelAccess> T runIncludingSubLevels(final Level level, final Vec3 origin, final boolean shouldCheckOrigin, @Nullable final S subLevel, final BiFunction<S, BlockPos, T> converter) {
return this.runIncludingSubLevels(level, (Position) origin, shouldCheckOrigin, subLevel, converter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ 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, kept while held so unloaded sub-levels can be located. {@code null} if unoccupied.
*/
private final Pose3d[] lastKnownPoses;
/**
* The sub-level UUID of each plot, kept while held. {@code null} if unoccupied.
*/
private final UUID[] lastKnownUuids;
/**
* All observers/listeners for the plotgrid
*/
Expand Down Expand Up @@ -135,6 +143,8 @@ 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];
this.lastKnownUuids = new UUID[this.subLevels.length];
}

/**
Expand Down Expand Up @@ -261,6 +271,8 @@ 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.lastKnownUuids[index] = uuid;
this.allSubLevels.add(subLevel);
this.subLevelsByUUID.put(subLevel.getUniqueId(), subLevel);
this.observers.forEach(observer -> observer.onSubLevelAdded(subLevel));
Expand Down Expand Up @@ -487,7 +499,91 @@ 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 {
// Held, not removed: remember its pose so it can be located while unloaded.
this.lastKnownPoses[index] = new Pose3d(subLevel.logicalPose());
}

if (this.level instanceof final ServerLevel serverLevel) {
SubLevelOccupancySavedData.getOrLoad(serverLevel).setDirty();
}
}

/**
* @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;
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 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;
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 sub-level UUID stored for the plot 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 live pose if the plot's sub-level is loaded, otherwise its last-known pose
*/
@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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
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 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 {

// 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<Double> 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<Integer> 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.
if (Sable.HELPER.getContaining(level, pos.x, pos.z) != null) {
return false;
}
// 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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 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 {

@Shadow
private BlockPos pos;

@Inject(method = "isValidInLevel", at = @At("HEAD"), cancellable = true)
private void sable$validOnHeldSubLevel(final ServerLevel level, final CallbackInfoReturnable<Boolean> 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, 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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;

/**
* 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 {

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<Pair<UUID, Vector3d>> freezeRef) {
// 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)));
}

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<Entity> cir, @Share("sableHeldFreeze") final LocalRef<Pair<UUID, Vector3d>> freezeRef) {
final Pair<UUID, Vector3d> 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 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) {
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);
}
}
Loading