Compare commits

...
1 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
11 changed files with 158 additions and 7 deletions

No files matched your search

+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.2 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
@@ -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();
}
}
@@ -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(
@@ -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