Public Access
This commit is contained in:
1 parent
3109938a97
commit
2b916ccd0b
19 files changed
+437
-24
No files matched your search
@@ -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
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
}
|
||||
}
|
||||
+50
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user