Headless Generation

This commit is contained in:
Brian Neumann-Fopiano
2026-06-11 21:32:54 -04:00
parent 6b24105640
commit f32094a404
26 changed files with 816 additions and 80 deletions
@@ -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);
@@ -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<T extends IrisRegistrant> 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() {
@@ -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<Thread> threads = new CopyOnWriteArrayList<>();
private final List<ExecutorService> services = new CopyOnWriteArrayList<>();
private final List<WeakReference<MeteredCache>> caches = new CopyOnWriteArrayList<>();
@@ -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!");
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
public interface PreservationRegistry {
void register(Thread thread);
void registerCache(MeteredCache cache);
void dereference();
}
@@ -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<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<EnumMap<IrisDecorationPart, IrisDecorator[]>> decoratorBuckets = new AtomicCache<>();
private final transient AtomicCache<Biome> derivativeResolved = new AtomicCache<>();
private final transient AtomicCache<Biome> vanillaDerivativeResolved = new AtomicCache<>();
private final transient AtomicCache<KList<Biome>> biomeScatterResolved = new AtomicCache<>();
private final transient AtomicCache<KList<Biome>> 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<Biome> biomeScatter = new KList<>();
@ArrayType(min = 1, type = Biome.class)
private KList<String> 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<Biome> biomeSkyScatter = new KList<>();
private KList<String> 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<Biome> getBiomeScatterResolved() {
return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter));
}
private KList<Biome> getBiomeSkyScatterResolved() {
return biomeSkyScatterResolved.aquire(() -> resolveBiomeKeys(biomeSkyScatter));
}
private static KList<Biome> resolveBiomeKeys(KList<String> keys) {
KList<Biome> 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) {
@@ -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<Particle> 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);
});
}
}
@@ -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<EntityType> 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")
@@ -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;
@@ -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<PotionEffectType> pt = new AtomicCache<>();
private final transient AtomicCache<ChronoLatch> latch = new AtomicCache<>();
private final transient AtomicCache<Particle> particleEffectResolved = new AtomicCache<>();
private final transient AtomicCache<Sound> 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> T resolveKeyed(Class<T> 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,
@@ -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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -59,6 +59,7 @@ import java.util.Optional;
@Data
public class IrisLoot {
private final transient AtomicCache<CNG> chance = new AtomicCache<>();
private final transient AtomicCache<DyeColor> 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<ItemFlag> itemFlags = new KList<>();
private KList<String> itemFlags = new KList<>();
@Desc("Apply enchantments to this item")
@ArrayType(min = 1, type = IrisEnchantment.class)
private KList<IrisEnchantment> 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<ItemFlag> getResolvedItemFlags() {
KList<ItemFlag> 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);
}
@@ -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)
@@ -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<BukkitChunkGenerator> injector;
StudioMode(Consumer<BukkitChunkGenerator> 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()));
}
}
}
@@ -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);
}
@@ -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<Integer, Runnable> 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) {
@@ -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;
@@ -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<MatterEntityGroup> {
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());
@@ -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<TileWrapper> {
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)));
}
@@ -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<Double> 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
@@ -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<T> extends BasicStream<T> 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
@@ -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<T> extends BasicStream<T> 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
+24
View File
@@ -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')
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Throwable> 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<Throwable> 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<String, Integer> distinct = new LinkedHashMap<>();
Map<String, String> 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<Throwable> failures = new ArrayList<>();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<PlatformBiome> 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<String, Integer> 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<Throwable> settleAndDrain() throws InterruptedException {
List<Throwable> drained = new ArrayList<>();
long quietSince = System.currentTimeMillis();
long start = quietSince;
while (System.currentTimeMillis() - start < 15000L) {
List<Throwable> 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<Throwable> drainReported() {
synchronized (REPORTED) {
List<Throwable> drained = new ArrayList<>(REPORTED);
REPORTED.clear();
return drained;
}
}
private static void printDistinctCauses(String label, List<Throwable> errors) {
if (errors.isEmpty()) {
return;
}
Map<String, Integer> 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<String, Integer> 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<Throwable> 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<Throwable> 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() : "<no frames>";
}
private static String hashChunk(Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> 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);
}
}
}
@@ -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<Throwable> ERROR_SINK = null;
public static void verbose(boolean verbose) {
VERBOSE = verbose;
}
public static void errorSink(Consumer<Throwable> 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<String, StubBlockState> CACHE = new ConcurrentHashMap<>();
@@ -161,6 +179,119 @@ public final class StubPlatform implements IrisPlatform {
}
}
private static final class StubBiome implements PlatformBiome {
private static final ConcurrentHashMap<String, StubBiome> 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<String> structureKeys() {
return List.of();
}
@Override
public List<String> structureSetKeys() {
return List.of();
}
@Override
public List<String> structureBiomeKeys(String structureKey) {
return List.of();
}
@Override
public List<String> objectFeatureKeys() {
return List.of();
}
@Override
public List<String> reachableStructureKeys(PlatformWorld world) {
return List.of();
}
@Override
public List<String> 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<PlatformBiome> 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<Throwable> sink = ERROR_SINK;
if (sink != null && error != null) {
sink.accept(error);
}
}
}