diff --git a/gradle.properties b/gradle.properties index 231477f..30550c5 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.8.10 +mod_version=0.6.8.11 immersive_portals_version=6.0.7 terrablender_version=4.1.0.8 serene_seasons_version_id=pHEgQQUE diff --git a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java index a6ccf71..9358aae 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java +++ b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java @@ -36,6 +36,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; @@ -44,10 +45,11 @@ import net.minecraft.world.phys.shapes.VoxelShape; public final class BookReceptacleBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(BookReceptacleBlock::new); public static final DirectionProperty FACING = BlockStateProperties.FACING; + public static final BooleanProperty HAS_BOOK = BlockStateProperties.HAS_BOOK; public BookReceptacleBlock(Properties properties) { super(properties); - registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(HAS_BOOK, false)); } @Override protected MapCodec codec() { return CODEC; } @@ -64,7 +66,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock { }; } @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(FACING); + builder.add(FACING, HAS_BOOK); } @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new BookReceptacleBlockEntity(pos, state); @@ -153,7 +155,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock { if (!state.is(replacement.getBlock()) && level instanceof ServerLevel server) { CrystalPortalNetwork.shutdown(server, pos); if (level.getBlockEntity(pos) instanceof BookReceptacleBlockEntity holder) { - ItemStack book = holder.takeBook(); + ItemStack book = holder.takeBookOnBreak(); if (!book.isEmpty()) level.addFreshEntity(new ItemEntity(level, pos.getX()+0.5, pos.getY()+0.5, pos.getZ()+0.5, book)); } diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/BookReceptacleBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/BookReceptacleBlockEntity.java index b3166f6..8bb0faa 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/entity/BookReceptacleBlockEntity.java +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/BookReceptacleBlockEntity.java @@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.block.entity; import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler; +import net.dnorberg.rivencraft.block.BookReceptacleBlock; import net.dnorberg.rivencraft.registry.ModBlockEntities; +import net.dnorberg.rivencraft.registry.ModBlocks; import net.dnorberg.rivencraft.portal.CrystalPortalNetwork; import net.minecraft.nbt.NbtOps; import net.minecraft.core.BlockPos; @@ -52,18 +54,36 @@ public final class BookReceptacleBlockEntity extends BlockEntity { } public ItemStack takeBook() { + return removeBook(true); + } + + /** The block is already being replaced, so do not write its state back into the world. */ + public ItemStack takeBookOnBreak() { + return removeBook(false); + } + + private ItemStack removeBook(boolean notify) { ItemStack result = book; book = ItemStack.EMPTY; destination = null; skyColor = 0xFFFFFF; - sync(); + if (notify) sync(); + else setChanged(); return result; } private void sync() { setChanged(); - if (level != null && !level.isClientSide) - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2); + if (level instanceof ServerLevel server) { + BlockState state = server.getBlockState(worldPosition); + if (!state.is(ModBlocks.BOOK_RECEPTACLE.get())) return; + boolean hasBook = !book.isEmpty(); + if (state.getValue(BookReceptacleBlock.HAS_BOOK) != hasBook) { + state = state.setValue(BookReceptacleBlock.HAS_BOOK, hasBook); + server.setBlock(worldPosition, state, 2); + } + server.sendBlockUpdated(worldPosition, state, state, 2); + } } @Override @@ -98,6 +118,10 @@ public final class BookReceptacleBlockEntity extends BlockEntity { public static void serverTick(net.minecraft.world.level.Level world, BlockPos pos, BlockState state, BookReceptacleBlockEntity holder) { if (!(world instanceof ServerLevel level)) return; + // Reconcile receptacles saved before HAS_BOOK existed, including an + // occupied receptacle restored from an older pre-release world. + boolean hasBook = !holder.book.isEmpty(); + if (state.getValue(BookReceptacleBlock.HAS_BOOK) != hasBook) holder.sync(); if (holder.book.isEmpty() || (!holder.restoreView && level.getGameTime() % 200 != 0)) return; // Startup maintenance must never turn a saved portal off while its // chunks and block entities are still arriving. Structural edits are diff --git a/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java index ebe98d5..4aa03dc 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java +++ b/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java @@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Axis; import net.dnorberg.rivencraft.block.BookReceptacleBlock; import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity; +import net.dnorberg.rivencraft.registry.ModBlocks; import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.client.model.BookModel; import net.minecraft.client.model.geom.ModelLayers; @@ -30,11 +31,15 @@ public final class BookReceptacleRenderer implements BlockEntityRenderer { // Rotate the whole assembled top-mounted receptacle/book pair diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java index 73a61eb..9eeab01 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java @@ -53,6 +53,8 @@ public final class CrystalPortalGameTests { helper.assertTrue(plan.valid(), "A closed crystal frame must form a portal"); BookReceptacleBlockEntity holder = (BookReceptacleBlockEntity) level.getBlockEntity(holderPos); holder.setBook(new ItemStack(ModItems.LINKING_BOOK.get())); + helper.assertTrue(level.getBlockState(holderPos).getValue(BookReceptacleBlock.HAS_BOOK), + "An occupied receptacle must expose its book state to client renderers"); LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0); holder.setDestination(destination); BookReceptacleBlockEntity restored = new BookReceptacleBlockEntity(holderPos, holder.getBlockState()); @@ -89,6 +91,12 @@ public final class CrystalPortalGameTests { "A north/south opening must only show its two player-facing sides"); CrystalPortalNetwork.shutdown(level, holderPos); helper.assertTrue(level.getBlockState(center).isAir(), "Removing the book must release the portal"); + helper.assertTrue(holder.takeBook().is(ModItems.LINKING_BOOK.get()), + "Removing the book must return the stored item"); + helper.assertTrue(!level.getBlockState(holderPos).getValue(BookReceptacleBlock.HAS_BOOK), + "Removing the book must clear the client-visible model state"); + helper.assertTrue(!holder.getUpdateTag(level.registryAccess()).contains("Book"), + "The block-entity update must also clear the old client-side book stack"); helper.succeed(); } diff --git a/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json b/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json index d21bddf..f5e8bb0 100644 --- a/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json +++ b/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json @@ -1 +1,10 @@ -{"variants":{"facing=north":{"model":"rivencraft:block/book_receptacle","y":180},"facing=east":{"model":"rivencraft:block/book_receptacle","y":270},"facing=south":{"model":"rivencraft:block/book_receptacle"},"facing=west":{"model":"rivencraft:block/book_receptacle","y":90},"facing=up":{"model":"rivencraft:block/book_receptacle","x":90},"facing=down":{"model":"rivencraft:block/book_receptacle","x":270,"y":180}}} +{ + "multipart": [ + {"when": {"facing": "north"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 180}}, + {"when": {"facing": "east"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 270}}, + {"when": {"facing": "south"}, "apply": {"model": "rivencraft:block/book_receptacle"}}, + {"when": {"facing": "west"}, "apply": {"model": "rivencraft:block/book_receptacle", "y": 90}}, + {"when": {"facing": "up"}, "apply": {"model": "rivencraft:block/book_receptacle", "x": 90}}, + {"when": {"facing": "down"}, "apply": {"model": "rivencraft:block/book_receptacle", "x": 270, "y": 180}} + ] +}