Public Access
This commit is contained in:
1 parent
0e602f0154
commit
3109938a97
15 files changed
+390
-8
No files matched your search
@@ -18,6 +18,12 @@ 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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Java 21 JDK
|
||||
|
||||
+13
-1
@@ -46,7 +46,8 @@ the symbol grammar is introduced.
|
||||
- [x] Nearby landing search and clear failure messages.
|
||||
- [x] Descriptive Book identity and Age reference components.
|
||||
- [ ] Return-link creation as part of Descriptive Book travel.
|
||||
- [ ] Lectern/book-stand interaction.
|
||||
- [x] Lectern-to-Book-Stand conversion, displayed book, activation, removal,
|
||||
persistence, and drops.
|
||||
- [ ] Automated in-game tests for binding, persistence, and travel.
|
||||
|
||||
The temporary shapeless Linking Book recipe makes the linking subsystem easy
|
||||
@@ -57,6 +58,17 @@ Age references use UUIDs rather than dimension keys. Milestone 3 will own the
|
||||
mapping from that stable identity to server data and a runtime dimension. This
|
||||
keeps saved books valid if an Age is migrated or regenerated.
|
||||
|
||||
Vanilla lecterns accept extensible item tags when inserting books, but their
|
||||
block entity still validates only vanilla writable and written books. To avoid
|
||||
an invalid lectern state, inserting a Rivencraft book converts the lectern into
|
||||
a visually compatible Rivencraft Book Stand. Removing the book converts it
|
||||
back.
|
||||
|
||||
Mystcraft Legacy did not impose an item cooldown when linking. Rivencraft's
|
||||
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 policy
|
||||
|
||||
Versions follow `x.y.z`:
|
||||
|
||||
+1
-1
@@ -32,7 +32,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.2
|
||||
mod_version=0.1.3
|
||||
# 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
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.mojang.logging.LogUtils;
|
||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
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.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@Mod(Rivencraft.MOD_ID)
|
||||
@@ -15,8 +19,11 @@ public final class Rivencraft {
|
||||
|
||||
public Rivencraft(IEventBus modEventBus) {
|
||||
ModDataComponents.register(modEventBus);
|
||||
ModBlocks.register(modEventBus);
|
||||
ModBlockEntities.register(modEventBus);
|
||||
ModItems.register(modEventBus);
|
||||
ModCreativeTabs.register(modEventBus);
|
||||
NeoForge.EVENT_BUS.addListener(BookStandConversionHandler::onRightClickBlock);
|
||||
LOGGER.info("Initializing Rivencraft");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** A lectern-compatible stand that safely stores and activates Rivencraft books. */
|
||||
public final class BookStandBlock extends LecternBlock {
|
||||
public static final MapCodec<LecternBlock> CODEC = simpleCodec(BookStandBlock::new);
|
||||
|
||||
public BookStandBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<LecternBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new BookStandBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useWithoutItem(
|
||||
BlockState state,
|
||||
Level level,
|
||||
BlockPos pos,
|
||||
Player player,
|
||||
BlockHitResult hitResult
|
||||
) {
|
||||
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
if (player.isShiftKeyDown()) {
|
||||
if (!level.isClientSide) {
|
||||
ItemStack book = stand.takeBook();
|
||||
BlockState lectern = Blocks.LECTERN.defaultBlockState()
|
||||
.setValue(FACING, state.getValue(FACING));
|
||||
level.setBlock(pos, lectern, 3);
|
||||
if (!player.getInventory().add(book)) {
|
||||
player.drop(book, false);
|
||||
}
|
||||
level.playSound(null, pos, SoundEvents.BOOK_PAGE_TURN, SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
|
||||
ItemStack book = stand.getBook();
|
||||
if (book.getItem() instanceof LinkingBookItem && player instanceof ServerPlayer serverPlayer) {
|
||||
LinkingBookItem.useBoundBook(serverPlayer, book);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
|
||||
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof BookStandBlockEntity stand) {
|
||||
ItemStack book = stand.takeBook();
|
||||
if (!book.isEmpty()) {
|
||||
ItemEntity droppedBook = new ItemEntity(
|
||||
level,
|
||||
pos.getX() + 0.5,
|
||||
pos.getY() + 1.0,
|
||||
pos.getZ() + 0.5,
|
||||
book
|
||||
);
|
||||
droppedBook.setDefaultPickUpDelay();
|
||||
level.addFreshEntity(droppedBook);
|
||||
}
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class BookStandBlockEntity extends BlockEntity {
|
||||
private static final String BOOK_TAG = "Book";
|
||||
private ItemStack book = ItemStack.EMPTY;
|
||||
|
||||
public BookStandBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ModBlockEntities.BOOK_STAND.get(), pos, state);
|
||||
}
|
||||
|
||||
public ItemStack getBook() {
|
||||
return book;
|
||||
}
|
||||
|
||||
public void setBook(ItemStack book) {
|
||||
this.book = book;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public ItemStack takeBook() {
|
||||
ItemStack result = book;
|
||||
book = ItemStack.EMPTY;
|
||||
setChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.loadAdditional(tag, registries);
|
||||
book = tag.contains(BOOK_TAG, CompoundTag.TAG_COMPOUND)
|
||||
? ItemStack.parse(registries, tag.getCompound(BOOK_TAG)).orElse(ItemStack.EMPTY)
|
||||
: ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.saveAdditional(tag, registries);
|
||||
if (!book.isEmpty()) {
|
||||
tag.put(BOOK_TAG, book.save(registries));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
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.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.EnchantTableRenderer;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlockEntity> {
|
||||
private final BookModel bookModel;
|
||||
|
||||
public BookStandRenderer(BlockEntityRendererProvider.Context context) {
|
||||
bookModel = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(
|
||||
BookStandBlockEntity blockEntity,
|
||||
float partialTick,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource bufferSource,
|
||||
int packedLight,
|
||||
int packedOverlay
|
||||
) {
|
||||
BlockState state = blockEntity.getBlockState();
|
||||
if (!state.getValue(BookStandBlock.HAS_BOOK)) {
|
||||
return;
|
||||
}
|
||||
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5F, 1.0625F, 0.5F);
|
||||
float rotation = state.getValue(BookStandBlock.FACING).getClockWise().toYRot();
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-rotation));
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5F));
|
||||
poseStack.translate(0.0F, -0.125F, 0.0F);
|
||||
bookModel.setupAnim(0.0F, 0.1F, 0.9F, 1.2F);
|
||||
VertexConsumer consumer = EnchantTableRenderer.BOOK_LOCATION.buffer(bufferSource, RenderType::entitySolid);
|
||||
bookModel.render(poseStack, consumer, packedLight, packedOverlay, -1);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
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.neoforge.client.event.EntityRenderersEvent;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class ClientEvents {
|
||||
private ClientEvents() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.dnorberg.rivencraft.event;
|
||||
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
|
||||
public final class BookStandConversionHandler {
|
||||
private BookStandConversionHandler() {
|
||||
}
|
||||
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
ItemStack heldBook = event.getItemStack();
|
||||
if (!heldBook.is(ModItems.LINKING_BOOK.get()) && !heldBook.is(ModItems.DESCRIPTIVE_BOOK.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockState lectern = event.getLevel().getBlockState(event.getPos());
|
||||
if (!lectern.is(Blocks.LECTERN) || lectern.getValue(LecternBlock.HAS_BOOK)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.sidedSuccess(event.getLevel().isClientSide));
|
||||
if (event.getLevel().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockState bookStand = ModBlocks.BOOK_STAND.get().defaultBlockState()
|
||||
.setValue(BookStandBlock.FACING, lectern.getValue(LecternBlock.FACING))
|
||||
.setValue(BookStandBlock.HAS_BOOK, true);
|
||||
event.getLevel().setBlock(event.getPos(), bookStand, 3);
|
||||
if (event.getLevel().getBlockEntity(event.getPos()) instanceof BookStandBlockEntity stand) {
|
||||
stand.setBook(heldBook.consumeAndReturn(1, event.getEntity()));
|
||||
}
|
||||
event.getLevel().playSound(
|
||||
null,
|
||||
event.getPos(),
|
||||
SoundEvents.BOOK_PUT,
|
||||
SoundSource.BLOCKS,
|
||||
1.0F,
|
||||
1.0F
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,6 @@ import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class LinkingBookItem extends Item {
|
||||
private static final int LINK_COOLDOWN_TICKS = 40;
|
||||
|
||||
public LinkingBookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
@@ -47,12 +45,21 @@ public final class LinkingBookItem extends Item {
|
||||
return InteractionResultHolder.success(stack);
|
||||
}
|
||||
|
||||
LinkingService.LinkResult result = LinkingService.travel(serverPlayer, target);
|
||||
return useBoundBook(serverPlayer, stack);
|
||||
}
|
||||
|
||||
public static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack) {
|
||||
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
|
||||
if (target == null) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.unbound"), true);
|
||||
return InteractionResultHolder.fail(stack);
|
||||
}
|
||||
|
||||
LinkingService.LinkResult result = LinkingService.travel(player, target);
|
||||
if (result == LinkingService.LinkResult.SUCCESS) {
|
||||
serverPlayer.getCooldowns().addCooldown(this, LINK_COOLDOWN_TICKS);
|
||||
serverPlayer.level().playSound(
|
||||
player.level().playSound(
|
||||
null,
|
||||
serverPlayer.blockPosition(),
|
||||
player.blockPosition(),
|
||||
SoundEvents.PORTAL_TRAVEL,
|
||||
SoundSource.PLAYERS,
|
||||
0.35F,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
public final class ModBlockEntities {
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, Rivencraft.MOD_ID);
|
||||
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<BookStandBlockEntity>> BOOK_STAND =
|
||||
BLOCK_ENTITIES.register("book_stand", () -> BlockEntityType.Builder
|
||||
.of(BookStandBlockEntity::new, ModBlocks.BOOK_STAND.get())
|
||||
.build(null));
|
||||
|
||||
private ModBlockEntities() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
BLOCK_ENTITIES.register(modEventBus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredBlock;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
public final class ModBlocks {
|
||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(Rivencraft.MOD_ID);
|
||||
|
||||
public static final DeferredBlock<BookStandBlock> BOOK_STAND = BLOCKS.registerBlock(
|
||||
"book_stand",
|
||||
BookStandBlock::new,
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN)
|
||||
);
|
||||
|
||||
private ModBlocks() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
BLOCKS.register(modEventBus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=east": {
|
||||
"model": "minecraft:block/lectern",
|
||||
"y": 90
|
||||
},
|
||||
"facing=north": {
|
||||
"model": "minecraft:block/lectern"
|
||||
},
|
||||
"facing=south": {
|
||||
"model": "minecraft:block/lectern",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"model": "minecraft:block/lectern",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"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.linked": "Linked",
|
||||
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"bonus_rolls": 0.0,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:lectern"
|
||||
}
|
||||
],
|
||||
"rolls": 1.0
|
||||
}
|
||||
],
|
||||
"random_sequence": "rivencraft:blocks/book_stand"
|
||||
}
|
||||
Reference in new issue
Block a user