Datapack export fidelity fixes

- Aquatic decorators place on the sea floor in water only
- Decorator waterlogging follows the placement site
- Templates never bake waterlogged blocks
- Object blocks outside the nominal box stay in the template
- Procedural objects export their pre-built variants as content-keyed templates
- Surface objects anchor on the ocean floor heightmap
- Surface objects reject leaf, log, or fluid ground under the origin
- Surface support checks test base corners with a slope-scaled drop
- Support ground search covers the full 16 block window
- MIN_HEIGHT, MAX_HEIGHT, and stilt modes sample terrain across every allowed rotation
- Procedural tree roots extend down to the ground
- Underwater objects need water and stay below the surface
- On-water objects anchor at the fluid surface
- Unsupported coral, sea pickles, and wall fans are left out of templates
- Object placements honor slopeCondition
- Two-block plants place both halves
- Shore decorators need solid ground below
- Decorator coverage matches the Iris style density
- Patchy decorator styles export as noise threshold patches
- Palette end weights round to the nearest count
- Ores in deepslate place their deepslate variants
- Pack description tells players to keep the Default pack selected
- Aquifer floods open sea cells near the fluid height
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-31 03:36:32 -04:00
1 parent 1ed1044474
commit 7ff4292013
25 files changed
+2518 -186

No files matched your search

