Public Access
Apply authored biome distributions and expose biome pages (#33)
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
e28079566b
commit
050a9537e9
44 files changed
+981
-133
No files matched your search
@@ -10,6 +10,7 @@ import net.dnorberg.rivencraft.registry.ModSounds;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||
import net.dnorberg.rivencraft.registry.ModBiomeSources;
|
||||
import net.dnorberg.rivencraft.registry.ModChunkGenerators;
|
||||
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
||||
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
||||
import net.dnorberg.rivencraft.event.AgeLifecycleEvents;
|
||||
@@ -40,6 +41,7 @@ public final class Rivencraft {
|
||||
modEventBus.addListener(ModNetworking::register);
|
||||
ModDataComponents.register(modEventBus);
|
||||
ModBiomeSources.register(modEventBus);
|
||||
ModChunkGenerators.register(modEventBus);
|
||||
ModBlocks.register(modEventBus);
|
||||
ModBlockEntities.register(modEventBus);
|
||||
ModItems.register(modEventBus);
|
||||
|
||||
@@ -21,6 +21,10 @@ import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -116,7 +120,12 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
OrderedWriting writing = currentWriting();
|
||||
if (!writingGrammar().canCreate()) return false;
|
||||
try {
|
||||
AgeGenerationPlan.compile(writing, 0L, AgeGenerationProfiles.STANDARD);
|
||||
if (level instanceof ServerLevel serverLevel) {
|
||||
AgeRegistrySavedData.validateNewWriting(serverLevel, title, writing);
|
||||
} else {
|
||||
AgeGenerationPlan.compile(writing, 0L, AgeGenerationProfiles.STANDARD,
|
||||
net.dnorberg.rivencraft.book.AgeTraits.select(0L), level.registryAccess());
|
||||
}
|
||||
return true;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
@@ -126,15 +135,40 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
public WritingGrammar.Result writingGrammar() {
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(currentWriting());
|
||||
if (!parsed.canCreate()) return parsed;
|
||||
List<WritingGrammar.Diagnostic> diagnostics = new ArrayList<>(parsed.diagnostics());
|
||||
var terrain = parsed.symbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)
|
||||
.findFirst();
|
||||
if (terrain.isEmpty()) return parsed;
|
||||
var profile = ((SymbolSemantics.ResourceValue) terrain.orElseThrow().semantics().value()).id();
|
||||
if (AgeRuntimeProfiles.profiles().containsKey(profile)) return parsed;
|
||||
List<WritingGrammar.Diagnostic> diagnostics = new ArrayList<>(parsed.diagnostics());
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNSUPPORTED_PROFILE,
|
||||
terrain.orElseThrow().index(), "Restore the Age profile " + profile + " or remove this page"));
|
||||
if (terrain.isPresent()) {
|
||||
var profileId = ((SymbolSemantics.ResourceValue) terrain.orElseThrow().semantics().value()).id();
|
||||
if (!AgeRuntimeProfiles.profiles().containsKey(profileId)) {
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNSUPPORTED_PROFILE,
|
||||
terrain.orElseThrow().index(), "Restore the Age profile " + profileId + " or remove this page"));
|
||||
}
|
||||
}
|
||||
if (level != null) {
|
||||
var biomes = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
var selectedProfile = terrain.isPresent()
|
||||
? ((SymbolSemantics.ResourceValue) terrain.orElseThrow().semantics().value()).id()
|
||||
: level instanceof ServerLevel serverLevel
|
||||
? AgeRegistrySavedData.previewGenerationProfile(serverLevel, title)
|
||||
: AgeGenerationProfiles.STANDARD;
|
||||
var runtimeProfile = AgeRuntimeProfiles.profiles().get(selectedProfile);
|
||||
boolean cave = runtimeProfile != null && AgeBiomeSelection.isCave(runtimeProfile);
|
||||
parsed.symbols().stream().filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.BIOME)
|
||||
.forEach(symbol -> {
|
||||
var id = ((SymbolSemantics.ResourceValue) symbol.semantics().value()).id();
|
||||
var holder = biomes.get(ResourceKey.create(Registries.BIOME, id));
|
||||
if (holder.isEmpty()) {
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNKNOWN_BIOME,
|
||||
symbol.index(), "Restore biome " + id + " or remove this page"));
|
||||
} else if (cave && !holder.orElseThrow().is(AgeBiomeSelection.CAVE_BIOMES)
|
||||
&& !holder.orElseThrow().is(BiomeTags.IS_NETHER)) {
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(WritingGrammar.Code.UNSUPPORTED_BIOME,
|
||||
symbol.index(), "Cave Ages require cave or Nether biomes; remove " + id));
|
||||
}
|
||||
});
|
||||
}
|
||||
return new WritingGrammar.Result(parsed.symbols(), diagnostics);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
|
||||
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList;
|
||||
|
||||
/** Frozen biome pool and controller for an authored Age. Repeated IDs are intentional weights. */
|
||||
public record AgeBiomeSelection(AgeTraits.BiomeDistribution distribution, List<ResourceLocation> biomes) {
|
||||
public static final TagKey<Biome> CAVE_BIOMES = TagKey.create(Registries.BIOME,
|
||||
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "cave_biomes"));
|
||||
|
||||
public AgeBiomeSelection {
|
||||
if (distribution == null) throw new IllegalArgumentException("Missing biome distribution");
|
||||
biomes = List.copyOf(biomes);
|
||||
if (biomes.isEmpty()) throw new IllegalArgumentException("An Age biome pool cannot be empty");
|
||||
}
|
||||
|
||||
public static AgeBiomeSelection compile(WritingGrammar.Result writing, AgeTraits traits,
|
||||
AgeRuntimeProfile profile, long seed, RegistryAccess registries) {
|
||||
List<ResourceLocation> authored = writing.symbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.BIOME)
|
||||
.map(symbol -> ((SymbolSemantics.ResourceValue) symbol.semantics().value()).id()).toList();
|
||||
var controller = writing.symbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.BIOME_CONTROLLER)
|
||||
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
|
||||
.findFirst();
|
||||
AgeTraits.BiomeDistribution distribution = controller
|
||||
.map(value -> AgeTraits.BiomeDistribution.valueOf(value.toUpperCase(java.util.Locale.ROOT)))
|
||||
.orElse(traits.biomeDistribution());
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID && controller.isEmpty() && authored.isEmpty()) {
|
||||
distribution = AgeTraits.BiomeDistribution.SINGLE;
|
||||
}
|
||||
if (controller.isEmpty() && !authored.isEmpty()) {
|
||||
if (distribution == AgeTraits.BiomeDistribution.NATIVE
|
||||
|| (distribution == AgeTraits.BiomeDistribution.SINGLE && authored.size() > 1)) {
|
||||
distribution = authored.size() == 1 ? AgeTraits.BiomeDistribution.SINGLE
|
||||
: authored.size() == 2 ? AgeTraits.BiomeDistribution.TILED
|
||||
: AgeTraits.BiomeDistribution.MEDIUM;
|
||||
}
|
||||
}
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
boolean cave = isCave(profile);
|
||||
for (ResourceLocation id : authored) {
|
||||
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"));
|
||||
if (cave && !holder.is(CAVE_BIOMES) && !holder.is(BiomeTags.IS_NETHER)) {
|
||||
throw new IllegalArgumentException("Biome " + id
|
||||
+ " is not a cave or Nether biome; use #rivencraft:cave_biomes or #minecraft:is_nether");
|
||||
}
|
||||
}
|
||||
List<ResourceLocation> eligible = eligible(registries, profile);
|
||||
if (eligible.isEmpty()) throw new IllegalArgumentException("No eligible biomes for terrain " + profile.noiseSettings());
|
||||
if (distribution == AgeTraits.BiomeDistribution.NATIVE) {
|
||||
if (!authored.isEmpty()) throw new IllegalArgumentException("Native biome distribution cannot use biome pages");
|
||||
return new AgeBiomeSelection(distribution, profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
||||
? List.of(ResourceLocation.withDefaultNamespace("plains")) : eligible);
|
||||
}
|
||||
List<ResourceLocation> selected = new ArrayList<>(authored);
|
||||
Set<ResourceLocation> distinct = new LinkedHashSet<>(selected);
|
||||
int minimum = minimum(distribution);
|
||||
int target = minimum;
|
||||
if (authored.isEmpty()) {
|
||||
int maximum = Math.min(maximum(distribution), new LinkedHashSet<>(eligible).size());
|
||||
if (maximum >= minimum) {
|
||||
target += new Random(seed ^ 0x42494F4D45434F55L).nextInt(maximum - minimum + 1);
|
||||
}
|
||||
}
|
||||
List<ResourceLocation> filler = new ArrayList<>(eligible);
|
||||
java.util.Collections.shuffle(filler, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
for (ResourceLocation id : filler) {
|
||||
if (distinct.size() >= target) break;
|
||||
if (distinct.add(id)) selected.add(id);
|
||||
}
|
||||
if (distinct.size() < minimum) {
|
||||
throw new IllegalArgumentException(distribution + " requires " + minimum
|
||||
+ " distinct biomes; supply more biome pages for this terrain");
|
||||
}
|
||||
return new AgeBiomeSelection(distribution, selected);
|
||||
}
|
||||
|
||||
public static List<ResourceLocation> eligible(RegistryAccess registries, AgeRuntimeProfile profile) {
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
if (isCave(profile)) {
|
||||
List<ResourceLocation> tagged = registry.listElements().filter(holder -> holder.is(CAVE_BIOMES))
|
||||
.map(holder -> holder.key().location()).sorted().toList();
|
||||
if (!tagged.isEmpty()) return tagged;
|
||||
return List.of("minecraft:deep_dark", "minecraft:dripstone_caves", "minecraft:lush_caves")
|
||||
.stream().map(ResourceLocation::parse)
|
||||
.filter(id -> registry.get(ResourceKey.create(Registries.BIOME, id)).isPresent()).toList();
|
||||
}
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
||||
return List.of(ResourceLocation.withDefaultNamespace("the_void"));
|
||||
}
|
||||
ResourceLocation preset = profile.terrain() == AgeRuntimeProfile.Terrain.FLAT
|
||||
? ResourceLocation.withDefaultNamespace("overworld") : profile.biomePreset();
|
||||
var parameters = registries.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)
|
||||
.getOrThrow(ResourceKey.create(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, preset));
|
||||
return MultiNoiseBiomeSource.createFromPreset(parameters).possibleBiomes().stream()
|
||||
.map(holder -> holder.unwrapKey().orElseThrow().location())
|
||||
.sorted(Comparator.naturalOrder()).toList();
|
||||
}
|
||||
|
||||
public static boolean isCave(AgeRuntimeProfile profile) {
|
||||
return profile.terrain() == AgeRuntimeProfile.Terrain.NOISE
|
||||
&& profile.noiseSettings().equals(ResourceLocation.withDefaultNamespace("caves"));
|
||||
}
|
||||
|
||||
public static int minimum(AgeTraits.BiomeDistribution distribution) {
|
||||
return switch (distribution) {
|
||||
case NATIVE, SINGLE -> 1;
|
||||
case TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE -> 2;
|
||||
};
|
||||
}
|
||||
|
||||
/** Random-age limits; authored biome pages are never truncated to this maximum. */
|
||||
public static int maximum(AgeTraits.BiomeDistribution distribution) {
|
||||
return switch (distribution) {
|
||||
case NATIVE, SINGLE -> 1;
|
||||
case TILED, GRID -> 4;
|
||||
case TINY, SMALL, MEDIUM, LARGE, HUGE -> 6;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public record AgeDefinition(
|
||||
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "random_vanilla"
|
||||
);
|
||||
public static final int CURRENT_GENERATION_VERSION = 15;
|
||||
public static final int CURRENT_GENERATION_VERSION = 16;
|
||||
|
||||
public AgeDefinition {
|
||||
Objects.requireNonNull(id, "id");
|
||||
|
||||
@@ -6,8 +6,12 @@ import dev.galacticraft.dynamicdimensions.api.event.DimensionRemovedCallback;
|
||||
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Comparator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
@@ -32,12 +36,13 @@ import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
|
||||
import net.dnorberg.rivencraft.world.ScaledBiomeSource;
|
||||
import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.SurfaceRules;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
@@ -287,12 +292,15 @@ public final class AgeDimensionService {
|
||||
|
||||
private static ChunkGenerator createGenerator(MinecraftServer server, AgeDefinition age) {
|
||||
AgeRuntimeProfile profile = runtimeProfile(age);
|
||||
BiomeSource biomeSource = createBiomeSource(server, age, profile);
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.FLAT) {
|
||||
return new FlatLevelSource(FlatLevelGeneratorSettings.getDefault(
|
||||
var settings = FlatLevelGeneratorSettings.getDefault(
|
||||
server.registryAccess().lookupOrThrow(Registries.BIOME),
|
||||
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
|
||||
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
));
|
||||
);
|
||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
||||
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings);
|
||||
}
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
||||
FlatLevelGeneratorSettings settings = new FlatLevelGeneratorSettings(
|
||||
@@ -301,17 +309,13 @@ public final class AgeDimensionService {
|
||||
List.of()
|
||||
);
|
||||
settings.updateLayers();
|
||||
return new FlatLevelSource(settings);
|
||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
||||
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings);
|
||||
}
|
||||
|
||||
var biomeParameters = server.registryAccess()
|
||||
.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
|
||||
var noiseSettings = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
ResourceKey<NoiseGeneratorSettings> settingsKey = ResourceKey.create(
|
||||
Registries.NOISE_SETTINGS, profile.noiseSettings());
|
||||
ResourceKey<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomePreset =
|
||||
ResourceKey.create(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, profile.biomePreset());
|
||||
BiomeSource biomeSource = createBiomeSource(server, age, biomeParameters, biomePreset);
|
||||
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
|
||||
return new NoiseBasedChunkGenerator(biomeSource, Holder.direct(withBiomeMaterialRules(baseSettings)));
|
||||
}
|
||||
@@ -358,34 +362,86 @@ public final class AgeDimensionService {
|
||||
private static BiomeSource createBiomeSource(
|
||||
MinecraftServer server,
|
||||
AgeDefinition age,
|
||||
net.minecraft.core.HolderGetter<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomeParameters,
|
||||
ResourceKey<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomePreset
|
||||
AgeRuntimeProfile profile
|
||||
) {
|
||||
List<net.minecraft.core.Holder.Reference<Biome>> biomes = server.registryAccess()
|
||||
.lookupOrThrow(Registries.BIOME)
|
||||
.listElements()
|
||||
.sorted(Comparator.comparing(holder -> holder.key().location().toString()))
|
||||
.toList();
|
||||
if (age.traits().biomeDistribution() == AgeTraits.BiomeDistribution.SINGLE && !biomes.isEmpty()) {
|
||||
int index = Math.floorMod(Long.hashCode(age.seed() ^ 0x53494E474C454249L), biomes.size());
|
||||
return new FixedBiomeSource(biomes.get(index));
|
||||
if (age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION) {
|
||||
return legacyBiomeSource(server, age, profile);
|
||||
}
|
||||
AgeTraits.BiomeDistribution distribution = age.traits().biomeDistribution();
|
||||
if ((distribution == AgeTraits.BiomeDistribution.TILED
|
||||
|| distribution == AgeTraits.BiomeDistribution.GRID) && biomes.size() >= 2) {
|
||||
List<net.minecraft.core.Holder.Reference<Biome>> selected = new java.util.ArrayList<>(biomes);
|
||||
long salt = distribution == AgeTraits.BiomeDistribution.GRID
|
||||
? 0x4752494442494F4DL
|
||||
: 0x54494C454442494FL;
|
||||
Collections.shuffle(selected, new java.util.Random(age.seed() ^ salt));
|
||||
return new CheckerboardColumnBiomeSource(
|
||||
HolderSet.direct(List.of(selected.get(0), selected.get(1))),
|
||||
distribution == AgeTraits.BiomeDistribution.GRID
|
||||
? GRID_BIOME_SCALE
|
||||
: TILED_BIOME_SCALE
|
||||
);
|
||||
AgeBiomeSelection selection = age.plan() != null && age.plan().biomeSelection() != null
|
||||
? age.plan().biomeSelection()
|
||||
: AgeBiomeSelection.compile(net.dnorberg.rivencraft.writing.WritingGrammar.parse(OrderedWriting.EMPTY),
|
||||
age.traits(), profile, age.seed(), server.registryAccess());
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
List<Holder<Biome>> biomes = new ArrayList<>();
|
||||
List<ResourceLocation> fallbacks = AgeBiomeSelection.eligible(server.registryAccess(), profile);
|
||||
Set<ResourceLocation> present = new LinkedHashSet<>();
|
||||
for (ResourceLocation id : selection.biomes()) {
|
||||
var holder = registry.get(ResourceKey.create(Registries.BIOME, id));
|
||||
if (holder.isEmpty()) {
|
||||
ResourceLocation fallback = fallbacks.stream().filter(candidate -> !present.contains(candidate))
|
||||
.findFirst().orElse(fallbacks.getFirst());
|
||||
Rivencraft.LOGGER.warn("Age {} biome {} is unavailable; using {} for this load. Restore its data pack to recover the authored biome.",
|
||||
age.id(), id, fallback);
|
||||
holder = registry.get(ResourceKey.create(Registries.BIOME, fallback));
|
||||
}
|
||||
Holder.Reference<Biome> resolved = holder.orElseThrow();
|
||||
biomes.add(resolved);
|
||||
present.add(resolved.key().location());
|
||||
}
|
||||
for (ResourceLocation fallback : fallbacks) {
|
||||
if (present.size() >= AgeBiomeSelection.minimum(selection.distribution())) break;
|
||||
if (present.add(fallback)) biomes.add(registry.getOrThrow(
|
||||
ResourceKey.create(Registries.BIOME, fallback)));
|
||||
}
|
||||
AgeTraits.BiomeDistribution distribution = selection.distribution();
|
||||
if (distribution == AgeTraits.BiomeDistribution.NATIVE && !AgeBiomeSelection.isCave(profile)
|
||||
&& profile.terrain() == AgeRuntimeProfile.Terrain.NOISE) {
|
||||
var parameters = server.registryAccess()
|
||||
.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
|
||||
var preset = ResourceKey.create(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, profile.biomePreset());
|
||||
return MultiNoiseBiomeSource.createFromPreset(parameters.getOrThrow(preset));
|
||||
}
|
||||
if (distribution == AgeTraits.BiomeDistribution.SINGLE || biomes.size() == 1) {
|
||||
return new FixedBiomeSource(biomes.getFirst());
|
||||
}
|
||||
if (distribution == AgeTraits.BiomeDistribution.TILED || distribution == AgeTraits.BiomeDistribution.GRID) {
|
||||
return new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||
distribution == AgeTraits.BiomeDistribution.GRID ? GRID_BIOME_SCALE : TILED_BIOME_SCALE);
|
||||
}
|
||||
int scaledBiomeScale = switch (distribution) {
|
||||
case NATIVE, TINY -> 0;
|
||||
case SMALL -> 1;
|
||||
case MEDIUM -> 2;
|
||||
case LARGE -> 3;
|
||||
case HUGE -> 4;
|
||||
default -> -1;
|
||||
};
|
||||
if (biomes.size() >= 2) {
|
||||
return new ScaledBiomeSource(HolderSet.direct(biomes), age.seed(), scaledBiomeScale);
|
||||
}
|
||||
return new CheckerboardColumnBiomeSource(HolderSet.direct(biomes), GRID_BIOME_SCALE);
|
||||
}
|
||||
|
||||
/** Preserve the exact selection algorithm for Ages created before biome pools were frozen. */
|
||||
private static BiomeSource legacyBiomeSource(MinecraftServer server, AgeDefinition age, AgeRuntimeProfile profile) {
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
List<Holder.Reference<Biome>> all = registry.listElements()
|
||||
.sorted(Comparator.comparing(holder -> holder.key().location().toString())).toList();
|
||||
AgeTraits.BiomeDistribution distribution = age.traits().biomeDistribution();
|
||||
if (distribution == AgeTraits.BiomeDistribution.SINGLE && !all.isEmpty()) {
|
||||
int index = Math.floorMod(Long.hashCode(age.seed() ^ 0x53494E474C454249L), all.size());
|
||||
return new FixedBiomeSource(all.get(index));
|
||||
}
|
||||
if ((distribution == AgeTraits.BiomeDistribution.TILED || distribution == AgeTraits.BiomeDistribution.GRID)
|
||||
&& all.size() >= 2) {
|
||||
List<Holder.Reference<Biome>> selected = new ArrayList<>(all);
|
||||
long salt = distribution == AgeTraits.BiomeDistribution.GRID
|
||||
? 0x4752494442494F4DL : 0x54494C454442494FL;
|
||||
Collections.shuffle(selected, new Random(age.seed() ^ salt));
|
||||
return new CheckerboardColumnBiomeSource(HolderSet.direct(List.of(selected.get(0), selected.get(1))),
|
||||
distribution == AgeTraits.BiomeDistribution.GRID ? GRID_BIOME_SCALE : TILED_BIOME_SCALE);
|
||||
}
|
||||
int scale = switch (distribution) {
|
||||
case TINY -> 0;
|
||||
case SMALL -> 1;
|
||||
case MEDIUM -> 2;
|
||||
@@ -393,20 +449,17 @@ public final class AgeDimensionService {
|
||||
case HUGE -> 4;
|
||||
default -> -1;
|
||||
};
|
||||
if (scaledBiomeScale >= 0 && biomes.size() >= 3) {
|
||||
List<net.minecraft.core.Holder.Reference<Biome>> selected = new java.util.ArrayList<>(biomes);
|
||||
if (scale >= 0 && all.size() >= 3) {
|
||||
List<Holder.Reference<Biome>> selected = new ArrayList<>(all);
|
||||
long salt = distribution == AgeTraits.BiomeDistribution.TINY
|
||||
? 0x54494E5942494F4DL
|
||||
: 0x5343414C45444249L;
|
||||
Collections.shuffle(selected, new java.util.Random(age.seed() ^ salt));
|
||||
return new ScaledBiomeSource(
|
||||
HolderSet.direct(List.of(selected.get(0), selected.get(1), selected.get(2))),
|
||||
age.seed(),
|
||||
scaledBiomeScale
|
||||
);
|
||||
? 0x54494E5942494F4DL : 0x5343414C45444249L;
|
||||
Collections.shuffle(selected, new Random(age.seed() ^ salt));
|
||||
return new ScaledBiomeSource(HolderSet.direct(List.of(selected.get(0), selected.get(1), selected.get(2))),
|
||||
age.seed(), scale);
|
||||
}
|
||||
return MultiNoiseBiomeSource.createFromPreset(
|
||||
biomeParameters.getOrThrow(biomePreset));
|
||||
var parameters = server.registryAccess().lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
|
||||
return MultiNoiseBiomeSource.createFromPreset(parameters.getOrThrow(ResourceKey.create(
|
||||
Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, profile.biomePreset())));
|
||||
}
|
||||
|
||||
private static DimensionType createDimensionType(MinecraftServer server, AgeDefinition age) {
|
||||
|
||||
@@ -10,21 +10,28 @@ import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** A frozen interpretation of authored pages. Later slices consume its resolved branches. */
|
||||
public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile) {
|
||||
public static final int CURRENT_VERSION = 1;
|
||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection) {
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
|
||||
public AgeGenerationPlan {
|
||||
if (version != CURRENT_VERSION) throw new IllegalArgumentException("Unsupported Age plan version " + version);
|
||||
if (version < 1 || version > CURRENT_VERSION) {
|
||||
throw new IllegalArgumentException("Unsupported Age plan version " + version);
|
||||
}
|
||||
Objects.requireNonNull(writing, "writing");
|
||||
symbols = List.copyOf(symbols);
|
||||
Objects.requireNonNull(runtimeProfile, "runtimeProfile");
|
||||
if (runtimeProfile.schemaVersion() != AgeRuntimeProfile.CURRENT_SCHEMA_VERSION) {
|
||||
throw new IllegalArgumentException("Unsupported runtime profile snapshot version");
|
||||
}
|
||||
if (version == CURRENT_VERSION && biomeSelection == null) {
|
||||
throw new IllegalArgumentException("Version 2 Age plan requires a biome selection snapshot");
|
||||
}
|
||||
for (ResolvedSymbol symbol : symbols) {
|
||||
if (symbol.index() < 0 || symbol.index() >= writing.entries().size()
|
||||
|| !symbol.reference().equals(writing.entries().get(symbol.index()).symbol().orElse(null))) {
|
||||
@@ -33,8 +40,15 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
}
|
||||
}
|
||||
|
||||
/** Existing version 1 plans retain their original biome behavior on reload. */
|
||||
public AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile) {
|
||||
this(version, ageSeed, writing, symbols, runtimeProfile, null);
|
||||
}
|
||||
|
||||
/** No plan is made for old page-free books or Link-Panel-only random Ages. */
|
||||
public static AgeGenerationPlan compile(OrderedWriting writing, long ageSeed, ResourceLocation seedProfile) {
|
||||
public static AgeGenerationPlan compile(OrderedWriting writing, long ageSeed, ResourceLocation seedProfile,
|
||||
AgeTraits traits, RegistryAccess registries) {
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(writing);
|
||||
if (!parsed.canCreate()) throw new IllegalArgumentException(parsed.firstError().orElseThrow().message());
|
||||
if (parsed.randomFallback()) return null;
|
||||
@@ -50,10 +64,11 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
ResourceLocation selected = terrain == null ? seedProfile : terrain;
|
||||
AgeRuntimeProfile profile = AgeRuntimeProfiles.profiles().get(selected);
|
||||
if (profile == null) throw new IllegalArgumentException("Unsupported Age terrain profile " + selected);
|
||||
AgeBiomeSelection biomes = AgeBiomeSelection.compile(parsed, traits, profile, ageSeed, registries);
|
||||
return new AgeGenerationPlan(CURRENT_VERSION, ageSeed, writing,
|
||||
parsed.symbols().stream().map(symbol -> new ResolvedSymbol(
|
||||
symbol.index(), symbol.reference(), symbol.semantics())).toList(),
|
||||
profile);
|
||||
profile, biomes);
|
||||
}
|
||||
|
||||
public ResourceLocation authoredTerrain() {
|
||||
@@ -94,6 +109,14 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
profile.putString("Biomes", runtimeProfile.biomePreset().toString());
|
||||
profile.putString("Dimension", runtimeProfile.dimensionType().toString());
|
||||
tag.put("RuntimeProfile", profile);
|
||||
if (biomeSelection != null) {
|
||||
CompoundTag biomeTag = new CompoundTag();
|
||||
biomeTag.putString("Distribution", biomeSelection.distribution().name());
|
||||
ListTag biomeIds = new ListTag();
|
||||
biomeSelection.biomes().forEach(id -> biomeIds.add(StringTag.valueOf(id.toString())));
|
||||
biomeTag.put("Ids", biomeIds);
|
||||
tag.put("BiomeSelection", biomeTag);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
@@ -124,12 +147,23 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
new SymbolSemantics(role, value)));
|
||||
}
|
||||
CompoundTag profile = tag.getCompound("RuntimeProfile");
|
||||
AgeBiomeSelection selection = null;
|
||||
if (version >= 2) {
|
||||
CompoundTag biomeTag = tag.getCompound("BiomeSelection");
|
||||
ListTag biomeIds = biomeTag.getList("Ids", CompoundTag.TAG_STRING);
|
||||
List<ResourceLocation> ids = new ArrayList<>();
|
||||
for (int index = 0; index < biomeIds.size(); index++) {
|
||||
ids.add(ResourceLocation.parse(biomeIds.getString(index)));
|
||||
}
|
||||
selection = new AgeBiomeSelection(AgeTraits.BiomeDistribution.valueOf(
|
||||
biomeTag.getString("Distribution")), ids);
|
||||
}
|
||||
return new AgeGenerationPlan(version, tag.getLong("AgeSeed"), new OrderedWriting(pages), resolved,
|
||||
new AgeRuntimeProfile(profile.getInt("Version"),
|
||||
AgeRuntimeProfile.Terrain.valueOf(profile.getString("Terrain")),
|
||||
ResourceLocation.parse(profile.getString("Noise")),
|
||||
ResourceLocation.parse(profile.getString("Biomes")),
|
||||
ResourceLocation.parse(profile.getString("Dimension"))));
|
||||
ResourceLocation.parse(profile.getString("Dimension"))), selection);
|
||||
}
|
||||
|
||||
public record ResolvedSymbol(int index, SymbolReference reference, SymbolSemantics semantics) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.List;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -56,7 +57,24 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
|
||||
public static AgeDefinition resolveOrCreate(ServerLevel level, AgeReference reference, OrderedWriting writing) {
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
return get(overworld).getOrCreate(reference, writing, overworld.getSeed(), overworld.getGameTime());
|
||||
return get(overworld).getOrCreate(reference, writing, overworld.getSeed(), overworld.getGameTime(),
|
||||
overworld.registryAccess());
|
||||
}
|
||||
|
||||
/** Validate with the same name-derived seed and terrain completion used when the Age is created. */
|
||||
public static void validateNewWriting(ServerLevel level, String name, OrderedWriting writing) {
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
AgeRegistrySavedData data = get(overworld);
|
||||
long seed = ageSeed(overworld.getSeed(), data.availableDimensionId(normalizedName(name)));
|
||||
AgeGenerationPlan.compile(writing, seed, AgeRuntimeProfiles.select(seed), AgeTraits.select(seed),
|
||||
overworld.registryAccess());
|
||||
}
|
||||
|
||||
public static ResourceLocation previewGenerationProfile(ServerLevel level, String name) {
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
AgeRegistrySavedData data = get(overworld);
|
||||
long seed = ageSeed(overworld.getSeed(), data.availableDimensionId(normalizedName(name)));
|
||||
return AgeRuntimeProfiles.select(seed);
|
||||
}
|
||||
|
||||
public Optional<AgeDefinition> find(UUID ageId) {
|
||||
@@ -85,7 +103,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
}
|
||||
|
||||
private AgeDefinition getOrCreate(AgeReference reference, OrderedWriting writing,
|
||||
long worldSeed, long createdGameTime) {
|
||||
long worldSeed, long createdGameTime, RegistryAccess registries) {
|
||||
AgeDefinition existing = ages.get(reference.id());
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
@@ -95,7 +113,8 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
ResourceLocation dimensionId = availableDimensionId(name);
|
||||
long seed = ageSeed(worldSeed, dimensionId);
|
||||
ResourceLocation seedProfile = AgeRuntimeProfiles.select(seed);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, seed, seedProfile);
|
||||
AgeTraits traits = AgeTraits.select(seed);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, seed, seedProfile, traits, registries);
|
||||
AgeDefinition created = new AgeDefinition(
|
||||
reference.id(),
|
||||
name,
|
||||
@@ -105,7 +124,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
createdGameTime,
|
||||
dimensionId,
|
||||
null,
|
||||
AgeTraits.select(seed),
|
||||
traits,
|
||||
plan
|
||||
);
|
||||
ages.put(created.id(), created);
|
||||
|
||||
@@ -63,6 +63,10 @@ public record AgeTraits(
|
||||
return celestialPeriod == STOPPED_CELESTIAL_PERIOD;
|
||||
}
|
||||
|
||||
public AgeTraits withBiomeDistribution(BiomeDistribution distribution) {
|
||||
return new AgeTraits(distribution, lighting, weather, celestialPeriod, celestialAngle, celestialPhase);
|
||||
}
|
||||
|
||||
public static <E extends Enum<E>> E parseOrDefault(String value, E fallback) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
@@ -16,8 +17,9 @@ import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
@@ -27,6 +29,167 @@ import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
public final class TerrainWritingGameTests {
|
||||
private TerrainWritingGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void seedSelectedCavePreflightRejectsSurfaceBiome(GameTestHelper helper) {
|
||||
String candidate = null;
|
||||
for (int index = 0; index < 1000; index++) {
|
||||
String name = "Cave preflight " + index;
|
||||
if (AgeRegistrySavedData.previewGenerationProfile(helper.getLevel(), name)
|
||||
.equals(AgeGenerationProfiles.CAVE)) {
|
||||
candidate = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(candidate != null, "A deterministic name must select Cave terrain");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/plains")))));
|
||||
boolean rejected = false;
|
||||
try {
|
||||
AgeRegistrySavedData.validateNewWriting(helper.getLevel(), candidate, writing);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
rejected = exception.getMessage().contains("not a cave or Nether biome");
|
||||
}
|
||||
helper.assertTrue(rejected, "The preflight must use the actual seed-selected terrain profile");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty", timeoutTicks = 600)
|
||||
public static void caveBiomePagesControlGeneratedPool(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("biome/desert"),
|
||||
"Desert must remain in the bundled creative page index");
|
||||
var creativePages = PageItem.creativeSymbolPages(helper.getLevel().registryAccess());
|
||||
helper.assertTrue(creativePages.stream().anyMatch(page -> PageItem.content(page).symbol()
|
||||
.map(symbol -> symbol.id().equals(ResourceLocation.parse("rivencraft:biome/desert")))
|
||||
.orElse(false))
|
||||
&& creativePages.stream().anyMatch(page -> PageItem.content(page).symbol()
|
||||
.map(symbol -> symbol.id().equals(ResourceLocation.parse("rivencraft:biome/jungle")))
|
||||
.orElse(false)),
|
||||
"Both bundled and registry-generated biome pages must be available to creative and JEI");
|
||||
var jungle = ResourceLocation.withDefaultNamespace("jungle");
|
||||
helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(ResourceLocation.parse("rivencraft:biome/jungle")))
|
||||
&& PageItem.isUsableWriting(PageItem.createCreativeBiomePage(jungle)),
|
||||
"A generated Jungle page must be usable as server-authored writing");
|
||||
helper.getLevel().registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.map(holder -> holder.key().location())
|
||||
.filter(id -> id.getNamespace().equals("minecraft"))
|
||||
.forEach(id -> helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(
|
||||
ResourceLocation.fromNamespaceAndPath("rivencraft", "biome/" + id.getPath()))),
|
||||
"Every registered vanilla biome must have a usable symbol page: " + id));
|
||||
var overworldPool = AgeBiomeSelection.eligible(helper.getLevel().registryAccess(),
|
||||
AgeRuntimeProfiles.resolve(AgeGenerationProfiles.STANDARD));
|
||||
helper.assertTrue(overworldPool.containsAll(List.of(Biomes.LUSH_CAVES.location(),
|
||||
Biomes.DRIPSTONE_CAVES.location(), Biomes.DEEP_DARK.location())),
|
||||
"Standard terrain must retain the vanilla Overworld underground cave biomes");
|
||||
OrderedWriting standardCaveWriting = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/standard"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/dripstone_caves"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/single")))));
|
||||
helper.assertValueEqual(AgeGenerationPlan.compile(standardCaveWriting, 45L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(45L), helper.getLevel().registryAccess())
|
||||
.biomeSelection().biomes(), List.of(Biomes.DRIPSTONE_CAVES.location()),
|
||||
"A standard Age must accept an explicitly written cave biome");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/cave"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/lush_caves"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/nether_wastes"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/tiled")))));
|
||||
var reference = new AgeReference(UUID.randomUUID(), "Written Cave Biomes");
|
||||
var target = AgeDimensionService.resolve(player, reference, writing).orElseThrow();
|
||||
var age = AgeRegistrySavedData.get(helper.getLevel()).find(reference.id()).orElseThrow();
|
||||
var selection = age.plan().biomeSelection();
|
||||
helper.assertValueEqual(selection.distribution(), AgeTraits.BiomeDistribution.TILED,
|
||||
"The authored distribution must override the random trait");
|
||||
helper.assertValueEqual(selection.biomes(), List.of(Biomes.LUSH_CAVES.location(), Biomes.NETHER_WASTES.location()),
|
||||
"An authored cave pool must include only its selected cave and Nether biomes");
|
||||
helper.assertValueEqual(AgeGenerationPlan.load(age.plan().save()).biomeSelection(), selection,
|
||||
"Biome IDs, count and distribution must survive a plan save/reload");
|
||||
helper.runAfterDelay(1, () -> {
|
||||
var level = helper.getLevel().getServer().getLevel(target.location().dimension());
|
||||
helper.assertTrue(level != null, "The cave Age must load");
|
||||
helper.assertValueEqual(level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().size(), 2,
|
||||
"The runtime generator must expose both authored biomes");
|
||||
helper.succeed();
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredBiomeMinimumAndRandomAgeRange(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var profile = AgeRuntimeProfiles.resolve(AgeGenerationProfiles.STANDARD);
|
||||
OrderedWriting twoBiomeTiny = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/plains"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/desert"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/tiny")))));
|
||||
var written = AgeBiomeSelection.compile(net.dnorberg.rivencraft.writing.WritingGrammar.parse(twoBiomeTiny),
|
||||
AgeTraits.select(42L), profile, 42L, registries);
|
||||
helper.assertValueEqual(written.biomes(), List.of(Biomes.PLAINS.location(), Biomes.DESERT.location()),
|
||||
"Two authored Tiny biomes must not acquire a third random biome");
|
||||
var biomeRegistry = registries.lookupOrThrow(Registries.BIOME);
|
||||
var scaled = new net.dnorberg.rivencraft.world.ScaledBiomeSource(net.minecraft.core.HolderSet.direct(
|
||||
biomeRegistry.getOrThrow(Biomes.PLAINS), biomeRegistry.getOrThrow(Biomes.DESERT)), 42L, 0);
|
||||
helper.assertValueEqual(scaled.possibleBiomes().size(), 2,
|
||||
"The Tiny runtime biome source must accept exactly two authored biomes");
|
||||
|
||||
OrderedWriting oneBiomeTiny = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/plains"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/tiny")))));
|
||||
var partial = AgeBiomeSelection.compile(net.dnorberg.rivencraft.writing.WritingGrammar.parse(oneBiomeTiny),
|
||||
AgeTraits.select(42L), profile, 42L, registries);
|
||||
helper.assertValueEqual(partial.biomes().size(), 2,
|
||||
"One authored Tiny biome must receive only the missing second biome");
|
||||
helper.assertValueEqual(partial.biomes().getFirst(), Biomes.PLAINS.location(),
|
||||
"Random completion must preserve the authored biome first");
|
||||
|
||||
var randomWriting = net.dnorberg.rivencraft.writing.WritingGrammar.parse(OrderedWriting.EMPTY);
|
||||
var traits = new AgeTraits(AgeTraits.BiomeDistribution.TINY, AgeTraits.Lighting.NORMAL,
|
||||
AgeTraits.Weather.NORMAL, 1.0D, 0.0F, 0.0F);
|
||||
java.util.Set<Integer> counts = new java.util.HashSet<>();
|
||||
for (long seed = 0; seed < 64; seed++) {
|
||||
var random = AgeBiomeSelection.compile(randomWriting, traits, profile, seed, registries);
|
||||
helper.assertTrue(random.biomes().size() >= 2 && random.biomes().size() <= 6,
|
||||
"Random Tiny ages must select between two and six distinct biomes");
|
||||
helper.assertValueEqual(random, AgeBiomeSelection.compile(randomWriting, traits, profile, seed, registries),
|
||||
"The random count and chosen biomes must be deterministic for an Age seed");
|
||||
counts.add(random.biomes().size());
|
||||
}
|
||||
helper.assertTrue(counts.size() > 1, "Random Ages must vary their biome count across seeds");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty", timeoutTicks = 600)
|
||||
public static void skyIslandAllowsExplicitEndBiome(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
OrderedWriting writing = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/island"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/the_end"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:biome/single")))));
|
||||
var reference = new AgeReference(UUID.randomUUID(), "End Island Biome");
|
||||
var target = AgeDimensionService.resolve(player, reference, writing).orElseThrow();
|
||||
var age = AgeRegistrySavedData.get(helper.getLevel()).find(reference.id()).orElseThrow();
|
||||
helper.assertValueEqual(age.plan().biomeSelection().biomes(), List.of(Biomes.THE_END.location()),
|
||||
"A sky island may use an explicitly authored End biome");
|
||||
helper.runAfterDelay(1, () -> {
|
||||
var server = helper.getLevel().getServer();
|
||||
var level = server.getLevel(target.location().dimension());
|
||||
helper.assertTrue(level != null, "The island Age must load");
|
||||
helper.assertTrue(level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
|
||||
.allMatch(biome -> biome.is(Biomes.THE_END)),
|
||||
"The runtime sky-island generator must use the End biome page");
|
||||
helper.assertValueEqual(AgeDimensionService.unload(server, age), AgeDimensionService.UnloadResult.UNLOADED,
|
||||
"The island Age must unload");
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(template = "empty", timeoutTicks = 600)
|
||||
public static void writtenTerrainFamiliesGenerateSafelyAndReload(GameTestHelper helper) {
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
@@ -159,7 +322,7 @@ public final class TerrainWritingGameTests {
|
||||
helper.assertValueEqual(((NoiseBasedChunkGenerator) generator).generatorSettings().value().noiseSettings(),
|
||||
expected.noiseSettings(), "Written noise settings must be applied to the generator");
|
||||
} else {
|
||||
helper.assertTrue(generator instanceof FlatLevelSource,
|
||||
helper.assertTrue(generator instanceof AgeFlatChunkGenerator,
|
||||
"Flat and Void terrain must use a flat-source generator");
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
|
||||
helper.assertTrue(generator.getBiomeSource().possibleBiomes().stream()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.dnorberg.rivencraft.compat.jei;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import mezz.jei.api.IModPlugin;
|
||||
import mezz.jei.api.JeiPlugin;
|
||||
import mezz.jei.api.constants.VanillaTypes;
|
||||
@@ -13,8 +16,13 @@ import mezz.jei.api.recipe.category.AbstractRecipeCategory;
|
||||
import mezz.jei.api.registration.IRecipeCatalystRegistration;
|
||||
import mezz.jei.api.registration.IRecipeCategoryRegistration;
|
||||
import mezz.jei.api.registration.IRecipeRegistration;
|
||||
import mezz.jei.api.registration.ISubtypeRegistration;
|
||||
import mezz.jei.api.registration.IExtraIngredientRegistration;
|
||||
import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
|
||||
import mezz.jei.api.runtime.IJeiRuntime;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
@@ -36,6 +44,52 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "jei_writing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerItemSubtypes(ISubtypeRegistration registration) {
|
||||
registration.registerSubtypeInterpreter(ModItems.PAGE.get(),
|
||||
(IIngredientSubtypeInterpreter<ItemStack>) (stack, context) -> {
|
||||
PageContent content = PageItem.content(stack);
|
||||
return switch (content.kind()) {
|
||||
case BLANK -> "blank";
|
||||
case LINK_PANEL -> "link_panel";
|
||||
case SYMBOL -> "symbol:" + content.symbol().orElseThrow().id();
|
||||
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerExtraIngredients(IExtraIngredientRegistration registration) {
|
||||
List<ItemStack> pages = new ArrayList<>();
|
||||
pages.add(PageItem.createLinkPanelPage());
|
||||
var connection = Minecraft.getInstance().getConnection();
|
||||
if (connection != null) {
|
||||
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
|
||||
} else {
|
||||
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
|
||||
.forEach(pages::add);
|
||||
}
|
||||
registration.addExtraItemStacks(pages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRuntimeAvailable(IJeiRuntime runtime) {
|
||||
var connection = Minecraft.getInstance().getConnection();
|
||||
if (connection == null) return;
|
||||
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
|
||||
.filter(stack -> stack.is(ModItems.PAGE.get()))
|
||||
.map(PageItem::content)
|
||||
.filter(content -> content.kind() == PageContent.Kind.SYMBOL)
|
||||
.map(content -> content.symbol().orElseThrow().id())
|
||||
.collect(Collectors.toSet());
|
||||
List<ItemStack> missing = PageItem.creativeSymbolPages(connection.registryAccess()).stream()
|
||||
.filter(stack -> !known.contains(PageItem.content(stack).symbol().orElseThrow().id()))
|
||||
.toList();
|
||||
if (!missing.isEmpty()) {
|
||||
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCategories(IRecipeCategoryRegistration registration) {
|
||||
IGuiHelper guiHelper = registration.getJeiHelpers().getGuiHelper();
|
||||
|
||||
@@ -59,6 +59,7 @@ public final class BookBinderGameTests {
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE, terrainPage());
|
||||
var saved = binder.saveWithoutMetadata(helper.getLevel().registryAccess());
|
||||
var restored = new BookBinderBlockEntity(binder.getBlockPos(), binder.getBlockState());
|
||||
restored.setLevel(helper.getLevel());
|
||||
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(restored.title(), "Ordered Age", "Title must persist");
|
||||
helper.assertTrue(restored.getItem(BookBinderBlockEntity.COVER).is(Items.LEATHER), "Cover must persist");
|
||||
|
||||
@@ -2,6 +2,8 @@ package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
@@ -12,6 +14,7 @@ import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.compat.PreviewBackendSelector;
|
||||
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
@@ -418,46 +421,34 @@ public final class LinkingGameTests {
|
||||
}
|
||||
if (profile.equals(AgeGenerationProfiles.AMPLIFIED)) {
|
||||
var biomeSource = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
helper.assertValueEqual(
|
||||
biomeSource.possibleBiomes().size(),
|
||||
2,
|
||||
"Tiled Biome Distribution must expose exactly two seeded biomes"
|
||||
);
|
||||
helper.assertTrue(biomeSource.possibleBiomes().size() >= 2
|
||||
&& biomeSource.possibleBiomes().size() <= 4,
|
||||
"Random Tiled distribution must use two to four seeded biomes");
|
||||
var origin = biomeSource.getNoiseBiome(0, 0, 0, null);
|
||||
var eastTile = biomeSource.getNoiseBiome(64, 0, 0, null);
|
||||
var diagonalTile = biomeSource.getNoiseBiome(64, 0, 64, null);
|
||||
helper.assertFalse(origin.equals(eastTile),
|
||||
"Adjacent 256-block Tiled squares must alternate biomes");
|
||||
helper.assertTrue(origin.equals(diagonalTile),
|
||||
"Diagonal 256-block Tiled squares must repeat the biome");
|
||||
"Adjacent 256-block Tiled squares must use different biomes");
|
||||
}
|
||||
if (profile.equals(AgeGenerationProfiles.ISLAND)) {
|
||||
var biomeSource = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
helper.assertValueEqual(
|
||||
biomeSource.possibleBiomes().size(),
|
||||
2,
|
||||
"Grid Biome Distribution must expose exactly two seeded biomes"
|
||||
);
|
||||
helper.assertTrue(biomeSource.possibleBiomes().size() >= 2
|
||||
&& biomeSource.possibleBiomes().size() <= 4,
|
||||
"Random Grid distribution must use two to four seeded biomes");
|
||||
var origin = biomeSource.getNoiseBiome(0, 0, 0, null);
|
||||
var insideOriginChunk = biomeSource.getNoiseBiome(3, 0, 3, null);
|
||||
var eastChunk = biomeSource.getNoiseBiome(4, 0, 0, null);
|
||||
var diagonalChunk = biomeSource.getNoiseBiome(4, 0, 4, null);
|
||||
helper.assertTrue(origin.equals(insideOriginChunk),
|
||||
"Grid biomes must remain constant inside each 16-block chunk");
|
||||
helper.assertFalse(origin.equals(eastChunk),
|
||||
"Adjacent Grid chunks must alternate biomes");
|
||||
helper.assertTrue(origin.equals(diagonalChunk),
|
||||
"Diagonal Grid chunks must repeat the biome");
|
||||
"Adjacent Grid chunks must use different biomes");
|
||||
}
|
||||
if (profile.equals(AgeGenerationProfiles.CAVE)) {
|
||||
var biomeSource = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
helper.assertTrue(biomeSource instanceof net.dnorberg.rivencraft.world.ScaledBiomeSource,
|
||||
"Tiny Biome Distribution must use the serializable scaled biome source");
|
||||
helper.assertValueEqual(
|
||||
biomeSource.possibleBiomes().size(),
|
||||
3,
|
||||
"Tiny Biome Distribution must expose exactly three seeded biomes"
|
||||
);
|
||||
helper.assertTrue(biomeSource.possibleBiomes().size() >= 2
|
||||
&& biomeSource.possibleBiomes().size() <= 3,
|
||||
"Random cave Tiny distribution must use two to three eligible biomes");
|
||||
java.util.Set<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>> sampled =
|
||||
new java.util.HashSet<>();
|
||||
for (int x = -32; x <= 32; x += 4) {
|
||||
@@ -531,11 +522,9 @@ public final class LinkingGameTests {
|
||||
var biomeSource = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
helper.assertTrue(biomeSource instanceof net.dnorberg.rivencraft.world.ScaledBiomeSource,
|
||||
distribution + " Biome Distribution must use the serializable scaled biome source");
|
||||
helper.assertValueEqual(
|
||||
biomeSource.possibleBiomes().size(),
|
||||
3,
|
||||
distribution + " Biome Distribution must expose exactly three seeded biomes"
|
||||
);
|
||||
helper.assertTrue(biomeSource.possibleBiomes().size() >= 2
|
||||
&& biomeSource.possibleBiomes().size() <= 6,
|
||||
distribution + " random distribution must use two to six seeded biomes");
|
||||
helper.assertValueEqual(
|
||||
((net.dnorberg.rivencraft.world.ScaledBiomeSource)biomeSource).scale(),
|
||||
scale,
|
||||
@@ -553,23 +542,10 @@ public final class LinkingGameTests {
|
||||
net.minecraft.resources.ResourceKey<net.minecraft.world.level.biome.Biome> biome,
|
||||
net.minecraft.world.level.block.Block expectedBlock
|
||||
) {
|
||||
var biomeRegistry = helper.getLevel().registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME);
|
||||
var sortedBiomes = biomeRegistry.listElements()
|
||||
.sorted(java.util.Comparator.comparing(holder -> holder.key().location().toString()))
|
||||
.toList();
|
||||
int targetIndex = -1;
|
||||
for (int index = 0; index < sortedBiomes.size(); index++) {
|
||||
if (sortedBiomes.get(index).is(biome)) {
|
||||
targetIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(targetIndex >= 0, "The requested material-test biome must be registered");
|
||||
|
||||
long seed = 0L;
|
||||
while (Math.floorMod(Long.hashCode(seed ^ 0x53494E474C454249L), sortedBiomes.size()) != targetIndex) {
|
||||
seed++;
|
||||
}
|
||||
var plan = new AgeGenerationPlan(AgeGenerationPlan.CURRENT_VERSION, seed, OrderedWriting.EMPTY,
|
||||
java.util.List.of(), AgeRuntimeProfiles.resolve(AgeGenerationProfiles.STANDARD),
|
||||
new AgeBiomeSelection(AgeTraits.BiomeDistribution.SINGLE, java.util.List.of(biome.location())));
|
||||
UUID id = UUID.randomUUID();
|
||||
AgeDefinition definition = new AgeDefinition(
|
||||
id,
|
||||
@@ -589,7 +565,8 @@ public final class LinkingGameTests {
|
||||
1.0D,
|
||||
0.0F,
|
||||
0.0F
|
||||
)
|
||||
),
|
||||
plan
|
||||
);
|
||||
var level = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), definition);
|
||||
helper.assertTrue(level != null, biome.location() + " material test Age must load");
|
||||
|
||||
@@ -7,16 +7,21 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
||||
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@@ -41,6 +46,20 @@ public final class PageItem extends Item {
|
||||
return List.copyOf(CREATIVE_SYMBOLS.keySet());
|
||||
}
|
||||
|
||||
/** Exact symbol-page variants offered in the creative tab and to optional ingredient viewers. */
|
||||
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
||||
List<ItemStack> pages = new ArrayList<>();
|
||||
CREATIVE_SYMBOLS.keySet().forEach(id -> pages.add(createBundledCreativePage(id)));
|
||||
Set<String> bundled = CREATIVE_SYMBOLS.keySet();
|
||||
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.map(holder -> holder.key().location())
|
||||
.filter(id -> id.getNamespace().equals("minecraft"))
|
||||
.filter(id -> !bundled.contains("biome/" + id.getPath()))
|
||||
.sorted()
|
||||
.forEach(id -> pages.add(createCreativeBiomePage(id)));
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
/** Bundled symbols for the creative tab; the server still validates catalog membership on use. */
|
||||
public static ItemStack createBundledCreativePage(String path) {
|
||||
SymbolDisplayMetadata metadata = CREATIVE_SYMBOLS.get(path);
|
||||
@@ -53,6 +72,13 @@ public final class PageItem extends Item {
|
||||
return page;
|
||||
}
|
||||
|
||||
public static ItemStack createCreativeBiomePage(net.minecraft.resources.ResourceLocation biomeId) {
|
||||
var symbolId = BiomeSymbolPages.symbolId(biomeId);
|
||||
ItemStack page = create(PageContent.symbol(new SymbolReference(symbolId)));
|
||||
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), BiomeSymbolPages.definition(biomeId).display(symbolId));
|
||||
return page;
|
||||
}
|
||||
|
||||
private static Map<String, SymbolDisplayMetadata> loadCreativeSymbols() {
|
||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
||||
@@ -157,9 +183,12 @@ public final class PageItem extends Item {
|
||||
private static Component symbolName(ItemStack stack, SymbolReference reference) {
|
||||
SymbolDisplayMetadata metadata = stack.getOrDefault(
|
||||
ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference));
|
||||
return metadata.resolved()
|
||||
? Component.translatable(metadata.nameTranslationKey())
|
||||
: Component.translatable("item.rivencraft.page.unresolved");
|
||||
if (!metadata.resolved()) return Component.translatable("item.rivencraft.page.unresolved");
|
||||
return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& reference.id().getPath().startsWith("biome/")
|
||||
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
||||
BiomeSymbolPages.fallbackName(reference.id()))
|
||||
: Component.translatable(metadata.nameTranslationKey());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -118,7 +118,9 @@ public final class ModNetworking {
|
||||
PacketDistributor.sendToPlayer(player, new OpenDescriptiveBookScreenPayload(
|
||||
payload.fromStand(), payload.offhand(),
|
||||
GlobalPos.of(player.level().dimension(), payload.standPos()), access.stack().copy(),
|
||||
definition == null ? null : definition.traits(), writingSymbols));
|
||||
definition == null ? null : definition.plan() != null && definition.plan().biomeSelection() != null
|
||||
? definition.traits().withBiomeDistribution(definition.plan().biomeSelection().distribution())
|
||||
: definition.traits(), writingSymbols));
|
||||
}
|
||||
|
||||
private static void bindDescriptiveBook(BindDescriptiveBookPayload payload, IPayloadContext context) {
|
||||
@@ -144,9 +146,8 @@ public final class ModNetworking {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AgeGenerationPlan.compile(access.stack()
|
||||
.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY),
|
||||
0L, AgeGenerationProfiles.STANDARD);
|
||||
AgeRegistrySavedData.validateNewWriting(player.serverLevel(), title,
|
||||
access.stack().getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.invalid_writing",
|
||||
exception.getMessage()), true);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Serializable generators for Age profiles. */
|
||||
public final class ModChunkGenerators {
|
||||
private static final DeferredRegister<MapCodec<? extends ChunkGenerator>> GENERATORS =
|
||||
DeferredRegister.create(Registries.CHUNK_GENERATOR, Rivencraft.MOD_ID);
|
||||
|
||||
static { GENERATORS.register("age_flat", () -> AgeFlatChunkGenerator.CODEC); }
|
||||
|
||||
private ModChunkGenerators() { }
|
||||
|
||||
public static void register(IEventBus bus) { GENERATORS.register(bus); }
|
||||
}
|
||||
@@ -35,9 +35,7 @@ public final class ModCreativeTabs {
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(ModItems.PAGE.get());
|
||||
output.accept(PageItem.createLinkPanelPage());
|
||||
for (String id : PageItem.bundledCreativeSymbols()) {
|
||||
output.accept(PageItem.createBundledCreativePage(id));
|
||||
}
|
||||
PageItem.creativeSymbolPages(parameters.holders()).forEach(output::accept);
|
||||
})
|
||||
.build());
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
|
||||
/** Vanilla flat layers with a serializable authored biome source instead of a fixed biome. */
|
||||
public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
public static final MapCodec<AgeFlatChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance ->
|
||||
instance.group(BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource),
|
||||
FlatLevelGeneratorSettings.CODEC.fieldOf("settings").forGetter(AgeFlatChunkGenerator::settings))
|
||||
.apply(instance, AgeFlatChunkGenerator::new));
|
||||
|
||||
private final FlatLevelGeneratorSettings settings;
|
||||
|
||||
public AgeFlatChunkGenerator(BiomeSource biomeSource, FlatLevelGeneratorSettings settings) {
|
||||
super(biomeSource, Util.memoize(settings::adjustGenerationSettings));
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public FlatLevelGeneratorSettings settings() { return settings; }
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structures, RandomState random, long seed) {
|
||||
Stream<Holder<StructureSet>> entries = settings.structureOverrides()
|
||||
.map(HolderSet::stream)
|
||||
.orElseGet(() -> structures.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion level, StructureManager structures, RandomState random, ChunkAccess chunk) { }
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor level) {
|
||||
return level.getMinBuildHeight() + Math.min(level.getHeight(), settings.getLayers().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState random,
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
List<BlockState> layers = settings.getLayers();
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
for (int i = 0; i < Math.min(chunk.getHeight(), layers.size()); i++) {
|
||||
BlockState state = layers.get(i);
|
||||
if (state == null) continue;
|
||||
int y = chunk.getMinBuildHeight() + i;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
chunk.setBlockState(position.set(x, y, z), state, false);
|
||||
floor.update(x, y, z, state);
|
||||
surface.update(x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
|
||||
List<BlockState> layers = settings.getLayers();
|
||||
for (int i = Math.min(layers.size(), level.getHeight()) - 1; i >= 0; i--) {
|
||||
BlockState state = layers.get(i);
|
||||
if (state != null && type.isOpaque().test(state)) return level.getMinBuildHeight() + i + 1;
|
||||
}
|
||||
return level.getMinBuildHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
|
||||
return new NoiseColumn(level.getMinBuildHeight(), settings.getLayers().stream()
|
||||
.limit(level.getHeight()).map(state -> state == null ? Blocks.AIR.defaultBlockState() : state)
|
||||
.toArray(BlockState[]::new));
|
||||
}
|
||||
|
||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random, BlockPos position) { }
|
||||
@Override public void applyCarvers(WorldGenRegion level, long seed, RandomState random, BiomeManager biomes,
|
||||
StructureManager structures, ChunkAccess chunk, GenerationStep.Carving step) { }
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { }
|
||||
@Override public int getMinY() { return 0; }
|
||||
@Override public int getGenDepth() { return 384; }
|
||||
@Override public int getSeaLevel() { return -63; }
|
||||
}
|
||||
@@ -24,8 +24,8 @@ public final class ScaledBiomeSource extends BiomeSource {
|
||||
private final int cellWidth;
|
||||
|
||||
public ScaledBiomeSource(HolderSet<Biome> biomes, long seed, int scale) {
|
||||
if (biomes.size() < 3) {
|
||||
throw new IllegalArgumentException("Scaled biome distributions require at least three biomes");
|
||||
if (biomes.size() < 2) {
|
||||
throw new IllegalArgumentException("Scaled biome distributions require at least two biomes");
|
||||
}
|
||||
this.biomes = biomes;
|
||||
this.seed = seed;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Built-in fallback pages for registered vanilla biomes without hand-authored symbol JSON. */
|
||||
public final class BiomeSymbolPages {
|
||||
private static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome");
|
||||
|
||||
private BiomeSymbolPages() { }
|
||||
|
||||
public static ResourceLocation symbolId(ResourceLocation biomeId) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/" + biomeId.getPath());
|
||||
}
|
||||
|
||||
public static SymbolDefinition definition(ResourceLocation biomeId) {
|
||||
String path = biomeId.getPath();
|
||||
String glyphWord = Arrays.stream(path.split("_"))
|
||||
.map(part -> part.replaceAll("[^A-Za-z]", ""))
|
||||
.filter(part -> !part.isEmpty())
|
||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||
.reduce("", String::concat);
|
||||
if (glyphWord.isEmpty()) glyphWord = "World";
|
||||
return new SymbolDefinition(SymbolDefinition.CURRENT_SCHEMA_VERSION,
|
||||
"symbol.rivencraft.biome." + path,
|
||||
"symbol.rivencraft.biome.generated.description",
|
||||
CATEGORY,
|
||||
Optional.of(new SymbolSemantics(SymbolSemantics.Role.BIOME,
|
||||
new SymbolSemantics.ResourceValue(biomeId))),
|
||||
"Nature Form " + glyphWord + " Biome");
|
||||
}
|
||||
|
||||
public static String fallbackName(ResourceLocation symbolId) {
|
||||
String path = symbolId.getPath().substring("biome/".length());
|
||||
return Arrays.stream(path.split("_"))
|
||||
.filter(part -> !part.isEmpty())
|
||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||
.reduce((left, right) -> left + " " + right).orElse("Unknown") + " Biome";
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
@@ -23,7 +25,7 @@ public final class SymbolCatalog {
|
||||
}
|
||||
|
||||
public static void register(AddReloadListenerEvent event) {
|
||||
event.addListener(new Loader());
|
||||
event.addListener(new Loader(event.getRegistryAccess()));
|
||||
}
|
||||
|
||||
public static Optional<SymbolDefinition> find(SymbolReference reference) {
|
||||
@@ -46,8 +48,11 @@ public final class SymbolCatalog {
|
||||
}
|
||||
|
||||
private static final class Loader extends SimpleJsonResourceReloadListener {
|
||||
private Loader() {
|
||||
private final RegistryAccess registries;
|
||||
|
||||
private Loader(RegistryAccess registries) {
|
||||
super(GSON, DIRECTORY);
|
||||
this.registries = registries;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,6 +67,11 @@ public final class SymbolCatalog {
|
||||
Rivencraft.LOGGER.error("Ignoring invalid writing symbol {}: {}", id, exception.getMessage());
|
||||
}
|
||||
});
|
||||
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.map(holder -> holder.key().location())
|
||||
.filter(id -> id.getNamespace().equals("minecraft"))
|
||||
.forEach(id -> loaded.putIfAbsent(BiomeSymbolPages.symbolId(id),
|
||||
BiomeSymbolPages.definition(id)));
|
||||
symbols = Map.copyOf(loaded);
|
||||
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public final class WritingGrammar {
|
||||
case "single" -> 1;
|
||||
case "tiled", "grid" -> 2;
|
||||
case "native" -> 0;
|
||||
default -> 3;
|
||||
default -> 2;
|
||||
};
|
||||
if (biomeCount < minimum) diagnostics.add(new Diagnostic(Code.INCOMPLETE, -1,
|
||||
"The Age compiler will fill " + (minimum - biomeCount) + " missing biome page(s)"));
|
||||
@@ -151,7 +151,8 @@ public final class WritingGrammar {
|
||||
|
||||
public enum Code {
|
||||
TOO_MANY, MISSING_PANEL, PANEL_ORDER, DUPLICATE_PANEL, UNRESOLVED, UNSUPPORTED,
|
||||
DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER, INCOMPLETE, UNSUPPORTED_PROFILE
|
||||
DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER, INCOMPLETE, UNSUPPORTED_PROFILE,
|
||||
UNKNOWN_BIOME, UNSUPPORTED_BIOME
|
||||
}
|
||||
|
||||
public record Diagnostic(Code code, int index, String message) {
|
||||
|
||||
@@ -132,6 +132,39 @@
|
||||
"symbol.rivencraft.terrain.void.description": "An empty world with a safe arrival platform.",
|
||||
"symbol.rivencraft.biome.native": "Native Biomes",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain profile's native biome climate.",
|
||||
"symbol.rivencraft.biome.the_end": "The End Biome",
|
||||
"symbol.rivencraft.biome.the_end.description": "Use the End biome in a written Age, including sky islands.",
|
||||
"symbol.rivencraft.biome.lush_caves": "Lush Caves Biome",
|
||||
"symbol.rivencraft.biome.lush_caves.description": "Add lush cave regions to a cave Age.",
|
||||
"symbol.rivencraft.biome.dripstone_caves": "Dripstone Caves Biome",
|
||||
"symbol.rivencraft.biome.dripstone_caves.description": "Add dripstone cave regions to a cave Age.",
|
||||
"symbol.rivencraft.biome.deep_dark": "Deep Dark Biome",
|
||||
"symbol.rivencraft.biome.deep_dark.description": "Add deep dark regions to a cave Age.",
|
||||
"symbol.rivencraft.biome.nether_wastes": "Nether Wastes Biome",
|
||||
"symbol.rivencraft.biome.nether_wastes.description": "Add Nether Wastes regions to a cave Age.",
|
||||
"symbol.rivencraft.biome.plains": "Plains Biome",
|
||||
"symbol.rivencraft.biome.plains.description": "Add plains regions to a written Age.",
|
||||
"symbol.rivencraft.biome.forest": "Forest Biome",
|
||||
"symbol.rivencraft.biome.forest.description": "Add forest regions to a written Age.",
|
||||
"symbol.rivencraft.biome.desert": "Desert Biome",
|
||||
"symbol.rivencraft.biome.desert.description": "Add desert regions to a written Age.",
|
||||
"symbol.rivencraft.biome.generated.description": "Selects this biome for a written Age.",
|
||||
"symbol.rivencraft.biome.single": "Single Biome",
|
||||
"symbol.rivencraft.biome.single.description": "Use one biome across the Age.",
|
||||
"symbol.rivencraft.biome.tiled": "Tiled Biomes",
|
||||
"symbol.rivencraft.biome.tiled.description": "Alternate selected biomes in large tiles.",
|
||||
"symbol.rivencraft.biome.grid": "Grid Biomes",
|
||||
"symbol.rivencraft.biome.grid.description": "Alternate selected biomes in smaller grid cells.",
|
||||
"symbol.rivencraft.biome.tiny": "Tiny Biome Regions",
|
||||
"symbol.rivencraft.biome.tiny.description": "Distribute selected biomes in tiny regions.",
|
||||
"symbol.rivencraft.biome.small": "Small Biome Regions",
|
||||
"symbol.rivencraft.biome.small.description": "Distribute selected biomes in small regions.",
|
||||
"symbol.rivencraft.biome.medium": "Medium Biome Regions",
|
||||
"symbol.rivencraft.biome.medium.description": "Distribute selected biomes in medium regions.",
|
||||
"symbol.rivencraft.biome.large": "Large Biome Regions",
|
||||
"symbol.rivencraft.biome.large.description": "Distribute selected biomes in large regions.",
|
||||
"symbol.rivencraft.biome.huge": "Huge Biome Regions",
|
||||
"symbol.rivencraft.biome.huge.description": "Distribute selected biomes in huge regions.",
|
||||
"symbol.rivencraft.lighting.normal": "Normal Lighting",
|
||||
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness.",
|
||||
"symbol.rivencraft.weather.normal": "Normal Weather",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.deep_dark",
|
||||
"description": "symbol.rivencraft.biome.deep_dark.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:deep_dark" },
|
||||
"poem": "Dark Cavern Silence Depth"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.desert",
|
||||
"description": "symbol.rivencraft.biome.desert.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:desert" },
|
||||
"poem": "Sand Sun Heat Dry"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.dripstone_caves",
|
||||
"description": "symbol.rivencraft.biome.dripstone_caves.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:dripstone_caves" },
|
||||
"poem": "Stone Cavern Point Water"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.forest",
|
||||
"description": "symbol.rivencraft.biome.forest.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:forest" },
|
||||
"poem": "Trees Land Shade Growth"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.grid","description":"symbol.rivencraft.biome.grid.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"grid"},"poem":"Constraint Nature Pattern Grid"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.huge","description":"symbol.rivencraft.biome.huge.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"huge"},"poem":"Constraint Nature Region Huge"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.large","description":"symbol.rivencraft.biome.large.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"large"},"poem":"Constraint Nature Region Large"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.lush_caves",
|
||||
"description": "symbol.rivencraft.biome.lush_caves.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:lush_caves" },
|
||||
"poem": "Growth Cavern Moss Water"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.medium","description":"symbol.rivencraft.biome.medium.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"medium"},"poem":"Constraint Nature Region Medium"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.nether_wastes",
|
||||
"description": "symbol.rivencraft.biome.nether_wastes.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:nether_wastes" },
|
||||
"poem": "Fire Cavern Waste Heat"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.plains",
|
||||
"description": "symbol.rivencraft.biome.plains.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:plains" },
|
||||
"poem": "Grass Land Open Wind"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.single","description":"symbol.rivencraft.biome.single.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"single"},"poem":"Constraint Nature Solitary Form"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.small","description":"symbol.rivencraft.biome.small.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"small"},"poem":"Constraint Nature Region Small"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.biome.the_end",
|
||||
"description": "symbol.rivencraft.biome.the_end.description",
|
||||
"category": "rivencraft:biome",
|
||||
"semantics": { "role": "biome", "value": "minecraft:the_end" },
|
||||
"poem": "Island Void Sky Eternity"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.tiled","description":"symbol.rivencraft.biome.tiled.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"tiled"},"poem":"Constraint Nature Pattern Tile"}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.biome.tiny","description":"symbol.rivencraft.biome.tiny.description","category":"rivencraft:biome","semantics":{"role":"biome_controller","value":"tiny"},"poem":"Constraint Nature Region Tiny"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:deep_dark"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user