This commit is contained in:
Brian Neumann-Fopiano
2026-08-11 20:35:27 -04:00
parent 82290a3090
commit 623a02025c
76 changed files with 4820 additions and 280 deletions
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
@@ -284,8 +285,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structureSets, RandomState randomState, long seed) {
return ChunkGeneratorStructureState.createForNormal(
ChunkGeneratorStructureState state = ChunkGeneratorStructureState.createForNormal(
randomState, seed, structureBiomeSource.forStructureState(structureSets), structureSets);
return ModdedStructureSetFrequencyOverrides.apply(state, configuredImportedStructures());
}
private IrisImportedStructureControl configuredImportedStructures() {
Engine current = engine;
if (current != null && !current.isClosed() && !current.isClosing()) {
IrisDimension dimension = current.getDimension();
if (dimension != null && dimension.getImportedStructures() != null) {
return dimension.getImportedStructures();
}
}
IrisImportedStructureControl importedStructures = configuredPack().dimension().getImportedStructures();
return importedStructures == null ? new IrisImportedStructureControl() : importedStructures;
}
@Override
@@ -444,10 +458,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return;
}
String remedy = integratedEnvironment()
? "enable 'Generate Structures' for this world; Iris requires it, and individual structures "
+ "are denied through importedStructures.disabled"
? "enable 'Generate Structures' for this world; Iris requires it, then deny families through "
+ "importedStructures.disabled or complete keys through importedStructures.disabledExact"
: "set generate-structures=true in server.properties, restart the server, "
+ "and deny individual structures through importedStructures.disabled";
+ "then deny families through importedStructures.disabled or complete keys through "
+ "importedStructures.disabledExact";
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot bind while generate-structures=false; " + remedy);
}
@@ -287,7 +287,8 @@ final class ModdedNativeStructureStage {
+ " because generate-structures=false disables them outside the pack. That flag is fixed when "
+ "the world is created (server.properties generate-structures, or the Generate Structures "
+ "toggle in singleplayer), so it cannot be changed for this world: create a new world with "
+ "structures enabled, and deny individual structures through importedStructures.disabled");
+ "structures enabled, then deny families through importedStructures.disabled or complete keys "
+ "through importedStructures.disabledExact");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
@@ -0,0 +1,285 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.NativeStructureFrequencyScale;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class ModdedStructureSetFrequencyOverrides {
private ModdedStructureSetFrequencyOverrides() {
}
static ChunkGeneratorStructureState apply(
ChunkGeneratorStructureState state,
IrisImportedStructureControl importedStructures
) {
if (!importedStructures.hasFrequencyOverrides()) {
return state;
}
List<Holder<StructureSet>> original = state.possibleStructureSets();
List<Holder<StructureSet>> scaled = scaleSets(original, importedStructures);
if (scaled == original) {
return state;
}
Field possibleSetsField = declaredField(ChunkGeneratorStructureState.class, List.class);
try {
possibleSetsField.set(state, scaled);
return state;
} catch (IllegalAccessException error) {
throw new IllegalStateException(
"Could not apply native structure-set frequency overrides", error);
}
}
static List<Holder<StructureSet>> scaleSets(
List<Holder<StructureSet>> structureSets,
IrisImportedStructureControl importedStructures
) {
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
boolean changed = false;
for (Holder<StructureSet> holder : structureSets) {
Holder<StructureSet> scaled = scaleHolder(
holder, importedStructures, holdersByKey, scaledByIdentity);
scaledSets.add(scaled);
changed |= scaled != holder;
}
return changed ? List.copyOf(scaledSets) : structureSets;
}
private static Holder<StructureSet> scaleHolder(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity
) {
if (scaledByIdentity.containsKey(holder)) {
return scaledByIdentity.get(holder);
}
if (!dependsOnOverride(holder, importedStructures, holdersByKey)) {
scaledByIdentity.put(holder, holder);
return holder;
}
ResourceKey<StructureSet> holderKey = holder.unwrapKey().orElseThrow(() ->
new IllegalStateException("An affected native structure-set exclusion graph has an unkeyed holder"));
ScaledStructureSetHolder scaledHolder = new ScaledStructureSetHolder(holderKey);
scaledByIdentity.put(holder, scaledHolder);
StructureSet originalSet = holder.value();
StructurePlacement originalPlacement = originalSet.placement();
Optional<StructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
Optional<StructurePlacement.ExclusionZone> scaledZone = originalZone;
if (originalZone.isPresent()) {
Holder<StructureSet> target = canonicalHolder(
originalZone.get().otherSet(), holdersByKey);
Holder<StructureSet> scaledTarget = scaleHolder(
target, importedStructures, holdersByKey, scaledByIdentity);
if (scaledTarget != originalZone.get().otherSet()) {
scaledZone = Optional.of(new StructurePlacement.ExclusionZone(
scaledTarget, originalZone.get().chunkCount()));
}
}
double multiplier = importedStructures.frequencyMultiplier(structureSetKey(holder));
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
? originalPlacement
: scalePlacement(originalPlacement, originalZone, scaledZone, multiplier);
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
return scaledHolder;
}
private static boolean dependsOnOverride(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey
) {
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Holder<StructureSet> current = holder;
while (visited.add(current)) {
if (importedStructures.frequencyMultiplier(structureSetKey(current)) != 1D) {
return true;
}
Optional<StructurePlacement.ExclusionZone> zone =
exclusionZone(current.value().placement());
if (zone.isEmpty()) {
return false;
}
current = canonicalHolder(zone.get().otherSet(), holdersByKey);
}
return false;
}
private static Holder<StructureSet> canonicalHolder(
Holder<StructureSet> holder,
Map<String, Holder<StructureSet>> holdersByKey
) {
String key = structureSetKey(holder);
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
}
private static StructurePlacement scalePlacement(
StructurePlacement placement,
Optional<StructurePlacement.ExclusionZone> originalZone,
Optional<StructurePlacement.ExclusionZone> scaledZone,
double multiplier
) {
Vec3i locateOffset = (Vec3i) declaredFieldValue(
StructurePlacement.class, placement, Vec3i.class);
StructurePlacement.FrequencyReductionMethod reductionMethod =
(StructurePlacement.FrequencyReductionMethod) declaredFieldValue(
StructurePlacement.class,
placement,
StructurePlacement.FrequencyReductionMethod.class);
float frequency = (float) declaredFieldValue(
StructurePlacement.class, placement, float.class);
int salt = (int) declaredFieldValue(
StructurePlacement.class, placement, int.class);
if (placement.getClass() == RandomSpreadStructurePlacement.class) {
RandomSpreadStructurePlacement randomSpread =
(RandomSpreadStructurePlacement) placement;
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, randomSpread.spacing(), randomSpread.separation(), multiplier);
if (scale.frequency() == frequency
&& scale.spacing() == randomSpread.spacing()
&& scaledZone.equals(originalZone)) {
return placement;
}
return new RandomSpreadStructurePlacement(
locateOffset,
reductionMethod,
scale.frequency(),
salt,
scaledZone,
scale.spacing(),
randomSpread.separation(),
randomSpread.spreadType());
}
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
ConcentricRingsStructurePlacement rings =
(ConcentricRingsStructurePlacement) placement;
float scaledFrequency = NativeStructureFrequencyScale.probability(frequency, multiplier);
if (scaledFrequency == frequency && scaledZone.equals(originalZone)) {
return placement;
}
return new ConcentricRingsStructurePlacement(
locateOffset,
reductionMethod,
scaledFrequency,
salt,
scaledZone,
rings.distance(),
rings.spread(),
rings.count(),
rings.preferredBiomes());
}
throw new IllegalStateException("Unsupported native structure placement: "
+ placement.getClass().getName());
}
private static Optional<StructurePlacement.ExclusionZone> exclusionZone(
StructurePlacement placement
) {
Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class);
if (value instanceof Optional<?> optional && (optional.isEmpty()
|| optional.get() instanceof StructurePlacement.ExclusionZone)) {
@SuppressWarnings("unchecked")
Optional<StructurePlacement.ExclusionZone> resolved =
(Optional<StructurePlacement.ExclusionZone>) optional;
return resolved;
}
throw new IllegalStateException("Could not read native structure exclusion zone from "
+ placement.getClass().getName());
}
private static String structureSetKey(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
return key.map(resourceKey -> resourceKey.identifier().toString()).orElse(null);
}
private static Object declaredFieldValue(
Class<?> declaringType,
Object target,
Class<?> fieldType
) {
Field field = declaredField(declaringType, fieldType);
try {
return field.get(target);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Could not read " + fieldType.getName()
+ " field on " + declaringType.getName(), error);
}
}
private static Field declaredField(Class<?> declaringType, Class<?> fieldType) {
Field match = null;
for (Field field : declaringType.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
continue;
}
if (match != null) {
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
+ declaringType.getName());
}
match = field;
}
if (match == null || !match.trySetAccessible()) {
throw new IllegalStateException("Missing accessible " + fieldType.getName()
+ " field on " + declaringType.getName());
}
return match;
}
private static final class ScaledStructureSetHolder extends Holder.Reference<StructureSet> {
private ScaledStructureSetHolder(ResourceKey<StructureSet> key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, key, null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
}
@@ -0,0 +1,248 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisStructureSetFrequencyOverride;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
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.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class ModdedStructureSetFrequencyOverridesTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void createStateReplacementScalesOnlyTheExactSetAndRetainsEntries() throws Exception {
Holder<StructureSet> complexes = structureSet(
"minecraft:nether_complexes", 27, 4, 30084232);
Holder<StructureSet> fossils = structureSet(
"minecraft:nether_fossils", 2, 1, 14357921);
ChunkGeneratorStructureState state = state(List.of(complexes, fossils));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
ChunkGeneratorStructureState returned =
ModdedStructureSetFrequencyOverrides.apply(state, control);
assertSame(state, returned);
List<Holder<StructureSet>> scaledSets = state.possibleStructureSets();
RandomSpreadStructurePlacement scaledComplexes =
(RandomSpreadStructurePlacement) scaledSets.get(0).value().placement();
assertEquals(26, scaledComplexes.spacing());
assertEquals(4, scaledComplexes.separation());
assertSame(complexes.value().structures(), scaledSets.get(0).value().structures());
assertSame(fossils, scaledSets.get(1));
}
@Test
public void unrelatedCustomPlacementTypeRemainsUntouched() {
Holder<StructureSet> custom = new BoundStructureSetHolder(
"example:custom",
new StructureSet(List.of(), new UnsupportedPlacement()));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled =
ModdedStructureSetFrequencyOverrides.scaleSets(List.of(custom), control);
assertSame(custom, scaled.getFirst());
}
@Test
public void unrelatedExclusionCycleRemainsUntouched() {
BoundStructureSetHolder first = new BoundStructureSetHolder("example:first");
BoundStructureSetHolder second = new BoundStructureSetHolder("example:second");
first.bind(new StructureSet(List.of(), placement(32, 8, 1, second)));
second.bind(new StructureSet(List.of(), placement(40, 10, 2, first)));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled = ModdedStructureSetFrequencyOverrides.scaleSets(
List.of(first, second), control);
assertSame(first, scaled.get(0));
assertSame(second, scaled.get(1));
}
@Test
public void affectedExclusionCycleUsesBoundScaledHolders() {
BoundStructureSetHolder first = new BoundStructureSetHolder("example:first");
BoundStructureSetHolder second = new BoundStructureSetHolder("example:second");
first.bind(new StructureSet(List.of(), placement(32, 8, 1, second)));
second.bind(new StructureSet(List.of(), placement(40, 10, 2, first)));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:first")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
List<Holder<StructureSet>> scaled = ModdedStructureSetFrequencyOverrides.scaleSets(
List.of(first, second), control);
assertEquals(31, ((RandomSpreadStructurePlacement)
scaled.get(0).value().placement()).spacing());
assertEquals(40, ((RandomSpreadStructurePlacement)
scaled.get(1).value().placement()).spacing());
}
@Test
public void affectedRandomSpreadSubclassFailsInsteadOfLosingSubtypeBehavior() {
Holder<StructureSet> custom = new BoundStructureSetHolder(
"example:custom",
new StructureSet(List.of(), new CustomRandomSpreadPlacement()));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:custom")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
assertThrows(IllegalStateException.class, () ->
ModdedStructureSetFrequencyOverrides.scaleSets(List.of(custom), control));
}
private static Holder<StructureSet> structureSet(
String key,
int spacing,
int separation,
int salt
) {
StructureSet value = new StructureSet(
List.of(),
new RandomSpreadStructurePlacement(
spacing, separation, RandomSpreadType.LINEAR, salt));
return new BoundStructureSetHolder(key, value);
}
private static RandomSpreadStructurePlacement placement(
int spacing,
int separation,
int salt,
Holder<StructureSet> exclusionTarget
) {
return new RandomSpreadStructurePlacement(
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1F,
salt,
Optional.of(new StructurePlacement.ExclusionZone(exclusionTarget, 1)),
spacing,
separation,
RandomSpreadType.LINEAR);
}
private static ChunkGeneratorStructureState state(
List<Holder<StructureSet>> sets
) throws Exception {
Constructor<ChunkGeneratorStructureState> constructor =
ChunkGeneratorStructureState.class.getDeclaredConstructor(
RandomState.class,
BiomeSource.class,
long.class,
long.class,
List.class);
constructor.setAccessible(true);
return constructor.newInstance(
null,
new EmptyBiomeSource(),
1L,
1L,
sets);
}
private static final class BoundStructureSetHolder extends Holder.Reference<StructureSet> {
private BoundStructureSetHolder(String key) {
this(key, null);
}
private BoundStructureSetHolder(String key, StructureSet value) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), value);
}
private void bind(StructureSet value) {
bindValue(value);
}
}
private static final class EmptyBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
return null;
}
@Override
protected com.mojang.serialization.MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException();
}
@Override
protected java.util.stream.Stream<Holder<Biome>> collectPossibleBiomes() {
return HolderSet.<Biome>empty().stream();
}
}
private static final class UnsupportedPlacement extends StructurePlacement {
private UnsupportedPlacement() {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, Optional.empty());
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public StructurePlacementType<?> type() {
return null;
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -53,6 +53,7 @@ public class NativeStructureFailureContractTest {
assertTrue(error.getMessage().contains("overworld:overworld"));
assertTrue(error.getMessage().contains("generate-structures=false"));
assertTrue(error.getMessage().contains("importedStructures.disabled"));
assertTrue(error.getMessage().contains("importedStructures.disabledExact"));
}
@Test