Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4541cbbba5 | ||
|
|
68db795216 |
No files matched your search
@@ -21,9 +21,17 @@ The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
|
|||||||
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
||||||
The Ink Mixer now offers a Legacy-style station for producing Link Panels:
|
The Ink Mixer now offers a Legacy-style station for producing Link Panels:
|
||||||
insert paper and Ink Vials, then take a Link Panel from the result slot. Each
|
insert paper and Ink Vials, then take a Link Panel from the result slot. Each
|
||||||
vial fills its 16-unit basin and returns a glass bottle; no server tick is
|
vial fills its 16-unit basin and returns a glass bottle; taking one Link Panel
|
||||||
needed. The earlier paper-in-one-hand method remains available. Ink modifier
|
uses the full vial. No server tick is needed. The earlier paper-in-one-hand
|
||||||
ingredients and link properties are planned for Milestone 5.
|
method still uses the configurable link-panel ink cost. Ink Mixer
|
||||||
|
additives can roll Disarm, Maintain Momentum, and Intra Linking onto a Link
|
||||||
|
Panel. Descriptive Books retain Disarm and Momentum; Intra Linking is only
|
||||||
|
meaningful for same-dimension travel with a Linking Book.
|
||||||
|
TerraBlender 1.21.1 integration is optional: registered modded biomes receive
|
||||||
|
Biome pages, with cave and Nether biomes in a separate Cave Biome creative tab.
|
||||||
|
TerraBlender Overworld/Nether regions extend compatible Age
|
||||||
|
biome pools. See [the compatibility matrix](docs/TERRABLENDER_COMPATIBILITY.md)
|
||||||
|
for supported versions and in-game verification.
|
||||||
The Writing Desk can copy a selected written page from its collection tabs.
|
The Writing Desk can copy a selected written page from its collection tabs.
|
||||||
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
|
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
|
||||||
It occupies two adjacent blocks, with one shared inventory; loaded paper and
|
It occupies two adjacent blocks, with one shared inventory; loaded paper and
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ repositories {
|
|||||||
content { includeGroup 'curse.maven' }
|
content { includeGroup 'curse.maven' }
|
||||||
}
|
}
|
||||||
maven { url = 'https://maven.bai.lol' }
|
maven { url = 'https://maven.bai.lol' }
|
||||||
|
maven {
|
||||||
|
name = 'TerraBlenderMaven'
|
||||||
|
url = 'https://maven.minecraftforge.net/releases/'
|
||||||
|
content { includeGroup 'com.github.glitchfiend' }
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url = 'https://api.modrinth.com/maven'
|
url = 'https://api.modrinth.com/maven'
|
||||||
content { includeGroup 'maven.modrinth' }
|
content { includeGroup 'maven.modrinth' }
|
||||||
@@ -238,6 +243,11 @@ dependencies {
|
|||||||
// Keep them on the compile classpath without forcing either mod at runtime.
|
// Keep them on the compile classpath without forcing either mod at runtime.
|
||||||
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
||||||
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
||||||
|
// TerraBlender's region API is only used by the guarded compat adapter.
|
||||||
|
compileOnly "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
||||||
|
if (providers.gradleProperty('withTerraBlender').map { it.toBoolean() }.orElse(false).get()) {
|
||||||
|
localRuntime "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
||||||
|
}
|
||||||
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
|
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
|
||||||
implementation "lol.bai:badpackets:neo-${badpackets_version}"
|
implementation "lol.bai:badpackets:neo-${badpackets_version}"
|
||||||
// JEI integration is isolated in its plugin and absent from the published runtime dependencies.
|
// JEI integration is isolated in its plugin and absent from the published runtime dependencies.
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ right-click the ink basin once; one Gunpowder should be consumed, and hovering
|
|||||||
the basin should show a 20% Disarm chance. Black Dye, Clay Balls, and an
|
the basin should show a 20% Disarm chance. Black Dye, Clay Balls, and an
|
||||||
unrelated item such as Stone should remain untouched.
|
unrelated item such as Stone should remain untouched.
|
||||||
Closing and reopening the mixer should retain the chance. Taking a Link Panel
|
Closing and reopening the mixer should retain the chance. Taking a Link Panel
|
||||||
should consume one paper and the configured ink cost, clear the current
|
should consume one paper and one full Ink Vial, clear the current
|
||||||
mixture, and possibly give a panel with Disarm on its tooltip. Repeat until
|
mixture, and possibly give a panel with Disarm on its tooltip. Repeat until
|
||||||
a property-bearing panel appears, craft it with leather, and confirm the
|
a property-bearing panel appears, craft it with leather, and confirm the
|
||||||
unwritten Linking Book preserves the property tooltip. After binding it,
|
unwritten Linking Book preserves the property tooltip. After binding it,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Optional TerraBlender biome compatibility (Milestone 5, issue #37)
|
||||||
|
|
||||||
|
Rivencraft targets TerraBlender NeoForge **1.21.1-4.1.0.8**. The official
|
||||||
|
[1.21.1 release](https://modrinth.com/mod/terrablender/version/4.1.0.8) and
|
||||||
|
[region API](https://github.com/Glitchfiend/TerraBlender/blob/1.21.1/Common/src/main/java/terrablender/api/Regions.java)
|
||||||
|
are available for this Minecraft version. TerraBlender is `compileOnly` and
|
||||||
|
is **not** required by the published Rivencraft jar. For a local development
|
||||||
|
run with TerraBlender, use `-PwithTerraBlender=true`.
|
||||||
|
|
||||||
|
TerraBlender's API registers Overworld and Nether biome *regions*; it does not
|
||||||
|
register arbitrary Rivencraft Age dimensions. Rivencraft therefore keeps its
|
||||||
|
own deterministic Age biome sources and uses a guarded adapter to read the
|
||||||
|
registered region biome IDs. Only biomes still present in Minecraft's biome
|
||||||
|
registry are accepted. TerraBlender Overworld region biomes extend the pool
|
||||||
|
for standard, flat, and sky-island Ages; Nether region biomes extend Cave Age
|
||||||
|
pools and are valid authored Cave biome pages. Vanilla Nether-tagged biomes
|
||||||
|
are also valid Cave biomes. This is generic: no Biomes O' Plenty IDs are
|
||||||
|
hard-coded.
|
||||||
|
|
||||||
|
Every registered biome receives a generated page in Rivencraft's Biome or Cave
|
||||||
|
Biome creative tab, and in JEI when JEI is installed. Cave-tagged, Nether-tagged,
|
||||||
|
and TerraBlender Nether-region biomes go in the Cave Biome tab. Existing vanilla page IDs such as
|
||||||
|
`rivencraft:biome/desert` are unchanged; modded biome IDs include their
|
||||||
|
namespace, for example `rivencraft:biome/example/orchard` for a biome named
|
||||||
|
`example:orchard`.
|
||||||
|
Authoring still validates the exact biome registry key. New Age plans freeze
|
||||||
|
the selected biome IDs, distribution, and seed, so a reload with the same mods
|
||||||
|
retains the result. If a modded biome later disappears, the Age loader warns
|
||||||
|
and uses an eligible fallback for that load without rewriting its frozen plan.
|
||||||
|
Restore the mod to recover the authored biome. Do not remove biome mods from
|
||||||
|
an existing play world without a backup.
|
||||||
|
|
||||||
|
## Compatibility matrix
|
||||||
|
|
||||||
|
| Setup | Expected result | Validation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Rivencraft without TerraBlender | Vanilla biome pages and Age generation remain functional; no TerraBlender classes load on the core path. | Default GameTest server and build. |
|
||||||
|
| Rivencraft with TerraBlender 4.1.0.8, no biome addon | Same baseline pool; default TerraBlender regions add no modded biome IDs. | GameTest server with `-PwithTerraBlender=true`. |
|
||||||
|
| Rivencraft + TerraBlender + Biomes O' Plenty 1.21.1 | BOP registry biomes appear as distinct Biome pages, with Nether-region pages in the Cave Biome tab. Its Overworld regions can enter composed standard/sky-island Age pools; its Nether regions can enter Cave Age pools. | In-game check below; this combination is not bundled in automated tests. |
|
||||||
|
| TerraBlender present but its region API cannot be read | Log one warning and use Rivencraft's normal biome pool. | Guarded adapter fallback; intentionally not forced by GameTest. |
|
||||||
|
|
||||||
|
## In-game check requested for Biomes O' Plenty
|
||||||
|
|
||||||
|
Use a **new test world** with compatible NeoForge 1.21.1 builds of
|
||||||
|
TerraBlender and Biomes O' Plenty installed on both sides.
|
||||||
|
|
||||||
|
1. In the Rivencraft Biome and Cave Biome Pages creative tabs (and JEI, if installed), find
|
||||||
|
at least one BOP Overworld biome and one BOP Nether biome, respectively. The page tooltip
|
||||||
|
should identify the biome and not show an unresolved symbol.
|
||||||
|
2. Bind a Descriptive Book with a Link Panel, Standard Terrain, a two-biome
|
||||||
|
distribution page, and two different BOP Overworld biome pages. Travel to
|
||||||
|
the Age and confirm both named biomes can occur. Reopen the same world and
|
||||||
|
confirm the Age still uses the same authored biome pool.
|
||||||
|
3. Bind a second book using Cave Terrain and a BOP Nether biome page. The
|
||||||
|
Binder must accept the page, and the resulting Cave Age must use the
|
||||||
|
selected Nether biome rather than rejecting it as non-cave.
|
||||||
|
4. Separately, run Rivencraft without TerraBlender and confirm a Link-Panel-
|
||||||
|
only random Age can still be created and entered. Do not reuse the BOP test
|
||||||
|
world for this absence check.
|
||||||
|
|
||||||
|
The previous broad registry-enumeration GameTest stalled in the test server,
|
||||||
|
so the BOP-specific and client-side checks are deliberately handed off for
|
||||||
|
manual testing instead of retaining that stalled test.
|
||||||
+2
-1
@@ -36,8 +36,9 @@ 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.7.3
|
mod_version=0.4.8.1
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
|
terrablender_version=4.1.0.8
|
||||||
# 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.
|
||||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import net.minecraft.core.registries.Registries;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||||
import net.minecraft.tags.BiomeTags;
|
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -163,8 +162,8 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
|||||||
if (holder.isEmpty()) {
|
if (holder.isEmpty()) {
|
||||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNKNOWN_BIOME,
|
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNKNOWN_BIOME,
|
||||||
symbol.index(), "Restore biome " + id + " or remove this page"));
|
symbol.index(), "Restore biome " + id + " or remove this page"));
|
||||||
} else if (cave && !holder.orElseThrow().is(AgeBiomeSelection.CAVE_BIOMES)
|
} else if (cave && !AgeBiomeSelection.isCaveBiome(level.registryAccess(),
|
||||||
&& !holder.orElseThrow().is(BiomeTags.IS_NETHER)) {
|
holder.orElseThrow())) {
|
||||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNSUPPORTED_BIOME,
|
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNSUPPORTED_BIOME,
|
||||||
symbol.index(), "Cave Ages require cave or Nether biomes; remove " + id));
|
symbol.index(), "Cave Ages require cave or Nether biomes; remove " + id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
|||||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.NonNullList;
|
import net.minecraft.core.NonNullList;
|
||||||
@@ -127,14 +126,13 @@ public final class InkMixerBlockEntity extends BlockEntity implements Container
|
|||||||
|
|
||||||
public boolean canCreateLinkPanel() {
|
public boolean canCreateLinkPanel() {
|
||||||
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
||||||
int cost = WritingInkService.linkPanelCost();
|
return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, InkVialItem.CAPACITY);
|
||||||
return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, cost);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Commit inputs only when a virtual output is actually taken. */
|
/** Commit inputs only when a virtual output is actually taken. */
|
||||||
public boolean consumeLinkPanelMaterials() {
|
public boolean consumeLinkPanelMaterials() {
|
||||||
if (!canCreateLinkPanel()) return false;
|
if (!canCreateLinkPanel()) return false;
|
||||||
int cost = WritingInkService.linkPanelCost();
|
int cost = InkVialItem.CAPACITY;
|
||||||
if (inkRemaining < cost) {
|
if (inkRemaining < cost) {
|
||||||
inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES);
|
inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
@@ -59,9 +60,10 @@ public record AgeBiomeSelection(AgeTraits.BiomeDistribution distribution, List<R
|
|||||||
for (ResourceLocation id : authored) {
|
for (ResourceLocation id : authored) {
|
||||||
Holder.Reference<Biome> holder = registry.get(ResourceKey.create(Registries.BIOME, id))
|
Holder.Reference<Biome> holder = registry.get(ResourceKey.create(Registries.BIOME, id))
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Unknown biome " + id + "; restore its data pack or remove its page"));
|
.orElseThrow(() -> new IllegalArgumentException("Unknown biome " + id + "; restore its data pack or remove its page"));
|
||||||
if (cave && !holder.is(CAVE_BIOMES) && !holder.is(BiomeTags.IS_NETHER)) {
|
if (cave && !isCaveBiome(registries, holder)) {
|
||||||
throw new IllegalArgumentException("Biome " + id
|
throw new IllegalArgumentException("Biome " + id
|
||||||
+ " is not a cave or Nether biome; use #rivencraft:cave_biomes or #minecraft:is_nether");
|
+ " is not a cave or Nether biome; use #rivencraft:cave_biomes, #minecraft:is_nether,"
|
||||||
|
+ " or a TerraBlender Nether region");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<ResourceLocation> availableBiomes = eligible(registries, profile);
|
List<ResourceLocation> availableBiomes = eligible(registries, profile);
|
||||||
@@ -69,15 +71,16 @@ public record AgeBiomeSelection(AgeTraits.BiomeDistribution distribution, List<R
|
|||||||
if (distribution == AgeTraits.BiomeDistribution.NATIVE) {
|
if (distribution == AgeTraits.BiomeDistribution.NATIVE) {
|
||||||
if (!authored.isEmpty()) throw new IllegalArgumentException("Native biome distribution cannot use biome pages");
|
if (!authored.isEmpty()) throw new IllegalArgumentException("Native biome distribution cannot use biome pages");
|
||||||
return new AgeBiomeSelection(distribution, profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
return new AgeBiomeSelection(distribution, profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
||||||
? List.of(ResourceLocation.withDefaultNamespace("plains")) : availableBiomes);
|
? List.of(ResourceLocation.withDefaultNamespace("plains"))
|
||||||
|
: isCave(profile) ? availableBiomes : presetBiomes(registries, profile));
|
||||||
}
|
}
|
||||||
List<ResourceLocation> selected = new ArrayList<>(authored);
|
List<ResourceLocation> selected = new ArrayList<>(authored);
|
||||||
Set<ResourceLocation> distinct = new LinkedHashSet<>(selected);
|
Set<ResourceLocation> distinct = new LinkedHashSet<>(selected);
|
||||||
int minimum = minimum(distribution);
|
int minimum = minimum(distribution);
|
||||||
int target = minimum;
|
int target = minimum;
|
||||||
if (authored.isEmpty()) {
|
if (authored.isEmpty()) {
|
||||||
// The terrain's available pool is the upper bound. For Cave terrain,
|
// The terrain's available pool is the upper bound, including any
|
||||||
// eligible() returns the cave tag; other terrains use their preset.
|
// compatible TerraBlender region biomes present at creation time.
|
||||||
int availableBiomeCount = new LinkedHashSet<>(availableBiomes).size();
|
int availableBiomeCount = new LinkedHashSet<>(availableBiomes).size();
|
||||||
int maximum = distribution == AgeTraits.BiomeDistribution.SINGLE ? 1 : availableBiomeCount;
|
int maximum = distribution == AgeTraits.BiomeDistribution.SINGLE ? 1 : availableBiomeCount;
|
||||||
if (maximum >= minimum) {
|
if (maximum >= minimum) {
|
||||||
@@ -102,14 +105,26 @@ public record AgeBiomeSelection(AgeTraits.BiomeDistribution distribution, List<R
|
|||||||
if (isCave(profile)) {
|
if (isCave(profile)) {
|
||||||
List<ResourceLocation> tagged = registry.listElements().filter(holder -> holder.is(CAVE_BIOMES))
|
List<ResourceLocation> tagged = registry.listElements().filter(holder -> holder.is(CAVE_BIOMES))
|
||||||
.map(holder -> holder.key().location()).sorted().toList();
|
.map(holder -> holder.key().location()).sorted().toList();
|
||||||
if (!tagged.isEmpty()) return tagged;
|
List<ResourceLocation> vanilla = !tagged.isEmpty() ? tagged : List.of("minecraft:deep_dark", "minecraft:dripstone_caves", "minecraft:lush_caves")
|
||||||
return List.of("minecraft:deep_dark", "minecraft:dripstone_caves", "minecraft:lush_caves")
|
|
||||||
.stream().map(ResourceLocation::parse)
|
.stream().map(ResourceLocation::parse)
|
||||||
.filter(id -> registry.get(ResourceKey.create(Registries.BIOME, id)).isPresent()).toList();
|
.filter(id -> registry.get(ResourceKey.create(Registries.BIOME, id)).isPresent()).toList();
|
||||||
|
List<ResourceLocation> nether = registry.listElements().filter(holder -> holder.is(BiomeTags.IS_NETHER))
|
||||||
|
.map(holder -> holder.key().location()).toList();
|
||||||
|
return merge(merge(vanilla, nether), TerraBlenderBiomes.nether(registries));
|
||||||
}
|
}
|
||||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
||||||
return List.of(ResourceLocation.withDefaultNamespace("the_void"));
|
return List.of(ResourceLocation.withDefaultNamespace("the_void"));
|
||||||
}
|
}
|
||||||
|
ResourceLocation preset = profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
||||||
|
? ResourceLocation.withDefaultNamespace("overworld") : profile.biomePreset();
|
||||||
|
List<ResourceLocation> nativeBiomes = presetBiomes(registries, profile);
|
||||||
|
return merge(nativeBiomes, preset.equals(ResourceLocation.withDefaultNamespace("nether"))
|
||||||
|
? TerraBlenderBiomes.nether(registries) : TerraBlenderBiomes.overworld(registries));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ResourceLocation> presetBiomes(RegistryAccess registries, AgeRuntimeProfile profile) {
|
||||||
|
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||||
|
return List.of(ResourceLocation.withDefaultNamespace("the_void"));
|
||||||
ResourceLocation preset = profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
ResourceLocation preset = profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
||||||
? ResourceLocation.withDefaultNamespace("overworld") : profile.biomePreset();
|
? ResourceLocation.withDefaultNamespace("overworld") : profile.biomePreset();
|
||||||
var parameters = registries.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)
|
var parameters = registries.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)
|
||||||
@@ -119,6 +134,15 @@ public record AgeBiomeSelection(AgeTraits.BiomeDistribution distribution, List<R
|
|||||||
.sorted(Comparator.naturalOrder()).toList();
|
.sorted(Comparator.naturalOrder()).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isCaveBiome(RegistryAccess registries, Holder.Reference<Biome> holder) {
|
||||||
|
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||||
|
|| TerraBlenderBiomes.nether(registries).contains(holder.key().location());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ResourceLocation> merge(List<ResourceLocation> vanilla, List<ResourceLocation> additions) {
|
||||||
|
return java.util.stream.Stream.concat(vanilla.stream(), additions.stream()).distinct().sorted().toList();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isCave(AgeRuntimeProfile profile) {
|
public static boolean isCave(AgeRuntimeProfile profile) {
|
||||||
return profile.terrain() == AgeRuntimeProfile.Terrain.NOISE
|
return profile.terrain() == AgeRuntimeProfile.Terrain.NOISE
|
||||||
&& profile.noiseSettings().equals(ResourceLocation.withDefaultNamespace("caves"));
|
&& profile.noiseSettings().equals(ResourceLocation.withDefaultNamespace("caves"));
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
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.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
@@ -26,38 +24,23 @@ public final class EnvironmentWritingGameTests {
|
|||||||
private EnvironmentWritingGameTests() {}
|
private EnvironmentWritingGameTests() {}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void symbolCategoriesPartitionCreativePages(GameTestHelper helper) {
|
public static void caveBiomesHaveSeparateCreativePages(GameTestHelper helper) {
|
||||||
var registries = helper.getLevel().registryAccess();
|
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,
|
var biomePages = PageItem.creativeSymbolPages(registries,
|
||||||
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome"));
|
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome"));
|
||||||
var generationPages = PageItem.creativeSymbolPages(registries,
|
var cavePages = PageItem.creativeSymbolPages(registries,
|
||||||
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome_generation"));
|
ResourceLocation.fromNamespaceAndPath("rivencraft", "cave_biome"));
|
||||||
|
helper.assertTrue(PageItem.bundledCreativeCategories().contains(
|
||||||
|
ResourceLocation.fromNamespaceAndPath("rivencraft", "cave_biome")),
|
||||||
|
"The cave biome creative tab must be registered");
|
||||||
helper.assertTrue(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
helper.assertTrue(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
.id().getPath().equals("biome/desert")), "Biome pages must remain in the Biome tab");
|
.id().getPath().equals("biome/desert")), "Biome pages must remain in the Biome tab");
|
||||||
|
helper.assertTrue(cavePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
|
.id().getPath().equals("biome/lush_caves")), "Cave biomes need their own tab");
|
||||||
|
helper.assertTrue(cavePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
|
.id().getPath().equals("biome/crimson_forest")), "Nether biomes belong in the Cave tab");
|
||||||
helper.assertFalse(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
helper.assertFalse(biomePages.stream().anyMatch(page -> PageItem.content(page).symbol().orElseThrow()
|
||||||
.id().getPath().equals("biome/tiny")), "Biome generation types must leave the Biome tab");
|
.id().getPath().equals("biome/lush_caves")), "Cave biomes must leave the general 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();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.fml.ModList;
|
||||||
|
|
||||||
|
/** Optional boundary: core Age code never loads TerraBlender API classes. */
|
||||||
|
public final class TerraBlenderBiomes {
|
||||||
|
private static final AtomicBoolean WARNED = new AtomicBoolean();
|
||||||
|
private TerraBlenderBiomes() {}
|
||||||
|
|
||||||
|
public static List<ResourceLocation> overworld(RegistryAccess registries) {
|
||||||
|
return collect(registries, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ResourceLocation> nether(RegistryAccess registries) {
|
||||||
|
return collect(registries, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ResourceLocation> collect(RegistryAccess registries, boolean nether) {
|
||||||
|
if (!ModList.get().isLoaded("terrablender")) return List.of();
|
||||||
|
try {
|
||||||
|
return TerraBlenderRegionAdapter.collect(registries, nether);
|
||||||
|
} catch (RuntimeException | LinkageError exception) {
|
||||||
|
if (WARNED.compareAndSet(false, true))
|
||||||
|
Rivencraft.LOGGER.warn("TerraBlender biome regions could not be read; using Rivencraft's biome pool", exception);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import terrablender.api.Region;
|
||||||
|
import terrablender.api.RegionType;
|
||||||
|
import terrablender.api.Regions;
|
||||||
|
|
||||||
|
/** Loaded only after the optional TerraBlender mod is confirmed present. */
|
||||||
|
final class TerraBlenderRegionAdapter {
|
||||||
|
private TerraBlenderRegionAdapter() {}
|
||||||
|
|
||||||
|
static List<ResourceLocation> collect(RegistryAccess registries, boolean nether) {
|
||||||
|
var biomes = registries.registryOrThrow(Registries.BIOME);
|
||||||
|
RegionType type = nether ? RegionType.NETHER : RegionType.OVERWORLD;
|
||||||
|
Set<ResourceLocation> ids = new LinkedHashSet<>();
|
||||||
|
for (Region region : Regions.get(type)) {
|
||||||
|
region.addBiomes(biomes, mapping -> {
|
||||||
|
ResourceKey<Biome> key = mapping.getSecond();
|
||||||
|
ResourceLocation id = key.location();
|
||||||
|
// Vanilla's default region is always present. Only additions from
|
||||||
|
// other mods extend an Age's existing deterministic biome pool.
|
||||||
|
if (!id.getNamespace().equals("minecraft") && biomes.containsKey(id)) ids.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ids.stream().sorted().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
|||||||
import net.dnorberg.rivencraft.book.AgeReference;
|
import net.dnorberg.rivencraft.book.AgeReference;
|
||||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||||
|
import net.dnorberg.rivencraft.book.LinkingService;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
@@ -17,6 +20,7 @@ import net.dnorberg.rivencraft.writing.SymbolReference;
|
|||||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.GlobalPos;
|
||||||
import net.minecraft.gametest.framework.GameTest;
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
import net.minecraft.gametest.framework.GameTestHelper;
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -185,6 +189,50 @@ public final class BookBinderGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void panelOnlyBookRetainsApplicableModifiers(GameTestHelper helper) {
|
||||||
|
BookBinderBlockEntity binder = place(helper);
|
||||||
|
binder.setTitle("Modified Random Age");
|
||||||
|
binder.setItem(BookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||||
|
ItemStack panel = PageItem.createLinkPanelPage();
|
||||||
|
panel.set(ModDataComponents.LINK_PROPERTIES.get(), new LinkProperties(
|
||||||
|
LinkProperties.DISARM | LinkProperties.MAINTAIN_MOMENTUM | LinkProperties.INTRA_LINKING));
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.moveTo(binder.getBlockPos().getX() + 0.5, binder.getBlockPos().getY() + 0.5,
|
||||||
|
binder.getBlockPos().getZ() + 0.5);
|
||||||
|
BookBinderMenu menu = new BookBinderMenu(1, player.getInventory(), binder);
|
||||||
|
menu.setCarried(panel);
|
||||||
|
helper.assertTrue(menu.clickMenuButton(player, BookBinderMenu.STRIP_CLICK_BASE),
|
||||||
|
"The link strip must accept the modified panel without symbol pages");
|
||||||
|
ItemStack result = menu.quickMoveStack(player, BookBinderMenu.RESULT);
|
||||||
|
helper.assertTrue(result.is(ModItems.DESCRIPTIVE_BOOK.get()),
|
||||||
|
"A Link Panel alone must create a random-Age Descriptive Book");
|
||||||
|
LinkProperties expected = new LinkProperties(LinkProperties.DISARM | LinkProperties.MAINTAIN_MOMENTUM);
|
||||||
|
helper.assertValueEqual(result.get(ModDataComponents.LINK_PROPERTIES.get()), expected,
|
||||||
|
"The panel-only book must keep travel modifiers but omit Intra Linking");
|
||||||
|
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(),
|
||||||
|
(net.minecraft.nbt.CompoundTag) result.saveOptional(helper.getLevel().registryAccess()));
|
||||||
|
helper.assertValueEqual(restored.get(ModDataComponents.LINK_PROPERTIES.get()), expected,
|
||||||
|
"Panel-only book modifiers must survive save and reload");
|
||||||
|
BlockPos departure = player.blockPosition();
|
||||||
|
BlockPos destination = departure.offset(3, 0, 0);
|
||||||
|
helper.getLevel().setBlock(destination.below(), Blocks.STONE.defaultBlockState(), 3);
|
||||||
|
helper.getLevel().setBlock(destination, Blocks.AIR.defaultBlockState(), 3);
|
||||||
|
helper.getLevel().setBlock(destination.above(), Blocks.AIR.defaultBlockState(), 3);
|
||||||
|
player.getInventory().setItem(4, new ItemStack(Items.DIAMOND, 3));
|
||||||
|
LinkTarget target = new LinkTarget(GlobalPos.of(helper.getLevel().dimension(), destination), 0.0F, 0.0F);
|
||||||
|
helper.assertValueEqual(LinkingService.travelWithFooting(player, target,
|
||||||
|
restored.get(ModDataComponents.LINK_PROPERTIES.get()).forDescriptiveBook()),
|
||||||
|
LinkingService.LinkResult.SUCCESS,
|
||||||
|
"The panel-only book's properties must be accepted by grounded travel");
|
||||||
|
helper.assertTrue(player.getInventory().getItem(4).isEmpty(),
|
||||||
|
"Disarm must remove the carried items during panel-only book travel");
|
||||||
|
helper.assertTrue(helper.getLevel().getEntitiesOfClass(ItemEntity.class,
|
||||||
|
new AABB(departure).inflate(5.0), entity -> entity.getItem().is(Items.DIAMOND))
|
||||||
|
.stream().mapToInt(entity -> entity.getItem().getCount()).sum() >= 3,
|
||||||
|
"Disarm must drop the items at the departure location");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void linkPanelOnlyCreatesCompletedRandomAgeBook(GameTestHelper helper) {
|
public static void linkPanelOnlyCreatesCompletedRandomAgeBook(GameTestHelper helper) {
|
||||||
BookBinderBlockEntity binder = place(helper);
|
BookBinderBlockEntity binder = place(helper);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package net.dnorberg.rivencraft.gametest;
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
||||||
import net.minecraft.gametest.framework.GameTest;
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
import net.minecraft.gametest.framework.GameTestHelper;
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@@ -87,6 +89,21 @@ public final class CoreGameTests {
|
|||||||
BookBinderGameTests.assemblyIsAtomicAndRetainsPageOrder(helper);
|
BookBinderGameTests.assemblyIsAtomicAndRetainsPageOrder(helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void panelOnlyBookModifiers(GameTestHelper helper) {
|
||||||
|
BookBinderGameTests.panelOnlyBookRetainsApplicableModifiers(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void biomePageNamespaces(GameTestHelper helper) {
|
||||||
|
helper.assertValueEqual(BiomeSymbolPages.symbolId(ResourceLocation.parse("minecraft:desert")),
|
||||||
|
ResourceLocation.parse("rivencraft:biome/desert"), "Vanilla biome page IDs must remain stable");
|
||||||
|
helper.assertFalse(BiomeSymbolPages.symbolId(ResourceLocation.parse("example:orchard"))
|
||||||
|
.equals(BiomeSymbolPages.symbolId(ResourceLocation.parse("another:orchard"))),
|
||||||
|
"Equal biome paths from different mods must not share a page ID");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void binderRejectsConflictingGrammar(GameTestHelper helper) {
|
public static void binderRejectsConflictingGrammar(GameTestHelper helper) {
|
||||||
BookBinderGameTests.conflictingGrammarKeepsBinderInputsRecoverable(helper);
|
BookBinderGameTests.conflictingGrammarKeepsBinderInputsRecoverable(helper);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
|||||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.gametest.framework.GameTest;
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
import net.minecraft.gametest.framework.GameTestHelper;
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
@@ -111,8 +110,9 @@ public final class InkMixerGameTests {
|
|||||||
helper.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give a Link Panel");
|
helper.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give a Link Panel");
|
||||||
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
||||||
"Taking result must use exactly one paper");
|
"Taking result must use exactly one paper");
|
||||||
helper.assertValueEqual(mixer.inkRemaining(), 16 - WritingInkService.linkPanelCost(),
|
helper.assertValueEqual(mixer.inkRemaining(), 0,
|
||||||
"Taking result must spend the configured ink cost");
|
"Taking one panel must spend the entire Ink Vial");
|
||||||
|
helper.assertFalse(mixer.canCreateLinkPanel(), "A second panel needs another Ink Vial");
|
||||||
var saved = mixer.saveWithoutMetadata(helper.getLevel().registryAccess());
|
var saved = mixer.saveWithoutMetadata(helper.getLevel().registryAccess());
|
||||||
var restored = new InkMixerBlockEntity(mixer.getBlockPos(), ModBlocks.INK_MIXER.get().defaultBlockState());
|
var restored = new InkMixerBlockEntity(mixer.getBlockPos(), ModBlocks.INK_MIXER.get().defaultBlockState());
|
||||||
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
|
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
|
||||||
@@ -122,6 +122,24 @@ public final class InkMixerGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void eachLinkPanelConsumesOneInkVial(GameTestHelper helper) {
|
||||||
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
|
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 3));
|
||||||
|
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get(), 2));
|
||||||
|
helper.assertTrue(mixer.canCreateLinkPanel(), "First vial must enable the first panel");
|
||||||
|
helper.assertTrue(mixer.consumeLinkPanelMaterials(), "First panel must be produced");
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 16, "Second vial must refill after first panel");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.BOTTLES).getCount(), 2,
|
||||||
|
"Both spent vials must return bottles");
|
||||||
|
helper.assertTrue(mixer.consumeLinkPanelMaterials(), "Second panel must be produced");
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 0, "Second panel must use the whole second vial");
|
||||||
|
helper.assertFalse(mixer.canCreateLinkPanel(), "Two vials must not make a third panel");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
||||||
|
"Each panel must also use one paper");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void bottleCapacityBlocksAndThenPermitsRefill(GameTestHelper helper) {
|
public static void bottleCapacityBlocksAndThenPermitsRefill(GameTestHelper helper) {
|
||||||
InkMixerBlockEntity mixer = place(helper);
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashSet;
|
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.book.AgeBiomeSelection;
|
||||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
|
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
@@ -25,9 +27,11 @@ 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.RegistryAccess;
|
||||||
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;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
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;
|
||||||
@@ -36,6 +40,10 @@ import net.minecraft.world.item.TooltipFlag;
|
|||||||
public final class PageItem extends Item {
|
public final class PageItem extends Item {
|
||||||
private static final ResourceLocation BIOME_GENERATION_CATEGORY =
|
private static final ResourceLocation BIOME_GENERATION_CATEGORY =
|
||||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome_generation");
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome_generation");
|
||||||
|
private static final ResourceLocation CAVE_BIOME_CATEGORY =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "cave_biome");
|
||||||
|
private static final ResourceLocation BIOME_CATEGORY =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome");
|
||||||
private static final Map<String, CreativeSymbol> CREATIVE_SYMBOLS = loadCreativeSymbols();
|
private static final Map<String, CreativeSymbol> CREATIVE_SYMBOLS = loadCreativeSymbols();
|
||||||
|
|
||||||
private record CreativeSymbol(SymbolDisplayMetadata display, ResourceLocation creativeCategory) {}
|
private record CreativeSymbol(SymbolDisplayMetadata display, ResourceLocation creativeCategory) {}
|
||||||
@@ -62,10 +70,14 @@ public final class PageItem extends Item {
|
|||||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||||
categories.add(BlockSymbolPages.CATEGORY);
|
categories.add(BlockSymbolPages.CATEGORY);
|
||||||
categories.add(BlockSymbolPages.FLUID_CATEGORY);
|
categories.add(BlockSymbolPages.FLUID_CATEGORY);
|
||||||
|
categories.add(CAVE_BIOME_CATEGORY);
|
||||||
return List.copyOf(categories);
|
return List.copyOf(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ItemStack bundledCreativeCategoryIcon(ResourceLocation category) {
|
public static ItemStack bundledCreativeCategoryIcon(ResourceLocation category) {
|
||||||
|
if (category.equals(CAVE_BIOME_CATEGORY)) {
|
||||||
|
return createCreativeBiomePage(ResourceLocation.withDefaultNamespace("lush_caves"));
|
||||||
|
}
|
||||||
if (category.equals(BlockSymbolPages.CATEGORY) || category.equals(BlockSymbolPages.FLUID_CATEGORY)) {
|
if (category.equals(BlockSymbolPages.CATEGORY) || category.equals(BlockSymbolPages.FLUID_CATEGORY)) {
|
||||||
return createCreativeBlockPage(ResourceLocation.withDefaultNamespace(
|
return createCreativeBlockPage(ResourceLocation.withDefaultNamespace(
|
||||||
category.equals(BlockSymbolPages.FLUID_CATEGORY) ? "water" : "stone"));
|
category.equals(BlockSymbolPages.FLUID_CATEGORY) ? "water" : "stone"));
|
||||||
@@ -90,8 +102,7 @@ public final class PageItem extends Item {
|
|||||||
Set<String> bundled = CREATIVE_SYMBOLS.keySet();
|
Set<String> bundled = CREATIVE_SYMBOLS.keySet();
|
||||||
registries.lookupOrThrow(Registries.BIOME).listElements()
|
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
.map(holder -> holder.key().location())
|
.map(holder -> holder.key().location())
|
||||||
.filter(id -> id.getNamespace().equals("minecraft"))
|
.filter(id -> !bundled.contains(BiomeSymbolPages.symbolId(id).getPath()))
|
||||||
.filter(id -> !bundled.contains("biome/" + id.getPath()))
|
|
||||||
.sorted()
|
.sorted()
|
||||||
.forEach(id -> pages.add(createCreativeBiomePage(id)));
|
.forEach(id -> pages.add(createCreativeBiomePage(id)));
|
||||||
return pages;
|
return pages;
|
||||||
@@ -103,11 +114,34 @@ public final class PageItem extends Item {
|
|||||||
return BlockSymbolPages.availableBlocks(category).stream()
|
return BlockSymbolPages.availableBlocks(category).stream()
|
||||||
.map(PageItem::createCreativeBlockPage).toList();
|
.map(PageItem::createCreativeBlockPage).toList();
|
||||||
}
|
}
|
||||||
|
Set<ResourceLocation> caveBiomes = category.equals(BIOME_CATEGORY) || category.equals(CAVE_BIOME_CATEGORY)
|
||||||
|
? caveBiomeIds(registries) : Set.of();
|
||||||
return creativeBundledAndBiomePages(registries).stream()
|
return creativeBundledAndBiomePages(registries).stream()
|
||||||
.filter(page -> category.equals(creativeCategory(page)))
|
.filter(page -> category.equals(creativeCategory(page, caveBiomes)))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation creativeCategory(ItemStack page, HolderLookup.Provider registries) {
|
||||||
|
return creativeCategory(page, caveBiomeIds(registries));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceLocation creativeCategory(ItemStack page, Set<ResourceLocation> caveBiomes) {
|
||||||
|
ResourceLocation category = creativeCategory(page);
|
||||||
|
if (!category.equals(BIOME_CATEGORY)) return category;
|
||||||
|
ResourceLocation symbolId = content(page).symbol().orElseThrow().id();
|
||||||
|
ResourceLocation biomeId = BiomeSymbolPages.biomeId(symbolId);
|
||||||
|
return caveBiomes.contains(biomeId) ? CAVE_BIOME_CATEGORY : category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<ResourceLocation> caveBiomeIds(HolderLookup.Provider registries) {
|
||||||
|
LinkedHashSet<ResourceLocation> ids = new LinkedHashSet<>();
|
||||||
|
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
|
.filter(holder -> holder.is(AgeBiomeSelection.CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER))
|
||||||
|
.forEach(holder -> ids.add(holder.key().location()));
|
||||||
|
if (registries instanceof RegistryAccess access) ids.addAll(TerraBlenderBiomes.nether(access));
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
public static ResourceLocation creativeCategory(ItemStack page) {
|
public static ResourceLocation creativeCategory(ItemStack page) {
|
||||||
SymbolDisplayMetadata metadata = page.get(ModDataComponents.PAGE_SYMBOL_DISPLAY.get());
|
SymbolDisplayMetadata metadata = page.get(ModDataComponents.PAGE_SYMBOL_DISPLAY.get());
|
||||||
if (metadata == null) throw new IllegalArgumentException("Page has no symbol display metadata");
|
if (metadata == null) throw new IllegalArgumentException("Page has no symbol display metadata");
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public final class ModCreativeTabs {
|
|||||||
.build());
|
.build());
|
||||||
|
|
||||||
private static final List<String> CATEGORY_ORDER = List.of(
|
private static final List<String> CATEGORY_ORDER = List.of(
|
||||||
"terrain", "biome", "biome_generation", "weather", "lighting", "sky", "celestial", "block", "fluid", "feature");
|
"terrain", "biome", "cave_biome", "biome_generation", "weather", "lighting", "sky", "celestial", "block", "fluid", "feature");
|
||||||
|
|
||||||
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||||
registerSymbolPageTabs();
|
registerSymbolPageTabs();
|
||||||
|
|||||||
@@ -6,14 +6,27 @@ import java.util.Optional;
|
|||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
/** Built-in fallback pages for registered vanilla biomes without hand-authored symbol JSON. */
|
/** Registry-derived pages for biomes without hand-authored symbol JSON. */
|
||||||
public final class BiomeSymbolPages {
|
public final class BiomeSymbolPages {
|
||||||
private static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome");
|
private static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome");
|
||||||
|
|
||||||
private BiomeSymbolPages() { }
|
private BiomeSymbolPages() { }
|
||||||
|
|
||||||
public static ResourceLocation symbolId(ResourceLocation biomeId) {
|
public static ResourceLocation symbolId(ResourceLocation biomeId) {
|
||||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/" + biomeId.getPath());
|
// Preserve old vanilla page IDs, while keeping modded namespaces distinct.
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/"
|
||||||
|
+ (biomeId.getNamespace().equals("minecraft") ? "" : biomeId.getNamespace() + "/")
|
||||||
|
+ biomeId.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation biomeId(ResourceLocation symbolId) {
|
||||||
|
String path = symbolId.getPath();
|
||||||
|
if (!symbolId.getNamespace().equals(Rivencraft.MOD_ID) || !path.startsWith("biome/"))
|
||||||
|
throw new IllegalArgumentException("Not a biome symbol: " + symbolId);
|
||||||
|
path = path.substring("biome/".length());
|
||||||
|
int separator = path.indexOf('/');
|
||||||
|
return separator < 0 ? ResourceLocation.withDefaultNamespace(path)
|
||||||
|
: ResourceLocation.fromNamespaceAndPath(path.substring(0, separator), path.substring(separator + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SymbolDefinition definition(ResourceLocation biomeId) {
|
public static SymbolDefinition definition(ResourceLocation biomeId) {
|
||||||
@@ -24,8 +37,11 @@ public final class BiomeSymbolPages {
|
|||||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||||
.reduce("", String::concat);
|
.reduce("", String::concat);
|
||||||
if (glyphWord.isEmpty()) glyphWord = "World";
|
if (glyphWord.isEmpty()) glyphWord = "World";
|
||||||
|
if (glyphWord.length() > 48) glyphWord = glyphWord.substring(0, 48);
|
||||||
return new SymbolDefinition(SymbolDefinition.CURRENT_SCHEMA_VERSION,
|
return new SymbolDefinition(SymbolDefinition.CURRENT_SCHEMA_VERSION,
|
||||||
"symbol.rivencraft.biome." + path,
|
biomeId.getNamespace().equals("minecraft")
|
||||||
|
? "symbol.rivencraft.biome." + path
|
||||||
|
: "biome." + biomeId.getNamespace() + "." + path.replace('/', '.'),
|
||||||
"symbol.rivencraft.biome.generated.description",
|
"symbol.rivencraft.biome.generated.description",
|
||||||
CATEGORY,
|
CATEGORY,
|
||||||
Optional.of(new SymbolSemantics(SymbolSemantics.Role.BIOME,
|
Optional.of(new SymbolSemantics(SymbolSemantics.Role.BIOME,
|
||||||
@@ -35,9 +51,14 @@ public final class BiomeSymbolPages {
|
|||||||
|
|
||||||
public static String fallbackName(ResourceLocation symbolId) {
|
public static String fallbackName(ResourceLocation symbolId) {
|
||||||
String path = symbolId.getPath().substring("biome/".length());
|
String path = symbolId.getPath().substring("biome/".length());
|
||||||
return Arrays.stream(path.split("_"))
|
int separator = path.indexOf('/');
|
||||||
|
String namespace = separator < 0 ? "" : path.substring(0, separator);
|
||||||
|
if (separator >= 0) path = path.substring(separator + 1);
|
||||||
|
String label = path.replace('/', '_');
|
||||||
|
String name = Arrays.stream(label.split("_"))
|
||||||
.filter(part -> !part.isEmpty())
|
.filter(part -> !part.isEmpty())
|
||||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||||
.reduce((left, right) -> left + " " + right).orElse("Unknown") + " Biome";
|
.reduce((left, right) -> left + " " + right).orElse("Unknown") + " Biome";
|
||||||
|
return namespace.isEmpty() ? name : name + " (" + namespace + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,6 @@ public final class SymbolCatalog {
|
|||||||
});
|
});
|
||||||
registries.lookupOrThrow(Registries.BIOME).listElements()
|
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
.map(holder -> holder.key().location())
|
.map(holder -> holder.key().location())
|
||||||
.filter(id -> id.getNamespace().equals("minecraft"))
|
|
||||||
.forEach(id -> loaded.putIfAbsent(BiomeSymbolPages.symbolId(id),
|
.forEach(id -> loaded.putIfAbsent(BiomeSymbolPages.symbolId(id),
|
||||||
BiomeSymbolPages.definition(id)));
|
BiomeSymbolPages.definition(id)));
|
||||||
BlockSymbolPages.availableBlocks().forEach(id -> loaded.putIfAbsent(
|
BlockSymbolPages.availableBlocks().forEach(id -> loaded.putIfAbsent(
|
||||||
|
|||||||
@@ -258,6 +258,7 @@
|
|||||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||||
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
||||||
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
||||||
|
"itemGroup.rivencraft.pages.cave_biome": "Rivencraft Cave Biome Pages",
|
||||||
"itemGroup.rivencraft.pages.biome_generation": "Rivencraft Biome Generation Pages",
|
"itemGroup.rivencraft.pages.biome_generation": "Rivencraft Biome Generation Pages",
|
||||||
"itemGroup.rivencraft.pages.weather": "Rivencraft Weather Pages",
|
"itemGroup.rivencraft.pages.weather": "Rivencraft Weather Pages",
|
||||||
"itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages",
|
"itemGroup.rivencraft.pages.lighting": "Rivencraft Lighting Pages",
|
||||||
|
|||||||
Reference in new issue
Block a user