Compare commits

...
2 Commits
Author SHA1 Message Date
dnorberg 7ae41da23e Add Legacy-style symbol portfolios (#92)
Build / build (push) Canceled after 0s
2026-10-21 14:13:34 -04:00
dnorberg 9cb63e6994 Align Writing Desk numerals with Legacy glyphs (#43)
Build / build (push) Canceled after 0s
2026-10-21 14:01:02 -04:00
48 changed files with 179 additions and 16 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+1 -1
View File
@@ -20,7 +20,7 @@ Milestone 5 issue #29 adds typed meaning to the existing data-pack symbol catalo
`poem` is optional, client-safe artwork metadata: exactly four alphabetic words, ordered top, right, bottom, left. The client layers their glyphs from the shared Legacy component sheet at runtime. GUI thumbnails use the full transparent artwork over their page backgrounds; item icons use the same artwork reduced from 16 to 13 pixels across, over the page texture. Unregistered words use a stable generated component combination; when a symbol has no poem, the six original built-in IDs retain their Legacy poems and other IDs produce four stable fallback words. This affects appearance only, never Age generation. Generated images are cached up to 256 entries and discarded on resource reload. `poem` is optional, client-safe artwork metadata: exactly four alphabetic words, ordered top, right, bottom, left. The client layers their glyphs from the shared Legacy component sheet at runtime. GUI thumbnails use the full transparent artwork over their page backgrounds; item icons use the same artwork reduced from 16 to 13 pixels across, over the page texture. Unregistered words use a stable generated component combination; when a symbol has no poem, the six original built-in IDs retain their Legacy poems and other IDs produce four stable fallback words. This affects appearance only, never Age generation. Generated images are cached up to 256 entries and discarded on resource reload.
The Writing Desk's 88 collection tabs use Legacy's number components, chiefly the bottom row of `symbolcomponents.png`, rather than word sigils. Legacy's `ModWords.constructNumber` combines four unit marks (1–4) with four five-step marks (5–20) to form 1–24 and uses a separate zero mark. Rivencraft extends these marks positionally in base 25 for tabs 25–88 (for example, 25 is 1 followed by 0). Hovering still reveals the decimal number for accessibility. The former arbitrary-word glyph generator remains useful for rendering text: on page-free Descriptive Books, the generated Age description now appears as a sequence of layered word glyphs instead of Minecraft's enchantment font. The Age name stays readable. The Writing Desk retains 88 collection tabs, numbered from zero as in Legacy. Legacy's `ModWords.constructNumber` defines 0 as component 1 and 1–24 by overlaying a unit mark (1–4) and a five-step mark (5–20) from the bottom row of `symbolcomponents.png` in the *same square*. Legacy's Writing Desk has 25 tabs (0–24) and scrolls its four-tab window **one tab at a time**. Thus the supplied Legacy screenshots labeled 1–4, 5–8, and so on show overlapping windows, not 88 distinct numerals. Rivencraft retains its previously requested four-at-a-time navigation and 88 tabs. Its additional tab numbers use positional base 25: each place is an overlaid Legacy digit, with the places side by side (25 = 1,0). The two glyphs are sized and positioned to fit the tab. Hovering reveals the decimal tab index for accessibility. The generated-word mechanism renders page-free Descriptive Book text as layered glyphs instead of Minecraft's enchantment font. The Age name stays readable.
| Role | Value type and allowed values | | Role | Value type and allowed values |
| --- | --- | | --- | --- |
+1 -1
View File
@@ -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. # 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.6.7.1 mod_version=0.6.7.3
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
terrablender_version=4.1.0.8 terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE serene_seasons_version_id=pHEgQQUE
@@ -196,7 +196,7 @@ public final class SymbolPageTextures {
graphics.setColor(1F, 1F, 1F, 1F); graphics.setColor(1F, 1F, 1F, 1F);
} }
/** Legacy's single-number marks; numbers above 24 use positional base 25. */ /** Overlay each Legacy digit's components; place base-25 digits side by side. */
public static void drawNumeral(GuiGraphics graphics, int number, int x, int y, int size) { public static void drawNumeral(GuiGraphics graphics, int number, int x, int y, int size) {
int[] digits = SymbolNumerals.digitsFor(number); int[] digits = SymbolNumerals.digitsFor(number);
int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4); int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4);
@@ -151,8 +151,8 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F); if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F);
graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37); graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37);
graphics.setColor(1F, 1F, 1F, 1F); graphics.setColor(1F, 1F, 1F, 1F);
// Encode the visible one-based tab number with Legacy's numeric word glyph. // Legacy numbers its Writing Desk collections from zero.
SymbolPageTextures.drawNumeral(graphics, tab + 1, x + 8, tabY + 3, 19); SymbolPageTextures.drawNumeral(graphics, tab, x + (tab < 25 ? 8 : 4), tabY + 3, 19);
ItemStack collection = menu.getSlot(tab).getItem(); ItemStack collection = menu.getSlot(tab).getItem();
if (!collection.isEmpty()) { if (!collection.isEmpty()) {
String name = collection.getHoverName().getString(); String name = collection.getHoverName().getString();
@@ -227,7 +227,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) { else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) {
int tab = menu.firstVisibleTab() + (localY - 29) / 37; int tab = menu.firstVisibleTab() + (localY - 29) / 37;
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol", graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol",
tab + 1), mouseX, mouseY); tab), mouseX, mouseY);
} }
} }
} }
@@ -25,12 +25,14 @@ import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.book.LinkProperties; import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.writing.InkModifierIngredients; import net.dnorberg.rivencraft.writing.InkModifierIngredients;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -63,6 +65,11 @@ public final class RivencraftJeiPlugin implements IModPlugin {
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id(); case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id();
}; };
}); });
registration.registerSubtypeInterpreter(ModItems.SYMBOL_PORTFOLIO.get(),
(IIngredientSubtypeInterpreter<ItemStack>) (stack, context) -> {
Component name = stack.get(DataComponents.CUSTOM_NAME);
return name == null ? "empty" : name.getString();
});
} }
@Override @Override
@@ -72,6 +79,7 @@ public final class RivencraftJeiPlugin implements IModPlugin {
var connection = Minecraft.getInstance().getConnection(); var connection = Minecraft.getInstance().getConnection();
if (connection != null) { if (connection != null) {
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess())); pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
pages.addAll(ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess()));
} else { } else {
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage) PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
.forEach(pages::add); .forEach(pages::add);
@@ -84,6 +92,8 @@ public final class RivencraftJeiPlugin implements IModPlugin {
// JEI discovers the base Page item as a blank stack; only authored pages belong in its list. // JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK, runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient)); List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient));
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
List.of(new ItemStack(ModItems.SYMBOL_PORTFOLIO.get())), List.of(UidContext.Ingredient));
var connection = Minecraft.getInstance().getConnection(); var connection = Minecraft.getInstance().getConnection();
if (connection == null) return; if (connection == null) return;
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream() Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
@@ -98,6 +108,19 @@ public final class RivencraftJeiPlugin implements IModPlugin {
if (!missing.isEmpty()) { if (!missing.isEmpty()) {
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing); runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing);
} }
Set<String> knownPortfolios = runtime.getIngredientManager().getAllItemStacks().stream()
.filter(stack -> stack.is(ModItems.SYMBOL_PORTFOLIO.get()))
.map(stack -> stack.get(DataComponents.CUSTOM_NAME))
.filter(java.util.Objects::nonNull)
.map(Component::getString)
.collect(Collectors.toSet());
List<ItemStack> missingPortfolios = ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess())
.stream()
.filter(stack -> !knownPortfolios.contains(stack.getHoverName().getString()))
.toList();
if (!missingPortfolios.isEmpty()) {
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missingPortfolios);
}
} }
@Override @Override
@@ -0,0 +1,59 @@
package net.dnorberg.rivencraft.gametest;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.PageContent;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class SymbolPortfolioGameTests {
private SymbolPortfolioGameTests() {}
@GameTest(template = "empty")
public static void portfoliosCoverEverySymbolCategoryWithoutTruncation(GameTestHelper helper) {
var registries = helper.getLevel().registryAccess();
List<ItemStack> creative = ModCreativeTabs.creativeSymbolPortfolios(registries);
helper.assertTrue(!creative.isEmpty(), "Creative tab must include prefilled portfolios");
for (ResourceLocation category : PageItem.bundledCreativeCategories()) {
List<ItemStack> expected = PageItem.creativeSymbolPages(registries, category);
List<ItemStack> volumes = SymbolPortfolioItem.createVolumes(category, expected);
helper.assertTrue(!volumes.isEmpty(), "Missing portfolio for " + category);
List<ItemStack> actual = new ArrayList<>();
for (ItemStack volume : volumes) {
helper.assertTrue(volume.is(ModItems.SYMBOL_PORTFOLIO.get())
&& CollationFolderItem.isCollection(volume)
&& creative.stream().anyMatch(candidate ->
candidate.getHoverName().getString().equals(volume.getHoverName().getString())),
"Portfolio must be an editable creative collection for " + category);
NotebookContents contents = volume.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(contents != null && contents.pageCount() > 0
&& contents.pageCount() <= NotebookContents.MAX_PAGES,
"Each portfolio volume must contain valid pages");
helper.assertTrue(contents.pages().stream().allMatch(page ->
PageItem.content(page).kind() == PageContent.Kind.SYMBOL),
"Portfolios must contain symbol pages, not Link Panels");
actual.addAll(contents.pages());
}
helper.assertTrue(actual.size() == expected.size(), "Portfolio omitted pages in " + category);
for (int index = 0; index < expected.size(); index++) {
helper.assertTrue(ItemStack.isSameItemSameComponents(actual.get(index), expected.get(index)),
"Portfolio pages must match the category tab in " + category);
}
}
helper.succeed();
}
}
@@ -151,11 +151,23 @@ public final class WritingDataGameTests {
"Zero must use Legacy's dedicated number mark"); "Zero must use Legacy's dedicated number mark");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(1), new int[] {56}), helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(1), new int[] {56}),
"One must use Legacy's first bottom-row mark"); "One must use Legacy's first bottom-row mark");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(5), new int[] {60}),
"Five must use Legacy's first five-step mark");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(10), new int[] {61}),
"Ten must use Legacy's second five-step mark");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(14), new int[] {61, 59}),
"Fourteen must overlay the ten and four marks");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(20), new int[] {63}),
"Twenty must use Legacy's final five-step mark");
helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(24), new int[] {63, 59}), helper.assertTrue(Arrays.equals(SymbolNumerals.componentsForDigit(24), new int[] {63, 59}),
"Twenty-four must combine Legacy's five and unit marks"); "Twenty-four must combine Legacy's five and unit marks");
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(25), new int[] {1, 0}), helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(25), new int[] {1, 0}),
"Twenty-five must start a second base-25 place"); "Twenty-five must start a second base-25 place");
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(88), new int[] {3, 13}), helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(49), new int[] {1, 24}),
"Forty-nine must use twenty-four in its ones place");
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(50), new int[] {2, 0}),
"Fifty must carry into the second base-25 place");
helper.assertTrue(Arrays.equals(SymbolNumerals.digitsFor(87), new int[] {3, 12}),
"Last writing-desk tab must retain its numeric value"); "Last writing-desk tab must retain its numeric value");
String display = "\"name\":\"symbol.example.test\"," String display = "\"name\":\"symbol.example.test\","
@@ -13,11 +13,13 @@ public final class CollationFolderItem extends NotebookItem {
} }
public static boolean isCollection(ItemStack stack) { public static boolean isCollection(ItemStack stack) {
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get()); return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get())
|| stack.is(ModItems.SYMBOL_PORTFOLIO.get());
} }
public static int capacity(ItemStack stack) { public static int capacity(ItemStack stack) {
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
: stack.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES : 0; : stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.SYMBOL_PORTFOLIO.get())
? NotebookContents.MAX_PAGES : 0;
} }
} }
@@ -39,8 +39,7 @@ public class NotebookItem extends Item {
NotebookContents contents = stack.getOrDefault( NotebookContents contents = stack.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
tooltip.add(Component.translatable("item.rivencraft.notebook.pages", tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get()) contents.pageCount(), CollationFolderItem.capacity(stack))
? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES)
.withStyle(ChatFormatting.GRAY)); .withStyle(ChatFormatting.GRAY));
if (contents.unresolvedCount() > 0) { if (contents.unresolvedCount() > 0) {
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved", tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
@@ -0,0 +1,40 @@
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.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/** Non-craftable, prefilled Legacy-style collections of one symbol-page category. */
public final class SymbolPortfolioItem extends NotebookItem {
public SymbolPortfolioItem(Properties properties) {
super(properties);
}
public static List<ItemStack> createVolumes(ResourceLocation category, List<ItemStack> pages) {
if (pages.isEmpty()) return List.of();
int capacity = NotebookContents.MAX_PAGES;
int volumeCount = (pages.size() + capacity - 1) / capacity;
List<ItemStack> volumes = new ArrayList<>(volumeCount);
for (int volume = 0; volume < volumeCount; volume++) {
ItemStack portfolio = new ItemStack(ModItems.SYMBOL_PORTFOLIO.get());
int first = volume * capacity;
int last = Math.min(first + capacity, pages.size());
portfolio.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
new NotebookContents(pages.subList(first, last)));
Component categoryName = Component.translatableWithFallback(
"itemGroup.rivencraft.pages." + category.getPath(), category.getPath());
portfolio.set(DataComponents.CUSTOM_NAME, volumeCount == 1
? Component.translatable("item.rivencraft.symbol_portfolio.named", categoryName)
: Component.translatable("item.rivencraft.symbol_portfolio.volume",
categoryName, volume + 1, volumeCount));
volumes.add(portfolio);
}
return List.copyOf(volumes);
}
}
@@ -7,6 +7,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.minecraft.core.HolderLookup;
import net.minecraft.world.item.ItemStack;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -28,6 +31,7 @@ public final class ModCreativeTabs {
output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.NOTEBOOK.get()); output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.COLLATION_FOLDER.get()); output.accept(ModItems.COLLATION_FOLDER.get());
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
output.accept(ModItems.SEALED_NOTEBOOK.get()); output.accept(ModItems.SEALED_NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get()); output.accept(ModItems.INK_VIAL.get());
output.accept(ModItems.WRITING_DESK.get()); output.accept(ModItems.WRITING_DESK.get());
@@ -59,14 +63,28 @@ public final class ModCreativeTabs {
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES = public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
registerSymbolPageTabs(); registerSymbolPageTabs();
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() { /** Prefilled portfolios mirror the symbol-page tabs, including data-pack and modded pages. */
public static List<ItemStack> creativeSymbolPortfolios(HolderLookup.Provider registries) {
List<ItemStack> portfolios = new ArrayList<>();
for (ResourceLocation category : orderedSymbolCategories()) {
portfolios.addAll(SymbolPortfolioItem.createVolumes(category,
PageItem.creativeSymbolPages(registries, category)));
}
return List.copyOf(portfolios);
}
private static List<ResourceLocation> orderedSymbolCategories() {
List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories()); List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
categories.sort(Comparator.comparingInt((ResourceLocation id) -> { categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
int index = CATEGORY_ORDER.indexOf(id.getPath()); int index = CATEGORY_ORDER.indexOf(id.getPath());
return index < 0 ? CATEGORY_ORDER.size() : index; return index < 0 ? CATEGORY_ORDER.size() : index;
}).thenComparing(ResourceLocation::toString)); }).thenComparing(ResourceLocation::toString));
return List.copyOf(categories);
}
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>(); Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>();
for (ResourceLocation category : categories) { for (ResourceLocation category : orderedSymbolCategories()) {
String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_'); String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder() tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(), .title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem; import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.dnorberg.rivencraft.item.SealedNotebookItem; import net.dnorberg.rivencraft.item.SealedNotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
@@ -39,6 +40,8 @@ public final class ModItems {
); );
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem( public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1)); "collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1));
public static final DeferredItem<SymbolPortfolioItem> SYMBOL_PORTFOLIO = ITEMS.registerItem(
"symbol_portfolio", SymbolPortfolioItem::new, new Item.Properties().stacksTo(1));
public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem( public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16)); "sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem( public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
@@ -1,8 +1,8 @@
package net.dnorberg.rivencraft.writing; package net.dnorberg.rivencraft.writing;
/** /**
* Number components from Mystcraft Legacy's ModWords#constructNumber. * Positional base-25 numerals using Mystcraft Legacy's 0–24 digit components.
* Legacy registers 0–25; larger Rivencraft tab numbers use positional base 25. * Components within one digit are layered in the same square, not laid side by side.
*/ */
public final class SymbolNumerals { public final class SymbolNumerals {
private SymbolNumerals() {} private SymbolNumerals() {}
@@ -19,7 +19,7 @@ public final class SymbolNumerals {
} }
public static int[] componentsForDigit(int digit) { public static int[] componentsForDigit(int digit) {
if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Numeral digit must be in [0, 24]"); if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Base-25 digit must be in [0, 24]");
if (digit == 0) return new int[] {1}; if (digit == 0) return new int[] {1};
int fifths = digit / 5; int fifths = digit / 5;
int remainder = digit % 5; int remainder = digit % 5;
@@ -22,6 +22,9 @@
"item.rivencraft.page.unresolved": "Unresolved Symbol Page", "item.rivencraft.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook", "item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder", "item.rivencraft.collation_folder": "Collation Folder",
"item.rivencraft.symbol_portfolio": "Symbol Portfolio",
"item.rivencraft.symbol_portfolio.named": "%s Portfolio",
"item.rivencraft.symbol_portfolio.volume": "%s Portfolio %s/%s",
"item.rivencraft.sealed_notebook": "Sealed Notebook", "item.rivencraft.sealed_notebook": "Sealed Notebook",
"block.rivencraft.link_modifier": "Link Modifier", "block.rivencraft.link_modifier": "Link Modifier",
"container.rivencraft.link_modifier": "Link Modifier", "container.rivencraft.link_modifier": "Link Modifier",
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/symbol_portfolio" }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B