Add book GUI and Ponder preview foundation
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 07:24:12 -04:00
1 parent 3109938a97
commit 2b916ccd0b
19 files changed
+437 -24

No files matched your search

+12 -5
View File
@@ -12,11 +12,13 @@ been ported yet.
## Linking Book
Craft a Linking Book from a vanilla book and an ender pearl. Sneak-right-click
the unbound book to record the current location, then right-click it to return
there. The destination is stored on the item and survives inventory and world
saves. Travel only succeeds if the target dimension is loaded and a solid,
collision-free landing position can be found nearby.
Craft a Linking Book from a vanilla book and an ender pearl, then use it in the
air. An unbound book opens a screen where it can be named and bound to the
current location. A bound book opens its destination screen and travels only
when the Link button is pressed. The destination is stored on the item and
survives inventory and world saves. Travel only succeeds if the target
dimension is loaded and a solid, collision-free landing position can be found
nearby.
Right-click an empty vanilla lectern with a Rivencraft book to convert it into
a Book Stand and place the book on it. Use a Linking Book on the stand by
@@ -24,11 +26,16 @@ right-clicking with an empty hand. Sneak-right-click with an empty hand to take
the book back and restore the vanilla lectern. Breaking a Book Stand drops both
the stored book and a vanilla lectern.
Version 0.1.4 establishes a Ponder-backed destination-preview boundary and an
animated loading view. Transmitting a destination snapshot and rendering its
actual blocks as the rotating scene is the next preview iteration.
## Requirements
- A Java 21 JDK
- Minecraft 1.21.1
- NeoForge 21.1.251 or newer in the 21.1 line
- Ponder 1.0.82 for Minecraft 1.21.1 (client)
## Build and run
+2 -1
View File
@@ -27,7 +27,7 @@ sourceSets.main.resources {
}
repositories {
// Add here additional repositories if required by some of the dependencies below.
maven { url = 'https://maven.createmod.net' }
}
base {
@@ -116,6 +116,7 @@ configurations {
}
dependencies {
implementation "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
// Example optional mod dependency with JEI
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
+16
View File
@@ -48,6 +48,11 @@ the symbol grammar is introduced.
- [ ] Return-link creation as part of Descriptive Book travel.
- [x] Lectern-to-Book-Stand conversion, displayed book, activation, removal,
persistence, and drops.
- [x] Book-screen foundation with server-authoritative naming, binding, and
explicit link activation.
- [x] Ponder dependency and isolated destination-preview level boundary.
- [ ] Server-provided destination snapshots rendered as rotating Ponder scenes.
- [ ] Book Stand floating names and opening the stored book screen.
- [ ] Automated in-game tests for binding, persistence, and travel.
The temporary shapeless Linking Book recipe makes the linking subsystem easy
@@ -69,6 +74,17 @@ initial cooldown used Minecraft's item-type cooldown system, which delayed all
Linking Book stacks at once. Version 0.1.3 removes that cooldown to match the
legacy behavior.
Version 0.1.4 moves Linking Book binding and travel behind a book screen. The
client requests an action, while the server revalidates the held book and owns
all changes to its destination and custom name. Descriptive Book Age references
now carry a display name alongside their stable UUID, with backward-compatible
decoding for existing UUID-only data.
The preview code is separated behind a renderer interface and uses an isolated
Ponder level. The first iteration supplies the animated loading boundary; the
next iteration will add server-authoritative snapshot transport and populate
that level with the destination blocks.
## Version policy
Versions follow `x.y.z`:
+2 -1
View File
@@ -19,6 +19,7 @@ minecraft_version=1.21.1
minecraft_version_range=[1.21.1]
# The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=21.1.251
ponder_version=1.0.82
# The loader version range can only use the major version of FML as bounds
loader_version_range=[1,)
@@ -32,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.1.3
mod_version=0.1.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
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
@@ -18,6 +19,7 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger();
public Rivencraft(IEventBus modEventBus) {
modEventBus.addListener(ModNetworking::register);
ModDataComponents.register(modEventBus);
ModBlocks.register(modEventBus);
ModBlockEntities.register(modEventBus);
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.book;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
import net.minecraft.core.UUIDUtil;
@@ -13,11 +14,21 @@ import net.minecraft.network.codec.StreamCodec;
* Books only retain the identifier so dimension regeneration or migration does
* not invalidate every existing book.</p>
*/
public record AgeReference(UUID id) {
public static final Codec<AgeReference> CODEC = UUIDUtil.STRING_CODEC.xmap(AgeReference::new, AgeReference::id);
public record AgeReference(UUID id, String name) {
private static final String UNNAMED_AGE = "Unnamed Age";
private static final Codec<AgeReference> RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group(
UUIDUtil.STRING_CODEC.fieldOf("id").forGetter(AgeReference::id),
Codec.STRING.optionalFieldOf("name", UNNAMED_AGE).forGetter(AgeReference::name)
).apply(instance, AgeReference::new));
public static final Codec<AgeReference> CODEC = Codec.withAlternative(
RECORD_CODEC,
UUIDUtil.STRING_CODEC.xmap(id -> new AgeReference(id, UNNAMED_AGE), AgeReference::id)
);
public static final StreamCodec<ByteBuf, AgeReference> STREAM_CODEC = StreamCodec.composite(
UUIDUtil.STREAM_CODEC,
AgeReference::id,
net.minecraft.network.codec.ByteBufCodecs.STRING_UTF8,
AgeReference::name,
AgeReference::new
);
@@ -0,0 +1,29 @@
package net.dnorberg.rivencraft.client;
import net.dnorberg.rivencraft.client.screen.LinkingBookScreen;
import net.dnorberg.rivencraft.client.screen.DescriptiveBookScreen;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.Minecraft;
import net.minecraft.world.InteractionResult;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
public final class ClientBookInteractionHandler {
private ClientBookInteractionHandler() {
}
public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
boolean linkingBook = event.getItemStack().is(ModItems.LINKING_BOOK.get());
boolean descriptiveBook = event.getItemStack().is(ModItems.DESCRIPTIVE_BOOK.get());
if (!linkingBook && !descriptiveBook) {
return;
}
event.setCanceled(true);
event.setCancellationResult(InteractionResult.SUCCESS);
if (linkingBook) {
Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getHand(), event.getItemStack().copy()));
} else {
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getItemStack().copy()));
}
}
}
@@ -5,6 +5,8 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
@@ -16,4 +18,9 @@ public final class ClientEvents {
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
}
@SubscribeEvent
public static void clientSetup(FMLClientSetupEvent event) {
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
}
}
@@ -0,0 +1,13 @@
package net.dnorberg.rivencraft.client.preview;
import net.minecraft.client.gui.GuiGraphics;
public interface DestinationPreviewRenderer extends AutoCloseable {
void tick();
void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick);
@Override
default void close() {
}
}
@@ -0,0 +1,50 @@
package net.dnorberg.rivencraft.client.preview;
import net.createmod.ponder.api.level.PonderLevel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
/**
* Initial Ponder-backed preview boundary.
*
* <p>The isolated level is ready to receive server-provided snapshot blocks.
* Until snapshot networking lands, the panel renders an animated loading
* state rather than reading arbitrary remote chunks from the client.</p>
*/
public final class PonderDestinationPreviewRenderer implements DestinationPreviewRenderer {
private final PonderLevel previewLevel;
private int ticks;
public PonderDestinationPreviewRenderer() {
Minecraft minecraft = Minecraft.getInstance();
previewLevel = new PonderLevel(BlockPos.ZERO, minecraft.level);
}
@Override
public void tick() {
ticks++;
previewLevel.tick();
}
@Override
public void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick) {
graphics.fill(left, top, left + width, top + height, 0xFF080C18);
graphics.renderOutline(left, top, width, height, 0xFF7B6AA8);
double angle = (ticks + partialTick) * 0.04;
int centerX = left + width / 2;
int centerY = top + height / 2;
int orbitX = centerX + (int)(Math.cos(angle) * Math.max(8, width / 4));
int orbitY = centerY + (int)(Math.sin(angle) * Math.max(4, height / 8));
graphics.fill(orbitX - 2, orbitY - 2, orbitX + 3, orbitY + 3, 0xFF8ED6FF);
graphics.drawCenteredString(
Minecraft.getInstance().font,
Component.translatable("screen.rivencraft.book.preview_pending"),
centerX,
centerY - 4,
0xFFB8C5E0
);
}
}
@@ -0,0 +1,39 @@
package net.dnorberg.rivencraft.client.screen;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
public final class DescriptiveBookScreen extends Screen {
private final AgeReference age;
public DescriptiveBookScreen(ItemStack book) {
super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get());
}
@Override
protected void init() {
addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
.bounds(width / 2 - 75, height / 2 + 55, 150, 20)
.build());
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick);
int centerX = width / 2;
graphics.drawCenteredString(font, title, centerX, height / 2 - 70, 0xFFFFFFFF);
if (age == null) {
graphics.drawCenteredString(font, Component.translatable("screen.rivencraft.book.unwritten"),
centerX, height / 2 - 4, 0xFFAAAAAA);
} else {
graphics.drawCenteredString(font, Component.literal(age.name()), centerX, height / 2 - 16, 0xFFE7D7A9);
graphics.drawCenteredString(font, Component.literal(age.id().toString()), centerX, height / 2 + 4, 0xFF777777);
}
}
}
@@ -0,0 +1,111 @@
package net.dnorberg.rivencraft.client.screen;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer;
import net.dnorberg.rivencraft.client.preview.PonderDestinationPreviewRenderer;
import net.dnorberg.rivencraft.network.BindLinkingBookPayload;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.network.UseLinkingBookPayload;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.PacketDistributor;
public final class LinkingBookScreen extends Screen {
private static final int PANEL_WIDTH = 180;
private static final int PREVIEW_HEIGHT = 100;
private final InteractionHand hand;
private final ItemStack book;
private final LinkTarget target;
private DestinationPreviewRenderer preview;
private EditBox titleInput;
public LinkingBookScreen(InteractionHand hand, ItemStack book) {
super(book.getHoverName());
this.hand = hand;
this.book = book;
target = book.get(ModDataComponents.LINK_TARGET.get());
}
@Override
protected void init() {
int left = (width - PANEL_WIDTH) / 2;
int buttonY = height / 2 + 72;
if (target == null) {
titleInput = new EditBox(font, left, height / 2 - 10, PANEL_WIDTH, 20,
Component.translatable("screen.rivencraft.book.title"));
titleInput.setMaxLength(ModNetworking.MAX_BOOK_TITLE_LENGTH);
titleInput.setHint(Component.translatable("screen.rivencraft.book.title_hint"));
addRenderableWidget(titleInput);
setInitialFocus(titleInput);
addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.bind"), button -> bind())
.bounds(left, height / 2 + 20, 86, 20)
.build());
addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose())
.bounds(left + 94, height / 2 + 20, 86, 20)
.build());
} else {
preview = new PonderDestinationPreviewRenderer();
addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.link"), button -> link())
.bounds(left, buttonY, 86, 20)
.build());
addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
.bounds(left + 94, buttonY, 86, 20)
.build());
}
}
@Override
public void tick() {
if (preview != null) {
preview.tick();
}
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick);
int centerX = width / 2;
graphics.drawCenteredString(font, title, centerX, height / 2 - 82, 0xFFFFFFFF);
if (target == null) {
graphics.drawCenteredString(font, Component.translatable("screen.rivencraft.book.name_prompt"),
centerX, height / 2 - 34, 0xFFB8B8B8);
return;
}
int left = (width - PANEL_WIDTH) / 2;
int previewTop = height / 2 - 60;
preview.render(graphics, left, previewTop, PANEL_WIDTH, PREVIEW_HEIGHT, partialTick);
BlockPos pos = target.location().pos();
graphics.drawCenteredString(font, Component.literal(
target.location().dimension().location() + " " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ()),
centerX, previewTop + PREVIEW_HEIGHT + 8, 0xFFB8B8B8);
}
@Override
public void removed() {
if (preview != null) {
preview.close();
}
}
private void bind() {
String name = titleInput.getValue().strip();
if (!name.isEmpty()) {
PacketDistributor.sendToServer(new BindLinkingBookPayload(hand == InteractionHand.OFF_HAND, name));
onClose();
}
}
private void link() {
PacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
onClose();
}
}
@@ -37,7 +37,7 @@ public final class DescriptiveBookItem extends Item {
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
MutableComponent status = age == null
? Component.translatable("item.rivencraft.descriptive_book.unwritten")
: Component.translatable("item.rivencraft.descriptive_book.age", age.shortId());
: Component.translatable("item.rivencraft.descriptive_book.age", age.name());
tooltipComponents.add(status.withStyle(ChatFormatting.GRAY));
}
}
@@ -33,19 +33,9 @@ public final class LinkingBookItem extends Item {
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
if (target == null) {
if (!player.isShiftKeyDown()) {
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bind_hint"), true);
return InteractionResultHolder.fail(stack);
}
target = LinkTarget.from(serverPlayer);
stack.set(ModDataComponents.LINK_TARGET.get(), target);
level.playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound"), true);
return InteractionResultHolder.success(stack);
return InteractionResultHolder.consume(stack);
}
return useBoundBook(serverPlayer, stack);
return InteractionResultHolder.consume(stack);
}
public static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack) {
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record BindLinkingBookPayload(boolean offhand, String title) implements CustomPacketPayload {
public static final Type<BindLinkingBookPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_linking_book")
);
public static final StreamCodec<RegistryFriendlyByteBuf, BindLinkingBookPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL,
BindLinkingBookPayload::offhand,
ByteBufCodecs.STRING_UTF8,
BindLinkingBookPayload::title,
BindLinkingBookPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,72 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
public final class ModNetworking {
public static final int MAX_BOOK_TITLE_LENGTH = 32;
private ModNetworking() {
}
public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("1");
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook);
}
private static void bindBook(BindLinkingBookPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
}
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
if (!stack.is(ModItems.LINKING_BOOK.get()) || stack.has(ModDataComponents.LINK_TARGET.get())) {
return;
}
String title = normalizeTitle(payload.title());
if (title.isEmpty()) {
return;
}
stack.set(ModDataComponents.LINK_TARGET.get(), LinkTarget.from(player));
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound", title), true);
}
private static void useBook(UseLinkingBookPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
}
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
if (stack.is(ModItems.LINKING_BOOK.get()) && stack.has(ModDataComponents.LINK_TARGET.get())) {
LinkingBookItem.useBoundBook(player, stack);
}
}
private static InteractionHand hand(boolean offhand) {
return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
}
private static String normalizeTitle(String title) {
String normalized = title.strip();
return normalized.length() <= MAX_BOOK_TITLE_LENGTH
? normalized
: normalized.substring(0, MAX_BOOK_TITLE_LENGTH).stripTrailing();
}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record UseLinkingBookPayload(boolean offhand) implements CustomPacketPayload {
public static final Type<UseLinkingBookPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_linking_book")
);
public static final StreamCodec<RegistryFriendlyByteBuf, UseLinkingBookPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL,
UseLinkingBookPayload::offhand,
UseLinkingBookPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -1,15 +1,22 @@
{
"item.rivencraft.descriptive_book": "Descriptive Book",
"item.rivencraft.descriptive_book.unwritten": "Unwritten",
"item.rivencraft.descriptive_book.age": "Age %s",
"item.rivencraft.descriptive_book.age": "%s",
"item.rivencraft.linking_book": "Linking Book",
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
"message.rivencraft.linking_book.unbound": "This Linking Book is unbound",
"message.rivencraft.linking_book.bound": "Linking Book bound",
"message.rivencraft.linking_book.bound": "Linking Book bound as %s",
"message.rivencraft.linking_book.linked": "Linked",
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
"screen.rivencraft.book.title": "Book title",
"screen.rivencraft.book.title_hint": "Name this Linking Book",
"screen.rivencraft.book.name_prompt": "Name the book before binding it to this location",
"screen.rivencraft.book.bind": "Bind",
"screen.rivencraft.book.link": "Link",
"screen.rivencraft.book.unwritten": "This Descriptive Book is unwritten",
"screen.rivencraft.book.preview_pending": "Ponder preview loading",
"itemGroup.rivencraft": "Rivencraft"
}
@@ -86,6 +86,13 @@ A spiritual successor and modern port of Mystcraft for Minecraft 1.21.1 on NeoFo
ordering="NONE"
side="BOTH"
[[dependencies.${mod_id}]]
modId="ponder"
type="required"
versionRange="[1.0.82,2.0.0)"
ordering="AFTER"
side="CLIENT"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.