diff --git a/dev images/Pasted image.png b/dev images/Pasted image.png new file mode 100644 index 0000000..7218961 Binary files /dev/null and b/dev images/Pasted image.png differ diff --git a/dev images/book binder.png b/dev images/book binder.png deleted file mode 100644 index 93cdee6..0000000 Binary files a/dev images/book binder.png and /dev/null differ diff --git a/dev images/ink mixer.png b/dev images/ink mixer.png deleted file mode 100644 index 910a822..0000000 Binary files a/dev images/ink mixer.png and /dev/null differ diff --git a/docs/LEGACY_M7_AUDIT.md b/docs/LEGACY_M7_AUDIT.md index 3d447f9..abcc1fd 100644 --- a/docs/LEGACY_M7_AUDIT.md +++ b/docs/LEGACY_M7_AUDIT.md @@ -46,7 +46,7 @@ For a manual check, create a fresh surface Age and run `/rivencraft library near Issue #75's comment calls this a despawning drop. The code differs subtly: [`EntityLinkbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/entity/EntityLinkbook.java#L190-L224) explicitly cannot despawn, but takes one starvation damage every 10,000 ticks and damage while wet. It can eventually be destroyed if left unattended; a vanilla item entity's fixed five-minute timeout is not parity. A modern equivalent should retain the visible book and avoid silently deleting its Age identity at that timeout. -Legacy's travel path creates no automatic Return Link. Rivencraft now defaults to `DISABLED` in new server configs while retaining `ON_CREATION` and `ALWAYS` as opt-ins. Existing saved server configs keep their explicit value. Handheld-book drop is tracked in [#85](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues/85), not portal-frame logic. +Legacy's travel path creates no automatic Return Link. Rivencraft now defaults to `DISABLED` in new server configs while retaining `ON_CREATION` and `ALWAYS` as opt-ins. Existing saved server configs keep their explicit value. Issue #85 adds the active-book drop only to hand-held Linking and Descriptive Book travel. On a successful link, the exact active stack becomes a dedicated `DroppedBookEntity` at the departure point, rendered with the same open `BookModel` and textures as the book stand; a rejected link restores it to the original hand. Stand and portal use leave their books in place. The entity has unlimited vanilla lifetime and loses one of its five item-health points every 10,000 loaded ticks, plus one per wet tick, matching Legacy's slow/wet decay. Unlike Legacy's custom entity, the modern book is picked up by ordinary contact, not sneaking with an empty hand, and remains subject to ordinary item hazards. Following is not yet a Rivencraft modifier, so no Following exception is exposed or silently simulated. Both hands can activate a book, whereas Legacy's explicit stack removal tested only its selected main-hand slot. ## Crafting, sounds, and progression diff --git a/gradle.properties b/gradle.properties index cc1c5a0..3acf11e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,7 +36,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. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.6.6.2 +mod_version=0.6.6.6 immersive_portals_version=6.0.7 terrablender_version=4.1.0.8 serene_seasons_version_id=B6nTjh4b diff --git a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index fb172d4..6e8f1a4 100644 --- a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -54,6 +54,7 @@ public final class Rivencraft { ModBiomeSources.register(modEventBus); ModChunkGenerators.register(modEventBus); ModBlocks.register(modEventBus); + net.dnorberg.rivencraft.registry.ModEntities.register(modEventBus); ModBlockEntities.register(modEventBus); ModItems.register(modEventBus); ModMenus.register(modEventBus); diff --git a/src/main/java/net/dnorberg/rivencraft/book/HandheldBookTravelGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/HandheldBookTravelGameTests.java new file mode 100644 index 0000000..5c3d325 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/HandheldBookTravelGameTests.java @@ -0,0 +1,127 @@ +package net.dnorberg.rivencraft.book; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.entity.DroppedBookEntity; +import net.dnorberg.rivencraft.registry.ModEntities; +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.world.InteractionHand; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.AABB; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; + +@GameTestHolder(Rivencraft.MOD_ID) +@PrefixGameTestTemplate(false) +public final class HandheldBookTravelGameTests { + private HandheldBookTravelGameTests() { + } + + @GameTest(template = "empty") + public static void successfulHandheldTravelLeavesOnlyActiveBookAtOrigin(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get()); + player.setItemInHand(InteractionHand.MAIN_HAND, book); + player.getInventory().setItem(1, new ItemStack(Items.DIAMOND)); + var source = player.position(); + BlockPos destination = helper.absolutePos(new BlockPos(2, 1, 2)); + helper.getLevel().setBlock(destination, Blocks.AIR.defaultBlockState(), 2); + helper.getLevel().setBlock(destination.above(), Blocks.AIR.defaultBlockState(), 2); + LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 0, 0); + + helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.MAIN_HAND, book, target, + new LinkProperties(LinkProperties.INTRA_LINKING), false), LinkingService.LinkResult.SUCCESS, + "A valid intra-dimensional handheld link must succeed"); + helper.assertTrue(player.getMainHandItem().isEmpty(), "Only the active book must leave the hand"); + helper.assertTrue(player.getInventory().getItem(1).is(Items.DIAMOND), + "An unrelated inventory item must remain with the player"); + var drops = helper.getLevel().getEntitiesOfClass(DroppedBookEntity.class, new AABB(source, source).inflate(2.0), + entity -> entity.getItem().is(ModItems.LINKING_BOOK.get())); + helper.assertValueEqual(drops.size(), 1, "Exactly one recoverable book must remain at the departure point"); + helper.assertTrue(drops.getFirst().getItem() == book, + "The model-rendered entity must retain the exact active book stack"); + helper.assertTrue(drops.getFirst().copy() instanceof DroppedBookEntity, + "The pickup-animation copy must keep the custom renderer's entity type"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void rejectedHandheldTravelKeepsBook(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get()); + player.setItemInHand(InteractionHand.OFF_HAND, book); + LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), player.blockPosition()), 0, 0); + + helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.OFF_HAND, book, target, + LinkProperties.NONE, false), LinkingService.LinkResult.INTRA_LINKING_REQUIRED, + "A same-dimension link without Intra Linking must be rejected"); + helper.assertTrue(player.getOffhandItem() == book, "The exact rejected book must be restored to its hand"); + helper.assertTrue(helper.getLevel().getEntitiesOfClass(ItemEntity.class, + new AABB(player.position(), player.position()).inflate(2.0), + entity -> entity.getItem().is(ModItems.LINKING_BOOK.get())).isEmpty(), + "A rejected link must not leave a second copy at the origin"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void disarmDropsOtherInventoryWithoutDuplicatingActiveBook(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get()); + player.setItemInHand(InteractionHand.MAIN_HAND, book); + player.getInventory().setItem(1, new ItemStack(Items.DIAMOND)); + var source = player.position(); + BlockPos destination = helper.absolutePos(new BlockPos(2, 1, 2)); + helper.getLevel().setBlock(destination, Blocks.AIR.defaultBlockState(), 2); + helper.getLevel().setBlock(destination.above(), Blocks.AIR.defaultBlockState(), 2); + LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 0, 0); + + helper.assertValueEqual(LinkingService.travelHandheld(player, InteractionHand.MAIN_HAND, book, target, + new LinkProperties(LinkProperties.INTRA_LINKING | LinkProperties.DISARM), false), + LinkingService.LinkResult.SUCCESS, "Disarm must not block handheld travel"); + var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(source, source).inflate(2.0)); + helper.assertValueEqual(drops.stream().filter(entity -> entity instanceof DroppedBookEntity).count(), + 1L, "Disarm must not create a second copy of the active book"); + helper.assertValueEqual(drops.stream().filter(entity -> entity.getItem().is(Items.DIAMOND)).count(), + 1L, "Disarm must still drop unrelated inventory items"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void originBookUsesSlowDecayWithoutVanillaDespawn(GameTestHelper helper) { + DroppedBookEntity book = new DroppedBookEntity(helper.getLevel(), + net.minecraft.world.phys.Vec3.ZERO, new ItemStack(ModItems.LINKING_BOOK.get())); + CompoundTag seeded = new CompoundTag(); + book.saveWithoutId(seeded); + seeded.putInt("BookDecayTicks", 9_999); + book.readAdditionalSaveData(seeded); + book.tick(); + CompoundTag saved = new CompoundTag(); + book.saveWithoutId(saved); + helper.assertValueEqual(saved.getShort("Age"), (short)-32_768, + "The dropped book must not expire after vanilla's five minutes"); + helper.assertValueEqual(saved.getShort("Health"), (short)4, + "A dropped book must lose one item-health point after 10,000 loaded ticks"); + helper.assertValueEqual(saved.getInt("BookDecayTicks"), 0, + "Slow decay must reset its interval and persist across a restart"); + DroppedBookEntity restored = new DroppedBookEntity(ModEntities.DROPPED_BOOK.get(), helper.getLevel()); + restored.readAdditionalSaveData(saved); + CompoundTag resaved = new CompoundTag(); + restored.saveWithoutId(resaved); + helper.assertTrue(restored.getItem().is(ModItems.LINKING_BOOK.get()), + "The custom book entity must retain its book on reload"); + helper.assertValueEqual(resaved.getShort("Age"), (short)-32_768, + "The custom book must remain non-despawning after reload"); + helper.assertValueEqual(resaved.getShort("Health"), (short)4, + "Decay damage must persist after reload"); + helper.assertTrue(restored.copy().getItem().is(ModItems.LINKING_BOOK.get()), + "A pickup-animation copy must retain the book data after reload"); + helper.succeed(); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java index 719096e..0dc302c 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java @@ -5,10 +5,13 @@ import java.util.Optional; import java.util.stream.IntStream; import java.util.stream.Stream; import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders; +import net.dnorberg.rivencraft.entity.DroppedBookEntity; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; import net.minecraft.tags.BlockTags; import net.minecraft.world.level.border.WorldBorder; import net.minecraft.world.level.levelgen.Heightmap; @@ -32,6 +35,36 @@ public final class LinkingService { return travel(player, target, false, properties, false); } + /** Removes only the active handheld book after a successful link, leaving it at the departure point. */ + public static LinkResult travelHandheld(ServerPlayer player, InteractionHand hand, ItemStack activeBook, + LinkTarget target, LinkProperties properties, boolean requiresFooting) { + if (player.getItemInHand(hand) != activeBook || activeBook.isEmpty()) { + return LinkResult.NO_SAFE_DESTINATION; + } + + ServerLevel sourceLevel = player.serverLevel(); + Vec3 sourcePosition = player.position(); + player.setItemInHand(hand, ItemStack.EMPTY); + boolean dropped = false; + try { + LinkResult result = requiresFooting + ? travelWithFooting(player, target, properties) + : travel(player, target, properties); + if (result != LinkResult.SUCCESS) return result; + + DroppedBookEntity book = new DroppedBookEntity(sourceLevel, sourcePosition, activeBook); + dropped = sourceLevel.addFreshEntity(book); + return result; + } finally { + // Failed validation or a rejected entity spawn must never eat the book. + if (!dropped) { + if (player.getItemInHand(hand).isEmpty()) player.setItemInHand(hand, activeBook); + else if (!player.getInventory().add(activeBook)) player.drop(activeBook, false); + } + player.containerMenu.broadcastChanges(); + } + } + /** Portal travel uses the vanilla transfer if its destination is not yet loaded. */ public static LinkResult travelPortal(ServerPlayer player, LinkTarget target, LinkProperties properties) { return travel(player, target, false, properties, true); diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java index 62b8a5e..87355ea 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java @@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities; import net.dnorberg.rivencraft.registry.ModBlocks; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.registry.ModEntities; import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.client.renderer.ItemBlockRenderTypes; import net.minecraft.client.renderer.RenderType; @@ -34,6 +35,7 @@ public final class ClientEvents { @SubscribeEvent public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { + event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new); diff --git a/src/main/java/net/dnorberg/rivencraft/client/DroppedBookRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/DroppedBookRenderer.java new file mode 100644 index 0000000..2a1a2dd --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/DroppedBookRenderer.java @@ -0,0 +1,51 @@ +package net.dnorberg.rivencraft.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.dnorberg.rivencraft.entity.DroppedBookEntity; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.client.model.BookModel; +import net.minecraft.client.model.geom.ModelLayers; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.ResourceLocation; + +/** Renders the same open book model and textures as Rivencraft's book stands. */ +public final class DroppedBookRenderer extends EntityRenderer { + private static final ResourceLocation LINKING_TEXTURE = ResourceLocation.fromNamespaceAndPath( + "rivencraft", "textures/entity/linking_book.png"); + private static final ResourceLocation DESCRIPTIVE_TEXTURE = ResourceLocation.fromNamespaceAndPath( + "rivencraft", "textures/entity/descriptive_book.png"); + private final BookModel model; + + public DroppedBookRenderer(EntityRendererProvider.Context context) { + super(context); + model = new BookModel(context.bakeLayer(ModelLayers.BOOK)); + shadowRadius = 0.25F; + } + + @Override + public void render(DroppedBookEntity entity, float yaw, float partialTick, PoseStack poseStack, + MultiBufferSource buffers, int packedLight) { + poseStack.pushPose(); + poseStack.translate(0.0F, 0.0625F, 0.0F); + poseStack.mulPose(Axis.YP.rotationDegrees(yaw + 90.0F)); + // The opened model's covers rotate toward the YZ plane. As in Legacy's + // dropped-book renderer, a Z-axis quarter-turn lays the pages skyward. + poseStack.mulPose(Axis.ZP.rotationDegrees(90.0F)); + poseStack.scale(0.8F, 0.8F, 0.8F); + model.setupAnim(0.0F, 0.0F, 0.0F, 1.2F); + VertexConsumer vertices = buffers.getBuffer(RenderType.entitySolid(getTextureLocation(entity))); + model.render(poseStack, vertices, packedLight, net.minecraft.client.renderer.texture.OverlayTexture.NO_OVERLAY, -1); + poseStack.popPose(); + super.render(entity, yaw, partialTick, poseStack, buffers, packedLight); + } + + @Override + public ResourceLocation getTextureLocation(DroppedBookEntity entity) { + return entity.getItem().is(ModItems.DESCRIPTIVE_BOOK.get()) ? DESCRIPTIVE_TEXTURE : LINKING_TEXTURE; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/entity/DroppedBookEntity.java b/src/main/java/net/dnorberg/rivencraft/entity/DroppedBookEntity.java new file mode 100644 index 0000000..fc987a0 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/entity/DroppedBookEntity.java @@ -0,0 +1,62 @@ +package net.dnorberg.rivencraft.entity; + +import net.dnorberg.rivencraft.registry.ModEntities; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A recoverable, model-rendered book left behind by handheld linking. */ +public final class DroppedBookEntity extends ItemEntity { + private static final String DECAY_TICKS_TAG = "BookDecayTicks"; + private static final int SLOW_DECAY_INTERVAL = 10_000; + private int decayTicks; + + public DroppedBookEntity(EntityType type, Level level) { + super(type, level); + } + + public DroppedBookEntity(Level level, Vec3 position, ItemStack stack) { + this(ModEntities.DROPPED_BOOK.get(), level); + setPos(position.x, position.y + 0.25, position.z); + setDeltaMovement(Vec3.ZERO); + setItem(stack); + setUnlimitedLifetime(); + setPickUpDelay(20); + } + + /** Vanilla's pickup particle calls ItemEntity.copy() and renders the returned entity. */ + @Override + public DroppedBookEntity copy() { + DroppedBookEntity copy = new DroppedBookEntity(ModEntities.DROPPED_BOOK.get(), level()); + copy.copyPosition(this); + copy.setItem(getItem().copy()); + copy.setUnlimitedLifetime(); + return copy; + } + + @Override + public void tick() { + super.tick(); + if (level().isClientSide || isRemoved()) return; + if (++decayTicks >= SLOW_DECAY_INTERVAL) { + decayTicks = 0; + hurt(damageSources().starve(), 1.0F); + } + if (!isRemoved() && isInWaterOrRain()) hurt(damageSources().drown(), 1.0F); + } + + @Override + public void addAdditionalSaveData(CompoundTag tag) { + super.addAdditionalSaveData(tag); + tag.putInt(DECAY_TICKS_TAG, decayTicks); + } + + @Override + public void readAdditionalSaveData(CompoundTag tag) { + super.readAdditionalSaveData(tag); + decayTicks = Math.max(0, Math.min(tag.getInt(DECAY_TICKS_TAG), SLOW_DECAY_INTERVAL - 1)); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java index 9273fef..0c41671 100644 --- a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java +++ b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java @@ -18,6 +18,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; @@ -41,14 +42,18 @@ public final class DescriptiveBookItem extends Item { } public static boolean useWrittenBook(ServerPlayer player, ItemStack stack) { - return useWrittenBook(player, stack, false); + return useWrittenBook(player, stack, false, null); + } + + public static boolean useHandheldBook(ServerPlayer player, InteractionHand hand) { + return useWrittenBook(player, player.getItemInHand(hand), false, hand); } public static boolean usePortalBook(ServerPlayer player, ItemStack stack) { - return useWrittenBook(player, stack, true); + return useWrittenBook(player, stack, true, null); } - private static boolean useWrittenBook(ServerPlayer player, ItemStack stack, boolean portal) { + private static boolean useWrittenBook(ServerPlayer player, ItemStack stack, boolean portal, InteractionHand hand) { AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get()); if (age == null) { player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unwritten"), true); @@ -68,7 +73,8 @@ public final class DescriptiveBookItem extends Item { .forDescriptiveBook(); LinkingService.LinkResult result = portal ? LinkingService.travelDescriptivePortal(player, ageTarget, properties) - : LinkingService.travelWithFooting(player, ageTarget, properties); + : hand == null ? LinkingService.travelWithFooting(player, ageTarget, properties) + : LinkingService.travelHandheld(player, hand, stack, ageTarget, properties, true); if (result != LinkingService.LinkResult.SUCCESS) { player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unsafe"), true); return false; diff --git a/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java index f408ee9..5062b88 100644 --- a/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java +++ b/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java @@ -43,14 +43,19 @@ public final class LinkingBookItem extends Item { } public static InteractionResultHolder useBoundBook(ServerPlayer player, ItemStack stack) { - return useBoundBook(player, stack, false); + return useBoundBook(player, stack, false, null); + } + + public static InteractionResultHolder useHandheldBook(ServerPlayer player, InteractionHand hand) { + return useBoundBook(player, player.getItemInHand(hand), false, hand); } public static InteractionResultHolder usePortalBook(ServerPlayer player, ItemStack stack) { - return useBoundBook(player, stack, true); + return useBoundBook(player, stack, true, null); } - private static InteractionResultHolder useBoundBook(ServerPlayer player, ItemStack stack, boolean portal) { + private static InteractionResultHolder useBoundBook(ServerPlayer player, ItemStack stack, boolean portal, + InteractionHand hand) { LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get()); if (target == null) { player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.unbound"), true); @@ -62,7 +67,8 @@ public final class LinkingBookItem extends Item { LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE); LinkingService.LinkResult result = portal ? LinkingService.travelPortal(player, target, properties) - : LinkingService.travel(player, target, properties); + : hand == null ? LinkingService.travel(player, target, properties) + : LinkingService.travelHandheld(player, hand, stack, target, properties, false); if (result == LinkingService.LinkResult.SUCCESS) { LinkVisualEffects.play(sourceLevel, sourcePosition, player, properties, portal); ProgressionAdvancements.awardAgeTravel(player); diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 7fc845c..98cffa8 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -211,7 +211,7 @@ public final class ModNetworking { ItemStack stack = player.getItemInHand(hand(payload.offhand())); if (stack.is(ModItems.LINKING_BOOK.get()) && stack.has(ModDataComponents.LINK_TARGET.get())) { - LinkingBookItem.useBoundBook(player, stack); + LinkingBookItem.useHandheldBook(player, hand(payload.offhand())); } } @@ -245,7 +245,8 @@ public final class ModNetworking { } if (stack.is(ModItems.DESCRIPTIVE_BOOK.get())) { - DescriptiveBookItem.useWrittenBook(player, stack); + if (payload.fromStand()) DescriptiveBookItem.useWrittenBook(player, stack); + else DescriptiveBookItem.useHandheldBook(player, hand(payload.offhand())); } } diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModEntities.java b/src/main/java/net/dnorberg/rivencraft/registry/ModEntities.java new file mode 100644 index 0000000..7889c66 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModEntities.java @@ -0,0 +1,30 @@ +package net.dnorberg.rivencraft.registry; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.entity.DroppedBookEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +public final class ModEntities { + private static final DeferredRegister> ENTITIES = + DeferredRegister.create(Registries.ENTITY_TYPE, Rivencraft.MOD_ID); + + public static final DeferredHolder, EntityType> DROPPED_BOOK = + ENTITIES.register("dropped_book", () -> EntityType.Builder + .of(DroppedBookEntity::new, MobCategory.MISC) + .sized(0.65F, 0.4F) + .clientTrackingRange(10) + .updateInterval(20) + .build(Rivencraft.MOD_ID + ":dropped_book")); + + private ModEntities() { + } + + public static void register(IEventBus bus) { + ENTITIES.register(bus); + } +}