Find alternate grounded age arrivals
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 09:37:06 -04:00
1 parent 67ce69e8b2
commit a9390c3e4c
5 files changed
+70 -7

No files matched your search

+2 -1
View File
@@ -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
+3
View File
@@ -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.
+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.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
@@ -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<Vec3> 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<Vec3> findFootedAgeDestination(ServerLevel level, BlockPos origin) {
Optional<Vec3> 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<BlockPos> 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<BlockPos> 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<BlockPos> candidates(BlockPos origin) {
return BlockPos.betweenClosedStream(
origin.offset(-HORIZONTAL_SEARCH_RADIUS, -VERTICAL_SEARCH_RADIUS, -HORIZONTAL_SEARCH_RADIUS),
@@ -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();
}