Compare commits

...
1 Commits
Author SHA1 Message Date
dnorberg 5066e71897 Match Legacy notebook sparse portrait pages
Build / build (push) Canceled after 0s
2026-09-24 05:02:58 -04:00
7 changed files with 113 additions and 48 deletions

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ Mystcraft Legacy copyright: XCompWiz and contributors
Mystcraft Legacy license: GNU Lesser General Public License version 3 Mystcraft Legacy license: GNU Lesser General Public License version 3
The animated ink-vial item texture in this project is copied from Mystcraft The animated ink-vial item texture in this project is copied from Mystcraft
Legacy's assets/mystcraft/textures/items/ink_vial.png under that license. Legacy's assets/mystcraft/textures/items/ink_vial.png under that license.
The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_pagel.png
under the same license.
The initial build layout is derived from the official NeoForge 1.21.1 The initial build layout is derived from the official NeoForge 1.21.1
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
+1
View File
@@ -7,6 +7,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| Rivencraft asset | Upstream asset | Upstream revision | Changes | | Rivencraft asset | Upstream asset | Upstream revision | Changes |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `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/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample | | `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample |
| `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem | | `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem |
+7
View File
@@ -65,6 +65,13 @@ composition from its page background and symbol-component sheet. Data-pack
symbols without a bundled resource-pack model use the legacy question-mark symbols without a bundled resource-pack model use the legacy question-mark
fallback while keeping their real identifier and display metadata. fallback while keeping their real identifier and display metadata.
The Notebook surface displays pages by their persistent slot index instead of
packing occupied pages together. Empty positions through the last placed page
appear darkened. While carrying a Page, all available positions are shown for
placement. The surface draws the native 30×40 portrait page region from
Legacy's book UI atlas; symbols are layered at a uniform scale, while the
square item models remain unchanged in inventories.
## Ink and baseline link panels ## Ink and baseline link panels
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket. An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
+1 -1
View File
@@ -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. # 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 mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.3.4.1 mod_version=0.3.4.2
immersive_portals_version=6.0.7 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. # 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. # This should match the base package used for the mod sources.
@@ -1,8 +1,8 @@
package net.dnorberg.rivencraft.client.screen; package net.dnorberg.rivencraft.client.screen;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.menu.NotebookMenu; import net.dnorberg.rivencraft.menu.NotebookMenu;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
@@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ClickType; import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -24,6 +25,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private static final int PAGE_STEP_X = 31; private static final int PAGE_STEP_X = 31;
private static final int PAGE_STEP_Y = 41; private static final int PAGE_STEP_Y = 41;
private static final int PAGE_COLUMNS = 5; private static final int PAGE_COLUMNS = 5;
private static final ResourceLocation PAGE_ATLAS = texture("gui/notebook_pages");
private static final ResourceLocation GENERIC_SYMBOL = texture("item/page_symbol");
private static final ResourceLocation UNRESOLVED_SYMBOL = texture("item/page_unresolved");
private static final ResourceLocation STANDARD_TERRAIN = texture("item/page_symbol_terrain_standard");
private static final ResourceLocation NATIVE_BIOMES = texture("item/page_symbol_biome_native");
private static final ResourceLocation NORMAL_LIGHTING = texture("item/page_symbol_lighting_normal");
private static final ResourceLocation NORMAL_WEATHER = texture("item/page_symbol_weather_normal");
private static final ResourceLocation NORMAL_TIME = texture("item/page_symbol_celestial_normal_time");
private static final ResourceLocation CAVES = texture("item/page_symbol_feature_caves");
private EditBox textInput; private EditBox textInput;
private boolean draggingScrollbar; private boolean draggingScrollbar;
private boolean surfaceClickPendingRelease; private boolean surfaceClickPendingRelease;
@@ -59,25 +69,25 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override @Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick); super.render(graphics, mouseX, mouseY, partialTick);
List<Integer> pages = visiblePages(); int pageSlots = displayedPageSlots();
scroll = Math.min(scroll, maxScroll(pages.size())); scroll = Math.min(scroll, maxScroll(pageSlots));
graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT,
topPos + SURFACE_BOTTOM); topPos + SURFACE_BOTTOM);
for (int position = 0; position < pages.size(); position++) { for (int index = 0; index < pageSlots; index++) {
int x = leftPos + (position % PAGE_COLUMNS) * PAGE_STEP_X; int x = leftPos + (index % PAGE_COLUMNS) * PAGE_STEP_X;
int y = topPos + SURFACE_TOP + (position / PAGE_COLUMNS) * PAGE_STEP_Y - scroll; int y = topPos + SURFACE_TOP + (index / PAGE_COLUMNS) * PAGE_STEP_Y - scroll;
if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) { if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) {
drawPage(graphics, pages.get(position), x, y); drawPage(graphics, index, x, y);
} }
} }
graphics.disableScissor(); graphics.disableScissor();
drawScrollbar(graphics, pages.size()); drawScrollbar(graphics, pageSlots);
int hovered = pageAt(mouseX, mouseY, pages); int hovered = pageAt(mouseX, mouseY);
if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) { if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) {
graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8); graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8);
} else if (hovered >= 0) { } else if (hovered >= 0 && !menu.getSlot(hovered).getItem().isEmpty()) {
ItemStack page = menu.getSlot(hovered).getItem(); ItemStack page = menu.getSlot(hovered).getItem();
if (!page.isEmpty() && matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY); if (matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY);
} else { } else {
renderTooltip(graphics, mouseX, mouseY); renderTooltip(graphics, mouseX, mouseY);
} }
@@ -103,13 +113,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseClicked(double mouseX, double mouseY, int button) { public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (inScrollbar(mouseX, mouseY) && button == 0) { if (inScrollbar(mouseX, mouseY) && button == 0) {
draggingScrollbar = true; draggingScrollbar = true;
setScrollFromMouse(mouseY, visiblePages().size()); setScrollFromMouse(mouseY, displayedPageSlots());
return true; return true;
} }
int index = pageAt(mouseX, mouseY, visiblePages()); int index = pageAt(mouseX, mouseY);
if (index >= 0 && (button == 0 || button == 1)) { if (index >= 0 && (button == 0 || button == 1)) {
ItemStack page = menu.getSlot(index).getItem(); ItemStack page = menu.getSlot(index).getItem();
if (!page.isEmpty() && !matchesSearch(page)) return true; if ((!page.isEmpty() && !matchesSearch(page))
|| (page.isEmpty() && !menu.getCarried().is(ModItems.PAGE.get()))) {
return true;
}
setFocused(null); setFocused(null);
textInput.setFocused(false); textInput.setFocused(false);
selectedSlot = index; selectedSlot = index;
@@ -123,7 +136,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override @Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (draggingScrollbar && button == 0) { if (draggingScrollbar && button == 0) {
setScrollFromMouse(mouseY, visiblePages().size()); setScrollFromMouse(mouseY, displayedPageSlots());
return true; return true;
} }
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
@@ -144,7 +157,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) { if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) {
scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20, scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20,
0, maxScroll(visiblePages().size())); 0, maxScroll(displayedPageSlots()));
return true; return true;
} }
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
@@ -170,21 +183,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
return super.keyPressed(keyCode, scanCode, modifiers); return super.keyPressed(keyCode, scanCode, modifiers);
} }
private List<Integer> visiblePages() { private int displayedPageSlots() {
List<Integer> pages = new ArrayList<>(); if (menu.getCarried().is(ModItems.PAGE.get())) {
int firstEmpty = -1; return NotebookMenu.NOTEBOOK_SLOTS;
for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) { }
ItemStack stack = menu.getSlot(index).getItem(); for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) {
if (stack.isEmpty()) { if (!menu.getSlot(index).getItem().isEmpty()) {
if (firstEmpty < 0) firstEmpty = index; return index + 1;
} else {
pages.add(index);
} }
} }
if (firstEmpty >= 0 && menu.getCarried().is(ModItems.PAGE.get())) { return 0;
pages.add(firstEmpty);
}
return pages;
} }
private boolean matchesSearch(ItemStack page) { private boolean matchesSearch(ItemStack page) {
@@ -193,15 +201,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
.contains(textInput.getValue().toLowerCase(Locale.ROOT)); .contains(textInput.getValue().toLowerCase(Locale.ROOT));
} }
private int pageAt(double mouseX, double mouseY, List<Integer> pages) { private int pageAt(double mouseX, double mouseY) {
if (!inSurface(mouseX, mouseY)) return -1; if (!inSurface(mouseX, mouseY)) return -1;
int localX = (int)mouseX - leftPos; int localX = (int)mouseX - leftPos;
int localY = (int)mouseY - topPos - SURFACE_TOP + scroll; int localY = (int)mouseY - topPos - SURFACE_TOP + scroll;
int column = localX / PAGE_STEP_X; int column = localX / PAGE_STEP_X;
int position = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column; int index = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column;
return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH
&& localY % PAGE_STEP_Y < PAGE_HEIGHT && position < pages.size() && localY % PAGE_STEP_Y < PAGE_HEIGHT && index < displayedPageSlots()
? pages.get(position) : -1; ? index : -1;
} }
private boolean inSurface(double mouseX, double mouseY) { private boolean inSurface(double mouseX, double mouseY) {
@@ -243,20 +251,44 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private void drawPage(GuiGraphics graphics, int index, int x, int y) { private void drawPage(GuiGraphics graphics, int index, int x, int y) {
ItemStack page = menu.getSlot(index).getItem(); ItemStack page = menu.getSlot(index).getItem();
boolean visible = !page.isEmpty() && matchesSearch(page); boolean dimmed = page.isEmpty() || !matchesSearch(page);
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, // Legacy's Notebook draws the atlas' native 30x40 page, not a stretched item icon.
visible ? 0xFFE5D4A7 : 0x704B4945); graphics.blit(PAGE_ATLAS, x, y, PAGE_WIDTH, PAGE_HEIGHT,
graphics.fill(x + 2, y + 2, x + PAGE_WIDTH - 2, y + PAGE_HEIGHT - 2, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
visible ? 0xFFF4E5C3 : 0x603B3934); if (!page.isEmpty()) {
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, switch (PageItem.content(page).kind()) {
index == selectedSlot ? 0xFFFFD061 : 0xFF564A34); case BLANK -> { }
if (visible) { case LINK_PANEL -> graphics.fill(x + 5, y + 6, x + 25, y + 20, 0xFF000000);
graphics.pose().pushPose(); case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(symbolTexture(page), x + 1, y + 6,
graphics.pose().translate(x + 3, y + 8, 0); 28, 28, 16, 16, 96, 96, 128, 128);
graphics.pose().scale(1.5F, 1.5F, 1.0F); }
graphics.renderItem(page, 0, 0);
graphics.pose().popPose();
} }
if (dimmed) {
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xB8000000);
}
if (index == selectedSlot && !page.isEmpty()) {
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFFD061);
}
}
private static ResourceLocation symbolTexture(ItemStack page) {
var content = PageItem.content(page);
if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) {
return UNRESOLVED_SYMBOL;
}
return switch (content.symbol().orElseThrow().id().toString()) {
case "rivencraft:terrain/standard" -> STANDARD_TERRAIN;
case "rivencraft:biome/native" -> NATIVE_BIOMES;
case "rivencraft:lighting/normal" -> NORMAL_LIGHTING;
case "rivencraft:weather/normal" -> NORMAL_WEATHER;
case "rivencraft:celestial/normal_time" -> NORMAL_TIME;
case "rivencraft:feature/caves" -> CAVES;
default -> GENERIC_SYMBOL;
};
}
private static ResourceLocation texture(String path) {
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
} }
private void moveSelected(boolean next) { private void moveSelected(boolean next) {
@@ -203,6 +203,29 @@ public final class WritingDataGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void notebookMenuPreservesSparsePagePositions(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel();
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.NOTEBOOK.get()));
NotebookMenu menu = new NotebookMenu(9, player.getInventory(), InteractionHand.MAIN_HAND);
ItemStack page = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
menu.setCarried(page);
menu.clicked(9, 0, ClickType.PICKUP, player);
helper.assertTrue(menu.getCarried().isEmpty(),
"Clicking a distant empty Notebook slot must place the carried Page");
NotebookContents saved = player.getMainHandItem().get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(saved != null && saved.pages().size() > 9,
"Notebook persistence must extend through the highest occupied position");
for (int index = 0; index < 9; index++) {
helper.assertTrue(saved.pages().get(index).isEmpty(),
"Earlier empty page positions must remain empty rather than being packed together");
}
helper.assertValueEqual(PageItem.content(saved.pages().get(9)).kind(), PageContent.Kind.SYMBOL,
"The Page must remain in the selected distant position");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) { public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) {
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get()); ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB