diff --git a/gradle.properties b/gradle.properties index f230ba7..1f4060f 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.8 +mod_version=0.6.6.9 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/LinkModifierBlock.java b/src/main/java/net/dnorberg/rivencraft/block/LinkModifierBlock.java new file mode 100644 index 0000000..6b42ceb --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/block/LinkModifierBlock.java @@ -0,0 +1,63 @@ +package net.dnorberg.rivencraft.block; + +import com.mojang.serialization.MapCodec; +import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity; +import net.dnorberg.rivencraft.menu.LinkModifierMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +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.phys.BlockHitResult; + +public final class LinkModifierBlock extends BaseEntityBlock { + public static final MapCodec CODEC = simpleCodec(LinkModifierBlock::new); + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + public LinkModifierBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override protected MapCodec codec() { return CODEC; } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { builder.add(FACING); } + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new LinkModifierBlockEntity(pos, state); + } + @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + if (!level.isClientSide && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) { + serverPlayer.openMenu(new SimpleMenuProvider( + (id, inventory, ignored) -> new LinkModifierMenu(id, inventory, modifier), + Component.translatable("container.rivencraft.link_modifier")), + buffer -> buffer.writeBlockPos(pos)); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + @Override protected void onRemove(BlockState state, Level level, BlockPos pos, + BlockState replacement, boolean moving) { + if (!state.is(replacement.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) { + Containers.dropContents(level, pos, modifier); + } + super.onRemove(state, level, pos, replacement, moving); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/BookBinderBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/BookBinderBlockEntity.java index fd6a8d3..5ed6146 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/entity/BookBinderBlockEntity.java +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/BookBinderBlockEntity.java @@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.writing.NotebookContents; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.WritingGrammar; @@ -96,7 +97,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe case COVER -> validCover(stack); case LINK_PANEL -> stack.is(ModItems.PAGE.get()) && PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL; - case NOTEBOOK -> stack.is(ModItems.NOTEBOOK.get()); + case NOTEBOOK -> CollationFolderItem.isCollection(stack); default -> slot >= FIRST_PAGE && slot < SIZE && validPage(stack); }; } @@ -296,7 +297,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe /** Import a Notebook from either its stored slot or the player's cursor. */ public ItemStack importNotebookFromStack(ItemStack notebook, int index) { - if (!notebook.is(ModItems.NOTEBOOK.get()) || notebook.getCount() != 1) return ItemStack.EMPTY; + if (!CollationFolderItem.isCollection(notebook) || notebook.getCount() != 1) return ItemStack.EMPTY; List pages = notebook.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); if (pages.isEmpty()) return ItemStack.EMPTY; int end = nextPageIndex(); diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/LinkModifierBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/LinkModifierBlockEntity.java new file mode 100644 index 0000000..7ea0742 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/LinkModifierBlockEntity.java @@ -0,0 +1,63 @@ +package net.dnorberg.rivencraft.block.entity; + +import net.dnorberg.rivencraft.registry.ModBlockEntities; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.Container; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** One persisted book slot; edits are performed by the server menu. */ +public final class LinkModifierBlockEntity extends BlockEntity implements Container { + private NonNullList items = NonNullList.withSize(1, ItemStack.EMPTY); + + public LinkModifierBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.LINK_MODIFIER.get(), pos, state); + } + + @Override public int getContainerSize() { return 1; } + @Override public boolean isEmpty() { return items.getFirst().isEmpty(); } + @Override public ItemStack getItem(int slot) { return items.get(slot); } + @Override public ItemStack removeItem(int slot, int amount) { + ItemStack removed = ContainerHelper.removeItem(items, slot, amount); + if (!removed.isEmpty()) setChanged(); + return removed; + } + @Override public ItemStack removeItemNoUpdate(int slot) { + ItemStack removed = ContainerHelper.takeItem(items, slot); + if (!removed.isEmpty()) setChanged(); + return removed; + } + @Override public void setItem(int slot, ItemStack stack) { + items.set(slot, stack.copyWithCount(Math.min(1, stack.getCount()))); + setChanged(); + } + @Override public boolean canPlaceItem(int slot, ItemStack stack) { + return slot == 0 && (stack.is(ModItems.LINKING_BOOK.get()) + || stack.is(ModItems.DESCRIPTIVE_BOOK.get())); + } + @Override public boolean stillValid(Player player) { + return level != null && level.isLoaded(worldPosition) && level.getBlockEntity(worldPosition) == this + && player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5, + worldPosition.getZ() + 0.5) <= 64.0; + } + @Override public void clearContent() { + items = NonNullList.withSize(1, ItemStack.EMPTY); + setChanged(); + } + @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + items = NonNullList.withSize(1, ItemStack.EMPTY); + ContainerHelper.loadAllItems(tag, items, registries); + } + @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + ContainerHelper.saveAllItems(tag, items, registries); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java index ca211cf..83214d3 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.block.entity; import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.writing.InkVialExchange; import net.dnorberg.rivencraft.writing.WritingInkService; import net.dnorberg.rivencraft.writing.PageContent; @@ -87,7 +88,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain worldPosition.getZ() + 0.5) <= 64.0; } @Override public boolean canPlaceItem(int slot, ItemStack stack) { - if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get()) + if (slot < COLLECTIONS) return CollationFolderItem.isCollection(stack) && stack.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY) .pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL) || stack.is(ModItems.PAGE.get()) diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDeletionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDeletionService.java new file mode 100644 index 0000000..fe69aa7 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDeletionService.java @@ -0,0 +1,56 @@ +package net.dnorberg.rivencraft.book; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.AtomicMoveNotSupportedException; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** Retires an exact Age and moves its unloaded region data out of the live world. */ +public final class AgeDeletionService { + public enum Result { RETIRED, ALREADY_DEAD, OCCUPIED, UNLOAD_FAILED, STORAGE_FAILED } + + private AgeDeletionService() { } + + public static Result retire(MinecraftServer server, AgeDefinition age) { + AgeRegistrySavedData registry = AgeRegistrySavedData.get(server.overworld()); + if (registry.isDead(age.id())) return Result.ALREADY_DEAD; + var level = server.getLevel(AgeDimensionService.dimensionKey(age)); + if (level != null && !level.players().isEmpty()) return Result.OCCUPIED; + if (AgeDimensionService.unload(server, age) == AgeDimensionService.UnloadResult.FAILED) + return Result.UNLOAD_FAILED; + + Path world = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); + Path ageRoot = world.resolve("dimensions/rivencraft/age").normalize(); + Path data = world.resolve("dimensions") + .resolve(age.dimensionId().getNamespace()) + .resolve(age.dimensionId().getPath()).normalize(); + // Exact Rivencraft Age only; never move the shared age/ directory. + if (!data.startsWith(ageRoot) || data.equals(ageRoot) + || !"rivencraft".equals(age.dimensionId().getNamespace()) + || !age.dimensionId().getPath().startsWith("age/")) return Result.STORAGE_FAILED; + try { + if (Files.isSymbolicLink(world.resolve("dimensions")) + || Files.isSymbolicLink(world.resolve("dimensions/rivencraft")) + || Files.isSymbolicLink(ageRoot) || Files.isSymbolicLink(data)) return Result.STORAGE_FAILED; + if (Files.exists(data, LinkOption.NOFOLLOW_LINKS)) { + Path archive = world.getParent().resolve(world.getFileName() + "-retired-ages") + .resolve(age.id().toString()); + if (Files.exists(archive, LinkOption.NOFOLLOW_LINKS)) return Result.STORAGE_FAILED; + Files.createDirectories(archive.getParent()); + try { Files.move(data, archive, StandardCopyOption.ATOMIC_MOVE); } + catch (AtomicMoveNotSupportedException exception) { Files.move(data, archive); } + Rivencraft.LOGGER.warn("Retired Age {} data moved from {} to {}", age.id(), data, archive); + } + } catch (IOException exception) { + Rivencraft.LOGGER.error("Cannot archive data for Age {}", age.id(), exception); + return Result.STORAGE_FAILED; + } + registry.markDead(age.id()); + return Result.RETIRED; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index a700460..303d4c5 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -74,6 +74,7 @@ public final class AgeDimensionService { DynamicDimensionLoadCallback.register((server, loader) -> { ServerLevel overworld = server.overworld(); for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) { + if (AgeRegistrySavedData.get(overworld).isDead(age.id())) continue; if (!canLoadProfile(server, age)) continue; ServerLevel level = loader.loadDynamicDimension( age.dimensionId(), @@ -96,6 +97,7 @@ public final class AgeDimensionService { public static Optional resolve(ServerPlayer player, AgeReference reference, OrderedWriting writing) { AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing); + if (AgeRegistrySavedData.get(player.serverLevel()).isDead(definition.id())) return Optional.empty(); ServerLevel level = ensureLoaded(player.getServer(), definition); if (level == null) { return Optional.empty(); @@ -122,6 +124,7 @@ public final class AgeDimensionService { } public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) { + if (AgeRegistrySavedData.get(server.overworld()).isDead(definition.id())) return null; ResourceKey key = dimensionKey(definition); ServerLevel existing = server.getLevel(key); if (existing != null) { diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java index f8f9c7e..13fa9df 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java @@ -23,6 +23,24 @@ public final class AgePersistenceGameTests { private AgePersistenceGameTests() { } + @GameTest(template = "empty") + public static void retiredAgeIdentitySurvivesSaveAndCannotLoad(GameTestHelper helper) { + UUID id = UUID.randomUUID(); + AgeReference reference = new AgeReference(id, "Retired " + id); + AgeDefinition age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), reference); + AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel()); + helper.assertTrue(registry.markDead(id), "The first retirement must set the tombstone"); + helper.assertTrue(!registry.markDead(id), "Retirement must be idempotent"); + helper.assertTrue(AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), age) == null, + "A retired Age must never load from an old book"); + AgeRegistrySavedData restored = AgeRegistrySavedData.load( + registry.save(new CompoundTag(), helper.getLevel().registryAccess()), + helper.getLevel().registryAccess()); + helper.assertTrue(restored.isDead(id) && restored.find(id).isPresent(), + "Both the retired flag and reserved Age identity must survive reload"); + helper.succeed(); + } + @GameTest(template = "empty") public static void authoredPlanRetainsResolvedMeaningAndAgeIdentity(GameTestHelper helper) { OrderedWriting writing = new OrderedWriting(List.of( diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java index 8300a99..990a639 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java @@ -5,6 +5,9 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.List; +import java.util.HashSet; +import java.util.Set; +import net.minecraft.nbt.StringTag; import net.dnorberg.rivencraft.writing.OrderedWriting; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; @@ -21,6 +24,7 @@ import net.neoforged.fml.ModList; public final class AgeRegistrySavedData extends SavedData { private static final String DATA_NAME = "rivencraft_ages"; private static final String AGES_TAG = "Ages"; + private static final String DEAD_AGES_TAG = "DeadAges"; private static final String ID_TAG = "Id"; private static final String NAME_TAG = "Name"; private static final String SEED_TAG = "Seed"; @@ -50,6 +54,7 @@ public final class AgeRegistrySavedData extends SavedData { ); private final Map ages = new HashMap<>(); + private final Set deadAges = new HashSet<>(); public static AgeRegistrySavedData get(ServerLevel level) { return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); @@ -110,6 +115,16 @@ public final class AgeRegistrySavedData extends SavedData { return List.copyOf(ages.values()); } + public boolean isDead(UUID ageId) { return deadAges.contains(ageId); } + + /** Keep the ID and definition reserved so an old book cannot regenerate a retired Age. */ + public boolean markDead(UUID ageId) { + if (!ages.containsKey(ageId)) return false; + boolean changed = deadAges.add(ageId); + if (changed) setDirty(); + return changed; + } + public AgeDefinition setSpawn(UUID ageId, BlockPos spawn) { AgeDefinition existing = ages.get(ageId); if (existing == null) { @@ -161,6 +176,11 @@ public final class AgeRegistrySavedData extends SavedData { static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) { AgeRegistrySavedData data = new AgeRegistrySavedData(); + ListTag deadTags = tag.getList(DEAD_AGES_TAG, StringTag.TAG_STRING); + for (int index = 0; index < deadTags.size(); index++) { + try { data.deadAges.add(UUID.fromString(deadTags.getString(index))); } + catch (IllegalArgumentException ignored) { } + } ListTag serializedAges = tag.getList(AGES_TAG, CompoundTag.TAG_COMPOUND); for (int index = 0; index < serializedAges.size(); index++) { CompoundTag entry = serializedAges.getCompound(index); @@ -264,6 +284,9 @@ public final class AgeRegistrySavedData extends SavedData { serializedAges.add(entry); }); tag.put(AGES_TAG, serializedAges); + ListTag deadTags = new ListTag(); + deadAges.stream().sorted().forEach(id -> deadTags.add(StringTag.valueOf(id.toString()))); + tag.put(DEAD_AGES_TAG, deadTags); return tag; } diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java index 87355ea..2d5ff31 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java @@ -26,6 +26,7 @@ import net.dnorberg.rivencraft.client.screen.NotebookScreen; import net.dnorberg.rivencraft.client.screen.WritingDeskScreen; import net.dnorberg.rivencraft.client.screen.BookBinderScreen; import net.dnorberg.rivencraft.client.screen.InkMixerScreen; +import net.dnorberg.rivencraft.client.screen.LinkModifierScreen; import net.dnorberg.rivencraft.registry.ModMenus; @EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT) @@ -47,6 +48,7 @@ public final class ClientEvents { event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new); event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new); event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new); + event.register(ModMenus.LINK_MODIFIER.get(), LinkModifierScreen::new); } @SubscribeEvent diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java index f43ffd4..3c8836a 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java @@ -27,6 +27,7 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.network.chat.Component; +import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; import net.minecraft.util.FormattedCharSequence; @@ -48,6 +49,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo private static final Component EXPLORE_ICON = Component.literal("👁"); private final AgeReference age; + private final Component displayName; private final ResourceLocation generationProfile; private final AgeTraits traits; private final OrderedWriting writing; @@ -68,6 +70,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo List writingSymbols) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); + displayName = book.get(DataComponents.CUSTOM_NAME); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); this.traits = traits; writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); @@ -82,6 +85,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo List writingSymbols) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); + displayName = book.get(DataComponents.CUSTOM_NAME); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); this.traits = traits; writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); @@ -245,7 +249,8 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo } return; } - int nameEnd = drawWrappedWithoutShadow(graphics, Component.literal(age.name()), + int nameEnd = drawWrappedWithoutShadow(graphics, + displayName == null ? Component.literal(age.name()) : displayName, x, top + 40, 110); int detailsY = drawWrappedWithoutShadow(graphics, author == null || author.isBlank() diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java new file mode 100644 index 0000000..d539099 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java @@ -0,0 +1,105 @@ +package net.dnorberg.rivencraft.client.screen; + +import net.dnorberg.rivencraft.book.LinkProperties; +import net.dnorberg.rivencraft.menu.LinkModifierMenu; +import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload; +import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.network.PacketDistributor; + +public final class LinkModifierScreen extends AbstractContainerScreen { + private EditBox titleInput; + private Button disarm; + private Button momentum; + private Button intra; + private Button retire; + private ItemStack lastBook = ItemStack.EMPTY; + + public LinkModifierScreen(LinkModifierMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 176; + imageHeight = 194; + inventoryLabelX = 8; + inventoryLabelY = 99; + } + + @Override protected void init() { + super.init(); + titleInput = new EditBox(font, leftPos + 8, topPos + 54, 160, 14, + Component.translatable("screen.rivencraft.link_modifier.title")); + titleInput.setMaxLength(32); + titleInput.setResponder(value -> PacketDistributor.sendToServer( + new SetLinkModifierTitlePayload(menu.containerId, value))); + addRenderableWidget(titleInput); + disarm = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_DISARM)) + .bounds(leftPos + 8, topPos + 74, 76, 20).build()); + momentum = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_MOMENTUM)) + .bounds(leftPos + 92, topPos + 74, 76, 20).build()); + intra = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_INTRA)) + .bounds(leftPos + 8, topPos + 18, 64, 20).build()); + retire = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.DELETE_AGE)) + .bounds(leftPos + 104, topPos + 18, 64, 20).build()); + refreshControls(); + } + + private void click(int id) { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id); + } + } + + @Override protected void containerTick() { refreshControls(); } + + private void refreshControls() { + ItemStack book = menu.book(); + if (!ItemStack.isSameItemSameComponents(book, lastBook) && !titleInput.isFocused()) { + Component name = book.get(DataComponents.CUSTOM_NAME); + titleInput.setValue(name == null ? "" : name.getString()); + lastBook = book.copy(); + } + boolean present = !book.isEmpty(); + titleInput.setEditable(present); + LinkProperties properties = book.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE); + disarm.active = present; + disarm.setMessage(Component.literal("Disarm " + (properties.has(LinkProperties.DISARM) ? "✓" : "×"))); + momentum.active = present; + momentum.setMessage(Component.literal("Momentum " + (properties.has(LinkProperties.MAINTAIN_MOMENTUM) ? "✓" : "×"))); + intra.visible = book.is(ModItems.LINKING_BOOK.get()); + intra.setMessage(Component.literal("Intra " + (properties.has(LinkProperties.INTRA_LINKING) ? "✓" : "×"))); + retire.visible = menu.canDelete(minecraft.player); + retire.setMessage(Component.translatable(menu.awaitingDeletionConfirmation() + ? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire")); + } + + @Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + graphics.fill(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight, 0xFFD6C7A7); + graphics.renderOutline(leftPos, topPos, imageWidth, imageHeight, 0xFF493C34); + graphics.fill(leftPos + 78, topPos + 28, leftPos + 98, topPos + 48, 0xFF83785F); + graphics.renderOutline(leftPos + 77, topPos + 27, 22, 22, 0xFF493C34); + for (int row = 0; row < 3; row++) for (int col = 0; col < 9; col++) + slotBackground(graphics, 8 + col * 18, 112 + row * 18); + for (int col = 0; col < 9; col++) slotBackground(graphics, 8 + col * 18, 170); + } + + private void slotBackground(GuiGraphics graphics, int x, int y) { + graphics.fill(leftPos + x - 1, topPos + y - 1, leftPos + x + 17, topPos + y + 17, 0xFF81745C); + } + + @Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + graphics.drawString(font, title, 8, 6, 0xFF332722, false); + graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF332722, false); + } + + @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java index 32f1865..8e927f4 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java @@ -182,9 +182,9 @@ public final class NotebookScreen extends AbstractContainerScreen private int displayedPageSlots() { if (menu.getCarried().is(ModItems.PAGE.get())) { - return NotebookMenu.NOTEBOOK_SLOTS; + return menu.pageCapacity(); } - for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) { + for (int index = menu.pageCapacity() - 1; index >= 0; index--) { if (!menu.getSlot(index).getItem().isEmpty()) { return index + 1; } diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java index 6d3f211..6d0e5c6 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java @@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; import net.minecraft.client.gui.GuiGraphics; @@ -234,7 +235,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen pages(int collectionIndex) { ItemStack collection = menu.getSlot(collectionIndex).getItem(); if (collection.is(ModItems.PAGE.get())) return List.of(collection); - if (collection.is(ModItems.NOTEBOOK.get())) { + if (CollationFolderItem.isCollection(collection)) { return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); } return List.of(); @@ -301,9 +302,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen pages = current.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); - if (pages.size() < NotebookContents.MAX_PAGES || pages.stream().anyMatch(ItemStack::isEmpty)) + if (pages.size() < CollationFolderItem.capacity(current) || pages.stream().anyMatch(ItemStack::isEmpty)) return index; } else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held) && current.getCount() < slot.getMaxStackSize(held)) return index; diff --git a/src/main/java/net/dnorberg/rivencraft/command/RivencraftCommands.java b/src/main/java/net/dnorberg/rivencraft/command/RivencraftCommands.java index 761dadb..8372984 100644 --- a/src/main/java/net/dnorberg/rivencraft/command/RivencraftCommands.java +++ b/src/main/java/net/dnorberg/rivencraft/command/RivencraftCommands.java @@ -112,8 +112,9 @@ public final class RivencraftCommands { source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false); for (AgeDefinition age : ages) { boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null; + boolean retired = AgeRegistrySavedData.get(source.getLevel()).isDead(age.id()); source.sendSuccess(() -> Component.literal( - (loaded ? "[loaded] " : "[unloaded] ") + (retired ? "[retired] " : loaded ? "[loaded] " : "[unloaded] ") + age.name() + " — " + age.dimensionId() + (age.seasonMode() == null ? "" : " (season: " + age.seasonMode().name().toLowerCase(java.util.Locale.ROOT) + ")") diff --git a/src/main/java/net/dnorberg/rivencraft/item/CollationFolderItem.java b/src/main/java/net/dnorberg/rivencraft/item/CollationFolderItem.java new file mode 100644 index 0000000..ab0ca5f --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/item/CollationFolderItem.java @@ -0,0 +1,23 @@ +package net.dnorberg.rivencraft.item; + +import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.writing.NotebookContents; +import net.minecraft.world.item.ItemStack; + +/** Legacy's compact page collection, using the existing Notebook page editor. */ +public final class CollationFolderItem extends NotebookItem { + public static final int MAX_PAGES = 27; + + public CollationFolderItem(Properties properties) { + super(properties); + } + + public static boolean isCollection(ItemStack stack) { + return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get()); + } + + public static int capacity(ItemStack stack) { + return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES + : stack.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES : 0; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/item/NotebookItem.java b/src/main/java/net/dnorberg/rivencraft/item/NotebookItem.java index 6d8e6ac..f4f9bf5 100644 --- a/src/main/java/net/dnorberg/rivencraft/item/NotebookItem.java +++ b/src/main/java/net/dnorberg/rivencraft/item/NotebookItem.java @@ -16,7 +16,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.Level; -public final class NotebookItem extends Item { +public class NotebookItem extends Item { public NotebookItem(Properties properties) { super(properties); } @@ -39,7 +39,9 @@ public final class NotebookItem extends Item { NotebookContents contents = stack.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY); tooltip.add(Component.translatable("item.rivencraft.notebook.pages", - contents.pageCount(), NotebookContents.MAX_PAGES).withStyle(ChatFormatting.GRAY)); + contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get()) + ? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES) + .withStyle(ChatFormatting.GRAY)); if (contents.unresolvedCount() > 0) { tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved", contents.unresolvedCount()).withStyle(ChatFormatting.RED)); diff --git a/src/main/java/net/dnorberg/rivencraft/item/SealedNotebookItem.java b/src/main/java/net/dnorberg/rivencraft/item/SealedNotebookItem.java new file mode 100644 index 0000000..9f77c5c --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/item/SealedNotebookItem.java @@ -0,0 +1,62 @@ +package net.dnorberg.rivencraft.item; + +import java.util.ArrayList; +import java.util.List; +import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.writing.NotebookContents; +import net.dnorberg.rivencraft.writing.SymbolCatalog; +import net.dnorberg.rivencraft.writing.SymbolReference; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** Opens into a 16-page Collation Folder, as Legacy's booster item did. */ +public final class SealedNotebookItem extends Item { + private static final int PAGE_COUNT = 16; + + public SealedNotebookItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack sealed = player.getItemInHand(hand); + if (level.isClientSide) return InteractionResultHolder.sidedSuccess(sealed, true); + if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResultHolder.pass(sealed); + ItemStack folder = open(serverPlayer); + if (folder.isEmpty()) return InteractionResultHolder.fail(sealed); + if (!player.hasInfiniteMaterials()) sealed.shrink(1); + if (sealed.isEmpty()) { + player.setItemInHand(hand, folder); + } else if (!player.getInventory().add(folder)) { + player.drop(folder, false); + } + return InteractionResultHolder.success(player.getItemInHand(hand)); + } + + static ItemStack open(ServerPlayer player) { + // Current symbol definitions have no Legacy rank. Draw from the authored + // symbol pool, excluding virtual block/biome pages and corrupted pages. + List candidates = SymbolCatalog.symbols().entrySet().stream() + .filter(entry -> entry.getKey().getNamespace().equals("rivencraft") + && !entry.getKey().getPath().startsWith("block/") + && !entry.getKey().getPath().startsWith("biome/") + && !entry.getValue().category().getPath().equals("corrupted")) + .map(java.util.Map.Entry::getKey).sorted().toList(); + if (candidates.isEmpty()) return ItemStack.EMPTY; + List pages = new ArrayList<>(PAGE_COUNT); + for (int i = 0; i < PAGE_COUNT; i++) { + ResourceLocation id = candidates.get(player.getRandom().nextInt(candidates.size())); + pages.add(PageItem.createSymbolPage(new SymbolReference(id))); + } + ItemStack folder = new ItemStack(ModItems.COLLATION_FOLDER.get()); + folder.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages)); + return folder; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/menu/BookBinderMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/BookBinderMenu.java index b5cd228..6fde0c3 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/BookBinderMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/BookBinderMenu.java @@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity; import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.WritingGrammar; @@ -172,7 +173,7 @@ public final class BookBinderMenu extends AbstractContainerMenu { private boolean clickStrip(int index) { ItemStack carried = getCarried(); - if (carried.is(ModItems.NOTEBOOK.get())) { + if (CollationFolderItem.isCollection(carried)) { ItemStack emptied = binder.importNotebookFromStack(carried, Math.min(index == 0 ? 0 : index - 1, binder.nextPageIndex())); if (emptied.isEmpty()) return false; @@ -227,7 +228,7 @@ public final class BookBinderMenu extends AbstractContainerMenu { moved = moveItemStackTo(source, PLAYER_START, slots.size(), true); } else if (source.is(Items.LEATHER) || BookBinderBlockEntity.validCover(source)) { moved = moveItemStackTo(source, BookBinderBlockEntity.COVER, BookBinderBlockEntity.COVER + 1, false); - } else if (source.is(ModItems.NOTEBOOK.get())) { + } else if (CollationFolderItem.isCollection(source)) { ItemStack emptied = binder.importNotebookFromStack(source, binder.nextPageIndex()); if (emptied.isEmpty()) return ItemStack.EMPTY; sourceSlot.setByPlayer(emptied); diff --git a/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java new file mode 100644 index 0000000..42bf818 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java @@ -0,0 +1,145 @@ +package net.dnorberg.rivencraft.menu; + +import java.util.UUID; +import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity; +import net.dnorberg.rivencraft.book.AgeDefinition; +import net.dnorberg.rivencraft.book.AgeDeletionService; +import net.dnorberg.rivencraft.book.AgeReference; +import net.dnorberg.rivencraft.book.AgeRegistrySavedData; +import net.dnorberg.rivencraft.book.LinkProperties; +import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.registry.ModMenus; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +/** Server-owned book edits and a two-stage, OP-only Age retirement control. */ +public final class LinkModifierMenu extends AbstractContainerMenu { + public static final int TOGGLE_DISARM = 1; + public static final int TOGGLE_MOMENTUM = 2; + public static final int TOGGLE_INTRA = 3; + public static final int DELETE_AGE = 4; + private static final int PLAYER_START = 1; + private static final int MAX_TITLE_LENGTH = 32; + + private final LinkModifierBlockEntity modifier; + private final Container bookSlot; + private final boolean clientSide; + private int confirmation; + private UUID pendingAge; + + public LinkModifierMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { + this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) + instanceof LinkModifierBlockEntity found ? found : null); + } + + public LinkModifierMenu(int id, Inventory inventory, LinkModifierBlockEntity modifier) { + super(ModMenus.LINK_MODIFIER.get(), id); + this.modifier = modifier; + this.bookSlot = modifier == null ? new SimpleContainer(1) : modifier; + this.clientSide = inventory.player.level().isClientSide; + addDataSlot(new DataSlot() { + @Override public int get() { return confirmation; } + @Override public void set(int value) { confirmation = value; } + }); + addSlot(new Slot(bookSlot, 0, 80, 30) { + @Override public boolean mayPlace(ItemStack stack) { return bookSlot.canPlaceItem(0, stack); } + @Override public int getMaxStackSize() { return 1; } + @Override public void setChanged() { + super.setChanged(); + pendingAge = null; + confirmation = 0; + } + }); + for (int row = 0; row < 3; row++) for (int col = 0; col < 9; col++) + addSlot(new Slot(inventory, col + row * 9 + 9, 8 + col * 18, 112 + row * 18)); + for (int col = 0; col < 9; col++) + addSlot(new Slot(inventory, col, 8 + col * 18, 170)); + } + + public ItemStack book() { return bookSlot.getItem(0); } + public boolean awaitingDeletionConfirmation() { return confirmation == 1; } + public boolean canDelete(Player player) { + return player.hasPermissions(2) && book().is(ModItems.DESCRIPTIVE_BOOK.get()) + && book().has(ModDataComponents.AGE_REFERENCE.get()); + } + + public boolean setTitle(Player player, String title) { + if (clientSide || !stillValid(player) || title == null || title.length() > MAX_TITLE_LENGTH + || book().isEmpty()) return false; + String clean = title.strip(); + if (clean.isEmpty()) book().remove(DataComponents.CUSTOM_NAME); + else book().set(DataComponents.CUSTOM_NAME, Component.literal(clean)); + bookSlot.setChanged(); + broadcastChanges(); + return true; + } + + @Override public boolean clickMenuButton(Player player, int id) { + if (clientSide || !stillValid(player) || book().isEmpty()) return false; + if (id == DELETE_AGE) return retireAge(player); + int flag = switch (id) { + case TOGGLE_DISARM -> LinkProperties.DISARM; + case TOGGLE_MOMENTUM -> LinkProperties.MAINTAIN_MOMENTUM; + case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING; + default -> 0; + }; + if (flag == 0 || (flag == LinkProperties.INTRA_LINKING + && book().is(ModItems.DESCRIPTIVE_BOOK.get()))) return false; + int flags = book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE).flags(); + book().set(ModDataComponents.LINK_PROPERTIES.get(), new LinkProperties(flags ^ flag)); + bookSlot.setChanged(); + pendingAge = null; + confirmation = 0; + broadcastChanges(); + return true; + } + + private boolean retireAge(Player player) { + if (!(player instanceof ServerPlayer serverPlayer) || !canDelete(player)) return false; + AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get()); + if (reference == null) return false; + AgeDefinition age = AgeRegistrySavedData.get(serverPlayer.serverLevel()).find(reference.id()).orElse(null); + if (age == null) return false; + if (confirmation != 1 || !reference.id().equals(pendingAge)) { + pendingAge = reference.id(); + confirmation = 1; + broadcastChanges(); + return true; + } + confirmation = 0; + pendingAge = null; + AgeDeletionService.Result result = AgeDeletionService.retire(serverPlayer.getServer(), age); + player.displayClientMessage(Component.translatable( + "message.rivencraft.link_modifier.delete." + result.name().toLowerCase()), false); + broadcastChanges(); + return result == AgeDeletionService.Result.RETIRED; + } + + @Override public boolean stillValid(Player player) { + return modifier != null && modifier.stillValid(player); + } + + @Override public ItemStack quickMoveStack(Player player, int index) { + Slot slot = slots.get(index); + if (!slot.hasItem()) return ItemStack.EMPTY; + ItemStack source = slot.getItem(); + ItemStack copy = source.copy(); + if (index == 0) { + if (!moveItemStackTo(source, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY; + } else if (!moveItemStackTo(source, 0, 1, false)) return ItemStack.EMPTY; + if (source.isEmpty()) slot.set(ItemStack.EMPTY); + else slot.setChanged(); + return copy; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java index bdedd00..3fc3b0b 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java @@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.minecraft.core.NonNullList; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.Container; @@ -28,6 +29,7 @@ public final class NotebookMenu extends AbstractContainerMenu { private final InteractionHand hand; private final int openedHotbarSlot; private final NotebookContainer contents; + private final int pageCapacity; public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) { this(containerId, inventory, data.readBoolean() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); @@ -38,11 +40,12 @@ public final class NotebookMenu extends AbstractContainerMenu { this.playerInventory = inventory; this.hand = hand; this.openedHotbarSlot = inventory.selected; + this.pageCapacity = CollationFolderItem.capacity(inventory.player.getItemInHand(hand)); this.contents = new NotebookContainer(inventory, hand); for (int page = 0; page < NOTEBOOK_SLOTS; page++) { // The client draws these pages on a filtered, scrollable surface instead. - addSlot(new PageSlot(contents, page, -1000, -1000)); + addSlot(new PageSlot(contents, page, -1000, -1000, page < pageCapacity)); } for (int row = 0; row < 3; row++) { for (int column = 0; column < 9; column++) { @@ -61,11 +64,13 @@ public final class NotebookMenu extends AbstractContainerMenu { : new Slot(playerInventory, inventoryIndex, x, y); } + public int pageCapacity() { return pageCapacity; } + @Override public boolean stillValid(Player player) { return player.isAlive() && (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot) - && player.getItemInHand(hand).is(ModItems.NOTEBOOK.get()); + && CollationFolderItem.isCollection(player.getItemInHand(hand)); } @Override @@ -108,7 +113,7 @@ public final class NotebookMenu extends AbstractContainerMenu { } else { return false; } - if (source < 0 || source >= NOTEBOOK_SLOTS || target < 0 || target >= NOTEBOOK_SLOTS + if (source < 0 || source >= pageCapacity || target < 0 || target >= pageCapacity || contents.getItem(source).isEmpty()) { return false; } @@ -132,14 +137,18 @@ public final class NotebookMenu extends AbstractContainerMenu { } private static final class PageSlot extends Slot { - private PageSlot(Container container, int slot, int x, int y) { + private final boolean enabled; + + private PageSlot(Container container, int slot, int x, int y, boolean enabled) { super(container, slot, x, y); + this.enabled = enabled; } @Override public boolean mayPlace(ItemStack stack) { - return stack.is(ModItems.PAGE.get()) + return enabled && stack.is(ModItems.PAGE.get()) && PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL; } + @Override public boolean mayPickup(Player player) { return enabled; } @Override public int getMaxStackSize() { return 1; } } @@ -184,11 +193,12 @@ public final class NotebookMenu extends AbstractContainerMenu { public void setChanged() { super.setChanged(); ItemStack notebook = inventory.player.getItemInHand(hand); - if (loading || !notebook.is(ModItems.NOTEBOOK.get())) { + if (loading || !CollationFolderItem.isCollection(notebook)) { return; } - NonNullList saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY); - for (int index = 0; index < NOTEBOOK_SLOTS; index++) { + int capacity = CollationFolderItem.capacity(notebook); + NonNullList saved = NonNullList.withSize(capacity, ItemStack.EMPTY); + for (int index = 0; index < capacity; index++) { ItemStack page = getItem(index); saved.set(index, page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1)); } diff --git a/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java index 1513dd4..92b2880 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java @@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.writing.NotebookContents; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.SymbolCatalog; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; @@ -107,7 +108,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu { public int pageCapacity(int collectionIndex) { if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0; ItemStack collection = desk.getItem(collectionIndex); - return collection.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES + return CollationFolderItem.isCollection(collection) ? CollationFolderItem.capacity(collection) : collection.is(ModItems.PAGE.get()) ? 1 : 0; } public List catalog() { return catalog; } @@ -183,7 +184,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu { ItemStack carried = getCarried(); if (clientSide || !storablePage(carried)) return false; ItemStack collection = desk.getItem(collectionIndex); - if (collection.is(ModItems.NOTEBOOK.get())) { + if (CollationFolderItem.isCollection(collection)) { + if (pageIndex < 0 || pageIndex >= CollationFolderItem.capacity(collection)) return false; var pages = new java.util.ArrayList<>(collection.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()); while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY); @@ -207,8 +209,10 @@ public final class WritingDeskMenu extends AbstractContainerMenu { if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false; ItemStack sourceBook = desk.getItem(sourceCollection); ItemStack targetBook = desk.getItem(targetCollection); - boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get()); - boolean targetNotebook = targetBook.is(ModItems.NOTEBOOK.get()); + boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook); + boolean targetNotebook = CollationFolderItem.isCollection(targetBook); + if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook) + || targetNotebook && targetPage >= CollationFolderItem.capacity(targetBook)) return false; if ((!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) || (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false; var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault( @@ -247,7 +251,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu { private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) { if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false; ItemStack sourceBook = desk.getItem(sourceCollection); - boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get()); + boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook); + if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)) return false; if (!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) return false; var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null; @@ -263,12 +268,12 @@ public final class WritingDeskMenu extends AbstractContainerMenu { Slot target = slots.get(targetSlot); if (!target.isActive() || !target.mayPlace(moving)) return false; ItemStack current = target.getItem(); - if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) { + if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) { var targetPages = targetSlot == sourceCollection ? sourcePages : new java.util.ArrayList<>(current.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()); int position = -1; - for (int i = 0; i < NotebookContents.MAX_PAGES; i++) { + for (int i = 0; i < CollationFolderItem.capacity(current); i++) { if (i >= targetPages.size() || targetPages.get(i).isEmpty()) { position = i; break; @@ -342,7 +347,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu { private boolean moveIntoDesk(ItemStack moving) { if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false); - if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false); + if (CollationFolderItem.isCollection(moving)) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false); if (moving.is(Items.PAPER)) return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER, WritingDeskBlockEntity.PAPER + 1, false); @@ -374,7 +379,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu { || pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY; ItemStack collection = desk.getItem(collectionIndex); if (collection.is(ModItems.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY; - if (collection.is(ModItems.NOTEBOOK.get())) { + if (CollationFolderItem.isCollection(collection)) { + if (pageIndex >= CollationFolderItem.capacity(collection)) return ItemStack.EMPTY; List pages = collection.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY; diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 98cffa8..3ef7278 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -18,6 +18,7 @@ import net.dnorberg.rivencraft.event.AgeCelestialAngleSync; import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.menu.BookBinderMenu; +import net.dnorberg.rivencraft.menu.LinkModifierMenu; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.writing.OrderedWriting; @@ -59,7 +60,7 @@ public final class ModNetworking { } public static void register(RegisterPayloadHandlersEvent event) { - PayloadRegistrar registrar = event.registrar("15"); + PayloadRegistrar registrar = event.registrar("16"); registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, ModNetworking::requestDescriptiveBookScreen); registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, @@ -85,6 +86,8 @@ public final class ModNetworking { ClientWritingDeskCatalog::handle); registrar.playToServer(SetBookBinderTitlePayload.TYPE, SetBookBinderTitlePayload.STREAM_CODEC, ModNetworking::setBookBinderTitle); + registrar.playToServer(SetLinkModifierTitlePayload.TYPE, SetLinkModifierTitlePayload.STREAM_CODEC, + ModNetworking::setLinkModifierTitle); } private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) { @@ -95,6 +98,14 @@ public final class ModNetworking { } } + private static void setLinkModifierTitle(SetLinkModifierTitlePayload payload, IPayloadContext context) { + if (context.player() instanceof ServerPlayer player + && player.containerMenu instanceof LinkModifierMenu menu + && menu.containerId == payload.containerId()) { + menu.setTitle(player, payload.title()); + } + } + private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) { if (!(context.player() instanceof ServerPlayer player)) { return; diff --git a/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierTitlePayload.java b/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierTitlePayload.java new file mode 100644 index 0000000..baf3a90 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierTitlePayload.java @@ -0,0 +1,19 @@ +package net.dnorberg.rivencraft.network; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +public record SetLinkModifierTitlePayload(int containerId, String title) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_title")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierTitlePayload::containerId, + ByteBufCodecs.STRING_UTF8, SetLinkModifierTitlePayload::title, + SetLinkModifierTitlePayload::new); + + @Override public Type type() { return TYPE; } +} diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java b/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java index 8614a5f..db0e5d4 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java @@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity; import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity; +import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity; import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity; import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity; import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity; @@ -31,6 +32,9 @@ public final class ModBlockEntities { public static final DeferredHolder, BlockEntityType> INK_MIXER = BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder .of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null)); + public static final DeferredHolder, BlockEntityType> LINK_MODIFIER = + BLOCK_ENTITIES.register("link_modifier", () -> BlockEntityType.Builder + .of(LinkModifierBlockEntity::new, ModBlocks.LINK_MODIFIER.get()).build(null)); public static final DeferredHolder, BlockEntityType> STAR_FISSURE = BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder .of(StarFissureBlockEntity::new, ModBlocks.STAR_FISSURE.get()).build(null)); diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java b/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java index 9c8b3c0..cc5c5cb 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java @@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.BookStandBlock; import net.dnorberg.rivencraft.block.BookBinderBlock; import net.dnorberg.rivencraft.block.WritingDeskBlock; import net.dnorberg.rivencraft.block.InkMixerBlock; +import net.dnorberg.rivencraft.block.LinkModifierBlock; import net.dnorberg.rivencraft.block.BuddingCrystalBlock; import net.dnorberg.rivencraft.block.StarFissureBlock; import net.dnorberg.rivencraft.block.DecayBlock; @@ -40,6 +41,9 @@ public final class ModBlocks { "ink_mixer", InkMixerBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion() ); + public static final DeferredBlock LINK_MODIFIER = BLOCKS.registerBlock( + "link_modifier", LinkModifierBlock::new, + BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()); public static final DeferredBlock CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block", Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK).mapColor(MapColor.COLOR_BLUE)); public static final DeferredBlock BOOK_RECEPTACLE = BLOCKS.registerBlock("book_receptacle", diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java index 21e3f30..52ca125 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java @@ -27,9 +27,12 @@ public final class ModCreativeTabs { output.accept(ModItems.DESCRIPTIVE_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.NOTEBOOK.get()); + output.accept(ModItems.COLLATION_FOLDER.get()); + output.accept(ModItems.SEALED_NOTEBOOK.get()); output.accept(ModItems.INK_VIAL.get()); output.accept(ModItems.WRITING_DESK.get()); output.accept(ModItems.INK_MIXER.get()); + output.accept(ModItems.LINK_MODIFIER.get()); output.accept(ModItems.BOOK_BINDER.get()); output.accept(ModItems.CRYSTAL_BLOCK.get()); output.accept(ModItems.BOOK_RECEPTACLE.get()); diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java index 8222c15..3292920 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java @@ -5,6 +5,8 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.NotebookItem; +import net.dnorberg.rivencraft.item.CollationFolderItem; +import net.dnorberg.rivencraft.item.SealedNotebookItem; import net.dnorberg.rivencraft.item.InkVialItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.BlockItem; @@ -35,6 +37,10 @@ public final class ModItems { NotebookItem::new, new Item.Properties().stacksTo(1) ); + public static final DeferredItem COLLATION_FOLDER = ITEMS.registerItem( + "collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1)); + public static final DeferredItem SEALED_NOTEBOOK = ITEMS.registerItem( + "sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16)); public static final DeferredItem INK_VIAL = ITEMS.registerItem( "ink_vial", InkVialItem::new, @@ -43,6 +49,7 @@ public final class ModItems { public static final DeferredItem WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK); public static final DeferredItem BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER); public static final DeferredItem INK_MIXER = ITEMS.registerSimpleBlockItem(ModBlocks.INK_MIXER); + public static final DeferredItem LINK_MODIFIER = ITEMS.registerSimpleBlockItem(ModBlocks.LINK_MODIFIER); public static final DeferredItem CRYSTAL_BLOCK = ITEMS.registerSimpleBlockItem(ModBlocks.CRYSTAL_BLOCK); public static final DeferredItem BOOK_RECEPTACLE = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_RECEPTACLE); public static final DeferredItem BUDDING_CRYSTAL = ITEMS.registerSimpleBlockItem(ModBlocks.BUDDING_CRYSTAL); diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java b/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java index a59029a..2b7223a 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java @@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.menu.NotebookMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.BookBinderMenu; import net.dnorberg.rivencraft.menu.InkMixerMenu; +import net.dnorberg.rivencraft.menu.LinkModifierMenu; import net.minecraft.core.registries.Registries; import net.minecraft.world.inventory.MenuType; import net.neoforged.bus.api.IEventBus; @@ -24,6 +25,8 @@ public final class ModMenus { MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new)); public static final DeferredHolder, MenuType> INK_MIXER = MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new)); + public static final DeferredHolder, MenuType> LINK_MODIFIER = + MENUS.register("link_modifier", () -> IMenuTypeExtension.create(LinkModifierMenu::new)); private ModMenus() { } diff --git a/src/main/java/net/dnorberg/rivencraft/writing/WritingDeskCatalog.java b/src/main/java/net/dnorberg/rivencraft/writing/WritingDeskCatalog.java index 7b57ac7..961ca9b 100644 --- a/src/main/java/net/dnorberg/rivencraft/writing/WritingDeskCatalog.java +++ b/src/main/java/net/dnorberg/rivencraft/writing/WritingDeskCatalog.java @@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.item.CollationFolderItem; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; @@ -24,7 +25,7 @@ public final class WritingDeskCatalog { for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) { ItemStack collection = desk.getItem(slot); if (collection.is(ModItems.PAGE.get())) add(unique, collection); - if (collection.is(ModItems.NOTEBOOK.get())) { + if (CollationFolderItem.isCollection(collection)) { List pages = collection.getOrDefault( ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); pages.forEach(page -> add(unique, page)); diff --git a/src/main/resources/assets/rivencraft/blockstates/link_modifier.json b/src/main/resources/assets/rivencraft/blockstates/link_modifier.json new file mode 100644 index 0000000..b19abd3 --- /dev/null +++ b/src/main/resources/assets/rivencraft/blockstates/link_modifier.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=north": { "model": "rivencraft:block/link_modifier" }, + "facing=east": { "model": "rivencraft:block/link_modifier", "y": 90 }, + "facing=south": { "model": "rivencraft:block/link_modifier", "y": 180 }, + "facing=west": { "model": "rivencraft:block/link_modifier", "y": 270 } + } +} diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index be375c5..236ae3f 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -17,6 +17,18 @@ "item.rivencraft.link_property.intra_linking": "Intra Linking", "item.rivencraft.page.unresolved": "Unresolved Symbol Page", "item.rivencraft.notebook": "Notebook", + "item.rivencraft.collation_folder": "Collation Folder", + "item.rivencraft.sealed_notebook": "Sealed Notebook", + "block.rivencraft.link_modifier": "Link Modifier", + "container.rivencraft.link_modifier": "Link Modifier", + "screen.rivencraft.link_modifier.title": "Book title", + "screen.rivencraft.link_modifier.retire": "Retire Age", + "screen.rivencraft.link_modifier.confirm": "Confirm!", + "message.rivencraft.link_modifier.delete.retired": "Age retired. Its terrain was archived outside the active world.", + "message.rivencraft.link_modifier.delete.already_dead": "This Age is already retired.", + "message.rivencraft.link_modifier.delete.occupied": "Cannot retire an Age while players are inside it.", + "message.rivencraft.link_modifier.delete.unload_failed": "Could not unload this Age; its data is unchanged.", + "message.rivencraft.link_modifier.delete.storage_failed": "Could not archive this Age's data; it was not retired.", "item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s", "item.rivencraft.ink_vial": "Ink Vial", diff --git a/src/main/resources/assets/rivencraft/models/block/link_modifier.json b/src/main/resources/assets/rivencraft/models/block/link_modifier.json new file mode 100644 index 0000000..395eb5f --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/block/link_modifier.json @@ -0,0 +1,120 @@ +{ + "parent": "block/block", + "textures": { + "particle": "rivencraft:block/link_modifier_base", + "0": "rivencraft:block/link_modifier_base", + "1": "rivencraft:block/link_modifier_details" + }, + "elements": [ + { + "from": [ 0.0, 0.0, 0.0 ], + "to": [ 16.0, 12.0, 16.0 ], + "faces": { + "north": { "texture": "#0", "uv": [ 0.0, 4.0, 4.0, 7.0 ] }, + "east": { "texture": "#0", "uv": [ 4.0, 4.0, 8.0, 7.0 ] }, + "south": { "texture": "#0", "uv": [ 8.0, 4.0, 12.0, 7.0 ] }, + "west": { "texture": "#0", "uv": [ 12.0, 4.0, 16.0, 7.0 ] }, + "up": { "texture": "#0", "uv": [ 4.0, 0.0, 8.0, 4.0 ] }, + "down": { "texture": "#0", "uv": [ 8.0, 0.0, 12.0, 4.0 ] } + } + }, + { + "from": [ 1.5, 12.0, 11.0 ], + "to": [ 3.5, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 3.7, 12.0, 11.0 ], + "to": [ 5.7, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 5.9, 12.0, 11.0 ], + "to": [ 7.9, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 8.1, 12.0, 11.0 ], + "to": [ 10.1, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 10.3, 12.0, 11.0 ], + "to": [ 12.3, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 12.5, 12.0, 11.0 ], + "to": [ 14.5, 13.0, 13.0 ], + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] }, + "west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] }, + "up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] }, + "down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] } + } + }, + { + "from": [ 2.0, 9.4, 1.0 ], + "to": [ 14.0, 12.4, 8.0 ], + "rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 }, + "faces": { + "north": { "texture": "#1", "uv": [ 0.0, 4.0, 6.0, 5.5 ] }, + "east": { "texture": "#1", "uv": [ 6.0, 4.0, 9.5, 5.5 ] }, + "south": { "texture": "#1", "uv": [ 0.0, 6.0, 6.0, 7.5 ] }, + "west": { "texture": "#1", "uv": [ 6.0, 6.0, 9.5, 7.5 ] }, + "up": { "texture": "#1", "uv": [ 0.0, 0.0, 6.0, 3.5 ] }, + "down": { "texture": "#1", "uv": [ 6.0, 0.0, 12.0, 3.5 ] } + } + }, + { + "from": [ 4.0, 11.5, 3.5 ], + "to": [ 12.0, 15.5, 5.5 ], + "rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 }, + "faces": { + "north": { "texture": "#1", "uv": [ 1.0, 9.0, 5.0, 11.0 ] }, + "east": { "texture": "#1", "uv": [ 0.0, 8.0, 1.0, 11.0 ] }, + "south": { "texture": "#1", "uv": [ 6.0, 9.0, 10.0, 11.0 ] }, + "west": { "texture": "#1", "uv": [ 5.0, 9.0, 6.0, 11.0 ] }, + "up": { "texture": "#1", "uv": [ 1.0, 8.0, 5.0, 9.0 ] }, + "down": { "texture": "#1", "uv": [ 5.0, 8.0, 9.0, 9.0 ] } + } + } + ] +} diff --git a/src/main/resources/assets/rivencraft/models/item/collation_folder.json b/src/main/resources/assets/rivencraft/models/item/collation_folder.json new file mode 100644 index 0000000..d34e9b0 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/item/collation_folder.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:item/generated", + "textures": { "layer0": "rivencraft:item/collation_folder" } +} diff --git a/src/main/resources/assets/rivencraft/models/item/link_modifier.json b/src/main/resources/assets/rivencraft/models/item/link_modifier.json new file mode 100644 index 0000000..fd219d1 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/item/link_modifier.json @@ -0,0 +1 @@ +{ "parent": "rivencraft:block/link_modifier" } diff --git a/src/main/resources/assets/rivencraft/models/item/sealed_notebook.json b/src/main/resources/assets/rivencraft/models/item/sealed_notebook.json new file mode 100644 index 0000000..3089975 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/item/sealed_notebook.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:item/generated", + "textures": { "layer0": "rivencraft:item/sealed_notebook" } +} diff --git a/src/main/resources/assets/rivencraft/textures/block/link_modifier_base.png b/src/main/resources/assets/rivencraft/textures/block/link_modifier_base.png new file mode 100644 index 0000000..be4e424 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/block/link_modifier_base.png differ diff --git a/src/main/resources/assets/rivencraft/textures/block/link_modifier_details.png b/src/main/resources/assets/rivencraft/textures/block/link_modifier_details.png new file mode 100644 index 0000000..05102fb Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/block/link_modifier_details.png differ diff --git a/src/main/resources/assets/rivencraft/textures/item/collation_folder.png b/src/main/resources/assets/rivencraft/textures/item/collation_folder.png new file mode 100644 index 0000000..f8b27f4 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/item/collation_folder.png differ diff --git a/src/main/resources/assets/rivencraft/textures/item/sealed_notebook.png b/src/main/resources/assets/rivencraft/textures/item/sealed_notebook.png new file mode 100644 index 0000000..6457693 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/item/sealed_notebook.png differ diff --git a/src/main/resources/data/rivencraft/loot_table/blocks/link_modifier.json b/src/main/resources/data/rivencraft/loot_table/blocks/link_modifier.json new file mode 100644 index 0000000..3564eb1 --- /dev/null +++ b/src/main/resources/data/rivencraft/loot_table/blocks/link_modifier.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:link_modifier" }] }] +} diff --git a/src/main/resources/data/rivencraft/loot_table/chests/mystcraft_library.json b/src/main/resources/data/rivencraft/loot_table/chests/mystcraft_library.json index b72ea94..9bdf5fb 100644 --- a/src/main/resources/data/rivencraft/loot_table/chests/mystcraft_library.json +++ b/src/main/resources/data/rivencraft/loot_table/chests/mystcraft_library.json @@ -9,7 +9,7 @@ { "type": "minecraft:item", "name": "minecraft:leather", "weight": 5, "functions": [{ "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 1, "max": 3 } }] }, { "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 4 }, - { "type": "minecraft:item", "name": "minecraft:book", "weight": 3 }, + { "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 20 }, { "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 } ] }, @@ -17,7 +17,7 @@ "rolls": { "type": "minecraft:uniform", "min": 1, "max": 2 }, "entries": [ { "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 3 }, - { "type": "minecraft:item", "name": "minecraft:book", "weight": 2 }, + { "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 2 }, { "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 } ] }