Require footing for descriptive book travel
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 09:31:41 -04:00
1 parent e30c0a25cc
commit 67ce69e8b2
6 files changed
+43 -7

No files matched your search

+2 -1
View File
@@ -36,7 +36,8 @@ independently validates every link.
Until generated Ages arrive in Milestone 3, written Descriptive Books resolve
deterministically to the Overworld, Nether, or End according to their Age UUID.
The Rivencraft creative tab includes one sample book for each. Linking creates
a bound Return Link to the exact source position and orientation.
a bound Return Link to the exact source position and orientation. Descriptive
Book destinations additionally require solid footing beneath the player.
## Requirements
+3
View File
@@ -156,6 +156,9 @@ Version 0.2.2 adds the same server-provided Ponder destination preview to
Descriptive Books. If no snapshot is available, the left page shows an
animated linking-panel fallback.
Version 0.2.3 requires Descriptive Book destinations to have a sturdy floor,
while Linking Books continue to permit passable floorless destinations.
The preview code is separated behind a renderer interface and uses an isolated
Ponder level populated from server-authoritative destination snapshots.
+1 -1
View File
@@ -33,7 +33,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later
# The mod version. See https://semver.org/
mod_version=0.2.2
mod_version=0.2.3
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
@@ -4,6 +4,7 @@ import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Stream;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BlockTags;
@@ -19,12 +20,21 @@ public final class LinkingService {
}
public static LinkResult travel(ServerPlayer player, LinkTarget target) {
return travel(player, target, false);
}
/** Travels to an Age-style destination, which must have solid footing. */
public static LinkResult travelWithFooting(ServerPlayer player, LinkTarget target) {
return travel(player, target, true);
}
private static LinkResult travel(ServerPlayer player, LinkTarget target, boolean requiresFooting) {
ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension());
if (destinationLevel == null) {
return LinkResult.MISSING_DIMENSION;
}
Optional<Vec3> destination = findSafeDestination(destinationLevel, target.location().pos());
Optional<Vec3> destination = findSafeDestination(destinationLevel, target.location().pos(), requiresFooting);
if (destination.isEmpty()) {
return LinkResult.NO_SAFE_DESTINATION;
}
@@ -43,11 +53,15 @@ public final class LinkingService {
}
static Optional<Vec3> findSafeDestination(ServerLevel level, BlockPos origin) {
return findSafeDestination(level, origin, false);
}
private static Optional<Vec3> findSafeDestination(ServerLevel level, BlockPos origin, boolean requiresFooting) {
WorldBorder border = level.getWorldBorder();
return candidates(origin)
.filter(level::isInWorldBounds)
.filter(border::isWithinBounds)
.filter(position -> isSafe(level, position))
.filter(position -> isSafe(level, position, requiresFooting))
.map(position -> Vec3.atBottomCenterOf(position))
.findFirst();
}
@@ -61,9 +75,11 @@ public final class LinkingService {
.sorted(Comparator.comparingDouble(position -> position.distSqr(origin)));
}
private static boolean isSafe(ServerLevel level, BlockPos feet) {
private static boolean isSafe(ServerLevel level, BlockPos feet, boolean requiresFooting) {
BlockPos head = feet.above();
return isPassableSpawnSpace(level, feet) && isPassableSpawnSpace(level, head);
return isPassableSpawnSpace(level, feet)
&& isPassableSpawnSpace(level, head)
&& (!requiresFooting || level.getBlockState(feet.below()).isFaceSturdy(level, feet.below(), Direction.UP));
}
private static boolean isPassableSpawnSpace(ServerLevel level, BlockPos position) {
@@ -83,6 +83,22 @@ public final class LinkingGameTests {
helper.succeed();
}
@GameTest(template = "empty")
public static void descriptiveTravelRequiresFooting(GameTestHelper helper) {
BlockPos absoluteOrigin = helper.absolutePos(ORIGIN);
for (BlockPos position : BlockPos.betweenClosed(
absoluteOrigin.offset(-4, -3, -4), absoluteOrigin.offset(4, 3, 4))) {
helper.getLevel().setBlock(position, Blocks.AIR.defaultBlockState(), 2);
}
ServerPlayer player = helper.makeMockServerPlayerInLevel();
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), absoluteOrigin), 0.0F, 0.0F);
helper.assertValueEqual(LinkingService.travelWithFooting(player, target), LinkingService.LinkResult.NO_SAFE_DESTINATION,
"Descriptive Book travel must not place players without solid footing");
helper.succeed();
}
@GameTest(template = "empty")
public static void travelRejectsBlockedDestination(GameTestHelper helper) {
BlockPos absoluteOrigin = helper.absolutePos(ORIGIN);
@@ -49,7 +49,7 @@ public final class DescriptiveBookItem extends Item {
return false;
}
LinkingService.LinkResult result = LinkingService.travel(player, ageTarget);
LinkingService.LinkResult result = LinkingService.travelWithFooting(player, ageTarget);
if (result != LinkingService.LinkResult.SUCCESS) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unsafe"), true);
return false;