diff --git a/README.md b/README.md index f9f6e90..10075ef 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ 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. Descriptive -Book destinations additionally require solid footing beneath the player. +Book destinations additionally require solid footing beneath the player. If +their recorded arrival is unsafe, they search nearby terrain for a footed spot. ## Requirements diff --git a/docs/PORTING.md b/docs/PORTING.md index 6073320..ff273a1 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -159,6 +159,9 @@ 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. +Version 0.2.4 expands Descriptive Book arrival validation with an outward, +terrain-aware search for an alternate grounded location near an unsafe target. + 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 ed3b7f3..7d9b0d6 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.3 +mod_version=0.2.4 # 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 63020c7..3abf979 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.book; import java.util.Comparator; import java.util.Optional; +import java.util.stream.IntStream; import java.util.stream.Stream; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -9,12 +10,14 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.tags.BlockTags; import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.phys.Vec3; /** Server-side validation and execution for book travel. */ public final class LinkingService { private static final int HORIZONTAL_SEARCH_RADIUS = 4; private static final int VERTICAL_SEARCH_RADIUS = 2; + private static final int AGE_ARRIVAL_SEARCH_RADIUS = 64; private LinkingService() { } @@ -25,7 +28,20 @@ public final class LinkingService { /** Travels to an Age-style destination, which must have solid footing. */ public static LinkResult travelWithFooting(ServerPlayer player, LinkTarget target) { - return travel(player, target, true); + ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension()); + if (destinationLevel == null) { + return LinkResult.MISSING_DIMENSION; + } + + Optional destination = findFootedAgeDestination(destinationLevel, target.location().pos()); + if (destination.isEmpty()) { + return LinkResult.NO_SAFE_DESTINATION; + } + + Vec3 position = destination.get(); + player.teleportTo(destinationLevel, position.x, position.y, position.z, target.yaw(), target.pitch()); + player.resetFallDistance(); + return LinkResult.SUCCESS; } private static LinkResult travel(ServerPlayer player, LinkTarget target, boolean requiresFooting) { @@ -66,6 +82,42 @@ public final class LinkingService { .findFirst(); } + /** Finds a footed arrival near an unsafe Age spawn without scanning the whole dimension. */ + private static Optional findFootedAgeDestination(ServerLevel level, BlockPos origin) { + Optional localDestination = findSafeDestination(level, origin, true); + if (localDestination.isPresent()) { + return localDestination; + } + + WorldBorder border = level.getWorldBorder(); + return ageArrivalColumns(origin) + .flatMap(column -> surfaceCandidates(level, column)) + .filter(level::isInWorldBounds) + .filter(border::isWithinBounds) + .filter(position -> isSafe(level, position, true)) + .map(Vec3::atBottomCenterOf) + .findFirst(); + } + + private static Stream ageArrivalColumns(BlockPos origin) { + return IntStream.rangeClosed(1, AGE_ARRIVAL_SEARCH_RADIUS) + .boxed() + .flatMap(radius -> BlockPos.betweenClosedStream( + origin.offset(-radius, 0, -radius), + origin.offset(radius, 0, radius) + ) + .filter(position -> Math.max(Math.abs(position.getX() - origin.getX()), + Math.abs(position.getZ() - origin.getZ())) == radius) + ) + .map(BlockPos::immutable); + } + + private static Stream surfaceCandidates(ServerLevel level, BlockPos column) { + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, column.getX(), column.getZ()); + BlockPos surface = new BlockPos(column.getX(), surfaceY, column.getZ()); + return Stream.of(surface, surface.above(), surface.below(), surface.above(2), surface.below(2)); + } + private static Stream candidates(BlockPos origin) { return BlockPos.betweenClosedStream( origin.offset(-HORIZONTAL_SEARCH_RADIUS, -VERTICAL_SEARCH_RADIUS, -HORIZONTAL_SEARCH_RADIUS), diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index 45c8997..62bc089 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.GlobalPos; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestHelper; @@ -84,18 +85,24 @@ public final class LinkingGameTests { } @GameTest(template = "empty") - public static void descriptiveTravelRequiresFooting(GameTestHelper helper) { + public static void descriptiveTravelFindsAlternateFooting(GameTestHelper helper) { BlockPos absoluteOrigin = helper.absolutePos(ORIGIN); for (BlockPos position : BlockPos.betweenClosed( - absoluteOrigin.offset(-4, -3, -4), absoluteOrigin.offset(4, 3, 4))) { + absoluteOrigin.offset(-6, -3, -6), absoluteOrigin.offset(6, 3, 6))) { helper.getLevel().setBlock(position, Blocks.AIR.defaultBlockState(), 2); } + BlockPos alternateDestination = absoluteOrigin.offset(6, 0, 0); + helper.getLevel().setBlock(alternateDestination.below(), Blocks.STONE.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.assertValueEqual(LinkingService.travelWithFooting(player, target), LinkingService.LinkResult.SUCCESS, + "Descriptive Book travel must find an alternate grounded arrival"); + BlockPos landing = player.blockPosition(); + helper.assertTrue(helper.getLevel().getBlockState(landing.below()).isFaceSturdy( + helper.getLevel(), landing.below(), Direction.UP), + "The alternate arrival must have sturdy footing"); helper.succeed(); }