Public Access
Allow authored structures in mixed biomes and score overrides (#71)
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
dd219bb58a
commit
78a0f7ee51
8 files changed
+172
-11
No files matched your search
@@ -97,7 +97,7 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
||||
String name = COUNTED_STRUCTURES.get(resource.id());
|
||||
if (name == null) continue;
|
||||
int count = structureCounts.merge(resource.id(), 1, Integer::sum);
|
||||
if (!AgeStructureSelection.biomeCompatible(resource.id(), plan.biomeSelection(), registries)) {
|
||||
if (AgeStructureSelection.requiresBiomeOverride(resource.id(), plan.biomeSelection(), registries)) {
|
||||
contributions.add(new Contribution("forced_biome_" + resource.id().getPath(), symbol.index(),
|
||||
FORCED_STRUCTURE_BIOME_COST,
|
||||
name + " occurrence " + count + " is forced into an unsupported biome"));
|
||||
|
||||
@@ -9,6 +9,9 @@ import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
@@ -165,6 +168,88 @@ public final class AgeStructureGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void mixedEndAndTaigaVillagesChargeForEndPortion(GameTestHelper helper) {
|
||||
for (String firstBiome : List.of("the_end", "plains")) {
|
||||
var writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("terrain/standard"), page("biome/" + firstBiome), page("biome/taiga"),
|
||||
page("biome/tiled"), page("lighting/normal"),
|
||||
page("feature/villages"), page("feature/villages")));
|
||||
var age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(java.util.UUID.randomUUID(), "Mixed " + firstBiome), writing);
|
||||
int expected = firstBiome.equals("the_end") ? 200 : 0;
|
||||
helper.assertValueEqual(age.instability().score(), expected,
|
||||
"Each Village page must charge 100 if any selected biome needs the override");
|
||||
helper.assertValueEqual(age.instability().breakdown().stream()
|
||||
.filter(part -> part.reason().equals("forced_biome_villages")).count(),
|
||||
firstBiome.equals("the_end") ? 2L : 0L,
|
||||
"Compatible Taiga must not erase the End portion's forced-biome charges");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void forcedVillageSurvivesStructureStateBiomeGate(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
var end = new FixedBiomeSource(registries.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_END));
|
||||
var villages = repeated("villages", 1);
|
||||
var random = helper.getLevel().getChunkSource().randomState();
|
||||
var baseline = ChunkGeneratorStructureState.createForNormal(random, 42L, end,
|
||||
AgeStructureSelection.filteredLookup(sets, villages, end));
|
||||
helper.assertTrue(baseline.possibleStructureSets().stream()
|
||||
.noneMatch(holder -> holder.unwrapKey().filter(BuiltinStructureSets.VILLAGES::equals).isPresent()),
|
||||
"Vanilla rejects a Village structure set in a single End biome before the biome mixin runs");
|
||||
|
||||
var planned = ChunkGeneratorStructureState.createForNormal(random, 42L,
|
||||
AgeStructureSelection.planningBiomes(end, sets, villages),
|
||||
AgeStructureSelection.filteredLookup(sets, villages, end));
|
||||
helper.assertTrue(planned.possibleStructureSets().stream()
|
||||
.anyMatch(holder -> holder.unwrapKey().filter(BuiltinStructureSets.VILLAGES::equals).isPresent()),
|
||||
"The authored Village must survive the structure-state biome gate");
|
||||
var village = sets.getOrThrow(BuiltinStructureSets.VILLAGES).value().structures().getFirst().structure();
|
||||
helper.assertTrue(!planned.getPlacementsForStructure(village).isEmpty(),
|
||||
"The forced Village must have an actual placement in the Age structure state");
|
||||
helper.assertTrue(planned.possibleStructureSets().stream().noneMatch(holder -> holder.unwrapKey()
|
||||
.filter(BuiltinStructureSets.DESERT_PYRAMIDS::equals).isPresent()),
|
||||
"Planning biomes must not enable unrelated desert structures in the End Age");
|
||||
helper.assertTrue(end.possibleBiomes().size() == 1 && end.possibleBiomes().contains(
|
||||
registries.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_END)),
|
||||
"The actual Age biome source must remain End-only");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void otherAuthoredStructuresOverrideBiomeWithInstability(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
var end = new FixedBiomeSource(registries.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_END));
|
||||
var random = helper.getLevel().getChunkSource().randomState();
|
||||
for (var entry : Map.of("shipwrecks", BuiltinStructureSets.SHIPWRECKS,
|
||||
"trial_chambers", BuiltinStructureSets.TRIAL_CHAMBERS).entrySet()) {
|
||||
String feature = entry.getKey();
|
||||
var choices = repeated(feature, 1);
|
||||
var planned = ChunkGeneratorStructureState.createForNormal(random, 42L,
|
||||
AgeStructureSelection.planningBiomes(end, sets, choices),
|
||||
AgeStructureSelection.filteredLookup(sets, choices, end));
|
||||
helper.assertTrue(planned.possibleStructureSets().stream()
|
||||
.anyMatch(holder -> holder.unwrapKey().filter(entry.getValue()::equals).isPresent()),
|
||||
"Authored " + feature + " must survive the structure-state biome gate");
|
||||
var structure = sets.getOrThrow(entry.getValue()).value().structures().getFirst().structure();
|
||||
helper.assertTrue(!planned.getPlacementsForStructure(structure).isEmpty(),
|
||||
"Authored " + feature + " must have a placement in the End Age");
|
||||
|
||||
var writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("terrain/standard"), page("biome/the_end"), page("biome/single"),
|
||||
page("feature/" + feature)));
|
||||
var age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(java.util.UUID.randomUUID(), "Forced " + feature), writing);
|
||||
helper.assertValueEqual(age.instability().score(), 100,
|
||||
"An authored " + feature + " outside its normal biomes must add 100 instability");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static List<AgeFeatureSelection.Choice> repeated(String feature, int count) {
|
||||
return java.util.stream.IntStream.range(0, count)
|
||||
.mapToObj(ignored -> new AgeFeatureSelection.Choice(id(feature), Optional.<ResourceLocation>empty()))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -18,6 +19,9 @@ import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
@@ -133,6 +137,13 @@ public final class AgeStructureSelection {
|
||||
return compatible.isEmpty() || biomes.biomes().stream().anyMatch(compatible::contains);
|
||||
}
|
||||
|
||||
/** A mixed pool still needs the authored override wherever its structure is not normally allowed. */
|
||||
public static boolean requiresBiomeOverride(ResourceLocation feature, AgeBiomeSelection biomes,
|
||||
RegistryAccess registries) {
|
||||
Set<ResourceLocation> compatible = compatibleBiomes(feature, registries);
|
||||
return !compatible.isEmpty() && biomes.biomes().stream().anyMatch(biome -> !compatible.contains(biome));
|
||||
}
|
||||
|
||||
/** True when this exact registered structure belongs to an authored structure page. */
|
||||
public static boolean authoredStructure(Structure structure, RegistryAccess registries,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
@@ -177,22 +188,38 @@ public final class AgeStructureSelection {
|
||||
|
||||
public static Stream<Holder<StructureSet>> allowedSets(Stream<Holder<StructureSet>> sets,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
return allowedSets(sets, choices, null);
|
||||
}
|
||||
|
||||
public static Stream<Holder<StructureSet>> allowedSets(Stream<Holder<StructureSet>> sets,
|
||||
List<AgeFeatureSelection.Choice> choices,
|
||||
BiomeSource actualBiomes) {
|
||||
Map<ResourceLocation, Integer> enabled = enabledSets(choices);
|
||||
return sets.filter(holder -> holder.unwrapKey()
|
||||
.map(key -> allowed(key.location(), enabled.keySet())).orElse(false))
|
||||
.map(key -> allowed(key.location(), enabled.keySet())
|
||||
&& (actualBiomes == null || enabled.containsKey(key.location())
|
||||
|| hasActualBiome(holder.value(), actualBiomes))).orElse(false))
|
||||
.map(holder -> scaledHolder(holder, enabled));
|
||||
}
|
||||
|
||||
public static HolderLookup<StructureSet> filteredLookup(HolderLookup<StructureSet> lookup,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
return filteredLookup(lookup, choices, null);
|
||||
}
|
||||
|
||||
public static HolderLookup<StructureSet> filteredLookup(HolderLookup<StructureSet> lookup,
|
||||
List<AgeFeatureSelection.Choice> choices,
|
||||
BiomeSource actualBiomes) {
|
||||
Map<ResourceLocation, Integer> enabled = enabledSets(choices);
|
||||
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> scaled = new HashMap<>();
|
||||
return new HolderLookup<>() {
|
||||
@Override
|
||||
public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
|
||||
if (!allowed(key.location(), enabled.keySet())) return Optional.empty();
|
||||
return lookup.get(key).map(holder -> scaled.computeIfAbsent(key,
|
||||
ignored -> scaledReference(holder, enabled.getOrDefault(key.location(), 1))));
|
||||
return lookup.get(key).filter(holder -> actualBiomes == null || enabled.containsKey(key.location())
|
||||
|| hasActualBiome(holder.value(), actualBiomes))
|
||||
.map(holder -> scaled.computeIfAbsent(key,
|
||||
ignored -> scaledReference(holder, enabled.getOrDefault(key.location(), 1))));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,6 +231,8 @@ public final class AgeStructureSelection {
|
||||
public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||
return lookup.listElements()
|
||||
.filter(holder -> allowed(holder.key().location(), enabled.keySet()))
|
||||
.filter(holder -> actualBiomes == null || enabled.containsKey(holder.key().location())
|
||||
|| hasActualBiome(holder.value(), actualBiomes))
|
||||
.map(holder -> scaled.computeIfAbsent(holder.key(),
|
||||
ignored -> scaledReference(holder,
|
||||
enabled.getOrDefault(holder.key().location(), 1))));
|
||||
@@ -220,6 +249,50 @@ public final class AgeStructureSelection {
|
||||
return !CONTROLLED_SETS.contains(set) || enabled.contains(set);
|
||||
}
|
||||
|
||||
private static boolean hasActualBiome(StructureSet set, BiomeSource actualBiomes) {
|
||||
Set<Holder<Biome>> present = actualBiomes.possibleBiomes();
|
||||
return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.anyMatch(present::contains);
|
||||
}
|
||||
|
||||
/** Only the structure planner sees these extra biomes; terrain and weather keep the real source. */
|
||||
public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup<StructureSet> sets,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
Set<ResourceLocation> enabled = enabledSets(choices).keySet();
|
||||
List<Holder<Biome>> structureBiomes = enabled.stream()
|
||||
.map(id -> sets.get(ResourceKey.create(Registries.STRUCTURE_SET, id)))
|
||||
.flatMap(Optional::stream)
|
||||
.flatMap(holder -> holder.value().structures().stream())
|
||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.distinct().toList();
|
||||
return structureBiomes.isEmpty() ? actual : new PlanningBiomeSource(actual, structureBiomes);
|
||||
}
|
||||
|
||||
private static final class PlanningBiomeSource extends BiomeSource {
|
||||
private final BiomeSource actual;
|
||||
private final List<Holder<Biome>> structureBiomes;
|
||||
|
||||
private PlanningBiomeSource(BiomeSource actual, List<Holder<Biome>> structureBiomes) {
|
||||
this.actual = actual;
|
||||
this.structureBiomes = structureBiomes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Structure-planning biome sources are never serialized");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.concat(actual.possibleBiomes().stream(), structureBiomes.stream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
return actual.getNoiseBiome(x, y, z, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> scaledHolder(Holder<StructureSet> holder,
|
||||
Map<ResourceLocation, Integer> enabled) {
|
||||
int occurrences = holder.unwrapKey().map(key -> enabled.getOrDefault(key.location(), 1)).orElse(1);
|
||||
|
||||
@@ -110,8 +110,10 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets, RandomState random, long seed) {
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed, getBiomeSource(),
|
||||
AgeStructureSelection.filteredLookup(sets, features));
|
||||
BiomeSource actual = getBiomeSource();
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed,
|
||||
AgeStructureSelection.planningBiomes(actual, sets, features),
|
||||
AgeStructureSelection.filteredLookup(sets, features, actual));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -72,8 +72,9 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
Stream<Holder<StructureSet>> entries = settings.structureOverrides()
|
||||
.map(HolderSet::stream)
|
||||
.orElseGet(() -> structures.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource,
|
||||
AgeStructureSelection.allowedSets(entries, features));
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed,
|
||||
AgeStructureSelection.planningBiomes(biomeSource, structures, features),
|
||||
AgeStructureSelection.allowedSets(entries, features, biomeSource));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in new issue
Block a user