diff --git a/README.md b/README.md index 6fb3362..8372083 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,14 @@ Inputs are consumed only when the result is taken. The completed book receives its Age identity when assembled. With only a Link Panel, it describes a randomly generated Age. Its writing is stored on the -book; Milestone 5 will interpret additional symbols during Age generation. +book; the Descriptive Book screen's turned pages show ordered symbols and +unresolved IDs. Until Milestone 5, symbols do not affect generation and written +books use the same random Age fallback. The Descriptive Book screen follows +Legacy's book spread: the title and Age details are on the left, the destination +preview is on the right, and clicks in the bottom corners turn pages. Ordered +symbol pages appear before the final blank spread; their pictograms await +Milestone 5. Link-panel-only books show their details in the enchantment-table +font. ## Linking Book @@ -84,6 +91,8 @@ its preview panel is clicked. The destination is stored on the item and survives inventory and world saves. Travel only succeeds if the target dimension is loaded and a non-fluid, passable landing position can be found nearby. +The Linking Book uses the same Legacy page layout and bottom-corner page turns +as the Descriptive Book, but without the Descriptive Book's gold cover edges. Successful book travel plays Mystcraft Legacy's original linking sound at both the departure and arrival locations. diff --git a/dev images/Pasted image (2).png b/dev images/Pasted image (2).png deleted file mode 100644 index 9f5d330..0000000 Binary files a/dev images/Pasted image (2).png and /dev/null differ diff --git a/dev images/Pasted image (3).png b/dev images/Pasted image (3).png deleted file mode 100644 index 41f2ed0..0000000 Binary files a/dev images/Pasted image (3).png and /dev/null differ diff --git a/dev images/Pasted image.png b/dev images/Pasted image.png deleted file mode 100644 index e525c44..0000000 Binary files a/dev images/Pasted image.png and /dev/null differ diff --git a/dev images/first page.png b/dev images/first page.png new file mode 100644 index 0000000..f292897 Binary files /dev/null and b/dev images/first page.png differ diff --git a/dev images/last page.png b/dev images/last page.png new file mode 100644 index 0000000..dda775e Binary files /dev/null and b/dev images/last page.png differ diff --git a/dev images/middle of book.png b/dev images/middle of book.png new file mode 100644 index 0000000..a6a236c Binary files /dev/null and b/dev images/middle of book.png differ diff --git a/docs/ASSET_PROVENANCE.md b/docs/ASSET_PROVENANCE.md index df1f1d0..3b6abc8 100644 --- a/docs/ASSET_PROVENANCE.md +++ b/docs/ASSET_PROVENANCE.md @@ -8,6 +8,10 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license. | --- | --- | --- | --- | | `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` | +| `assets/rivencraft/textures/gui/bookui_cover.png` | `assets/mystcraft/gui/bookui_cover.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; shared book cover, with gold edges drawn only for Descriptive Books | +| `assets/rivencraft/textures/gui/bookui_pagel.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; turned left page | +| `assets/rivencraft/textures/gui/bookui_pager.png` | `assets/mystcraft/gui/bookui_pager.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; link-panel frame on the first right page | +| `assets/rivencraft/textures/gui/bookui_rpage_full.png` | `assets/mystcraft/gui/bookui_rpage_full.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; full right written page | | `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/block/writing_desk.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/gui/writing_desk_legacy.png` | `assets/mystcraft/gui/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; full desk panel, tabs, and cycling controls at original atlas coordinates | diff --git a/docs/WRITING_DATA.md b/docs/WRITING_DATA.md index 5bc9db9..e39d0f7 100644 --- a/docs/WRITING_DATA.md +++ b/docs/WRITING_DATA.md @@ -40,6 +40,18 @@ a symbol reference or a baseline link-panel marker. `OrderedWriting` retains up to 256 entries in exact player-authored order. All three contracts have both persistent codecs and bounded network codecs. +Assembled Descriptive Books store `OrderedWriting` schema version 1 alongside +their Age reference. Books assembled in 0.3.7.2 saved writing as a bare list; +that form still loads as version 1 without reordering or dropping unknown IDs. +Older page-free books without writing continue to work unchanged. Copying a +book copies its writing and Age reference; merely copying it never creates a +new Age. The Book Binder creates the Age when its finished output is taken. +The Descriptive Book screen can inspect the ordered pages by turning them from +the bottom corners and identifies unresolved IDs using server-provided symbol +metadata. Until Milestone 5, the +saved symbols are informational only: Age creation uses the existing random +fallback and does not apply them to generation. + Removing a data pack never removes its IDs from saved writing. A missing ID is resolved as client-safe **Unresolved Symbol** display metadata while retaining the original namespaced ID for diagnostics and later restoration. This is diff --git a/gradle.properties b/gradle.properties index c3b3bf7..8ba39dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,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.3.7.2 +mod_version=0.3.8.2 immersive_portals_version=6.0.7 # 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. diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java index f1bd602..e5c78f9 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java @@ -92,8 +92,8 @@ public final class ClientBookInteractionHandler { return; } minecraft.setScreen(payload.fromStand() - ? new DescriptiveBookScreen(payload.source().pos(), payload.book(), payload.traits()) - : new DescriptiveBookScreen(hand, payload.book(), payload.traits())); + ? new DescriptiveBookScreen(payload.source().pos(), payload.book(), payload.traits(), payload.writingSymbols()) + : new DescriptiveBookScreen(hand, payload.book(), payload.traits(), payload.writingSymbols())); }); } } 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 62496c3..249d5b1 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java @@ -1,5 +1,7 @@ package net.dnorberg.rivencraft.client.screen; +import java.util.List; +import java.util.Locale; import net.dnorberg.rivencraft.book.AgeReference; import net.dnorberg.rivencraft.book.AgeTraits; import net.dnorberg.rivencraft.client.ClientBookExploreOverlay; @@ -10,6 +12,9 @@ import net.dnorberg.rivencraft.network.RequestLinkPreviewPayload; import net.dnorberg.rivencraft.network.LinkPreviewPayload; import net.dnorberg.rivencraft.network.ReleaseLinkPreviewPayload; import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.writing.OrderedWriting; +import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; +import net.dnorberg.rivencraft.writing.WritingEntry; import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer; import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderers; import net.minecraft.core.BlockPos; @@ -25,47 +30,56 @@ import net.minecraft.world.InteractionHand; import net.neoforged.neoforge.network.PacketDistributor; import org.jetbrains.annotations.Nullable; -import java.util.Locale; - public final class DescriptiveBookScreen extends Screen implements ExplorableBookScreen { private static final int BOOK_TEXT_COLOR = 0xFF3B281B; - private static final ResourceLocation BOOK_TEXTURE = ResourceLocation.fromNamespaceAndPath( - "rivencraft", "textures/gui/linking_book.png" - ); + private static final int UNRESOLVED_TEXT_COLOR = 0xFF9B302C; + private static final ResourceLocation ENCHANTMENT_FONT = ResourceLocation.withDefaultNamespace("alt"); private static final int BOOK_WIDTH = 327; private static final int BOOK_HEIGHT = 199; - private static final int TEXTURE_WIDTH = 512; - private static final int TEXTURE_HEIGHT = 312; - private static final int LINK_PANEL_X = 28; - private static final int LINK_PANEL_Y = 35; - private static final int LINK_PANEL_WIDTH = 126; - private static final int LINK_PANEL_HEIGHT = 86; + private static final int LINK_PANEL_X = 173; + private static final int LINK_PANEL_Y = 20; + private static final int LINK_PANEL_WIDTH = 132; + private static final int LINK_PANEL_HEIGHT = 83; private static final Component EXPLORE_ICON = Component.literal("👁"); private final AgeReference age; private final ResourceLocation generationProfile; private final AgeTraits traits; + private final OrderedWriting writing; + private final List writingSymbols; + private final boolean pageFree; private final InteractionHand hand; private final BlockPos bookStandPos; private DestinationPreviewRenderer preview; private EditBox titleInput; private Button exploreButton; + private Button createButton; + private Button cancelButton; + private int pageIndex; private boolean movingToExploreOverlay; - public DescriptiveBookScreen(InteractionHand hand, ItemStack book, @Nullable AgeTraits traits) { + public DescriptiveBookScreen(InteractionHand hand, ItemStack book, @Nullable AgeTraits traits, + List writingSymbols) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); this.traits = traits; + writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); + this.writingSymbols = List.copyOf(writingSymbols); + pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL); this.hand = hand; bookStandPos = null; } - public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book, @Nullable AgeTraits traits) { + public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book, @Nullable AgeTraits traits, + List writingSymbols) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); this.traits = traits; + writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); + this.writingSymbols = List.copyOf(writingSymbols); + pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL); hand = InteractionHand.MAIN_HAND; this.bookStandPos = bookStandPos.immutable(); } @@ -75,19 +89,22 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo int left = (width - BOOK_WIDTH) / 2; int top = (height - BOOK_HEIGHT) / 2; if (age == null) { - int rightPage = left + 177; - titleInput = new EditBox(font, rightPage, top + 69, 124, 20, + int leftPage = left + 40; + titleInput = new EditBox(font, leftPage, top + 69, 110, 20, Component.translatable("screen.rivencraft.age.title")); titleInput.setMaxLength(ModNetworking.MAX_BOOK_TITLE_LENGTH); titleInput.setHint(Component.translatable("screen.rivencraft.age.title_hint")); addRenderableWidget(titleInput); setInitialFocus(titleInput); - addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.age.create"), button -> createAge()) - .bounds(rightPage, top + 100, 59, 20) + createButton = addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.age.create"), button -> createAge()) + .bounds(leftPage, top + 100, 53, 20) .build()); - addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose()) - .bounds(rightPage + 65, top + 100, 59, 20) + cancelButton = addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose()) + .bounds(leftPage + 57, top + 100, 53, 20) .build()); + titleInput.visible = pageIndex == 0; + createButton.visible = pageIndex == 0; + cancelButton.visible = pageIndex == 0; } else { boolean requestPreview = preview == null; if (requestPreview) { @@ -96,8 +113,9 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo if (preview.supportsExploration()) { exploreButton = addRenderableWidget(Button.builder( EXPLORE_ICON, button -> enterExploreOverlay()) - .bounds(left + 81, top + 145, 20, 20).build()); + .bounds(left + 229, top + 145, 20, 20).build()); exploreButton.active = bookStandPos == null; + exploreButton.visible = pageIndex == 0; } if (requestPreview) { PacketDistributor.sendToServer(new RequestLinkPreviewPayload( @@ -137,45 +155,95 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo private void renderBook(GuiGraphics graphics, float partialTick) { int left = (width - BOOK_WIDTH) / 2; int top = (height - BOOK_HEIGHT) / 2; - graphics.blit(BOOK_TEXTURE, left, top, BOOK_WIDTH, BOOK_HEIGHT, 0, 0, - TEXTURE_WIDTH, TEXTURE_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); + LegacyBookArtwork.drawCover(graphics, left, top, true); + if (pageIndex > 0) { + LegacyBookArtwork.drawLeftPage(graphics, left, top); + } + if (pageIndex == 0) { + if (age != null && preview != null) { + preview.render(graphics, left + LINK_PANEL_X, top + LINK_PANEL_Y, + LINK_PANEL_WIDTH, LINK_PANEL_HEIGHT, partialTick); + } + LegacyBookArtwork.drawPanelPage(graphics, left, top); + graphics.enableScissor(left + 38, top + 38, left + 154, top + 174); + renderFirstPageText(graphics, left, top); + graphics.disableScissor(); + } else if (pageIndex < pageCount()) { + LegacyBookArtwork.drawFullPage(graphics, left, top); + renderWrittenPage(graphics, left, top); + } + String counter = pageIndex + "/" + pageCount(); + graphics.drawString(font, counter, left + 165 - font.width(counter) / 2, + top + 185, 0xFF000000, false); + } + + private int pageCount() { + // Older page-free Descriptive Books do not have a BOOK_WRITING component. + return Math.max(1, writing.entries().size()); + } + + private void renderFirstPageText(GuiGraphics graphics, int left, int top) { + int x = left + 40; + drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.title"), + x, top + 40, 110); if (age == null) { drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.name_prompt"), - left + 177, top + 45, 124); - } else { - preview.render(graphics, left + LINK_PANEL_X, top + LINK_PANEL_Y, LINK_PANEL_WIDTH, LINK_PANEL_HEIGHT, partialTick); - int detailsY = drawWrappedWithoutShadow( - graphics, Component.literal(age.name()), left + 177, top + 43, 124) + 3; - detailsY = Math.max(detailsY, top + 72); - if (generationProfile != null) { - Component profileName = generationProfile.getNamespace().equals("rivencraft") - ? Component.translatable("generator.rivencraft." + generationProfile.getPath()) - : Component.literal(generationProfile.toString()); - detailsY = drawWrappedWithoutShadow(graphics, - Component.translatable("screen.rivencraft.age.generator", profileName), - left + 177, detailsY, 124) + 2; - } - if (traits != null) { - detailsY = drawTrait(graphics, "biomes", traitName("biome_distribution", traits.biomeDistribution()), - left + 177, detailsY); - detailsY = drawTrait(graphics, "lighting", traitName("lighting", traits.lighting()), - left + 177, detailsY); - detailsY = drawTrait(graphics, "weather", traitName("weather", traits.weather()), - left + 177, detailsY); - Component dayLength = traits.hasStoppedCelestialCycle() - ? Component.translatable("trait.rivencraft.day_length.stopped") - : Component.literal(String.format(Locale.ROOT, "%.2fx", traits.celestialPeriod())); - detailsY = drawTrait(graphics, "day_length", dayLength, left + 177, detailsY); - detailsY = drawTrait(graphics, "celestial_angle", - Component.literal(String.format(Locale.ROOT, "%.0f°", traits.celestialAngle())), - left + 177, detailsY); - drawTrait(graphics, "celestial_phase", - Component.literal(String.format(Locale.ROOT, "%.0f%%", traits.celestialPhase() * 100.0F)), - left + 177, detailsY); - } + x, top + 52, 110); + return; } + int detailsY = drawWrappedWithoutShadow(graphics, Component.literal(age.name()), + x + 10, top + 52, 100) + 10; + if (generationProfile != null) { + Component profileName = generationProfile.getNamespace().equals("rivencraft") + ? Component.translatable("generator.rivencraft." + generationProfile.getPath()) + : Component.literal(generationProfile.toString()); + detailsY = drawWrappedWithoutShadow(graphics, + Component.translatable("screen.rivencraft.age.generator", profileName), + x, detailsY, 110) + 2; + } + if (traits != null) { + detailsY = drawTrait(graphics, "biomes", traitName("biome_distribution", traits.biomeDistribution()), x, detailsY); + detailsY = drawTrait(graphics, "lighting", traitName("lighting", traits.lighting()), x, detailsY); + detailsY = drawTrait(graphics, "weather", traitName("weather", traits.weather()), x, detailsY); + Component dayLength = traits.hasStoppedCelestialCycle() + ? Component.translatable("trait.rivencraft.day_length.stopped") + : Component.literal(String.format(Locale.ROOT, "%.2fx", traits.celestialPeriod())); + detailsY = drawTrait(graphics, "day_length", dayLength, x, detailsY); + detailsY = drawTrait(graphics, "celestial_angle", + Component.literal(String.format(Locale.ROOT, "%.0f°", traits.celestialAngle())), x, detailsY); + drawTrait(graphics, "celestial_phase", + Component.literal(String.format(Locale.ROOT, "%.0f%%", traits.celestialPhase() * 100.0F)), x, detailsY); + } + } + private void renderWrittenPage(GuiGraphics graphics, int left, int top) { + WritingEntry entry = writing.entries().get(pageIndex); + int x = left + 176; + if (entry.kind() == WritingEntry.Kind.LINK_PANEL) { + drawWrappedWithoutShadow(graphics, Component.translatable("item.rivencraft.page.link_panel"), + x, top + 39, 126); + return; + } + int symbolIndex = (int) writing.entries().subList(0, pageIndex + 1).stream() + .filter(candidate -> candidate.kind() == WritingEntry.Kind.SYMBOL).count() - 1; + SymbolDisplayMetadata display = symbolIndex < writingSymbols.size() + ? writingSymbols.get(symbolIndex) : SymbolDisplayMetadata.unresolved(entry.symbol().orElseThrow()); + int color = display.resolved() ? BOOK_TEXT_COLOR : UNRESOLVED_TEXT_COLOR; + drawWrappedWithoutShadow(graphics, Component.translatable(display.nameTranslationKey()), + x, top + 39, 126, color); + drawWrappedWithoutShadow(graphics, Component.literal(display.id().toString()), + x, top + 64, 126, color); + drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.pages_not_applied"), + x, top + 137, 126); + } + + private void turnPage(int direction) { + pageIndex = Math.max(0, Math.min(pageCount(), pageIndex + direction)); + if (titleInput != null) titleInput.visible = pageIndex == 0; + if (createButton != null) createButton.visible = pageIndex == 0; + if (cancelButton != null) cancelButton.visible = pageIndex == 0; + if (exploreButton != null) exploreButton.visible = pageIndex == 0; } @Override @@ -201,14 +269,25 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo resetExploration(); return true; } - if (age != null && button == 0) { - int panelLeft = (width - BOOK_WIDTH) / 2 + LINK_PANEL_X; - int panelTop = (height - BOOK_HEIGHT) / 2 + LINK_PANEL_Y; - if (mouseX >= panelLeft && mouseX < panelLeft + LINK_PANEL_WIDTH - && mouseY >= panelTop && mouseY < panelTop + LINK_PANEL_HEIGHT) { + if (button == 0) { + int left = (width - BOOK_WIDTH) / 2; + int top = (height - BOOK_HEIGHT) / 2; + if (pageIndex == 0 && age != null + && mouseX >= left + LINK_PANEL_X && mouseX < left + LINK_PANEL_X + LINK_PANEL_WIDTH + && mouseY >= top + LINK_PANEL_Y && mouseY < top + LINK_PANEL_Y + LINK_PANEL_HEIGHT) { link(); return true; } + if (mouseY >= top + 164 && mouseY < top + 195) { + if (pageIndex > 0 && mouseX >= left + 7 && mouseX < left + 35) { + turnPage(-1); + return true; + } + if (pageIndex < pageCount() && mouseX >= left + 293 && mouseX < left + 320) { + turnPage(1); + return true; + } + } } return super.mouseClicked(mouseX, mouseY, button); } @@ -268,7 +347,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo int top = (height - BOOK_HEIGHT) / 2; graphics.drawCenteredString(font, Component.translatable("screen.rivencraft.book.return_from_explore"), - left + 91, top + 154, BOOK_TEXT_COLOR); + left + 239, top + 154, BOOK_TEXT_COLOR); } @Override @@ -317,7 +396,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo private int drawTrait(GuiGraphics graphics, String key, Component value, int x, int y) { return drawWrappedWithoutShadow(graphics, - Component.translatable("screen.rivencraft.age.trait." + key, value), x, y, 124) + 2; + Component.translatable("screen.rivencraft.age.trait." + key, value), x, y, 110) + 2; } private static Component traitName(String group, Enum value) { @@ -326,8 +405,14 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo } private int drawWrappedWithoutShadow(GuiGraphics graphics, Component text, int x, int y, int maxWidth) { - for (FormattedCharSequence line : font.split(text, maxWidth)) { - graphics.drawString(font, line, x, y, BOOK_TEXT_COLOR, false); + return drawWrappedWithoutShadow(graphics, text, x, y, maxWidth, BOOK_TEXT_COLOR); + } + + private int drawWrappedWithoutShadow(GuiGraphics graphics, Component text, int x, int y, int maxWidth, int color) { + Component displayed = pageFree && age != null && pageIndex == 0 + ? text.copy().withStyle(style -> style.withFont(ENCHANTMENT_FONT)) : text; + for (FormattedCharSequence line : font.split(displayed, maxWidth)) { + graphics.drawString(font, line, x, y, color, false); y += font.lineHeight; } return y; diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LegacyBookArtwork.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LegacyBookArtwork.java new file mode 100644 index 0000000..2f8d7f2 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LegacyBookArtwork.java @@ -0,0 +1,48 @@ +package net.dnorberg.rivencraft.client.screen; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +/** Original Mystcraft Legacy book atlases and page composition (LGPL-3.0, + * copyright XCompWiz, revision 9bc8ddc061845df0cd5ea47f4fdef773cff786ae). */ +final class LegacyBookArtwork { + private static final int ATLAS_SIZE = 256; + private static final ResourceLocation COVER = texture("bookui_cover.png"); + private static final ResourceLocation LEFT_PAGE = texture("bookui_pagel.png"); + private static final ResourceLocation RIGHT_PANEL = texture("bookui_pager.png"); + private static final ResourceLocation RIGHT_FULL = texture("bookui_rpage_full.png"); + + private LegacyBookArtwork() {} + + static void drawCover(GuiGraphics graphics, int left, int top, boolean gilded) { + blit(graphics, COVER, left, top + 7, 34, 192, 152, 0); + blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0); + blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0); + blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0); + if (gilded) { + blit(graphics, COVER, left, top + 7, 34, 192, 186, 0); + blit(graphics, COVER, left + 293, top + 7, 34, 192, 186, 0); + } + } + + static void drawLeftPage(GuiGraphics graphics, int left, int top) { + blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0); + } + + static void drawPanelPage(GuiGraphics graphics, int left, int top) { + blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0); + } + + static void drawFullPage(GuiGraphics graphics, int left, int top) { + blit(graphics, RIGHT_FULL, left + 163, top, 156, 195, 0, 0); + } + + private static ResourceLocation texture(String filename) { + return ResourceLocation.fromNamespaceAndPath("rivencraft", "textures/gui/" + filename); + } + + private static void blit(GuiGraphics graphics, ResourceLocation texture, int x, int y, + int width, int height, int u, int v) { + graphics.blit(texture, x, y, width, height, u, v, width, height, ATLAS_SIZE, ATLAS_SIZE); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java index bf13a77..41e5654 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java @@ -18,8 +18,9 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.util.FormattedCharSequence; import net.minecraft.world.InteractionHand; import net.minecraft.world.item.ItemStack; @@ -27,17 +28,12 @@ import net.neoforged.neoforge.network.PacketDistributor; public final class LinkingBookScreen extends Screen implements ExplorableBookScreen { private static final int BOOK_TEXT_COLOR = 0xFF3B281B; - private static final ResourceLocation BOOK_TEXTURE = ResourceLocation.fromNamespaceAndPath( - "rivencraft", "textures/gui/linking_book.png" - ); private static final int BOOK_WIDTH = 327; private static final int BOOK_HEIGHT = 199; - private static final int TEXTURE_WIDTH = 512; - private static final int TEXTURE_HEIGHT = 312; - private static final int PREVIEW_X = 28; - private static final int PREVIEW_Y = 35; - private static final int PREVIEW_WIDTH = 126; - private static final int PREVIEW_HEIGHT = 86; + private static final int PREVIEW_X = 173; + private static final int PREVIEW_Y = 20; + private static final int PREVIEW_WIDTH = 132; + private static final int PREVIEW_HEIGHT = 83; private static final Component EXPLORE_ICON = Component.literal("👁"); private final InteractionHand hand; @@ -47,6 +43,9 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr private DestinationPreviewRenderer preview; private EditBox titleInput; private Button exploreButton; + private Button bindButton; + private Button cancelButton; + private int pageIndex; private boolean movingToExploreOverlay; public LinkingBookScreen(InteractionHand hand, ItemStack book) { @@ -70,19 +69,22 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr int left = (width - BOOK_WIDTH) / 2; int top = (height - BOOK_HEIGHT) / 2; if (target == null) { - int rightPage = left + 177; - titleInput = new EditBox(font, rightPage, top + 69, 124, 20, + int leftPage = left + 40; + titleInput = new EditBox(font, leftPage, top + 69, 110, 20, Component.translatable("screen.rivencraft.book.title")); titleInput.setMaxLength(ModNetworking.MAX_BOOK_TITLE_LENGTH); titleInput.setHint(Component.translatable("screen.rivencraft.book.title_hint")); addRenderableWidget(titleInput); setInitialFocus(titleInput); - addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.bind"), button -> bind()) - .bounds(rightPage, top + 100, 59, 20) + bindButton = addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.bind"), button -> bind()) + .bounds(leftPage, top + 100, 53, 20) .build()); - addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose()) - .bounds(rightPage + 65, top + 100, 59, 20) + cancelButton = addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose()) + .bounds(leftPage + 57, top + 100, 53, 20) .build()); + titleInput.visible = pageIndex == 0; + bindButton.visible = pageIndex == 0; + cancelButton.visible = pageIndex == 0; } else { boolean requestPreview = preview == null; if (requestPreview) { @@ -91,8 +93,9 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr if (preview.supportsExploration()) { exploreButton = addRenderableWidget(Button.builder( EXPLORE_ICON, button -> enterExploreOverlay()) - .bounds(left + 81, top + 145, 20, 20).build()); + .bounds(left + 229, top + 145, 20, 20).build()); exploreButton.active = bookStandPos == null; + exploreButton.visible = pageIndex == 0; } if (requestPreview) { PacketDistributor.sendToServer(new RequestLinkPreviewPayload( @@ -133,28 +136,42 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr private void renderBook(GuiGraphics graphics, float partialTick) { int left = (width - BOOK_WIDTH) / 2; int top = (height - BOOK_HEIGHT) / 2; - graphics.blit(BOOK_TEXTURE, left, top, BOOK_WIDTH, BOOK_HEIGHT, 0, 0, - TEXTURE_WIDTH, TEXTURE_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT); - - if (target == null) { - drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.book.name_prompt"), - left + 177, top + 45, 124); + LegacyBookArtwork.drawCover(graphics, left, top, false); + if (pageIndex > 0) { + LegacyBookArtwork.drawLeftPage(graphics, left, top); } else { - int previewLeft = left + PREVIEW_X; - int previewTop = top + PREVIEW_Y; - preview.render(graphics, previewLeft, previewTop, PREVIEW_WIDTH, PREVIEW_HEIGHT, partialTick); - - int rightCenter = left + 239; - drawWrappedWithoutShadow(graphics, title, left + 177, top + 43, 124); - BlockPos pos = target.location().pos(); - drawCenteredWithoutShadow(graphics, - LinkingBookItem.destinationName(book, target), rightCenter, top + 76); - drawCenteredWithoutShadow(graphics, - Component.literal(pos.getX() + ", " + pos.getY() + ", " + pos.getZ()), rightCenter, top + 89); - drawCenteredWithoutShadow(graphics, - Component.translatable("screen.rivencraft.book.click_to_link"), rightCenter, top + 119); + if (target != null && preview != null) { + preview.render(graphics, left + PREVIEW_X, top + PREVIEW_Y, + PREVIEW_WIDTH, PREVIEW_HEIGHT, partialTick); + } + LegacyBookArtwork.drawPanelPage(graphics, left, top); + graphics.enableScissor(left + 38, top + 38, left + 154, top + 174); + if (target == null) { + drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.book.name_prompt"), + left + 40, top + 43, 110); + } else { + int x = left + 40; + Component customName = book.get(DataComponents.CUSTOM_NAME); + boolean returnLink = customName != null + && customName.getContents() instanceof TranslatableContents translated + && translated.getKey().equals("item.rivencraft.linking_book.return"); + if (!returnLink) { + drawWrappedWithoutShadow(graphics, title, x, top + 40, 110); + } + int destinationY = returnLink ? top + 40 : top + 58; + drawWrappedWithoutShadow(graphics, LinkingBookItem.destinationName(book, target), + x + 10, destinationY, 100); + BlockPos pos = target.location().pos(); + drawWrappedWithoutShadow(graphics, + Component.literal(pos.getX() + ", " + pos.getY() + ", " + pos.getZ()), + x, destinationY + 28, 110); + } + graphics.disableScissor(); } + String counter = pageIndex + "/1"; + graphics.drawString(font, counter, left + 165 - font.width(counter) / 2, + top + 185, 0xFF000000, false); } @Override @@ -163,18 +180,37 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr resetExploration(); return true; } - if (target != null && button == 0) { - int left = (width - BOOK_WIDTH) / 2 + PREVIEW_X; - int top = (height - BOOK_HEIGHT) / 2 + PREVIEW_Y; - if (mouseX >= left && mouseX < left + PREVIEW_WIDTH - && mouseY >= top && mouseY < top + PREVIEW_HEIGHT) { + if (button == 0) { + int left = (width - BOOK_WIDTH) / 2; + int top = (height - BOOK_HEIGHT) / 2; + if (pageIndex == 0 && target != null + && mouseX >= left + PREVIEW_X && mouseX < left + PREVIEW_X + PREVIEW_WIDTH + && mouseY >= top + PREVIEW_Y && mouseY < top + PREVIEW_Y + PREVIEW_HEIGHT) { link(); return true; } + if (mouseY >= top + 164 && mouseY < top + 195) { + if (pageIndex > 0 && mouseX >= left + 7 && mouseX < left + 35) { + turnPage(0); + return true; + } + if (pageIndex == 0 && mouseX >= left + 293 && mouseX < left + 320) { + turnPage(1); + return true; + } + } } return super.mouseClicked(mouseX, mouseY, button); } + private void turnPage(int page) { + pageIndex = page; + if (titleInput != null) titleInput.visible = pageIndex == 0; + if (bindButton != null) bindButton.visible = pageIndex == 0; + if (cancelButton != null) cancelButton.visible = pageIndex == 0; + if (exploreButton != null) exploreButton.visible = pageIndex == 0; + } + @Override public void removed() { if (preview != null && !movingToExploreOverlay) { @@ -247,7 +283,7 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr int top = (height - BOOK_HEIGHT) / 2; drawCenteredWithoutShadow(graphics, Component.translatable("screen.rivencraft.book.return_from_explore"), - left + 91, top + 154); + left + 239, top + 154); } @Override diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/BookBinderGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/BookBinderGameTests.java index 36e6fc6..a87de4d 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/BookBinderGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/BookBinderGameTests.java @@ -99,6 +99,8 @@ public final class BookBinderGameTests { helper.assertTrue(age != null, "Assembled book must already be a written Age book"); OrderedWriting writing = book.get(ModDataComponents.BOOK_WRITING.get()); helper.assertTrue(writing != null, "Assembled book must keep its writing"); + helper.assertValueEqual(writing.schemaVersion(), OrderedWriting.CURRENT_SCHEMA_VERSION, + "Assembled writing must use the current schema"); helper.assertValueEqual(writing.entries().size(), 3, "Link Panel and two pages must be present"); helper.assertValueEqual(writing.entries().get(0).kind(), WritingEntry.Kind.LINK_PANEL, "Link Panel must be first"); @@ -108,6 +110,15 @@ public final class BookBinderGameTests { "rivencraft:weather/normal", "Weather page order must be preserved"); helper.assertTrue(AgeRegistrySavedData.get(helper.getLevel()).find(age.id()).isPresent(), "Taking the completed book must register its random Age definition"); + ItemStack copy = book.copy(); + helper.assertValueEqual(copy.get(ModDataComponents.BOOK_WRITING.get()), writing, + "A book copy must retain identical ordered writing"); + helper.assertValueEqual(copy.get(ModDataComponents.AGE_REFERENCE.get()), age, + "A book copy must retain the same Age rather than assigning a new one"); + var savedBook = (net.minecraft.nbt.CompoundTag) copy.saveOptional(helper.getLevel().registryAccess()); + ItemStack restoredBook = ItemStack.parseOptional(helper.getLevel().registryAccess(), savedBook); + helper.assertValueEqual(restoredBook.get(ModDataComponents.BOOK_WRITING.get()), writing, + "Versioned ordered writing must survive full book save/load"); helper.assertTrue(binder.getItem(BookBinderBlockEntity.COVER).isEmpty(), "Cover must be consumed once"); helper.assertTrue(binder.getItem(BookBinderBlockEntity.LINK_PANEL).isEmpty(), "Link Panel must be consumed once"); helper.assertTrue(binder.getItem(BookBinderBlockEntity.FIRST_PAGE).isEmpty(), "Pages must be consumed once"); diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java index efa7958..253e33e 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java @@ -46,6 +46,8 @@ public final class WritingDataGameTests { )); var json = OrderedWriting.CODEC.encodeStart(JsonOps.INSTANCE, original).result().orElseThrow(); + helper.assertValueEqual(json.getAsJsonObject().get("schema_version").getAsInt(), + OrderedWriting.CURRENT_SCHEMA_VERSION, "Writing must save an explicit schema version"); OrderedWriting persistentCopy = OrderedWriting.CODEC.parse(JsonOps.INSTANCE, json).result().orElseThrow(); helper.assertValueEqual(persistentCopy, original, "Persistent writing codec must retain entry order and unknown symbol IDs"); @@ -58,6 +60,24 @@ public final class WritingDataGameTests { helper.succeed(); } + @GameTest(template = "empty") + public static void oldBareListWritingAndPageFreeBooksRemainReadable(GameTestHelper helper) { + OrderedWriting writing = new OrderedWriting(List.of( + WritingEntry.linkPanel(), WritingEntry.symbol(reference("example:removed_symbol")))); + var oldJson = WritingEntry.CODEC.listOf().encodeStart(JsonOps.INSTANCE, writing.entries()) + .result().orElseThrow(); + OrderedWriting migrated = OrderedWriting.CODEC.parse(JsonOps.INSTANCE, oldJson).result().orElseThrow(); + helper.assertValueEqual(migrated, writing, + "0.3.7.2 bare-list writing must load into schema version 1 intact"); + + ItemStack oldPageFreeBook = new ItemStack(ModItems.DESCRIPTIVE_BOOK.get()); + CompoundTag saved = (CompoundTag) oldPageFreeBook.saveOptional(helper.getLevel().registryAccess()); + ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), saved); + helper.assertValueEqual(restored.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY), + OrderedWriting.EMPTY, "Older page-free books must not require writing migration"); + helper.succeed(); + } + @GameTest(template = "empty") public static void serverCatalogResolvesBuiltinsAndKeepsUnknownReferencesReadable(GameTestHelper helper) { SymbolReference known = reference("rivencraft:terrain/standard"); diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 74a94f3..c34946a 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -16,6 +16,10 @@ import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.menu.BookBinderMenu; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.writing.OrderedWriting; +import net.dnorberg.rivencraft.writing.SymbolCatalog; +import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; +import net.dnorberg.rivencraft.writing.WritingEntry; import net.minecraft.core.component.DataComponents; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; @@ -50,7 +54,7 @@ public final class ModNetworking { } public static void register(RegisterPayloadHandlersEvent event) { - PayloadRegistrar registrar = event.registrar("12"); + PayloadRegistrar registrar = event.registrar("13"); registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, ModNetworking::requestDescriptiveBookScreen); registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, @@ -103,10 +107,15 @@ public final class ModNetworking { // Requests run on the same server thread as Age creation. A request queued // during generation can only open the finished book, never a stale client copy. player.inventoryMenu.broadcastChanges(); + List writingSymbols = access.stack() + .getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY).entries().stream() + .filter(entry -> entry.kind() == WritingEntry.Kind.SYMBOL) + .map(entry -> SymbolCatalog.display(entry.symbol().orElseThrow())) + .toList(); PacketDistributor.sendToPlayer(player, new OpenDescriptiveBookScreenPayload( payload.fromStand(), payload.offhand(), GlobalPos.of(player.level().dimension(), payload.standPos()), access.stack().copy(), - definition == null ? null : definition.traits())); + definition == null ? null : definition.traits(), writingSymbols)); } private static void bindDescriptiveBook(BindDescriptiveBookPayload payload, IPayloadContext context) { diff --git a/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java b/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java index db9cfb8..ff0bf71 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java +++ b/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java @@ -1,7 +1,11 @@ package net.dnorberg.rivencraft.network; +import java.util.ArrayList; +import java.util.List; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.book.AgeTraits; +import net.dnorberg.rivencraft.writing.OrderedWriting; +import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; import net.minecraft.core.GlobalPos; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -16,7 +20,8 @@ public record OpenDescriptiveBookScreenPayload( boolean offhand, GlobalPos source, ItemStack book, - @Nullable AgeTraits traits + @Nullable AgeTraits traits, + List writingSymbols ) implements CustomPacketPayload { public static final Type TYPE = new Type<>( @@ -40,6 +45,11 @@ public record OpenDescriptiveBookScreenPayload( buffer.writeFloat(traits.celestialAngle()); buffer.writeFloat(traits.celestialPhase()); } + if (payload.writingSymbols().size() > OrderedWriting.MAX_ENTRIES) { + throw new IllegalArgumentException("Too many book symbol displays"); + } + buffer.writeVarInt(payload.writingSymbols().size()); + payload.writingSymbols().forEach(symbol -> SymbolDisplayMetadata.STREAM_CODEC.encode(buffer, symbol)); } private static OpenDescriptiveBookScreenPayload decode(RegistryFriendlyByteBuf buffer) { @@ -57,7 +67,16 @@ public record OpenDescriptiveBookScreenPayload( buffer.readFloat() ) : null; - return new OpenDescriptiveBookScreenPayload(fromStand, offhand, source, book, traits); + int count = buffer.readVarInt(); + if (count < 0 || count > OrderedWriting.MAX_ENTRIES) { + throw new IllegalArgumentException("Invalid book symbol display count " + count); + } + List writingSymbols = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + writingSymbols.add(SymbolDisplayMetadata.STREAM_CODEC.decode(buffer)); + } + return new OpenDescriptiveBookScreenPayload(fromStand, offhand, source, book, traits, + List.copyOf(writingSymbols)); } @Override diff --git a/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java b/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java index bec36ff..1853a1b 100644 --- a/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java +++ b/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.writing; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; import io.netty.buffer.ByteBuf; import java.util.ArrayList; import java.util.List; @@ -10,30 +11,50 @@ import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; /** Immutable ordered writing shared by pages, notebooks, desks, and books. */ -public record OrderedWriting(List entries) { +public record OrderedWriting(int schemaVersion, List entries) { + public static final int CURRENT_SCHEMA_VERSION = 1; public static final int MAX_ENTRIES = 256; public static final OrderedWriting EMPTY = new OrderedWriting(List.of()); - public static final Codec CODEC = WritingEntry.CODEC.listOf() + private static final Codec> ENTRIES_CODEC = WritingEntry.CODEC.listOf() .validate(entries -> entries.size() <= MAX_ENTRIES ? DataResult.success(entries) - : DataResult.error(() -> "Writing exceeds " + MAX_ENTRIES + " entries")) - .xmap(OrderedWriting::new, OrderedWriting::entries); + : DataResult.error(() -> "Writing exceeds " + MAX_ENTRIES + " entries")); + private static final Codec VERSIONED_CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.intRange(CURRENT_SCHEMA_VERSION, CURRENT_SCHEMA_VERSION) + .fieldOf("schema_version").forGetter(OrderedWriting::schemaVersion), + ENTRIES_CODEC.fieldOf("entries").forGetter(OrderedWriting::entries) + ).apply(instance, OrderedWriting::new)); + // 0.3.7.2 stored BOOK_WRITING as a bare list. Decode it without changing its order. + public static final Codec CODEC = Codec.withAlternative( + VERSIONED_CODEC, ENTRIES_CODEC.xmap(OrderedWriting::new, OrderedWriting::entries)); public static final StreamCodec STREAM_CODEC = StreamCodec.of( OrderedWriting::encode, OrderedWriting::decode); public OrderedWriting { + if (schemaVersion != CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException("Unsupported writing schema version " + schemaVersion); + } entries = List.copyOf(Objects.requireNonNull(entries, "entries")); if (entries.size() > MAX_ENTRIES) { throw new IllegalArgumentException("Writing exceeds " + MAX_ENTRIES + " entries"); } } + public OrderedWriting(List entries) { + this(CURRENT_SCHEMA_VERSION, entries); + } + private static void encode(ByteBuf buffer, OrderedWriting writing) { + ByteBufCodecs.VAR_INT.encode(buffer, writing.schemaVersion); ByteBufCodecs.VAR_INT.encode(buffer, writing.entries.size()); writing.entries.forEach(entry -> WritingEntry.STREAM_CODEC.encode(buffer, entry)); } private static OrderedWriting decode(ByteBuf buffer) { + int schemaVersion = ByteBufCodecs.VAR_INT.decode(buffer); + if (schemaVersion != CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException("Unsupported writing schema version " + schemaVersion); + } int size = ByteBufCodecs.VAR_INT.decode(buffer); if (size < 0 || size > MAX_ENTRIES) { throw new IllegalArgumentException("Invalid writing entry count " + size); @@ -42,6 +63,6 @@ public record OrderedWriting(List entries) { for (int index = 0; index < size; index++) { entries.add(WritingEntry.STREAM_CODEC.decode(buffer)); } - return new OrderedWriting(entries); + return new OrderedWriting(schemaVersion, entries); } } diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index 34ec84e..9a1b39d 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -60,6 +60,10 @@ "screen.rivencraft.age.name_prompt": "Name this Age before writing the Descriptive Book", "screen.rivencraft.age.create": "Create", "screen.rivencraft.age.generator": "Terrain: %s", + "screen.rivencraft.age.pages": "Pages", + "screen.rivencraft.age.details": "Details", + "screen.rivencraft.age.pages_count": "Writing %s/%s", + "screen.rivencraft.age.pages_not_applied": "Symbols not applied yet", "screen.rivencraft.age.trait.biomes": "Biomes: %s", "screen.rivencraft.age.trait.lighting": "Lighting: %s", "screen.rivencraft.age.trait.weather": "Weather: %s", @@ -110,7 +114,6 @@ "generator.rivencraft.void": "Void", "generator.rivencraft.random_vanilla": "Standard", "screen.rivencraft.book.preview_pending": "Loading destination...", - "screen.rivencraft.book.click_to_link": "Click the panel to link", "screen.rivencraft.book.explore": "Explore", "screen.rivencraft.book.exploring": "Exploring", "screen.rivencraft.book.return_from_explore": "Right-click to return", diff --git a/src/main/resources/assets/rivencraft/textures/gui/bookui_cover.png b/src/main/resources/assets/rivencraft/textures/gui/bookui_cover.png new file mode 100644 index 0000000..e2d8f56 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/bookui_cover.png differ diff --git a/src/main/resources/assets/rivencraft/textures/gui/bookui_pagel.png b/src/main/resources/assets/rivencraft/textures/gui/bookui_pagel.png new file mode 100644 index 0000000..6bc7d4f Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/bookui_pagel.png differ diff --git a/src/main/resources/assets/rivencraft/textures/gui/bookui_pager.png b/src/main/resources/assets/rivencraft/textures/gui/bookui_pager.png new file mode 100644 index 0000000..f7e6920 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/bookui_pager.png differ diff --git a/src/main/resources/assets/rivencraft/textures/gui/bookui_rpage_full.png b/src/main/resources/assets/rivencraft/textures/gui/bookui_rpage_full.png new file mode 100644 index 0000000..54f03ec Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/bookui_rpage_full.png differ