Public Access
This commit is contained in:
1 parent
aed8d6a5c3
commit
3e8cbf6bc5
12 files changed
+233
-14
No files matched your search
@@ -4,11 +4,10 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of
|
||||
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
|
||||
1.21.1 on NeoForge.
|
||||
|
||||
The bootstrap milestone is complete and books/linking is in progress. The
|
||||
project currently provides an unwritten Descriptive Book and a functional
|
||||
Linking Book with persistent, dimension-aware destinations and safe landing
|
||||
checks. Age generation, symbols, instability, and writing mechanics have not
|
||||
been ported yet.
|
||||
The bootstrap and books/linking milestones are complete. The project currently
|
||||
provides functional Linking Books and temporary vanilla-dimension Descriptive
|
||||
Book destinations with generated Return Links. Age generation, symbols,
|
||||
instability, and writing mechanics have not been ported yet.
|
||||
|
||||
## Linking Book
|
||||
|
||||
@@ -17,7 +16,7 @@ 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 when
|
||||
its preview panel is clicked. 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
|
||||
dimension is loaded and a non-fluid, passable landing position can be found
|
||||
nearby.
|
||||
|
||||
Right-click an empty vanilla lectern with a Rivencraft book to convert it into
|
||||
@@ -32,6 +31,13 @@ rotating Ponder scene on the left page, backed by the
|
||||
destination biome's sky color. The snapshot is visual only; the server
|
||||
independently validates every link.
|
||||
|
||||
## Descriptive Book
|
||||
|
||||
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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Java 21 JDK
|
||||
|
||||
+7
-3
@@ -10,7 +10,7 @@ split into independently testable milestones.
|
||||
1. **Bootstrap** — NeoForge 1.21.1 build, mod metadata, item registration, and
|
||||
CI. **Complete.**
|
||||
2. **Books and linking** — descriptive/linking book data components, lectern-like
|
||||
interaction, safe teleportation, and return links. **In progress.**
|
||||
interaction, safe teleportation, and return links. **Complete.**
|
||||
3. **Ages** — dimension definitions backed by data packs, deterministic seeds,
|
||||
lifecycle management, and persistence.
|
||||
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
|
||||
@@ -42,10 +42,10 @@ the symbol grammar is introduced.
|
||||
- [x] Persistent, network-synchronized link-target data component.
|
||||
- [x] Linking Book binding with dimension, block position, yaw, and pitch.
|
||||
- [x] Server-authoritative cross-dimension travel with world-border, collision,
|
||||
fluid, and floor checks.
|
||||
and fluid checks.
|
||||
- [x] Nearby landing search and clear failure messages.
|
||||
- [x] Descriptive Book identity and Age reference components.
|
||||
- [ ] Return-link creation as part of Descriptive Book travel.
|
||||
- [x] Temporary vanilla-dimension Descriptive Book travel with return-link creation.
|
||||
- [x] Lectern-to-Book-Stand conversion, displayed book, activation, removal,
|
||||
persistence, and drops.
|
||||
- [x] Book-screen foundation with server-authoritative naming, binding, and
|
||||
@@ -145,6 +145,10 @@ unchanged.
|
||||
Version 0.1.21 treats air and leaf blocks as passable linking destinations;
|
||||
links no longer require a solid floor beneath the player.
|
||||
|
||||
Version 0.2.0 completes Milestone 2. Until Milestone 3 provides generated Age
|
||||
dimensions, Descriptive Book UUIDs deterministically resolve to vanilla
|
||||
dimensions and create a bound Return Link to the source on successful travel.
|
||||
|
||||
The preview code is separated behind a renderer interface and uses an isolated
|
||||
Ponder level populated from server-authoritative destination snapshots.
|
||||
|
||||
|
||||
+1
-1
@@ -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.1.21
|
||||
mod_version=0.2.0
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.GlobalPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Temporary Milestone 2 destination resolver.
|
||||
*
|
||||
* <p>Until Ages own generated dimensions, an Age UUID deterministically selects
|
||||
* one of the three vanilla dimensions. Milestone 3 will replace this with the
|
||||
* persistent Age-to-dimension mapping without changing book identities.</p>
|
||||
*/
|
||||
public final class VanillaAgeResolver {
|
||||
private static final List<ResourceKey<Level>> VANILLA_DIMENSIONS = List.of(
|
||||
Level.OVERWORLD,
|
||||
Level.NETHER,
|
||||
Level.END
|
||||
);
|
||||
private static final List<AgeReference> DEMO_AGES = List.of(
|
||||
new AgeReference(new UUID(0L, 0L), "Overworld Age"),
|
||||
new AgeReference(new UUID(0L, 1L), "Nether Age"),
|
||||
new AgeReference(new UUID(0L, 2L), "End Age")
|
||||
);
|
||||
|
||||
private VanillaAgeResolver() {
|
||||
}
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference age) {
|
||||
ResourceKey<Level> dimension = VANILLA_DIMENSIONS.get(Math.floorMod(age.id().hashCode(), VANILLA_DIMENSIONS.size()));
|
||||
ServerLevel level = player.getServer().getLevel(dimension);
|
||||
if (level == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new LinkTarget(
|
||||
GlobalPos.of(dimension, level.getSharedSpawnPos()),
|
||||
level.getSharedSpawnAngle(),
|
||||
0.0F
|
||||
));
|
||||
}
|
||||
|
||||
/** Written sample books exposed in the creative tab until the writing system exists. */
|
||||
public static List<AgeReference> demoAges() {
|
||||
return DEMO_AGES;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public final class ClientBookInteractionHandler {
|
||||
if (linkingBook) {
|
||||
Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getHand(), event.getItemStack().copy()));
|
||||
} else {
|
||||
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getItemStack().copy()));
|
||||
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class ClientBookInteractionHandler {
|
||||
if (book.is(ModItems.LINKING_BOOK.get())) {
|
||||
Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getPos(), book.copy()));
|
||||
} else if (book.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(book.copy()));
|
||||
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getPos(), book.copy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,43 @@
|
||||
package net.dnorberg.rivencraft.client.screen;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.network.UseDescriptiveBookPayload;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
|
||||
public final class DescriptiveBookScreen extends Screen {
|
||||
private final AgeReference age;
|
||||
private final InteractionHand hand;
|
||||
private final BlockPos bookStandPos;
|
||||
|
||||
public DescriptiveBookScreen(ItemStack book) {
|
||||
public DescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
age = book.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
this.hand = hand;
|
||||
bookStandPos = null;
|
||||
}
|
||||
|
||||
public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
age = book.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
hand = InteractionHand.MAIN_HAND;
|
||||
this.bookStandPos = bookStandPos.immutable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (age != null) {
|
||||
addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.link"), button -> link())
|
||||
.bounds(width / 2 - 75, height / 2 + 30, 150, 20)
|
||||
.build());
|
||||
}
|
||||
addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
|
||||
.bounds(width / 2 - 75, height / 2 + 55, 150, 20)
|
||||
.build());
|
||||
@@ -36,4 +56,13 @@ public final class DescriptiveBookScreen extends Screen {
|
||||
graphics.drawCenteredString(font, Component.literal(age.id().toString()), centerX, height / 2 + 4, 0xFF777777);
|
||||
}
|
||||
}
|
||||
|
||||
private void link() {
|
||||
PacketDistributor.sendToServer(new UseDescriptiveBookPayload(
|
||||
bookStandPos != null,
|
||||
bookStandPos == null && hand == InteractionHand.OFF_HAND,
|
||||
bookStandPos == null ? BlockPos.ZERO : bookStandPos
|
||||
));
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -44,6 +45,23 @@ public final class LinkingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void returnBookRetainsSourceLocation(GameTestHelper helper) {
|
||||
LinkTarget source = new LinkTarget(
|
||||
GlobalPos.of(helper.getLevel().dimension(), helper.absolutePos(ORIGIN)),
|
||||
45.0F,
|
||||
-12.0F
|
||||
);
|
||||
|
||||
ItemStack returnBook = DescriptiveBookItem.createReturnBook(source);
|
||||
|
||||
helper.assertTrue(returnBook.is(ModItems.LINKING_BOOK.get()),
|
||||
"Descriptive Book travel must create a Linking Book");
|
||||
helper.assertValueEqual(returnBook.get(ModDataComponents.LINK_TARGET.get()), source,
|
||||
"The Return Link must retain the source dimension, location, and orientation");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void travelUsesLeafyDestinationWithoutFloor(GameTestHelper helper) {
|
||||
helper.setBlock(ORIGIN.below(), Blocks.AIR);
|
||||
|
||||
@@ -2,10 +2,18 @@ package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.book.VanillaAgeResolver;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
@@ -27,6 +35,42 @@ public final class DescriptiveBookItem extends Item {
|
||||
return stack.has(ModDataComponents.AGE_REFERENCE.get()) || super.isFoil(stack);
|
||||
}
|
||||
|
||||
public static boolean useWrittenBook(ServerPlayer player, ItemStack stack) {
|
||||
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
|
||||
if (age == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unwritten"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
LinkTarget returnTarget = LinkTarget.from(player);
|
||||
LinkTarget ageTarget = VanillaAgeResolver.resolve(player, age).orElse(null);
|
||||
if (ageTarget == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
LinkingService.LinkResult result = LinkingService.travel(player, ageTarget);
|
||||
if (result != LinkingService.LinkResult.SUCCESS) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unsafe"), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemStack returnBook = createReturnBook(returnTarget);
|
||||
if (!player.getInventory().add(returnBook)) {
|
||||
player.drop(returnBook, false);
|
||||
}
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.PORTAL_TRAVEL, SoundSource.PLAYERS, 0.35F, 1.0F);
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.linked", age.name()), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ItemStack createReturnBook(LinkTarget returnTarget) {
|
||||
ItemStack returnBook = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||
returnBook.set(ModDataComponents.LINK_TARGET.get(), returnTarget);
|
||||
returnBook.set(DataComponents.CUSTOM_NAME, Component.translatable("item.rivencraft.linking_book.return"));
|
||||
return returnBook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(
|
||||
ItemStack stack,
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.network;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
@@ -30,9 +31,11 @@ public final class ModNetworking {
|
||||
}
|
||||
|
||||
public static void register(RegisterPayloadHandlersEvent event) {
|
||||
PayloadRegistrar registrar = event.registrar("3");
|
||||
PayloadRegistrar registrar = event.registrar("4");
|
||||
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
|
||||
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook);
|
||||
registrar.playToServer(UseDescriptiveBookPayload.TYPE, UseDescriptiveBookPayload.STREAM_CODEC,
|
||||
ModNetworking::useDescriptiveBook);
|
||||
registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC, ModNetworking::useBookStand);
|
||||
registrar.playToServer(RequestLinkPreviewPayload.TYPE, RequestLinkPreviewPayload.STREAM_CODEC,
|
||||
ModNetworking::requestLinkPreview);
|
||||
@@ -85,6 +88,27 @@ public final class ModNetworking {
|
||||
}
|
||||
}
|
||||
|
||||
private static void useDescriptiveBook(UseDescriptiveBookPayload payload, IPayloadContext context) {
|
||||
if (!(context.player() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack stack;
|
||||
if (payload.fromStand()) {
|
||||
if (player.distanceToSqr(Vec3.atCenterOf(payload.standPos())) > 64.0D
|
||||
|| !(player.level().getBlockEntity(payload.standPos()) instanceof BookStandBlockEntity stand)) {
|
||||
return;
|
||||
}
|
||||
stack = stand.getBook();
|
||||
} else {
|
||||
stack = player.getItemInHand(hand(payload.offhand()));
|
||||
}
|
||||
|
||||
if (stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
DescriptiveBookItem.useWrittenBook(player, stack);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requestLinkPreview(RequestLinkPreviewPayload payload, IPayloadContext context) {
|
||||
if (!(context.player() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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;
|
||||
|
||||
/** Requests a server-validated Descriptive Book link from a hand or Book Stand. */
|
||||
public record UseDescriptiveBookPayload(boolean fromStand, boolean offhand, BlockPos standPos)
|
||||
implements CustomPacketPayload {
|
||||
public static final Type<UseDescriptiveBookPayload> TYPE = new Type<>(
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_descriptive_book")
|
||||
);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, UseDescriptiveBookPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, UseDescriptiveBookPayload::fromStand,
|
||||
ByteBufCodecs.BOOL, UseDescriptiveBookPayload::offhand,
|
||||
BlockPos.STREAM_CODEC, UseDescriptiveBookPayload::standPos,
|
||||
UseDescriptiveBookPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.VanillaAgeResolver;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
@@ -18,6 +21,12 @@ public final class ModCreativeTabs {
|
||||
.icon(() -> ModItems.DESCRIPTIVE_BOOK.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
VanillaAgeResolver.demoAges().forEach(age -> {
|
||||
ItemStack book = new ItemStack(ModItems.DESCRIPTIVE_BOOK.get());
|
||||
book.set(ModDataComponents.AGE_REFERENCE.get(), age);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(age.name()));
|
||||
output.accept(book);
|
||||
});
|
||||
output.accept(ModItems.LINKING_BOOK.get());
|
||||
})
|
||||
.build());
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
"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)",
|
||||
"item.rivencraft.linking_book.return": "Return Link",
|
||||
"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 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",
|
||||
"message.rivencraft.descriptive_book.unwritten": "This Descriptive Book is unwritten",
|
||||
"message.rivencraft.descriptive_book.unavailable": "That Age is unavailable",
|
||||
"message.rivencraft.descriptive_book.unsafe": "No safe destination was found in that Age",
|
||||
"message.rivencraft.descriptive_book.linked": "Linked to %s — a Return Link was created",
|
||||
"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",
|
||||
|
||||
Reference in new issue
Block a user