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