From f32094a404fa8faf77e3dc31341974083c8cfd07 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 11 Jun 2026 21:32:54 -0400 Subject: [PATCH] Headless Generation --- .../src/main/java/art/arcane/iris/Iris.java | 3 + .../iris/core/loader/ResourceLoader.java | 4 +- .../iris/core/service/PreservationSVC.java | 3 +- .../art/arcane/iris/engine/IrisEngine.java | 3 +- .../framework/PreservationRegistry.java | 27 ++ .../arcane/iris/engine/object/IrisBiome.java | 61 +++- .../object/IrisBiomeCustomParticle.java | 13 +- .../engine/object/IrisBiomeCustomSpawn.java | 16 +- .../iris/engine/object/IrisDimension.java | 17 +- .../arcane/iris/engine/object/IrisEffect.java | 51 ++- .../arcane/iris/engine/object/IrisEntity.java | 2 - .../iris/engine/object/IrisEnvironment.java | 36 ++ .../arcane/iris/engine/object/IrisLoot.java | 36 +- .../arcane/iris/engine/object/IrisRegion.java | 2 - .../arcane/iris/engine/object/StudioMode.java | 39 +- .../iris/util/common/reflect/KeyedType.java | 11 + .../arcane/iris/util/common/scheduling/J.java | 18 +- .../project/matter/IrisMatterSupport.java | 14 + .../project/matter/slices/EntityMatter.java | 7 + .../project/matter/slices/TileMatter.java | 7 + .../stream/utility/CachedDoubleStream2D.java | 4 +- .../stream/utility/CachedStream2D.java | 4 +- .../stream/utility/CachedStream3D.java | 4 +- probe/build.gradle | 24 ++ .../arcane/iris/probe/GenerationProbe.java | 339 ++++++++++++++++++ .../art/arcane/iris/probe/StubPlatform.java | 151 +++++++- 26 files changed, 816 insertions(+), 80 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java create mode 100644 probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 166869478..f780ace3f 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -44,11 +44,13 @@ import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.service.EditSVC; +import art.arcane.iris.core.service.PreservationSVC; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.EnginePanic; import art.arcane.iris.engine.framework.BlockEditAccess; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.object.IrisCompat; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisWorld; @@ -583,6 +585,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { IrisServices.register(i.getClass(), i); }); IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class)); + IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class)); IO.delete(new File("iris")); compat = IrisCompat.configured(getDataFile("compat.json")); IrisServices.register(IrisCompat.class, compat); diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index d15fa0a9e..f01244b17 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -24,7 +24,7 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.project.SchemaBuilder; -import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.MeteredCache; @@ -100,7 +100,7 @@ public class ResourceLoader implements MeteredCache { this.folderName = folderName; loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getResourceLoaderCacheSize()); IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath()); - IrisServices.get(PreservationSVC.class).registerCache(this); + IrisServices.get(PreservationRegistry.class).registerCache(this); } public JSONObject buildSchema() { diff --git a/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java index e28f9eed6..373d7f37a 100644 --- a/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java @@ -21,6 +21,7 @@ package art.arcane.iris.core.service; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.data.KCache; import art.arcane.volmlib.util.format.Form; @@ -36,7 +37,7 @@ import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import java.util.stream.Stream; -public class PreservationSVC implements IrisService { +public class PreservationSVC implements IrisService, PreservationRegistry { private final List threads = new CopyOnWriteArrayList<>(); private final List services = new CopyOnWriteArrayList<>(); private final List> caches = new CopyOnWriteArrayList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index b171dbf49..3dc2c1a38 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -33,7 +33,6 @@ import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.project.IrisProject; -import art.arcane.iris.core.service.PreservationSVC; import art.arcane.iris.core.structure.StructureIndexService; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.AtomicCache; @@ -601,7 +600,7 @@ public class IrisEngine implements Engine { worldManager = null; getData().dump(); getData().clearLists(); - IrisServices.get(PreservationSVC.class).dereference(); + IrisServices.get(PreservationRegistry.class).dereference(); IrisLogging.debug("Engine Fully Shutdown!"); } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java new file mode 100644 index 000000000..2d8299db2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java @@ -0,0 +1,27 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.framework; + +public interface PreservationRegistry { + void register(Thread thread); + + void registerCache(MeteredCache cache); + + void dereference(); +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 5f9da1cbd..d39746c21 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -32,18 +32,19 @@ import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.DataProvider; +import art.arcane.iris.util.common.data.registry.RegistryUtil; import art.arcane.volmlib.util.data.VanillaBiomeMap; import art.arcane.volmlib.util.inventorygui.RandomColor; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.common.plugin.VolmitSender; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import art.arcane.iris.spi.PlatformBlockState; +import org.bukkit.NamespacedKey; import org.bukkit.block.Biome; import java.awt.*; @@ -51,7 +52,6 @@ import java.util.EnumMap; @Accessors(chain = true) @NoArgsConstructor -@AllArgsConstructor @Desc("Represents a biome in iris. Biomes are placed inside of regions and hold objects.\nA biome consists of layers (block palletes), decorations, objects & generators.") @Data @EqualsAndHashCode(callSuper = false) @@ -81,6 +81,10 @@ public class IrisBiome extends IrisRegistrant implements IRare { private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); private final transient AtomicCache> decoratorBuckets = new AtomicCache<>(); + private final transient AtomicCache derivativeResolved = new AtomicCache<>(); + private final transient AtomicCache vanillaDerivativeResolved = new AtomicCache<>(); + private final transient AtomicCache> biomeScatterResolved = new AtomicCache<>(); + private final transient AtomicCache> biomeSkyScatterResolved = new AtomicCache<>(); private static final IrisDecorator[] EMPTY_BUCKET = new IrisDecorator[0]; @MinNumber(2) @Required @@ -120,16 +124,16 @@ public class IrisBiome extends IrisRegistrant implements IRare { private String color = null; @Required @Desc("The raw derivative of this biome. This is required or the terrain will not properly generate. Use any vanilla biome type. Look in examples/biome-list.txt") - private Biome derivative = Biome.THE_VOID; + private String derivative = "minecraft:the_void"; @Required @Desc("Override the derivative when vanilla places structures to this derivative. This is useful for example if you have an ocean biome, but you have set the derivative to desert to get a brown-ish color. To prevent desert structures from spawning on top of your ocean, you can set your vanillaDerivative to ocean, to allow for vanilla structures. Not defining this value will simply select the derivative.") - private Biome vanillaDerivative = null; - @ArrayType(min = 1, type = Biome.class) + private String vanillaDerivative = null; + @ArrayType(min = 1, type = String.class) @Desc("You can instead specify multiple biome derivatives to randomly scatter colors in this biome") - private KList biomeScatter = new KList<>(); - @ArrayType(min = 1, type = Biome.class) + private KList biomeScatter = new KList<>(); + @ArrayType(min = 1, type = String.class) @Desc("Since 1.13 supports 3D biomes, you can add different derivative colors for anything above the terrain. (Think swampy tree leaves with a desert looking grass surface)") - private KList biomeSkyScatter = new KList<>(); + private KList biomeSkyScatter = new KList<>(); @DependsOn({"children"}) @Desc("If this biome has children biomes, and the gen layer chooses one of this biomes children, how much smaller will it be (inside of this biome). Higher values means a smaller biome relative to this biome's size. Set higher than 1.0 and below 3.0 for best results.") private double childShrinkFactor = 1.5; @@ -259,8 +263,39 @@ public class IrisBiome extends IrisRegistrant implements IRare { }); } + public Biome getDerivative() { + return derivativeResolved.aquire(() -> resolveBiomeKey(derivative)); + } + public Biome getVanillaDerivative() { - return vanillaDerivative == null ? derivative : vanillaDerivative; + Biome resolved = vanillaDerivative == null + ? null + : vanillaDerivativeResolved.aquire(() -> resolveBiomeKey(vanillaDerivative)); + return resolved == null ? getDerivative() : resolved; + } + + private KList getBiomeScatterResolved() { + return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter)); + } + + private KList getBiomeSkyScatterResolved() { + return biomeSkyScatterResolved.aquire(() -> resolveBiomeKeys(biomeSkyScatter)); + } + + private static KList resolveBiomeKeys(KList keys) { + KList resolved = new KList<>(); + for (String key : keys) { + resolved.add(resolveBiomeKey(key)); + } + return resolved; + } + + private static Biome resolveBiomeKey(String key) { + if (key == null) { + return null; + } + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + return namespacedKey == null ? null : RegistryUtil.lookup(Biome.class).get(namespacedKey); } public boolean isCustom() { @@ -676,14 +711,14 @@ public class IrisBiome extends IrisRegistrant implements IRare { public Biome getSkyBiome(RNG rng, double x, double y, double z) { if (biomeSkyScatter.size() == 1) { - return biomeSkyScatter.get(0); + return getBiomeSkyScatterResolved().get(0); } if (biomeSkyScatter.isEmpty()) { return getGroundBiome(rng, x, y, z); } - return biomeSkyScatter.get(getBiomeGenerator(rng).fit(0, biomeSkyScatter.size() - 1, x, y, z)); + return getBiomeSkyScatterResolved().get(getBiomeGenerator(rng).fit(0, biomeSkyScatter.size() - 1, x, y, z)); } public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) { @@ -729,10 +764,10 @@ public class IrisBiome extends IrisRegistrant implements IRare { } if (biomeScatter.size() == 1) { - return biomeScatter.get(0); + return getBiomeScatterResolved().get(0); } - return getBiomeGenerator(rng).fit(biomeScatter, x, y, z); + return getBiomeGenerator(rng).fit(getBiomeScatterResolved(), x, y, z); } public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java index c537615f7..d84f422eb 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java @@ -18,11 +18,14 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.util.common.data.registry.RegistryUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; import org.bukkit.Particle; @Snippet("custom-biome-particle") @@ -32,12 +35,20 @@ import org.bukkit.Particle; @Desc("A custom biome ambient particle") @Data public class IrisBiomeCustomParticle { + private final transient AtomicCache particleResolved = new AtomicCache<>(); @Required @Desc("The biome's particle type") - private Particle particle = Particle.FLASH; + private String particle = "minecraft:flash"; @MinNumber(1) @MaxNumber(10000) @Desc("The rarity") private int rarity = 35; + + public Particle getParticle() { + return particleResolved.aquire(() -> { + NamespacedKey namespacedKey = NamespacedKey.fromString(particle); + return namespacedKey == null ? null : RegistryUtil.lookup(Particle.class).get(namespacedKey); + }); + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java index f90df8676..70e6aea0d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java @@ -18,11 +18,14 @@ package art.arcane.iris.engine.object; +import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.util.common.data.registry.RegistryUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import org.bukkit.NamespacedKey; import org.bukkit.entity.EntityType; @Snippet("custom-biome-spawn") @@ -32,9 +35,20 @@ import org.bukkit.entity.EntityType; @Desc("A custom biome spawn") @Data public class IrisBiomeCustomSpawn { + private final transient AtomicCache typeResolved = new AtomicCache<>(); @Required @Desc("The biome's entity type") - private EntityType type = EntityType.COW; + private String type = "minecraft:cow"; + + public EntityType getType() { + if (type == null) { + return null; + } + return typeResolved.aquire(() -> { + NamespacedKey namespacedKey = NamespacedKey.fromString(type); + return namespacedKey == null ? null : RegistryUtil.lookup(EntityType.class).get(namespacedKey); + }); + } @MinNumber(1) @Desc("The min to spawn") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index f3f868f7c..e5eb183db 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -43,7 +43,6 @@ import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.common.plugin.VolmitSender; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -62,7 +61,6 @@ import java.util.Locale; import java.util.Map; @Accessors(chain = true) -@AllArgsConstructor @NoArgsConstructor @Desc("Represents a dimension") @Data @@ -157,7 +155,7 @@ public class IrisDimension extends IrisRegistrant { @Desc("forceConvertTo320Height") private Boolean forceConvertTo320Height = false; @Desc("The world environment") - private Environment environment = Environment.NORMAL; + private IrisEnvironment environment = IrisEnvironment.NORMAL; @RegistryListResource(IrisRegion.class) @Required @ArrayType(min = 1, type = String.class) @@ -395,7 +393,16 @@ public class IrisDimension extends IrisRegistrant { } public Environment getEnvironment() { - return environment; + if (environment == null) { + return null; + } + + return switch (environment) { + case NORMAL -> Environment.NORMAL; + case NETHER -> Environment.NETHER; + case THE_END -> Environment.THE_END; + case CUSTOM -> Environment.CUSTOM; + }; } public boolean hasFocusRegion() { @@ -505,7 +512,7 @@ public class IrisDimension extends IrisRegistrant { } public Dimension getBaseDimension() { - return switch (getEnvironment()) { + return switch (environment) { case NETHER -> Dimension.NETHER; case THE_END -> Dimension.END; default -> Dimension.OVERWORLD; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java index 279bdc430..2639da840 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java @@ -24,12 +24,14 @@ import art.arcane.iris.engine.object.annotations.*; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.data.registry.RegistryUtil; import art.arcane.iris.util.common.scheduling.J; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import org.bukkit.Location; +import org.bukkit.NamespacedKey; import org.bukkit.Particle; import org.bukkit.Sound; import org.bukkit.entity.Entity; @@ -47,11 +49,13 @@ import org.bukkit.util.Vector; public class IrisEffect { private final transient AtomicCache pt = new AtomicCache<>(); private final transient AtomicCache latch = new AtomicCache<>(); + private final transient AtomicCache particleEffectResolved = new AtomicCache<>(); + private final transient AtomicCache soundResolved = new AtomicCache<>(); @RegistryListPotionEffect @Desc("The potion effect to apply in this area") private String potionEffect = ""; @Desc("The particle effect to apply in the area") - private Particle particleEffect = null; + private String particleEffect = null; @DependsOn({"particleEffect"}) @MinNumber(-32) @MaxNumber(32) @@ -82,7 +86,7 @@ public class IrisEffect { @Desc("Randomize the altZ by -altZ to altZ") private boolean randomAltZ = true; @Desc("The sound to play") - private Sound sound = null; + private String sound = null; @DependsOn({"sound"}) @MinNumber(0) @MaxNumber(512) @@ -155,6 +159,25 @@ public class IrisEffect { return latch.aquire(() -> new ChronoLatch(interval)).flip(); } + public Particle getParticleEffect() { + if (particleEffect == null) { + return null; + } + return particleEffectResolved.aquire(() -> resolveKeyed(Particle.class, particleEffect)); + } + + public Sound getSound() { + if (sound == null) { + return null; + } + return soundResolved.aquire(() -> resolveKeyed(Sound.class, sound)); + } + + private static T resolveKeyed(Class type, String key) { + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + return namespacedKey == null ? null : RegistryUtil.lookup(type).get(namespacedKey); + } + public PotionEffectType getRealType() { return pt.aquire(() -> { @@ -195,19 +218,21 @@ public class IrisEffect { } BukkitFx.run(p, () -> { - if (sound != null) { + Sound soundType = getSound(); + Particle particleType = getParticleEffect(); + if (soundType != null) { Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); - p.playSound(part, getSound(), (float) volume, (float) RNG.r.d(minPitch, maxPitch)); + p.playSound(part, soundType, (float) volume, (float) RNG.r.d(minPitch, maxPitch)); } - if (particleEffect != null) { + if (particleType != null) { Location part = p.getLocation().clone().add(p.getLocation().getDirection().clone().multiply(RNG.r.i(particleDistance) + particleAway)).clone().add(p.getLocation().getDirection().clone().rotateAroundY(Math.toRadians(90)).multiply(RNG.r.d(-particleDistanceWidth, particleDistanceWidth))); part.setY(Math.round(g.getHeight(part.getBlockX(), part.getBlockZ())) + 1); part.add(RNG.r.d(), 0, RNG.r.d()); int offset = p.getWorld().getMinHeight(); if (extra != 0) { - p.spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), + p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, @@ -215,7 +240,7 @@ public class IrisEffect { randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, extra); } else { - p.spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), + p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, @@ -256,17 +281,19 @@ public class IrisEffect { } J.runEntity(p, () -> { - if (sound != null) { + Sound soundType = getSound(); + Particle particleType = getParticleEffect(); + if (soundType != null) { Location part = p.getLocation().clone().add(RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance), RNG.r.i(-soundDistance, soundDistance)); - p.getWorld().playSound(part, getSound(), (float) volume, (float) RNG.r.d(minPitch, maxPitch)); + p.getWorld().playSound(part, soundType, (float) volume, (float) RNG.r.d(minPitch, maxPitch)); } - if (particleEffect != null) { + if (particleType != null) { Location part = p.getLocation().clone().add(0, 0.25, 0).add(new Vector(1, 1, 1).multiply(RNG.r.d())).subtract(new Vector(1, 1, 1).multiply(RNG.r.d())); part.add(RNG.r.d(), 0, RNG.r.d()); int offset = p.getWorld().getMinHeight(); if (extra != 0) { - p.getWorld().spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, @@ -274,7 +301,7 @@ public class IrisEffect { randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, extra); } else { - p.getWorld().spawnParticle(particleEffect, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java index e535f7b43..7ffcab8f8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java @@ -38,7 +38,6 @@ import art.arcane.iris.util.common.plugin.Chunks; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -66,7 +65,6 @@ import static art.arcane.iris.util.common.data.registry.Particles.ITEM; @SuppressWarnings("ALL") @Accessors(chain = true) @NoArgsConstructor -@AllArgsConstructor @Desc("Represents an iris entity.") @Data diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java new file mode 100644 index 000000000..bfd30cbca --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEnvironment.java @@ -0,0 +1,36 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.object.annotations.Desc; + +@Desc("Represents a world environment") +public enum IrisEnvironment { + @Desc("The overworld environment") + NORMAL, + + @Desc("The nether environment") + NETHER, + + @Desc("The end environment") + THE_END, + + @Desc("A custom environment") + CUSTOM +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java index 7aeb52946..c06f069e5 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java @@ -59,6 +59,7 @@ import java.util.Optional; @Data public class IrisLoot { private final transient AtomicCache chance = new AtomicCache<>(); + private final transient AtomicCache dyeColorResolved = new AtomicCache<>(); @Desc("The target inventory slot types to fill this loot with") private InventorySlotType slotTypes = InventorySlotType.STORAGE; @MinNumber(1) @@ -85,9 +86,9 @@ public class IrisLoot { private Integer customModel = null; @Desc("Set this to true to prevent it from being broken") private boolean unbreakable = false; - @ArrayType(min = 1, type = ItemFlag.class) + @ArrayType(min = 1, type = String.class) @Desc("The item flags to add") - private KList itemFlags = new KList<>(); + private KList itemFlags = new KList<>(); @Desc("Apply enchantments to this item") @ArrayType(min = 1, type = IrisEnchantment.class) private KList enchantments = new KList<>(); @@ -102,7 +103,7 @@ public class IrisLoot { @Desc("This is the item or block type. Does not accept minecraft:*, only materials such as DIAMOND_SWORD or DIRT. The exception are modded materials, as they require a namespace.") private String type = ""; @Desc("The dye color") - private DyeColor dyeColor = null; + private String dyeColor = null; @Desc("The leather armor color") private String leatherColor = null; @Desc("Defines a custom NBT Tag for the item.") @@ -112,6 +113,33 @@ public class IrisLoot { return BukkitBlockResolution.getMaterial(type); } + public DyeColor getDyeColor() { + if (dyeColor == null) { + return null; + } + return dyeColorResolved.aquire(() -> { + try { + return DyeColor.valueOf(dyeColor); + } catch (IllegalArgumentException e) { + return null; + } + }); + } + + private KList getResolvedItemFlags() { + KList resolved = new KList<>(); + for (String flag : itemFlags) { + if (flag == null) { + continue; + } + try { + resolved.add(ItemFlag.valueOf(flag)); + } catch (IllegalArgumentException ignored) { + } + } + return resolved; + } + public ItemStack get(boolean debug, RNG rng) { try { ItemStack is = getItemStack(rng); @@ -175,7 +203,7 @@ public class IrisLoot { } m.setUnbreakable(isUnbreakable()); - for (ItemFlag i : getItemFlags()) { + for (ItemFlag i : getResolvedItemFlags()) { m.addItemFlags(i); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 48d882ecb..7dccc1dc2 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -36,7 +36,6 @@ import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.common.plugin.VolmitSender; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -48,7 +47,6 @@ import java.util.Random; @Accessors(chain = true) @NoArgsConstructor -@AllArgsConstructor @Desc("Represents an iris region") @Data @EqualsAndHashCode(callSuper = false) diff --git a/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java index 8e619e5a8..eeed3a89a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java @@ -23,29 +23,28 @@ import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.studio.generators.BiomeBuffetGenerator; import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator; -import java.util.function.Consumer; - @Desc("Represents a studio mode") public enum StudioMode { - NORMAL(i -> i.setStudioGenerator(null)), - BIOME_BUFFET_1x1(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 1))), - BIOME_BUFFET_3x3(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 3))), - BIOME_BUFFET_5x5(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 5))), - BIOME_BUFFET_9x9(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 9))), - BIOME_BUFFET_18x18(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 18))), - BIOME_BUFFET_36x36(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 36))), - REGION_BUFFET(i -> i.setStudioGenerator(null)), - OBJECT_BUFFET(i -> i.setStudioGenerator(new ObjectStudioGenerator(i.getEngine()))), - - ; - - private final Consumer injector; - - StudioMode(Consumer injector) { - this.injector = injector; - } + NORMAL, + BIOME_BUFFET_1x1, + BIOME_BUFFET_3x3, + BIOME_BUFFET_5x5, + BIOME_BUFFET_9x9, + BIOME_BUFFET_18x18, + BIOME_BUFFET_36x36, + REGION_BUFFET, + OBJECT_BUFFET; public void inject(BukkitChunkGenerator c) { - injector.accept(c); + switch (this) { + case NORMAL, REGION_BUFFET -> c.setStudioGenerator(null); + case BIOME_BUFFET_1x1 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 1)); + case BIOME_BUFFET_3x3 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 3)); + case BIOME_BUFFET_5x5 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 5)); + case BIOME_BUFFET_9x9 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 9)); + case BIOME_BUFFET_18x18 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 18)); + case BIOME_BUFFET_36x36 -> c.setStudioGenerator(new BiomeBuffetGenerator(c.getEngine(), 36)); + case OBJECT_BUFFET -> c.setStudioGenerator(new ObjectStudioGenerator(c.getEngine())); + } } } diff --git a/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java b/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java index 53400adc1..983238035 100644 --- a/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java +++ b/core/src/main/java/art/arcane/iris/util/common/reflect/KeyedType.java @@ -13,6 +13,16 @@ import java.lang.reflect.Method; public class KeyedType { private static final boolean KEYED_LENIENT = Boolean.getBoolean("iris.keyed-lenient"); + private static final boolean KEYED_PRESENT = detectKeyed(); + + private static boolean detectKeyed() { + try { + Class.forName("org.bukkit.Keyed", false, KeyedType.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } public static String[] values(Class type) { if (!isKeyed(type)) return new String[0]; @@ -25,6 +35,7 @@ public class KeyedType { } public static boolean isKeyed(Class type) { + if (!KEYED_PRESENT) return false; if (KEYED_LENIENT) return !RegistryUtil.lookup(type).isEmpty(); return Keyed.class.isAssignableFrom(type); } diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java index e13793963..8ee3e0dcc 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -21,7 +21,7 @@ package art.arcane.iris.util.common.scheduling; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.platform.bukkit.BukkitPlatform; -import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.volmlib.util.function.NastyFunction; import art.arcane.volmlib.util.function.NastyFuture; @@ -56,6 +56,16 @@ public class J { private static final AtomicInteger TASK_IDS = new AtomicInteger(1); private static final Map REPEATING_CANCELLERS = new ConcurrentHashMap<>(); private static final StartupQueueSupport STARTUP_QUEUE = new StartupQueueSupport(); + private static final boolean BUKKIT_PRESENT = detectBukkit(); + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.Bukkit", false, J.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } static { SchedulerBridge.setSyncScheduler(J::s); @@ -72,7 +82,7 @@ public class J { SchedulerBridge.setInfoLogger(IrisLogging::debug); SchedulerBridge.setThreadRegistrar(thread -> { try { - IrisServices.get(PreservationSVC.class).register(thread); + IrisServices.get(PreservationRegistry.class).register(thread); } catch (Throwable e) { IrisLogging.reportError(e); } @@ -170,7 +180,7 @@ public class J { } public static boolean isFolia() { - return FoliaScheduler.isFolia(Bukkit.getServer()); + return BUKKIT_PRESENT && FoliaScheduler.isFolia(Bukkit.getServer()); } public static boolean isPrimaryThread() { @@ -570,7 +580,7 @@ public class J { } private static boolean isPluginEnabled() { - return BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin()); + return BUKKIT_PRESENT && BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin()); } private static long ticksToMilliseconds(int ticks) { diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java index 9e26ab889..262c70de0 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java @@ -33,11 +33,25 @@ import art.arcane.iris.util.common.math.IrisBlockVector; import java.io.File; public final class IrisMatterSupport { + private static final boolean BUKKIT_PRESENT = detectBukkit(); private static boolean registered; private IrisMatterSupport() { } + public static boolean isBukkitPresent() { + return BUKKIT_PRESENT; + } + + private static boolean detectBukkit() { + try { + Class.forName("org.bukkit.World", false, IrisMatterSupport.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + public static synchronized void ensureRegistered() { if (registered) { return; diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java index fc251d8b1..6a89092cc 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/EntityMatter.java @@ -21,6 +21,7 @@ package art.arcane.iris.util.project.matter.slices; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.util.project.matter.IrisMatterSupport; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.data.Varint; @@ -52,6 +53,12 @@ public class EntityMatter extends RawMatter { public EntityMatter(int width, int height, int depth) { super(width, height, depth, MatterEntityGroup.class); + if (IrisMatterSupport.isBukkitPresent()) { + registerBukkitIO(); + } + } + + private void registerBukkitIO() { registerWriter(World.class, ((w, d, x, y, z) -> { for (MatterEntity i : d.getEntities()) { Location realPosition = new Location(w, x + i.getXOff(), y + i.getYOff(), z + i.getZOff()); diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java index c52f5c668..71dee5144 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/slices/TileMatter.java @@ -20,6 +20,7 @@ package art.arcane.iris.util.project.matter.slices; import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.util.project.matter.IrisMatterSupport; import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.volmlib.util.data.palette.Palette; import art.arcane.volmlib.util.matter.Sliced; @@ -41,6 +42,12 @@ public class TileMatter extends RawMatter { public TileMatter(int width, int height, int depth) { super(width, height, depth, TileWrapper.class); + if (IrisMatterSupport.isBukkitPresent()) { + registerBukkitIO(); + } + } + + private void registerBukkitIO() { registerWriter(World.class, (w, d, x, y, z) -> TileData.setTileState(w.getBlockAt(new Location(w, x, y, z)), d.getData())); registerReader(World.class, (w, x, y, z) -> new TileWrapper(TileData.getTileState(w.getBlockAt(new Location(w, x, y, z)), false))); } diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java index 8f30a358b..bd7971a00 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedDoubleStream2D.java @@ -1,7 +1,7 @@ package art.arcane.iris.util.project.stream.utility; import art.arcane.iris.spi.IrisServices; -import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.MeteredCache; import art.arcane.iris.util.project.stream.BasicStream; @@ -19,7 +19,7 @@ public class CachedDoubleStream2D extends BasicStream implements Procedu this.stream = stream; this.engine = engine; this.cache = new WorldCache2DDouble((x, z) -> stream.getDouble(x, z), size); - IrisServices.get(PreservationSVC.class).registerCache(this); + IrisServices.get(PreservationRegistry.class).registerCache(this); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java index d30ed0a3e..9ecbfe26f 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java @@ -19,7 +19,7 @@ package art.arcane.iris.util.project.stream.utility; import art.arcane.iris.spi.IrisServices; -import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.MeteredCache; import art.arcane.volmlib.util.cache.ChunkCache2D; @@ -38,7 +38,7 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea this.stream = stream; this.engine = engine; cache = new WorldCache2D<>(stream::get, size, () -> new ChunkCache2D<>("iris")); - IrisServices.get(PreservationSVC.class).registerCache(this); + IrisServices.get(PreservationRegistry.class).registerCache(this); } @Override diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java index ac33bb251..35593bf93 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream3D.java @@ -19,7 +19,7 @@ package art.arcane.iris.util.project.stream.utility; import art.arcane.iris.spi.IrisServices; -import art.arcane.iris.core.service.PreservationSVC; +import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.MeteredCache; import art.arcane.volmlib.util.data.KCache; @@ -36,7 +36,7 @@ public class CachedStream3D extends BasicStream implements ProceduralStrea this.stream = stream; this.engine = engine; cache = new KCache<>((k) -> stream.get(k.getX(), k.getY(), k.getZ()), size); - IrisServices.get(PreservationSVC.class).registerCache(this); + IrisServices.get(PreservationRegistry.class).registerCache(this); } @Override diff --git a/probe/build.gradle b/probe/build.gradle index dbd0a046b..a993b86e2 100644 --- a/probe/build.gradle +++ b/probe/build.gradle @@ -9,8 +9,17 @@ application { mainClass = 'art.arcane.iris.probe.ClassloadProbe' } +String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') + .orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT') + .get() + dependencies { implementation(project(':core')) + compileOnly(libs.spigot) + compileOnly(volmLibCoordinate) { + changing = true + transitive = false + } runtimeOnly(libs.paralithic) runtimeOnly(libs.gson) runtimeOnly(libs.lru) @@ -35,3 +44,18 @@ tasks.named('run', JavaExec).configure { jvmArgs('--add-modules', 'jdk.incubator.vector') dependsOn(':core:compileJava') } + +String probePack = providers.gradleProperty('probePack') + .orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.1.2/plugins/Iris/packs/overworld') + .get() +String probeRadius = providers.gradleProperty('probeRadius').orElse('2').get() + +tasks.register('genProbe', JavaExec) { + group = 'verification' + description = 'Constructs a real Iris engine over a real pack and generates chunks into buffers on a Bukkit-free JVM.' + mainClass = 'art.arcane.iris.probe.GenerationProbe' + classpath = sourceSets.main.runtimeClasspath + jvmArgs('--add-modules', 'jdk.incubator.vector') + args(probePack, probeRadius) + dependsOn(':core:compileJava') +} diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java new file mode 100644 index 000000000..3da84db28 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -0,0 +1,339 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.probe; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisEngine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.EngineWorldManagerProvider; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import org.bukkit.Chunk; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class GenerationProbe { + private static final String DIMENSION_KEY = "overworld"; + private static final long SEED = 1337L; + private static final int BIOME_STEP = 4; + private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); + + private static final class InertPreservation implements PreservationRegistry { + @Override + public void register(Thread thread) { + } + + @Override + public void registerCache(MeteredCache cache) { + } + + @Override + public void dereference() { + } + } + + private static final class InertWorldManager implements EngineWorldManager { + @Override + public void close() { + } + + @Override + public int getEntityCount() { + return 0; + } + + @Override + public int getChunkCount() { + return 0; + } + + @Override + public double getEntitySaturation() { + return 0; + } + + @Override + public void onTick() { + } + + @Override + public void onSave() { + } + + @Override + public void onBlockBreak(BlockBreakEvent e) { + } + + @Override + public void onBlockPlace(BlockPlaceEvent e) { + } + + @Override + public void onChunkLoad(Chunk e, boolean generated) { + } + + @Override + public void onChunkUnload(Chunk e) { + } + + @Override + public void teleportAsync(PlayerTeleportEvent e) { + } + } + + public static void main(String[] args) throws Exception { + IrisPlatforms.bind(new StubPlatform()); + StubPlatform.verbose(true); + StubPlatform.errorSink(REPORTED::add); + IrisServices.register(PreservationRegistry.class, new InertPreservation()); + IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager()); + + File packSource = new File(args[0]); + int radius = args.length > 1 ? Integer.parseInt(args[1]) : 2; + if (!packSource.isDirectory()) { + System.out.println("[genprobe] pack folder not found: " + packSource.getAbsolutePath()); + System.exit(1); + } + + File workRoot = Files.createTempDirectory("iris-genprobe").toFile(); + File pack = clonePack(packSource, workRoot); + System.out.println("[genprobe] pack: " + packSource.getAbsolutePath()); + System.out.println("[genprobe] work copy: " + pack.getAbsolutePath()); + System.out.println("[genprobe] radius: " + radius + " (" + ((2 * radius + 1) * (2 * radius + 1)) + " chunks)"); + + Engine engine; + try { + IrisData data = IrisData.get(pack); + IrisDimension dimension = data.getDimensionLoader().load(DIMENSION_KEY); + if (dimension == null) { + System.out.println("[genprobe] FAIL: dimension '" + DIMENSION_KEY + "' did not load from " + pack.getAbsolutePath()); + System.exit(1); + return; + } + + IrisWorld world = IrisWorld.builder() + .name("probe") + .seed(SEED) + .worldFolder(new File(workRoot, "world")) + .minHeight(dimension.getMinHeight()) + .maxHeight(dimension.getMaxHeight()) + .build(); + EngineTarget target = new EngineTarget(world, dimension, data); + engine = new IrisEngine(target, false); + } catch (Throwable e) { + System.out.println("[genprobe] FAIL: engine construction threw before generation could start"); + e.printStackTrace(System.out); + printDistinctCauses("construction-time reported errors", drainReported()); + System.exit(1); + return; + } + + List initNoise = settleAndDrain(); + printDistinctCauses("engine-init reported errors (non-fatal, async)", initNoise); + + int height = engine.getTarget().getHeight(); + System.out.println("[genprobe] engine up: dim=" + engine.getDimension().getLoadKey() + + " seed=" + engine.getSeedManager().getSeed() + + " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight()); + + Map distinct = new LinkedHashMap<>(); + Map firstChunk = new LinkedHashMap<>(); + int ok = 0; + int failed = 0; + + for (int cz = -radius; cz <= radius; cz++) { + for (int cx = -radius; cx <= radius; cx++) { + String at = cx + "," + cz; + drainReported(); + List failures = new ArrayList<>(); + Hunk blocks = Hunk.newArrayHunk(16, height, 16); + Hunk biomes = Hunk.newArrayHunk(16, height, 16); + try { + engine.generate(cx << 4, cz << 4, blocks, biomes, false); + } catch (Throwable e) { + failures.add(e); + } + failures.addAll(drainReported()); + + if (failures.isEmpty()) { + ok++; + System.out.println("[genprobe] chunk " + at + " OK " + hashChunk(blocks, biomes, height)); + } else { + failed++; + System.out.println("[genprobe] chunk " + at + " FAILED (" + failures.size() + " error(s))"); + for (Throwable failure : failures) { + failure.printStackTrace(System.out); + String key = causeKey(failure); + distinct.merge(key, 1, Integer::sum); + firstChunk.putIfAbsent(key, at); + } + } + } + } + + System.out.println("[genprobe] generated OK: " + ok + ", failed: " + failed); + if (!distinct.isEmpty()) { + System.out.println("[genprobe] DISTINCT ROOT CAUSES (" + distinct.size() + "):"); + for (Map.Entry entry : distinct.entrySet()) { + System.out.println(" x" + entry.getValue() + " (first at chunk " + firstChunk.get(entry.getKey()) + ") " + entry.getKey()); + } + } + System.out.println("[genprobe] RESULT: " + (failed == 0 ? "PASS" : "FAIL")); + System.exit(failed == 0 ? 0 : 1); + } + + private static File clonePack(File source, File workRoot) throws Exception { + File destination = new File(workRoot, source.getName()); + Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath()) + .inheritIO() + .start(); + if (clone.waitFor() != 0) { + Process copy = new ProcessBuilder("cp", "-R", source.getAbsolutePath(), destination.getAbsolutePath()) + .inheritIO() + .start(); + if (copy.waitFor() != 0) { + throw new IllegalStateException("Failed to copy pack to " + destination.getAbsolutePath()); + } + } + return destination; + } + + private static List settleAndDrain() throws InterruptedException { + List drained = new ArrayList<>(); + long quietSince = System.currentTimeMillis(); + long start = quietSince; + while (System.currentTimeMillis() - start < 15000L) { + List batch = drainReported(); + if (batch.isEmpty()) { + if (System.currentTimeMillis() - quietSince >= 1500L) { + break; + } + } else { + drained.addAll(batch); + quietSince = System.currentTimeMillis(); + } + Thread.sleep(50L); + } + return drained; + } + + private static List drainReported() { + synchronized (REPORTED) { + List drained = new ArrayList<>(REPORTED); + REPORTED.clear(); + return drained; + } + } + + private static void printDistinctCauses(String label, List errors) { + if (errors.isEmpty()) { + return; + } + Map distinct = new LinkedHashMap<>(); + for (Throwable error : errors) { + distinct.merge(causeKey(error), 1, Integer::sum); + } + System.out.println("[genprobe] " + label + " (" + distinct.size() + " distinct):"); + for (Map.Entry entry : distinct.entrySet()) { + System.out.println(" x" + entry.getValue() + " " + entry.getKey()); + } + for (Throwable error : errors) { + error.printStackTrace(System.out); + } + } + + private static String causeKey(Throwable failure) { + List chain = new ArrayList<>(); + Throwable cause = failure; + while (cause != null && !chain.contains(cause)) { + chain.add(cause); + cause = cause.getCause(); + } + Throwable root = chain.get(chain.size() - 1); + return root.getClass().getName() + ": " + root.getMessage() + " @ " + siteOf(chain); + } + + private static String siteOf(List chain) { + for (int i = chain.size() - 1; i >= 0; i--) { + for (StackTraceElement frame : chain.get(i).getStackTrace()) { + String className = frame.getClassName(); + if (className.startsWith("art.arcane.") && !className.startsWith("art.arcane.iris.probe.")) { + return frame.toString(); + } + } + } + StackTraceElement[] trace = chain.get(chain.size() - 1).getStackTrace(); + return trace.length > 0 ? trace[0].toString() : ""; + } + + private static String hashChunk(Hunk blocks, Hunk biomes, int height) { + MessageDigest blockDigest = sha256(); + MessageDigest biomeDigest = sha256(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < height; y++) { + PlatformBlockState state = blocks.get(x, y, z); + String key = state == null ? "minecraft:air" : state.key(); + blockDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8)); + } + } + } + for (int x = 0; x < 16; x += BIOME_STEP) { + for (int z = 0; z < 16; z += BIOME_STEP) { + for (int y = 0; y < height; y += BIOME_STEP) { + PlatformBiome biome = biomes.get(x, y, z); + String key = biome == null ? "null" : biome.key(); + biomeDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8)); + } + } + } + return HexFormat.of().formatHex(blockDigest.digest()).substring(0, 16) + + " " + HexFormat.of().formatHex(biomeDigest.digest()).substring(0, 16); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java index 3a94b8f13..57cede732 100644 --- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java +++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java @@ -29,13 +29,31 @@ import art.arcane.iris.spi.PlatformItem; import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformScheduler; import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; import java.io.File; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; public final class StubPlatform implements IrisPlatform { + private static volatile boolean VERBOSE = false; + private static volatile Consumer ERROR_SINK = null; + + public static void verbose(boolean verbose) { + VERBOSE = verbose; + } + + public static void errorSink(Consumer sink) { + ERROR_SINK = sink; + } + private final StubRegistries registries = new StubRegistries(); + private final StubScheduler scheduler = new StubScheduler(); + private final PlatformCapabilities capabilities = new PlatformCapabilities() { + }; + private final StubStructureHooks structureHooks = new StubStructureHooks(); + private final StubBiomeWriter biomeWriter = new StubBiomeWriter(); private static final class StubBlockState implements PlatformBlockState { private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); @@ -161,6 +179,119 @@ public final class StubPlatform implements IrisPlatform { } } + private static final class StubBiome implements PlatformBiome { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + private final String key; + + private StubBiome(String key) { + this.key = key; + } + + static StubBiome of(String key) { + return CACHE.computeIfAbsent(key, StubBiome::new); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + int colon = key.indexOf(':'); + return colon >= 0 ? key.substring(0, colon) : "minecraft"; + } + + @Override + public Object nativeHandle() { + return key; + } + } + + private static final class StubScheduler implements PlatformScheduler { + @Override + public void global(Runnable task) { + task.run(); + } + + @Override + public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) { + task.run(); + } + + @Override + public void async(Runnable task) { + task.run(); + } + + @Override + public void laterGlobal(Runnable task, int ticks) { + } + + @Override + public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) { + } + } + + private static final class StubStructureHooks implements PlatformStructureHooks { + @Override + public List structureKeys() { + return List.of(); + } + + @Override + public List structureSetKeys() { + return List.of(); + } + + @Override + public List structureBiomeKeys(String structureKey) { + return List.of(); + } + + @Override + public List objectFeatureKeys() { + return List.of(); + } + + @Override + public List reachableStructureKeys(PlatformWorld world) { + return List.of(); + } + + @Override + public List possibleBiomeKeys(PlatformWorld world) { + return List.of(); + } + + @Override + public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) { + return false; + } + + @Override + public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + return null; + } + + @Override + public boolean supportsStructurePlacement() { + return false; + } + } + + private static final class StubBiomeWriter implements PlatformBiomeWriter { + @Override + public int biomeIdFor(String key) { + return 0; + } + + @Override + public List allBiomes() { + return List.of(); + } + } + private static final class StubRegistries implements PlatformRegistries { @Override public PlatformBlockState block(String key) { @@ -189,7 +320,7 @@ public final class StubPlatform implements IrisPlatform { @Override public PlatformBiome biome(String key) { - return null; + return StubBiome.of(key); } @Override @@ -235,22 +366,22 @@ public final class StubPlatform implements IrisPlatform { @Override public PlatformScheduler scheduler() { - return null; + return scheduler; } @Override public PlatformCapabilities capabilities() { - return null; + return capabilities; } @Override public PlatformStructureHooks structureHooks() { - return null; + return structureHooks; } @Override public PlatformBiomeWriter biomeWriter() { - return null; + return biomeWriter; } @Override @@ -288,13 +419,23 @@ public final class StubPlatform implements IrisPlatform { @Override public void log(LogLevel level, String message) { + if (VERBOSE) { + System.out.println("[stub/" + level + "] " + message); + } } @Override public void msg(String message) { + if (VERBOSE) { + System.out.println("[stub/MSG] " + message); + } } @Override public void reportError(Throwable error) { + Consumer sink = ERROR_SINK; + if (sink != null && error != null) { + sink.accept(error); + } } }