mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 00:40:01 +00:00
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:
1 parent
1ed1044474
commit
7ff4292013
25 files changed
+2518
-186
No files matched your search
+24
-15
@@ -34,25 +34,39 @@ public final class StructureTemplateEncoder {
|
||||
.thenComparingInt(TemplateBlock::z);
|
||||
|
||||
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<>();
|
||||
Vector3i center = object.getCenter();
|
||||
int droppedTiles = 0;
|
||||
String firstDropped = null;
|
||||
int outside = 0;
|
||||
String firstOutside = null;
|
||||
Set<String> customBlocks = new TreeSet<>();
|
||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) {
|
||||
IrisBlockVector signed = entry.getKey();
|
||||
int x = signed.getBlockX() + center.getX();
|
||||
int y = signed.getBlockY() + center.getY();
|
||||
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;
|
||||
try {
|
||||
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,
|
||||
"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) {
|
||||
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 "
|
||||
+ 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 {
|
||||
|
||||
@@ -58,6 +58,17 @@ public record VanillaBlockState(String name, Map<String, String> 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() {
|
||||
if (properties.isEmpty()) {
|
||||
return name;
|
||||
|
||||
+18
-2
@@ -18,6 +18,12 @@ public final class AquiferCompiler {
|
||||
private static final int FLUID_BAND_CENTER = 20;
|
||||
private static final double SPREAD_LEVEL_SCALE = 10.0;
|
||||
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 PARTIAL = new Field.Const(0.6);
|
||||
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);
|
||||
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 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) {
|
||||
|
||||
+220
-53
@@ -6,7 +6,6 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
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.compile.BlockStateResolver;
|
||||
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.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class DecoratorCompiler {
|
||||
@@ -33,9 +34,14 @@ public final class DecoratorCompiler {
|
||||
private static final double CAVE_AIR_SHARE = 0.08;
|
||||
private static final int CAVE_SCAN_STEPS = 12;
|
||||
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 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 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 FeatureCatalog catalog;
|
||||
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) {
|
||||
this.context = context;
|
||||
@@ -115,7 +123,7 @@ public final class DecoratorCompiler {
|
||||
List<Candidate> candidates = bucket.getValue();
|
||||
double[] chances = new double[candidates.size()];
|
||||
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);
|
||||
for (int index = 0; index < shares.length; index++) {
|
||||
@@ -129,9 +137,12 @@ public final class DecoratorCompiler {
|
||||
}
|
||||
if (bucket.getKey() == IrisDecorationPart.CEILING) {
|
||||
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]));
|
||||
} 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<>();
|
||||
int airWeight = 0;
|
||||
int total = 0;
|
||||
for (IrisBlockData block : decorator.getPalette()) {
|
||||
Optional<VanillaBlockState> state = blocks.resolve(block, resource);
|
||||
if (state.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int weight = Math.max(1, block.getWeight());
|
||||
total += weight;
|
||||
if (AIR.contains(state.get().name())) {
|
||||
airWeight += weight;
|
||||
for (WeightedState entry : weighted(decorator.getPalette(), decorator.getVariance(), resource)) {
|
||||
total += entry.weight();
|
||||
if (AIR.contains(entry.state().name())) {
|
||||
airWeight += entry.weight();
|
||||
} else {
|
||||
entries.add(new WeightedState(state.get(), weight));
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
if (total == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!isStatic(decorator.getStyle()) || (entries.size() > 1 && !isStatic(decorator.getVariance()))) {
|
||||
report(resource, ExportLimit.DECORATOR_STYLE, STYLE_MESSAGE);
|
||||
if (entries.size() > 1 && !isStatic(decorator.getVariance())) {
|
||||
report(resource, ExportLimit.DECORATOR_STYLE, VARIANCE_MESSAGE);
|
||||
}
|
||||
if (decorator.isScaleStack()) {
|
||||
report(resource, ExportLimit.SCALE_STACK, STACK_MESSAGE);
|
||||
@@ -167,35 +173,64 @@ public final class DecoratorCompiler {
|
||||
report(resource, ExportLimit.SLOPE, RepaintFeatureCompiler.SLOPE_MESSAGE);
|
||||
}
|
||||
List<WeightedState> top = new ArrayList<>();
|
||||
for (IrisBlockData block : decorator.getTopPalette()) {
|
||||
Optional<VanillaBlockState> state = blocks.resolve(block, resource);
|
||||
if (state.isPresent() && !AIR.contains(state.get().name())) {
|
||||
top.add(new WeightedState(state.get(), Math.max(1, block.getWeight())));
|
||||
for (WeightedState entry : weighted(decorator.getTopPalette(), decorator.getVariance(), resource)) {
|
||||
if (!AIR.contains(entry.state().name())) {
|
||||
top.add(entry);
|
||||
}
|
||||
}
|
||||
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),
|
||||
names(decorator.getBlacklist(), resource)));
|
||||
return Optional.of(new Candidate(decorator, entries, top, (double) airWeight / total, force, aquatic(entries, top),
|
||||
names(decorator.getWhitelist(), resource), names(decorator.getBlacklist(), resource), coverage(decorator)));
|
||||
}
|
||||
|
||||
private String column(IrisDecorationPart part, Candidate candidate, double sequential) {
|
||||
String featureId = feature(candidate, false);
|
||||
private List<WeightedState> weighted(List<IrisBlockData> palette, IrisGeneratorStyle variance, String resource) {
|
||||
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();
|
||||
String heightmap = part == IrisDecorationPart.SEA_FLOOR ? Placements.OCEAN_FLOOR : Placements.WORLD_SURFACE;
|
||||
for (JsonObject modifier : Placements.everyColumn()) {
|
||||
placement.add(modifier);
|
||||
}
|
||||
placement.add(Placements.heightmap(heightmap));
|
||||
if (sequential < 1.0) {
|
||||
placement.add(Placements.randomChance(sequential));
|
||||
placement.add(Placements.heightmap(underwater ? Placements.OCEAN_FLOOR : Placements.WORLD_SURFACE));
|
||||
if (chance < 1.0) {
|
||||
placement.add(Placements.randomChance(chance));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
private String ceiling(Candidate candidate, double share) {
|
||||
String featureId = feature(candidate, true);
|
||||
String featureId = feature(candidate, true, false);
|
||||
JsonArray placement = new JsonArray();
|
||||
placement.add(Placements.count(Placements.meanCount(Math.min(MAX_ATTEMPTS, COLUMNS * share / CAVE_AIR_SHARE))));
|
||||
placement.add(Placements.inSquare());
|
||||
@@ -210,13 +245,19 @@ public final class DecoratorCompiler {
|
||||
private List<JsonObject> predicates(IrisDecorationPart part, Candidate candidate) {
|
||||
List<JsonObject> predicates = new ArrayList<>();
|
||||
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 -> {
|
||||
predicates.add(Placements.air(0, 0, 0));
|
||||
predicates.add(Placements.water(0, -1, 0));
|
||||
}
|
||||
case SHORE_LINE -> {
|
||||
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),
|
||||
Placements.water(0, -1, -1))));
|
||||
}
|
||||
@@ -231,40 +272,35 @@ public final class DecoratorCompiler {
|
||||
if (!candidate.blacklist().isEmpty()) {
|
||||
predicates.add(Placements.not(Placements.matchingBlocks(candidate.blacklist(), 0, -1, 0)));
|
||||
}
|
||||
IrisSlopeClip slope = 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)));
|
||||
}
|
||||
}
|
||||
predicates.addAll(Placements.slopeLimits(candidate.decorator().getSlopeCondition()));
|
||||
return predicates;
|
||||
}
|
||||
|
||||
private String feature(Candidate candidate, boolean hanging) {
|
||||
private String feature(Candidate candidate, boolean hanging, boolean underwater) {
|
||||
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));
|
||||
String name = name(candidate);
|
||||
List<WeightedState> entries = waterlogged(candidate.entries(), underwater);
|
||||
List<WeightedState> top = waterlogged(candidate.top(), underwater);
|
||||
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();
|
||||
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);
|
||||
} else {
|
||||
JsonArray layers = new JsonArray(2);
|
||||
if (candidate.top().isEmpty()) {
|
||||
layers.add(layer(Placements.intRange(stackMin, stackMax), candidate.entries()));
|
||||
if (top.isEmpty()) {
|
||||
layers.add(layer(Placements.intRange(stackMin, stackMax), entries));
|
||||
} else {
|
||||
layers.add(layer(Placements.intRange(stackMin - 1, stackMax - 1), candidate.entries()));
|
||||
layers.add(layer(Placements.intRange(1, 1), candidate.top()));
|
||||
layers.add(layer(Placements.intRange(stackMin - 1, stackMax - 1), entries));
|
||||
layers.add(layer(Placements.intRange(1, 1), top));
|
||||
}
|
||||
JsonObject config = new JsonObject();
|
||||
config.add("layers", layers);
|
||||
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);
|
||||
main = catalog.feature(name, "minecraft:block_column", config);
|
||||
}
|
||||
@@ -284,6 +320,112 @@ public final class DecoratorCompiler {
|
||||
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) {
|
||||
JsonObject layer = new JsonObject();
|
||||
layer.add("height", height);
|
||||
@@ -321,11 +463,36 @@ public final class DecoratorCompiler {
|
||||
return style == null || style.getStyle() == null || style.getStyle() == NoiseStyle.STATIC;
|
||||
}
|
||||
|
||||
private static int rise(double slope) {
|
||||
return Math.max(1, (int) Math.round(slope));
|
||||
private static boolean aquatic(List<WeightedState> entries, List<WeightedState> top) {
|
||||
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,
|
||||
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) {
|
||||
}
|
||||
}
|
||||
+113
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
-5
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@@ -33,6 +34,14 @@ public final class DepositCompiler {
|
||||
private static final int MAX_ANCHOR = 2031;
|
||||
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 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 ExportedBiomes biomes;
|
||||
@@ -110,12 +119,17 @@ public final class DepositCompiler {
|
||||
List<JsonObject> hosts = hosts(deposit, resource);
|
||||
List<String> ores = new ArrayList<>(states.size());
|
||||
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) {
|
||||
JsonObject target = new JsonObject();
|
||||
target.add("target", host.deepCopy());
|
||||
target.add("state", syntax.blockState(state));
|
||||
targets.add(target);
|
||||
boolean deepslateHost = host.has("block") && DEEPSLATE_HOSTS.contains(host.get("block").getAsString());
|
||||
targets.add(target(host, stoneVariant(state, deepslateHost)));
|
||||
}
|
||||
JsonObject config = new JsonObject();
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
int minCount = Math.max(0, Math.min(deposit.getMinPerChunk(), deposit.getMaxPerChunk()));
|
||||
int maxCount = Math.max(minCount, deposit.getMaxPerChunk());
|
||||
|
||||
+35
-5
@@ -1,10 +1,12 @@
|
||||
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.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -14,6 +16,7 @@ public final class Placements {
|
||||
private static final int CHUNK_EDGE = 15;
|
||||
private static final int WEIGHT_SCALE = 1000;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final double SLOPE_CEILING = 10.0;
|
||||
|
||||
private Placements() {
|
||||
}
|
||||
@@ -147,17 +150,26 @@ public final class Placements {
|
||||
}
|
||||
|
||||
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");
|
||||
json.addProperty("tag", "minecraft:air");
|
||||
json.addProperty("tag", tag);
|
||||
return offset(json, x, y, z);
|
||||
}
|
||||
|
||||
public static JsonObject water(int x, int y, int z) {
|
||||
JsonArray fluids = new JsonArray(2);
|
||||
fluids.add("minecraft:water");
|
||||
fluids.add("minecraft:flowing_water");
|
||||
return fluids(List.of("minecraft:water", "minecraft:flowing_water"), x, y, z);
|
||||
}
|
||||
|
||||
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");
|
||||
json.add("fluids", fluids);
|
||||
json.add("fluids", list);
|
||||
return offset(json, x, y, z);
|
||||
}
|
||||
|
||||
@@ -215,6 +227,20 @@ public final class Placements {
|
||||
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) {
|
||||
JsonObject[] directions = new JsonObject[CARDINALS.length];
|
||||
for (int index = 0; index < CARDINALS.length; index++) {
|
||||
@@ -280,4 +306,8 @@ public final class Placements {
|
||||
json.addProperty("weight", weight);
|
||||
return json;
|
||||
}
|
||||
|
||||
private static int slopeRise(double slope) {
|
||||
return Math.max(1, (int) Math.round(slope));
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -5,10 +5,11 @@ import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||
|
||||
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 {
|
||||
VanillaResourceIdentifier.requireIdentifier(id, "template id");
|
||||
Objects.requireNonNull(volume, "Template volume");
|
||||
Objects.requireNonNull(roots, "Template roots");
|
||||
if (baseY > 0) {
|
||||
throw new IllegalArgumentException("Template base offset must not be positive, got " + baseY);
|
||||
}
|
||||
|
||||
+124
@@ -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
|
||||
}
|
||||
}
|
||||
+88
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-11
@@ -2,12 +2,14 @@ package art.arcane.iris.pack.datapack.export.compile.object;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
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.IrisRegion;
|
||||
import art.arcane.iris.pack.datapack.DataVersion;
|
||||
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.VanillaBlockState;
|
||||
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||
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.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
@@ -56,6 +59,7 @@ public final class ObjectStage implements CompileStage {
|
||||
private static final class Run {
|
||||
private static final int MAX_STILT_BASES = 16;
|
||||
private static final String TREE_FOLDER = "trees/";
|
||||
private static final String PROCEDURAL_FOLDER = "procedural/";
|
||||
private static final String CAVE_SUFFIX = "_cave";
|
||||
|
||||
private final PackCompileContext context;
|
||||
@@ -70,7 +74,8 @@ public final class ObjectStage implements CompileStage {
|
||||
private final LootCompiler loot;
|
||||
private final WideObjectStructures wide;
|
||||
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<IrisObjectPlacement, VariantPlanner.Plan> plans = 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());
|
||||
LootCompiler.LootScope scope = new LootCompiler.LootScope(dimension, region, biome);
|
||||
List<IrisObjectPlacement> placements = new ArrayList<>(biome.getObjects());
|
||||
placements.addAll(procedural(biome.getProceduralObjects()));
|
||||
if (region != null) {
|
||||
placements.addAll(region.getObjects());
|
||||
placements.addAll(procedural(region.getProceduralObjects()));
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace().isEmpty()) {
|
||||
@@ -136,6 +143,32 @@ public final class ObjectStage implements CompileStage {
|
||||
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) {
|
||||
String name = name(placement, anchor);
|
||||
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<BakedVariant> all = new ArrayList<>();
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
TemplateVolume volume = volume(objectKey);
|
||||
if (volume == null) {
|
||||
ObjectSource source = source(objectKey);
|
||||
if (source == null) {
|
||||
continue;
|
||||
}
|
||||
String objectPath = VanillaResourceIdentifier.sanitizePath(objectKey);
|
||||
@@ -200,7 +233,7 @@ public final class ObjectStage implements CompileStage {
|
||||
for (VariantPlanner.PlannedVariant planned : plan.variants()) {
|
||||
String id = ObjectVariantBaker.templateId(namespace, objectPath, planned.spec(), target);
|
||||
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()));
|
||||
all.add(variant);
|
||||
}
|
||||
@@ -223,14 +256,14 @@ public final class ObjectStage implements CompileStage {
|
||||
return objects;
|
||||
}
|
||||
|
||||
private TemplateVolume volume(String objectKey) {
|
||||
if (volumes.containsKey(objectKey)) {
|
||||
return volumes.get(objectKey);
|
||||
private ObjectSource source(String objectKey) {
|
||||
if (sources.containsKey(objectKey)) {
|
||||
return sources.get(objectKey);
|
||||
}
|
||||
IrisObject object = data.getObjectLoader().load(objectKey, false);
|
||||
TemplateVolume volume = object == null ? null : StructureTemplateEncoder.volume(object, "objects/" + objectKey, diagnostics);
|
||||
volumes.put(objectKey, volume);
|
||||
return volume;
|
||||
ObjectSource source = object == null ? null : ObjectSource.of(object, "objects/" + objectKey, diagnostics);
|
||||
sources.put(objectKey, source);
|
||||
return source;
|
||||
}
|
||||
|
||||
private Map<VanillaBlockState, String> bindings(IrisObjectPlacement placement, List<ObjectTemplates> objects, LootCompiler.LootScope scope) {
|
||||
|
||||
+91
-20
@@ -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.VanillaResourceIdentifier;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -19,6 +19,11 @@ import java.util.TreeMap;
|
||||
|
||||
public final class ObjectVariantBaker {
|
||||
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 int SNOW_LAYER_STEPS = 7;
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
List<Voxel> voxels = prepare(source, spec, seed);
|
||||
TemplateVolume template = source.volume();
|
||||
List<Voxel> voxels = prepare(template, spec, seed, rootCells(source.roots()));
|
||||
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 sizeY = source.sizeY();
|
||||
int sizeZ = source.sizeZ();
|
||||
int sizeX = template.sizeX();
|
||||
int sizeY = template.sizeY();
|
||||
int sizeZ = template.sizeZ();
|
||||
if (spec.scale() != 1) {
|
||||
sizeX = scaledSize(sizeX, spec.scale());
|
||||
sizeY = scaledSize(sizeY, spec.scale());
|
||||
@@ -58,7 +66,7 @@ public final class ObjectVariantBaker {
|
||||
if (spec.snow() > 0) {
|
||||
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) {
|
||||
@@ -79,12 +87,12 @@ public final class ObjectVariantBaker {
|
||||
blocks.add(new TemplateBlock(block.x(), block.y() + lift, block.z(), block.state(), block.nbt()));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
for (TemplateBlock block : source.blocks()) {
|
||||
if (AIR.contains(block.state().name())) {
|
||||
@@ -114,11 +122,29 @@ public final class ObjectVariantBaker {
|
||||
if (spec.preventLeafDecay()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
double cursor = roll * edit.totalWeight();
|
||||
for (WeightedState state : edit.replace()) {
|
||||
@@ -146,6 +172,51 @@ public final class ObjectVariantBaker {
|
||||
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) {
|
||||
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 x = minX; x <= maxX; x++) {
|
||||
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 y = (int) Math.floor(rotated[1]) + rotatedCenterY;
|
||||
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());
|
||||
}
|
||||
@@ -241,7 +312,7 @@ public final class ObjectVariantBaker {
|
||||
double worldZ = voxel.z() + originZ;
|
||||
int x = (int) (voxel.x() + warp.field().offset(worldX, voxel.y(), worldZ));
|
||||
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());
|
||||
}
|
||||
@@ -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 layers = Math.max(0, Math.min(index, SNOW_LAYER_STEPS)) + 1;
|
||||
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;
|
||||
}
|
||||
|
||||
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()) {
|
||||
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 extentZ = 0;
|
||||
@@ -287,10 +358,10 @@ public final class ObjectVariantBaker {
|
||||
}
|
||||
List<Voxel> local = new ArrayList<>(voxels.size());
|
||||
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);
|
||||
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) {
|
||||
@@ -323,6 +394,6 @@ public final class ObjectVariantBaker {
|
||||
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) {
|
||||
}
|
||||
}
|
||||
+313
-32
@@ -1,5 +1,7 @@
|
||||
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.VanillaResourceIdentifier;
|
||||
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
||||
@@ -18,20 +20,26 @@ import com.google.gson.JsonObject;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public final class PlacementCompiler {
|
||||
public static final int MAX_FEATURE_WIDTH = 32;
|
||||
private static final int TILE_WIDTH = 17;
|
||||
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_FLOOR_MARGIN = 8;
|
||||
private static final int MIN_ANCHOR = -2032;
|
||||
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 ROTATION_SHARES = 12L;
|
||||
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,
|
||||
ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT,
|
||||
ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT);
|
||||
@@ -42,12 +50,14 @@ public final class PlacementCompiler {
|
||||
private final FeatureJson json;
|
||||
private final DimensionDef dimension;
|
||||
private final WideObjectStructures wide;
|
||||
private final RootExtension roots;
|
||||
|
||||
public PlacementCompiler(String namespace, ExportCapabilities capabilities, DimensionDef dimension) {
|
||||
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "placement namespace");
|
||||
json = new FeatureJson(capabilities);
|
||||
this.dimension = dimension;
|
||||
wide = new WideObjectStructures(namespace);
|
||||
roots = new RootExtension(this.namespace, json);
|
||||
}
|
||||
|
||||
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(),
|
||||
"Vacuum terrain shaping is not exported; the object places at its center height"));
|
||||
}
|
||||
if (!narrow.isEmpty()) {
|
||||
compileNarrow(input, narrow, chance, density, (double) narrow.size() / total, resources, placed);
|
||||
for (List<ObjectTemplates> group : depthGroups(input, narrow)) {
|
||||
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()) {
|
||||
List<TemplateChoice> templates = weighted(wideObjects);
|
||||
@@ -108,22 +122,32 @@ public final class PlacementCompiler {
|
||||
if (paint) {
|
||||
processors.add(gravity(placement));
|
||||
}
|
||||
JsonObject feature = template(templates, processors);
|
||||
if (stilted(placement.getMode())) {
|
||||
feature = stilts(feature, templates, input.stiltBases());
|
||||
HeightAnchor.Sampling sampling = input.anchor() == PlacementInput.Anchor.SURFACE && !paint
|
||||
&& placement.getMode() != ObjectPlaceMode.FLOATING ? HeightAnchor.sampling(placement.getMode()) : HeightAnchor.Sampling.CENTER;
|
||||
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 featurePath = "object/" + name + "_" + ContentIds.hash(feature);
|
||||
resources.add(new JsonResource("data/" + namespace + "/" + json.syntax().featureFolder() + "/" + featurePath + ".json", feature));
|
||||
JsonObject placedFeature = new JsonObject();
|
||||
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);
|
||||
resources.add(new JsonResource("data/" + namespace + "/worldgen/placed_feature/" + placedPath + ".json", placedFeature));
|
||||
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();
|
||||
int translateX = placement.getTranslate().getX();
|
||||
int translateY = placement.getTranslate().getY();
|
||||
@@ -143,18 +167,32 @@ public final class PlacementCompiler {
|
||||
} else if (placement.getMode() == ObjectPlaceMode.FLOATING) {
|
||||
modifiers.add(fixedHeight(translateY + base + spreadLow, translateY + base + spreadHigh));
|
||||
} else {
|
||||
modifiers.add(Placements.heightmap(placement.isUnderwater() ? "OCEAN_FLOOR_WG" : "WORLD_SURFACE_WG"));
|
||||
if (!placement.isForcePlace() && placement.isRequireSurfaceSupport()) {
|
||||
modifiers.add(Placements.filter(support(templates, placement)));
|
||||
boolean underwater = placement.isUnderwater();
|
||||
boolean onwater = onwater(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) {
|
||||
modifiers.addAll(json.offset(translateX, 0, 0, translateZ));
|
||||
} else {
|
||||
modifiers.addAll(json.offset(translateX, translateY - 1 + base + spreadLow, translateY - 1 + base + spreadHigh, translateZ));
|
||||
} else if (!branched) {
|
||||
modifiers.addAll(surfaceOffset(placement, base));
|
||||
}
|
||||
}
|
||||
JsonObject clamp = clamp(placement.getClamp());
|
||||
if (clamp != null) {
|
||||
if (clamp != null && !branched) {
|
||||
modifiers.add(Placements.filter(clamp));
|
||||
}
|
||||
modifiers.add(Placements.typed("minecraft:biome"));
|
||||
@@ -165,6 +203,128 @@ public final class PlacementCompiler {
|
||||
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) {
|
||||
JsonArray entries = new JsonArray(templates.size());
|
||||
for (TemplateChoice template : templates) {
|
||||
@@ -262,20 +422,66 @@ public final class PlacementCompiler {
|
||||
return json.syntax().feature("minecraft:single_block_pillar", fields);
|
||||
}
|
||||
|
||||
private static JsonObject canReplace() {
|
||||
JsonObject fluids = Placements.typed("minecraft:matching_fluids");
|
||||
JsonArray list = new JsonArray(FLUIDS.size());
|
||||
for (String fluid : FLUIDS) {
|
||||
list.add(fluid);
|
||||
static JsonObject canReplace() {
|
||||
return Placements.anyOf(List.of(Placements.air(0, 0, 0), Placements.fluids(FLUIDS, 0, 0, 0), Placements.typed("minecraft:replaceable")));
|
||||
}
|
||||
|
||||
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);
|
||||
return Placements.anyOf(List.of(Placements.air(0, 0, 0), fluids, Placements.typed("minecraft:replaceable")));
|
||||
Map<Integer, List<ObjectTemplates>> groups = new TreeMap<>();
|
||||
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) {
|
||||
JsonObject gravity = new JsonObject();
|
||||
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);
|
||||
return gravity;
|
||||
}
|
||||
@@ -291,17 +497,88 @@ public final class PlacementCompiler {
|
||||
}
|
||||
|
||||
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) {
|
||||
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 depth = Math.max(1, placement.getSurfaceSupportDepth());
|
||||
JsonObject predicate = Placements.typed("minecraft:volume_match");
|
||||
predicate.add("match", Placements.typed("minecraft:solid"));
|
||||
predicate.add("max", FeatureJson.vector(reach, -1, reach));
|
||||
predicate.add("min", FeatureJson.vector(-reach, -depth, -reach));
|
||||
return predicate;
|
||||
int buffer = Math.max(0, placement.getSurfaceSupportBuffer());
|
||||
int thickness = Math.max(1, Math.min(MAX_SUPPORT_DROP, placement.getSurfaceSupportDepth()));
|
||||
int[] xs = {reach(minX - buffer), reach(maxX + buffer)};
|
||||
int[] zs = {reach(minZ - buffer), reach(maxZ + buffer)};
|
||||
Set<Long> seen = new LinkedHashSet<>();
|
||||
List<JsonObject> corners = new ArrayList<>(4);
|
||||
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) {
|
||||
@@ -323,7 +600,7 @@ public final class PlacementCompiler {
|
||||
|
||||
private JsonObject caveRange() {
|
||||
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();
|
||||
height.addProperty("type", "minecraft:uniform");
|
||||
height.add("max_inclusive", FeatureJson.absolute(Math.max(floor, ceiling)));
|
||||
@@ -393,4 +670,8 @@ public final class PlacementCompiler {
|
||||
}
|
||||
return tallest;
|
||||
}
|
||||
|
||||
private record Branching(PlacementInput input, List<JsonObject> processors, HeightAnchor.Sampling sampling, boolean grounded,
|
||||
List<JsonObject> offset, JsonObject clamp, Map<String, JsonResource> shared) {
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -132,7 +132,7 @@ abstract class BaseWorldgenJsonWriter implements WorldgenJsonWriter {
|
||||
|
||||
private JsonObject packMetadata(ExportIr ir) {
|
||||
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("max_format", target.getPackFormat());
|
||||
JsonObject metadata = new JsonObject();
|
||||
|
||||
+73
-1
@@ -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.compile.PackCompileContext;
|
||||
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.ExportLimit;
|
||||
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||
@@ -26,6 +27,16 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class AquiferCompilerTest {
|
||||
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 PARTIAL = 0.6;
|
||||
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));
|
||||
|
||||
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) {
|
||||
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
|
||||
public void lavaAboveMinus10ReportsLimit() throws Exception {
|
||||
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 {
|
||||
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),
|
||||
Map.of("cave-only", profile));
|
||||
IrisData data = IrisData.openRuntime(pack);
|
||||
try {
|
||||
PackCompileContext context = context(data, "cave-only");
|
||||
context.setHeight(height);
|
||||
context.setSurfaceEstimate(estimate);
|
||||
new CaveStage().apply(context);
|
||||
return context;
|
||||
} finally {
|
||||
|
||||
+8
-1
@@ -133,7 +133,14 @@ public class CaveStageTest {
|
||||
new CaveStage().apply(context);
|
||||
|
||||
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());
|
||||
} finally {
|
||||
data.close();
|
||||
|
||||
+246
-1
@@ -1,9 +1,11 @@
|
||||
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.ExportTestPacks;
|
||||
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.noise.VanillaNoiseModel;
|
||||
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.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.loading.IrisData;
|
||||
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.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -27,6 +32,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@@ -132,6 +138,167 @@ public class DecorationStageTest {
|
||||
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
|
||||
public void columnWinnerSharesFollowIrisReservoirPick() {
|
||||
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();
|
||||
assertEquals("minecraft:ore", ore.get("type").getAsString());
|
||||
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("{\"id\":\"minecraft:coal_ore\"}", target.get("state").toString());
|
||||
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
|
||||
public void vanillaOreVeinsDisabled() throws Exception {
|
||||
Map<String, String> biomes = new LinkedHashMap<>();
|
||||
@@ -260,6 +455,56 @@ public class DecorationStageTest {
|
||||
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) {
|
||||
List<String> features = new ArrayList<>();
|
||||
for (JsonElement feature : biome.json().getAsJsonArray("features").get(step.ordinal()).getAsJsonArray()) {
|
||||
|
||||
+157
@@ -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;
|
||||
}
|
||||
}
|
||||
+81
@@ -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
|
||||
public void lootBoundPerBiome() throws Exception {
|
||||
File pack = pack();
|
||||
|
||||
+101
-10
@@ -38,8 +38,8 @@ public class ObjectVariantBakerTest {
|
||||
.build();
|
||||
String id = ObjectVariantBaker.templateId(NAMESPACE, "trees/oak", spec, DataVersion.V26_3);
|
||||
|
||||
BakedVariant first = new ObjectVariantBaker().bake(id, tree, spec, "trees/oak", new ArrayList<>());
|
||||
BakedVariant second = 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, new ObjectSource(tree, 0, TreeRoots.NONE), spec);
|
||||
|
||||
assertEquals(first, second);
|
||||
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")));
|
||||
|
||||
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(2, variant.volume().sizeX());
|
||||
@@ -66,6 +66,30 @@ public class ObjectVariantBakerTest {
|
||||
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
|
||||
public void quarterRotationMovesBlocksAndFacing() {
|
||||
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]")));
|
||||
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()
|
||||
.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]")));
|
||||
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();
|
||||
assertEquals("minecraft:oak_slab", slab.state().name());
|
||||
@@ -104,7 +128,7 @@ public class ObjectVariantBakerTest {
|
||||
block(1, 1, 1, "minecraft:oak_log[axis=y]")));
|
||||
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(4, big.volume().sizeY());
|
||||
@@ -124,8 +148,8 @@ public class ObjectVariantBakerTest {
|
||||
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);
|
||||
|
||||
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/floor/a", floor,
|
||||
VariantSpec.builder().edits(List.of(always, half)).build(), "floor", new ArrayList<>());
|
||||
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/floor/a", new ObjectSource(floor, 0, TreeRoots.NONE),
|
||||
VariantSpec.builder().edits(List.of(always, half)).build());
|
||||
|
||||
Set<String> names = new HashSet<>();
|
||||
int mossy = 0;
|
||||
@@ -141,8 +165,8 @@ public class ObjectVariantBakerTest {
|
||||
|
||||
@Test
|
||||
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 flat = new BakedVariant("iris_export:object/a/flat", new TemplateVolume(1, 1, 1, List.of(block(0, 0, 0, "minecraft:stone"))), 0);
|
||||
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, TreeRoots.NONE);
|
||||
|
||||
List<BakedVariant> aligned = ObjectVariantBaker.alignBases(List.of(low, flat));
|
||||
|
||||
@@ -154,6 +178,73 @@ public class ObjectVariantBakerTest {
|
||||
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() {
|
||||
List<TemplateBlock> blocks = new ArrayList<>();
|
||||
for (int y = 0; y < 5; y++) {
|
||||
|
||||
+565
-28
@@ -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.TemplateVolume;
|
||||
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.ir.DimensionDef;
|
||||
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||
@@ -21,9 +22,11 @@ import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -59,17 +62,244 @@ public class PlacementCompilerTest {
|
||||
assertFalse(templates.get(0).getAsJsonObject().getAsJsonObject("data").has("rotations"));
|
||||
JsonObject placed = placed(compiled);
|
||||
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");
|
||||
assertEquals(0.5, modifiers.get(0).getAsJsonObject().get("chance").getAsDouble(), 1e-6);
|
||||
assertEquals(3, modifiers.get(1).getAsJsonObject().get("count").getAsInt());
|
||||
assertEquals("WORLD_SURFACE_WG", modifiers.get(3).getAsJsonObject().get("heightmap").getAsString());
|
||||
assertEquals(1, modifiers.get(5).getAsJsonObject().get("y").getAsInt());
|
||||
JsonObject support = modifiers.get(4).getAsJsonObject().getAsJsonObject("predicate");
|
||||
assertEquals("minecraft:volume_match", support.get("type").getAsString());
|
||||
assertEquals("OCEAN_FLOOR_WG", modifiers.get(3).getAsJsonObject().get("heightmap").getAsString());
|
||||
assertEquals(1, modifiers.get(6).getAsJsonObject().get("y").getAsInt());
|
||||
assertTrue(StageTestPacks.ofType(placed, "minecraft:volume_match").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
|
||||
public void wideObjectBecomesSinglePieceStructure() {
|
||||
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT, 0.02, 1);
|
||||
@@ -112,7 +342,8 @@ public class PlacementCompilerTest {
|
||||
@Test
|
||||
public void writeWindowNeverExceeded() {
|
||||
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 translate : List.of(0, 3, -5, 12)) {
|
||||
IrisObjectPlacement placement = placement(mode, 1, 1);
|
||||
@@ -160,13 +391,13 @@ public class PlacementCompilerTest {
|
||||
assertEquals("minecraft:rule", processors.get(0).getAsJsonObject().get("processor_type").getAsString());
|
||||
JsonObject gravity = processors.get(1).getAsJsonObject();
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
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(),
|
||||
List.of(VanillaBlockState.parse("minecraft:oak_log[axis=y]")), "hut");
|
||||
|
||||
@@ -210,6 +441,254 @@ public class PlacementCompilerTest {
|
||||
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) {
|
||||
Map<String, Integer> footprints = new HashMap<>();
|
||||
for (ObjectTemplates object : objects) {
|
||||
@@ -217,10 +696,23 @@ public class PlacementCompilerTest {
|
||||
footprints.put(choice.variant().id(), choice.variant().volume().footprint());
|
||||
}
|
||||
}
|
||||
JsonObject placed = placed(compiled);
|
||||
int offsetX = 0;
|
||||
int offsetZ = 0;
|
||||
for (JsonElement element : placed.getAsJsonArray("placement")) {
|
||||
int[] offset = probeWindow(placed(compiled).getAsJsonArray("placement"), 0, 0, label);
|
||||
JsonObject feature = feature(compiled, null);
|
||||
if (!feature.get("type").getAsString().equals("minecraft:weighted_random_selector")) {
|
||||
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();
|
||||
String type = modifier.get("type").getAsString();
|
||||
if (type.equals("minecraft:offset")) {
|
||||
@@ -229,15 +721,26 @@ public class PlacementCompilerTest {
|
||||
} else if (type.equals("minecraft:random_offset")) {
|
||||
offsetX += modifier.get("xz_spread").getAsInt();
|
||||
offsetZ += modifier.get("xz_spread").getAsInt();
|
||||
} else if (type.equals("minecraft:block_predicate_filter")) {
|
||||
JsonObject predicate = modifier.getAsJsonObject("predicate");
|
||||
if (predicate.get("type").getAsString().equals("minecraft:volume_match")) {
|
||||
assertRange(label + " support", predicate.getAsJsonArray("min").get(0).getAsInt() + offsetX,
|
||||
predicate.getAsJsonArray("max").get(0).getAsInt() + offsetX);
|
||||
} else if (type.equals("minecraft:block_predicate_filter") || type.equals("minecraft:environment_scan")) {
|
||||
for (JsonObject probe : StageTestPacks.find(modifier, object -> object.has("offset") || object.has("min"))) {
|
||||
for (String key : List.of("offset", "min", "max")) {
|
||||
if (!probe.has(key)) {
|
||||
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();
|
||||
JsonObject body = feature.has("config") ? feature.getAsJsonObject("config") : feature;
|
||||
if (type.equals("minecraft:fossil")) {
|
||||
@@ -255,16 +758,27 @@ public class PlacementCompilerTest {
|
||||
assertNotNull(label, footprint);
|
||||
assertTemplate(label, footprint, offsetX, offsetZ);
|
||||
}
|
||||
if (type.equals("minecraft:overlay")) {
|
||||
JsonArray features = body.getAsJsonArray("features");
|
||||
for (int index = 1; index < features.size(); index++) {
|
||||
JsonArray tile = features.get(index).getAsJsonObject().getAsJsonArray("placement");
|
||||
int tileX = tile.get(0).getAsJsonObject().get("x").getAsInt() + offsetX;
|
||||
int tileZ = tile.get(0).getAsJsonObject().get("z").getAsInt() + offsetZ;
|
||||
int size = tile.get(1).getAsJsonObject().get("xz_size").getAsInt();
|
||||
assertRange(label + " stilt tile x", tileX, tileX + size);
|
||||
assertRange(label + " stilt tile z", tileZ, tileZ + size);
|
||||
if (!type.equals("minecraft:overlay")) {
|
||||
return;
|
||||
}
|
||||
JsonArray features = body.getAsJsonArray("features");
|
||||
for (int index = 1; index < features.size(); index++) {
|
||||
JsonArray part = features.get(index).getAsJsonObject().getAsJsonArray("placement");
|
||||
int partX = offsetX;
|
||||
int partZ = offsetZ;
|
||||
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];
|
||||
TemplateBlock block = new TemplateBlock(0, 0, 0, VanillaBlockState.parse("minecraft:stone"), null);
|
||||
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))));
|
||||
}
|
||||
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() {
|
||||
JsonObject rule = new JsonObject();
|
||||
rule.addProperty("processor_type", "minecraft:rule");
|
||||
|
||||
@@ -70,6 +70,12 @@ public class Format26_3WriterTest {
|
||||
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
|
||||
public void noiseFileUsesNormalNoiseParameters() {
|
||||
JsonObject noise = json(write(fixture(WorldgenExportMode.PRESET)), "data/iris_export/worldgen/noise/flat/height.json");
|
||||
|
||||
Reference in new issue
Block a user