Compare commits

...
Author SHA1 Message Date
dnorberg 56005ac512 Port crystal growth, pages, ink vials and link crafting to 26.3
Build / build (push) Canceled after 0s
2026-10-29 15:07:46 -04:00
dnorberg 9ff931afa9 Add 26.3 descriptive book binding and travel
Build / build (push) Canceled after 0s
2026-10-29 14:32:50 -04:00
37 changed files with 780 additions and 25 deletions

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# Rivencraft 1.0.1.3-alpha.1 — Minecraft 26.3
Experimental **NeoForge 26.3** port preview. This is separate from the
supported Minecraft 1.21.1 release and does **not** have feature parity. Use
a disposable test world or make a backup first. Do not migrate a 1.21.1
Rivencraft world to this alpha.
## Changes since 1.0.1.2-alpha.1
- An unbound Descriptive Book now opens a naming screen. Binding it creates a
distinct prototype Age with a random seed; duplicate names receive a
numbered suffix.
- A bound Descriptive Book opens a book screen whose destination panel can be
clicked to enter its Age. The server validates the held book and waits for
the client's Age catalog acknowledgement before travel.
- Retired Age IDs are excluded when assigning a new book's Age name, so an
archived world cannot be silently reused.
## Still experimental
These Ages use prototype vanilla terrain. Page-authored generation, crafting
and workstations, most items and blocks, portals, Link modifiers, instability,
live destination previews, and optional mod integrations are not ported. The
book panel is a placeholder, not a rendered destination view. The
[port status](MINECRAFT_26_3_PORT.md) and [open port issues](https://git.paddedcell.org/dnorberg/rivencraft/issues)
track the remaining work. The stable 1.21.1 release remains available
separately.
## Verification
- Clean 26.3 Gradle build passed.
- Disposable dedicated-server smoke test passed: two distinct Age seeds and
their terrain persisted across restart.
- The player verified Descriptive Book binding and travel in game on
2026-10-29.
These checks are **not** the full port acceptance matrix or a stability claim.
+45
View File
@@ -374,6 +374,51 @@ prototype vanilla terrain, normal authoring and most content are absent, and
worlds should be disposable or backed up. The 1.21.1 stable release remains worlds should be disposable or backed up. The 1.21.1 stable release remains
independent; issues #96–#104 stay open until the full port and validation pass. independent; issues #96–#104 stay open until the full port and validation pass.
The next development slice, `1.0.1.3-alpha.1`, adds a basic Descriptive Book
screen and server-validated binding. Naming an unbound book creates a unique
prototype Age with a random seed; clicking the bound book's placeholder panel
uses the acknowledged-catalog travel gate. This is not authored Page binding
or a live destination preview. The player confirmed the Descriptive Book
binding and travel interaction in game on 2026-10-29.
The following `1.0.1.4-alpha.1` development iteration registers a small
Rivencraft creative tab for the two Books, Crystal Block, and Crystal Shard.
It builds and passes the dedicated-server Age smoke test. The player confirmed
the tab and four entries in game on 2026-10-29. No Page categories or
unported items are advertised in the 26.3 tab.
The next local `1.0.1.5-alpha.1` iteration selects Legacy's unwritten
Linking Book sprite until a target is bound, then the bound sprite. It also
restores the four-Crystal-Shard crafting recipe for a Crystal Block. Both
changes passed the player's in-game visual/crafting check on 2026-10-29.
The local `1.0.1.6-alpha.1` iteration ports Budding Crystal and the four
blue crystal growth stages, reusing the 1.21.1 sprites and vanilla amethyst
growth/harvest rules. Crystal clusters yield Shards, but natural crystal
formation generation remains unported. Test growth with a Creative or
command-placed Budding Crystal; the player passed the in-game check.
The local `1.0.1.7-alpha.1` iteration adds one persistent Page item with
unwritten and Link Panel variants. It reuses the existing Page/Link Panel
artwork and exposes only the Link Panel variant in its own creative tab,
matching the 1.21.1 separation of Link Pages from symbol pages. Blank Pages
remain command-only bases; the Ink Mixer, Book Binder and symbol writing are
not yet ported. The player passed the in-game visual and persistence check.
The local `1.0.1.8-alpha.1` iteration ports the Ink Vial item, its animated
sprite, and the black-dye/glass-bottle/water-bucket crafting recipe. Using
it with paper in the other hand prepares a Link Panel at the current 1.21.1
default cost of 2 ink from a 16-use vial. Ink remaining persists per vial;
an empty vial yields a glass bottle. The Ink Mixer, configurable ink cost,
and Book Binder remain unported. The player passed the in-game craft/use check.
The local `1.0.1.9-alpha.1` iteration ports the special shapeless leather +
Link Panel recipe for an unwritten Linking Book. The 26.3 recipe serializer
rejects blank Pages and extra ingredients and preserves a panel's custom name
without carrying its Page identity onto the Book. No Link Panel modifiers are
ported yet. The player passed the in-game crafting check on 2026-10-29. The
full Book Binder GUI and Age authoring remain the next larger port step.
## Verification of this first slice ## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+23 -2
View File
@@ -3,12 +3,33 @@
This is an **experimental alpha port** for issues #97–#104, not gameplay parity This is an **experimental alpha port** for issues #97–#104, not gameplay parity
with the Minecraft 1.21.1 release. It registers basic items and a runtime Age with the Minecraft 1.21.1 release. It registers basic items and a runtime Age
prototype with persisted seeds. The operator-only `rivencraft_port age` prototype with persisted seeds. The operator-only `rivencraft_port age`
commands create and inspect separate vanilla-terrain levels. Descriptive Book commands create and inspect separate vanilla-terrain levels. The current
travel and named Linking Book binding/travel work as limited probes. Authored development slice can name an unbound Descriptive Book to create a prototype
Age, then enter it from the book GUI; the player verified this interaction in game.
Named Linking Book binding/travel works as a limited probe. Authored
generation, most content, recipes, workstations, portal systems, modifiers and generation, most content, recipes, workstations, portal systems, modifiers and
the destination preview renderer remain unported. Use a test world and keep a the destination preview renderer remain unported. Use a test world and keep a
backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves. backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
The creative tab contains the two Books, Crystal Block and Crystal Shard;
the player verified these in game. Unbound Linking Books use the Legacy
sprite, and four Crystal Shards craft into a Crystal Block; both were also
verified in game. The current local iteration adds Budding Crystal and all
four blue crystal growth stages with vanilla-style harvesting. Natural crystal
formation generation is not yet ported; use Creative or commands to obtain a
Budding Crystal for this test. The player verified the growth chain in game.
The next local slice adds the Link Panel Page as a separate creative entry,
using the existing item texture. Blank Pages remain unavailable in Creative;
the player verified the Link Panel's appearance in game. Ink Vials now use
the existing crafting recipe and animated sprite. With paper in the other
hand, an Ink Vial prepares a Link Panel for two of its sixteen ink uses, as
in the current 1.21.1 default. The Book Binder and symbol-writing systems
are still unported. The player verified the Ink Vial interaction in game.
A prepared Link Panel can now be combined shapelessly with leather to craft
an unwritten Linking Book, as in 1.21.1. This custom recipe rejects a blank
Page and extra ingredients. The Book Binder remains the next larger step for
assembling Descriptive Books from authored pages.
From the repository root: From the repository root:
```sh ```sh
+28
View File
@@ -63,10 +63,17 @@ tasks.named('processResources', ProcessResources).configure {
// Reuse the existing sprite and base model without copying binary assets. // Reuse the existing sprite and base model without copying binary assets.
from('../../src/main/resources') { from('../../src/main/resources') {
include 'assets/rivencraft/textures/item/crystal_shard.png' include 'assets/rivencraft/textures/item/crystal_shard.png'
include 'assets/rivencraft/textures/item/page.png'
include 'assets/rivencraft/textures/item/page_link_panel.png'
include 'assets/rivencraft/models/item/page_link_panel.json'
include 'assets/rivencraft/textures/item/ink_vial.png'
include 'assets/rivencraft/textures/item/ink_vial.png.mcmeta'
include 'assets/rivencraft/models/item/ink_vial.json'
include 'assets/rivencraft/models/item/crystal_shard.json' include 'assets/rivencraft/models/item/crystal_shard.json'
include 'assets/rivencraft/textures/item/descriptive_book.png' include 'assets/rivencraft/textures/item/descriptive_book.png'
include 'assets/rivencraft/models/item/descriptive_book.json' include 'assets/rivencraft/models/item/descriptive_book.json'
include 'assets/rivencraft/textures/item/linking_book.png' include 'assets/rivencraft/textures/item/linking_book.png'
include 'assets/rivencraft/textures/item/linking_book_unwritten.png'
include 'assets/rivencraft/models/item/linking_book_bound.json' include 'assets/rivencraft/models/item/linking_book_bound.json'
include 'assets/rivencraft/textures/gui/bookui_cover.png' include 'assets/rivencraft/textures/gui/bookui_cover.png'
include 'assets/rivencraft/textures/gui/notebook_pages.png' include 'assets/rivencraft/textures/gui/notebook_pages.png'
@@ -74,7 +81,28 @@ tasks.named('processResources', ProcessResources).configure {
include 'assets/rivencraft/textures/block/crystal_block.png' include 'assets/rivencraft/textures/block/crystal_block.png'
include 'assets/rivencraft/models/block/crystal_block.json' include 'assets/rivencraft/models/block/crystal_block.json'
include 'assets/rivencraft/blockstates/crystal_block.json' include 'assets/rivencraft/blockstates/crystal_block.json'
include 'assets/rivencraft/blockstates/budding_crystal.json'
include 'assets/rivencraft/blockstates/small_crystal_bud.json'
include 'assets/rivencraft/blockstates/medium_crystal_bud.json'
include 'assets/rivencraft/blockstates/large_crystal_bud.json'
include 'assets/rivencraft/blockstates/crystal_cluster.json'
include 'assets/rivencraft/models/block/budding_crystal.json'
include 'assets/rivencraft/models/block/small_crystal_bud.json'
include 'assets/rivencraft/models/block/medium_crystal_bud.json'
include 'assets/rivencraft/models/block/large_crystal_bud.json'
include 'assets/rivencraft/models/block/crystal_cluster.json'
include 'assets/rivencraft/models/item/budding_crystal.json'
include 'assets/rivencraft/models/item/small_crystal_bud.json'
include 'assets/rivencraft/models/item/medium_crystal_bud.json'
include 'assets/rivencraft/models/item/large_crystal_bud.json'
include 'assets/rivencraft/models/item/crystal_cluster.json'
include 'assets/rivencraft/textures/block/small_crystal_bud.png'
include 'assets/rivencraft/textures/block/medium_crystal_bud.png'
include 'assets/rivencraft/textures/block/large_crystal_bud.png'
include 'assets/rivencraft/textures/block/crystal_cluster.png'
include 'data/rivencraft/loot_table/blocks/crystal_block.json' include 'data/rivencraft/loot_table/blocks/crystal_block.json'
include 'data/minecraft/tags/block/mineable/pickaxe.json'
include 'data/minecraft/tags/item/cluster_max_harvestables.json'
} }
} }
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft mod_id=rivencraft
mod_name=Rivencraft mod_name=Rivencraft
mod_license=LGPL-3.0-or-later mod_license=LGPL-3.0-or-later
mod_version=1.0.1.2-alpha.1 mod_version=1.0.1.9-alpha.1
@@ -1,21 +1,31 @@
package net.dnorberg.rivencraft; package net.dnorberg.rivencraft;
import com.mojang.logging.LogUtils; import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import net.dnorberg.rivencraft.age.RuntimeAgeBackend; import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
import net.dnorberg.rivencraft.age.PortAgeCommands; import net.dnorberg.rivencraft.age.PortAgeCommands;
import net.dnorberg.rivencraft.age.AgeCatalogSync; import net.dnorberg.rivencraft.age.AgeCatalogSync;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkBookTarget; import net.dnorberg.rivencraft.item.LinkBookTarget;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.network.PortBookNetworking; import net.dnorberg.rivencraft.network.PortBookNetworking;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.registries.DeferredBlock; import net.neoforged.neoforge.registries.DeferredBlock;
@@ -32,6 +42,8 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger(); public static final Logger LOGGER = LogUtils.getLogger();
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID); private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID); private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
private static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
DeferredRegister.create(Registries.RECIPE_SERIALIZER, MOD_ID);
private static final DeferredRegister.DataComponents COMPONENTS = private static final DeferredRegister.DataComponents COMPONENTS =
DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MOD_ID); DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MOD_ID);
@@ -41,20 +53,54 @@ public final class Rivencraft {
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType( public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC) "link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
.networkSynchronized(LinkBookTarget.STREAM_CODEC)); .networkSynchronized(LinkBookTarget.STREAM_CODEC));
/** A present component distinguishes Link Panels from unwritten base Pages. */
public static final Supplier<DataComponentType<Boolean>> LINK_PANEL_PAGE = COMPONENTS.registerComponentType(
"link_panel_page", builder -> builder.persistent(Codec.BOOL)
.networkSynchronized(ByteBufCodecs.BOOL));
public static final Supplier<DataComponentType<Integer>> INK_REMAINING = COMPONENTS.registerComponentType(
"ink_remaining", builder -> builder.persistent(Codec.INT)
.networkSynchronized(ByteBufCodecs.VAR_INT));
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block", public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK)); Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK); public static final DeferredItem<BlockItem> CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK);
public static final DeferredBlock<BuddingCrystalBlock> BUDDING_CRYSTAL = BLOCKS.registerBlock(
"budding_crystal", BuddingCrystalBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.BUDDING_AMETHYST).mapColor(MapColor.COLOR_BLUE));
public static final DeferredItem<BlockItem> BUDDING_CRYSTAL_ITEM = ITEMS.registerSimpleBlockItem(BUDDING_CRYSTAL);
public static final DeferredBlock<AmethystClusterBlock> SMALL_CRYSTAL_BUD = BLOCKS.registerBlock(
"small_crystal_bud", properties -> new AmethystClusterBlock(3.0F, 4.0F, properties),
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.SMALL_AMETHYST_BUD));
public static final DeferredItem<BlockItem> SMALL_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(SMALL_CRYSTAL_BUD);
public static final DeferredBlock<AmethystClusterBlock> MEDIUM_CRYSTAL_BUD = BLOCKS.registerBlock(
"medium_crystal_bud", properties -> new AmethystClusterBlock(4.0F, 3.0F, properties),
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.MEDIUM_AMETHYST_BUD));
public static final DeferredItem<BlockItem> MEDIUM_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(MEDIUM_CRYSTAL_BUD);
public static final DeferredBlock<AmethystClusterBlock> LARGE_CRYSTAL_BUD = BLOCKS.registerBlock(
"large_crystal_bud", properties -> new AmethystClusterBlock(5.0F, 3.0F, properties),
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.LARGE_AMETHYST_BUD));
public static final DeferredItem<BlockItem> LARGE_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(LARGE_CRYSTAL_BUD);
public static final DeferredBlock<AmethystClusterBlock> CRYSTAL_CLUSTER = BLOCKS.registerBlock(
"crystal_cluster", properties -> new AmethystClusterBlock(7.0F, 3.0F, properties),
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_CLUSTER));
public static final DeferredItem<BlockItem> CRYSTAL_CLUSTER_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_CLUSTER);
public static final DeferredItem<Item> CRYSTAL_SHARD = ITEMS.registerSimpleItem("crystal_shard"); public static final DeferredItem<Item> CRYSTAL_SHARD = ITEMS.registerSimpleItem("crystal_shard");
public static final DeferredItem<PageItem> PAGE = ITEMS.registerItem("page", PageItem::new);
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
"ink_vial", InkVialItem::new, properties -> properties.stacksTo(16));
public static final DeferredItem<DescriptiveBookItem> DESCRIPTIVE_BOOK = ITEMS.registerItem( public static final DeferredItem<DescriptiveBookItem> DESCRIPTIVE_BOOK = ITEMS.registerItem(
"descriptive_book", DescriptiveBookItem::new, properties -> properties.stacksTo(1)); "descriptive_book", DescriptiveBookItem::new, properties -> properties.stacksTo(1));
public static final DeferredItem<LinkingBookItem> LINKING_BOOK = ITEMS.registerItem( public static final DeferredItem<LinkingBookItem> LINKING_BOOK = ITEMS.registerItem(
"linking_book", LinkingBookItem::new, properties -> properties.stacksTo(1)); "linking_book", LinkingBookItem::new, properties -> properties.stacksTo(1));
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
public Rivencraft(IEventBus modEventBus) { public Rivencraft(IEventBus modEventBus) {
BLOCKS.register(modEventBus); BLOCKS.register(modEventBus);
ITEMS.register(modEventBus); ITEMS.register(modEventBus);
COMPONENTS.register(modEventBus); COMPONENTS.register(modEventBus);
RECIPE_SERIALIZERS.register(modEventBus);
ModCreativeTabs.register(modEventBus);
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad); NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register); NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin); NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
@@ -153,6 +153,13 @@ public final class RuntimeAgeBackend {
return index(server).entries(); return index(server).entries();
} }
/** Includes retired IDs, which must never be silently reused by a new book. */
public static boolean isNameTaken(MinecraftServer server, Identifier id) {
checkServerThread(server);
ageName(id);
return index(server).contains(id);
}
private static AgeLevelIndex index(MinecraftServer server) { private static AgeLevelIndex index(MinecraftServer server) {
return server.overworld().getDataStorage().computeIfAbsent(AgeLevelIndex.TYPE); return server.overworld().getDataStorage().computeIfAbsent(AgeLevelIndex.TYPE);
} }
@@ -0,0 +1,45 @@
package net.dnorberg.rivencraft.block;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.AmethystBlock;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.BuddingAmethystBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Fluids;
/** Vanilla amethyst growth, using Rivencraft's blue crystal stages. */
public final class BuddingCrystalBlock extends AmethystBlock {
public BuddingCrystalBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
if (random.nextInt(5) != 0) return;
Direction direction = Direction.values()[random.nextInt(Direction.values().length)];
BlockPos target = pos.relative(direction);
BlockState existing = level.getBlockState(target);
Block next = nextStage(existing, direction);
if (next == null) return;
level.setBlockAndUpdate(target, next.defaultBlockState()
.setValue(AmethystClusterBlock.FACING, direction)
.setValue(AmethystClusterBlock.WATERLOGGED,
existing.getFluidState().getType() == Fluids.WATER));
}
public static Block nextStage(BlockState existing, Direction direction) {
if (BuddingAmethystBlock.canClusterGrowAtState(existing)) return Rivencraft.SMALL_CRYSTAL_BUD.get();
if (!existing.hasProperty(AmethystClusterBlock.FACING)
|| existing.getValue(AmethystClusterBlock.FACING) != direction) return null;
if (existing.is(Rivencraft.SMALL_CRYSTAL_BUD)) return Rivencraft.MEDIUM_CRYSTAL_BUD.get();
if (existing.is(Rivencraft.MEDIUM_CRYSTAL_BUD)) return Rivencraft.LARGE_CRYSTAL_BUD.get();
if (existing.is(Rivencraft.LARGE_CRYSTAL_BUD)) return Rivencraft.CRYSTAL_CLUSTER.get();
return null;
}
}
@@ -16,11 +16,14 @@ public final class PortBookInteractionHandler {
@SubscribeEvent @SubscribeEvent
public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) { public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
if (!event.getEntity().level().isClientSide() if (!event.getEntity().level().isClientSide()) return;
|| !event.getItemStack().is(Rivencraft.LINKING_BOOK.get())) return; boolean linking = event.getItemStack().is(Rivencraft.LINKING_BOOK.get());
boolean descriptive = event.getItemStack().is(Rivencraft.DESCRIPTIVE_BOOK.get());
if (!linking && !descriptive) return;
event.setCanceled(true); event.setCanceled(true);
event.setCancellationResult(InteractionResult.SUCCESS); event.setCancellationResult(InteractionResult.SUCCESS);
Minecraft.getInstance().setScreenAndShow(new PortLinkingBookScreen( Minecraft.getInstance().setScreenAndShow(linking
event.getHand(), event.getItemStack().copy())); ? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
: new PortDescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
} }
} }
@@ -0,0 +1,116 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.platform.InputConstants;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.network.BindDescriptiveBookPayload;
import net.dnorberg.rivencraft.network.PortBookNetworking;
import net.dnorberg.rivencraft.network.UseDescriptiveBookPayload;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
/** Temporary 26.3 descriptive-book interaction; authored pages and live previews remain unported. */
public final class PortDescriptiveBookScreen extends Screen {
private static final int WIDTH = 327;
private static final int HEIGHT = 199;
private static final int TEXT_COLOR = 0xFF3B281B;
private static final Identifier COVER = texture("bookui_cover.png");
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
private final InteractionHand hand;
private final boolean bound;
private EditBox titleInput;
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
super(book.getHoverName());
this.hand = hand;
bound = book.has(Rivencraft.AGE_BOOK_ID.get());
}
@Override
protected void init() {
if (bound) return;
int left = (width - WIDTH) / 2;
int top = (height - HEIGHT) / 2;
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
Component.literal("Age name"));
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
titleInput.setHint(Component.literal("Name this Age"));
addRenderableWidget(titleInput);
setInitialFocus(titleInput);
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
.bounds(left + 40, top + 100, 53, 20).build());
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
.bounds(left + 97, top + 100, 53, 20).build());
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
int left = (width - WIDTH) / 2;
int top = (height - HEIGHT) / 2;
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
if (bound) {
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
graphics.text(font, Component.literal("Age preview unavailable"), left + 181, top + 47,
0xFFE9DFCB, false);
graphics.text(font, Component.literal("Click to enter"), left + 205, top + 65,
0xFFE9DFCB, false);
} else {
graphics.text(font, Component.literal("Bind a prototype Age"), left + 183, top + 39,
TEXT_COLOR, false);
}
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
int left = (width - WIDTH) / 2;
int top = (height - HEIGHT) / 2;
if (event.x() >= left + 173 && event.x() < left + 305
&& event.y() >= top + 20 && event.y() < top + 103) {
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
onClose();
return true;
}
}
return super.mouseClicked(event, doubleClick);
}
@Override
public boolean isPauseScreen() {
return false;
}
private void bind() {
if (titleInput == null) return;
String name = titleInput.getValue().strip();
if (name.isEmpty()) return;
ClientPacketDistributor.sendToServer(new BindDescriptiveBookPayload(hand == InteractionHand.OFF_HAND, name));
onClose();
}
private static Identifier texture(String filename) {
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
}
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
int width, int height, int u, int v) {
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
}
}
@@ -2,10 +2,8 @@ package net.dnorberg.rivencraft.item;
import java.util.function.Consumer; import java.util.function.Consumer;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortAgeTravel;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
@@ -15,7 +13,7 @@ import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay; import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
/** First 26.3 bound-book slice; authoring and the book GUI still need porting. */ /** Descriptive Book shell for the 26.3 prototype Age backend. */
public final class DescriptiveBookItem extends Item { public final class DescriptiveBookItem extends Item {
public DescriptiveBookItem(Properties properties) { public DescriptiveBookItem(Properties properties) {
super(properties); super(properties);
@@ -28,18 +26,8 @@ public final class DescriptiveBookItem extends Item {
@Override @Override
public InteractionResult use(Level level, Player player, InteractionHand hand) { public InteractionResult use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand); // The client opens the GUI. Binding and travel are validated on the
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get()); // server through dedicated packets; item use cannot bypass them.
if (id == null) {
if (player instanceof ServerPlayer serverPlayer) {
serverPlayer.sendOverlayMessage(Component.literal("Unbound Descriptive Book; use the port's Age book probe"));
}
return InteractionResult.SUCCESS;
}
if (player instanceof ServerPlayer serverPlayer) {
String failure = PortAgeTravel.enter(serverPlayer, id);
serverPlayer.sendOverlayMessage(Component.literal(failure == null ? "Entered " + id : failure));
}
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} }
@@ -0,0 +1,76 @@
package net.dnorberg.rivencraft.item;
import java.util.function.Consumer;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Prediction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.Level;
/** A stackable 16-use vial; using it with paper in the other hand prepares a Link Panel. */
public final class InkVialItem extends Item {
public static final int CAPACITY = 16;
// Match the 1.21.1 default; port the configurable cost with the full configuration system.
public static final int LINK_PANEL_COST = 2;
public InkVialItem(Properties properties) {
super(properties);
}
public static int remaining(ItemStack vial) {
return vial.getOrDefault(Rivencraft.INK_REMAINING.get(), CAPACITY);
}
@Override
public InteractionResult use(Level level, Player player, InteractionHand hand) {
InteractionHand paperHand = hand == InteractionHand.MAIN_HAND
? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
ItemStack vial = player.getItemInHand(hand);
ItemStack paper = player.getItemInHand(paperHand);
int ink = remaining(vial);
if (!vial.is(Rivencraft.INK_VIAL.get()) || !paper.is(Items.PAPER)
|| ink < LINK_PANEL_COST || ink > CAPACITY) {
return InteractionResult.PASS;
}
if (level.isClientSide()) return InteractionResult.SUCCESS;
ItemStack panel = PageItem.createLinkPanelPage();
int newInk = ink - LINK_PANEL_COST;
ItemStack spentVial = newInk == 0
? new ItemStack(Items.GLASS_BOTTLE)
: new ItemStack(Rivencraft.INK_VIAL.get());
if (newInk > 0) spentVial.set(Rivencraft.INK_REMAINING.get(), newInk);
if (paper.getCount() == 1) {
player.setItemInHand(paperHand, panel);
} else {
paper.shrink(1);
giveOrDrop(player, panel);
}
if (vial.getCount() == 1) {
player.setItemInHand(hand, spentVial);
} else {
vial.shrink(1);
giveOrDrop(player, spentVial);
}
return InteractionResult.SUCCESS;
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
Consumer<Component> tooltip, TooltipFlag flag) {
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.remaining", remaining(stack), CAPACITY));
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.usage"));
}
private static void giveOrDrop(Player player, ItemStack stack) {
if (!player.getInventory().add(stack)) player.drop(stack, false, Prediction.SERVER_ONLY);
}
}
@@ -0,0 +1,43 @@
package net.dnorberg.rivencraft.item;
import java.util.function.Consumer;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay;
/** One Page item with a persistent Link Panel variant; symbol writing is not ported yet. */
public final class PageItem extends Item {
public PageItem(Properties properties) {
super(properties);
}
public static ItemStack createLinkPanelPage() {
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
stack.set(Rivencraft.LINK_PANEL_PAGE.get(), true);
return stack;
}
public static boolean isLinkPanel(ItemStack stack) {
return stack.is(Rivencraft.PAGE.get()) && stack.getOrDefault(Rivencraft.LINK_PANEL_PAGE.get(), false);
}
@Override
public Component getName(ItemStack stack) {
Component customName = stack.get(DataComponents.CUSTOM_NAME);
if (customName != null) return customName;
return Component.translatable(isLinkPanel(stack)
? "item.rivencraft.page.link_panel" : "item.rivencraft.page.blank");
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
Consumer<Component> tooltip, TooltipFlag flag) {
tooltip.accept(Component.translatable(isLinkPanel(stack)
? "item.rivencraft.page.link_panel.port_status"
: "item.rivencraft.page.blank.port_status"));
}
}
@@ -0,0 +1,22 @@
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.Identifier;
public record BindDescriptiveBookPayload(boolean offhand, String title) implements CustomPacketPayload {
public static final Type<BindDescriptiveBookPayload> TYPE = new Type<>(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_descriptive_book"));
public static final StreamCodec<RegistryFriendlyByteBuf, BindDescriptiveBookPayload> STREAM_CODEC =
StreamCodec.composite(ByteBufCodecs.BOOL, BindDescriptiveBookPayload::offhand,
ByteBufCodecs.stringUtf8(32), BindDescriptiveBookPayload::title,
BindDescriptiveBookPayload::new);
@Override
public Type<BindDescriptiveBookPayload> type() {
return TYPE;
}
}
@@ -1,16 +1,20 @@
package net.dnorberg.rivencraft.network; package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortAgeTravel;
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
import net.dnorberg.rivencraft.item.LinkBookTarget; import net.dnorberg.rivencraft.item.LinkBookTarget;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource; import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import java.util.Locale;
/** Server-authoritative handheld Linking Book binding and travel. */ /** Server-authoritative handheld Linking Book binding and travel. */
public final class PortBookNetworking { public final class PortBookNetworking {
@@ -29,6 +33,14 @@ public final class PortBookNetworking {
(payload, context) -> { (payload, context) -> {
if (context.player() instanceof ServerPlayer player) use(player, payload); if (context.player() instanceof ServerPlayer player) use(player, payload);
}); });
registrar.playToServer(BindDescriptiveBookPayload.TYPE, BindDescriptiveBookPayload.STREAM_CODEC,
(payload, context) -> {
if (context.player() instanceof ServerPlayer player) bindAge(player, payload);
});
registrar.playToServer(UseDescriptiveBookPayload.TYPE, UseDescriptiveBookPayload.STREAM_CODEC,
(payload, context) -> {
if (context.player() instanceof ServerPlayer player) enterAge(player, payload);
});
} }
private static void bind(ServerPlayer player, BindLinkingBookPayload payload) { private static void bind(ServerPlayer player, BindLinkingBookPayload payload) {
@@ -51,6 +63,45 @@ public final class PortBookNetworking {
if (failure != null) player.sendOverlayMessage(Component.literal(failure)); if (failure != null) player.sendOverlayMessage(Component.literal(failure));
} }
private static void bindAge(ServerPlayer player, BindDescriptiveBookPayload payload) {
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get()) || stack.has(Rivencraft.AGE_BOOK_ID.get())) return;
String title = payload.title().strip();
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
String base = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_")
.replaceAll("^[_\\.-]+|[_\\.-]+$", "");
if (base.isEmpty()) base = "age";
var server = player.level().getServer();
String name = base;
int suffix = 2;
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
while (RuntimeAgeBackend.isNameTaken(server, id)) {
name = base + "_" + suffix++;
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
}
try {
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong());
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return;
}
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
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.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title));
player.containerMenu.broadcastChanges();
}
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
if (id == null) return;
String failure = PortAgeTravel.enter(player, id);
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
}
private static InteractionHand hand(boolean offhand) { private static InteractionHand hand(boolean offhand) {
return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
} }
@@ -0,0 +1,21 @@
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.Identifier;
public record UseDescriptiveBookPayload(boolean offhand) implements CustomPacketPayload {
public static final Type<UseDescriptiveBookPayload> TYPE = new Type<>(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_descriptive_book"));
public static final StreamCodec<RegistryFriendlyByteBuf, UseDescriptiveBookPayload> STREAM_CODEC =
StreamCodec.composite(ByteBufCodecs.BOOL, UseDescriptiveBookPayload::offhand,
UseDescriptiveBookPayload::new);
@Override
public Type<UseDescriptiveBookPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,46 @@
package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredRegister;
/** Only items already ported to 26.3 appear here. */
public final class ModCreativeTabs {
private static final DeferredRegister<CreativeModeTab> TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, Rivencraft.MOD_ID);
static {
TABS.register("rivencraft", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.rivencraft"))
.icon(() -> Rivencraft.DESCRIPTIVE_BOOK.get().getDefaultInstance())
.displayItems((parameters, output) -> {
output.accept(Rivencraft.DESCRIPTIVE_BOOK.get());
output.accept(Rivencraft.LINKING_BOOK.get());
output.accept(Rivencraft.CRYSTAL_BLOCK_ITEM.get());
output.accept(Rivencraft.BUDDING_CRYSTAL_ITEM.get());
output.accept(Rivencraft.SMALL_CRYSTAL_BUD_ITEM.get());
output.accept(Rivencraft.MEDIUM_CRYSTAL_BUD_ITEM.get());
output.accept(Rivencraft.LARGE_CRYSTAL_BUD_ITEM.get());
output.accept(Rivencraft.CRYSTAL_CLUSTER_ITEM.get());
output.accept(Rivencraft.CRYSTAL_SHARD.get());
output.accept(Rivencraft.INK_VIAL.get());
})
.build());
TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.rivencraft.pages.link"))
.icon(PageItem::createLinkPanelPage)
.displayItems((parameters, output) -> output.accept(PageItem.createLinkPanelPage()))
.build());
}
private ModCreativeTabs() {
}
public static void register(IEventBus modEventBus) {
TABS.register(modEventBus);
}
}
@@ -0,0 +1,63 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingInput;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.level.Level;
/** Legacy's shapeless leather + Link Panel recipe; never accepts a blank Page. */
public final class LinkingBookRecipe extends CustomRecipe {
private static final LinkingBookRecipe INSTANCE = new LinkingBookRecipe();
public static final MapCodec<LinkingBookRecipe> MAP_CODEC = MapCodec.unit(INSTANCE);
public static final StreamCodec<RegistryFriendlyByteBuf, LinkingBookRecipe> STREAM_CODEC = StreamCodec.unit(INSTANCE);
public static final RecipeSerializer<LinkingBookRecipe> SERIALIZER = new RecipeSerializer<>(MAP_CODEC, STREAM_CODEC);
private LinkingBookRecipe() {
}
private static ItemStack findPanel(CraftingInput input) {
if (input.ingredientCount() != 2) return ItemStack.EMPTY;
ItemStack panel = ItemStack.EMPTY;
boolean leather = false;
for (ItemStack stack : input.items()) {
if (stack.isEmpty()) continue;
if (PageItem.isLinkPanel(stack) && panel.isEmpty()) {
panel = stack;
} else if (stack.is(Items.LEATHER) && !leather) {
leather = true;
} else {
return ItemStack.EMPTY;
}
}
return leather ? panel : ItemStack.EMPTY;
}
@Override
public boolean matches(CraftingInput input, Level level) {
return !findPanel(input).isEmpty();
}
@Override
public ItemStack assemble(CraftingInput input) {
ItemStack panel = findPanel(input);
if (panel.isEmpty()) return ItemStack.EMPTY;
ItemStack book = new ItemStack(Rivencraft.LINKING_BOOK.get());
if (panel.has(DataComponents.CUSTOM_NAME)) {
book.set(DataComponents.CUSTOM_NAME, panel.get(DataComponents.CUSTOM_NAME));
}
return book;
}
@Override
public RecipeSerializer<LinkingBookRecipe> getSerializer() {
return SERIALIZER;
}
}
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/budding_crystal"}}
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/crystal_cluster"}}
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/ink_vial"}}
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/large_crystal_bud"}}
@@ -1,6 +1,15 @@
{ {
"model": { "model": {
"type": "minecraft:model", "type": "minecraft:condition",
"model": "rivencraft:item/linking_book_bound" "property": "minecraft:has_component",
"component": "rivencraft:link_book_target",
"on_true": {
"type": "minecraft:model",
"model": "rivencraft:item/linking_book_bound"
},
"on_false": {
"type": "minecraft:model",
"model": "rivencraft:item/linking_book_unwritten"
}
} }
} }
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/medium_crystal_bud"}}
@@ -0,0 +1,9 @@
{
"model": {
"type": "minecraft:condition",
"property": "minecraft:has_component",
"component": "rivencraft:link_panel_page",
"on_true": {"type": "minecraft:model", "model": "rivencraft:item/page_link_panel"},
"on_false": {"type": "minecraft:model", "model": "rivencraft:item/page"}
}
}
@@ -0,0 +1 @@
{"model":{"type":"minecraft:model","model":"rivencraft:item/small_crystal_bud"}}
@@ -2,5 +2,20 @@
"item.rivencraft.crystal_shard": "Crystal Shard", "item.rivencraft.crystal_shard": "Crystal Shard",
"item.rivencraft.descriptive_book": "Descriptive Book", "item.rivencraft.descriptive_book": "Descriptive Book",
"item.rivencraft.linking_book": "Linking Book", "item.rivencraft.linking_book": "Linking Book",
"block.rivencraft.crystal_block": "Crystal Block" "item.rivencraft.page": "Page",
"item.rivencraft.page.blank": "Blank Page",
"item.rivencraft.page.link_panel": "Link Panel Page",
"item.rivencraft.ink_vial": "Ink Vial",
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand to prepare a Link Panel.",
"item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.",
"item.rivencraft.page.link_panel.port_status": "Prepared for book binding; the Book Binder is not yet ported.",
"block.rivencraft.crystal_block": "Crystal Block",
"block.rivencraft.budding_crystal": "Budding Crystal",
"block.rivencraft.small_crystal_bud": "Small Crystal Bud",
"block.rivencraft.medium_crystal_bud": "Medium Crystal Bud",
"block.rivencraft.large_crystal_bud": "Large Crystal Bud",
"block.rivencraft.crystal_cluster": "Crystal Cluster",
"itemGroup.rivencraft": "Rivencraft",
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages"
} }
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/linking_book_unwritten"
}
}
@@ -0,0 +1 @@
{"parent":"minecraft:item/generated","textures":{"layer0":"rivencraft:item/page"}}
@@ -0,0 +1 @@
{"type":"minecraft:block","random_sequence":"rivencraft:blocks/budding_crystal"}
@@ -0,0 +1,23 @@
{
"type": "minecraft:block",
"pools": [{
"entries": [{
"type": "minecraft:alternatives",
"children": [
{"type": "minecraft:item", "condition": "minecraft:tool/can_silk_touch", "name": "rivencraft:crystal_cluster"},
{"type": "minecraft:alternatives", "children": [
{"type": "minecraft:item", "condition": {"type": "minecraft:match_tool", "predicate": {"items": "#minecraft:cluster_max_harvestables"}}, "modifier": [
{"type": "minecraft:set_count", "count": 4},
{"type": "minecraft:apply_bonus", "enchantment": "minecraft:fortune", "formula": "minecraft:ore_drops"}
], "name": "rivencraft:crystal_shard"},
{"type": "minecraft:item", "modifier": [
{"type": "minecraft:set_count", "count": 2},
{"type": "minecraft:explosion_decay"}
], "name": "rivencraft:crystal_shard"}
]}
]
}],
"rolls": 1
}],
"random_sequence": "rivencraft:blocks/crystal_cluster"
}
@@ -0,0 +1 @@
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:large_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/large_crystal_bud"}
@@ -0,0 +1 @@
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:medium_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/medium_crystal_bud"}
@@ -0,0 +1 @@
{"type":"minecraft:block","pools":[{"condition":"minecraft:tool/can_silk_touch","entries":[{"type":"minecraft:item","name":"rivencraft:small_crystal_bud"}],"rolls":1}],"random_sequence":"rivencraft:blocks/small_crystal_bud"}
@@ -0,0 +1,12 @@
{
"type": "minecraft:crafting_shaped",
"category": "misc",
"pattern": ["SS", "SS"],
"key": {
"S": "rivencraft:crystal_shard"
},
"result": {
"id": "rivencraft:crystal_block",
"count": 1
}
}
@@ -0,0 +1,11 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
"minecraft:black_dye",
"minecraft:black_dye",
"minecraft:glass_bottle",
"minecraft:water_bucket"
],
"result": {"id": "rivencraft:ink_vial", "count": 1}
}
@@ -0,0 +1 @@
{"type":"rivencraft:linking_book"}