From 67ce69e8b2f6745be3b4f5439a86c83b4c04661d Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Mon, 21 Sep 2026 09:31:41 -0400 Subject: [PATCH] Require footing for descriptive book travel --- README.md | 3 ++- docs/PORTING.md | 3 +++ gradle.properties | 2 +- .../rivencraft/book/LinkingService.java | 24 +++++++++++++++---- .../rivencraft/gametest/LinkingGameTests.java | 16 +++++++++++++ .../rivencraft/item/DescriptiveBookItem.java | 2 +- 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5647821..f9f6e90 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/PORTING.md b/docs/PORTING.md index 32db7de..6073320 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -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. diff --git a/gradle.properties b/gradle.properties index 111aaeb..ed3b7f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java index 3368ec7..63020c7 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java @@ -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 destination = findSafeDestination(destinationLevel, target.location().pos()); + Optional 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 findSafeDestination(ServerLevel level, BlockPos origin) { + return findSafeDestination(level, origin, false); + } + + private static Optional 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) { diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index b9a705d..45c8997 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -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); diff --git a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java index 203c15c..af8abbe 100644 --- a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java +++ b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java @@ -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;