Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84a1f32732 |
No files matched your search
@@ -79,6 +79,9 @@ With JEI installed, Rivencraft displays the Ink Mixer operation in its own
|
|||||||
category and the hand-use, copying, binding, and Linking Book crafting flows in a Writing category.
|
category and the hand-use, copying, binding, and Linking Book crafting flows in a Writing category.
|
||||||
The Link Panel and individual symbol Pages, including registered vanilla biome
|
The Link Panel and individual symbol Pages, including registered vanilla biome
|
||||||
pages, appear as separately searchable JEI ingredients.
|
pages, appear as separately searchable JEI ingredients.
|
||||||
|
Creative inventory separates Link Pages and each symbol category into their own
|
||||||
|
tabs, including distinct Biome and Biome Generation tabs. Blank Pages are hidden
|
||||||
|
from creative inventory and JEI.
|
||||||
JEI is optional; vanilla crafting
|
JEI is optional; vanilla crafting
|
||||||
recipes and all writing interactions work without it. For a development client
|
recipes and all writing interactions work without it. For a development client
|
||||||
with JEI, run `./gradlew runClient -PwithJei=true`.
|
with JEI, run `./gradlew runClient -PwithJei=true`.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ authored pages: the Binder's 27 symbol-page positions bound an authored book.
|
|||||||
Native cannot be paired with explicit biome pages. Legacy filled Tiny–Huge to
|
Native cannot be paired with explicit biome pages. Legacy filled Tiny–Huge to
|
||||||
three; Rivencraft deliberately uses two so a pair of written biome pages stays
|
three; Rivencraft deliberately uses two so a pair of written biome pages stays
|
||||||
exactly as written.
|
exactly as written.
|
||||||
The Pages creative tab offers pages for every registered vanilla biome.
|
The Biome Pages creative tab offers pages for every registered vanilla biome.
|
||||||
Hand-authored symbol definitions take precedence; the remaining vanilla pages
|
Hand-authored symbol definitions take precedence; the remaining vanilla pages
|
||||||
are generated from the biome registry, so they do not each need a JSON file.
|
are generated from the biome registry, so they do not each need a JSON file.
|
||||||
Custom/modded biomes still require a data-pack symbol definition.
|
Custom/modded biomes still require a data-pack symbol definition.
|
||||||
@@ -99,7 +99,7 @@ cannot reproduce those Legacy distinctions yet, and Snow remains planned for
|
|||||||
the optional season integrations. A data pack that supplies these unsupported
|
the optional season integrations. A data pack that supplies these unsupported
|
||||||
weather choices receives a binding error rather than a false success.
|
weather choices receives a binding error rather than a false success.
|
||||||
|
|
||||||
The Pages creative tab and optional JEI page ingredients use the same generated
|
The category creative tabs and optional JEI page ingredients use the same generated
|
||||||
index of bundled environment symbols. Existing Age definitions and page-free
|
index of bundled environment symbols. Existing Age definitions and page-free
|
||||||
random Ages retain their saved or seeded environment unchanged.
|
random Ages retain their saved or seeded environment unchanged.
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,24 @@ Logs and configuration remain available. Cleanup never targets a custom
|
|||||||
`-PgameTestRunDir` or a player world; callers using a custom directory manage
|
`-PgameTestRunDir` or a player world; callers using a custom directory manage
|
||||||
it themselves. The full suite should still be run
|
it themselves. The full suite should still be run
|
||||||
before shipping changes in the archived systems.
|
before shipping changes in the archived systems.
|
||||||
|
|
||||||
|
## Manual Age unload/reload check
|
||||||
|
|
||||||
|
The automated suite does not unload a freshly generated Age. Minecraft can
|
||||||
|
queue point-of-interest block updates after generation; closing that Age's
|
||||||
|
region storage while a queued update is still pending can leave the dedicated
|
||||||
|
GameTest server waiting forever in `SectionStorage.readColumn`. Plan save/load,
|
||||||
|
terrain generation, and safe spawn remain automated; runtime unload/reload is
|
||||||
|
checked in a normal game session:
|
||||||
|
|
||||||
|
1. Back up the test world. Bind one Link-Panel-only Descriptive Book and one
|
||||||
|
Descriptive Book with a terrain page (for example, Island), using distinct
|
||||||
|
names. Bring a working way back to the Overworld.
|
||||||
|
2. Travel through each book, confirm the named Age and expected terrain, then
|
||||||
|
return to the Overworld. Close book screens and wait a few seconds.
|
||||||
|
3. With cheats enabled, run `/rivencraft age list` and record each Age's
|
||||||
|
`rivencraft:age/...` dimension ID. Run `/rivencraft age unload <dimension>`
|
||||||
|
for one Age at a time, then list again to confirm it is unloaded.
|
||||||
|
4. Reopen the same Descriptive Books and travel back. Confirm each book reaches
|
||||||
|
its original Age, the terrain and safe arrival remain intact, and no extra
|
||||||
|
Age entry was created. Save, restart the world, and repeat the book check.
|
||||||
@@ -10,4 +10,4 @@ Missing terrain, controller, weather, lighting, celestial, or the controller's m
|
|||||||
|
|
||||||
The plan stores the original writing order, the authoritative semantic value of each resolved symbol, the Age seed, and a copy of the selected runtime profile. An explicit terrain page replaces the random terrain selection; an omitted terrain uses the seed-selected profile. The built-in Standard, Amplified, Cave, Island, Flat, and Void terrain pages select their matching generation resources. Age loading uses the frozen runtime profile and does not need the original symbol definition or runtime-profile JSON, though its referenced noise, biome-preset, and dimension-type registry entries must remain available. Existing bound books without plans are not reinterpreted. Page-free and Link-Panel-only books keep the pre-existing random path. Biome and environment pages now affect new Ages; feature effects remain issue #35 work, and instability remains Milestone 6 work.
|
The plan stores the original writing order, the authoritative semantic value of each resolved symbol, the Age seed, and a copy of the selected runtime profile. An explicit terrain page replaces the random terrain selection; an omitted terrain uses the seed-selected profile. The built-in Standard, Amplified, Cave, Island, Flat, and Void terrain pages select their matching generation resources. Age loading uses the frozen runtime profile and does not need the original symbol definition or runtime-profile JSON, though its referenced noise, biome-preset, and dimension-type registry entries must remain available. Existing bound books without plans are not reinterpreted. Page-free and Link-Panel-only books keep the pre-existing random path. Biome and environment pages now affect new Ages; feature effects remain issue #35 work, and instability remains Milestone 6 work.
|
||||||
|
|
||||||
The Rivencraft Pages creative tab reads a generated index of bundled symbol definitions. Adding another valid JSON symbol under `src/main/resources/data/rivencraft/rivencraft/symbols/` includes its page automatically in the next build; no Java starter-page list needs updating. The server catalog still decides whether that page is valid for writing. External server data-pack symbols are not included in this bundled index.
|
The Rivencraft symbol-page creative tabs read a generated index of bundled symbol definitions and group Pages by each definition's category, except biome-controller roles, which appear in a separate Biome Generation tab rather than the Biome tab. Adding another valid JSON symbol under `src/main/resources/data/rivencraft/rivencraft/symbols/` includes its page in the matching category tab on the next build; a new bundled category receives its own tab. Link Pages have a separate tab. Blank Pages appear in neither the creative tabs nor JEI's ingredient list. The server catalog still decides whether a page is valid for writing. External server data-pack symbols are not included in this bundled index.
|
||||||
+1
-1
@@ -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.4.6.0
|
mod_version=0.4.6.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,11 +1,14 @@
|
|||||||
package net.dnorberg.rivencraft.book;
|
package net.dnorberg.rivencraft.book;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||||
@@ -22,6 +25,42 @@ import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
|||||||
public final class EnvironmentWritingGameTests {
|
public final class EnvironmentWritingGameTests {
|
||||||
private EnvironmentWritingGameTests() {}
|
private EnvironmentWritingGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void symbolCategoriesPartitionCreativePages(GameTestHelper helper) {
|
||||||
|
var registries = helper.getLevel().registryAccess();
|
||||||
|
var allPages = PageItem.creativeSymbolPages(registries);
|
||||||
|
Set<ResourceLocation> collected = new HashSet<>();
|
||||||
|
helper.assertTrue(ModCreativeTabs.SYMBOL_PAGES.keySet().containsAll(PageItem.bundledCreativeCategories()),
|
||||||
|
"Every bundled page category must receive a creative tab");
|
||||||
|
for (ResourceLocation category : PageItem.bundledCreativeCategories()) {
|
||||||
|
var categoryPages = PageItem.creativeSymbolPages(registries, category);
|
||||||
|
helper.assertFalse(categoryPages.isEmpty(), "Every category tab must contain pages: " + category);
|
||||||
|
for (var page : categoryPages) {
|
||||||
|
helper.assertValueEqual(PageItem.creativeCategory(page), category,
|
||||||
|
"Category tabs must not mix symbol pages");
|
||||||
|
helper.assertTrue(collected.add(PageItem.content(page).symbol().orElseThrow().id()),
|
||||||
|
"A symbol page must appear in exactly one category tab");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.assertValueEqual(collected.size(), allPages.size(),
|
||||||
|
"Category tabs must contain every bundled and generated biome page");
|
||||||
|
var biomePages = PageItem.creativeSymbolPages(registries,
|
||||||
|
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome"));
|
||||||
|
var generationPages = PageItem.creativeSymbolPages(registries,
|
||||||
|
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome_generation"));
|
||||||
|
helper.assertTrue(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
|
.id().getPath().equals("biome/desert")), "Biome pages must remain in the Biome tab");
|
||||||
|
helper.assertFalse(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
|
.id().getPath().equals("biome/tiny")), "Biome generation types must leave the Biome tab");
|
||||||
|
helper.assertTrue(generationPages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
|
.id().getPath().equals("biome/tiny")), "Biome generation types need their own tab");
|
||||||
|
helper.assertTrue(allPages.stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.BLANK),
|
||||||
|
"Blank pages must not enter the creative or JEI symbol-page feed");
|
||||||
|
helper.assertValueEqual(PageItem.content(PageItem.createLinkPanelPage()).kind(), PageContent.Kind.LINK_PANEL,
|
||||||
|
"Link pages must stay separate from symbol categories");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
|
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
|
||||||
Set<String> expected = Set.of(
|
Set<String> expected = Set.of(
|
||||||
|
|||||||
@@ -206,18 +206,14 @@ public final class TerrainWritingGameTests {
|
|||||||
helper.assertTrue(level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
|
helper.assertTrue(level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
|
||||||
.allMatch(biome -> biome.is(Biomes.THE_END)),
|
.allMatch(biome -> biome.is(Biomes.THE_END)),
|
||||||
"The runtime sky-island generator must use the End biome page");
|
"The runtime sky-island generator must use the End biome page");
|
||||||
helper.assertValueEqual(AgeDimensionService.unload(server, age), AgeDimensionService.UnloadResult.UNLOADED,
|
helper.assertValueEqual(AgeGenerationPlan.load(age.plan().save()).biomeSelection(),
|
||||||
"The island Age must unload");
|
age.plan().biomeSelection(), "The authored End biome must survive a plan save/load round trip");
|
||||||
var reloaded = AgeDimensionService.ensureLoaded(server, age);
|
|
||||||
helper.assertTrue(reloaded != null && reloaded.getChunkSource().getGenerator().getBiomeSource()
|
|
||||||
.possibleBiomes().stream().allMatch(biome -> biome.is(Biomes.THE_END)),
|
|
||||||
"The authored End biome must survive runtime dimension reload");
|
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty", timeoutTicks = 600)
|
@GameTest(template = "empty", timeoutTicks = 600)
|
||||||
public static void writtenTerrainFamiliesGenerateSafelyAndReload(GameTestHelper helper) {
|
public static void writtenTerrainFamiliesGenerateSafely(GameTestHelper helper) {
|
||||||
var player = helper.makeMockServerPlayerInLevel();
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
checkWrittenTerrain(helper, player, AgeGenerationProfiles.all(), 0);
|
checkWrittenTerrain(helper, player, AgeGenerationProfiles.all(), 0);
|
||||||
}
|
}
|
||||||
@@ -259,14 +255,8 @@ public final class TerrainWritingGameTests {
|
|||||||
&& level.getBlockState(spawn.above()).isAir(),
|
&& level.getBlockState(spawn.above()).isAir(),
|
||||||
"Every written terrain family must retain a safe arrival platform");
|
"Every written terrain family must retain a safe arrival platform");
|
||||||
|
|
||||||
helper.assertValueEqual(AgeDimensionService.unload(server, age),
|
helper.assertValueEqual(AgeGenerationPlan.load(age.plan().save()).runtimeProfile(),
|
||||||
AgeDimensionService.UnloadResult.UNLOADED,
|
age.plan().runtimeProfile(), "The written terrain profile must survive plan persistence");
|
||||||
"A written Age must unload without losing its plan");
|
|
||||||
var reloaded = AgeDimensionService.ensureLoaded(server, age);
|
|
||||||
helper.assertTrue(reloaded != null, "The written Age must reload from its frozen plan");
|
|
||||||
assertGenerator(helper, reloaded, age.plan().runtimeProfile());
|
|
||||||
helper.assertValueEqual(reloaded.dimensionType().effectsLocation(),
|
|
||||||
level.dimensionType().effectsLocation(), "Reload must retain dimension effects");
|
|
||||||
helper.runAfterDelay(1, () -> checkWrittenTerrain(helper, player, profiles, index + 1));
|
helper.runAfterDelay(1, () -> checkWrittenTerrain(helper, player, profiles, index + 1));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import mezz.jei.api.registration.IRecipeRegistration;
|
|||||||
import mezz.jei.api.registration.ISubtypeRegistration;
|
import mezz.jei.api.registration.ISubtypeRegistration;
|
||||||
import mezz.jei.api.registration.IExtraIngredientRegistration;
|
import mezz.jei.api.registration.IExtraIngredientRegistration;
|
||||||
import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
|
import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
|
||||||
|
import mezz.jei.api.ingredients.subtypes.UidContext;
|
||||||
import mezz.jei.api.runtime.IJeiRuntime;
|
import mezz.jei.api.runtime.IJeiRuntime;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
@@ -74,6 +75,9 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRuntimeAvailable(IJeiRuntime runtime) {
|
public void onRuntimeAvailable(IJeiRuntime runtime) {
|
||||||
|
// JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
|
||||||
|
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
|
||||||
|
List.of(new ItemStack(ModItems.PAGE.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()
|
||||||
|
|||||||
@@ -313,42 +313,6 @@ public final class LinkingGameTests {
|
|||||||
throw new IllegalStateException("Could not select profile " + profile);
|
throw new IllegalStateException("Could not select profile " + profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty", timeoutTicks = 100)
|
|
||||||
public static void unloadedAgeReloadsWithTheSameDefinition(GameTestHelper helper) {
|
|
||||||
UUID id = UUID.randomUUID();
|
|
||||||
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(
|
|
||||||
helper.getLevel(),
|
|
||||||
new AgeReference(id, "Lifecycle Test Age " + id)
|
|
||||||
);
|
|
||||||
|
|
||||||
var server = helper.getLevel().getServer();
|
|
||||||
var first = AgeDimensionService.ensureLoaded(server, definition);
|
|
||||||
helper.assertTrue(first != null, "The lifecycle test Age must load");
|
|
||||||
helper.runAfterDelay(1, () -> {
|
|
||||||
helper.assertValueEqual(
|
|
||||||
AgeDimensionService.unload(server, definition),
|
|
||||||
AgeDimensionService.UnloadResult.UNLOADED,
|
|
||||||
"A loaded Age must unload without deleting its definition"
|
|
||||||
);
|
|
||||||
helper.runAfterDelay(1, () -> {
|
|
||||||
helper.assertTrue(server.getLevel(AgeDimensionService.dimensionKey(definition)) == null,
|
|
||||||
"An unloaded Age must leave the active server level map");
|
|
||||||
|
|
||||||
var reloaded = AgeDimensionService.ensureLoaded(server, definition);
|
|
||||||
helper.assertTrue(reloaded != null, "A book must be able to reload an archived Age");
|
|
||||||
helper.runAfterDelay(1, () -> {
|
|
||||||
var registered = server.getLevel(AgeDimensionService.dimensionKey(definition));
|
|
||||||
helper.assertTrue(registered != null, "The reloaded Age must rejoin the active server level map");
|
|
||||||
helper.assertValueEqual(registered.getSeed(), definition.seed(),
|
|
||||||
"Reloading an Age must preserve its immutable seed");
|
|
||||||
helper.assertValueEqual(registered.dimension().location(), definition.dimensionId(),
|
|
||||||
"Reloading an Age must preserve its dimension identifier");
|
|
||||||
helper.succeed();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@GameTest(template = "empty", timeoutTicks = 200)
|
@GameTest(template = "empty", timeoutTicks = 200)
|
||||||
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
|
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
|
||||||
for (var profile : AgeGenerationProfiles.all()) {
|
for (var profile : AgeGenerationProfiles.all()) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public final class WritingDataGameTests {
|
|||||||
for (String terrain : List.of("standard", "amplified", "cave", "island", "flat", "void")) {
|
for (String terrain : List.of("standard", "amplified", "cave", "island", "flat", "void")) {
|
||||||
String path = "terrain/" + terrain;
|
String path = "terrain/" + terrain;
|
||||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains(path),
|
helper.assertTrue(PageItem.bundledCreativeSymbols().contains(path),
|
||||||
"The Pages creative tab must include " + path);
|
"The matching symbol-page creative tab must include " + path);
|
||||||
ItemStack page = PageItem.createBundledCreativePage(path);
|
ItemStack page = PageItem.createBundledCreativePage(path);
|
||||||
helper.assertValueEqual(PageItem.content(page).symbol().orElseThrow().id(),
|
helper.assertValueEqual(PageItem.content(page).symbol().orElseThrow().id(),
|
||||||
ResourceLocation.fromNamespaceAndPath("rivencraft", path),
|
ResourceLocation.fromNamespaceAndPath("rivencraft", path),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
@@ -18,18 +19,24 @@ import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
|||||||
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
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.world.item.Item;
|
import net.minecraft.world.item.Item;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.TooltipFlag;
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
|
||||||
/** One stackable Page item whose server-owned component identifies its writing state. */
|
/** One stackable Page item whose server-owned component identifies its writing state. */
|
||||||
public final class PageItem extends Item {
|
public final class PageItem extends Item {
|
||||||
private static final Map<String, SymbolDisplayMetadata> CREATIVE_SYMBOLS = loadCreativeSymbols();
|
private static final ResourceLocation BIOME_GENERATION_CATEGORY =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome_generation");
|
||||||
|
private static final Map<String, CreativeSymbol> CREATIVE_SYMBOLS = loadCreativeSymbols();
|
||||||
|
|
||||||
|
private record CreativeSymbol(SymbolDisplayMetadata display, ResourceLocation creativeCategory) {}
|
||||||
|
|
||||||
public PageItem(Properties properties) {
|
public PageItem(Properties properties) {
|
||||||
super(properties);
|
super(properties);
|
||||||
@@ -46,6 +53,21 @@ public final class PageItem extends Item {
|
|||||||
return List.copyOf(CREATIVE_SYMBOLS.keySet());
|
return List.copyOf(CREATIVE_SYMBOLS.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bundled categories become separate creative tabs without a Java page list. */
|
||||||
|
public static List<ResourceLocation> bundledCreativeCategories() {
|
||||||
|
return List.copyOf(CREATIVE_SYMBOLS.values().stream()
|
||||||
|
.map(CreativeSymbol::creativeCategory)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStack bundledCreativeCategoryIcon(ResourceLocation category) {
|
||||||
|
return CREATIVE_SYMBOLS.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().creativeCategory().equals(category))
|
||||||
|
.findFirst()
|
||||||
|
.map(entry -> createBundledCreativePage(entry.getKey()))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("No bundled pages for category " + category));
|
||||||
|
}
|
||||||
|
|
||||||
/** Exact symbol-page variants offered in the creative tab and to optional ingredient viewers. */
|
/** Exact symbol-page variants offered in the creative tab and to optional ingredient viewers. */
|
||||||
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
||||||
List<ItemStack> pages = new ArrayList<>();
|
List<ItemStack> pages = new ArrayList<>();
|
||||||
@@ -60,12 +82,30 @@ public final class PageItem extends Item {
|
|||||||
return List.copyOf(pages);
|
return List.copyOf(pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries,
|
||||||
|
ResourceLocation category) {
|
||||||
|
return creativeSymbolPages(registries).stream()
|
||||||
|
.filter(page -> category.equals(creativeCategory(page)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation creativeCategory(ItemStack page) {
|
||||||
|
SymbolDisplayMetadata metadata = page.get(ModDataComponents.PAGE_SYMBOL_DISPLAY.get());
|
||||||
|
if (metadata == null) throw new IllegalArgumentException("Page has no symbol display metadata");
|
||||||
|
if (metadata.id().getNamespace().equals(Rivencraft.MOD_ID)) {
|
||||||
|
CreativeSymbol bundled = CREATIVE_SYMBOLS.get(metadata.id().getPath());
|
||||||
|
if (bundled != null) return bundled.creativeCategory();
|
||||||
|
}
|
||||||
|
return metadata.category();
|
||||||
|
}
|
||||||
|
|
||||||
/** Bundled symbols for the creative tab; the server still validates catalog membership on use. */
|
/** Bundled symbols for the creative tab; the server still validates catalog membership on use. */
|
||||||
public static ItemStack createBundledCreativePage(String path) {
|
public static ItemStack createBundledCreativePage(String path) {
|
||||||
SymbolDisplayMetadata metadata = CREATIVE_SYMBOLS.get(path);
|
CreativeSymbol symbol = CREATIVE_SYMBOLS.get(path);
|
||||||
if (metadata == null) {
|
if (symbol == null) {
|
||||||
throw new IllegalArgumentException("Not a bundled symbol: " + path);
|
throw new IllegalArgumentException("Not a bundled symbol: " + path);
|
||||||
}
|
}
|
||||||
|
SymbolDisplayMetadata metadata = symbol.display();
|
||||||
SymbolReference reference = new SymbolReference(metadata.id());
|
SymbolReference reference = new SymbolReference(metadata.id());
|
||||||
ItemStack page = create(PageContent.symbol(reference));
|
ItemStack page = create(PageContent.symbol(reference));
|
||||||
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), metadata);
|
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), metadata);
|
||||||
@@ -79,7 +119,7 @@ public final class PageItem extends Item {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, SymbolDisplayMetadata> loadCreativeSymbols() {
|
private static Map<String, CreativeSymbol> loadCreativeSymbols() {
|
||||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||||
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
@@ -87,13 +127,18 @@ public final class PageItem extends Item {
|
|||||||
}
|
}
|
||||||
var json = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8))
|
var json = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8))
|
||||||
.getAsJsonObject();
|
.getAsJsonObject();
|
||||||
Map<String, SymbolDisplayMetadata> symbols = new LinkedHashMap<>();
|
Map<String, CreativeSymbol> symbols = new LinkedHashMap<>();
|
||||||
json.entrySet().forEach(entry -> {
|
json.entrySet().forEach(entry -> {
|
||||||
var id = net.minecraft.resources.ResourceLocation.parse(entry.getKey());
|
var id = net.minecraft.resources.ResourceLocation.parse(entry.getKey());
|
||||||
if (!id.getNamespace().equals(Rivencraft.MOD_ID)) {
|
if (!id.getNamespace().equals(Rivencraft.MOD_ID)) {
|
||||||
throw new IllegalStateException("Creative symbol is not a Rivencraft page: " + id);
|
throw new IllegalStateException("Creative symbol is not a Rivencraft page: " + id);
|
||||||
}
|
}
|
||||||
symbols.put(id.getPath(), SymbolDefinition.parse(entry.getValue()).display(id));
|
SymbolDefinition definition = SymbolDefinition.parse(entry.getValue());
|
||||||
|
ResourceLocation creativeCategory = definition.semantics()
|
||||||
|
.filter(semantics -> semantics.role() == SymbolSemantics.Role.BIOME_CONTROLLER)
|
||||||
|
.map(ignored -> BIOME_GENERATION_CATEGORY)
|
||||||
|
.orElse(definition.category());
|
||||||
|
symbols.put(id.getPath(), new CreativeSymbol(definition.display(id), creativeCategory));
|
||||||
});
|
});
|
||||||
return Collections.unmodifiableMap(symbols);
|
return Collections.unmodifiableMap(symbols);
|
||||||
} catch (java.io.IOException exception) {
|
} catch (java.io.IOException exception) {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
package net.dnorberg.rivencraft.registry;
|
package net.dnorberg.rivencraft.registry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
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.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.world.item.CreativeModeTab;
|
import net.minecraft.world.item.CreativeModeTab;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||||
@@ -28,17 +34,39 @@ public final class ModCreativeTabs {
|
|||||||
})
|
})
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> PAGES =
|
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> LINK_PAGES =
|
||||||
CREATIVE_TABS.register("rivencraft_pages", () -> CreativeModeTab.builder()
|
CREATIVE_TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder()
|
||||||
.title(Component.translatable("itemGroup.rivencraft.pages"))
|
.title(Component.translatable("itemGroup.rivencraft.pages.link"))
|
||||||
.icon(PageItem::createLinkPanelPage)
|
.icon(PageItem::createLinkPanelPage)
|
||||||
.displayItems((parameters, output) -> {
|
.displayItems((parameters, output) -> output.accept(PageItem.createLinkPanelPage()))
|
||||||
output.accept(ModItems.PAGE.get());
|
|
||||||
output.accept(PageItem.createLinkPanelPage());
|
|
||||||
PageItem.creativeSymbolPages(parameters.holders()).forEach(output::accept);
|
|
||||||
})
|
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
private static final List<String> CATEGORY_ORDER = List.of(
|
||||||
|
"terrain", "biome", "biome_generation", "weather", "lighting", "sky", "celestial", "feature");
|
||||||
|
|
||||||
|
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||||
|
registerSymbolPageTabs();
|
||||||
|
|
||||||
|
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
|
||||||
|
List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
|
||||||
|
categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
|
||||||
|
int index = CATEGORY_ORDER.indexOf(id.getPath());
|
||||||
|
return index < 0 ? CATEGORY_ORDER.size() : index;
|
||||||
|
}).thenComparing(ResourceLocation::toString));
|
||||||
|
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>();
|
||||||
|
for (ResourceLocation category : categories) {
|
||||||
|
String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
|
||||||
|
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
|
||||||
|
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
|
||||||
|
"Rivencraft " + category.getPath() + " Pages"))
|
||||||
|
.icon(() -> PageItem.bundledCreativeCategoryIcon(category))
|
||||||
|
.displayItems((parameters, output) -> PageItem.creativeSymbolPages(parameters.holders(), category)
|
||||||
|
.forEach(output::accept))
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
return Map.copyOf(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
private ModCreativeTabs() {
|
private ModCreativeTabs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,5 +230,13 @@
|
|||||||
"jei.rivencraft.writing.bind": "At a Book Binder, name the Age and add a cover plus Link Panel. Optional symbol Pages follow the panel.",
|
"jei.rivencraft.writing.bind": "At a Book Binder, name the Age and add a cover plus Link Panel. Optional symbol Pages follow the panel.",
|
||||||
"jei.rivencraft.writing.linking_book": "Craft one Link Panel Page with leather to make an unwritten Linking Book. Open it to name and bind it.",
|
"jei.rivencraft.writing.linking_book": "Craft one Link Panel Page with leather to make an unwritten Linking Book. Open it to name and bind it.",
|
||||||
"itemGroup.rivencraft": "Rivencraft",
|
"itemGroup.rivencraft": "Rivencraft",
|
||||||
"itemGroup.rivencraft.pages": "Rivencraft Pages"
|
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||||
|
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
||||||
|
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
||||||
|
"itemGroup.rivencraft.pages.biome_generation": "Rivencraft Biome Generation Pages",
|
||||||
|
"itemGroup.rivencraft.pages.weather": "Rivencraft Weather Pages",
|
||||||
|
"itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages",
|
||||||
|
"itemGroup.rivencraft.pages.sky": "Rivencraft Sky Pages",
|
||||||
|
"itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages",
|
||||||
|
"itemGroup.rivencraft.pages.feature": "Rivencraft Feature Pages"
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user