Add linking game tests
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 08:32:18 -04:00
1 parent 5a9a13f35c
commit b6f321e6cb
5 files changed
+86 -2

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ bin/
### Minecraft Modding ### ### Minecraft Modding ###
run/ run/
logs/
!**/src/**/run/ !**/src/**/run/
**/src/generated/**/.cache/ **/src/generated/**/.cache/
repo/ repo/
+6 -1
View File
@@ -54,7 +54,7 @@ the symbol grammar is introduced.
- [x] Server-provided destination snapshots rendered as rotating Ponder scenes. - [x] Server-provided destination snapshots rendered as rotating Ponder scenes.
- [x] Book Stand floating names synchronized from the stored book stack. - [x] Book Stand floating names synchronized from the stored book stack.
- [x] Open the stored book screen from a Book Stand with server-validated linking. - [x] Open the stored book screen from a Book Stand with server-validated linking.
- [ ] Automated in-game tests for binding, persistence, and travel. - [x] Automated in-game tests for binding, persistence, and travel.
The temporary shapeless Linking Book recipe makes the linking subsystem easy The temporary shapeless Linking Book recipe makes the linking subsystem easy
to test. It can move into the writing/bookbinder progression when that system to test. It can move into the writing/bookbinder progression when that system
@@ -118,6 +118,11 @@ Version 0.1.14 expands the destination snapshot to 9-by-7-by-17 blocks along
the Z axis, rescales the rotating scene to expose that depth, and colors the the Z axis, rescales the rotating scene to expose that depth, and colors the
panel background with the destination biome's sky color. panel background with the destination biome's sky color.
Version 0.1.15 adds three dedicated-server GameTests covering Linking Book
component serialization, successful safe travel with restored orientation, and
rejection of fully blocked destinations. The test suite uses a minimal bundled
structure template and runs with `./gradlew runGameTestServer`.
The preview code is separated behind a renderer interface and uses an isolated The preview code is separated behind a renderer interface and uses an isolated
Ponder level. The first iteration supplies the animated loading boundary; the Ponder level. The first iteration supplies the animated loading boundary; the
next iteration will add server-authoritative snapshot transport and populate next iteration will add server-authoritative snapshot transport and populate
+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. # 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 mod_license=LGPL-3.0-or-later
# The mod version. See https://semver.org/ # The mod version. See https://semver.org/
mod_version=0.1.14 mod_version=0.1.15
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # 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. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html
@@ -0,0 +1,78 @@
package net.dnorberg.rivencraft.gametest;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
/** Server-side regression coverage for persistent linking-book data and travel safety. */
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class LinkingGameTests {
private static final BlockPos ORIGIN = new BlockPos(0, 1, 0);
private LinkingGameTests() {
}
@GameTest(template = "empty")
public static void linkTargetSurvivesItemSerialization(GameTestHelper helper) {
LinkTarget expected = new LinkTarget(
GlobalPos.of(helper.getLevel().dimension(), helper.absolutePos(ORIGIN)),
127.5F,
-31.25F
);
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
book.set(ModDataComponents.LINK_TARGET.get(), expected);
CompoundTag serialized = (CompoundTag) book.saveOptional(helper.getLevel().registryAccess());
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized);
helper.assertValueEqual(restored.get(ModDataComponents.LINK_TARGET.get()), expected,
"A Linking Book must retain its dimension, position, and orientation after serialization");
helper.succeed();
}
@GameTest(template = "empty")
public static void travelUsesRecordedSafePosition(GameTestHelper helper) {
helper.setBlock(ORIGIN.below(), Blocks.STONE);
BlockPos destination = helper.absolutePos(ORIGIN);
ServerPlayer player = helper.makeMockServerPlayerInLevel();
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 90.0F, 15.0F);
LinkingService.LinkResult result = LinkingService.travel(player, target);
helper.assertValueEqual(result, LinkingService.LinkResult.SUCCESS,
"A Linking Book must travel to a clear location with a sturdy floor");
helper.assertTrue(player.level() == helper.getLevel(), "Travel must remain in the recorded dimension");
helper.assertTrue(Math.abs(player.getYRot() - 90.0F) < 0.01F && Math.abs(player.getXRot() - 15.0F) < 0.01F,
"Travel must restore the recorded orientation");
helper.succeed();
}
@GameTest(template = "empty")
public static void travelRejectsBlockedDestination(GameTestHelper helper) {
BlockPos absoluteOrigin = helper.absolutePos(ORIGIN);
for (BlockPos position : BlockPos.betweenClosed(
absoluteOrigin.offset(-4, -2, -4), absoluteOrigin.offset(4, 2, 4))) {
helper.getLevel().setBlockAndUpdate(position, Blocks.STONE.defaultBlockState());
}
ServerPlayer player = helper.makeMockServerPlayerInLevel();
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), absoluteOrigin), 0.0F, 0.0F);
helper.assertValueEqual(LinkingService.travel(player, target), LinkingService.LinkResult.NO_SAFE_DESTINATION,
"Linking must fail rather than placing a player inside a blocked destination");
helper.succeed();
}
}