@@ -34,25 +34,39 @@ public final class StructureTemplateEncoder {
.thenComparingInt(TemplateBlock::z); .thenComparingInt(TemplateBlock::z);
public static TemplateVolume volume(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) { public static TemplateVolume volume(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
List<TemplateBlock> inside = new ArrayList<>();
int outside = 0;
String firstOutside = null;
for (TemplateBlock block : blocks(object, resource, diagnostics)) {
if (block.x() < 0 || block.y() < 0 || block.z() < 0
|| block.x() >= object.getW() || block.y() >= object.getH() || block.z() >= object.getD()) {
outside++;
if (firstOutside == null) {
firstOutside = block.x() + "," + block.y() + "," + block.z();
}
continue;
}
inside.add(block);
}
if (outside > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.OBJECT_BOUNDS, resource,
outside + " blocks outside the " + object.getW() + "x" + object.getH() + "x" + object.getD()
+ " object size are left out of the template, first at " + firstOutside));
}
return new TemplateVolume(object.getW(), object.getH(), object.getD(), inside);
}
public static List<TemplateBlock> blocks(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
List<TemplateBlock> blocks = new ArrayList<>(); List<TemplateBlock> blocks = new ArrayList<>();
Vector3i center = object.getCenter(); Vector3i center = object.getCenter();
int droppedTiles = 0; int droppedTiles = 0;
String firstDropped = null; String firstDropped = null;
int outside = 0;
String firstOutside = null;
Set<String> customBlocks = new TreeSet<>(); Set<String> customBlocks = new TreeSet<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) { for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) {
IrisBlockVector signed = entry.getKey(); IrisBlockVector signed = entry.getKey();
int x = signed.getBlockX() + center.getX(); int x = signed.getBlockX() + center.getX();
int y = signed.getBlockY() + center.getY(); int y = signed.getBlockY() + center.getY();
int z = signed.getBlockZ() + center.getZ(); int z = signed.getBlockZ() + center.getZ();
if (x < 0 || y < 0 || z < 0 || x >= object.getW() || y >= object.getH() || z >= object.getD()) {
outside++;
if (firstOutside == null) {
firstOutside = x + "," + y + "," + z;
}
continue;
}
VanillaBlockState state; VanillaBlockState state;
try { try {
state = VanillaBlockState.parse(entry.getValue().key()); state = VanillaBlockState.parse(entry.getValue().key());
@@ -79,17 +93,12 @@ public final class StructureTemplateEncoder {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_BLOCK, resource, diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_BLOCK, resource,
"Blocks outside Minecraft are left out of the template: " + String.join(", ", customBlocks))); "Blocks outside Minecraft are left out of the template: " + String.join(", ", customBlocks)));
} }
if (outside > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.OBJECT_BOUNDS, resource,
outside + " blocks outside the " + object.getW() + "x" + object.getH() + "x" + object.getD()
+ " object size are left out of the template, first at " + firstOutside));
}
if (droppedTiles > 0) { if (droppedTiles > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.TILE_ENTITY, resource, diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.TILE_ENTITY, resource,
droppedTiles + " tile entities have no vanilla block entity form and export as plain blocks, first " droppedTiles + " tile entities have no vanilla block entity form and export as plain blocks, first "
+ firstDropped)); + firstDropped));
} }
return new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks); return blocks;
} }
public byte[] encode(IrisObject object, List<TemplateBlock> extraBlocks, DataVersion target) throws IOException { public byte[] encode(IrisObject object, List<TemplateBlock> extraBlocks, DataVersion target) throws IOException {
@@ -58,6 +58,17 @@ public record VanillaBlockState(String name, Map<String, String> properties) {
return new VanillaBlockState(name, properties); return new VanillaBlockState(name, properties);
} }
public VanillaBlockState waterlogged(boolean submerged) {
String value = Boolean.toString(submerged);
String current = properties.get("waterlogged");
if (current == null || current.equals(value)) {
return this;
}
Map<String, String> changed = new LinkedHashMap<>(properties);
changed.put("waterlogged", value);
return new VanillaBlockState(name, changed);
}
public String canonical() { public String canonical() {
if (properties.isEmpty()) { if (properties.isEmpty()) {
return name; return name;
@@ -18,6 +18,12 @@ public final class AquiferCompiler {
private static final int FLUID_BAND_CENTER = 20; private static final int FLUID_BAND_CENTER = 20;
private static final double SPREAD_LEVEL_SCALE = 10.0; private static final double SPREAD_LEVEL_SCALE = 10.0;
private static final double LEVEL_ROUNDING_MARGIN = 0.5; private static final double LEVEL_ROUNDING_MARGIN = 0.5;
private static final int OPEN_WATER_DEPTH = 8;
private static final int OPEN_WATER_HEADROOM = 8;
private static final int OPEN_WATER_REACH = 32;
private static final int OPEN_WATER_SHORE = 8;
private static final int OPEN_WATER_ESTIMATE_SLACK = 40;
private static final Field OPEN_WATER_LIFT = new Field.Const(2.0);
private static final Field DRY = new Field.Const(-1.0); private static final Field DRY = new Field.Const(-1.0);
private static final Field PARTIAL = new Field.Const(0.6); private static final Field PARTIAL = new Field.Const(0.6);
private static final Field LOW_BARRIER = new Field.Const(-1.0); private static final Field LOW_BARRIER = new Field.Const(-1.0);
@@ -49,11 +55,21 @@ public final class AquiferCompiler {
} }
boolean wet = any(selection, IrisCaveProfile::isAllowFluid); boolean wet = any(selection, IrisCaveProfile::isAllowFluid);
if (!lavaExported && !wet) { if (!lavaExported && !wet) {
return dry; return new AquiferDef(dry.barrier(), openWater(dry.floodedness()), dry.spread(), dry.lava(), dry.exclusion(), dry.surfaceLevel());
} }
Field spread = lavaExported ? lavaSpread(wet, dry.spread()) : dry.spread(); Field spread = lavaExported ? lavaSpread(wet, dry.spread()) : dry.spread();
Field lava = lavaExported ? lavaBands() : NO_LAVA; Field lava = lavaExported ? lavaBands() : NO_LAVA;
return new AquiferDef(LOW_BARRIER, floodedness(selection, lavaExported), spread, lava, NO_EXCLUSION, inputs.aquiferSurface()); return new AquiferDef(LOW_BARRIER, openWater(floodedness(selection, lavaExported)), spread, lava, NO_EXCLUSION, inputs.aquiferSurface());
}
private Field openWater(Field caveFlood) {
Field surface = inputs.surface();
Field nearFluid = new Field.RangeIf(blockY, inputs.fluidHeight() - OPEN_WATER_REACH, inputs.fluidHeight() + OPEN_WATER_HEADROOM + 1.0, INSIDE_BAND, DRY);
Field lowEstimate = new Field.Binary(Field.BinaryOp.SUB, new Field.Const(inputs.fluidHeight() + OPEN_WATER_ESTIMATE_SLACK), inputs.aquiferSurface());
Field aboveFloor = new Field.Binary(Field.BinaryOp.ADD, new Field.Binary(Field.BinaryOp.SUB, blockY, surface), new Field.Const(OPEN_WATER_DEPTH));
Field lowColumn = new Field.Binary(Field.BinaryOp.SUB, new Field.Const(inputs.fluidHeight() + OPEN_WATER_SHORE + LEVEL_ROUNDING_MARGIN), surface);
Field lift = new Field.Gate(aboveFloor, new Field.Gate(lowColumn, OPEN_WATER_LIFT));
return new Field.Binary(Field.BinaryOp.ADD, caveFlood, new Field.Gate(nearFluid, new Field.Gate(lowEstimate, lift)));
} }
private Field floodedness(CaveSelection selection, boolean lavaExported) { private Field floodedness(CaveSelection selection, boolean lavaExported) {
@@ -6,7 +6,6 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart;
import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle; import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisSlopeClip;
import art.arcane.iris.pack.datapack.export.VanillaBlockState; import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.BlockStateResolver; import art.arcane.iris.pack.datapack.export.compile.BlockStateResolver;
import art.arcane.iris.pack.datapack.export.compile.ExportedBiomes; import art.arcane.iris.pack.datapack.export.compile.ExportedBiomes;
@@ -22,10 +21,12 @@ import com.google.gson.JsonObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
public final class DecoratorCompiler { public final class DecoratorCompiler {
@@ -33,9 +34,14 @@ public final class DecoratorCompiler {
private static final double CAVE_AIR_SHARE = 0.08; private static final double CAVE_AIR_SHARE = 0.08;
private static final int CAVE_SCAN_STEPS = 12; private static final int CAVE_SCAN_STEPS = 12;
private static final int MAX_ATTEMPTS = 256; private static final int MAX_ATTEMPTS = 256;
private static final double SLOPE_CEILING = 10.0; private static final int MAX_PATTERN_STATES = 64;
private static final VanillaBlockState AIR_STATE = new VanillaBlockState("minecraft:air", Map.of());
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air"); private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
private static final String STYLE_MESSAGE = "Decorators gated or varied by a noise style place at the style's average density without its pattern"; private static final Set<String> AQUATIC = Set.of("minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:kelp",
"minecraft:kelp_plant", "minecraft:sea_pickle");
private static final String STYLE_MESSAGE = "Decorators gated by a noise style keep the style's coverage, with its pattern fitted to Minecraft noise";
private static final String STYLE_STACK_MESSAGE = "Stacked, forced, underwater, and ceiling decorators gated by a noise style keep the style's coverage without its pattern";
private static final String VARIANCE_MESSAGE = "Decorator palettes varied by a noise style pick their blocks at random";
private static final String STACK_MESSAGE = "Decorator stacks scaled to cave height use their fixed stack range"; private static final String STACK_MESSAGE = "Decorator stacks scaled to cave height use their fixed stack range";
private static final String CAVE_MESSAGE = "Cave ceiling decorators place at an estimated density because features cannot enumerate every cave ceiling"; private static final String CAVE_MESSAGE = "Cave ceiling decorators place at an estimated density because features cannot enumerate every cave ceiling";
@@ -44,6 +50,8 @@ public final class DecoratorCompiler {
private final BlockStateResolver blocks; private final BlockStateResolver blocks;
private final FeatureCatalog catalog; private final FeatureCatalog catalog;
private final WorldgenSyntax syntax; private final WorldgenSyntax syntax;
private final Map<CoverageKey, DecoratorCoverage.Coverage> coverages = new HashMap<>();
private final Map<DecoratorCoverage.Coverage, DecoratorCoverage.Pattern> patterns = new HashMap<>();
public DecoratorCompiler(PackCompileContext context, ExportedBiomes biomes, BlockStateResolver blocks, FeatureCatalog catalog) { public DecoratorCompiler(PackCompileContext context, ExportedBiomes biomes, BlockStateResolver blocks, FeatureCatalog catalog) {
this.context = context; this.context = context;
@@ -115,7 +123,7 @@ public final class DecoratorCompiler {
List<Candidate> candidates = bucket.getValue(); List<Candidate> candidates = bucket.getValue();
double[] chances = new double[candidates.size()]; double[] chances = new double[candidates.size()];
for (int index = 0; index < chances.length; index++) { for (int index = 0; index < chances.length; index++) {
chances[index] = Math.max(0.0, Math.min(1.0, candidates.get(index).decorator().getChance())); chances[index] = candidates.get(index).coverage().share();
} }
double[] shares = columnShares(chances); double[] shares = columnShares(chances);
for (int index = 0; index < shares.length; index++) { for (int index = 0; index < shares.length; index++) {
@@ -129,9 +137,12 @@ public final class DecoratorCompiler {
} }
if (bucket.getKey() == IrisDecorationPart.CEILING) { if (bucket.getKey() == IrisDecorationPart.CEILING) {
report(resource, ExportLimit.CAVE_DECORATOR, CAVE_MESSAGE); report(resource, ExportLimit.CAVE_DECORATOR, CAVE_MESSAGE);
if (candidate.coverage().patterned()) {
report(resource, ExportLimit.DECORATOR_STYLE, STYLE_STACK_MESSAGE);
}
context.features().add(biomeId, GenerationStep.UNDERGROUND_DECORATION, ceiling(candidate, shares[index])); context.features().add(biomeId, GenerationStep.UNDERGROUND_DECORATION, ceiling(candidate, shares[index]));
} else { } else {
context.features().add(biomeId, GenerationStep.VEGETAL_DECORATION, column(bucket.getKey(), candidate, sequential[index])); context.features().add(biomeId, GenerationStep.VEGETAL_DECORATION, column(bucket.getKey(), candidate, sequential[index], resource));
} }
} }
} }
@@ -141,24 +152,19 @@ public final class DecoratorCompiler {
List<WeightedState> entries = new ArrayList<>(); List<WeightedState> entries = new ArrayList<>();
int airWeight = 0; int airWeight = 0;
int total = 0; int total = 0;
for (IrisBlockData block : decorator.getPalette()) { for (WeightedState entry : weighted(decorator.getPalette(), decorator.getVariance(), resource)) {
Optional<VanillaBlockState> state = blocks.resolve(block, resource); total += entry.weight();
if (state.isEmpty()) { if (AIR.contains(entry.state().name())) {
continue; airWeight += entry.weight();
}
int weight = Math.max(1, block.getWeight());
total += weight;
if (AIR.contains(state.get().name())) {
airWeight += weight;
} else { } else {
entries.add(new WeightedState(state.get(), weight)); entries.add(entry);
} }
} }
if (total == 0) { if (total == 0) {
return Optional.empty(); return Optional.empty();
} }
if (!isStatic(decorator.getStyle()) || (entries.size() > 1 && !isStatic(decorator.getVariance()))) { if (entries.size() > 1 && !isStatic(decorator.getVariance())) {
report(resource, ExportLimit.DECORATOR_STYLE, STYLE_MESSAGE); report(resource, ExportLimit.DECORATOR_STYLE, VARIANCE_MESSAGE);
} }
if (decorator.isScaleStack()) { if (decorator.isScaleStack()) {
report(resource, ExportLimit.SCALE_STACK, STACK_MESSAGE); report(resource, ExportLimit.SCALE_STACK, STACK_MESSAGE);
@@ -167,35 +173,64 @@ public final class DecoratorCompiler {
report(resource, ExportLimit.SLOPE, RepaintFeatureCompiler.SLOPE_MESSAGE); report(resource, ExportLimit.SLOPE, RepaintFeatureCompiler.SLOPE_MESSAGE);
} }
List<WeightedState> top = new ArrayList<>(); List<WeightedState> top = new ArrayList<>();
for (IrisBlockData block : decorator.getTopPalette()) { for (WeightedState entry : weighted(decorator.getTopPalette(), decorator.getVariance(), resource)) {
Optional<VanillaBlockState> state = blocks.resolve(block, resource); if (!AIR.contains(entry.state().name())) {
if (state.isPresent() && !AIR.contains(state.get().name())) { top.add(entry);
top.add(new WeightedState(state.get(), Math.max(1, block.getWeight())));
} }
} }
VanillaBlockState force = decorator.getForceBlock() == null ? null : blocks.resolve(decorator.getForceBlock(), resource).orElse(null); VanillaBlockState force = decorator.getForceBlock() == null ? null : blocks.resolve(decorator.getForceBlock(), resource).orElse(null);
return Optional.of(new Candidate(decorator, entries, top, (double) airWeight / total, force, names(decorator.getWhitelist(), resource), return Optional.of(new Candidate(decorator, entries, top, (double) airWeight / total, force, aquatic(entries, top),
names(decorator.getBlacklist(), resource))); names(decorator.getWhitelist(), resource), names(decorator.getBlacklist(), resource), coverage(decorator)));
} }
private String column(IrisDecorationPart part, Candidate candidate, double sequential) { private List<WeightedState> weighted(List<IrisBlockData> palette, IrisGeneratorStyle variance, String resource) {
String featureId = feature(candidate, false); List<WeightedState> resolved = new ArrayList<>(palette.size());
int count = 0;
for (IrisBlockData block : palette) {
Optional<VanillaBlockState> state = blocks.resolve(block, resource);
if (state.isPresent()) {
int weight = Math.max(1, block.getWeight());
resolved.add(new WeightedState(state.get(), weight));
count += weight;
}
}
if (count <= 1 || !isStatic(variance)) {
return resolved;
}
List<WeightedState> rounded = new ArrayList<>(resolved.size());
int start = 0;
for (WeightedState entry : resolved) {
int end = start + entry.weight();
rounded.add(new WeightedState(entry.state(), 2 * entry.weight() - (start == 0 ? 1 : 0) - (end == count ? 1 : 0)));
start = end;
}
return rounded;
}
private String column(IrisDecorationPart part, Candidate candidate, double sequential, String resource) {
IrisDecorationPart target = candidate.aquatic() && part == IrisDecorationPart.NONE ? IrisDecorationPart.SEA_FLOOR : part;
boolean underwater = target == IrisDecorationPart.SEA_FLOOR;
boolean patterned = candidate.coverage().patterned() && !underwater && single(candidate);
if (candidate.coverage().patterned()) {
report(resource, ExportLimit.DECORATOR_STYLE, patterned ? STYLE_MESSAGE : STYLE_STACK_MESSAGE);
}
String featureId = patterned ? patterned(candidate) : feature(candidate, false, underwater);
double chance = patterned ? Math.min(1.0, sequential / candidate.coverage().share()) : sequential;
JsonArray placement = new JsonArray(); JsonArray placement = new JsonArray();
String heightmap = part == IrisDecorationPart.SEA_FLOOR ? Placements.OCEAN_FLOOR : Placements.WORLD_SURFACE;
for (JsonObject modifier : Placements.everyColumn()) { for (JsonObject modifier : Placements.everyColumn()) {
placement.add(modifier); placement.add(modifier);
} }
placement.add(Placements.heightmap(heightmap)); placement.add(Placements.heightmap(underwater ? Placements.OCEAN_FLOOR : Placements.WORLD_SURFACE));
if (sequential < 1.0) { if (chance < 1.0) {
placement.add(Placements.randomChance(sequential)); placement.add(Placements.randomChance(chance));
} }
placement.add(Placements.biome()); placement.add(Placements.biome());
placement.add(Placements.filter(Placements.allOf(predicates(part, candidate)))); placement.add(Placements.filter(Placements.allOf(predicates(target, candidate))));
return catalog.placed(name(candidate), featureId, placement); return catalog.placed(name(candidate), featureId, placement);
} }
private String ceiling(Candidate candidate, double share) { private String ceiling(Candidate candidate, double share) {
String featureId = feature(candidate, true); String featureId = feature(candidate, true, false);
JsonArray placement = new JsonArray(); JsonArray placement = new JsonArray();
placement.add(Placements.count(Placements.meanCount(Math.min(MAX_ATTEMPTS, COLUMNS * share / CAVE_AIR_SHARE)))); placement.add(Placements.count(Placements.meanCount(Math.min(MAX_ATTEMPTS, COLUMNS * share / CAVE_AIR_SHARE))));
placement.add(Placements.inSquare()); placement.add(Placements.inSquare());
@@ -210,13 +245,19 @@ public final class DecoratorCompiler {
private List<JsonObject> predicates(IrisDecorationPart part, Candidate candidate) { private List<JsonObject> predicates(IrisDecorationPart part, Candidate candidate) {
List<JsonObject> predicates = new ArrayList<>(); List<JsonObject> predicates = new ArrayList<>();
switch (part) { switch (part) {
case SEA_FLOOR -> predicates.add(Placements.water(0, 0, 0)); case SEA_FLOOR -> {
predicates.add(Placements.water(0, 0, 0));
if (tall(candidate)) {
predicates.add(Placements.water(0, 1, 0));
}
}
case SEA_SURFACE -> { case SEA_SURFACE -> {
predicates.add(Placements.air(0, 0, 0)); predicates.add(Placements.air(0, 0, 0));
predicates.add(Placements.water(0, -1, 0)); predicates.add(Placements.water(0, -1, 0));
} }
case SHORE_LINE -> { case SHORE_LINE -> {
predicates.add(Placements.air(0, 0, 0)); predicates.add(Placements.air(0, 0, 0));
predicates.add(Placements.solid(0, -1, 0));
predicates.add(Placements.anyOf(List.of(Placements.water(1, -1, 0), Placements.water(-1, -1, 0), Placements.water(0, -1, 1), predicates.add(Placements.anyOf(List.of(Placements.water(1, -1, 0), Placements.water(-1, -1, 0), Placements.water(0, -1, 1),
Placements.water(0, -1, -1)))); Placements.water(0, -1, -1))));
} }
@@ -231,40 +272,35 @@ public final class DecoratorCompiler {
if (!candidate.blacklist().isEmpty()) { if (!candidate.blacklist().isEmpty()) {
predicates.add(Placements.not(Placements.matchingBlocks(candidate.blacklist(), 0, -1, 0))); predicates.add(Placements.not(Placements.matchingBlocks(candidate.blacklist(), 0, -1, 0)));
} }
IrisSlopeClip slope = candidate.decorator().getSlopeCondition(); predicates.addAll(Placements.slopeLimits(candidate.decorator().getSlopeCondition()));
if (slope != null && !slope.isDefault()) {
if (slope.getMinimumSlope() > 0) {
predicates.add(Placements.steeperThan(rise(slope.getMinimumSlope()), -1));
}
if (slope.getMaximumSlope() < SLOPE_CEILING) {
predicates.add(Placements.not(Placements.steeperThan(rise(slope.getMaximumSlope()) + 1, -1)));
}
}
return predicates; return predicates;
} }
private String feature(Candidate candidate, boolean hanging) { private String feature(Candidate candidate, boolean hanging, boolean underwater) {
IrisDecorator decorator = candidate.decorator(); IrisDecorator decorator = candidate.decorator();
int stackMax = Math.max(1, Math.min(decorator.getStackMax(), Math.max(1, decorator.getAbsoluteMaxStack()))); int stackMax = stackMax(decorator);
int stackMin = Math.max(1, Math.min(decorator.getStackMin(), stackMax)); int stackMin = Math.max(1, Math.min(decorator.getStackMin(), stackMax));
String name = name(candidate); String name = name(candidate);
List<WeightedState> entries = waterlogged(candidate.entries(), underwater);
List<WeightedState> top = waterlogged(candidate.top(), underwater);
String main; String main;
if (!hanging && stackMax == 1 && candidate.top().isEmpty()) { boolean tall = !hanging && tall(candidate);
if (tall || !hanging && stackMax == 1 && top.isEmpty()) {
JsonObject config = new JsonObject(); JsonObject config = new JsonObject();
config.add("to_place", syntax.provider(candidate.entries())); config.add("to_place", syntax.provider(tall ? lowerHalves(entries) : entries));
main = catalog.feature(name, "minecraft:simple_block", config); main = catalog.feature(name, "minecraft:simple_block", config);
} else { } else {
JsonArray layers = new JsonArray(2); JsonArray layers = new JsonArray(2);
if (candidate.top().isEmpty()) { if (top.isEmpty()) {
layers.add(layer(Placements.intRange(stackMin, stackMax), candidate.entries())); layers.add(layer(Placements.intRange(stackMin, stackMax), entries));
} else { } else {
layers.add(layer(Placements.intRange(stackMin - 1, stackMax - 1), candidate.entries())); layers.add(layer(Placements.intRange(stackMin - 1, stackMax - 1), entries));
layers.add(layer(Placements.intRange(1, 1), candidate.top())); layers.add(layer(Placements.intRange(1, 1), top));
} }
JsonObject config = new JsonObject(); JsonObject config = new JsonObject();
config.add("layers", layers); config.add("layers", layers);
config.addProperty("direction", hanging ? "down" : "up"); config.addProperty("direction", hanging ? "down" : "up");
config.add("allowed_placement", Placements.air(0, 0, 0)); config.add("allowed_placement", underwater ? Placements.water(0, 0, 0) : Placements.air(0, 0, 0));
config.addProperty("prioritize_tip", true); config.addProperty("prioritize_tip", true);
main = catalog.feature(name, "minecraft:block_column", config); main = catalog.feature(name, "minecraft:block_column", config);
} }
@@ -284,6 +320,112 @@ public final class DecoratorCompiler {
return catalog.feature(name + "_forced", "minecraft:sequence", sequence); return catalog.feature(name + "_forced", "minecraft:sequence", sequence);
} }
private String patterned(Candidate candidate) {
IrisDecorator decorator = candidate.decorator();
DecoratorCoverage.Pattern pattern = patterns.computeIfAbsent(candidate.coverage(), coverage -> DecoratorCoverage.pattern(coverage, syntax.target()));
String salt = name(candidate) + "|" + decorator.getStyle().getStyle() + "|" + decorator.getStyle().getZoom() + "|" + decorator.getChance();
JsonObject provider = new JsonObject();
provider.addProperty("type", "minecraft:noise_threshold");
provider.addProperty("seed", (long) salt.hashCode());
provider.add("noise", patternNoise());
provider.addProperty("scale", (float) pattern.scale());
provider.addProperty("threshold", (float) pattern.threshold());
provider.addProperty("high_chance", 0.0f);
provider.add("default_state", syntax.blockState(AIR_STATE));
provider.add("low_states", expanded(waterlogged(candidate.entries(), false)));
JsonArray high = new JsonArray(1);
high.add(syntax.blockState(AIR_STATE));
provider.add("high_states", high);
JsonObject config = new JsonObject();
config.add("to_place", provider);
return catalog.feature(name(candidate), "minecraft:simple_block", config);
}
private JsonArray expanded(List<WeightedState> entries) {
int divisor = 0;
int total = 0;
for (WeightedState entry : entries) {
divisor = gcd(divisor, entry.weight());
total += entry.weight();
}
double shrink = Math.min(1.0, (double) MAX_PATTERN_STATES * divisor / total);
JsonArray states = new JsonArray();
for (WeightedState entry : entries) {
long copies = Math.max(1L, Math.round(entry.weight() / (double) divisor * shrink));
for (long copy = 0; copy < copies; copy++) {
states.add(syntax.blockState(entry.state()));
}
}
return states;
}
private static JsonObject patternNoise() {
JsonArray amplitudes = new JsonArray(DecoratorCoverage.PATTERN_AMPLITUDES.size());
for (double amplitude : DecoratorCoverage.PATTERN_AMPLITUDES) {
amplitudes.add(amplitude);
}
JsonObject noise = new JsonObject();
noise.addProperty("base_octave", DecoratorCoverage.PATTERN_OCTAVE);
noise.addProperty("octave_count", DecoratorCoverage.PATTERN_AMPLITUDES.size());
noise.add("amplitude_modifiers", amplitudes);
noise.addProperty("base_amplitude", 1.0);
noise.addProperty("normalize", false);
return noise;
}
private DecoratorCoverage.Coverage coverage(IrisDecorator decorator) {
double chance = Math.max(0.0, Math.min(1.0, decorator.getChance()));
if (isStatic(decorator.getStyle())) {
return new DecoratorCoverage.Coverage(chance, 0.0);
}
return coverages.computeIfAbsent(new CoverageKey(decorator.getStyle(), chance),
key -> DecoratorCoverage.measure(key.style(), key.chance(), context.data()));
}
private static boolean single(Candidate candidate) {
IrisDecorator decorator = candidate.decorator();
return Math.min(decorator.getStackMax(), decorator.getAbsoluteMaxStack()) <= 1 && candidate.top().isEmpty() && candidate.force() == null;
}
private static int gcd(int first, int second) {
return second == 0 ? first : gcd(second, first % second);
}
private static List<WeightedState> waterlogged(List<WeightedState> entries, boolean underwater) {
List<WeightedState> placed = new ArrayList<>(entries.size());
for (WeightedState entry : entries) {
placed.add(new WeightedState(entry.state().waterlogged(underwater), entry.weight()));
}
return placed;
}
private static int stackMax(IrisDecorator decorator) {
return Math.max(1, Math.min(decorator.getStackMax(), Math.max(1, decorator.getAbsoluteMaxStack())));
}
private static boolean tall(Candidate candidate) {
if (stackMax(candidate.decorator()) != 1) {
return false;
}
for (WeightedState entry : candidate.entries()) {
String half = entry.state().properties().get("half");
if (!"lower".equals(half) && !"upper".equals(half)) {
return false;
}
}
return true;
}
private static List<WeightedState> lowerHalves(List<WeightedState> entries) {
List<WeightedState> lower = new ArrayList<>(entries.size());
for (WeightedState entry : entries) {
Map<String, String> properties = new TreeMap<>(entry.state().properties());
properties.put("half", "lower");
lower.add(new WeightedState(new VanillaBlockState(entry.state().name(), properties), entry.weight()));
}
return lower;
}
private JsonObject layer(JsonElement height, List<WeightedState> entries) { private JsonObject layer(JsonElement height, List<WeightedState> entries) {
JsonObject layer = new JsonObject(); JsonObject layer = new JsonObject();
layer.add("height", height); layer.add("height", height);
@@ -321,11 +463,36 @@ public final class DecoratorCompiler {
return style == null || style.getStyle() == null || style.getStyle() == NoiseStyle.STATIC; return style == null || style.getStyle() == null || style.getStyle() == NoiseStyle.STATIC;
} }
private static int rise(double slope) { private static boolean aquatic(List<WeightedState> entries, List<WeightedState> top) {
return Math.max(1, (int) Math.round(slope)); if (entries.isEmpty()) {
return false;
}
for (WeightedState entry : entries) {
if (!aquatic(entry.state())) {
return false;
}
}
for (WeightedState entry : top) {
if (!aquatic(entry.state())) {
return false;
}
}
return true;
}
private static boolean aquatic(VanillaBlockState state) {
String name = state.name();
if ("true".equals(state.properties().get("waterlogged")) || AQUATIC.contains(name)) {
return true;
}
return name.startsWith("minecraft:") && name.contains("_coral") && !name.startsWith("minecraft:dead_");
} }
private record Candidate(IrisDecorator decorator, List<WeightedState> entries, List<WeightedState> top, double airShare, private record Candidate(IrisDecorator decorator, List<WeightedState> entries, List<WeightedState> top, double airShare,
VanillaBlockState force, List<String> whitelist, List<String> blacklist) { VanillaBlockState force, boolean aquatic, List<String> whitelist, List<String> blacklist,
DecoratorCoverage.Coverage coverage) {
}
private record CoverageKey(IrisGeneratorStyle style, double chance) {
} }
} }
@@ -0,0 +1,113 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.compile.noise.VanillaNoiseModel;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import java.util.Arrays;
import java.util.List;
final class DecoratorCoverage {
static final int PATTERN_OCTAVE = 0;
static final List<Double> PATTERN_AMPLITUDES = List.of(1.0);
private static final long STYLE_SEED = 0x0DEC_0A7EL;
private static final String PATTERN_NOISE = "iris_export:decorator_pattern";
private static final int GRID = 128;
private static final double STEP_X = 211.0;
private static final double STEP_Z = 157.0;
private static final int[] LAGS = {1, 2, 4, 8, 16, 32, 64, 128};
private static final double HALF_EXCESS = 0.5;
private static final double MIN_PATCH = 2.0;
private static final int MIN_HITS = 64;
private static final double VANILLA_SCALE = 1.0 / 64.0;
private static final double VANILLA_Y = 64.0;
private static final double MAX_THRESHOLD = 1.0;
private DecoratorCoverage() {
}
static Coverage measure(IrisGeneratorStyle style, double chance, IrisData data) {
CNG generator = style.createNoCache(new RNG(STYLE_SEED), data);
double zoom = style.getZoom();
return measure((x, z) -> generator.fitDouble(0.0, 1.0, x / zoom, z / zoom) <= chance);
}
static Pattern pattern(Coverage coverage, DataVersion target) {
VanillaNoiseModel model = VanillaNoiseModel.create(PATTERN_NOISE, PATTERN_OCTAVE, PATTERN_AMPLITUDES, 0L, target);
double[] sorted = new double[GRID * GRID];
int index = 0;
for (int i = 0; i < GRID; i++) {
for (int j = 0; j < GRID; j++) {
sorted[index++] = model.sample(i * STEP_X * VANILLA_SCALE, VANILLA_Y * VANILLA_SCALE, j * STEP_Z * VANILLA_SCALE);
}
}
Arrays.sort(sorted);
double position = coverage.share() * (sorted.length - 1);
int lower = (int) Math.floor(position);
int upper = Math.min(sorted.length - 1, lower + 1);
double threshold = sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
double clamped = Math.max(-MAX_THRESHOLD, Math.min(MAX_THRESHOLD, threshold));
Coverage vanilla = measure((x, z) -> model.sample(x * VANILLA_SCALE, VANILLA_Y * VANILLA_SCALE, z * VANILLA_SCALE) < clamped);
return new Pattern(clamped, VANILLA_SCALE * vanilla.patch() / coverage.patch());
}
private static Coverage measure(Gate gate) {
int hits = 0;
int[] both = new int[LAGS.length];
for (int i = 0; i < GRID; i++) {
for (int j = 0; j < GRID; j++) {
double x = i * STEP_X;
double z = j * STEP_Z;
if (!gate.passes(x, z)) {
continue;
}
hits++;
for (int lag = 0; lag < LAGS.length; lag++) {
both[lag] += gate.passes(x + LAGS[lag], z) ? 1 : 0;
both[lag] += gate.passes(x, z + LAGS[lag]) ? 1 : 0;
}
}
}
double share = (double) hits / (GRID * GRID);
if (hits < MIN_HITS || share >= 1.0) {
return new Coverage(share, 0.0);
}
return new Coverage(share, patch(both, hits, share));
}
private static double patch(int[] both, int hits, double share) {
double previousLag = 0.0;
double previousExcess = 1.0;
for (int lag = 0; lag < LAGS.length; lag++) {
double excess = ((double) both[lag] / (2.0 * hits) - share) / (1.0 - share);
if (excess < HALF_EXCESS) {
double fraction = (previousExcess - HALF_EXCESS) / (previousExcess - excess);
if (previousLag == 0.0) {
return LAGS[lag] * fraction;
}
double low = Math.log(previousLag);
return Math.exp(low + (Math.log(LAGS[lag]) - low) * fraction);
}
previousLag = LAGS[lag];
previousExcess = excess;
}
return LAGS[LAGS.length - 1];
}
record Coverage(double share, double patch) {
boolean patterned() {
return share > 0.0 && share < 1.0 && patch >= MIN_PATCH;
}
}
record Pattern(double threshold, double scale) {
}
@FunctionalInterface
private interface Gate {
boolean passes(double x, double z);
}
}
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
@@ -33,6 +34,14 @@ public final class DepositCompiler {
private static final int MAX_ANCHOR = 2031; private static final int MAX_ANCHOR = 2031;
private static final String HOST_TAG = "deposit_host"; private static final String HOST_TAG = "deposit_host";
private static final List<String> HOST_TAG_BASE = List.of("#minecraft:base_stone_overworld", "#minecraft:dirt", "#minecraft:sand"); private static final List<String> HOST_TAG_BASE = List.of("#minecraft:base_stone_overworld", "#minecraft:dirt", "#minecraft:sand");
private static final String DEEPSLATE = "minecraft:deepslate";
private static final String DEEPSLATE_PREFIX = "minecraft:deepslate_";
private static final Set<String> DEEPSLATE_ORES = Set.of("coal", "emerald", "diamond", "copper", "gold", "iron", "lapis", "redstone");
private static final Set<String> DEEPSLATE_HOSTS = Set.of(DEEPSLATE, "minecraft:deepslate_bricks", "minecraft:deepslate_brick_slab",
"minecraft:deepslate_brick_stairs", "minecraft:deepslate_brick_wall", "minecraft:deepslate_tiles", "minecraft:deepslate_tile_slab",
"minecraft:deepslate_tile_stairs", "minecraft:deepslate_tile_wall", "minecraft:cracked_deepslate_tiles", "minecraft:deepslate_coal_ore",
"minecraft:deepslate_iron_ore", "minecraft:deepslate_copper_ore", "minecraft:deepslate_diamond_ore", "minecraft:deepslate_emerald_ore",
"minecraft:deepslate_gold_ore", "minecraft:deepslate_lapis_ore", "minecraft:deepslate_redstone_ore");
private final PackCompileContext context; private final PackCompileContext context;
private final ExportedBiomes biomes; private final ExportedBiomes biomes;
@@ -110,12 +119,17 @@ public final class DepositCompiler {
List<JsonObject> hosts = hosts(deposit, resource); List<JsonObject> hosts = hosts(deposit, resource);
List<String> ores = new ArrayList<>(states.size()); List<String> ores = new ArrayList<>(states.size());
for (VanillaBlockState state : states) { for (VanillaBlockState state : states) {
JsonArray targets = new JsonArray(hosts.size()); JsonArray targets = new JsonArray(hosts.size() + 1);
VanillaBlockState deepslateOre = stoneVariant(state, true);
if (hosts.size() == 1 && "minecraft:tag_match".equals(hosts.getFirst().get("predicate_type").getAsString()) && !deepslateOre.equals(state)) {
JsonObject deepslate = new JsonObject();
deepslate.addProperty("predicate_type", "minecraft:block_match");
deepslate.addProperty("block", DEEPSLATE);
targets.add(target(deepslate, deepslateOre));
}
for (JsonObject host : hosts) { for (JsonObject host : hosts) {
JsonObject target = new JsonObject(); boolean deepslateHost = host.has("block") && DEEPSLATE_HOSTS.contains(host.get("block").getAsString());
target.add("target", host.deepCopy()); targets.add(target(host, stoneVariant(state, deepslateHost)));
target.add("state", syntax.blockState(state));
targets.add(target);
} }
JsonObject config = new JsonObject(); JsonObject config = new JsonObject();
config.addProperty("size", Math.max(0, size)); config.addProperty("size", Math.max(0, size));
@@ -135,6 +149,23 @@ public final class DepositCompiler {
return catalog.feature("deposit/" + path(states.getFirst()) + "_mix", "minecraft:simple_random_selector", selector); return catalog.feature("deposit/" + path(states.getFirst()) + "_mix", "minecraft:simple_random_selector", selector);
} }
private JsonObject target(JsonObject host, VanillaBlockState state) {
JsonObject target = new JsonObject();
target.add("target", host.deepCopy());
target.add("state", syntax.blockState(state));
return target;
}
private static VanillaBlockState stoneVariant(VanillaBlockState ore, boolean deepslate) {
String name = ore.name();
boolean deep = name.startsWith(DEEPSLATE_PREFIX);
String base = deep ? name.substring(DEEPSLATE_PREFIX.length()) : name.substring(name.indexOf(':') + 1);
if (deep == deepslate || !base.endsWith("_ore") || !DEEPSLATE_ORES.contains(base.substring(0, base.length() - "_ore".length()))) {
return ore;
}
return new VanillaBlockState(deepslate ? DEEPSLATE_PREFIX + base : "minecraft:" + base, ore.properties());
}
private JsonArray placement(IrisDepositGenerator deposit, List<String> biomeIds, String resource) { private JsonArray placement(IrisDepositGenerator deposit, List<String> biomeIds, String resource) {
int minCount = Math.max(0, Math.min(deposit.getMinPerChunk(), deposit.getMaxPerChunk())); int minCount = Math.max(0, Math.min(deposit.getMinPerChunk(), deposit.getMaxPerChunk()));
int maxCount = Math.max(minCount, deposit.getMaxPerChunk()); int maxCount = Math.max(minCount, deposit.getMaxPerChunk());
@@ -1,10 +1,12 @@
package art.arcane.iris.pack.datapack.export.compile.decoration; package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.generation.terrain.IrisSlopeClip;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -14,6 +16,7 @@ public final class Placements {
private static final int CHUNK_EDGE = 15; private static final int CHUNK_EDGE = 15;
private static final int WEIGHT_SCALE = 1000; private static final int WEIGHT_SCALE = 1000;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final double SLOPE_CEILING = 10.0;
private Placements() { private Placements() {
} }
@@ -147,17 +150,26 @@ public final class Placements {
} }
public static JsonObject air(int x, int y, int z) { public static JsonObject air(int x, int y, int z) {
return blockTag("minecraft:air", x, y, z);
}
public static JsonObject blockTag(String tag, int x, int y, int z) {
JsonObject json = typed("minecraft:matching_block_tag"); JsonObject json = typed("minecraft:matching_block_tag");
json.addProperty("tag", "minecraft:air"); json.addProperty("tag", tag);
return offset(json, x, y, z); return offset(json, x, y, z);
} }
public static JsonObject water(int x, int y, int z) { public static JsonObject water(int x, int y, int z) {
JsonArray fluids = new JsonArray(2); return fluids(List.of("minecraft:water", "minecraft:flowing_water"), x, y, z);
fluids.add("minecraft:water"); }
fluids.add("minecraft:flowing_water");
public static JsonObject fluids(Collection<String> fluids, int x, int y, int z) {
JsonArray list = new JsonArray(fluids.size());
for (String fluid : fluids) {
list.add(fluid);
}
JsonObject json = typed("minecraft:matching_fluids"); JsonObject json = typed("minecraft:matching_fluids");
json.add("fluids", fluids); json.add("fluids", list);
return offset(json, x, y, z); return offset(json, x, y, z);
} }
@@ -215,6 +227,20 @@ public final class Placements {
return anyOf(List.of(directions)); return anyOf(List.of(directions));
} }
public static List<JsonObject> slopeLimits(IrisSlopeClip clip) {
List<JsonObject> limits = new ArrayList<>(2);
if (clip == null || clip.isDefault()) {
return limits;
}
if (clip.getMinimumSlope() > 0) {
limits.add(steeperThan(slopeRise(clip.getMinimumSlope()), -1));
}
if (clip.getMaximumSlope() < SLOPE_CEILING) {
limits.add(not(steeperThan(slopeRise(clip.getMaximumSlope()) + 1, -1)));
}
return limits;
}
public static JsonObject dropAtLeast(int drop) { public static JsonObject dropAtLeast(int drop) {
JsonObject[] directions = new JsonObject[CARDINALS.length]; JsonObject[] directions = new JsonObject[CARDINALS.length];
for (int index = 0; index < CARDINALS.length; index++) { for (int index = 0; index < CARDINALS.length; index++) {
@@ -280,4 +306,8 @@ public final class Placements {
json.addProperty("weight", weight); json.addProperty("weight", weight);
return json; return json;
} }
private static int slopeRise(double slope) {
return Math.max(1, (int) Math.round(slope));
}
} }
@@ -5,10 +5,11 @@ import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.Objects; import java.util.Objects;
public record BakedVariant(String id, TemplateVolume volume, int baseY) { public record BakedVariant(String id, TemplateVolume volume, int baseY, TreeRoots roots) {
public BakedVariant { public BakedVariant {
VanillaResourceIdentifier.requireIdentifier(id, "template id"); VanillaResourceIdentifier.requireIdentifier(id, "template id");
Objects.requireNonNull(volume, "Template volume"); Objects.requireNonNull(volume, "Template volume");
Objects.requireNonNull(roots, "Template roots");
if (baseY > 0) { if (baseY > 0) {
throw new IllegalArgumentException("Template base offset must not be positive, got " + baseY); throw new IllegalArgumentException("Template base offset must not be positive, got " + baseY);
} }
@@ -0,0 +1,124 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.compile.decoration.Placements;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
final class HeightAnchor {
private static final int SCAN_STEPS = 32;
private static final int PROBE_LIMIT = 16;
private HeightAnchor() {
}
static Sampling sampling(ObjectPlaceMode mode) {
return switch (mode) {
case MIN_HEIGHT, FAST_MIN_HEIGHT, MIN_STILT, FAST_MIN_STILT -> Sampling.MIN;
case MAX_HEIGHT, FAST_MAX_HEIGHT, STILT, FAST_STILT, ERODE_STILT -> Sampling.MAX;
default -> Sampling.CENTER;
};
}
static boolean fast(ObjectPlaceMode mode) {
return switch (mode) {
case FAST_MIN_HEIGHT, FAST_MAX_HEIGHT, FAST_STILT, FAST_MIN_STILT -> true;
default -> false;
};
}
static JsonObject scan(Sampling sampling, boolean fast, boolean underwater, TemplateVolume volume, List<Integer> quarters,
TemplateColumn shift) {
boolean lowest = sampling == Sampling.MIN;
List<Box> boxes = new ArrayList<>();
for (int quarter : quarters) {
Box box = Box.of(volume, quarter, shift);
if (fast) {
boxes.addAll(box.fastSamples());
} else {
boxes.add(box);
}
}
JsonObject top = top(underwater);
JsonObject match = lowest ? top : Placements.not(top);
int probeY = lowest ? -1 : 0;
List<JsonObject> targets = new ArrayList<>(boxes.size());
for (Box box : outermost(boxes)) {
targets.add(box.matching(probeY, match.deepCopy()));
}
JsonObject scan = Placements.typed("minecraft:environment_scan");
scan.addProperty("direction_of_search", lowest ? "down" : "up");
scan.addProperty("max_steps", SCAN_STEPS);
scan.add("target_condition", Placements.allOf(targets));
return scan;
}
private static JsonObject top(boolean underwater) {
JsonObject terrain = Placements.allOf(List.of(Placements.solid(0, 0, 0),
Placements.not(Placements.blockTag("minecraft:leaves", 0, 0, 0)),
Placements.not(Placements.blockTag("minecraft:logs", 0, 0, 0))));
return underwater ? terrain : Placements.anyOf(List.of(terrain, Placements.fluids(PlacementCompiler.FLUIDS, 0, 0, 0)));
}
private static List<Box> outermost(List<Box> boxes) {
List<Box> kept = new ArrayList<>(boxes.size());
for (int index = 0; index < boxes.size(); index++) {
Box box = boxes.get(index);
boolean covered = false;
for (int other = 0; other < boxes.size() && !covered; other++) {
Box candidate = boxes.get(other);
covered = other != index && candidate.contains(box) && (!box.contains(candidate) || other < index);
}
if (!covered) {
kept.add(box);
}
}
return kept;
}
private record Box(int minX, int maxX, int minZ, int maxZ) {
static Box of(TemplateVolume volume, int quarter, TemplateColumn shift) {
TemplateColumn first = TemplateColumn.of(volume, 0, 0, quarter);
TemplateColumn last = TemplateColumn.of(volume, volume.sizeX() - 1, volume.sizeZ() - 1, quarter);
return new Box(clamp(Math.min(first.x(), last.x()) + shift.x()), clamp(Math.max(first.x(), last.x()) + shift.x()),
clamp(Math.min(first.z(), last.z()) + shift.z()), clamp(Math.max(first.z(), last.z()) + shift.z()));
}
List<Box> fastSamples() {
int stepX = (maxX - minX) / 2 + 1;
int stepZ = (maxZ - minZ) / 2 + 1;
List<Box> samples = new ArrayList<>(4);
for (int x = minX; x <= maxX; x += stepX) {
for (int z = minZ; z <= maxZ; z += stepZ) {
samples.add(new Box(x, x, z, z));
}
}
return samples;
}
boolean contains(Box other) {
return minX <= other.minX && maxX >= other.maxX && minZ <= other.minZ && maxZ >= other.maxZ;
}
JsonObject matching(int y, JsonObject match) {
JsonObject volume = Placements.typed("minecraft:volume_match");
volume.add("min", FeatureJson.vector(minX, y, minZ));
volume.add("max", FeatureJson.vector(maxX, y, maxZ));
volume.add("match", match);
return volume;
}
private static int clamp(int value) {
return Math.max(-PROBE_LIMIT, Math.min(PROBE_LIMIT, value));
}
}
enum Sampling {
CENTER,
MIN,
MAX
}
}
@@ -0,0 +1,88 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.pack.datapack.export.StructureTemplateEncoder;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.util.math.Vector3i;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.ToIntFunction;
public record ObjectSource(TemplateVolume volume, int baseY, TreeRoots roots) {
public ObjectSource {
Objects.requireNonNull(volume, "Object volume");
Objects.requireNonNull(roots, "Object roots");
if (baseY > 0) {
throw new IllegalArgumentException("Object base offset must not be positive, got " + baseY);
}
}
public static ObjectSource of(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
List<TemplateBlock> blocks = StructureTemplateEncoder.blocks(object, resource, diagnostics);
Vector3i center = object.getCenter();
Axis x = Axis.of(blocks, TemplateBlock::x, object.getW(), center.getX());
Axis z = Axis.of(blocks, TemplateBlock::z, object.getD(), center.getZ());
int lowestY = 0;
int highestY = object.getH() - 1;
for (TemplateBlock block : blocks) {
lowestY = Math.min(lowestY, block.y());
highestY = Math.max(highestY, block.y());
}
List<TemplateBlock> placed = new ArrayList<>(blocks.size());
Map<Long, TemplateBlock> byPosition = new HashMap<>(blocks.size() * 2);
for (TemplateBlock block : blocks) {
TemplateBlock shifted = new TemplateBlock(x.shift(block.x()), block.y() - lowestY, z.shift(block.z()),
block.state().waterlogged(false), block.nbt());
placed.add(shifted);
byPosition.put(position(shifted.x(), shifted.y(), shifted.z()), shifted);
}
TreeRoots roots = roots(object.getTreeGrounding(), center, x, z, lowestY, byPosition);
return new ObjectSource(new TemplateVolume(x.size(), highestY - lowestY + 1, z.size(), placed), lowestY, roots);
}
private static TreeRoots roots(TreeGrounding grounding, Vector3i center, Axis x, Axis z, int lowestY, Map<Long, TemplateBlock> blocks) {
if (grounding == null) {
return TreeRoots.NONE;
}
List<TemplateBlock> cells = new ArrayList<>(grounding.roots().size());
for (TreeGrounding.Root root : grounding.roots()) {
TemplateBlock block = blocks.get(position(x.shift(root.x() + center.getX()), root.y() + center.getY() - lowestY,
z.shift(root.z() + center.getZ())));
if (block != null) {
cells.add(block);
}
}
return new TreeRoots(cells, grounding.maxExtension());
}
private static long position(int x, int y, int z) {
return ((long) (x & 0x1FFFFF) << 42) | ((long) (y & 0x1FFFFF) << 21) | (z & 0x1FFFFF);
}
private record Axis(int size, int center, int nominalCenter) {
static Axis of(List<TemplateBlock> blocks, ToIntFunction<TemplateBlock> coordinate, int size, int center) {
int low = 0;
int high = size - 1;
for (TemplateBlock block : blocks) {
low = Math.min(low, coordinate.applyAsInt(block));
high = Math.max(high, coordinate.applyAsInt(block));
}
if (low == 0 && high == size - 1) {
return new Axis(size, center, center);
}
int reach = Math.max(center - low, high - center);
return new Axis(reach * 2 + 1, reach, center);
}
int shift(int value) {
return value - nominalCenter + center;
}
}
}
@@ -2,12 +2,14 @@ package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.decoration.tree.IrisProceduralTree;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.DataVersion; import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.StructureTemplateEncoder; import art.arcane.iris.pack.datapack.export.StructureTemplateEncoder;
import art.arcane.iris.pack.datapack.export.TemplateBlock; import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState; import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier; import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.CompileStage; import art.arcane.iris.pack.datapack.export.compile.CompileStage;
@@ -21,6 +23,7 @@ import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement; import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@@ -56,6 +59,7 @@ public final class ObjectStage implements CompileStage {
private static final class Run { private static final class Run {
private static final int MAX_STILT_BASES = 16; private static final int MAX_STILT_BASES = 16;
private static final String TREE_FOLDER = "trees/"; private static final String TREE_FOLDER = "trees/";
private static final String PROCEDURAL_FOLDER = "procedural/";
private static final String CAVE_SUFFIX = "_cave"; private static final String CAVE_SUFFIX = "_cave";
private final PackCompileContext context; private final PackCompileContext context;
@@ -70,7 +74,8 @@ public final class ObjectStage implements CompileStage {
private final LootCompiler loot; private final LootCompiler loot;
private final WideObjectStructures wide; private final WideObjectStructures wide;
private final StructureTemplateEncoder encoder = new StructureTemplateEncoder(); private final StructureTemplateEncoder encoder = new StructureTemplateEncoder();
private final Map<String, TemplateVolume> volumes = new HashMap<>(); private final Map<String, ObjectSource> sources = new HashMap<>();
private final Map<IrisProceduralPlacement, IrisObjectPlacement> proceduralPlacements = new IdentityHashMap<>();
private final Map<String, BakedVariant> baked = new HashMap<>(); private final Map<String, BakedVariant> baked = new HashMap<>();
private final Map<IrisObjectPlacement, VariantPlanner.Plan> plans = new IdentityHashMap<>(); private final Map<IrisObjectPlacement, VariantPlanner.Plan> plans = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, List<ObjectTemplates>> templates = new IdentityHashMap<>(); private final Map<IrisObjectPlacement, List<ObjectTemplates>> templates = new IdentityHashMap<>();
@@ -114,8 +119,10 @@ public final class ObjectStage implements CompileStage {
IrisRegion region = regions.get(entry.getValue()); IrisRegion region = regions.get(entry.getValue());
LootCompiler.LootScope scope = new LootCompiler.LootScope(dimension, region, biome); LootCompiler.LootScope scope = new LootCompiler.LootScope(dimension, region, biome);
List<IrisObjectPlacement> placements = new ArrayList<>(biome.getObjects()); List<IrisObjectPlacement> placements = new ArrayList<>(biome.getObjects());
placements.addAll(procedural(biome.getProceduralObjects()));
if (region != null) { if (region != null) {
placements.addAll(region.getObjects()); placements.addAll(region.getObjects());
placements.addAll(procedural(region.getProceduralObjects()));
} }
for (IrisObjectPlacement placement : placements) { for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace().isEmpty()) { if (placement == null || placement.getPlace().isEmpty()) {
@@ -136,6 +143,32 @@ public final class ObjectStage implements CompileStage {
emit(); emit();
} }
private List<IrisObjectPlacement> procedural(IrisProceduralObjects objects) {
if (objects == null || objects.isEmpty()) {
return List.of();
}
List<IrisObjectPlacement> placements = new ArrayList<>();
for (IrisProceduralPlacement placement : objects.getAllPlacements()) {
placements.add(proceduralPlacements.computeIfAbsent(placement, this::procedural));
}
return placements;
}
private IrisObjectPlacement procedural(IrisProceduralPlacement source) {
IrisObjectPlacement placement = source.asPlacement();
String folder = (source instanceof IrisProceduralTree ? TREE_FOLDER : "") + PROCEDURAL_FOLDER + source.getName() + "-";
KList<String> keys = new KList<>();
for (IrisObject variant : source.getVariantObjects(data)) {
ObjectSource object = ObjectSource.of(variant, variant.getLoadKey(), diagnostics);
String key = folder + ContentIds.hash(object.volume() + ";base=" + object.baseY()
+ (object.roots().present() ? ";roots=" + object.roots() : ""));
sources.putIfAbsent(key, object);
keys.add(key);
}
placement.setPlace(keys);
return placement;
}
private void place(String biomeId, IrisObjectPlacement placement, PlacementInput.Anchor anchor, LootCompiler.LootScope scope) { private void place(String biomeId, IrisObjectPlacement placement, PlacementInput.Anchor anchor, LootCompiler.LootScope scope) {
String name = name(placement, anchor); String name = name(placement, anchor);
VariantPlanner.Plan plan = plans.computeIfAbsent(placement, ignored -> planner.plan(placement, name, diagnostics)); VariantPlanner.Plan plan = plans.computeIfAbsent(placement, ignored -> planner.plan(placement, name, diagnostics));
@@ -191,8 +224,8 @@ public final class ObjectStage implements CompileStage {
List<List<TemplateChoice>> choices = new ArrayList<>(); List<List<TemplateChoice>> choices = new ArrayList<>();
List<BakedVariant> all = new ArrayList<>(); List<BakedVariant> all = new ArrayList<>();
for (String objectKey : placement.getPlace()) { for (String objectKey : placement.getPlace()) {
TemplateVolume volume = volume(objectKey); ObjectSource source = source(objectKey);
if (volume == null) { if (source == null) {
continue; continue;
} }
String objectPath = VanillaResourceIdentifier.sanitizePath(objectKey); String objectPath = VanillaResourceIdentifier.sanitizePath(objectKey);
@@ -200,7 +233,7 @@ public final class ObjectStage implements CompileStage {
for (VariantPlanner.PlannedVariant planned : plan.variants()) { for (VariantPlanner.PlannedVariant planned : plan.variants()) {
String id = ObjectVariantBaker.templateId(namespace, objectPath, planned.spec(), target); String id = ObjectVariantBaker.templateId(namespace, objectPath, planned.spec(), target);
BakedVariant variant = baked.computeIfAbsent(id, BakedVariant variant = baked.computeIfAbsent(id,
ignored -> baker.bake(id, volume, planned.spec(), "objects/" + objectKey, diagnostics)); ignored -> baker.bake(id, source, planned.spec()));
objectChoices.add(new TemplateChoice(variant, planned.quarters(), planned.weight())); objectChoices.add(new TemplateChoice(variant, planned.quarters(), planned.weight()));
all.add(variant); all.add(variant);
} }
@@ -223,14 +256,14 @@ public final class ObjectStage implements CompileStage {
return objects; return objects;
} }
private TemplateVolume volume(String objectKey) { private ObjectSource source(String objectKey) {
if (volumes.containsKey(objectKey)) { if (sources.containsKey(objectKey)) {
return volumes.get(objectKey); return sources.get(objectKey);
} }
IrisObject object = data.getObjectLoader().load(objectKey, false); IrisObject object = data.getObjectLoader().load(objectKey, false);
TemplateVolume volume = object == null ? null : StructureTemplateEncoder.volume(object, "objects/" + objectKey, diagnostics); ObjectSource source = object == null ? null : ObjectSource.of(object, "objects/" + objectKey, diagnostics);
volumes.put(objectKey, volume); sources.put(objectKey, source);
return volume; return source;
} }
private Map<VanillaBlockState, String> bindings(IrisObjectPlacement placement, List<ObjectTemplates> objects, LootCompiler.LootScope scope) { private Map<VanillaBlockState, String> bindings(IrisObjectPlacement placement, List<ObjectTemplates> objects, LootCompiler.LootScope scope) {
@@ -6,11 +6,11 @@ import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState; import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier; import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WeightedState; import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.volmlib.util.nbt.tag.CompoundTag; import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -19,6 +19,11 @@ import java.util.TreeMap;
public final class ObjectVariantBaker { public final class ObjectVariantBaker {
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air"); private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
private static final String TALL_SEAGRASS = "minecraft:tall_seagrass";
private static final Set<String> SEA_PLANTS = Set.of("minecraft:sea_pickle", "minecraft:seagrass", TALL_SEAGRASS);
private static final String CORAL = "_coral";
private static final String CORAL_FAN = "_coral_fan";
private static final String WALL_FAN = "_coral_wall_fan";
private static final String SNOW = "minecraft:snow"; private static final String SNOW = "minecraft:snow";
private static final int SNOW_LAYER_STEPS = 7; private static final int SNOW_LAYER_STEPS = 7;
private static final int WARP_SPACING_X = 4099; private static final int WARP_SPACING_X = 4099;
@@ -36,15 +41,18 @@ public final class ObjectVariantBaker {
return namespace + ":object/" + objectPath + "/" + ContentIds.hash(spec.descriptor() + ";target=" + target.name()); return namespace + ":object/" + objectPath + "/" + ContentIds.hash(spec.descriptor() + ";target=" + target.name());
} }
public BakedVariant bake(String templateId, TemplateVolume source, VariantSpec spec, String resource, List<ExportDiagnostic> diagnostics) { public BakedVariant bake(String templateId, ObjectSource source, VariantSpec spec) {
long seed = ContentIds.seed(spec.descriptor()); long seed = ContentIds.seed(spec.descriptor());
List<Voxel> voxels = prepare(source, spec, seed); TemplateVolume template = source.volume();
List<Voxel> voxels = prepare(template, spec, seed, rootCells(source.roots()));
if (!spec.reshapes()) { if (!spec.reshapes()) {
return new BakedVariant(templateId, volume(source.sizeX(), source.sizeY(), source.sizeZ(), voxels), 0); List<Voxel> kept = supported(voxels);
return new BakedVariant(templateId, volume(template.sizeX(), template.sizeY(), template.sizeZ(), kept), source.baseY(),
roots(kept, source.roots().reach()));
} }
int sizeX = source.sizeX(); int sizeX = template.sizeX();
int sizeY = source.sizeY(); int sizeY = template.sizeY();
int sizeZ = source.sizeZ(); int sizeZ = template.sizeZ();
if (spec.scale() != 1) { if (spec.scale() != 1) {
sizeX = scaledSize(sizeX, spec.scale()); sizeX = scaledSize(sizeX, spec.scale());
sizeY = scaledSize(sizeY, spec.scale()); sizeY = scaledSize(sizeY, spec.scale());
@@ -58,7 +66,7 @@ public final class ObjectVariantBaker {
if (spec.snow() > 0) { if (spec.snow() > 0) {
voxels = snow(voxels, spec.snow(), seed); voxels = snow(voxels, spec.snow(), seed);
} }
return reshaped(templateId, voxels, resource, diagnostics); return reshaped(templateId, supported(voxels), source.baseY(), (int) Math.ceil(source.roots().reach() * spec.scale()));
} }
public static List<BakedVariant> alignBases(List<BakedVariant> variants) { public static List<BakedVariant> alignBases(List<BakedVariant> variants) {
@@ -79,12 +87,12 @@ public final class ObjectVariantBaker {
blocks.add(new TemplateBlock(block.x(), block.y() + lift, block.z(), block.state(), block.nbt())); blocks.add(new TemplateBlock(block.x(), block.y() + lift, block.z(), block.state(), block.nbt()));
} }
aligned.add(new BakedVariant(variant.id() + "_lift" + lift, aligned.add(new BakedVariant(variant.id() + "_lift" + lift,
new TemplateVolume(volume.sizeX(), volume.sizeY() + lift, volume.sizeZ(), blocks), lowest)); new TemplateVolume(volume.sizeX(), volume.sizeY() + lift, volume.sizeZ(), blocks), lowest, variant.roots().lift(lift)));
} }
return aligned; return aligned;
} }
private List<Voxel> prepare(TemplateVolume source, VariantSpec spec, long seed) { private List<Voxel> prepare(TemplateVolume source, VariantSpec spec, long seed, Set<Long> roots) {
List<Voxel> voxels = new ArrayList<>(source.blocks().size()); List<Voxel> voxels = new ArrayList<>(source.blocks().size());
for (TemplateBlock block : source.blocks()) { for (TemplateBlock block : source.blocks()) {
if (AIR.contains(block.state().name())) { if (AIR.contains(block.state().name())) {
@@ -114,11 +122,29 @@ public final class ObjectVariantBaker {
if (spec.preventLeafDecay()) { if (spec.preventLeafDecay()) {
state = persistentLeaves(state); state = persistentLeaves(state);
} }
voxels.add(new Voxel(block.x(), block.y(), block.z(), state, nbt)); voxels.add(new Voxel(block.x(), block.y(), block.z(), state, nbt, roots.contains(key(block.x(), block.y(), block.z()))));
} }
return voxels; return voxels;
} }
private static Set<Long> rootCells(TreeRoots roots) {
Set<Long> cells = new HashSet<>(roots.cells().size() * 2);
for (TemplateBlock cell : roots.cells()) {
cells.add(key(cell.x(), cell.y(), cell.z()));
}
return cells;
}
private static TreeRoots roots(List<Voxel> voxels, int reach) {
List<TemplateBlock> cells = new ArrayList<>();
for (Voxel voxel : voxels) {
if (voxel.root()) {
cells.add(new TemplateBlock(voxel.x(), voxel.y(), voxel.z(), voxel.state(), null));
}
}
return cells.isEmpty() ? TreeRoots.NONE : new TreeRoots(cells, reach);
}
private static VanillaBlockState pick(ObjectEdit edit, double roll) { private static VanillaBlockState pick(ObjectEdit edit, double roll) {
double cursor = roll * edit.totalWeight(); double cursor = roll * edit.totalWeight();
for (WeightedState state : edit.replace()) { for (WeightedState state : edit.replace()) {
@@ -146,6 +172,51 @@ public final class ObjectVariantBaker {
return new VanillaBlockState(state.name(), persistent); return new VanillaBlockState(state.name(), persistent);
} }
private static List<Voxel> supported(List<Voxel> voxels) {
Map<Long, Voxel> occupied = new LinkedHashMap<>();
int lowest = Integer.MAX_VALUE;
for (Voxel voxel : voxels) {
occupied.put(key(voxel.x(), voxel.y(), voxel.z()), voxel);
lowest = Math.min(lowest, voxel.y());
}
List<Voxel> kept = new ArrayList<>(voxels.size());
for (Voxel voxel : voxels) {
if (anchored(voxel, occupied, lowest)) {
kept.add(voxel);
}
}
return kept;
}
private static boolean anchored(Voxel voxel, Map<Long, Voxel> occupied, int lowest) {
String name = voxel.state().name();
if (name.endsWith(WALL_FAN)) {
int[] behind = switch (voxel.state().properties().getOrDefault("facing", "north")) {
case "south" -> new int[]{0, -1};
case "east" -> new int[]{-1, 0};
case "west" -> new int[]{1, 0};
default -> new int[]{0, 1};
};
return support(occupied.get(key(voxel.x() + behind[0], voxel.y(), voxel.z() + behind[1])));
}
if (!SEA_PLANTS.contains(name) && !name.endsWith(CORAL) && !name.endsWith(CORAL_FAN)) {
return true;
}
Voxel below = occupied.get(key(voxel.x(), voxel.y() - 1, voxel.z()));
if (name.equals(TALL_SEAGRASS) && "upper".equals(voxel.state().properties().get("half"))) {
return below != null && below.state().name().equals(TALL_SEAGRASS);
}
return voxel.y() == lowest || support(below);
}
private static boolean support(Voxel voxel) {
if (voxel == null) {
return false;
}
String name = voxel.state().name();
return !SEA_PLANTS.contains(name) && !name.endsWith(CORAL) && !name.endsWith(CORAL_FAN) && !name.endsWith(WALL_FAN);
}
private static int scaledSize(int size, double scale) { private static int scaledSize(int size, double scale) {
return Math.max(1, (int) Math.ceil(size * scale)); return Math.max(1, (int) Math.ceil(size * scale));
} }
@@ -164,7 +235,7 @@ public final class ObjectVariantBaker {
for (int y = minY; y <= maxY; y++) { for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) { for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) { for (int z = minZ; z <= maxZ; z++) {
scaled.put(key(x, y, z), new Voxel(x, y, z, voxel.state(), voxel.nbt())); scaled.put(key(x, y, z), new Voxel(x, y, z, voxel.state(), voxel.nbt(), voxel.root()));
} }
} }
} }
@@ -184,7 +255,7 @@ public final class ObjectVariantBaker {
int x = (int) Math.floor(rotated[0]); int x = (int) Math.floor(rotated[0]);
int y = (int) Math.floor(rotated[1]) + rotatedCenterY; int y = (int) Math.floor(rotated[1]) + rotatedCenterY;
int z = (int) Math.floor(rotated[2]); int z = (int) Math.floor(rotated[2]);
anchored.put(key(x, y, z), new Voxel(x, y, z, states.apply(voxel.state()), voxel.nbt())); anchored.put(key(x, y, z), new Voxel(x, y, z, states.apply(voxel.state()), voxel.nbt(), voxel.root()));
} }
return new ArrayList<>(anchored.values()); return new ArrayList<>(anchored.values());
} }
@@ -241,7 +312,7 @@ public final class ObjectVariantBaker {
double worldZ = voxel.z() + originZ; double worldZ = voxel.z() + originZ;
int x = (int) (voxel.x() + warp.field().offset(worldX, voxel.y(), worldZ)); int x = (int) (voxel.x() + warp.field().offset(worldX, voxel.y(), worldZ));
int z = (int) (voxel.z() + warp.field().offset(worldZ, voxel.y(), worldX)); int z = (int) (voxel.z() + warp.field().offset(worldZ, voxel.y(), worldX));
warped.put(key(x, voxel.y(), z), new Voxel(x, voxel.y(), z, voxel.state(), voxel.nbt())); warped.put(key(x, voxel.y(), z), new Voxel(x, voxel.y(), z, voxel.state(), voxel.nbt(), voxel.root()));
} }
return new ArrayList<>(warped.values()); return new ArrayList<>(warped.values());
} }
@@ -266,14 +337,14 @@ public final class ObjectVariantBaker {
int index = steps <= 0 ? 0 : (int) Math.floor(unit(seed, top.x(), top.y(), top.z(), 0, SNOW_SALT) * steps); int index = steps <= 0 ? 0 : (int) Math.floor(unit(seed, top.x(), top.y(), top.z(), 0, SNOW_SALT) * steps);
int layers = Math.max(0, Math.min(index, SNOW_LAYER_STEPS)) + 1; int layers = Math.max(0, Math.min(index, SNOW_LAYER_STEPS)) + 1;
covered.add(new Voxel(top.x(), top.y() + 1, top.z(), covered.add(new Voxel(top.x(), top.y() + 1, top.z(),
new VanillaBlockState(SNOW, Map.of("layers", Integer.toString(layers))), null)); new VanillaBlockState(SNOW, Map.of("layers", Integer.toString(layers))), null, false));
} }
return covered; return covered;
} }
private BakedVariant reshaped(String templateId, List<Voxel> voxels, String resource, List<ExportDiagnostic> diagnostics) { private BakedVariant reshaped(String templateId, List<Voxel> voxels, int baseY, int reach) {
if (voxels.isEmpty()) { if (voxels.isEmpty()) {
return new BakedVariant(templateId, new TemplateVolume(1, 1, 1, List.of()), 0); return new BakedVariant(templateId, new TemplateVolume(1, 1, 1, List.of()), 0, TreeRoots.NONE);
} }
int extentX = 0; int extentX = 0;
int extentZ = 0; int extentZ = 0;
@@ -287,10 +358,10 @@ public final class ObjectVariantBaker {
} }
List<Voxel> local = new ArrayList<>(voxels.size()); List<Voxel> local = new ArrayList<>(voxels.size());
for (Voxel voxel : voxels) { for (Voxel voxel : voxels) {
local.add(new Voxel(voxel.x() + extentX, voxel.y() - minY, voxel.z() + extentZ, voxel.state(), voxel.nbt())); local.add(new Voxel(voxel.x() + extentX, voxel.y() - minY, voxel.z() + extentZ, voxel.state(), voxel.nbt(), voxel.root()));
} }
TemplateVolume volume = volume(extentX * 2 + 1, maxY - minY + 1, extentZ * 2 + 1, local); TemplateVolume volume = volume(extentX * 2 + 1, maxY - minY + 1, extentZ * 2 + 1, local);
return new BakedVariant(templateId, volume, minY); return new BakedVariant(templateId, volume, minY + baseY, roots(local, reach));
} }
private static TemplateVolume volume(int sizeX, int sizeY, int sizeZ, List<Voxel> voxels) { private static TemplateVolume volume(int sizeX, int sizeY, int sizeZ, List<Voxel> voxels) {
@@ -323,6 +394,6 @@ public final class ObjectVariantBaker {
return mixed ^ (mixed >>> 31); return mixed ^ (mixed >>> 31);
} }
private record Voxel(int x, int y, int z, VanillaBlockState state, CompoundTag nbt) { private record Voxel(int x, int y, int z, VanillaBlockState state, CompoundTag nbt, boolean root) {
} }
} }
@@ -1,5 +1,7 @@
package art.arcane.iris.pack.datapack.export.compile.object; package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState; import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier; import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WeightedState; import art.arcane.iris.pack.datapack.export.compile.WeightedState;
@@ -18,20 +20,26 @@ import com.google.gson.JsonObject;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
public final class PlacementCompiler { public final class PlacementCompiler {
public static final int MAX_FEATURE_WIDTH = 32; public static final int MAX_FEATURE_WIDTH = 32;
private static final int TILE_WIDTH = 17; private static final int TILE_WIDTH = 17;
private static final int WINDOW_REACH = 16; private static final int WINDOW_REACH = 16;
private static final int MAX_SUPPORT_DROP = 16;
private static final int CAVE_SCAN_STEPS = 32; private static final int CAVE_SCAN_STEPS = 32;
private static final int CAVE_FLOOR_MARGIN = 8; private static final int CAVE_FLOOR_MARGIN = 8;
private static final int MIN_ANCHOR = -2032; private static final int MIN_ANCHOR = -2032;
private static final int MAX_ANCHOR = 2031; private static final int MAX_ANCHOR = 2031;
private static final int DEFAULT_FLUID_HEIGHT = 63;
private static final long WEIGHT_CEILING = 1_000_000L; private static final long WEIGHT_CEILING = 1_000_000L;
private static final long ROTATION_SHARES = 12L;
private static final List<String> QUARTER_NAMES = List.of("none", "counterclockwise_90", "180", "clockwise_90"); private static final List<String> QUARTER_NAMES = List.of("none", "counterclockwise_90", "180", "clockwise_90");
private static final List<String> FLUIDS = List.of("minecraft:water", "minecraft:flowing_water", "minecraft:lava", "minecraft:flowing_lava"); static final List<String> FLUIDS = List.of("minecraft:water", "minecraft:flowing_water", "minecraft:lava", "minecraft:flowing_lava");
private static final Set<ObjectPlaceMode> STILT_MODES = Set.of(ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT, private static final Set<ObjectPlaceMode> STILT_MODES = Set.of(ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT,
ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT); ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT);
@@ -42,12 +50,14 @@ public final class PlacementCompiler {
private final FeatureJson json; private final FeatureJson json;
private final DimensionDef dimension; private final DimensionDef dimension;
private final WideObjectStructures wide; private final WideObjectStructures wide;
private final RootExtension roots;
public PlacementCompiler(String namespace, ExportCapabilities capabilities, DimensionDef dimension) { public PlacementCompiler(String namespace, ExportCapabilities capabilities, DimensionDef dimension) {
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "placement namespace"); this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "placement namespace");
json = new FeatureJson(capabilities); json = new FeatureJson(capabilities);
this.dimension = dimension; this.dimension = dimension;
wide = new WideObjectStructures(namespace); wide = new WideObjectStructures(namespace);
roots = new RootExtension(this.namespace, json);
} }
public static boolean stilted(ObjectPlaceMode mode) { public static boolean stilted(ObjectPlaceMode mode) {
@@ -83,8 +93,12 @@ public final class PlacementCompiler {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.OBJECT_VACUUM, input.name(), diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.OBJECT_VACUUM, input.name(),
"Vacuum terrain shaping is not exported; the object places at its center height")); "Vacuum terrain shaping is not exported; the object places at its center height"));
} }
if (!narrow.isEmpty()) { for (List<ObjectTemplates> group : depthGroups(input, narrow)) {
compileNarrow(input, narrow, chance, density, (double) narrow.size() / total, resources, placed); compileNarrow(input, group, chance, density, (double) group.size() / total, resources, placed);
}
if (!narrow.isEmpty() && slopeLimited(input)) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.SLOPE, input.name(),
"Object slope conditions measure slope over 3 blocks in the four cardinal directions"));
} }
if (!wideObjects.isEmpty()) { if (!wideObjects.isEmpty()) {
List<TemplateChoice> templates = weighted(wideObjects); List<TemplateChoice> templates = weighted(wideObjects);
@@ -108,22 +122,32 @@ public final class PlacementCompiler {
if (paint) { if (paint) {
processors.add(gravity(placement)); processors.add(gravity(placement));
} }
JsonObject feature = template(templates, processors); HeightAnchor.Sampling sampling = input.anchor() == PlacementInput.Anchor.SURFACE && !paint
if (stilted(placement.getMode())) { && placement.getMode() != ObjectPlaceMode.FLOATING ? HeightAnchor.sampling(placement.getMode()) : HeightAnchor.Sampling.CENTER;
feature = stilts(feature, templates, input.stiltBases()); boolean grounded = grounds(input) && rooted(templates);
boolean branched = grounded || sampling != HeightAnchor.Sampling.CENTER;
JsonObject feature;
if (branched) {
feature = branches(input, templates, processors, sampling, grounded, resources);
} else {
feature = template(templates, processors);
if (stilted(placement.getMode())) {
feature = stilts(feature, templates, input.stiltBases());
}
} }
String name = input.name(); String name = input.name();
String featurePath = "object/" + name + "_" + ContentIds.hash(feature); String featurePath = "object/" + name + "_" + ContentIds.hash(feature);
resources.add(new JsonResource("data/" + namespace + "/" + json.syntax().featureFolder() + "/" + featurePath + ".json", feature)); resources.add(new JsonResource("data/" + namespace + "/" + json.syntax().featureFolder() + "/" + featurePath + ".json", feature));
JsonObject placedFeature = new JsonObject(); JsonObject placedFeature = new JsonObject();
placedFeature.addProperty("feature", namespace + ":" + featurePath); placedFeature.addProperty("feature", namespace + ":" + featurePath);
placedFeature.add("placement", modifiers(input, templates, chance, density, share, paint)); placedFeature.add("placement", modifiers(input, templates, chance, density, share, paint, branched));
String placedPath = "object/" + name + "_" + ContentIds.hash(placedFeature); String placedPath = "object/" + name + "_" + ContentIds.hash(placedFeature);
resources.add(new JsonResource("data/" + namespace + "/worldgen/placed_feature/" + placedPath + ".json", placedFeature)); resources.add(new JsonResource("data/" + namespace + "/worldgen/placed_feature/" + placedPath + ".json", placedFeature));
placed.add(namespace + ":" + placedPath); placed.add(namespace + ":" + placedPath);
} }
private JsonArray modifiers(PlacementInput input, List<TemplateChoice> templates, double chance, int density, double share, boolean paint) { private JsonArray modifiers(PlacementInput input, List<TemplateChoice> templates, double chance, int density, double share, boolean paint,
boolean branched) {
IrisObjectPlacement placement = input.placement(); IrisObjectPlacement placement = input.placement();
int translateX = placement.getTranslate().getX(); int translateX = placement.getTranslate().getX();
int translateY = placement.getTranslate().getY(); int translateY = placement.getTranslate().getY();
@@ -143,18 +167,32 @@ public final class PlacementCompiler {
} else if (placement.getMode() == ObjectPlaceMode.FLOATING) { } else if (placement.getMode() == ObjectPlaceMode.FLOATING) {
modifiers.add(fixedHeight(translateY + base + spreadLow, translateY + base + spreadHigh)); modifiers.add(fixedHeight(translateY + base + spreadLow, translateY + base + spreadHigh));
} else { } else {
modifiers.add(Placements.heightmap(placement.isUnderwater() ? "OCEAN_FLOOR_WG" : "WORLD_SURFACE_WG")); boolean underwater = placement.isUnderwater();
if (!placement.isForcePlace() && placement.isRequireSurfaceSupport()) { boolean onwater = onwater(placement);
modifiers.add(Placements.filter(support(templates, placement))); modifiers.add(Placements.heightmap(onwater ? Placements.WORLD_SURFACE : Placements.OCEAN_FLOOR));
modifiers.add(Placements.filter(onwater ? Placements.water(0, -1, 0) : ground(underwater)));
if (underwater) {
modifiers.add(Placements.filter(belowSurface(fluidCeiling(templates, translateY))));
}
if (!underwater && !onwater && !placement.isForcePlace() && placement.isRequireSurfaceSupport() && !stilted(placement.getMode())) {
JsonObject support = support(templates, placement);
if (support != null) {
modifiers.add(Placements.filter(support));
}
}
if (slopeLimited(input)) {
for (JsonObject limit : Placements.slopeLimits(placement.getSlopeCondition())) {
modifiers.add(Placements.filter(limit));
}
} }
if (paint) { if (paint) {
modifiers.addAll(json.offset(translateX, 0, 0, translateZ)); modifiers.addAll(json.offset(translateX, 0, 0, translateZ));
} else { } else if (!branched) {
modifiers.addAll(json.offset(translateX, translateY - 1 + base + spreadLow, translateY - 1 + base + spreadHigh, translateZ)); modifiers.addAll(surfaceOffset(placement, base));
} }
} }
JsonObject clamp = clamp(placement.getClamp()); JsonObject clamp = clamp(placement.getClamp());
if (clamp != null) { if (clamp != null && !branched) {
modifiers.add(Placements.filter(clamp)); modifiers.add(Placements.filter(clamp));
} }
modifiers.add(Placements.typed("minecraft:biome")); modifiers.add(Placements.typed("minecraft:biome"));
@@ -165,6 +203,128 @@ public final class PlacementCompiler {
return array; return array;
} }
private List<JsonObject> surfaceOffset(IrisObjectPlacement placement, int base) {
int yRandom = placement.getTranslate().getYRandom();
int spreadLow = yRandom < 0 ? yRandom : 0;
int spreadHigh = yRandom > 0 ? yRandom - 1 : yRandom < 0 ? -1 : 0;
int lift = placement.getTranslate().getY() - 1 + base;
return json.offset(placement.getTranslate().getX(), lift + spreadLow, lift + spreadHigh, placement.getTranslate().getZ());
}
private JsonObject branches(PlacementInput input, List<TemplateChoice> templates, List<JsonObject> processors,
HeightAnchor.Sampling sampling, boolean grounded, List<JsonResource> resources) {
IrisObjectPlacement placement = input.placement();
Map<String, JsonResource> shared = new TreeMap<>();
Branching branching = new Branching(input, processors, sampling, grounded, surfaceOffset(placement, lowestBase(templates)),
clamp(placement.getClamp()), shared);
List<JsonObject> branches = new ArrayList<>();
List<Long> weights = new ArrayList<>();
for (TemplateChoice choice : templates) {
long weight = grounded ? choice.weight() * (ROTATION_SHARES / choice.quarters().size()) : choice.weight();
List<List<Integer>> groups = new ArrayList<>(choice.quarters().size());
if (grounded) {
for (int quarter : choice.quarters()) {
groups.add(List.of(quarter));
}
} else {
groups.add(choice.quarters());
}
for (List<Integer> quarters : groups) {
branches.add(branch(branching, choice.variant(), quarters));
weights.add(weight);
}
}
resources.addAll(shared.values());
long divisor = 0;
for (long weight : weights) {
divisor = BigInteger.valueOf(divisor).gcd(BigInteger.valueOf(weight)).longValue();
}
JsonArray entries = new JsonArray(branches.size());
for (int index = 0; index < branches.size(); index++) {
JsonObject entry = new JsonObject();
entry.add("data", branches.get(index));
entry.addProperty("weight", weights.get(index) / divisor);
entries.add(entry);
}
JsonObject fields = new JsonObject();
fields.add("features", entries);
return json.syntax().feature("minecraft:weighted_random_selector", fields);
}
private JsonObject branch(Branching branching, BakedVariant variant, List<Integer> quarters) {
IrisObjectPlacement placement = branching.input().placement();
TemplateChoice single = new TemplateChoice(variant, quarters, 1);
JsonObject feature = template(List.of(single), branching.processors());
List<JsonObject> modifiers = new ArrayList<>();
if (branching.sampling() != HeightAnchor.Sampling.CENTER) {
modifiers.add(HeightAnchor.scan(branching.sampling(), HeightAnchor.fast(placement.getMode()), placement.isUnderwater(),
variant.volume(), quarters, new TemplateColumn(placement.getTranslate().getX(), placement.getTranslate().getZ())));
}
modifiers.addAll(branching.offset());
if (branching.grounded() && variant.roots().present()) {
int quarter = quarters.getFirst();
feature = overlay(feature, roots.pillars(variant.roots(), variant.volume(), quarter, branching.shared()));
JsonObject gap = RootExtension.gap(variant.roots(), variant.volume(), quarter);
if (gap != null) {
modifiers.add(Placements.filter(gap));
}
} else if (stilted(placement.getMode())) {
feature = stilts(feature, List.of(single), branching.input().stiltBases());
}
if (branching.clamp() != null) {
modifiers.add(Placements.filter(branching.clamp().deepCopy()));
}
JsonObject branch = new JsonObject();
branch.add("feature", feature);
branch.add("placement", array(modifiers));
return branch;
}
private JsonObject overlay(JsonObject template, List<JsonObject> parts) {
JsonArray features = new JsonArray(parts.size() + 1);
JsonObject anchored = new JsonObject();
anchored.add("feature", template);
anchored.add("placement", new JsonArray());
features.add(anchored);
for (JsonObject part : parts) {
features.add(part);
}
JsonObject fields = new JsonObject();
fields.add("features", features);
return json.syntax().feature("minecraft:overlay", fields);
}
private static boolean grounds(PlacementInput input) {
IrisObjectPlacement placement = input.placement();
ObjectPlaceMode mode = placement.getMode();
return input.anchor() == PlacementInput.Anchor.SURFACE && !placement.isForcePlace() && !placement.isFromBottom()
&& mode != ObjectPlaceMode.PAINT && mode != ObjectPlaceMode.FLOATING && mode != ObjectPlaceMode.CEILING_HANG
&& mode != ObjectPlaceMode.STRUCTURE_PIECE;
}
private static boolean rooted(List<TemplateChoice> templates) {
for (TemplateChoice template : templates) {
if (template.variant().roots().present()) {
return true;
}
}
return false;
}
private static JsonArray array(List<JsonObject> values) {
JsonArray array = new JsonArray(values.size());
for (JsonObject value : values) {
array.add(value);
}
return array;
}
private static boolean slopeLimited(PlacementInput input) {
IrisObjectPlacement placement = input.placement();
return input.anchor() == PlacementInput.Anchor.SURFACE && placement.getMode() != ObjectPlaceMode.FLOATING
&& !placement.isForcePlace() && !Placements.slopeLimits(placement.getSlopeCondition()).isEmpty();
}
private JsonObject template(List<TemplateChoice> templates, List<JsonObject> processors) { private JsonObject template(List<TemplateChoice> templates, List<JsonObject> processors) {
JsonArray entries = new JsonArray(templates.size()); JsonArray entries = new JsonArray(templates.size());
for (TemplateChoice template : templates) { for (TemplateChoice template : templates) {
@@ -262,20 +422,66 @@ public final class PlacementCompiler {
return json.syntax().feature("minecraft:single_block_pillar", fields); return json.syntax().feature("minecraft:single_block_pillar", fields);
} }
private static JsonObject canReplace() { static JsonObject canReplace() {
JsonObject fluids = Placements.typed("minecraft:matching_fluids"); return Placements.anyOf(List.of(Placements.air(0, 0, 0), Placements.fluids(FLUIDS, 0, 0, 0), Placements.typed("minecraft:replaceable")));
JsonArray list = new JsonArray(FLUIDS.size()); }
for (String fluid : FLUIDS) {
list.add(fluid); private static JsonObject ground(boolean underwater) {
List<JsonObject> rules = new ArrayList<>(4);
rules.add(Placements.solid(0, -1, 0));
rules.add(Placements.not(Placements.blockTag("minecraft:leaves", 0, -1, 0)));
rules.add(Placements.not(Placements.blockTag("minecraft:logs", 0, -1, 0)));
rules.add(underwater ? Placements.water(0, 0, 0) : Placements.not(Placements.fluids(FLUIDS, 0, 0, 0)));
return Placements.allOf(rules);
}
private static boolean onwater(IrisObjectPlacement placement) {
return placement.isOnwater() && !placement.isUnderwater();
}
private List<List<ObjectTemplates>> depthGroups(PlacementInput input, List<ObjectTemplates> narrow) {
IrisObjectPlacement placement = input.placement();
if (narrow.isEmpty() || !placement.isUnderwater() || input.anchor() == PlacementInput.Anchor.CAVE
|| placement.getMode() == ObjectPlaceMode.FLOATING) {
return narrow.isEmpty() ? List.of() : List.of(narrow);
} }
fluids.add("fluids", list); Map<Integer, List<ObjectTemplates>> groups = new TreeMap<>();
return Placements.anyOf(List.of(Placements.air(0, 0, 0), fluids, Placements.typed("minecraft:replaceable"))); for (ObjectTemplates object : narrow) {
groups.computeIfAbsent(fluidCeiling(object.choices(), placement.getTranslate().getY()), ignored -> new ArrayList<>()).add(object);
}
return new ArrayList<>(groups.values());
}
private int fluidCeiling(List<TemplateChoice> templates, int translateY) {
int reach = 0;
for (TemplateChoice template : templates) {
reach = Math.max(reach, 3 * (rows(template.variant().volume()) / 2));
}
return (dimension == null ? DEFAULT_FLUID_HEIGHT : dimension.fluidHeight()) - reach - translateY;
}
private JsonObject belowSurface(int ceiling) {
int bottom = dimension == null ? MIN_ANCHOR : dimension.minY();
JsonObject predicate = Placements.typed("minecraft:height_range");
predicate.add("max_inclusive", FeatureJson.absolute(Math.max(MIN_ANCHOR, Math.min(MAX_ANCHOR, ceiling))));
predicate.add("min_inclusive", FeatureJson.absolute(Math.max(MIN_ANCHOR, Math.min(bottom, ceiling))));
return predicate;
}
private static int rows(TemplateVolume volume) {
int low = Integer.MAX_VALUE;
int high = Integer.MIN_VALUE;
for (TemplateBlock block : volume.blocks()) {
low = Math.min(low, block.y());
high = Math.max(high, block.y());
}
return low > high ? 1 : high - low + 1;
} }
private static JsonObject gravity(IrisObjectPlacement placement) { private static JsonObject gravity(IrisObjectPlacement placement) {
JsonObject gravity = new JsonObject(); JsonObject gravity = new JsonObject();
gravity.addProperty("processor_type", "minecraft:gravity"); gravity.addProperty("processor_type", "minecraft:gravity");
gravity.addProperty("heightmap", placement.isUnderwater() ? "OCEAN_FLOOR_WG" : "WORLD_SURFACE_WG"); gravity.addProperty("heightmap", onwater(placement) ? Placements.WORLD_SURFACE : Placements.OCEAN_FLOOR);
gravity.addProperty("offset", placement.getTranslate().getY() - 1); gravity.addProperty("offset", placement.getTranslate().getY() - 1);
return gravity; return gravity;
} }
@@ -291,17 +497,88 @@ public final class PlacementCompiler {
} }
private static JsonObject support(List<TemplateChoice> templates, IrisObjectPlacement placement) { private static JsonObject support(List<TemplateChoice> templates, IrisObjectPlacement placement) {
int half = Integer.MAX_VALUE; int minX = 0;
int maxX = 0;
int minZ = 0;
int maxZ = 0;
for (TemplateChoice template : templates) { for (TemplateChoice template : templates) {
half = Math.min(half, Math.min(template.variant().volume().sizeX(), template.variant().volume().sizeZ()) / 2); TemplateVolume volume = template.variant().volume();
int base = baseRow(volume);
for (TemplateBlock block : volume.blocks()) {
if (block.y() != base || foliage(block.state())) {
continue;
}
int x = block.x() - volume.sizeX() / 2;
int z = block.z() - volume.sizeZ() / 2;
for (int quarter : template.quarters()) {
int rotatedX = switch (quarter) {
case 1 -> z;
case 2 -> -x;
case 3 -> -z;
default -> x;
};
int rotatedZ = switch (quarter) {
case 1 -> -x;
case 2 -> -z;
case 3 -> x;
default -> z;
};
minX = Math.min(minX, rotatedX);
maxX = Math.max(maxX, rotatedX);
minZ = Math.min(minZ, rotatedZ);
maxZ = Math.max(maxZ, rotatedZ);
}
}
} }
int reach = Math.min(WINDOW_REACH, Math.max(0, half + Math.max(0, placement.getSurfaceSupportBuffer()))); int buffer = Math.max(0, placement.getSurfaceSupportBuffer());
int depth = Math.max(1, placement.getSurfaceSupportDepth()); int thickness = Math.max(1, Math.min(MAX_SUPPORT_DROP, placement.getSurfaceSupportDepth()));
JsonObject predicate = Placements.typed("minecraft:volume_match"); int[] xs = {reach(minX - buffer), reach(maxX + buffer)};
predicate.add("match", Placements.typed("minecraft:solid")); int[] zs = {reach(minZ - buffer), reach(maxZ + buffer)};
predicate.add("max", FeatureJson.vector(reach, -1, reach)); Set<Long> seen = new LinkedHashSet<>();
predicate.add("min", FeatureJson.vector(-reach, -depth, -reach)); List<JsonObject> corners = new ArrayList<>(4);
return predicate; for (int x : xs) {
for (int z : zs) {
if ((x == 0 && z == 0) || !seen.add(((long) x << 32) | (z & 0xFFFFFFFFL))) {
continue;
}
corners.add(groundBelow(x, z, thickness));
}
}
return corners.isEmpty() ? null : Placements.allOf(corners);
}
private static JsonObject groundBelow(int x, int z, int thickness) {
List<JsonObject> tops = new ArrayList<>(MAX_SUPPORT_DROP - thickness + 1);
for (int top = 1; top + thickness - 1 <= MAX_SUPPORT_DROP; top++) {
if (thickness == 1) {
tops.add(Placements.solid(x, -top, z));
continue;
}
List<JsonObject> layers = new ArrayList<>(thickness);
for (int layer = 0; layer < thickness; layer++) {
layers.add(Placements.solid(x, -(top + layer), z));
}
tops.add(Placements.allOf(layers));
}
return Placements.anyOf(tops);
}
private static int baseRow(TemplateVolume volume) {
int base = Integer.MAX_VALUE;
for (TemplateBlock block : volume.blocks()) {
if (!foliage(block.state())) {
base = Math.min(base, block.y());
}
}
return base;
}
private static boolean foliage(VanillaBlockState state) {
return state.name().endsWith("_leaves");
}
private static int reach(int offset) {
return Math.max(-WINDOW_REACH, Math.min(WINDOW_REACH, offset));
} }
private JsonObject clamp(IrisObjectLimit limit) { private JsonObject clamp(IrisObjectLimit limit) {
@@ -323,7 +600,7 @@ public final class PlacementCompiler {
private JsonObject caveRange() { private JsonObject caveRange() {
int floor = dimension == null ? 0 : dimension.minY() + CAVE_FLOOR_MARGIN; int floor = dimension == null ? 0 : dimension.minY() + CAVE_FLOOR_MARGIN;
int ceiling = dimension == null ? 63 : dimension.fluidHeight(); int ceiling = dimension == null ? DEFAULT_FLUID_HEIGHT : dimension.fluidHeight();
JsonObject height = new JsonObject(); JsonObject height = new JsonObject();
height.addProperty("type", "minecraft:uniform"); height.addProperty("type", "minecraft:uniform");
height.add("max_inclusive", FeatureJson.absolute(Math.max(floor, ceiling))); height.add("max_inclusive", FeatureJson.absolute(Math.max(floor, ceiling)));
@@ -393,4 +670,8 @@ public final class PlacementCompiler {
} }
return tallest; return tallest;
} }
private record Branching(PlacementInput input, List<JsonObject> processors, HeightAnchor.Sampling sampling, boolean grounded,
List<JsonObject> offset, JsonObject clamp, Map<String, JsonResource> shared) {
}
} }
@@ -0,0 +1,92 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.compile.decoration.Placements;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
final class RootExtension {
private static final int PROBE_LIMIT = 16;
private static final int QUARTER_DEGREES = 90;
private final String namespace;
private final FeatureJson json;
RootExtension(String namespace, FeatureJson json) {
this.namespace = namespace;
this.json = json;
}
static JsonObject gap(TreeRoots roots, TemplateVolume volume, int quarter) {
List<JsonObject> columns = new ArrayList<>(roots.cells().size());
for (TemplateBlock cell : roots.cells()) {
TemplateColumn column = TemplateColumn.of(volume, cell.x(), cell.z(), quarter);
int top = Math.min(PROBE_LIMIT, cell.y() - 1);
int bottom = Math.max(-PROBE_LIMIT, cell.y() - 1 - roots.reach());
if (bottom > top || Math.abs(column.x()) > PROBE_LIMIT || Math.abs(column.z()) > PROBE_LIMIT) {
continue;
}
JsonObject open = Placements.typed("minecraft:volume_match");
open.add("min", FeatureJson.vector(column.x(), bottom, column.z()));
open.add("max", FeatureJson.vector(column.x(), top, column.z()));
open.add("match", Placements.not(Placements.solid(0, 0, 0)));
columns.add(Placements.not(open));
}
return columns.isEmpty() ? null : Placements.allOf(columns);
}
List<JsonObject> pillars(TreeRoots roots, TemplateVolume volume, int quarter, Map<String, JsonResource> shared) {
BlockStateRotation rotation = BlockStateRotation.of(0, quarter * QUARTER_DEGREES, 0);
List<JsonObject> pillars = new ArrayList<>(roots.cells().size());
for (TemplateBlock cell : roots.cells()) {
TemplateColumn column = TemplateColumn.of(volume, cell.x(), cell.z(), quarter);
JsonObject entry = new JsonObject();
entry.addProperty("feature", pillar(rotation.apply(cell.state()), shared));
JsonArray placement = new JsonArray();
for (JsonObject offset : json.offset(column.x(), cell.y() - 1, cell.y() - 1, column.z())) {
placement.add(offset);
}
entry.add("placement", placement);
pillars.add(entry);
}
return pillars;
}
private String pillar(VanillaBlockState state, Map<String, JsonResource> shared) {
JsonObject block = json.syntax().provider(List.of(new WeightedState(state, 1)));
JsonObject cap = new JsonObject();
JsonObject capFields = new JsonObject();
capFields.add("to_place", block);
cap.add("feature", json.syntax().feature("minecraft:simple_block", capFields));
JsonArray capPlacement = new JsonArray(1);
capPlacement.add(Placements.verticalOffset(-1));
cap.add("placement", capPlacement);
JsonObject canReplace = PlacementCompiler.canReplace();
JsonObject columnFields = new JsonObject();
columnFields.add("block", block);
columnFields.add("can_replace", canReplace);
columnFields.addProperty("direction", "down");
columnFields.add("cap_feature", cap);
JsonObject gated = new JsonObject();
gated.add("feature", json.syntax().feature("minecraft:single_block_pillar", columnFields));
JsonArray gate = new JsonArray(1);
gate.add(Placements.filter(canReplace));
gated.add("placement", gate);
JsonArray features = new JsonArray(1);
features.add(gated);
JsonObject overlayFields = new JsonObject();
overlayFields.add("features", features);
JsonObject feature = json.syntax().feature("minecraft:overlay", overlayFields);
String path = "object/root/" + ContentIds.hash(feature);
shared.putIfAbsent(path, new JsonResource("data/" + namespace + "/" + json.syntax().featureFolder() + "/" + path + ".json", feature));
return namespace + ":" + path;
}
}
@@ -0,0 +1,18 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
record TemplateColumn(int x, int z) {
static TemplateColumn of(TemplateVolume volume, int templateX, int templateZ, int quarter) {
return new TemplateColumn(templateX - volume.sizeX() / 2, templateZ - volume.sizeZ() / 2).turn(quarter);
}
TemplateColumn turn(int quarter) {
return switch (quarter) {
case 1 -> new TemplateColumn(z, -x);
case 2 -> new TemplateColumn(-x, -z);
case 3 -> new TemplateColumn(-z, x);
default -> this;
};
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public record TreeRoots(List<TemplateBlock> cells, int reach) {
private static final int MAX_REACH = 16;
private static final Comparator<TemplateBlock> COLUMN_ORDER = Comparator
.comparingInt(TemplateBlock::x)
.thenComparingInt(TemplateBlock::z);
public static final TreeRoots NONE = new TreeRoots(List.of(), 0);
public TreeRoots {
cells = lowestPerColumn(cells);
reach = Math.max(0, Math.min(MAX_REACH, reach));
}
public boolean present() {
return !cells.isEmpty();
}
public TreeRoots lift(int dy) {
if (dy == 0 || cells.isEmpty()) {
return this;
}
List<TemplateBlock> lifted = new ArrayList<>(cells.size());
for (TemplateBlock cell : cells) {
lifted.add(new TemplateBlock(cell.x(), cell.y() + dy, cell.z(), cell.state(), null));
}
return new TreeRoots(lifted, reach);
}
private static List<TemplateBlock> lowestPerColumn(List<TemplateBlock> cells) {
Map<Long, TemplateBlock> columns = new HashMap<>();
for (TemplateBlock cell : cells) {
long key = ((long) cell.x() << 32) | (cell.z() & 0xFFFFFFFFL);
TemplateBlock previous = columns.get(key);
if (previous == null || cell.y() < previous.y()) {
columns.put(key, new TemplateBlock(cell.x(), cell.y(), cell.z(), cell.state(), null));
}
}
List<TemplateBlock> lowest = new ArrayList<>(columns.values());
lowest.sort(COLUMN_ORDER);
return List.copyOf(lowest);
}
}
@@ -132,7 +132,7 @@ abstract class BaseWorldgenJsonWriter implements WorldgenJsonWriter {
private JsonObject packMetadata(ExportIr ir) { private JsonObject packMetadata(ExportIr ir) {
JsonObject pack = new JsonObject(); JsonObject pack = new JsonObject();
pack.addProperty("description", "Iris worldgen export " + ir.namespace() + ":" + ir.dimension().name()); pack.addProperty("description", "Iris worldgen export " + ir.namespace() + ":" + ir.dimension().name() + ". Keep Default selected");
pack.addProperty("min_format", target.getPackFormat()); pack.addProperty("min_format", target.getPackFormat());
pack.addProperty("max_format", target.getPackFormat()); pack.addProperty("max_format", target.getPackFormat());
JsonObject metadata = new JsonObject(); JsonObject metadata = new JsonObject();
@@ -4,6 +4,7 @@ import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.pack.datapack.export.ExportTestPacks; import art.arcane.iris.pack.datapack.export.ExportTestPacks;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext; import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef; import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic; import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit; import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field; import art.arcane.iris.pack.datapack.export.ir.Field;
@@ -26,6 +27,16 @@ import static org.junit.Assert.assertTrue;
public class AquiferCompilerTest { public class AquiferCompilerTest {
private static final Field SURFACE = new Field.Const(120.0); private static final Field SURFACE = new Field.Const(120.0);
private static final Field SEA_FLOOR_SLOPE = new Field.Coord(Axis.X, Field.Tiling.CLAMP, -1000, 1000, -100.0, 300.0);
private static final Field HIGH_ESTIMATE = new Field.Const(120.0);
private static final int SEA_X = -275;
private static final int LAND_X = 400;
private static final int DEEP_SEA_X = -800;
private static final int SHORE_X = -165;
private static final int HILL_X = -125;
private static final int SEA_FLOOR = 45;
private static final int FLUID_HEIGHT = 63;
private static final double FULLY_FLOODED = 0.8;
private static final double DRY = -1.0; private static final double DRY = -1.0;
private static final double PARTIAL = 0.6; private static final double PARTIAL = 0.6;
private static final int FLUID_BAND_HEIGHT = 40; private static final int FLUID_BAND_HEIGHT = 40;
@@ -45,12 +56,67 @@ public class AquiferCompilerTest {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1)); PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1));
AquiferDef aquifer = context.aquifer(); AquiferDef aquifer = context.aquifer();
assertEquals(AquiferDef.dry(SURFACE), aquifer); AquiferDef vanilla = AquiferDef.dry(SURFACE);
assertEquals(vanilla.barrier(), aquifer.barrier());
assertEquals(vanilla.spread(), aquifer.spread());
assertEquals(vanilla.lava(), aquifer.lava());
assertEquals(vanilla.exclusion(), aquifer.exclusion());
assertEquals(vanilla.surfaceLevel(), aquifer.surfaceLevel());
for (int y = CaveTestPacks.MIN_Y; y < CaveTestPacks.MAX_Y; y += 8) { for (int y = CaveTestPacks.MIN_Y; y < CaveTestPacks.MAX_Y; y += 8) {
assertEquals(DRY, CaveTestPacks.sample(aquifer.floodedness(), context.noises(), 17, y, -9), 0.0); assertEquals(DRY, CaveTestPacks.sample(aquifer.floodedness(), context.noises(), 17, y, -9), 0.0);
} }
} }
@Test
public void openWaterAboveTheTerrainFloodsToSeaLevel() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1), SEA_FLOOR_SLOPE, SEA_FLOOR_SLOPE);
Field floodedness = context.aquifer().floodedness();
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SEA_X, SEA_FLOOR + 1, 4) > FULLY_FLOODED);
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SEA_X, FLUID_HEIGHT, 4) > FULLY_FLOODED);
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SEA_X, FLUID_HEIGHT + 6, 4) > FULLY_FLOODED);
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SEA_X, SEA_FLOOR - 4, 4) > FULLY_FLOODED);
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), SEA_X, SEA_FLOOR - 12, 4), 0.0);
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), LAND_X, FLUID_HEIGHT - 10, 4), 0.0);
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), LAND_X, SEA_FLOOR + 1, 4), 0.0);
}
@Test
public void openWaterLiftStaysNearSeaLevel() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1), SEA_FLOOR_SLOPE, SEA_FLOOR_SLOPE);
Field floodedness = context.aquifer().floodedness();
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), DEEP_SEA_X, FLUID_HEIGHT - 40, 4), 0.0);
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), DEEP_SEA_X, FLUID_HEIGHT - 20, 4) > FULLY_FLOODED);
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), SEA_X, FLUID_HEIGHT + 12, 4), 0.0);
}
@Test
public void openWaterLiftStopsAtTheShore() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1), SEA_FLOOR_SLOPE, SEA_FLOOR_SLOPE);
Field floodedness = context.aquifer().floodedness();
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SHORE_X, FLUID_HEIGHT, 4) > FULLY_FLOODED);
assertEquals(DRY, CaveTestPacks.sample(floodedness, context.noises(), HILL_X, FLUID_HEIGHT + 7, 4), 0.0);
}
@Test
public void openWaterLiftSkipsColumnsWithAHighSurfaceEstimate() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1), SEA_FLOOR_SLOPE, HIGH_ESTIMATE);
assertEquals(DRY, CaveTestPacks.sample(context.aquifer().floodedness(), context.noises(), SEA_X, SEA_FLOOR + 1, 4), 0.0);
}
@Test
public void wetCavesBelowTheTerrainKeepPartialFlooding() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(20), CaveTestPacks.constantProfile(-0.1).setAllowFluid(true), SEA_FLOOR_SLOPE, SEA_FLOOR_SLOPE);
Field floodedness = context.aquifer().floodedness();
assertTrue(CaveTestPacks.sample(floodedness, context.noises(), SEA_X, SEA_FLOOR + 6, 4) > FULLY_FLOODED);
assertEquals(PARTIAL, CaveTestPacks.sample(floodedness, context.noises(), SEA_X, SEA_FLOOR - 24, 4), 1.0e-6);
assertEquals(PARTIAL, CaveTestPacks.sample(floodedness, context.noises(), LAND_X, FLUID_HEIGHT - 10, 4), 1.0e-6);
}
@Test @Test
public void lavaAboveMinus10ReportsLimit() throws Exception { public void lavaAboveMinus10ReportsLimit() throws Exception {
PackCompileContext context = compile(CaveTestPacks.dimension(60), CaveTestPacks.constantProfile(-0.1).setAllowLava(true)); PackCompileContext context = compile(CaveTestPacks.dimension(60), CaveTestPacks.constantProfile(-0.1).setAllowLava(true));
@@ -115,11 +181,17 @@ public class AquiferCompilerTest {
} }
private PackCompileContext compile(JsonObject dimension, IrisCaveProfile profile) throws Exception { private PackCompileContext compile(JsonObject dimension, IrisCaveProfile profile) throws Exception {
return compile(dimension, profile, SURFACE, SURFACE);
}
private PackCompileContext compile(JsonObject dimension, IrisCaveProfile profile, Field height, Field estimate) throws Exception {
File pack = CaveTestPacks.pack(temporaryFolder.newFolder("pack"), dimension, CaveTestPacks.region(List.of("cave-only"), null), File pack = CaveTestPacks.pack(temporaryFolder.newFolder("pack"), dimension, CaveTestPacks.region(List.of("cave-only"), null),
Map.of("cave-only", profile)); Map.of("cave-only", profile));
IrisData data = IrisData.openRuntime(pack); IrisData data = IrisData.openRuntime(pack);
try { try {
PackCompileContext context = context(data, "cave-only"); PackCompileContext context = context(data, "cave-only");
context.setHeight(height);
context.setSurfaceEstimate(estimate);
new CaveStage().apply(context); new CaveStage().apply(context);
return context; return context;
} finally { } finally {
@@ -133,7 +133,14 @@ public class CaveStageTest {
new CaveStage().apply(context); new CaveStage().apply(context);
assertEquals(new Field.Const(1.0e6), context.caves()); assertEquals(new Field.Const(1.0e6), context.caves());
assertEquals(AquiferDef.dry(SURFACE), context.aquifer()); AquiferDef aquifer = context.aquifer();
AquiferDef vanilla = AquiferDef.dry(SURFACE);
assertEquals(vanilla.barrier(), aquifer.barrier());
assertEquals(vanilla.spread(), aquifer.spread());
assertEquals(vanilla.lava(), aquifer.lava());
assertEquals(vanilla.exclusion(), aquifer.exclusion());
assertEquals(vanilla.surfaceLevel(), aquifer.surfaceLevel());
assertEquals(-1.0, CaveTestPacks.sample(aquifer.floodedness(), context.noises(), 4, 40, 4), 0.0);
assertTrue(context.diagnostics().isEmpty()); assertTrue(context.diagnostics().isEmpty());
} finally { } finally {
data.close(); data.close();
@@ -1,9 +1,11 @@
package art.arcane.iris.pack.datapack.export.compile.decoration; package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.DataVersion; import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ExportTestPacks; import art.arcane.iris.pack.datapack.export.ExportTestPacks;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext; import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.StageTestPacks; import art.arcane.iris.pack.datapack.export.compile.StageTestPacks;
import art.arcane.iris.pack.datapack.export.compile.noise.VanillaNoiseModel;
import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler; import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler;
import art.arcane.iris.pack.datapack.export.compile.surface.SurfaceStage; import art.arcane.iris.pack.datapack.export.compile.surface.SurfaceStage;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef; import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
@@ -13,6 +15,9 @@ import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.JsonResource; import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@@ -27,6 +32,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
@@ -132,6 +138,167 @@ public class DecorationStageTest {
modern.under("worldgen/feature/decorator/").getFirst().json().toString()); modern.under("worldgen/feature/decorator/").getFirst().json().toString());
} }
@Test
public void aquaticDecoratorsPlaceOnlyUnderwater() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("reef", "{\"decorators\":[" + decorator("seagrass", 0.2) + ","
+ "{\"palette\":[{\"block\":\"kelp_plant\"}],\"chance\":0.02,\"stackMin\":2,\"stackMax\":6},"
+ "{\"palette\":[{\"block\":\"kelp_plant\"}],\"chance\":0.03,\"stackMin\":2,\"stackMax\":6,\"partOf\":\"SEA_FLOOR\"}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
List<JsonResource> placed = result.under("worldgen/placed_feature/decorator/");
assertEquals(3, placed.size());
for (JsonResource resource : placed) {
JsonArray placement = resource.json().getAsJsonObject().getAsJsonArray("placement");
assertEquals(resource.path(), Placements.OCEAN_FLOOR,
StageTestPacks.ofType(placement, "minecraft:heightmap").getFirst().get("heightmap").getAsString());
JsonObject filter = StageTestPacks.ofType(placement, "minecraft:block_predicate_filter").getLast();
assertFalse(resource.path(), StageTestPacks.ofType(filter, "minecraft:matching_fluids").isEmpty());
assertTrue(resource.path(), StageTestPacks.ofType(filter, "minecraft:matching_block_tag").isEmpty());
}
for (JsonResource resource : result.under("worldgen/feature/decorator/")) {
JsonObject feature = resource.json().getAsJsonObject();
if (feature.has("allowed_placement")) {
assertEquals(resource.path(), "minecraft:matching_fluids",
feature.getAsJsonObject("allowed_placement").get("type").getAsString());
}
}
}
@Test
public void shoreDecoratorsGrowOnSolidGroundBesideWater() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("beach", "{\"decorators\":[{\"palette\":[{\"block\":\"sugar_cane\"}],\"partOf\":\"SHORE_LINE\",\"chance\":0.5,"
+ "\"stackMin\":1,\"stackMax\":4}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonArray placement = result.under("worldgen/placed_feature/decorator/").getFirst().json().getAsJsonObject().getAsJsonArray("placement");
JsonObject filter = StageTestPacks.ofType(placement, "minecraft:block_predicate_filter").getLast();
List<String> supports = new ArrayList<>();
for (JsonObject solid : StageTestPacks.ofType(filter, "minecraft:solid")) {
supports.add(solid.get("offset").toString());
}
assertEquals(List.of("[0,-1,0]"), supports);
assertEquals(4, StageTestPacks.ofType(filter, "minecraft:matching_fluids").size());
}
@Test
public void tallPlantsPlaceBothHalves() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("reef", "{\"decorators\":[{\"palette\":[{\"block\":\"tall_seagrass\",\"data\":{\"half\":\"lower\"}}],"
+ "\"topPalette\":[{\"block\":\"tall_seagrass\",\"data\":{\"half\":\"upper\"}}],\"partOf\":\"SEA_FLOOR\",\"chance\":0.2}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonObject feature = result.under("worldgen/feature/decorator/").getFirst().json().getAsJsonObject();
assertEquals(feature.toString(), "minecraft:simple_block", feature.get("type").getAsString());
assertEquals("{\"id\":\"minecraft:tall_seagrass\",\"properties\":{\"half\":\"lower\"}}", feature.get("to_place").toString());
JsonArray placement = result.under("worldgen/placed_feature/decorator/").getFirst().json().getAsJsonObject().getAsJsonArray("placement");
List<String> water = new ArrayList<>();
for (JsonObject fluid : StageTestPacks.ofType(placement, "minecraft:matching_fluids")) {
water.add(fluid.has("offset") ? fluid.get("offset").toString() : "[0,0,0]");
}
assertEquals(List.of("[0,0,0]", "[0,1,0]"), water);
}
@Test
public void waterlogFollowsWhereTheDecoratorPlaces() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("reef", "{\"decorators\":[" + decorator("sea_pickle[pickles=2,waterlogged=false]", 0.2) + ","
+ "{\"palette\":[{\"block\":\"oak_stairs[waterlogged=true]\"},{\"block\":\"short_grass\"}],\"chance\":0.1}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
String pickles = result.resource("data/iris_export/worldgen/feature/decorator/sea_pickle.json").toString();
String stairs = result.resource("data/iris_export/worldgen/feature/decorator/oak_stairs.json").toString();
assertTrue(pickles, pickles.contains("\"waterlogged\":\"true\""));
assertTrue(stairs, stairs.contains("\"waterlogged\":\"false\""));
}
@Test
public void paletteEndsCarryHalfWeightLikeIrisRounding() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("meadow", "{\"decorators\":[{\"palette\":[{\"block\":\"bush\"},{\"block\":\"air\",\"weight\":4}],\"chance\":0.5},"
+ "{\"palette\":[{\"block\":\"fern\",\"weight\":2},{\"block\":\"short_grass\",\"weight\":3}],\"chance\":0.2,\"partOf\":\"SHORE_LINE\"}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonArray placement = placement(result, "data/iris_export/worldgen/placed_feature/decorator/bush.json");
assertEquals(0.5 / 8.0, StageTestPacks.ofType(placement, "minecraft:random_chance").getFirst().get("chance").getAsDouble(), 1.0e-6);
JsonArray weighted = result.resource("data/iris_export/worldgen/feature/decorator/fern.json").getAsJsonObject()
.getAsJsonObject("to_place").getAsJsonArray("entries");
assertEquals(3, weighted.get(0).getAsJsonObject().get("weight").getAsInt());
assertEquals(5, weighted.get(1).getAsJsonObject().get("weight").getAsInt());
}
@Test
public void styledDecoratorPlacesAtStyleCoverage() throws Exception {
String style = "{\"style\":\"SIMPLEX\",\"zoom\":0.2,\"fracture\":{\"style\":\"STATIC\",\"multiplier\":8}}";
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("meadow", "{\"decorators\":[{\"palette\":[{\"block\":\"poppy\"}],\"chance\":0.2,\"style\":" + style + "}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonArray placement = placement(result, "data/iris_export/worldgen/placed_feature/decorator/poppy.json");
double exported = StageTestPacks.ofType(placement, "minecraft:random_chance").getFirst().get("chance").getAsDouble();
double coverage = irisCoverage(style, 0.2);
assertTrue("coverage " + coverage, coverage < 0.1);
assertEquals(coverage, exported, coverage * 0.2);
assertEquals("{\"type\":\"minecraft:simple_block\",\"to_place\":{\"id\":\"minecraft:poppy\"}}",
result.resource("data/iris_export/worldgen/feature/decorator/poppy.json").toString());
}
@Test
public void decoratorWhoseStyleNeverPassesIsLeftOut() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("meadow", "{\"decorators\":[{\"palette\":[{\"block\":\"poppy\"}],\"chance\":0.5,\"style\":{\"style\":\"FLAT\"}},"
+ decorator("short_grass", 0.3) + "]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
assertEquals(List.of("iris_export:decorator/short_grass"), step(result.biomes().getFirst(), GenerationStep.VEGETAL_DECORATION));
}
@Test
public void clusteredStyleFollowsFittedNoisePattern() throws Exception {
String style = "{\"style\":\"SIMPLEX\",\"zoom\":4}";
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("meadow", "{\"decorators\":[{\"palette\":[{\"block\":\"poppy\"},{\"block\":\"dandelion\",\"weight\":2}],\"chance\":0.3,\"style\":"
+ style + "}]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonObject provider = result.resource("data/iris_export/worldgen/feature/decorator/poppy.json").getAsJsonObject()
.getAsJsonObject("to_place");
assertEquals("minecraft:noise_threshold", provider.get("type").getAsString());
assertEquals("minecraft:air", provider.getAsJsonObject("default_state").get("id").getAsString());
assertEquals(0.0, provider.get("high_chance").getAsDouble(), 0.0);
List<String> low = new ArrayList<>();
for (JsonElement state : provider.getAsJsonArray("low_states")) {
low.add(state.getAsJsonObject().get("id").getAsString());
}
assertEquals(List.of("minecraft:poppy", "minecraft:dandelion", "minecraft:dandelion", "minecraft:dandelion"), low);
assertTrue(StageTestPacks.ofType(placement(result, "data/iris_export/worldgen/placed_feature/decorator/poppy.json"),
"minecraft:random_chance").isEmpty());
JsonObject noise = provider.getAsJsonObject("noise");
List<Double> amplitudes = new ArrayList<>();
for (JsonElement amplitude : noise.getAsJsonArray("amplitude_modifiers")) {
amplitudes.add(amplitude.getAsDouble());
}
VanillaNoiseModel model = VanillaNoiseModel.create("iris_export:test", noise.get("base_octave").getAsInt(), amplitudes, 99L, DataVersion.V26_3);
double scale = provider.get("scale").getAsDouble();
double threshold = provider.get("threshold").getAsDouble();
GatePattern iris = irisPattern(style, 0.3);
GatePattern vanilla = pattern((x, z) -> model.sample(x * scale, 70.0 * scale, z * scale) < threshold);
assertTrue("iris coverage " + iris.coverage(), iris.coverage() > 0.1 && iris.coverage() < 0.5);
assertEquals(iris.coverage(), vanilla.coverage(), 0.03);
assertTrue("iris patch " + iris.patch() + " vanilla " + vanilla.patch(),
vanilla.patch() > iris.patch() * 0.7 && vanilla.patch() < iris.patch() * 1.4);
}
@Test @Test
public void columnWinnerSharesFollowIrisReservoirPick() { public void columnWinnerSharesFollowIrisReservoirPick() {
assertArrayEquals(new double[]{0.5, 0.5}, DecoratorCompiler.columnShares(new double[]{1.0, 1.0}), 1.0e-12); assertArrayEquals(new double[]{0.5, 0.5}, DecoratorCompiler.columnShares(new double[]{1.0, 1.0}), 1.0e-12);
@@ -163,12 +330,40 @@ public class DecorationStageTest {
JsonObject ore = result.resource("data/iris_export/worldgen/feature/deposit/coal_ore.json").getAsJsonObject(); JsonObject ore = result.resource("data/iris_export/worldgen/feature/deposit/coal_ore.json").getAsJsonObject();
assertEquals("minecraft:ore", ore.get("type").getAsString()); assertEquals("minecraft:ore", ore.get("type").getAsString());
assertEquals(8, ore.get("size").getAsInt()); assertEquals(8, ore.get("size").getAsInt());
JsonObject target = ore.getAsJsonArray("targets").get(0).getAsJsonObject(); JsonObject target = ore.getAsJsonArray("targets").get(1).getAsJsonObject();
assertEquals("{\"predicate_type\":\"minecraft:tag_match\",\"tag\":\"iris_export:deposit_host\"}", target.get("target").toString()); assertEquals("{\"predicate_type\":\"minecraft:tag_match\",\"tag\":\"iris_export:deposit_host\"}", target.get("target").toString());
assertEquals("{\"id\":\"minecraft:coal_ore\"}", target.get("state").toString()); assertEquals("{\"id\":\"minecraft:coal_ore\"}", target.get("state").toString());
assertNotNull(result.resource("data/iris_export/tags/block/deposit_host.json")); assertNotNull(result.resource("data/iris_export/tags/block/deposit_host.json"));
} }
@Test
public void depositOresFollowTheirHostStone() throws Exception {
String explicit = "{\"palette\":[{\"block\":\"deepslate_redstone_ore[lit=false]\"}],\"minHeight\":0,\"maxHeight\":40,"
+ "\"minPerChunk\":1,\"maxPerChunk\":2,\"minSize\":4,\"maxSize\":4,\"replaceableBlocks\":[\"stone\",\"deepslate\",\"tuff\"]}";
Map<String, String> biomes = new LinkedHashMap<>();
biomes.put("hills", "{\"deposits\":[" + COAL + "," + explicit + "]}");
Result result = decorate(biomes, DataVersion.V26_3, context -> {
});
JsonArray coal = result.resource("data/iris_export/worldgen/feature/deposit/coal_ore.json").getAsJsonObject().getAsJsonArray("targets");
assertEquals(2, coal.size());
assertEquals("{\"predicate_type\":\"minecraft:block_match\",\"block\":\"minecraft:deepslate\"}",
coal.get(0).getAsJsonObject().get("target").toString());
assertEquals("{\"id\":\"minecraft:deepslate_coal_ore\"}", coal.get(0).getAsJsonObject().get("state").toString());
assertEquals("{\"id\":\"minecraft:coal_ore\"}", coal.get(1).getAsJsonObject().get("state").toString());
JsonArray redstone = result.resource("data/iris_export/worldgen/feature/deposit/deepslate_redstone_ore.json").getAsJsonObject()
.getAsJsonArray("targets");
Map<String, String> byHost = new LinkedHashMap<>();
for (JsonElement element : redstone) {
JsonObject target = element.getAsJsonObject();
byHost.put(target.getAsJsonObject("target").get("block").getAsString(), target.get("state").toString());
}
assertEquals("{\"id\":\"minecraft:redstone_ore\",\"properties\":{\"lit\":\"false\"}}", byHost.get("minecraft:stone"));
assertEquals("{\"id\":\"minecraft:deepslate_redstone_ore\",\"properties\":{\"lit\":\"false\"}}", byHost.get("minecraft:deepslate"));
assertEquals("{\"id\":\"minecraft:redstone_ore\",\"properties\":{\"lit\":\"false\"}}", byHost.get("minecraft:tuff"));
}
@Test @Test
public void vanillaOreVeinsDisabled() throws Exception { public void vanillaOreVeinsDisabled() throws Exception {
Map<String, String> biomes = new LinkedHashMap<>(); Map<String, String> biomes = new LinkedHashMap<>();
@@ -260,6 +455,56 @@ public class DecorationStageTest {
return "{\"palette\":[{\"block\":\"" + block + "\"}],\"chance\":" + chance + "}"; return "{\"palette\":[{\"block\":\"" + block + "\"}],\"chance\":" + chance + "}";
} }
private static double irisCoverage(String styleJson, double chance) {
return irisPattern(styleJson, chance).coverage();
}
private static GatePattern irisPattern(String styleJson, double chance) {
IrisGeneratorStyle style = new Gson().fromJson(styleJson, IrisGeneratorStyle.class);
CNG generator = style.create(new RNG(4242L), null);
double zoom = style.getZoom();
return pattern((x, z) -> generator.fitDouble(0.0, 1.0, x / zoom, z / zoom) <= chance);
}
private static GatePattern pattern(BiPredicate<Double, Double> gate) {
int[] lags = {1, 2, 4, 8, 16, 32, 64, 128};
int hits = 0;
int total = 0;
int[] both = new int[lags.length];
for (int i = 0; i < 150; i++) {
for (int j = 0; j < 150; j++) {
double x = i * 197.0 + 3.0;
double z = j * 173.0 - 11.0;
total++;
if (!gate.test(x, z)) {
continue;
}
hits++;
for (int lag = 0; lag < lags.length; lag++) {
both[lag] += gate.test(x + lags[lag], z) ? 1 : 0;
both[lag] += gate.test(x, z + lags[lag]) ? 1 : 0;
}
}
}
double coverage = (double) hits / total;
double previousLag = 0.0;
double previousExcess = 1.0;
for (int lag = 0; lag < lags.length; lag++) {
double excess = ((double) both[lag] / (2.0 * hits) - coverage) / (1.0 - coverage);
if (excess < 0.5) {
double low = previousLag == 0.0 ? 0.0 : Math.log(previousLag);
double fraction = (previousExcess - 0.5) / (previousExcess - excess);
return new GatePattern(coverage, previousLag == 0.0 ? lags[lag] * fraction : Math.exp(low + (Math.log(lags[lag]) - low) * fraction));
}
previousLag = lags[lag];
previousExcess = excess;
}
return new GatePattern(coverage, lags[lags.length - 1]);
}
private record GatePattern(double coverage, double patch) {
}
private static List<String> step(BiomeDef biome, GenerationStep step) { private static List<String> step(BiomeDef biome, GenerationStep step) {
List<String> features = new ArrayList<>(); List<String> features = new ArrayList<>();
for (JsonElement feature : biome.json().getAsJsonArray("features").get(step.ordinal()).getAsJsonArray()) { for (JsonElement feature : biome.json().getAsJsonArray("features").get(step.ordinal()).getAsJsonArray()) {
@@ -0,0 +1,157 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ObjectSourceTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
NativeBlockState stone = blockState("minecraft:stone");
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(stone);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
when(platform.minecraftVersion()).thenReturn("26.3");
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void containedObjectKeepsItsBox() {
IrisObject object = new IrisObject(3, 3, 3);
object.setUnsigned(1, 0, 1, blockState("minecraft:oak_log[axis=y]"));
object.setUnsigned(0, 2, 2, blockState("minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]"));
List<ExportDiagnostic> diagnostics = new ArrayList<>();
ObjectSource source = ObjectSource.of(object, "objects/bush", diagnostics);
assertTrue(diagnostics.isEmpty());
assertEquals(0, source.baseY());
assertEquals(3, source.volume().sizeX());
assertEquals(3, source.volume().sizeY());
assertEquals(3, source.volume().sizeZ());
assertEquals(2, source.volume().blocks().size());
}
@Test
public void waterlogIsLeftToThePlacementSite() {
IrisObject object = new IrisObject(1, 2, 1);
object.setUnsigned(0, 0, 0, blockState("minecraft:tube_coral_block"));
object.setUnsigned(0, 1, 0, blockState("minecraft:sea_pickle[pickles=2,waterlogged=true]"));
ObjectSource source = ObjectSource.of(object, "objects/reef", new ArrayList<>());
TemplateBlock pickle = blockAt(source, 0, 1, 0);
assertEquals("false", pickle.state().properties().get("waterlogged"));
assertEquals("2", pickle.state().properties().get("pickles"));
}
@Test
public void rowsAndColumnsPastTheNominalBoxAreKeptAroundTheCenter() {
IrisObject object = new IrisObject(3, 3, 3);
object.setUnsigned(1, -1, 1, blockState("minecraft:oak_log[axis=y]"));
object.setUnsigned(1, 0, 1, blockState("minecraft:oak_log[axis=y]"));
object.setUnsigned(-1, 1, 1, blockState("minecraft:oak_log[axis=x]"));
List<ExportDiagnostic> diagnostics = new ArrayList<>();
ObjectSource source = ObjectSource.of(object, "objects/tree", diagnostics);
assertTrue(diagnostics.isEmpty());
assertEquals(3, source.volume().blocks().size());
assertEquals(-1, source.baseY());
int centerX = source.volume().sizeX() / 2;
int centerZ = source.volume().sizeZ() / 2;
TemplateBlock root = blockAt(source, centerX, 0, centerZ);
assertEquals("minecraft:oak_log", root.state().name());
TemplateBlock branch = blockAt(source, centerX - 2, 2, centerZ);
assertEquals("x", branch.state().properties().get("axis"));
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/tree/a", source, VariantSpec.builder().build());
assertEquals(-1, variant.baseY());
assertEquals(3, variant.volume().blocks().size());
}
@Test
public void treeRootsFollowTheTemplateShift() {
IrisObject object = new IrisObject(3, 3, 3);
NativeBlockState root = blockState("minecraft:oak_wood[axis=y]");
object.setUnsigned(1, -1, 1, root);
object.setUnsigned(0, -2, 1, root);
object.setUnsigned(0, -1, 1, root);
object.setUnsigned(1, 0, 1, blockState("minecraft:oak_log[axis=y]"));
object.setTreeGrounding(TreeGrounding.create(List.of(
new TreeGrounding.Root(0, -2, 0, root),
new TreeGrounding.Root(-1, -3, 0, root),
new TreeGrounding.Root(-1, -2, 0, root)), 3, Set.of()));
ObjectSource source = ObjectSource.of(object, "procedural/tree", new ArrayList<>());
assertEquals(-2, source.baseY());
assertEquals(3, source.roots().reach());
assertEquals(List.of("0,0,1", "1,1,1"), cells(source.roots()));
assertEquals("minecraft:oak_wood", source.roots().cells().getFirst().state().name());
}
@Test
public void objectsWithoutGroundingHaveNoRoots() {
IrisObject object = new IrisObject(1, 1, 1);
object.setUnsigned(0, 0, 0, blockState("minecraft:stone"));
assertFalse(ObjectSource.of(object, "objects/rock", new ArrayList<>()).roots().present());
}
private static List<String> cells(TreeRoots roots) {
List<String> cells = new ArrayList<>();
for (TemplateBlock cell : roots.cells()) {
cells.add(cell.x() + "," + cell.y() + "," + cell.z());
}
return cells;
}
private static TemplateBlock blockAt(ObjectSource source, int x, int y, int z) {
for (TemplateBlock block : source.volume().blocks()) {
if (block.x() == x && block.y() == y && block.z() == z) {
return block;
}
}
throw new AssertionError("No block at " + x + "," + y + "," + z + " in " + source.volume().blocks());
}
private static NativeBlockState blockState(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.namespace()).thenReturn(key.substring(0, key.indexOf(':')));
return state;
}
}
@@ -129,6 +129,87 @@ public class ObjectStageTest {
} }
} }
@Test
public void proceduralTreesExportAsTemplates() throws Exception {
File pack = pack();
CompatTestPlatform.write(pack, "biomes/plains.json", """
{"name":"Plains","proceduralObjects":{"trees":[{"name":"field-oak","chance":0.5,"density":2,"variants":2,"seed":7,
"trunk":"minecraft:oak_log","leaves":"minecraft:oak_leaves","heightMin":5,"heightMax":7}]}}
""");
IrisData data = IrisData.openRuntime(pack);
try {
ExportIr ir = compile(data, DataVersion.V26_3, true);
JsonArray plains = features(ir, "plains");
assertEquals(1, plains.get(GenerationStep.VEGETAL_DECORATION.ordinal()).getAsJsonArray().size());
long templates = ir.binaryResources().stream()
.filter(template -> template.path().contains("/procedural/") && template.path().contains("field-oak"))
.count();
assertTrue("procedural templates: " + templates, templates >= 2);
} finally {
data.close();
}
}
@Test
public void proceduralTreeRootsReachDownToTheTerrain() throws Exception {
File pack = pack();
CompatTestPlatform.write(pack, "biomes/plains.json", """
{"name":"Plains","proceduralObjects":{"trees":[{"name":"field-oak","chance":0.5,"density":2,"variants":2,"seed":7,
"trunk":"minecraft:oak_log","leaves":"minecraft:oak_leaves","heightMin":5,"heightMax":7,"rootStyle":"TAPROOT","rootDepth":3}]}}
""");
IrisData data = IrisData.openRuntime(pack);
try {
ExportIr ir = compile(data, DataVersion.V26_3, true);
String placedId = features(ir, "plains").get(GenerationStep.VEGETAL_DECORATION.ordinal()).getAsJsonArray().get(0).getAsString();
JsonObject placed = json(ir, "data/iris_export/worldgen/placed_feature/" + placedId.substring(NAMESPACE.length() + 1) + ".json");
String featureId = placed.get("feature").getAsString();
JsonObject feature = json(ir, "data/iris_export/worldgen/feature/" + featureId.substring(NAMESPACE.length() + 1) + ".json");
assertEquals("minecraft:weighted_random_selector", feature.get("type").getAsString());
Set<String> pillars = new HashSet<>();
for (JsonElement branch : feature.getAsJsonArray("features")) {
JsonObject entry = branch.getAsJsonObject().getAsJsonObject("data");
JsonArray parts = entry.getAsJsonObject("feature").getAsJsonArray("features");
for (int index = 1; index < parts.size(); index++) {
pillars.add(parts.get(index).getAsJsonObject().get("feature").getAsString());
}
}
assertFalse(pillars.isEmpty());
for (String pillar : pillars) {
assertTrue(pillar, paths(ir).contains("data/iris_export/worldgen/feature/" + pillar.substring(NAMESPACE.length() + 1) + ".json"));
}
} finally {
data.close();
}
}
@Test
public void proceduralObjectsSharedByBiomesShareTemplates() throws Exception {
String procedural = """
"proceduralObjects":{"trees":[{"name":"field-oak","chance":0.5,"density":2,"variants":2,"seed":7,
"trunk":"minecraft:oak_log","leaves":"minecraft:oak_leaves","heightMin":5,"heightMax":7}]}""";
File pack = pack();
CompatTestPlatform.write(pack, "biomes/plains.json", "{\"name\":\"Plains\"," + procedural + "}");
CompatTestPlatform.write(pack, "biomes/forest.json", "{\"name\":\"Forest\"," + procedural + "}");
IrisData data = IrisData.openRuntime(pack);
try {
ExportIr ir = compile(data, DataVersion.V26_3, true);
Set<String> objects = new HashSet<>();
for (BinaryResource template : ir.binaryResources()) {
if (template.path().contains("field-oak")) {
objects.add(template.path().substring(0, template.path().lastIndexOf('/')));
}
}
assertEquals(objects.toString(), 2, objects.size());
assertEquals(1, features(ir, "forest").get(GenerationStep.VEGETAL_DECORATION.ordinal()).getAsJsonArray().size());
assertEquals(1, features(ir, "plains").get(GenerationStep.VEGETAL_DECORATION.ordinal()).getAsJsonArray().size());
} finally {
data.close();
}
}
@Test @Test
public void lootBoundPerBiome() throws Exception { public void lootBoundPerBiome() throws Exception {
File pack = pack(); File pack = pack();
@@ -38,8 +38,8 @@ public class ObjectVariantBakerTest {
.build(); .build();
String id = ObjectVariantBaker.templateId(NAMESPACE, "trees/oak", spec, DataVersion.V26_3); String id = ObjectVariantBaker.templateId(NAMESPACE, "trees/oak", spec, DataVersion.V26_3);
BakedVariant first = new ObjectVariantBaker().bake(id, tree, spec, "trees/oak", new ArrayList<>()); BakedVariant first = new ObjectVariantBaker().bake(id, new ObjectSource(tree, 0, TreeRoots.NONE), spec);
BakedVariant second = new ObjectVariantBaker().bake(id, tree, spec, "trees/oak", new ArrayList<>()); BakedVariant second = new ObjectVariantBaker().bake(id, new ObjectSource(tree, 0, TreeRoots.NONE), spec);
assertEquals(first, second); assertEquals(first, second);
assertEquals(id, ObjectVariantBaker.templateId(NAMESPACE, "trees/oak", spec, DataVersion.V26_3)); assertEquals(id, ObjectVariantBaker.templateId(NAMESPACE, "trees/oak", spec, DataVersion.V26_3));
@@ -58,7 +58,7 @@ public class ObjectVariantBakerTest {
block(0, 1, 0, "minecraft:cave_air"))); block(0, 1, 0, "minecraft:cave_air")));
BakedVariant variant = new ObjectVariantBaker() BakedVariant variant = new ObjectVariantBaker()
.bake("iris_export:object/rock/a", volume, VariantSpec.builder().build(), "rock", new ArrayList<>()); .bake("iris_export:object/rock/a", new ObjectSource(volume, 0, TreeRoots.NONE), VariantSpec.builder().build());
assertEquals(List.of("minecraft:stone"), states(variant.volume())); assertEquals(List.of("minecraft:stone"), states(variant.volume()));
assertEquals(2, variant.volume().sizeX()); assertEquals(2, variant.volume().sizeX());
@@ -66,6 +66,30 @@ public class ObjectVariantBakerTest {
assertEquals(0, variant.baseY()); assertEquals(0, variant.baseY());
} }
@Test
public void plantsWithNothingUnderThemAreLeftOut() {
TemplateVolume reef = new TemplateVolume(3, 4, 3, List.of(
block(1, 0, 1, "minecraft:sea_pickle[pickles=1,waterlogged=false]"),
block(0, 0, 0, "minecraft:tube_coral_block"),
block(0, 1, 0, "minecraft:tube_coral_block"),
block(0, 2, 0, "minecraft:sea_pickle[pickles=2,waterlogged=false]"),
block(2, 2, 2, "minecraft:sea_pickle[pickles=3,waterlogged=false]"),
block(2, 1, 0, "minecraft:tube_coral_fan[waterlogged=false]"),
block(1, 1, 0, "minecraft:tube_coral_wall_fan[facing=east,waterlogged=false]"),
block(1, 2, 0, "minecraft:tube_coral_wall_fan[facing=west,waterlogged=false]"),
block(2, 3, 2, "minecraft:tube_coral_block")));
BakedVariant variant = new ObjectVariantBaker()
.bake("iris_export:object/reef/a", new ObjectSource(reef, 0, TreeRoots.NONE), VariantSpec.builder().build());
Set<String> kept = new HashSet<>();
for (TemplateBlock block : variant.volume().blocks()) {
kept.add(block.x() + "," + block.y() + "," + block.z() + " " + block.state().name());
}
assertEquals(Set.of("1,0,1 minecraft:sea_pickle", "0,0,0 minecraft:tube_coral_block", "0,1,0 minecraft:tube_coral_block",
"0,2,0 minecraft:sea_pickle", "1,1,0 minecraft:tube_coral_wall_fan", "2,3,2 minecraft:tube_coral_block"), kept);
}
@Test @Test
public void quarterRotationMovesBlocksAndFacing() { public void quarterRotationMovesBlocksAndFacing() {
TemplateVolume volume = new TemplateVolume(3, 1, 3, List.of( TemplateVolume volume = new TemplateVolume(3, 1, 3, List.of(
@@ -73,7 +97,7 @@ public class ObjectVariantBakerTest {
block(1, 0, 0, "minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]"))); block(1, 0, 0, "minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]")));
VariantSpec spec = VariantSpec.builder().rotation(new VariantSpec.Rotation(0, 90, 0)).build(); VariantSpec spec = VariantSpec.builder().rotation(new VariantSpec.Rotation(0, 90, 0)).build();
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/s/a", volume, spec, "s", new ArrayList<>()); BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/s/a", new ObjectSource(volume, 0, TreeRoots.NONE), spec);
TemplateBlock stairs = variant.volume().blocks().stream() TemplateBlock stairs = variant.volume().blocks().stream()
.filter(block -> block.state().name().equals("minecraft:oak_stairs")).findFirst().orElseThrow(); .filter(block -> block.state().name().equals("minecraft:oak_stairs")).findFirst().orElseThrow();
@@ -89,7 +113,7 @@ public class ObjectVariantBakerTest {
block(0, 1, 0, "minecraft:oak_slab[type=bottom,waterlogged=false]"))); block(0, 1, 0, "minecraft:oak_slab[type=bottom,waterlogged=false]")));
VariantSpec spec = VariantSpec.builder().rotation(new VariantSpec.Rotation(180, 0, 0)).build(); VariantSpec spec = VariantSpec.builder().rotation(new VariantSpec.Rotation(180, 0, 0)).build();
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/f/a", volume, spec, "f", new ArrayList<>()); BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/f/a", new ObjectSource(volume, 0, TreeRoots.NONE), spec);
TemplateBlock slab = variant.volume().blocks().getFirst(); TemplateBlock slab = variant.volume().blocks().getFirst();
assertEquals("minecraft:oak_slab", slab.state().name()); assertEquals("minecraft:oak_slab", slab.state().name());
@@ -104,7 +128,7 @@ public class ObjectVariantBakerTest {
block(1, 1, 1, "minecraft:oak_log[axis=y]"))); block(1, 1, 1, "minecraft:oak_log[axis=y]")));
VariantSpec scaled = VariantSpec.builder().scale(2).build(); VariantSpec scaled = VariantSpec.builder().scale(2).build();
BakedVariant big = new ObjectVariantBaker().bake("iris_export:object/l/a", volume, scaled, "l", new ArrayList<>()); BakedVariant big = new ObjectVariantBaker().bake("iris_export:object/l/a", new ObjectSource(volume, 0, TreeRoots.NONE), scaled);
assertEquals(16, big.volume().blocks().size()); assertEquals(16, big.volume().blocks().size());
assertEquals(4, big.volume().sizeY()); assertEquals(4, big.volume().sizeY());
@@ -124,8 +148,8 @@ public class ObjectVariantBakerTest {
ObjectEdit half = new ObjectEdit(List.of(VanillaBlockState.parse("minecraft:stone_bricks")), false, ObjectEdit half = new ObjectEdit(List.of(VanillaBlockState.parse("minecraft:stone_bricks")), false,
List.of(new WeightedState(VanillaBlockState.parse("minecraft:mossy_stone_bricks"), 1)), 0.5); List.of(new WeightedState(VanillaBlockState.parse("minecraft:mossy_stone_bricks"), 1)), 0.5);
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/floor/a", floor, BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/floor/a", new ObjectSource(floor, 0, TreeRoots.NONE),
VariantSpec.builder().edits(List.of(always, half)).build(), "floor", new ArrayList<>()); VariantSpec.builder().edits(List.of(always, half)).build());
Set<String> names = new HashSet<>(); Set<String> names = new HashSet<>();
int mossy = 0; int mossy = 0;
@@ -141,8 +165,8 @@ public class ObjectVariantBakerTest {
@Test @Test
public void basesAlignAcrossVariants() { public void basesAlignAcrossVariants() {
BakedVariant low = new BakedVariant("iris_export:object/a/low", new TemplateVolume(1, 3, 1, List.of(block(0, 0, 0, "minecraft:stone"))), -2); BakedVariant low = new BakedVariant("iris_export:object/a/low", new TemplateVolume(1, 3, 1, List.of(block(0, 0, 0, "minecraft:stone"))), -2, TreeRoots.NONE);
BakedVariant flat = new BakedVariant("iris_export:object/a/flat", new TemplateVolume(1, 1, 1, List.of(block(0, 0, 0, "minecraft:stone"))), 0); BakedVariant flat = new BakedVariant("iris_export:object/a/flat", new TemplateVolume(1, 1, 1, List.of(block(0, 0, 0, "minecraft:stone"))), 0, TreeRoots.NONE);
List<BakedVariant> aligned = ObjectVariantBaker.alignBases(List.of(low, flat)); List<BakedVariant> aligned = ObjectVariantBaker.alignBases(List.of(low, flat));
@@ -154,6 +178,73 @@ public class ObjectVariantBakerTest {
assertFalse(aligned.get(1).volume().blocks().isEmpty()); assertFalse(aligned.get(1).volume().blocks().isEmpty());
} }
@Test
public void rootsSurviveAnUnchangedBake() {
TemplateVolume volume = new TemplateVolume(3, 2, 3, List.of(
block(1, 0, 1, "minecraft:oak_wood[axis=y]"),
block(2, 0, 1, "minecraft:oak_wood[axis=y]"),
block(1, 1, 1, "minecraft:oak_log[axis=y]")));
TreeRoots roots = new TreeRoots(List.of(block(1, 0, 1, "minecraft:oak_wood[axis=y]"),
block(2, 0, 1, "minecraft:oak_wood[axis=y]")), 3);
BakedVariant variant = new ObjectVariantBaker()
.bake("iris_export:object/tree/a", new ObjectSource(volume, 0, roots), VariantSpec.builder().build());
assertEquals(roots, variant.roots());
}
@Test
public void rootsTurnWithTheirBlocks() {
TemplateVolume volume = new TemplateVolume(3, 2, 3, List.of(
block(1, 0, 1, "minecraft:oak_wood[axis=y]"),
block(2, 0, 1, "minecraft:spruce_wood[axis=x]"),
block(1, 1, 1, "minecraft:oak_log[axis=y]")));
TreeRoots roots = new TreeRoots(List.of(block(2, 0, 1, "minecraft:spruce_wood[axis=x]")), 2);
VariantSpec spec = VariantSpec.builder().rotation(new VariantSpec.Rotation(0, 90, 0)).build();
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/tree/b", new ObjectSource(volume, 0, roots), spec);
TemplateBlock moved = variant.volume().blocks().stream()
.filter(block -> block.state().name().equals("minecraft:spruce_wood")).findFirst().orElseThrow();
assertEquals(1, variant.roots().cells().size());
TemplateBlock root = variant.roots().cells().getFirst();
assertEquals(List.of(moved.x(), moved.y(), moved.z()), List.of(root.x(), root.y(), root.z()));
assertEquals("z", root.state().properties().get("axis"));
assertEquals(2, variant.roots().reach());
}
@Test
public void scaledRootsKeepTheLowestCellPerColumnAndStretchTheirReach() {
TemplateVolume volume = new TemplateVolume(2, 2, 1, List.of(
block(0, 0, 0, "minecraft:oak_wood[axis=y]"),
block(0, 1, 0, "minecraft:oak_log[axis=y]"),
block(1, 1, 0, "minecraft:oak_log[axis=y]")));
TreeRoots roots = new TreeRoots(List.of(block(0, 0, 0, "minecraft:oak_wood[axis=y]")), 3);
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/tree/c", new ObjectSource(volume, 0, roots),
VariantSpec.builder().scale(2).build());
assertEquals(6, variant.roots().reach());
assertEquals(4, variant.roots().cells().size());
for (TemplateBlock cell : variant.roots().cells()) {
assertEquals(0, cell.y());
assertEquals("minecraft:oak_wood", cell.state().name());
}
}
@Test
public void alignedBasesLiftRoots() {
TreeRoots roots = new TreeRoots(List.of(block(0, 0, 0, "minecraft:oak_wood[axis=y]")), 2);
BakedVariant low = new BakedVariant("iris_export:object/r/low", new TemplateVolume(1, 3, 1, List.of(block(0, 0, 0, "minecraft:stone"))), -2, TreeRoots.NONE);
BakedVariant rooted = new BakedVariant("iris_export:object/r/rooted", new TemplateVolume(1, 1, 1,
List.of(block(0, 0, 0, "minecraft:oak_wood[axis=y]"))), 0, roots);
BakedVariant lifted = ObjectVariantBaker.alignBases(List.of(low, rooted)).get(1);
assertEquals(2, lifted.roots().cells().getFirst().y());
assertEquals(2, lifted.roots().reach());
}
private static TemplateVolume tree() { private static TemplateVolume tree() {
List<TemplateBlock> blocks = new ArrayList<>(); List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 0; y < 5; y++) { for (int y = 0; y < 5; y++) {
@@ -4,6 +4,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.TemplateBlock; import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume; import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState; import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.StageTestPacks;
import art.arcane.iris.pack.datapack.export.compile.decoration.GenerationStep; import art.arcane.iris.pack.datapack.export.compile.decoration.GenerationStep;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef; import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic; import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
@@ -21,9 +22,11 @@ import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -59,17 +62,244 @@ public class PlacementCompilerTest {
assertFalse(templates.get(0).getAsJsonObject().getAsJsonObject("data").has("rotations")); assertFalse(templates.get(0).getAsJsonObject().getAsJsonObject("data").has("rotations"));
JsonObject placed = placed(compiled); JsonObject placed = placed(compiled);
assertEquals(List.of("minecraft:random_chance", "minecraft:count", "minecraft:in_square", "minecraft:heightmap", assertEquals(List.of("minecraft:random_chance", "minecraft:count", "minecraft:in_square", "minecraft:heightmap",
"minecraft:block_predicate_filter", "minecraft:offset", "minecraft:biome"), types(placed)); "minecraft:block_predicate_filter", "minecraft:block_predicate_filter", "minecraft:offset", "minecraft:biome"), types(placed));
JsonArray modifiers = placed.getAsJsonArray("placement"); JsonArray modifiers = placed.getAsJsonArray("placement");
assertEquals(0.5, modifiers.get(0).getAsJsonObject().get("chance").getAsDouble(), 1e-6); assertEquals(0.5, modifiers.get(0).getAsJsonObject().get("chance").getAsDouble(), 1e-6);
assertEquals(3, modifiers.get(1).getAsJsonObject().get("count").getAsInt()); assertEquals(3, modifiers.get(1).getAsJsonObject().get("count").getAsInt());
assertEquals("WORLD_SURFACE_WG", modifiers.get(3).getAsJsonObject().get("heightmap").getAsString()); assertEquals("OCEAN_FLOOR_WG", modifiers.get(3).getAsJsonObject().get("heightmap").getAsString());
assertEquals(1, modifiers.get(5).getAsJsonObject().get("y").getAsInt()); assertEquals(1, modifiers.get(6).getAsJsonObject().get("y").getAsInt());
JsonObject support = modifiers.get(4).getAsJsonObject().getAsJsonObject("predicate"); assertTrue(StageTestPacks.ofType(placed, "minecraft:volume_match").isEmpty());
assertEquals("minecraft:volume_match", support.get("type").getAsString());
assertTrue(diagnostics.isEmpty()); assertTrue(diagnostics.isEmpty());
} }
@Test
public void surfaceObjectsStandOnTerrainInsteadOfCanopiesOrWater() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
JsonObject placed = placed(compiler(DataVersion.V26_3).compile(input(placement, objects(5), List.of()), new ArrayList<>()));
JsonObject filter = filters(placed).getFirst();
assertEquals("OCEAN_FLOOR_WG", StageTestPacks.ofType(placed, "minecraft:heightmap").getFirst().get("heightmap").getAsString());
List<String> rejected = new ArrayList<>();
for (JsonObject negated : StageTestPacks.ofType(filter, "minecraft:not")) {
JsonObject inner = negated.getAsJsonObject("predicate");
rejected.add(inner.has("tag") ? inner.get("tag").getAsString() : inner.get("type").getAsString());
}
assertTrue(rejected.toString(), rejected.contains("minecraft:leaves"));
assertTrue(rejected.toString(), rejected.contains("minecraft:logs"));
assertTrue(rejected.toString(), rejected.contains("minecraft:matching_fluids"));
assertFalse(StageTestPacks.ofType(filter, "minecraft:solid").isEmpty());
}
@Test
public void supportFollowsTheObjectBaseInsteadOfAFlatPlane() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setSurfaceSupportBuffer(1);
placement.setSurfaceSupportDepth(2);
TemplateBlock trunk = new TemplateBlock(4, 0, 4, VanillaBlockState.parse("minecraft:oak_log[axis=y]"), null);
TemplateBlock crown = new TemplateBlock(0, 5, 0, VanillaBlockState.parse("minecraft:oak_leaves"), null);
BakedVariant tree = new BakedVariant(NAMESPACE + ":object/tree/v", new TemplateVolume(9, 6, 9, List.of(trunk, crown)), 0, TreeRoots.NONE);
List<ObjectTemplates> objects = List.of(new ObjectTemplates("tree", List.of(new TemplateChoice(tree, List.of(0, 1, 2, 3), 1))));
JsonObject support = filters(placed(compiler(DataVersion.V26_3).compile(input(placement, objects, List.of()), new ArrayList<>()))).get(1);
int reach = 0;
int depth = 0;
for (JsonObject solid : StageTestPacks.ofType(support, "minecraft:solid")) {
JsonArray offset = solid.getAsJsonArray("offset");
reach = Math.max(reach, Math.max(Math.abs(offset.get(0).getAsInt()), Math.abs(offset.get(2).getAsInt())));
depth = Math.min(depth, offset.get(1).getAsInt());
}
assertEquals(1, reach);
assertEquals(-16, depth);
assertFalse(StageTestPacks.ofType(support, "minecraft:any_of").isEmpty());
}
@Test
public void supportAcceptsSteepGroundAnywhereInTheScanWindow() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setSurfaceSupportBuffer(2);
placement.setSurfaceSupportDepth(2);
TemplateBlock trunk = new TemplateBlock(0, 0, 4, VanillaBlockState.parse("minecraft:spruce_log[axis=y]"), null);
BakedVariant tree = new BakedVariant(NAMESPACE + ":object/tree/v", new TemplateVolume(9, 20, 9, List.of(trunk)), 0, TreeRoots.NONE);
List<ObjectTemplates> objects = List.of(new ObjectTemplates("tree", List.of(new TemplateChoice(tree, List.of(0, 1, 2, 3), 1))));
JsonObject support = filters(placed(compiler(DataVersion.V26_3).compile(input(placement, objects, List.of()), new ArrayList<>()))).get(1);
Set<String> corners = new HashSet<>();
for (JsonObject solid : StageTestPacks.ofType(support, "minecraft:solid")) {
JsonArray offset = solid.getAsJsonArray("offset");
corners.add(offset.get(0).getAsInt() + "," + offset.get(2).getAsInt());
}
assertEquals(Set.of("-6,-6", "-6,6", "6,-6", "6,6"), corners);
Set<String> steepSlope = new HashSet<>();
Set<String> thinLedge = new HashSet<>();
for (String corner : corners) {
for (int y = -12; y >= -40; y--) {
steepSlope.add(cell(corner, y));
}
thinLedge.add(cell(corner, -12));
}
JsonObject predicate = support.getAsJsonObject("predicate");
assertTrue("ground 11 blocks below every corner must support the object", supported(predicate, steepSlope));
assertFalse("a one block ledge is thinner than surfaceSupportDepth", supported(predicate, thinLedge));
assertFalse("open air below every corner must not support the object", supported(predicate, Set.of()));
}
@Test
public void stiltedObjectsRelyOnTheirStiltsInsteadOfCornerSupport() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.FAST_MIN_STILT, 1, 1);
PlacementInput input = new PlacementInput(placement, PlacementInput.Anchor.SURFACE, true, objects(25), List.of(),
List.of(VanillaBlockState.parse("minecraft:dark_oak_wood[axis=y]")), "willow");
List<JsonObject> filters = filters(placed(compiler(DataVersion.V26_3).compile(input, new ArrayList<>())));
assertEquals(1, filters.size());
assertFalse(StageTestPacks.ofType(filters.getFirst(), "minecraft:matching_block_tag").isEmpty());
}
@Test
public void slopeConditionRejectsSteepAnchors() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setRequireSurfaceSupport(false);
placement.getSlopeCondition().setMaximumSlope(1.5);
List<ExportDiagnostic> diagnostics = new ArrayList<>();
List<JsonObject> filters = filters(placed(compiler(DataVersion.V26_3).compile(input(placement, objects(5), List.of()), diagnostics)));
assertTrue(diagnostics.stream().anyMatch(diagnostic -> diagnostic.limit() == ExportLimit.SLOPE));
assertEquals(2, filters.size());
JsonObject slope = filters.get(1).getAsJsonObject("predicate");
Set<String> flat = new HashSet<>();
Set<String> cliff = new HashSet<>();
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
for (int y = -20; y <= -1; y++) {
flat.add(x + "," + y + "," + z);
cliff.add(x + "," + y + "," + z);
}
}
}
for (int y = 0; y <= 4; y++) {
cliff.add("3," + y + ",0");
}
assertTrue("flat ground must pass the slope limit", supported(slope, flat));
assertFalse("a wall 3 blocks higher 3 blocks away is steeper than slope 1.5", supported(slope, cliff));
}
@Test
public void forcePlaceIgnoresSlopeCondition() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setForcePlace(true);
placement.getSlopeCondition().setMinimumSlope(2);
List<JsonObject> filters = filters(placed(compiler(DataVersion.V26_3).compile(input(placement, objects(5), List.of()), new ArrayList<>())));
assertEquals(1, filters.size());
}
private static String cell(String corner, int y) {
String[] parts = corner.split(",");
return parts[0] + "," + y + "," + parts[1];
}
private static boolean supported(JsonObject predicate, Set<String> solids) {
String type = predicate.get("type").getAsString();
return switch (type) {
case "minecraft:solid" -> {
JsonArray offset = predicate.getAsJsonArray("offset");
yield solids.contains(offset.get(0).getAsInt() + "," + offset.get(1).getAsInt() + "," + offset.get(2).getAsInt());
}
case "minecraft:all_of" -> {
for (JsonElement child : predicate.getAsJsonArray("predicates")) {
if (!supported(child.getAsJsonObject(), solids)) {
yield false;
}
}
yield true;
}
case "minecraft:any_of" -> {
for (JsonElement child : predicate.getAsJsonArray("predicates")) {
if (supported(child.getAsJsonObject(), solids)) {
yield true;
}
}
yield false;
}
case "minecraft:not" -> !supported(predicate.getAsJsonObject("predicate"), solids);
default -> throw new AssertionError("Unexpected support predicate " + type);
};
}
@Test
public void underwaterObjectsNeedWaterAndStayBelowTheSurface() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setUnderwater(true);
placement.setTranslate(new IrisObjectTranslate().setY(2));
JsonObject placed = placed(compiler(DataVersion.V26_3).compile(input(placement, columns(7), List.of()), new ArrayList<>()));
List<JsonObject> filters = filters(placed);
assertEquals(2, filters.size());
assertTrue(StageTestPacks.ofType(filters.getFirst(), "minecraft:not").stream()
.noneMatch(negated -> !StageTestPacks.ofType(negated, "minecraft:matching_fluids").isEmpty()));
JsonObject water = StageTestPacks.ofType(filters.getFirst(), "minecraft:matching_fluids").getFirst();
assertFalse(water.toString(), water.has("offset"));
assertEquals(DIMENSION.fluidHeight() - 3 * (7 / 2) - 2, ceiling(placed));
}
@Test
public void underwaterObjectsOfDifferentHeightsKeepTheirOwnDepth() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 0.6, 2);
placement.setUnderwater(true);
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(input(placement, columns(4, 9, 4), List.of()), new ArrayList<>());
assertEquals(2, compiled.placedFeatures().size());
List<Integer> ceilings = new ArrayList<>();
double perChunk = 0;
for (JsonResource resource : compiled.resources()) {
if (resource.path().contains("worldgen/placed_feature/")) {
JsonObject placed = resource.json().getAsJsonObject();
ceilings.add(ceiling(placed));
perChunk += expectedPerChunk(placed);
}
}
ceilings.sort(Integer::compareTo);
assertEquals(List.of(DIMENSION.fluidHeight() - 3 * (9 / 2), DIMENSION.fluidHeight() - 3 * (4 / 2)), ceilings);
assertEquals(0.6 * 2, perChunk, 1e-6);
}
@Test
public void onwaterObjectsStandOnTheWaterSurface() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setOnwater(true);
JsonObject placed = placed(compiler(DataVersion.V26_3).compile(input(placement, objects(5), List.of()), new ArrayList<>()));
assertEquals("WORLD_SURFACE_WG", StageTestPacks.ofType(placed, "minecraft:heightmap").getFirst().get("heightmap").getAsString());
List<JsonObject> filters = filters(placed);
assertEquals(1, filters.size());
assertTrue(StageTestPacks.ofType(filters.getFirst(), "minecraft:solid").isEmpty());
JsonObject water = StageTestPacks.ofType(filters.getFirst(), "minecraft:matching_fluids").getFirst();
assertEquals("[0,-1,0]", water.get("offset").toString());
assertTrue(StageTestPacks.ofType(placed, "minecraft:not").isEmpty());
}
@Test
public void onwaterPaintDropsOntoTheWaterSurface() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.PAINT, 1, 1);
placement.setOnwater(true);
placement.setTranslate(new IrisObjectTranslate().setY(1));
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(input(placement, objects(1), List.of()), new ArrayList<>());
JsonObject gravity = feature(compiled, "worldgen/feature/").getAsJsonObject("processors").getAsJsonArray("processors").get(0).getAsJsonObject();
assertEquals("WORLD_SURFACE_WG", gravity.get("heightmap").getAsString());
assertEquals(0, gravity.get("offset").getAsInt());
assertEquals("WORLD_SURFACE_WG", StageTestPacks.ofType(placed(compiled), "minecraft:heightmap").getFirst().get("heightmap").getAsString());
}
@Test @Test
public void wideObjectBecomesSinglePieceStructure() { public void wideObjectBecomesSinglePieceStructure() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 0.02, 1); IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 0.02, 1);
@@ -112,7 +342,8 @@ public class PlacementCompilerTest {
@Test @Test
public void writeWindowNeverExceeded() { public void writeWindowNeverExceeded() {
DataVersion target = DataVersion.V26_3; DataVersion target = DataVersion.V26_3;
for (ObjectPlaceMode mode : List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.PAINT)) { for (ObjectPlaceMode mode : List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.PAINT,
ObjectPlaceMode.MIN_HEIGHT, ObjectPlaceMode.FAST_MAX_HEIGHT)) {
for (int footprint = 1; footprint <= 40; footprint++) { for (int footprint = 1; footprint <= 40; footprint++) {
for (int translate : List.of(0, 3, -5, 12)) { for (int translate : List.of(0, 3, -5, 12)) {
IrisObjectPlacement placement = placement(mode, 1, 1); IrisObjectPlacement placement = placement(mode, 1, 1);
@@ -160,13 +391,13 @@ public class PlacementCompilerTest {
assertEquals("minecraft:rule", processors.get(0).getAsJsonObject().get("processor_type").getAsString()); assertEquals("minecraft:rule", processors.get(0).getAsJsonObject().get("processor_type").getAsString());
JsonObject gravity = processors.get(1).getAsJsonObject(); JsonObject gravity = processors.get(1).getAsJsonObject();
assertEquals("minecraft:gravity", gravity.get("processor_type").getAsString()); assertEquals("minecraft:gravity", gravity.get("processor_type").getAsString());
assertEquals("WORLD_SURFACE_WG", gravity.get("heightmap").getAsString()); assertEquals("OCEAN_FLOOR_WG", gravity.get("heightmap").getAsString());
assertEquals(-1, gravity.get("offset").getAsInt()); assertEquals(-1, gravity.get("offset").getAsInt());
} }
@Test @Test
public void stiltsUseOverlayPillarsOn26_3() { public void stiltsUseOverlayPillarsOn26_3() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.STILT, 1, 1); IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_STILT, 1, 1);
PlacementInput input = new PlacementInput(placement, PlacementInput.Anchor.SURFACE, false, objects(20), List.of(), PlacementInput input = new PlacementInput(placement, PlacementInput.Anchor.SURFACE, false, objects(20), List.of(),
List.of(VanillaBlockState.parse("minecraft:oak_log[axis=y]")), "hut"); List.of(VanillaBlockState.parse("minecraft:oak_log[axis=y]")), "hut");
@@ -210,6 +441,254 @@ public class PlacementCompilerTest {
assertEquals(first.resources().toString(), second.resources().toString()); assertEquals(first.resources().toString(), second.resources().toString());
} }
@Test
public void minHeightScansDownUntilTheWholeFootprintHasGround() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.MIN_HEIGHT, 1, 1);
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(input(placement, objects(9), List.of()), new ArrayList<>());
JsonObject placed = placed(compiled);
assertEquals(List.of("minecraft:in_square", "minecraft:heightmap", "minecraft:block_predicate_filter",
"minecraft:block_predicate_filter", "minecraft:biome"), types(placed));
JsonObject feature = feature(compiled, "worldgen/feature/");
assertEquals("minecraft:weighted_random_selector", feature.get("type").getAsString());
JsonArray branches = feature.getAsJsonArray("features");
assertEquals(1, branches.size());
JsonObject branch = branches.get(0).getAsJsonObject().getAsJsonObject("data");
assertEquals("minecraft:template", branch.getAsJsonObject("feature").get("type").getAsString());
assertEquals(List.of("minecraft:environment_scan", "minecraft:offset"), types(branch));
JsonObject scan = branch.getAsJsonArray("placement").get(0).getAsJsonObject();
assertEquals("down", scan.get("direction_of_search").getAsString());
assertEquals(32, scan.get("max_steps").getAsInt());
assertEquals(List.of("-4,-1,-4..4,-1,4"), boxes(scan));
JsonObject match = StageTestPacks.ofType(scan, "minecraft:volume_match").getFirst().getAsJsonObject("match");
assertFalse(StageTestPacks.ofType(match, "minecraft:solid").isEmpty());
assertFalse(StageTestPacks.ofType(match, "minecraft:matching_fluids").isEmpty());
assertEquals(-1, branch.getAsJsonArray("placement").get(1).getAsJsonObject().get("y").getAsInt());
}
@Test
public void maxHeightScansUpUntilEveryColumnIsClear() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.MAX_HEIGHT, 1, 1);
placement.setUnderwater(true);
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(input(placement, objects(9), List.of()), new ArrayList<>());
JsonObject scan = branchScan(feature(compiled, "worldgen/feature/"), 0);
assertEquals("up", scan.get("direction_of_search").getAsString());
assertEquals(List.of("-4,0,-4..4,0,4"), boxes(scan));
JsonObject match = StageTestPacks.ofType(scan, "minecraft:volume_match").getFirst().getAsJsonObject("match");
assertEquals("minecraft:not", match.get("type").getAsString());
assertTrue(StageTestPacks.ofType(match, "minecraft:matching_fluids").isEmpty());
}
@Test
public void fastHeightModesProbeTheRuntimeSamplePoints() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.FAST_MIN_HEIGHT, 1, 1);
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(input(placement, objects(9), List.of()), new ArrayList<>());
JsonObject scan = branchScan(feature(compiled, "worldgen/feature/"), 0);
assertEquals(List.of("-4,-1,-4..-4,-1,-4", "-4,-1,1..-4,-1,1", "1,-1,-4..1,-1,-4", "1,-1,1..1,-1,1"), boxes(scan));
}
@Test
public void heightScanCoversEveryAllowedQuarter() {
TemplateBlock block = new TemplateBlock(0, 0, 0, VanillaBlockState.parse("minecraft:stone"), null);
BakedVariant wall = new BakedVariant(NAMESPACE + ":object/wall/v", new TemplateVolume(5, 4, 9, List.of(block)), 0, TreeRoots.NONE);
IrisObjectPlacement placement = placement(ObjectPlaceMode.MIN_HEIGHT, 1, 1);
JsonObject turning = feature(compiler(DataVersion.V26_3).compile(input(placement, List.of(new ObjectTemplates("wall",
List.of(new TemplateChoice(wall, List.of(0, 1, 2, 3), 1)))), List.of()), new ArrayList<>()), "worldgen/feature/");
JsonObject fixed = feature(compiler(DataVersion.V26_3).compile(input(placement, List.of(new ObjectTemplates("wall",
List.of(new TemplateChoice(wall, List.of(0), 1)))), List.of()), new ArrayList<>()), "worldgen/feature/");
assertEquals(List.of("-2,-1,-4..2,-1,4", "-4,-1,-2..4,-1,2"), boxes(branchScan(turning, 0)));
assertEquals(List.of("-2,-1,-4..2,-1,4"), boxes(branchScan(fixed, 0)));
}
@Test
public void everyTemplateAnchorsOnItsOwnFootprint() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.MIN_HEIGHT, 1, 1);
JsonObject feature = feature(compiler(DataVersion.V26_3).compile(input(placement, objects(5, 9), List.of()), new ArrayList<>()),
"worldgen/feature/");
JsonArray branches = feature.getAsJsonArray("features");
assertEquals(2, branches.size());
assertEquals(branches.get(0).getAsJsonObject().get("weight"), branches.get(1).getAsJsonObject().get("weight"));
assertEquals(List.of("-2,-1,-2..2,-1,2"), boxes(branchScan(feature, 0)));
assertEquals(List.of("-4,-1,-4..4,-1,4"), boxes(branchScan(feature, 1)));
}
@Test
public void maxStiltsAnchorHighThenStilt() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.STILT, 1, 1);
PlacementInput input = new PlacementInput(placement, PlacementInput.Anchor.SURFACE, false, objects(20), List.of(),
List.of(VanillaBlockState.parse("minecraft:oak_log[axis=y]")), "hut");
JsonObject feature = feature(compiler(DataVersion.V26_3).compile(input, new ArrayList<>()), "worldgen/feature/");
JsonObject branch = feature.getAsJsonArray("features").get(0).getAsJsonObject().getAsJsonObject("data");
assertEquals("up", branchScan(feature, 0).get("direction_of_search").getAsString());
JsonObject stilts = branch.getAsJsonObject("feature");
assertEquals("minecraft:overlay", stilts.get("type").getAsString());
assertEquals(5, stilts.getAsJsonArray("features").size());
assertEquals("minecraft:single_block_pillar", stilts.getAsJsonArray("features").get(1).getAsJsonObject()
.getAsJsonObject("feature").get("type").getAsString());
}
@Test
public void groundedTreesExtendRootsToTheTerrain() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(tree(placement, rootedTree()), new ArrayList<>());
JsonObject feature = feature(compiled, "worldgen/feature/");
assertEquals("minecraft:weighted_random_selector", feature.get("type").getAsString());
JsonArray branches = feature.getAsJsonArray("features");
assertEquals(4, branches.size());
List<String> rotations = new ArrayList<>();
List<String> roots = new ArrayList<>();
List<String> gaps = new ArrayList<>();
String pillar = null;
for (JsonElement element : branches) {
assertEquals(branches.get(0).getAsJsonObject().get("weight"), element.getAsJsonObject().get("weight"));
JsonObject branch = element.getAsJsonObject().getAsJsonObject("data");
assertEquals(List.of("minecraft:offset", "minecraft:block_predicate_filter"), types(branch));
gaps.addAll(boxes(branch.getAsJsonArray("placement").get(1)));
JsonObject overlay = branch.getAsJsonObject("feature");
assertEquals("minecraft:overlay", overlay.get("type").getAsString());
JsonArray parts = overlay.getAsJsonArray("features");
assertEquals(2, parts.size());
JsonObject template = parts.get(0).getAsJsonObject().getAsJsonObject("feature");
rotations.add(template.getAsJsonArray("templates").get(0).getAsJsonObject().getAsJsonObject("data")
.getAsJsonArray("rotations").get(0).getAsString());
JsonObject root = parts.get(1).getAsJsonObject();
pillar = root.get("feature").getAsString();
JsonObject offset = root.getAsJsonArray("placement").get(0).getAsJsonObject();
roots.add(offset.get("x").getAsInt() + "," + offset.get("y").getAsInt() + "," + offset.get("z").getAsInt());
}
assertEquals(List.of("none", "counterclockwise_90", "180", "clockwise_90"), rotations);
assertEquals(List.of("1,-1,0", "0,-1,-1", "-1,-1,0", "0,-1,1"), roots);
assertEquals(List.of("1,-4,0..1,-1,0", "0,-4,-1..0,-1,-1", "-1,-4,0..-1,-1,0", "0,-4,1..0,-1,1"), gaps);
JsonObject shared = resourceAt(compiled, pillar);
JsonObject gated = shared.getAsJsonArray("features").get(0).getAsJsonObject();
assertEquals("minecraft:block_predicate_filter", gated.getAsJsonArray("placement").get(0).getAsJsonObject().get("type").getAsString());
JsonObject column = gated.getAsJsonObject("feature");
assertEquals("minecraft:single_block_pillar", column.get("type").getAsString());
assertEquals("down", column.get("direction").getAsString());
assertEquals("minecraft:spruce_wood", column.getAsJsonObject("block").get("id").getAsString());
JsonObject cap = column.getAsJsonObject("cap_feature");
assertEquals("minecraft:simple_block", cap.getAsJsonObject("feature").get("type").getAsString());
assertEquals(-1, cap.getAsJsonArray("placement").get(0).getAsJsonObject().get("y").getAsInt());
}
@Test
public void groundedRotationBranchesKeepTheTemplateOdds() {
BakedVariant turning = rootedTree().getFirst().choices().getFirst().variant();
BakedVariant fixed = new BakedVariant(NAMESPACE + ":object/tree/fixed", turning.volume(), 0, turning.roots());
List<ObjectTemplates> objects = List.of(new ObjectTemplates("tree", List.of(
new TemplateChoice(turning, List.of(0, 1, 2, 3), 1),
new TemplateChoice(fixed, List.of(2), 1))));
JsonObject feature = feature(compiler(DataVersion.V26_3).compile(tree(placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1), objects),
new ArrayList<>()), "worldgen/feature/");
List<Integer> weights = new ArrayList<>();
for (JsonElement branch : feature.getAsJsonArray("features")) {
weights.add(branch.getAsJsonObject().get("weight").getAsInt());
}
assertEquals(List.of(1, 1, 1, 1, 4), weights);
}
@Test
public void groundedTreesReplaceStiltsWithRoots() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_STILT, 1, 1);
JsonObject feature = feature(compiler(DataVersion.V26_3).compile(tree(placement, rootedTree()), new ArrayList<>()), "worldgen/feature/");
for (JsonElement element : feature.getAsJsonArray("features")) {
JsonObject overlay = element.getAsJsonObject().getAsJsonObject("data").getAsJsonObject("feature");
assertTrue(StageTestPacks.ofType(overlay, "minecraft:cuboid").isEmpty());
assertEquals(2, overlay.getAsJsonArray("features").size());
}
}
@Test
public void forcedTreesKeepTheirBuiltRoots() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 1, 1);
placement.setForcePlace(true);
JsonObject feature = feature(compiler(DataVersion.V26_3).compile(tree(placement, rootedTree()), new ArrayList<>()), "worldgen/feature/");
assertEquals("minecraft:template", feature.get("type").getAsString());
}
@Test
public void rootedHeightScansStayInsideTheFeatureWindow() {
IrisObjectPlacement placement = placement(ObjectPlaceMode.FAST_MAX_HEIGHT, 1, 1);
placement.setTranslate(new IrisObjectTranslate().setX(3).setZ(-3));
List<ObjectTemplates> objects = rootedTree();
CompiledPlacement compiled = compiler(DataVersion.V26_3).compile(tree(placement, objects), new ArrayList<>());
assertWithinWindow(compiled, objects, "rooted fast max");
}
private static List<ObjectTemplates> rootedTree() {
List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 1; y < 4; y++) {
blocks.add(new TemplateBlock(2, y, 2, VanillaBlockState.parse("minecraft:oak_log[axis=y]"), null));
}
TemplateBlock root = new TemplateBlock(3, 0, 2, VanillaBlockState.parse("minecraft:spruce_wood[axis=y]"), null);
blocks.add(root);
BakedVariant variant = new BakedVariant(NAMESPACE + ":object/tree/rooted", new TemplateVolume(5, 4, 5, blocks), 0,
new TreeRoots(List.of(root), 3));
return List.of(new ObjectTemplates("tree", List.of(new TemplateChoice(variant, List.of(0, 1, 2, 3), 1))));
}
private static PlacementInput tree(IrisObjectPlacement placement, List<ObjectTemplates> objects) {
return new PlacementInput(placement, PlacementInput.Anchor.SURFACE, true, objects, List.of(),
List.of(VanillaBlockState.parse("minecraft:oak_log[axis=y]")), "tree");
}
private static JsonObject branchScan(JsonObject selector, int index) {
JsonObject branch = selector.getAsJsonArray("features").get(index).getAsJsonObject().getAsJsonObject("data");
return StageTestPacks.ofType(branch, "minecraft:environment_scan").getFirst();
}
private static List<String> boxes(JsonElement root) {
List<String> boxes = new ArrayList<>();
for (JsonObject volume : StageTestPacks.ofType(root, "minecraft:volume_match")) {
JsonArray min = volume.getAsJsonArray("min");
JsonArray max = volume.getAsJsonArray("max");
boxes.add(min.get(0).getAsInt() + "," + min.get(1).getAsInt() + "," + min.get(2).getAsInt() + ".."
+ max.get(0).getAsInt() + "," + max.get(1).getAsInt() + "," + max.get(2).getAsInt());
}
return boxes;
}
private static JsonObject resourceAt(CompiledPlacement compiled, String id) {
String path = "/worldgen/feature/" + id.substring(NAMESPACE.length() + 1) + ".json";
for (JsonResource resource : compiled.resources()) {
if (resource.path().endsWith(path)) {
return resource.json().getAsJsonObject();
}
}
throw new AssertionError("No resource for " + id);
}
private static List<JsonObject> filters(JsonObject placed) {
List<JsonObject> filters = new ArrayList<>();
for (JsonElement element : placed.getAsJsonArray("placement")) {
if (element.getAsJsonObject().get("type").getAsString().equals("minecraft:block_predicate_filter")) {
filters.add(element.getAsJsonObject());
}
}
return filters;
}
private static void assertWithinWindow(CompiledPlacement compiled, List<ObjectTemplates> objects, String label) { private static void assertWithinWindow(CompiledPlacement compiled, List<ObjectTemplates> objects, String label) {
Map<String, Integer> footprints = new HashMap<>(); Map<String, Integer> footprints = new HashMap<>();
for (ObjectTemplates object : objects) { for (ObjectTemplates object : objects) {
@@ -217,10 +696,23 @@ public class PlacementCompilerTest {
footprints.put(choice.variant().id(), choice.variant().volume().footprint()); footprints.put(choice.variant().id(), choice.variant().volume().footprint());
} }
} }
JsonObject placed = placed(compiled); int[] offset = probeWindow(placed(compiled).getAsJsonArray("placement"), 0, 0, label);
int offsetX = 0; JsonObject feature = feature(compiled, null);
int offsetZ = 0; if (!feature.get("type").getAsString().equals("minecraft:weighted_random_selector")) {
for (JsonElement element : placed.getAsJsonArray("placement")) { assertFeatureWithinWindow(feature, footprints, offset[0], offset[1], label);
return;
}
for (JsonElement entry : feature.getAsJsonArray("features")) {
JsonObject branch = entry.getAsJsonObject().getAsJsonObject("data");
int[] branchOffset = probeWindow(branch.getAsJsonArray("placement"), offset[0], offset[1], label);
assertFeatureWithinWindow(branch.getAsJsonObject("feature"), footprints, branchOffset[0], branchOffset[1], label);
}
}
private static int[] probeWindow(JsonArray modifiers, int startX, int startZ, String label) {
int offsetX = startX;
int offsetZ = startZ;
for (JsonElement element : modifiers) {
JsonObject modifier = element.getAsJsonObject(); JsonObject modifier = element.getAsJsonObject();
String type = modifier.get("type").getAsString(); String type = modifier.get("type").getAsString();
if (type.equals("minecraft:offset")) { if (type.equals("minecraft:offset")) {
@@ -229,15 +721,26 @@ public class PlacementCompilerTest {
} else if (type.equals("minecraft:random_offset")) { } else if (type.equals("minecraft:random_offset")) {
offsetX += modifier.get("xz_spread").getAsInt(); offsetX += modifier.get("xz_spread").getAsInt();
offsetZ += modifier.get("xz_spread").getAsInt(); offsetZ += modifier.get("xz_spread").getAsInt();
} else if (type.equals("minecraft:block_predicate_filter")) { } else if (type.equals("minecraft:block_predicate_filter") || type.equals("minecraft:environment_scan")) {
JsonObject predicate = modifier.getAsJsonObject("predicate"); for (JsonObject probe : StageTestPacks.find(modifier, object -> object.has("offset") || object.has("min"))) {
if (predicate.get("type").getAsString().equals("minecraft:volume_match")) { for (String key : List.of("offset", "min", "max")) {
assertRange(label + " support", predicate.getAsJsonArray("min").get(0).getAsInt() + offsetX, if (!probe.has(key)) {
predicate.getAsJsonArray("max").get(0).getAsInt() + offsetX); continue;
}
JsonArray vector = probe.getAsJsonArray(key);
for (int axis = 0; axis < 3; axis++) {
assertTrue(label + " predicate " + key + " " + vector, Math.abs(vector.get(axis).getAsInt()) <= 16);
}
assertRange(label + " probe x", vector.get(0).getAsInt() + offsetX, vector.get(0).getAsInt() + offsetX);
assertRange(label + " probe z", vector.get(2).getAsInt() + offsetZ, vector.get(2).getAsInt() + offsetZ);
}
} }
} }
} }
JsonObject feature = feature(compiled, null); return new int[]{offsetX, offsetZ};
}
private static void assertFeatureWithinWindow(JsonObject feature, Map<String, Integer> footprints, int offsetX, int offsetZ, String label) {
String type = feature.get("type").getAsString(); String type = feature.get("type").getAsString();
JsonObject body = feature.has("config") ? feature.getAsJsonObject("config") : feature; JsonObject body = feature.has("config") ? feature.getAsJsonObject("config") : feature;
if (type.equals("minecraft:fossil")) { if (type.equals("minecraft:fossil")) {
@@ -255,16 +758,27 @@ public class PlacementCompilerTest {
assertNotNull(label, footprint); assertNotNull(label, footprint);
assertTemplate(label, footprint, offsetX, offsetZ); assertTemplate(label, footprint, offsetX, offsetZ);
} }
if (type.equals("minecraft:overlay")) { if (!type.equals("minecraft:overlay")) {
JsonArray features = body.getAsJsonArray("features"); return;
for (int index = 1; index < features.size(); index++) { }
JsonArray tile = features.get(index).getAsJsonObject().getAsJsonArray("placement"); JsonArray features = body.getAsJsonArray("features");
int tileX = tile.get(0).getAsJsonObject().get("x").getAsInt() + offsetX; for (int index = 1; index < features.size(); index++) {
int tileZ = tile.get(0).getAsJsonObject().get("z").getAsInt() + offsetZ; JsonArray part = features.get(index).getAsJsonObject().getAsJsonArray("placement");
int size = tile.get(1).getAsJsonObject().get("xz_size").getAsInt(); int partX = offsetX;
assertRange(label + " stilt tile x", tileX, tileX + size); int partZ = offsetZ;
assertRange(label + " stilt tile z", tileZ, tileZ + size); int size = 0;
for (JsonElement element : part) {
JsonObject modifier = element.getAsJsonObject();
String modifierType = modifier.get("type").getAsString();
if (modifierType.equals("minecraft:offset")) {
partX += modifier.get("x").getAsInt();
partZ += modifier.get("z").getAsInt();
} else if (modifierType.equals("minecraft:cuboid")) {
size = modifier.get("xz_size").getAsInt();
}
} }
assertRange(label + " part x", partX, partX + size);
assertRange(label + " part z", partZ, partZ + size);
} }
} }
@@ -378,12 +892,35 @@ public class PlacementCompilerTest {
int footprint = footprints[index]; int footprint = footprints[index];
TemplateBlock block = new TemplateBlock(0, 0, 0, VanillaBlockState.parse("minecraft:stone"), null); TemplateBlock block = new TemplateBlock(0, 0, 0, VanillaBlockState.parse("minecraft:stone"), null);
BakedVariant variant = new BakedVariant(NAMESPACE + ":object/o" + index + "/v" + footprint, BakedVariant variant = new BakedVariant(NAMESPACE + ":object/o" + index + "/v" + footprint,
new TemplateVolume(footprint, 4, footprint, List.of(block)), 0); new TemplateVolume(footprint, 4, footprint, List.of(block)), 0, TreeRoots.NONE);
objects.add(new ObjectTemplates("o" + index, List.of(new TemplateChoice(variant, List.of(0, 1, 2, 3), 1)))); objects.add(new ObjectTemplates("o" + index, List.of(new TemplateChoice(variant, List.of(0, 1, 2, 3), 1))));
} }
return objects; return objects;
} }
private static List<ObjectTemplates> columns(int... heights) {
List<ObjectTemplates> objects = new ArrayList<>();
for (int index = 0; index < heights.length; index++) {
List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 1; y <= heights[index]; y++) {
blocks.add(new TemplateBlock(1, y, 1, VanillaBlockState.parse("minecraft:tube_coral_block"), null));
}
BakedVariant variant = new BakedVariant(NAMESPACE + ":object/c" + index + "/h" + heights[index],
new TemplateVolume(3, heights[index] + 1, 3, blocks), 0, TreeRoots.NONE);
objects.add(new ObjectTemplates("c" + index, List.of(new TemplateChoice(variant, List.of(0, 1, 2, 3), 1))));
}
return objects;
}
private static int ceiling(JsonObject placed) {
List<JsonObject> ranges = new ArrayList<>();
for (JsonObject filter : filters(placed)) {
ranges.addAll(StageTestPacks.ofType(filter, "minecraft:height_range"));
}
assertEquals(placed.toString(), 1, ranges.size());
return ranges.getFirst().getAsJsonObject("max_inclusive").get("absolute").getAsInt();
}
private static JsonObject rule() { private static JsonObject rule() {
JsonObject rule = new JsonObject(); JsonObject rule = new JsonObject();
rule.addProperty("processor_type", "minecraft:rule"); rule.addProperty("processor_type", "minecraft:rule");
@@ -70,6 +70,12 @@ public class Format26_3WriterTest {
assertTrue(pack.get("description").getAsString().length() > 0); assertTrue(pack.get("description").getAsString().length() > 0);
} }
@Test
public void packDescriptionTellsPlayersToKeepTheVanillaPack() {
String description = json(write(fixture(WorldgenExportMode.OVERWORLD)), "pack.mcmeta").getAsJsonObject("pack").get("description").getAsString();
assertTrue(description, description.contains("Keep Default selected"));
}
@Test @Test
public void noiseFileUsesNormalNoiseParameters() { public void noiseFileUsesNormalNoiseParameters() {
JsonObject noise = json(write(fixture(WorldgenExportMode.PRESET)), "data/iris_export/worldgen/noise/flat/height.json"); JsonObject noise = json(write(fixture(WorldgenExportMode.PRESET)), "data/iris_export/worldgen/noise/flat/height.json");