mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
dwa
This commit is contained in:
@@ -124,7 +124,7 @@ public final class DatapackIngestService {
|
||||
Manifest manifest = readManifest(root);
|
||||
boolean stripOverrides = resolveStripOverrides();
|
||||
|
||||
message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + (stripOverrides ? C.GRAY + " (datapackOverrides=false: vanilla-key overrides will be stripped)" : "") + "...");
|
||||
message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + (stripOverrides ? C.GRAY + " (datapackOverrides=false: minecraft-namespaced structure overrides will be stripped)" : "") + "...");
|
||||
|
||||
for (String url : urls) {
|
||||
try {
|
||||
@@ -142,7 +142,7 @@ public final class DatapackIngestService {
|
||||
if (report.changed()) {
|
||||
message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate.");
|
||||
message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import <dimension> to import everything on demand. Reference an imported key from a 'structures' placement to position it manually.");
|
||||
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys and generate their own structures by default; set the dimension 'importedStructures.datapackOverrides' to false to keep vanilla untouched and stop ALL datapack structures from generating - they stay importable for manual placement only.");
|
||||
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; control non-minecraft datapack and mod structures with importedStructures.enabled/disabled.");
|
||||
if (restart) {
|
||||
ServerConfigurator.restart();
|
||||
} else {
|
||||
|
||||
@@ -36,8 +36,6 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -218,14 +216,6 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
return engine.getWorld().name();
|
||||
}
|
||||
|
||||
public boolean targetsWorld(WorldInfo world) {
|
||||
if (world == null || engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return WorldIdentity.key(world).equals(engine.getWorld().key());
|
||||
}
|
||||
|
||||
public boolean targetsWorldIdentity(String worldIdentity) {
|
||||
if (worldIdentity == null || engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
|
||||
@@ -7,13 +7,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class WorldLifecycleStaging {
|
||||
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
|
||||
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
|
||||
private static final AtomicReference<ChunkGenerator> pendingStemGenerator = new AtomicReference<>();
|
||||
|
||||
private WorldLifecycleStaging() {
|
||||
}
|
||||
@@ -29,7 +27,6 @@ public final class WorldLifecycleStaging {
|
||||
|
||||
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
|
||||
stagedStemGenerators.put(worldName, generator);
|
||||
pendingStemGenerator.set(generator);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -44,16 +41,7 @@ public final class WorldLifecycleStaging {
|
||||
|
||||
@Nullable
|
||||
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
|
||||
ChunkGenerator generator = stagedStemGenerators.remove(worldName);
|
||||
if (generator != null) {
|
||||
pendingStemGenerator.compareAndSet(generator, null);
|
||||
return generator;
|
||||
}
|
||||
ChunkGenerator pending = pendingStemGenerator.getAndSet(null);
|
||||
if (pending != null) {
|
||||
stagedStemGenerators.values().remove(pending);
|
||||
}
|
||||
return pending;
|
||||
return stagedStemGenerators.remove(worldName);
|
||||
}
|
||||
|
||||
public static void clearGenerator(@NotNull String worldName) {
|
||||
@@ -62,10 +50,7 @@ public final class WorldLifecycleStaging {
|
||||
}
|
||||
|
||||
public static void clearStem(@NotNull String worldName) {
|
||||
ChunkGenerator generator = stagedStemGenerators.remove(worldName);
|
||||
if (generator != null) {
|
||||
pendingStemGenerator.compareAndSet(generator, null);
|
||||
}
|
||||
stagedStemGenerators.remove(worldName);
|
||||
}
|
||||
|
||||
public static void clearAll(@NotNull String worldName) {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
//package art.arcane.iris.core.link;
|
||||
//
|
||||
//import com.jojodmo.customitems.api.CustomItemsAPI;
|
||||
//import com.jojodmo.customitems.item.custom.CustomItem;
|
||||
//import com.jojodmo.customitems.item.custom.block.CustomMushroomBlock;
|
||||
//import com.jojodmo.customitems.version.SafeMaterial;
|
||||
//import art.arcane.volmlib.util.collection.KList;
|
||||
//import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
//import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
//import org.bukkit.block.BlockFace;
|
||||
//import org.bukkit.block.data.BlockData;
|
||||
//import org.bukkit.block.data.MultipleFacing;
|
||||
//import org.bukkit.inventory.ItemStack;
|
||||
//
|
||||
//import java.util.Map;
|
||||
//import java.util.MissingResourceException;
|
||||
//
|
||||
//public class CustomItemsDataProvider extends ExternalDataProvider {
|
||||
//
|
||||
// private static final String FIELD_FACES = "faces";
|
||||
// private static final String METHOD_GET_MATERIAL = "getMaterial";
|
||||
//
|
||||
// private WrappedField<CustomMushroomBlock, Map<Integer, boolean[]>> mushroomFaces;
|
||||
// private WrappedReturningMethod<CustomMushroomBlock, SafeMaterial> mushroomMaterial;
|
||||
//
|
||||
// public CustomItemsDataProvider() {
|
||||
// super("CustomItems");
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void init() {
|
||||
// this.mushroomFaces = new WrappedField<>(CustomMushroomBlock.class, FIELD_FACES);
|
||||
// this.mushroomMaterial = new WrappedReturningMethod<>(CustomMushroomBlock.class, METHOD_GET_MATERIAL);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public BlockData getBlockData(Identifier blockId) throws MissingResourceException {
|
||||
// CustomItem item = CustomItem.get(blockId.key());
|
||||
// if(item == null) {
|
||||
// throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
// } else if(item.getBlockTexture().isSpawner()) {
|
||||
// throw new MissingResourceException("Iris does not yet support SpawnerBlocks from CustomItems.", blockId.namespace(), blockId.key());
|
||||
// } else if(item.getBlockTexture() != null && item.getBlockTexture().isValid()) {
|
||||
// throw new MissingResourceException("Tried to fetch BlockData for a CustomItem that is not placeable!", blockId.namespace(), blockId.key());
|
||||
// }
|
||||
// return getMushroomData(item);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public ItemStack getItemStack(Identifier itemId) throws MissingResourceException {
|
||||
// ItemStack stack = CustomItemsAPI.getCustomItem(itemId.key());
|
||||
// if(stack == null) {
|
||||
// throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
// }
|
||||
// return stack;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Identifier[] getBlockTypes() {
|
||||
// KList<Identifier> names = new KList<>();
|
||||
// for (String name : CustomItemsAPI.listBlockCustomItemIDs()) {
|
||||
// try {
|
||||
// Identifier key = new Identifier("cui", name);
|
||||
// if (getItemStack(key) != null)
|
||||
// names.add(key);
|
||||
// } catch (MissingResourceException ignored) { }
|
||||
// }
|
||||
//
|
||||
// return names.toArray(new Identifier[0]);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Identifier[] getItemTypes() {
|
||||
// KList<Identifier> names = new KList<>();
|
||||
// for (String name : CustomItemsAPI.listCustomItemIDs()) {
|
||||
// try {
|
||||
// Identifier key = new Identifier("cui", name);
|
||||
// if (getItemStack(key) != null)
|
||||
// names.add(key);
|
||||
// } catch (MissingResourceException ignored) { }
|
||||
// }
|
||||
//
|
||||
// return names.toArray(new Identifier[0]);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public boolean isValidProvider(Identifier key, boolean isItem) {
|
||||
// return key.namespace().equalsIgnoreCase("cui");
|
||||
// }
|
||||
//
|
||||
// private BlockData getMushroomData(CustomItem item) {
|
||||
// MultipleFacing data = (MultipleFacing)mushroomMaterial.invoke(item.getBlockTexture().getMushroomId()).parseMaterial().createBlockData();
|
||||
// boolean[] values = mushroomFaces.get().get(item.getBlockTexture().getMushroomId());
|
||||
// data.setFace(BlockFace.DOWN, values[0]);
|
||||
// data.setFace(BlockFace.EAST, values[1]);
|
||||
// data.setFace(BlockFace.NORTH, values[2]);
|
||||
// data.setFace(BlockFace.SOUTH, values[3]);
|
||||
// data.setFace(BlockFace.UP, values[4]);
|
||||
// data.setFace(BlockFace.WEST, values[5]);
|
||||
// return data;
|
||||
// }
|
||||
//}
|
||||
@@ -18,7 +18,6 @@ import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -31,7 +30,7 @@ import java.util.MissingResourceException;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ExternalDataProvider implements Listener {
|
||||
public abstract class ExternalDataProvider {
|
||||
|
||||
@NonNull
|
||||
private final String pluginId;
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
|
||||
public record Identifier(String namespace, String key) {
|
||||
|
||||
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||
|
||||
public static Identifier fromNamespacedKey(NamespacedKey key) {
|
||||
return new Identifier(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
public static Identifier fromString(String id) {
|
||||
String[] strings = id.split(":", 2);
|
||||
if (strings.length == 1) {
|
||||
return new Identifier(DEFAULT_NAMESPACE, strings[0]);
|
||||
} else {
|
||||
return new Identifier(strings[0], strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return namespace + ":" + key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier i) {
|
||||
return i.namespace().equals(this.namespace) && i.key().equals(this.key);
|
||||
} else if (obj instanceof NamespacedKey i) {
|
||||
return i.getNamespace().equals(this.namespace) && i.getKey().equals(this.key);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
public record Identifier(String namespace, String key) {
|
||||
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||
|
||||
public static Identifier fromString(String id) {
|
||||
String[] strings = id.split(":", 2);
|
||||
if (strings.length == 1) {
|
||||
return new Identifier(DEFAULT_NAMESPACE, strings[0]);
|
||||
}
|
||||
return new Identifier(strings[0], strings[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return namespace + ":" + key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,43 +25,48 @@ import org.mvplugins.multiverse.core.MultiverseCoreApi;
|
||||
import org.mvplugins.multiverse.core.world.MultiverseWorld;
|
||||
import org.mvplugins.multiverse.core.world.WorldManager;
|
||||
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
|
||||
import org.mvplugins.multiverse.core.world.options.RemoveWorldOptions;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class MultiverseCoreLink {
|
||||
private final boolean active;
|
||||
|
||||
public MultiverseCoreLink() {
|
||||
active = Bukkit.getPluginManager().getPlugin("Multiverse-Core") != null;
|
||||
}
|
||||
|
||||
public void removeFromConfig(World world) {
|
||||
removeFromConfig(world.getName());
|
||||
}
|
||||
|
||||
public void removeFromConfig(String world) {
|
||||
if (!active) return;
|
||||
var manager = worldManager();
|
||||
manager.removeWorld(world).onSuccess(manager::saveWorldsConfig);
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
WorldManager manager = worldManager();
|
||||
MultiverseWorld multiverseWorld = manager.getWorld(world).getOrElse((MultiverseWorld) null);
|
||||
if (multiverseWorld == null) {
|
||||
return;
|
||||
}
|
||||
manager.removeWorld(RemoveWorldOptions.world(multiverseWorld)).onSuccess(ignored -> manager.saveWorldsConfig());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void updateWorld(World bukkitWorld, String pack) {
|
||||
if (!active) return;
|
||||
var generator = "Iris:" + pack;
|
||||
var manager = worldManager();
|
||||
var world = manager.getWorld(bukkitWorld).getOrElse(() -> {
|
||||
var options = ImportWorldOptions.worldName(bukkitWorld.getName())
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
String generator = "Iris:" + pack;
|
||||
WorldManager manager = worldManager();
|
||||
MultiverseWorld multiverseWorld = manager.getWorld(bukkitWorld).getOrElse(() -> {
|
||||
ImportWorldOptions options = ImportWorldOptions.worldName(bukkitWorld.getName())
|
||||
.generator(generator)
|
||||
.environment(bukkitWorld.getEnvironment())
|
||||
.useSpawnAdjust(false);
|
||||
return manager.importWorld(options).get();
|
||||
});
|
||||
|
||||
world.setAutoLoad(false);
|
||||
if (!generator.equals(world.getGenerator())) {
|
||||
var field = MultiverseWorld.class.getDeclaredField("worldConfig");
|
||||
multiverseWorld.setAutoLoad(false);
|
||||
if (!generator.equals(multiverseWorld.getGenerator())) {
|
||||
Field field = MultiverseWorld.class.getDeclaredField("worldConfig");
|
||||
field.setAccessible(true);
|
||||
|
||||
var config = field.get(world);
|
||||
Object config = field.get(multiverseWorld);
|
||||
config.getClass()
|
||||
.getDeclaredMethod("setGenerator", String.class)
|
||||
.invoke(config, generator);
|
||||
@@ -71,7 +76,11 @@ public class MultiverseCoreLink {
|
||||
}
|
||||
|
||||
private WorldManager worldManager() {
|
||||
var api = MultiverseCoreApi.get();
|
||||
MultiverseCoreApi api = MultiverseCoreApi.get();
|
||||
return api.getWorldManager();
|
||||
}
|
||||
|
||||
private boolean isActive() {
|
||||
return Bukkit.getPluginManager().isPluginEnabled("Multiverse-Core");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ import art.arcane.volmlib.util.math.RNG;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.block.properties.BooleanProperty;
|
||||
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
|
||||
import net.momirealms.craftengine.core.block.properties.Property;
|
||||
import net.momirealms.craftengine.core.block.property.BooleanProperty;
|
||||
import net.momirealms.craftengine.core.block.property.IntegerProperty;
|
||||
import net.momirealms.craftengine.core.block.property.Property;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
@@ -52,12 +55,12 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
Key key = Key.of(blockId.namespace(), blockId.key());
|
||||
net.momirealms.craftengine.core.block.CustomBlock block = CraftEngineBlocks.byId(key);
|
||||
BlockDefinition block = CraftEngineBlocks.byId(key);
|
||||
if (block != null) {
|
||||
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
|
||||
}
|
||||
|
||||
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
|
||||
if (furniture != null) {
|
||||
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
|
||||
properties[4] = new BlockProperty(
|
||||
@@ -75,12 +78,12 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
net.momirealms.craftengine.core.item.CustomItem<ItemStack> item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
||||
BukkitItemDefinition item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
||||
if (item == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
|
||||
return item.buildItemStack();
|
||||
return item.buildBukkitItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,7 +103,7 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||
KMap<String, String> state = statePair.getB();
|
||||
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
|
||||
|
||||
net.momirealms.craftengine.core.block.CustomBlock customBlock = CraftEngineBlocks.byId(key);
|
||||
BlockDefinition customBlock = CraftEngineBlocks.byId(key);
|
||||
if (customBlock != null) {
|
||||
ImmutableBlockState blockState = customBlock.defaultState();
|
||||
|
||||
@@ -122,7 +125,7 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
|
||||
if (furniture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import com.willfp.ecoitems.items.EcoItem;
|
||||
import com.willfp.ecoitems.items.EcoItems;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import com.willfp.eco.core.items.CustomItem;
|
||||
import com.willfp.eco.core.items.Items;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -16,9 +14,6 @@ import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class EcoItemsDataProvider extends ExternalDataProvider {
|
||||
private WrappedField<EcoItem, ItemStack> itemStack;
|
||||
private WrappedField<EcoItem, NamespacedKey> id;
|
||||
|
||||
public EcoItemsDataProvider() {
|
||||
super("EcoItems");
|
||||
}
|
||||
@@ -26,30 +21,26 @@ public class EcoItemsDataProvider extends ExternalDataProvider {
|
||||
@Override
|
||||
public void init() {
|
||||
IrisLogging.info("Setting up EcoItems Link...");
|
||||
itemStack = new WrappedField<>(EcoItem.class, "_itemStack");
|
||||
if (this.itemStack.hasFailed()) {
|
||||
IrisLogging.error("Failed to set up EcoItems Link: Unable to fetch ItemStack field!");
|
||||
}
|
||||
id = new WrappedField<>(EcoItem.class, "id");
|
||||
if (this.id.hasFailed()) {
|
||||
IrisLogging.error("Failed to set up EcoItems Link: Unable to fetch id field!");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
EcoItem item = EcoItems.INSTANCE.getByID(itemId.key());
|
||||
if (item == null) throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key());
|
||||
return itemStack.get(item).clone();
|
||||
ItemStack item = Items.lookup(itemId.namespace() + ":" + itemId.key()).getItem();
|
||||
if (item.getType().isAir()) {
|
||||
throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ITEM) return List.of();
|
||||
return EcoItems.INSTANCE.values()
|
||||
return Items.getCustomItems()
|
||||
.stream()
|
||||
.map(x -> Identifier.fromNamespacedKey(id.get(x)))
|
||||
.map(CustomItem::getKey)
|
||||
.filter(key -> key.getNamespace().equalsIgnoreCase("ecoitems"))
|
||||
.map(key -> new Identifier(key.getNamespace(), key.getKey()))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class HMCLeavesDataProvider extends ExternalDataProvider {
|
||||
private Object apiInstance;
|
||||
private WrappedReturningMethod<Object, Material> worldBlockType;
|
||||
private WrappedReturningMethod<Object, Boolean> setCustomBlock;
|
||||
private Map<String, Object> blockDataMap = Map.of();
|
||||
private Map<String, Supplier<ItemStack>> itemDataField = Map.of();
|
||||
|
||||
public HMCLeavesDataProvider() {
|
||||
super("HMCLeaves");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return "HMCLeaves";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
worldBlockType = new WrappedReturningMethod<>((Class<Object>) Class.forName("io.github.fisher2911.hmcleaves.data.BlockData"), "worldBlockType");
|
||||
apiInstance = getApiInstance(Class.forName("io.github.fisher2911.hmcleaves.api.HMCLeavesAPI"));
|
||||
setCustomBlock = new WrappedReturningMethod<>((Class<Object>) apiInstance.getClass(), "setCustomBlock", Location.class, String.class, boolean.class);
|
||||
Object config = getLeavesConfig(apiInstance.getClass());
|
||||
blockDataMap = getMap(config, "blockDataMap");
|
||||
itemDataField = getMap(config, "itemSupplierMap");
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Failed to initialize HMCLeavesDataProvider: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
Object o = blockDataMap.get(blockId.key());
|
||||
if (o == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
Material material = worldBlockType.invoke(o, new Object[0]);
|
||||
if (material == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
BlockData blockData = Bukkit.createBlockData(material);
|
||||
if (IrisSettings.get().getGenerator().preventLeafDecay && blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
return IrisCustomData.of(blockData, ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
if (!itemDataField.containsKey(itemId.key()))
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return itemDataField.get(itemId.key()).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var pair = ExternalDataSVC.parseState(blockId);
|
||||
blockId = pair.getA();
|
||||
Boolean result = setCustomBlock.invoke(apiInstance, new Object[]{block.getLocation(), blockId.key(), false});
|
||||
if (result == null || !result)
|
||||
IrisLogging.warn("Failed to set custom block! " + blockId.key() + " " + block.getX() + " " + block.getY() + " " + block.getZ());
|
||||
else if (IrisSettings.get().getGenerator().preventLeafDecay) {
|
||||
BlockData blockData = block.getBlockData();
|
||||
if (blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return (dataType == DataType.BLOCK ? blockDataMap.keySet() : itemDataField.keySet())
|
||||
.stream()
|
||||
.map(x -> new Identifier("hmcleaves", x))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return (dataType == DataType.ITEM ? itemDataField.keySet() : blockDataMap.keySet()).contains(id.key());
|
||||
}
|
||||
|
||||
private <C, T> Map<String, T> getMap(C config, String name) {
|
||||
WrappedField<C, Map<String, T>> field = new WrappedField<>((Class<C>) config.getClass(), name);
|
||||
return field.get(config);
|
||||
}
|
||||
|
||||
private <A> A getApiInstance(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
return instance.invoke();
|
||||
}
|
||||
|
||||
private <A, C> C getLeavesConfig(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
WrappedField<A, C> config = new WrappedField<>(apiClass, "config");
|
||||
return config.get(instance.invoke());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -21,7 +22,7 @@ import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ItemAdderDataProvider extends ExternalDataProvider {
|
||||
public class ItemAdderDataProvider extends ExternalDataProvider implements Listener {
|
||||
|
||||
private volatile Set<String> itemNamespaces = Set.of();
|
||||
private volatile Set<String> blockNamespaces = Set.of();
|
||||
@@ -93,7 +94,7 @@ public class ItemAdderDataProvider extends ExternalDataProvider {
|
||||
}
|
||||
|
||||
private void updateNamespaces(DataType dataType) {
|
||||
var namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet());
|
||||
Set<String> namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet());
|
||||
if (dataType == DataType.ITEM) itemNamespaces = namespaces;
|
||||
else blockNamespaces = namespaces;
|
||||
IrisLogging.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces);
|
||||
|
||||
@@ -10,6 +10,8 @@ import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import me.kryniowesegryderiusz.kgenerators.Main;
|
||||
import me.kryniowesegryderiusz.kgenerators.api.KGeneratorsAPI;
|
||||
import me.kryniowesegryderiusz.kgenerators.api.interfaces.IGeneratorLocation;
|
||||
import me.kryniowesegryderiusz.kgenerators.generators.generator.objects.Generator;
|
||||
import me.kryniowesegryderiusz.kgenerators.generators.locations.objects.GeneratorLocation;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
@@ -39,23 +41,23 @@ public class KGeneratorsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
var gen = Main.getGenerators().get(itemId.key());
|
||||
if (gen == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return gen.getGeneratorItem();
|
||||
Generator generator = Main.getGenerators().get(itemId.key());
|
||||
if (generator == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return generator.getGeneratorItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (block.getType() != Material.STRUCTURE_VOID) return;
|
||||
var existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
|
||||
IGeneratorLocation existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
|
||||
if (existing != null) return;
|
||||
block.setBlockData(BukkitBlockResolution.getAir(), false);
|
||||
var gen = Main.getGenerators().get(blockId.key());
|
||||
if (gen == null) return;
|
||||
var loc = new GeneratorLocation(-1, gen, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
|
||||
Main.getDatabases().getDb().saveGenerator(loc);
|
||||
Main.getPlacedGenerators().addLoaded(loc);
|
||||
Main.getSchedules().schedule(loc, true);
|
||||
Generator generator = Main.getGenerators().get(blockId.key());
|
||||
if (generator == null) return;
|
||||
GeneratorLocation location = new GeneratorLocation(-1, generator, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
|
||||
Main.getDatabases().getDb().saveGenerator(location);
|
||||
Main.getPlacedGenerators().addLoaded(location);
|
||||
Main.getSchedules().schedule(location, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import net.Indyuce.mmoitems.MMOItems;
|
||||
import net.Indyuce.mmoitems.api.ItemTier;
|
||||
import net.Indyuce.mmoitems.api.Type;
|
||||
import net.Indyuce.mmoitems.api.block.CustomBlock;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -52,7 +53,7 @@ public class MMOItemsDataProvider extends ExternalDataProvider {
|
||||
CompletableFuture<ItemStack> future = new CompletableFuture<>();
|
||||
Runnable run = () -> {
|
||||
try {
|
||||
var type = api().getTypes().get(parts[1]);
|
||||
Type type = api().getTypes().get(parts[1]);
|
||||
int level = -1;
|
||||
ItemTier tier = null;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -38,10 +39,12 @@ import io.lumine.mythiccrucible.items.ItemManager;
|
||||
import io.lumine.mythiccrucible.items.blocks.CustomBlockItemContext;
|
||||
import io.lumine.mythiccrucible.items.furniture.FurnitureItemContext;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
@@ -58,11 +61,7 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
@Override
|
||||
public void init() {
|
||||
IrisLogging.info("Setting up MythicCrucible Link...");
|
||||
try {
|
||||
this.itemManager = MythicCrucible.inst().getItemManager();
|
||||
} catch (Exception e) {
|
||||
IrisLogging.error("Failed to set up MythicCrucible Link: Unable to fetch MythicCrucible instance!");
|
||||
}
|
||||
itemManager = MythicCrucible.inst().getItemManager();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -114,8 +113,8 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var parsedState = ExternalDataSVC.parseState(blockId);
|
||||
var state = parsedState.getB();
|
||||
Pair<Identifier, KMap<String, String>> parsedState = ExternalDataSVC.parseState(blockId);
|
||||
KMap<String, String> state = parsedState.getB();
|
||||
blockId = parsedState.getA();
|
||||
|
||||
Optional<CrucibleItem> item = itemManager.getItem(blockId.key());
|
||||
@@ -123,14 +122,14 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
FurnitureItemContext furniture = item.get().getFurnitureData();
|
||||
if (furniture == null) return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
|
||||
BiomeColor type = null;
|
||||
Chroma color = null;
|
||||
try {
|
||||
type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase());
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
color = Chroma.of(biomeColor.getRGB());
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@ package art.arcane.iris.core.link.data;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import io.lumine.mythic.api.adapters.AbstractLocation;
|
||||
import io.lumine.mythic.api.config.MythicLineConfig;
|
||||
import io.lumine.mythic.api.mobs.MythicMob;
|
||||
import io.lumine.mythic.api.mobs.entities.SpawnReason;
|
||||
import io.lumine.mythic.api.skills.conditions.ILocationCondition;
|
||||
import io.lumine.mythic.bukkit.BukkitAdapter;
|
||||
@@ -12,6 +17,7 @@ import io.lumine.mythic.bukkit.MythicBukkit;
|
||||
import io.lumine.mythic.bukkit.adapters.BukkitWorld;
|
||||
import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent;
|
||||
import io.lumine.mythic.core.mobs.ActiveMob;
|
||||
import io.lumine.mythic.core.mobs.MobExecutor;
|
||||
import io.lumine.mythic.core.mobs.MobStack;
|
||||
import io.lumine.mythic.core.skills.SkillCondition;
|
||||
import io.lumine.mythic.core.utils.annotations.MythicCondition;
|
||||
@@ -19,6 +25,7 @@ import io.lumine.mythic.core.utils.annotations.MythicField;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -30,7 +37,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
public class MythicMobsDataProvider extends ExternalDataProvider implements Listener {
|
||||
public MythicMobsDataProvider() {
|
||||
super("MythicMobs");
|
||||
}
|
||||
@@ -41,25 +48,25 @@ public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public @Nullable Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException {
|
||||
var mm = spawnMob(BukkitAdapter.adapt(location), entityId);
|
||||
return mm == null ? null : mm.getEntity().getBukkitEntity();
|
||||
ActiveMob activeMob = spawnMob(BukkitAdapter.adapt(location), entityId);
|
||||
return activeMob == null ? null : activeMob.getEntity().getBukkitEntity();
|
||||
}
|
||||
|
||||
private ActiveMob spawnMob(AbstractLocation location, Identifier entityId) throws MissingResourceException {
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
var mm = manager.getMythicMob(entityId.key()).orElse(null);
|
||||
if (mm == null) {
|
||||
var stack = manager.getMythicMobStack(entityId.key());
|
||||
MobExecutor manager = MythicBukkit.inst().getMobManager();
|
||||
MythicMob mythicMob = manager.getMythicMob(entityId.key()).orElse(null);
|
||||
if (mythicMob == null) {
|
||||
MobStack stack = manager.getMythicMobStack(entityId.key());
|
||||
if (stack == null) throw new MissingResourceException("Failed to find Mob!", entityId.namespace(), entityId.key());
|
||||
return stack.spawn(location, 1d, SpawnReason.OTHER, null);
|
||||
}
|
||||
return mm.spawn(location, 1d, SpawnReason.OTHER, null, null);
|
||||
return mythicMob.spawn(location, 1d, SpawnReason.OTHER, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ENTITY) return List.of();
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
MobExecutor manager = MythicBukkit.inst().getMobManager();
|
||||
return Stream.concat(manager.getMobNames().stream(),
|
||||
manager.getMobStacks()
|
||||
.stream()
|
||||
@@ -99,11 +106,11 @@ public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var biome = surface ?
|
||||
IrisBiome biome = surface ?
|
||||
engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) :
|
||||
engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
|
||||
return biomes.contains(biome.getLoadKey());
|
||||
@@ -123,11 +130,11 @@ public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
IrisRegion region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
return regions.contains(region.getLoadKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,24 +11,29 @@ import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.ItemDisplay;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static org.bukkit.Color.fromARGB;
|
||||
|
||||
public class NexoDataProvider extends ExternalDataProvider {
|
||||
public NexoDataProvider() {
|
||||
super("Nexo");
|
||||
@@ -84,8 +89,8 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var statePair = ExternalDataSVC.parseState(blockId);
|
||||
var state = statePair.getB();
|
||||
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||
KMap<String, String> state = statePair.getB();
|
||||
blockId = statePair.getA();
|
||||
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
@@ -96,7 +101,7 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
if (!NexoFurniture.isFurniture(blockId.key()))
|
||||
return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
|
||||
ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB());
|
||||
if (display == null) return;
|
||||
ItemStack itemStack = display.getItemStack();
|
||||
@@ -108,14 +113,13 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
var potionColor = Color.fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue());
|
||||
var meta = itemStack.getItemMeta();
|
||||
ItemMeta meta = itemStack.getItemMeta();
|
||||
switch (meta) {
|
||||
case LeatherArmorMeta armor -> armor.setColor(potionColor);
|
||||
case PotionMeta potion -> potion.setColor(potionColor);
|
||||
case MapMeta map -> map.setColor(potionColor);
|
||||
case LeatherArmorMeta armor -> armor.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()));
|
||||
case PotionMeta potion -> potion.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()));
|
||||
case MapMeta map -> map.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()));
|
||||
case null, default -> {}
|
||||
}
|
||||
itemStack.setItemMeta(meta);
|
||||
|
||||
@@ -40,8 +40,9 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
public final class DefaultPackBootstrapProvisioner {
|
||||
private static final URI DEFAULT_SOURCE = URI.create("https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip");
|
||||
private static final String WORLD_DATAPACK_DIRECTORY = "iris";
|
||||
private static final int PACK_FORMAT = 107;
|
||||
private static final int MARKER_SCHEMA = 1;
|
||||
private static final int MARKER_SCHEMA = 2;
|
||||
private static final int MAX_ARCHIVE_ENTRIES = 100_000;
|
||||
private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L;
|
||||
private static final long MAX_EXPANDED_BYTES = 2L * 1024L * 1024L * 1024L;
|
||||
@@ -77,24 +78,38 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
if (dataDirectory == null) {
|
||||
return false;
|
||||
}
|
||||
Path bootstrapRoot = dataDirectory.toAbsolutePath().normalize().resolve("bootstrap");
|
||||
Path datapackRoot = bootstrapRoot.resolve("datapack");
|
||||
Path packRoot = dataDirectory.toAbsolutePath().normalize().resolve("packs/overworld");
|
||||
Path markerFile = bootstrapRoot.resolve("provisioned.properties");
|
||||
if (!Files.isRegularFile(markerFile) || !isPackRoot(packRoot) || !isDatapackRoot(datapackRoot)) {
|
||||
try {
|
||||
return isProvisioned(
|
||||
dataDirectory,
|
||||
resolveLevelRoot(Path.of("").toAbsolutePath().normalize())
|
||||
);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isProvisioned(Path dataDirectory, Path levelRoot) {
|
||||
try {
|
||||
Path normalizedData = dataDirectory.toAbsolutePath().normalize();
|
||||
Path normalizedLevel = levelRoot.toAbsolutePath().normalize();
|
||||
Path bootstrapRoot = normalizedData.resolve("bootstrap");
|
||||
Path datapackRoot = worldDatapackRoot(normalizedLevel);
|
||||
Path packRoot = normalizedData.resolve("packs/overworld");
|
||||
Path markerFile = bootstrapRoot.resolve("provisioned.properties");
|
||||
if (!Files.isRegularFile(markerFile) || !isPackRoot(packRoot) || !isDatapackRoot(datapackRoot)) {
|
||||
return false;
|
||||
}
|
||||
Properties marker = loadProperties(markerFile);
|
||||
if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) {
|
||||
return false;
|
||||
}
|
||||
return directoryFingerprint(packRoot).equals(marker.getProperty("defaultPackFingerprint"))
|
||||
&& directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint"))
|
||||
&& datapackRoot.toString().equals(marker.getProperty("datapackPath"))
|
||||
&& packRootsFingerprint(IrisDatapackCompiler.collectPackRoots(
|
||||
dataDirectory.toAbsolutePath().normalize(),
|
||||
resolveLevelRoot(Path.of("").toAbsolutePath().normalize())
|
||||
)).equals(marker.getProperty("aggregateFingerprint"));
|
||||
normalizedData,
|
||||
normalizedLevel
|
||||
)).equals(marker.getProperty("aggregateFingerprint"));
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return false;
|
||||
}
|
||||
@@ -109,12 +124,15 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
Path packsRoot = normalizedData.resolve("packs");
|
||||
Path packRoot = packsRoot.resolve("overworld");
|
||||
Path bootstrapRoot = normalizedData.resolve("bootstrap");
|
||||
Path datapackRoot = bootstrapRoot.resolve("datapack");
|
||||
Path legacyDatapackRoot = bootstrapRoot.resolve("datapack");
|
||||
Path datapacksRoot = options.levelRoot().toAbsolutePath().normalize().resolve("datapacks");
|
||||
Path datapackRoot = datapacksRoot.resolve(WORLD_DATAPACK_DIRECTORY);
|
||||
Path markerFile = bootstrapRoot.resolve("provisioned.properties");
|
||||
Path cacheRoot = normalizedData.resolve("cache/bootstrap");
|
||||
Files.createDirectories(packsRoot);
|
||||
Files.createDirectories(bootstrapRoot);
|
||||
Files.createDirectories(cacheRoot);
|
||||
Files.createDirectories(datapacksRoot);
|
||||
|
||||
Properties previousMarker = Files.isRegularFile(markerFile) ? loadProperties(markerFile) : new Properties();
|
||||
boolean existingPack = isPackRoot(packRoot);
|
||||
@@ -137,7 +155,6 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
Path stagedPack = null;
|
||||
Path extractionRoot = null;
|
||||
Path compileContainer = null;
|
||||
Path stagedDatapack = null;
|
||||
Path packBackup = null;
|
||||
Path datapackBackup = null;
|
||||
boolean packReplaced = false;
|
||||
@@ -162,9 +179,10 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
boolean rebuildDatapack = replacePack
|
||||
|| !existingDatapack
|
||||
|| !aggregateFingerprint.equals(previousMarker.getProperty("aggregateFingerprint"))
|
||||
|| !datapackRoot.toString().equals(previousMarker.getProperty("datapackPath"))
|
||||
|| !directoryFingerprint(datapackRoot).equals(previousMarker.getProperty("datapackFingerprint"));
|
||||
if (rebuildDatapack) {
|
||||
compileContainer = bootstrapRoot.resolve(".compile-" + UUID.randomUUID());
|
||||
compileContainer = datapacksRoot.resolve("." + WORLD_DATAPACK_DIRECTORY + "-stage-" + UUID.randomUUID());
|
||||
Files.createDirectories(compileContainer);
|
||||
KList<File> outputFolders = new KList<File>().qadd(compileContainer.toFile());
|
||||
IDataFixer fixer = DataVersion.getLatest().get();
|
||||
@@ -172,14 +190,11 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap");
|
||||
}
|
||||
IrisDatapackCompiler.compile(packRoots, outputFolders, fixer, PACK_FORMAT, false);
|
||||
Path canonicalOutput = compileContainer;
|
||||
if (!isDatapackRoot(canonicalOutput)) {
|
||||
throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + canonicalOutput);
|
||||
if (!isDatapackRoot(compileContainer)) {
|
||||
throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + compileContainer);
|
||||
}
|
||||
stagedDatapack = bootstrapRoot.resolve(".datapack-stage-" + UUID.randomUUID());
|
||||
move(canonicalOutput, stagedDatapack, false);
|
||||
datapackBackup = replaceWithBackup(compileContainer, datapackRoot);
|
||||
compileContainer = null;
|
||||
datapackBackup = replaceWithBackup(stagedDatapack, datapackRoot);
|
||||
datapackReplaced = true;
|
||||
}
|
||||
|
||||
@@ -200,11 +215,13 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
marker.setProperty("defaultPackFingerprint", finalPackFingerprint);
|
||||
marker.setProperty("aggregateFingerprint", finalAggregateFingerprint);
|
||||
marker.setProperty("datapackFingerprint", finalDatapackFingerprint);
|
||||
marker.setProperty("datapackPath", datapackRoot.toString());
|
||||
marker.setProperty("completedAt", Long.toString(options.clock().millis()));
|
||||
storePropertiesAtomic(markerFile, marker);
|
||||
PROVISIONED_THIS_STARTUP.set(true);
|
||||
deleteQuietly(packBackup, feedback);
|
||||
deleteQuietly(datapackBackup, feedback);
|
||||
deleteQuietly(legacyDatapackRoot, feedback);
|
||||
|
||||
ProvisionStatus status;
|
||||
if (!existingPack && !existingDatapack) {
|
||||
@@ -230,7 +247,6 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
throw new IOException("Iris bootstrap provisioning failed", failure);
|
||||
} finally {
|
||||
deleteQuietly(stagedPack, feedback);
|
||||
deleteQuietly(stagedDatapack, feedback);
|
||||
deleteQuietly(extractionRoot, feedback);
|
||||
deleteQuietly(compileContainer, feedback);
|
||||
}
|
||||
@@ -329,7 +345,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
feedback.accept("Default overworld download failed; using the validated cached archive.");
|
||||
return new Archive(archivePath, sha256(archivePath));
|
||||
}
|
||||
if (isProvisionedOutputUsable(marker, cacheRoot.getParent().getParent())) {
|
||||
if (isManagedPackOutputUsable(marker, cacheRoot.getParent().getParent())) {
|
||||
String sourceSha = marker.getProperty("sourceSha256");
|
||||
return new Archive(null, sourceSha);
|
||||
}
|
||||
@@ -338,9 +354,18 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
: new IOException("Default overworld archive is unavailable and no valid cache exists", networkFailure);
|
||||
}
|
||||
|
||||
private static boolean isProvisionedOutputUsable(Properties marker, Path dataDirectory) {
|
||||
private static boolean isManagedPackOutputUsable(Properties marker, Path dataDirectory) {
|
||||
String sourceSha = marker.getProperty("sourceSha256");
|
||||
return sourceSha != null && !sourceSha.isBlank() && isProvisioned(dataDirectory);
|
||||
String expectedFingerprint = marker.getProperty("defaultPackFingerprint");
|
||||
if (sourceSha == null || sourceSha.isBlank() || expectedFingerprint == null || expectedFingerprint.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
Path packRoot = dataDirectory.toAbsolutePath().normalize().resolve("packs/overworld");
|
||||
try {
|
||||
return isPackRoot(packRoot) && directoryFingerprint(packRoot).equals(expectedFingerprint);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Path resolveLevelRoot(Path serverRoot) throws IOException {
|
||||
@@ -505,6 +530,10 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
&& Files.isDirectory(path.resolve("data/iris/dimension_type"));
|
||||
}
|
||||
|
||||
static Path worldDatapackRoot(Path levelRoot) {
|
||||
return levelRoot.toAbsolutePath().normalize().resolve("datapacks").resolve(WORLD_DATAPACK_DIRECTORY);
|
||||
}
|
||||
|
||||
private static String packRootsFingerprint(List<File> packRoots) throws IOException {
|
||||
MessageDigest digest = digest();
|
||||
List<Path> roots = packRoots.stream()
|
||||
|
||||
@@ -27,6 +27,7 @@ import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -51,6 +52,9 @@ public final class PackValidator {
|
||||
private static final String CACHE_FOLDER = "cache";
|
||||
private static final String OBJECTS_FOLDER = "objects";
|
||||
private static final String DIMENSIONS_FOLDER = "dimensions";
|
||||
private static final String STRUCTURES_FOLDER = "structures";
|
||||
private static final String JIGSAW_POOLS_FOLDER = "jigsaw-pools";
|
||||
private static final String JIGSAW_PIECES_FOLDER = "jigsaw-pieces";
|
||||
private static final List<String> STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes");
|
||||
private static final List<String> UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale");
|
||||
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||
@@ -83,6 +87,7 @@ public final class PackValidator {
|
||||
|
||||
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
blockingErrors.addAll(validateUnsupportedStructureTransforms(packFolder));
|
||||
blockingErrors.addAll(validateStructureGraph(packFolder));
|
||||
blockingErrors.addAll(validateSpawnerEntityReferences(
|
||||
new File(packFolder, "spawners"), new File(packFolder, "entities")));
|
||||
blockingErrors.addAll(validateCustomBiomeSpawns(
|
||||
@@ -138,6 +143,206 @@ public final class PackValidator {
|
||||
return blockingErrors;
|
||||
}
|
||||
|
||||
static List<String> validateStructureGraph(File packFolder) {
|
||||
List<String> blockingErrors = new ArrayList<>();
|
||||
if (packFolder == null || !packFolder.isDirectory()) {
|
||||
return blockingErrors;
|
||||
}
|
||||
|
||||
File structuresFolder = new File(packFolder, STRUCTURES_FOLDER);
|
||||
File poolsFolder = new File(packFolder, JIGSAW_POOLS_FOLDER);
|
||||
File piecesFolder = new File(packFolder, JIGSAW_PIECES_FOLDER);
|
||||
File objectsFolder = new File(packFolder, OBJECTS_FOLDER);
|
||||
Set<String> structureKeys = deriveRegistrantKeysExact(structuresFolder);
|
||||
Set<String> poolKeys = deriveRegistrantKeysExact(poolsFolder);
|
||||
Set<String> pieceKeys = deriveRegistrantKeysExact(piecesFolder);
|
||||
Set<String> objectKeys = deriveObjectKeysExact(objectsFolder);
|
||||
|
||||
validateStructurePlacements(packFolder, structureKeys, blockingErrors);
|
||||
validateStructureStartPools(structuresFolder, poolKeys, blockingErrors);
|
||||
validateJigsawPools(poolsFolder, poolKeys, pieceKeys, blockingErrors);
|
||||
validateJigsawPieces(piecesFolder, poolKeys, objectKeys, blockingErrors);
|
||||
return blockingErrors;
|
||||
}
|
||||
|
||||
private static void validateStructurePlacements(File packFolder,
|
||||
Set<String> structureKeys,
|
||||
List<String> blockingErrors) {
|
||||
for (String folderName : STRUCTURE_HOST_FOLDERS) {
|
||||
File resourceFolder = new File(packFolder, folderName);
|
||||
if (!resourceFolder.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
List<File> resourceFiles = listJsonRecursive(resourceFolder);
|
||||
resourceFiles.sort(Comparator.comparing(File::getPath));
|
||||
String resourceType = structureHostType(folderName);
|
||||
for (File resourceFile : resourceFiles) {
|
||||
JSONObject resource = readJson(resourceFile);
|
||||
if (resource == null) {
|
||||
continue;
|
||||
}
|
||||
JSONArray placements = resource.optJSONArray("structures");
|
||||
if (placements == null) {
|
||||
continue;
|
||||
}
|
||||
String resourceKey = deriveKey(resourceFolder, resourceFile);
|
||||
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
|
||||
JSONObject placement = placements.optJSONObject(placementIndex);
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
JSONArray references = placement.optJSONArray("structures");
|
||||
if (references == null) {
|
||||
continue;
|
||||
}
|
||||
for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) {
|
||||
Object rawReference = references.opt(referenceIndex);
|
||||
if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!structureKeys.contains(structureKey)) {
|
||||
blockingErrors.add(resourceType + " '" + resourceKey + "' structures["
|
||||
+ placementIndex + "].structures[" + referenceIndex
|
||||
+ "] references missing structure '" + structureKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateStructureStartPools(File structuresFolder,
|
||||
Set<String> poolKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!structuresFolder.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
List<File> structureFiles = listJsonRecursive(structuresFolder);
|
||||
structureFiles.sort(Comparator.comparing(File::getPath));
|
||||
for (File structureFile : structureFiles) {
|
||||
String structureKey = deriveKey(structuresFolder, structureFile);
|
||||
JSONObject structure = readGraphJson(structureFile, "Structure", structureKey, blockingErrors);
|
||||
if (structure == null || isLegacyStructureIndex(structureKey, structure)) {
|
||||
continue;
|
||||
}
|
||||
String startPool = structure.optString("startPool", "").trim();
|
||||
if (startPool.isEmpty()) {
|
||||
blockingErrors.add("Structure '" + structureKey + "' does not declare a startPool.");
|
||||
} else if (!poolKeys.contains(startPool)) {
|
||||
blockingErrors.add("Structure '" + structureKey + "' references missing start pool '"
|
||||
+ startPool + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJigsawPools(File poolsFolder,
|
||||
Set<String> poolKeys,
|
||||
Set<String> pieceKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!poolsFolder.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
List<File> poolFiles = listJsonRecursive(poolsFolder);
|
||||
poolFiles.sort(Comparator.comparing(File::getPath));
|
||||
for (File poolFile : poolFiles) {
|
||||
String poolKey = deriveKey(poolsFolder, poolFile);
|
||||
JSONObject pool = readGraphJson(poolFile, "Jigsaw pool", poolKey, blockingErrors);
|
||||
if (pool == null) {
|
||||
continue;
|
||||
}
|
||||
JSONArray entries = pool.optJSONArray("pieces");
|
||||
if (entries != null) {
|
||||
for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) {
|
||||
JSONObject entry = entries.optJSONObject(entryIndex);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
String pieceKey = entry.optString("piece", "").trim();
|
||||
if (!pieceKey.isEmpty() && !pieceKeys.contains(pieceKey)) {
|
||||
blockingErrors.add("Jigsaw pool '" + poolKey + "' pieces[" + entryIndex
|
||||
+ "] references missing piece '" + pieceKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
String fallback = pool.optString("fallback", "").trim();
|
||||
if (!fallback.isEmpty() && !poolKeys.contains(fallback)) {
|
||||
blockingErrors.add("Jigsaw pool '" + poolKey + "' references missing fallback pool '"
|
||||
+ fallback + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJigsawPieces(File piecesFolder,
|
||||
Set<String> poolKeys,
|
||||
Set<String> objectKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!piecesFolder.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
List<File> pieceFiles = listJsonRecursive(piecesFolder);
|
||||
pieceFiles.sort(Comparator.comparing(File::getPath));
|
||||
for (File pieceFile : pieceFiles) {
|
||||
String pieceKey = deriveKey(piecesFolder, pieceFile);
|
||||
JSONObject piece = readGraphJson(pieceFile, "Jigsaw piece", pieceKey, blockingErrors);
|
||||
if (piece == null) {
|
||||
continue;
|
||||
}
|
||||
String objectKey = piece.optString("object", "").trim();
|
||||
if (objectKey.isEmpty()) {
|
||||
blockingErrors.add("Jigsaw piece '" + pieceKey + "' does not declare an object.");
|
||||
} else if (!objectKeys.contains(objectKey)) {
|
||||
blockingErrors.add("Jigsaw piece '" + pieceKey + "' references missing object '"
|
||||
+ objectKey + "'.");
|
||||
}
|
||||
JSONArray connectors = piece.optJSONArray("connectors");
|
||||
if (connectors == null) {
|
||||
continue;
|
||||
}
|
||||
for (int connectorIndex = 0; connectorIndex < connectors.length(); connectorIndex++) {
|
||||
JSONObject connector = connectors.optJSONObject(connectorIndex);
|
||||
if (connector == null) {
|
||||
continue;
|
||||
}
|
||||
String poolKey = connector.optString("pool", "").trim();
|
||||
if (!poolKey.isEmpty() && !poolKeys.contains(poolKey)) {
|
||||
blockingErrors.add("Jigsaw piece '" + pieceKey + "' connectors[" + connectorIndex
|
||||
+ "] references missing pool '" + poolKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject readGraphJson(File file,
|
||||
String resourceType,
|
||||
String resourceKey,
|
||||
List<String> blockingErrors) {
|
||||
try {
|
||||
return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
String reason = e.getMessage();
|
||||
if (reason == null || reason.isBlank()) {
|
||||
reason = e.getClass().getSimpleName();
|
||||
}
|
||||
blockingErrors.add(resourceType + " '" + resourceKey + "' has invalid JSON: " + reason);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject readJson(File file) {
|
||||
try {
|
||||
return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLegacyStructureIndex(String structureKey, JSONObject structure) {
|
||||
return "structure-index".equals(structureKey)
|
||||
&& structure.has("counts")
|
||||
&& structure.has("structureSets")
|
||||
&& structure.has("iris");
|
||||
}
|
||||
|
||||
private static String structureHostType(String folderName) {
|
||||
return switch (folderName) {
|
||||
case "dimensions" -> "Dimension";
|
||||
@@ -546,6 +751,38 @@ public final class PackValidator {
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static Set<String> deriveRegistrantKeysExact(File folder) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
if (!folder.isDirectory()) {
|
||||
return keys;
|
||||
}
|
||||
for (File file : listJsonRecursive(folder)) {
|
||||
String key = deriveKey(folder, file);
|
||||
if (key != null && !key.isBlank()) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static Set<String> deriveObjectKeysExact(File folder) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
if (!folder.isDirectory()) {
|
||||
return keys;
|
||||
}
|
||||
try (Stream<Path> stream = Files.walk(folder.toPath())) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".iob"))
|
||||
.forEach(path -> {
|
||||
Path relative = folder.toPath().relativize(path);
|
||||
String key = relative.toString().replace(File.separatorChar, '/');
|
||||
keys.add(key.substring(0, key.length() - ".iob".length()));
|
||||
});
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -74,7 +75,7 @@ public final class StudioOpenCoordinator {
|
||||
return new StudioCloseResult(null, true, true, false, null);
|
||||
}
|
||||
|
||||
World world = provider.getTarget().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(provider.getTarget().getWorld());
|
||||
String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName();
|
||||
try {
|
||||
return closeWorld(provider, worldName, world, true, project);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
@@ -29,79 +28,91 @@ import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.io.JarScanner;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.server.PluginEnableEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
public class ExternalDataSVC implements IrisService {
|
||||
private static final String PROVIDER_PACKAGE = "art.arcane.iris.core.link.data.";
|
||||
private static final List<ProviderDefinition> BUILT_IN_PROVIDERS = List.of(
|
||||
new ProviderDefinition("CraftEngine", PROVIDER_PACKAGE + "CraftEngineDataProvider"),
|
||||
new ProviderDefinition("Nexo", PROVIDER_PACKAGE + "NexoDataProvider"),
|
||||
new ProviderDefinition("ItemsAdder", PROVIDER_PACKAGE + "ItemAdderDataProvider"),
|
||||
new ProviderDefinition("ExecutableItems", PROVIDER_PACKAGE + "ExecutableItemsDataProvider"),
|
||||
new ProviderDefinition("MMOItems", PROVIDER_PACKAGE + "MMOItemsDataProvider"),
|
||||
new ProviderDefinition("EcoItems", PROVIDER_PACKAGE + "EcoItemsDataProvider"),
|
||||
new ProviderDefinition("MythicMobs", PROVIDER_PACKAGE + "MythicMobsDataProvider"),
|
||||
new ProviderDefinition("MythicCrucible", PROVIDER_PACKAGE + "MythicCrucibleDataProvider"),
|
||||
new ProviderDefinition("KGenerators", PROVIDER_PACKAGE + "KGeneratorsDataProvider")
|
||||
);
|
||||
|
||||
private KList<ExternalDataProvider> providers = new KList<>(), activeProviders = new KList<>();
|
||||
private final KList<ExternalDataProvider> providers = new KList<>();
|
||||
private final KList<ExternalDataProvider> activeProviders = new KList<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
IrisLogging.info("Loading ExternalDataProvider...");
|
||||
Bukkit.getPluginManager().registerEvents(this, BukkitPlatform.plugin());
|
||||
|
||||
providers.addAll(createProviders());
|
||||
for (ExternalDataProvider p : providers) {
|
||||
if (p.isReady()) {
|
||||
activeProviders.add(p);
|
||||
p.init();
|
||||
BukkitPlatform.volmitPlugin().registerListener(p);
|
||||
IrisLogging.info("Enabled ExternalDataProvider for %s.", p.getPluginId());
|
||||
}
|
||||
for (ProviderDefinition definition : BUILT_IN_PROVIDERS) {
|
||||
activateConfiguredProvider(definition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
activeProviders.clear();
|
||||
providers.clear();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPluginEnable(PluginEnableEvent e) {
|
||||
if (activeProviders.stream().noneMatch(p -> e.getPlugin().equals(p.getPlugin()))) {
|
||||
providers.stream().filter(p -> p.isReady() && e.getPlugin().equals(p.getPlugin())).findFirst().ifPresent(edp -> {
|
||||
activeProviders.add(edp);
|
||||
edp.init();
|
||||
BukkitPlatform.volmitPlugin().registerListener(edp);
|
||||
IrisLogging.info("Enabled ExternalDataProvider for %s.", edp.getPluginId());
|
||||
});
|
||||
public void onPluginEnable(PluginEnableEvent event) {
|
||||
String pluginId = event.getPlugin().getName();
|
||||
Optional<ExternalDataProvider> existingProvider = findProvider(pluginId);
|
||||
if (existingProvider.isPresent()) {
|
||||
activateProvider(existingProvider.get());
|
||||
return;
|
||||
}
|
||||
|
||||
BUILT_IN_PROVIDERS.stream()
|
||||
.filter(definition -> definition.pluginId().equalsIgnoreCase(pluginId))
|
||||
.findFirst()
|
||||
.ifPresent(this::activateConfiguredProvider);
|
||||
}
|
||||
|
||||
public void registerProvider(@NonNull ExternalDataProvider provider) {
|
||||
String plugin = provider.getPluginId();
|
||||
if (providers.stream().map(ExternalDataProvider::getPluginId).anyMatch(plugin::equals))
|
||||
public void registerProvider(ExternalDataProvider provider) {
|
||||
ExternalDataProvider registeredProvider = Objects.requireNonNull(provider);
|
||||
String pluginId = registeredProvider.getPluginId();
|
||||
boolean builtIn = BUILT_IN_PROVIDERS.stream()
|
||||
.anyMatch(definition -> definition.pluginId().equalsIgnoreCase(pluginId));
|
||||
if (builtIn || findProvider(pluginId).isPresent()) {
|
||||
throw new IllegalArgumentException("A provider with the same plugin id already exists.");
|
||||
}
|
||||
|
||||
providers.add(provider);
|
||||
if (provider.isReady()) {
|
||||
activeProviders.add(provider);
|
||||
provider.init();
|
||||
BukkitPlatform.volmitPlugin().registerListener(provider);
|
||||
providers.add(registeredProvider);
|
||||
if (registeredProvider.isReady()) {
|
||||
activateProvider(registeredProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<BlockData> getBlockData(final Identifier key) {
|
||||
var pair = parseState(key);
|
||||
Pair<Identifier, KMap<String, String>> pair = parseState(key);
|
||||
Identifier mod = pair.getA();
|
||||
|
||||
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst();
|
||||
@@ -201,20 +212,52 @@ public class ExternalDataSVC implements IrisService {
|
||||
return new Identifier(key.namespace(), path);
|
||||
}
|
||||
|
||||
private static KList<ExternalDataProvider> createProviders() {
|
||||
JarScanner jar = new JarScanner(IrisPlatforms.get().pluginJar(), "art.arcane.iris.core.link.data", false);
|
||||
J.attempt(jar::scan);
|
||||
KList<ExternalDataProvider> providers = new KList<>();
|
||||
private Optional<ExternalDataProvider> findProvider(String pluginId) {
|
||||
return providers.stream()
|
||||
.filter(provider -> provider.getPluginId().equalsIgnoreCase(pluginId))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
for (Class<?> c : jar.getClasses()) {
|
||||
if (ExternalDataProvider.class.isAssignableFrom(c)) {
|
||||
try {
|
||||
ExternalDataProvider p = (ExternalDataProvider) c.getDeclaredConstructor().newInstance();
|
||||
if (p.getPlugin() != null) IrisLogging.info(p.getPluginId() + " found, loading " + c.getSimpleName() + "...");
|
||||
providers.add(p);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
private void activateConfiguredProvider(ProviderDefinition definition) {
|
||||
if (!isPluginEnabled(definition.pluginId()) || findProvider(definition.pluginId()).isPresent()) {
|
||||
return;
|
||||
}
|
||||
return providers;
|
||||
|
||||
try {
|
||||
Class<?> rawProviderClass = Class.forName(definition.className(), true, ExternalDataSVC.class.getClassLoader());
|
||||
Class<? extends ExternalDataProvider> providerClass = rawProviderClass.asSubclass(ExternalDataProvider.class);
|
||||
ExternalDataProvider provider = providerClass.getDeclaredConstructor().newInstance();
|
||||
IrisLogging.info(definition.pluginId() + " found, loading " + providerClass.getSimpleName() + "...");
|
||||
activateProvider(provider);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to create Iris external data provider " + definition.className() + ".", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void activateProvider(ExternalDataProvider provider) {
|
||||
if (activeProviders.contains(provider)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
provider.init();
|
||||
if (provider instanceof Listener listener) {
|
||||
BukkitPlatform.volmitPlugin().registerListener(listener);
|
||||
}
|
||||
if (!providers.contains(provider)) {
|
||||
providers.add(provider);
|
||||
}
|
||||
activeProviders.add(provider);
|
||||
IrisLogging.info("Enabled ExternalDataProvider for %s.", provider.getPluginId());
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to enable Iris external data provider " + provider.getPluginId() + ".", error);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPluginEnabled(String pluginId) {
|
||||
Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginId);
|
||||
return plugin != null && plugin.isEnabled();
|
||||
}
|
||||
|
||||
private record ProviderDefinition(String pluginId, String className) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
@@ -75,7 +76,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
}
|
||||
|
||||
public void register(Engine engine, ObjectStudioGenerator generator) {
|
||||
World world = engine.getTarget().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(engine.getTarget().getWorld());
|
||||
if (world == null) return;
|
||||
ObjectStudioLayout layout = generator.getLayout();
|
||||
if (layout == null) return;
|
||||
|
||||
@@ -21,7 +21,8 @@ package art.arcane.iris.core.structure;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.io.File;
|
||||
@@ -36,14 +37,16 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class StructureIndexService {
|
||||
private static final String INDEX_PATH = ".iris/structure-index.json";
|
||||
private static final String LEGACY_INDEX_PATH = "structures/structure-index.json";
|
||||
private static final String LEGACY_INDEX_KEY = "structure-index";
|
||||
private static final Set<String> GENERATED = ConcurrentHashMap.newKeySet();
|
||||
private static final boolean BUKKIT_PRESENT = IrisMatterSupport.isBukkitPresent();
|
||||
|
||||
private StructureIndexService() {
|
||||
}
|
||||
|
||||
public static File writeOnce(IrisData data) {
|
||||
if (!BUKKIT_PRESENT || data == null) {
|
||||
if (data == null || !IrisPlatforms.isBound() || IrisPlatforms.get().structureHooks() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -63,6 +66,7 @@ public final class StructureIndexService {
|
||||
}
|
||||
|
||||
public static File write(IrisData data) {
|
||||
removeLegacyIndex(data);
|
||||
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
|
||||
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
|
||||
|
||||
@@ -89,6 +93,7 @@ public final class StructureIndexService {
|
||||
List<String> iris = new ArrayList<>();
|
||||
if (data.getStructureLoader() != null) {
|
||||
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
|
||||
iris.removeIf(LEGACY_INDEX_KEY::equals);
|
||||
}
|
||||
|
||||
Collections.sort(vanilla);
|
||||
@@ -118,7 +123,7 @@ public final class StructureIndexService {
|
||||
root.put("structureSets", setsNode);
|
||||
root.put("iris", iris);
|
||||
|
||||
File file = new File(data.getDataFolder(), "structures/structure-index.json");
|
||||
File file = indexFile(data.getDataFolder());
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
String json = new GsonBuilder().setPrettyPrinting().create().toJson(root);
|
||||
@@ -128,4 +133,27 @@ public final class StructureIndexService {
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static File indexFile(File dataFolder) {
|
||||
return new File(dataFolder, INDEX_PATH);
|
||||
}
|
||||
|
||||
static File legacyIndexFile(File dataFolder) {
|
||||
return new File(dataFolder, LEGACY_INDEX_PATH);
|
||||
}
|
||||
|
||||
static void removeLegacyIndex(IrisData data) {
|
||||
File legacyFile = legacyIndexFile(data.getDataFolder());
|
||||
try {
|
||||
Files.deleteIfExists(legacyFile.toPath());
|
||||
ResourceLoader<IrisStructure> structureLoader = data.getStructureLoader();
|
||||
if (structureLoader != null) {
|
||||
structureLoader.getLoadCache().invalidate(LEGACY_INDEX_KEY);
|
||||
structureLoader.clearList();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Failed to remove legacy structure index '"
|
||||
+ legacyFile.getAbsolutePath() + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -230,7 +231,7 @@ public class IrisToolbelt {
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (activeProvider != null) {
|
||||
World activeWorld = activeProvider.getTarget().getWorld().realWorld();
|
||||
World activeWorld = BukkitWorldBinding.world(activeProvider.getTarget().getWorld());
|
||||
if (activeWorld != null && WorldIdentity.key(activeWorld).equals(WorldIdentity.key(world))) {
|
||||
if (activeProvider instanceof BukkitChunkGenerator bukkit) {
|
||||
bukkit.touch(world);
|
||||
@@ -273,7 +274,7 @@ public class IrisToolbelt {
|
||||
}
|
||||
|
||||
private static PregenCache resolvePregenCache(IrisWorld world) {
|
||||
String worldIdentity = world.key() == null ? null : world.key().toString();
|
||||
String worldIdentity = world.identity();
|
||||
if (worldIdentity == null) {
|
||||
throw new IllegalStateException("Pregeneration requires a namespaced world identity.");
|
||||
}
|
||||
@@ -306,14 +307,14 @@ public class IrisToolbelt {
|
||||
* @return the pregenerator job (already started)
|
||||
*/
|
||||
public static PregeneratorJob pregenerate(PregenTask task, PlatformChunkGenerator gen) {
|
||||
World world = gen.getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(gen.getEngine().getWorld());
|
||||
int threads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism());
|
||||
PregeneratorMethod method = new HybridPregenMethod(world, threads);
|
||||
return pregenerate(task, method, gen.getEngine());
|
||||
}
|
||||
|
||||
public static PregeneratorJob pregenerateSerial(PregenTask task, PlatformChunkGenerator gen) {
|
||||
World world = gen.getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(gen.getEngine().getWorld());
|
||||
PregeneratorMethod method = HybridPregenMethod.strictSerial(world);
|
||||
return pregenerate(task, method, gen.getEngine());
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
@@ -76,15 +77,15 @@ public class IrisWorldCreator {
|
||||
public WorldCreator create() {
|
||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(name);
|
||||
World.Environment environment = findEnvironment();
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.key(worldKey)
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(name)
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
.seed(seed)
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
|
||||
.environment(findEnvironment())
|
||||
.build();
|
||||
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio
|
||||
? dim.getLoader().getDataFolder() :
|
||||
@@ -92,18 +93,14 @@ public class IrisWorldCreator {
|
||||
|
||||
|
||||
return WorldCreator.ofKey(worldKey)
|
||||
.environment(w.environment())
|
||||
.environment(environment)
|
||||
.generateStructures(true)
|
||||
.generator(g).seed(seed);
|
||||
}
|
||||
|
||||
private World.Environment findEnvironment() {
|
||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||
if (dim == null || dim.getEnvironment() == null) {
|
||||
return World.Environment.NORMAL;
|
||||
} else {
|
||||
return dim.getEnvironment();
|
||||
}
|
||||
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
|
||||
}
|
||||
|
||||
public IrisWorldCreator studio(boolean studio) {
|
||||
|
||||
@@ -20,16 +20,20 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
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.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.SeedManager;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
@@ -46,18 +50,12 @@ import com.google.gson.Gson;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||
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.structure.StructureIndexService;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
@@ -112,6 +110,7 @@ public class IrisEngine implements Engine {
|
||||
private final ChronoLatch cleanLatch;
|
||||
private final SeedManager seedManager;
|
||||
private final GenerationSessionManager generationSessions;
|
||||
private final EnginePlatformHooks platformHooks;
|
||||
private final AtomicBoolean closing;
|
||||
private CompletableFuture<Long> hash32;
|
||||
private EngineMode mode;
|
||||
@@ -132,6 +131,7 @@ public class IrisEngine implements Engine {
|
||||
public IrisEngine(EngineTarget target, boolean studio) {
|
||||
this.studio = studio;
|
||||
this.target = target;
|
||||
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
|
||||
getEngineData();
|
||||
verifySeed();
|
||||
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
|
||||
@@ -233,13 +233,15 @@ public class IrisEngine implements Engine {
|
||||
currentMode.close();
|
||||
}
|
||||
|
||||
if (getWorld().hasRealWorld()) {
|
||||
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
|
||||
if (getWorld().hasPlatformWorld()) {
|
||||
J.a(() -> platformHooks.refreshWorkspace(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void setupEngine() {
|
||||
try {
|
||||
IrisStructureLocator.invalidate(this);
|
||||
StructureReachability.invalidate(this);
|
||||
generationSessions.activateNextSession();
|
||||
closing.set(false);
|
||||
IrisLogging.debug("Setup Engine " + getCacheID());
|
||||
@@ -251,8 +253,8 @@ public class IrisEngine implements Engine {
|
||||
upperContext = buildUpperContext();
|
||||
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - t0) + "ms");
|
||||
t0 = M.ms();
|
||||
effects = new IrisEngineEffects(this);
|
||||
IrisLogging.debug("[IrisEngine timing] IrisEngineEffects=" + (M.ms() - t0) + "ms");
|
||||
effects = IrisServices.get(EngineEffectsProvider.class).create(this);
|
||||
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - t0) + "ms");
|
||||
hash32 = new CompletableFuture<>();
|
||||
t0 = M.ms();
|
||||
mantle.hotload();
|
||||
@@ -276,7 +278,7 @@ public class IrisEngine implements Engine {
|
||||
.toArray(File[]::new);
|
||||
hash32.complete(IO.hashRecursiveMeta(roots));
|
||||
});
|
||||
J.a(() -> DatapackIngestService.refreshWorkspace(getData()));
|
||||
J.a(() -> platformHooks.refreshDatapackWorkspace(this));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("FAILED TO SETUP ENGINE!");
|
||||
IrisLogging.reportError(e);
|
||||
@@ -409,7 +411,7 @@ public class IrisEngine implements Engine {
|
||||
@Override
|
||||
public void hotload() {
|
||||
hotloadSilently();
|
||||
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(this));
|
||||
platformHooks.fireHotloadEvent(this);
|
||||
}
|
||||
|
||||
public void hotloadComplex() {
|
||||
@@ -422,15 +424,16 @@ public class IrisEngine implements Engine {
|
||||
try {
|
||||
getData().dump();
|
||||
getData().clearLists();
|
||||
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
|
||||
IrisDimension replacement = getData().getDimensionLoader().load(getDimension().getLoadKey());
|
||||
if (replacement == null) {
|
||||
throw new IllegalStateException("Studio hotload could not reload Iris dimension '" + getDimension().getLoadKey() + "'");
|
||||
}
|
||||
platformHooks.validateDimensionHotload(this, replacement);
|
||||
getTarget().setDimension(replacement);
|
||||
prehotload();
|
||||
setupEngine();
|
||||
if (getWorld().hasRealWorld()) {
|
||||
J.a(() -> {
|
||||
synchronized (ServerConfigurator.class) {
|
||||
ServerConfigurator.installDataPacks(false);
|
||||
}
|
||||
});
|
||||
if (getWorld().hasPlatformWorld()) {
|
||||
J.a(() -> platformHooks.reloadDatapacks(this));
|
||||
}
|
||||
broadcastStudioHotload(false, "");
|
||||
} catch (RuntimeException | Error e) {
|
||||
@@ -526,7 +529,7 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
private void computeBiomeMaxes() {
|
||||
for (IrisBiome i : getDimension().getAllBiomes(this)) {
|
||||
for (IrisBiome i : getDimension().getReachableBiomes(this)) {
|
||||
double density = 0;
|
||||
|
||||
for (IrisObjectPlacement j : i.getObjects()) {
|
||||
@@ -613,7 +616,7 @@ public class IrisEngine implements Engine {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
platformHooks.shutdownPregenerator(this);
|
||||
closing.set(true);
|
||||
closed = true;
|
||||
J.car(art);
|
||||
@@ -649,8 +652,7 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
private boolean isPregeneratorActiveForThisWorld() {
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(getWorld().identity());
|
||||
return platformHooks.isPregeneratorActive(this);
|
||||
}
|
||||
|
||||
private void savePrefetchOnce() {
|
||||
@@ -728,7 +730,7 @@ public class IrisEngine implements Engine {
|
||||
activeMode.generate(x, z, blocks, vbiomes, multicore, lease.sessionId());
|
||||
}
|
||||
|
||||
boolean skipRealFlag = J.isFolia() && getWorld().hasRealWorld() && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(getWorld().realWorld());
|
||||
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
|
||||
if (!skipRealFlag) {
|
||||
getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true);
|
||||
}
|
||||
|
||||
@@ -22,69 +22,93 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedComponent;
|
||||
import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.framework.EnginePlayer;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
|
||||
private final KMap<UUID, EnginePlayer> players;
|
||||
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
|
||||
|
||||
private final ConcurrentHashMap<UUID, EnginePlayer> players;
|
||||
private final Semaphore limit;
|
||||
private final AtomicBoolean playerMapUpdateQueued;
|
||||
|
||||
public IrisEngineEffects(Engine engine) {
|
||||
super(engine, "FX");
|
||||
players = new KMap<>();
|
||||
players = new ConcurrentHashMap<>();
|
||||
limit = new Semaphore(1);
|
||||
playerMapUpdateQueued = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePlayerMap() {
|
||||
List<Player> pr = getEngine().getWorld().getPlayers();
|
||||
if (!playerMapUpdateQueued.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (!J.runGlobal(() -> {
|
||||
try {
|
||||
syncPlayerMap();
|
||||
} finally {
|
||||
playerMapUpdateQueued.set(false);
|
||||
}
|
||||
})) {
|
||||
playerMapUpdateQueued.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncPlayerMap() {
|
||||
List<Player> pr = BukkitWorldBinding.players(getEngine().getWorld());
|
||||
|
||||
if (pr == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Player i : pr) {
|
||||
boolean pcc = players.containsKey(i.getUniqueId());
|
||||
if (!pcc) {
|
||||
players.put(i.getUniqueId(), new EnginePlayer(getEngine(), i));
|
||||
Set<UUID> activeIds = new HashSet<>(Math.max(16, pr.size() * 2));
|
||||
for (Player player : pr) {
|
||||
UUID playerId = player.getUniqueId();
|
||||
activeIds.add(playerId);
|
||||
EnginePlayer existing = players.get(playerId);
|
||||
if (existing == null || existing.getPlayer() != player) {
|
||||
players.put(playerId, new EnginePlayer(getEngine(), player));
|
||||
}
|
||||
}
|
||||
|
||||
for (UUID i : players.k()) {
|
||||
if (!pr.contains(players.get(i).getPlayer())) {
|
||||
players.remove(i);
|
||||
}
|
||||
}
|
||||
players.keySet().removeIf((UUID playerId) -> !activeIds.contains(playerId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tickRandomPlayer() {
|
||||
if (limit.tryAcquire()) {
|
||||
if (M.r(0.02)) {
|
||||
if (!limit.tryAcquire()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (players.isEmpty() || M.r(0.02)) {
|
||||
updatePlayerMap();
|
||||
limit.release();
|
||||
return;
|
||||
}
|
||||
|
||||
if (players.isEmpty()) {
|
||||
limit.release();
|
||||
List<EnginePlayer> snapshot = new ArrayList<>(players.values());
|
||||
if (snapshot.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double limitms = 1.5;
|
||||
int max = players.size();
|
||||
PrecisionStopwatch p = new PrecisionStopwatch();
|
||||
|
||||
while (max-- > 0 && M.ms() - p.getMilliseconds() < limitms) {
|
||||
players.v().getRandom().tick();
|
||||
long started = System.nanoTime();
|
||||
int remaining = snapshot.size();
|
||||
while (remaining-- > 0 && System.nanoTime() - started < EFFECT_BUDGET_NANOS) {
|
||||
snapshot.get(ThreadLocalRandom.current().nextInt(snapshot.size())).tick();
|
||||
}
|
||||
|
||||
} finally {
|
||||
limit.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
@@ -35,6 +36,7 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisSpawner;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
@@ -149,11 +151,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
interrupt();
|
||||
}
|
||||
|
||||
if (!getEngine().getWorld().hasRealWorld() && clw.flip()) {
|
||||
J.runGlobal(() -> getEngine().getWorld().tryGetRealWorld());
|
||||
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
|
||||
J.runGlobal(() -> BukkitWorldBinding.tryBind(getEngine().getWorld()));
|
||||
}
|
||||
|
||||
if (getEngine().getWorld().hasRealWorld()) {
|
||||
if (getEngine().getWorld().hasPlatformWorld()) {
|
||||
if (chunkUpdater.flip()) {
|
||||
updateChunks();
|
||||
}
|
||||
@@ -191,7 +193,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
|
||||
private void discoverChunks() {
|
||||
World world = getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
@@ -206,7 +208,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
if (getEngine().isClosed() || !world.equals(getEngine().getWorld().realWorld())) {
|
||||
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,7 +275,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
|
||||
private void updateChunks() {
|
||||
World world = getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
@@ -294,7 +296,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
private void updateChunksOnGlobal(World world) {
|
||||
try {
|
||||
if (getEngine().isClosed() || !world.equals(getEngine().getWorld().realWorld())) {
|
||||
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -496,7 +498,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
actuallySpawned = 0;
|
||||
|
||||
if (!getEngine().getWorld().hasRealWorld()) {
|
||||
if (!getEngine().getWorld().hasPlatformWorld()) {
|
||||
IrisLogging.debug("Can't spawn. No real world");
|
||||
J.sleep(5000);
|
||||
return false;
|
||||
@@ -504,7 +506,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
if (cl.flip()) {
|
||||
try {
|
||||
World realWorld = getEngine().getWorld().realWorld();
|
||||
World realWorld = BukkitWorldBinding.world(getEngine().getWorld());
|
||||
if (realWorld == null) {
|
||||
entityCount = 0;
|
||||
} else if (J.isFolia()) {
|
||||
@@ -540,7 +542,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
|
||||
int spawnBuffer = RNG.r.i(2, 12);
|
||||
World world = getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -560,7 +562,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
|
||||
private boolean isPregenActiveForThisWorld() {
|
||||
World world = getEngine().getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -574,7 +576,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
return job.targetsWorld(world);
|
||||
return job.targetsWorldIdentity(WorldIdentity.serialize(world));
|
||||
}
|
||||
|
||||
private Position2[] getLoadedChunkPositionsSnapshot(World world) {
|
||||
@@ -1122,9 +1124,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
@Override
|
||||
public void onBlockBreak(BlockBreakEvent e) {
|
||||
if (e.getBlock().getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
int blockX = e.getBlock().getX();
|
||||
int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight());
|
||||
int blockZ = e.getBlock().getZ();
|
||||
J.a(() -> {
|
||||
MatterMarker marker = getMantle().get(e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ(), MatterMarker.class);
|
||||
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
|
||||
|
||||
if (marker != null) {
|
||||
if (marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
|
||||
@@ -1134,7 +1139,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
|
||||
|
||||
if (mark == null || mark.isRemoveOnChange()) {
|
||||
getMantle().remove(e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ(), MatterMarker.class);
|
||||
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1168,6 +1173,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
}
|
||||
|
||||
static int toMantleY(int worldY, int minHeight) {
|
||||
return worldY - minHeight;
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> filterDrops(KList<IrisBlockDrops> drops, BlockBreakEvent e, IrisData data) {
|
||||
return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList());
|
||||
}
|
||||
@@ -1193,7 +1202,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
@Override
|
||||
public double getEntitySaturation() {
|
||||
if (!getEngine().getWorld().hasRealWorld()) {
|
||||
if (!getEngine().getWorld().hasPlatformWorld()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.framework;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
public interface BukkitEngineWorldManager {
|
||||
void onBlockBreak(BlockBreakEvent event);
|
||||
|
||||
void onBlockPlace(BlockPlaceEvent event);
|
||||
|
||||
void onChunkLoad(Chunk chunk, boolean generated);
|
||||
|
||||
void onChunkUnload(Chunk chunk);
|
||||
|
||||
void teleportAsync(PlayerTeleportEvent event);
|
||||
}
|
||||
@@ -22,12 +22,10 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.engine.framework.render.Renderer;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
@@ -94,6 +92,8 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
|
||||
EngineMode getMode();
|
||||
|
||||
EnginePlatformHooks getPlatformHooks();
|
||||
|
||||
int getBlockUpdatesPerSecond();
|
||||
|
||||
void printMetrics(VolmitSender sender);
|
||||
@@ -352,7 +352,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
KMap<String, IrisBiome> v = new KMap<>();
|
||||
|
||||
IrisDimension dim = getDimension();
|
||||
dim.getAllBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
|
||||
return v.v();
|
||||
}
|
||||
@@ -389,7 +389,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
}
|
||||
|
||||
default IrisPosition lookForBiome(IrisBiome biome, long timeout, Consumer<Integer> triesc) {
|
||||
if (!getWorld().hasRealWorld()) {
|
||||
if (!getWorld().hasPlatformWorld()) {
|
||||
IrisLogging.error("Cannot GOTO without a bound world (headless mode)");
|
||||
return null;
|
||||
}
|
||||
@@ -398,7 +398,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
long s = M.ms();
|
||||
int cpus = (Runtime.getRuntime().availableProcessors());
|
||||
|
||||
if (!getDimension().getAllBiomes(this).contains(biome)) {
|
||||
if (!getDimension().getReachableBiomes(this).contains(biome)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -460,7 +460,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
}
|
||||
|
||||
default IrisPosition lookForRegion(IrisRegion reg, long timeout, Consumer<Integer> triesc) {
|
||||
if (!getWorld().hasRealWorld()) {
|
||||
if (!getWorld().hasPlatformWorld()) {
|
||||
IrisLogging.error("Cannot GOTO without a bound world (headless mode)");
|
||||
return null;
|
||||
}
|
||||
@@ -626,7 +626,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> biomeKeys = getDimension().getAllBiomes(this).stream()
|
||||
Set<String> biomeKeys = getDimension().getReachableBiomes(this).stream()
|
||||
.filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey))
|
||||
.map(IrisRegistrant::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
@@ -678,24 +678,11 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
||||
}
|
||||
|
||||
default void cleanupMantleChunk(int x, int z) {
|
||||
if (shouldSkipMaintenanceCleanup()) {
|
||||
if (getPlatformHooks().shouldSkipMantleCleanup(this)) {
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
|
||||
getMantle().cleanupChunk(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldSkipMaintenanceCleanup() {
|
||||
IrisWorld world = getWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
String worldIdentity = world.identity();
|
||||
if (!WorldMaintenance.isWorldMaintenanceActive(worldIdentity)) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob == null || !pregeneratorJob.targetsWorldIdentity(worldIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
@@ -36,7 +37,7 @@ import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, Listener {
|
||||
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener {
|
||||
private final int taskId;
|
||||
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
|
||||
|
||||
@@ -53,7 +54,7 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent e) {
|
||||
for (Player i : e.getEngine().getWorld().getPlayers()) {
|
||||
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
|
||||
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
|
||||
VolmitSender s = new VolmitSender(i);
|
||||
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
|
||||
@@ -62,42 +63,42 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldSaveEvent e) {
|
||||
if (e.getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
getEngine().save();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldUnloadEvent e) {
|
||||
if (e.getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
getEngine().close();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(BlockBreakEvent e) {
|
||||
if (e.getPlayer().getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
onBlockBreak(e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(BlockPlaceEvent e) {
|
||||
if (e.getPlayer().getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
onBlockPlace(e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(ChunkLoadEvent e) {
|
||||
if (e.getChunk().getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
onChunkLoad(e.getChunk(), e.isNewChunk());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(ChunkUnloadEvent e) {
|
||||
if (e.getChunk().getWorld().equals(getTarget().getWorld().realWorld())) {
|
||||
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||
onChunkUnload(e.getChunk());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.math.RollingSequence;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
|
||||
@@ -39,10 +39,12 @@ public interface EngineComponent {
|
||||
|
||||
default void close() {
|
||||
try {
|
||||
BukkitPlatform.unregisterListener(this);
|
||||
EngineComponentCleanup cleanup = IrisServices.getOrNull(EngineComponentCleanup.class);
|
||||
if (cleanup != null) {
|
||||
cleanup.release(this);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.engine.framework;
|
||||
|
||||
public interface EngineComponentCleanup {
|
||||
void release(EngineComponent component);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.engine.framework;
|
||||
|
||||
public interface EngineEffectsProvider {
|
||||
EngineEffects create(Engine engine);
|
||||
}
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
@@ -31,7 +29,6 @@ import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
|
||||
public interface EngineMode extends Staged {
|
||||
RollingSequence r = new RollingSequence(64);
|
||||
@@ -76,10 +73,7 @@ public interface EngineMode extends Staged {
|
||||
|
||||
@BlockCoordinates
|
||||
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
|
||||
boolean cacheContext = true;
|
||||
if (J.isFolia() && getEngine().getWorld().hasRealWorld() && shouldDisableContextCacheForMaintenance()) {
|
||||
cacheContext = false;
|
||||
}
|
||||
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
|
||||
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
||||
ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics());
|
||||
|
||||
@@ -94,15 +88,4 @@ public interface EngineMode extends Staged {
|
||||
static boolean shouldDisableContextCacheForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
|
||||
return maintenanceActive && !pregeneratorTargetsWorld;
|
||||
}
|
||||
|
||||
private boolean shouldDisableContextCacheForMaintenance() {
|
||||
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(getEngine().getWorld().identity());
|
||||
if (!maintenanceActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(getEngine().getWorld().identity());
|
||||
return shouldDisableContextCacheForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
|
||||
public interface EnginePlatformHooks {
|
||||
default void refreshWorkspace(Engine engine) {
|
||||
}
|
||||
|
||||
default void refreshDatapackWorkspace(Engine engine) {
|
||||
}
|
||||
|
||||
default void reloadDatapacks(Engine engine) {
|
||||
}
|
||||
|
||||
default void fireHotloadEvent(Engine engine) {
|
||||
}
|
||||
|
||||
default void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
}
|
||||
|
||||
default boolean isPregeneratorActive(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void shutdownPregenerator(Engine engine) {
|
||||
}
|
||||
|
||||
default boolean shouldDisableChunkContextCache(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean shouldSkipMantleCleanup(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean shouldBypassMantleStages(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,14 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisEffect;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -42,38 +43,40 @@ public class EnginePlayer {
|
||||
public EnginePlayer(Engine engine, Player player) {
|
||||
this.engine = engine;
|
||||
this.player = player;
|
||||
lastLocation = player.getLocation().clone();
|
||||
lastLocation = null;
|
||||
lastSample = -1;
|
||||
sample();
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (sample() || !IrisSettings.get().getWorld().isEffectSystem())
|
||||
J.runEntity(player, this::tickOnEntity);
|
||||
}
|
||||
|
||||
private void tickOnEntity() {
|
||||
if (sample() || !IrisSettings.get().getWorld().isEffectSystem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
J.a(() -> {
|
||||
if (region != null) {
|
||||
for (IrisEffect j : region.getEffects()) {
|
||||
try {
|
||||
j.apply(player, getEngine());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
if (region != null) {
|
||||
for (IrisEffect effect : region.getEffects()) {
|
||||
try {
|
||||
effect.apply(player, getEngine());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (biome != null) {
|
||||
for (IrisEffect j : biome.getEffects()) {
|
||||
try {
|
||||
j.apply(player, getEngine());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
if (biome != null) {
|
||||
for (IrisEffect effect : biome.getEffects()) {
|
||||
try {
|
||||
effect.apply(player, getEngine());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public long ticksSinceLastSample() {
|
||||
@@ -82,10 +85,13 @@ public class EnginePlayer {
|
||||
|
||||
public boolean sample() {
|
||||
Location current = player.getLocation().clone();
|
||||
if (current.getWorld() != engine.getWorld().realWorld())
|
||||
if (current.getWorld() != BukkitWorldBinding.world(engine.getWorld())) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (ticksSinceLastSample() > 55 && current.distanceSquared(lastLocation) > 9 * 9) {
|
||||
boolean sampled = lastLocation != null;
|
||||
double distanceSquared = sampled ? current.distanceSquared(lastLocation) : 0D;
|
||||
if (needsSample(sampled, ticksSinceLastSample(), distanceSquared)) {
|
||||
lastLocation = current;
|
||||
lastSample = M.ms();
|
||||
biome = EngineBukkitOps.getBiome(engine, current);
|
||||
@@ -98,4 +104,8 @@ public class EnginePlayer {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean needsSample(boolean sampled, long elapsedMillis, double distanceSquared) {
|
||||
return !sampled || elapsedMillis > 55L && distanceSquared > 81D;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
@SuppressWarnings("EmptyMethod")
|
||||
public interface EngineWorldManager {
|
||||
void close();
|
||||
@@ -36,14 +31,4 @@ public interface EngineWorldManager {
|
||||
void onTick();
|
||||
|
||||
void onSave();
|
||||
|
||||
void onBlockBreak(BlockBreakEvent e);
|
||||
|
||||
void onBlockPlace(BlockPlaceEvent e);
|
||||
|
||||
void onChunkLoad(Chunk e, boolean generated);
|
||||
|
||||
void onChunkUnload(Chunk e);
|
||||
|
||||
void teleportAsync(PlayerTeleportEvent e);
|
||||
}
|
||||
|
||||
@@ -19,20 +19,22 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.object.StructureDistribution;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -40,17 +42,24 @@ import java.util.Set;
|
||||
* structure's own load key or its {@code vanillaSource} (so a vanilla key like
|
||||
* {@code minecraft:ancient_city} resolves to the imported {@code minecraft_ancient_city}).
|
||||
*
|
||||
* A per-pack index of placed keys is cached so {@code /locate} and {@code /iris goto} skip
|
||||
* A per-engine index of placed keys is cached so {@code /locate} and {@code /iris goto} skip
|
||||
* the grid scan entirely for keys that are not placed (keeping them as fast as vanilla).
|
||||
*
|
||||
* For RANDOM_SPREAD placements (the common, vanilla-style spaced grid) the locator jumps
|
||||
* straight to the single candidate chunk in each spacing-sized grid cell rather than testing
|
||||
* every chunk -- roughly a {@code spacing^2} reduction in placement checks, matching vanilla
|
||||
* locate performance. Non-grid distributions (DENSITY / CONCENTRIC_RINGS) fall back to the
|
||||
* exhaustive per-chunk ring scan.
|
||||
* locate performance. Density placements use a bounded per-chunk ring scan, while concentric
|
||||
* placements enumerate their finite configured starts directly.
|
||||
*/
|
||||
public final class IrisStructureLocator {
|
||||
private static final com.github.benmanes.caffeine.cache.Cache<IrisData, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
|
||||
private static final int DENSITY_CANDIDATE_BUDGET = 4_096;
|
||||
|
||||
private static final Cache<Engine, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
|
||||
private static final PlacementIndex EMPTY_INDEX = new PlacementIndex(
|
||||
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyList());
|
||||
private static final LocateResult NOT_FOUND_RESULT = new LocateResult(LocateStatus.NOT_FOUND, 0, 0, 0);
|
||||
private static final LocateResult SEARCH_LIMIT_RESULT =
|
||||
new LocateResult(LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
|
||||
|
||||
private IrisStructureLocator() {
|
||||
}
|
||||
@@ -67,70 +76,60 @@ public final class IrisStructureLocator {
|
||||
if (engine == null || key == null || key.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
PlacementIndex idx = index(engine);
|
||||
return idx.loadKeys.contains(key) || idx.vanillaSources.contains(key);
|
||||
PlacementIndex placementIndex = index(engine);
|
||||
String normalizedKey = normalize(key);
|
||||
return placementIndex.normalizedLoadKeys.contains(normalizedKey)
|
||||
|| placementIndex.vanillaSources.contains(normalizedKey);
|
||||
}
|
||||
|
||||
public static boolean suppressesVanilla(Engine engine, String vanillaKey) {
|
||||
if (engine == null || vanillaKey == null || vanillaKey.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String source : index(engine).vanillaSources) {
|
||||
if (source.equalsIgnoreCase(vanillaKey)) {
|
||||
return true;
|
||||
}
|
||||
return index(engine).vanillaSources.contains(normalize(vanillaKey));
|
||||
}
|
||||
|
||||
public static void invalidate(Engine engine) {
|
||||
if (engine != null) {
|
||||
INDEX_CACHE.invalidate(engine);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean startsInChunk(Engine engine, String key, int cx, int cz) {
|
||||
if (!isPlaced(engine, key)) {
|
||||
return false;
|
||||
}
|
||||
long seed = engine.getSeedManager().getMantle();
|
||||
RNG rng = new RNG(Cache.key(cx, cz) + seed);
|
||||
for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) {
|
||||
if (!matches(placement, key, engine.getData())) {
|
||||
continue;
|
||||
}
|
||||
if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return resolveInChunk(engine, key, cx, cz) != null;
|
||||
}
|
||||
|
||||
public static int[] locate(Engine engine, String key, int fromBlockX, int fromBlockZ, int maxRadiusChunks) {
|
||||
public static LocateResult locate(Engine engine, String key, int fromBlockX, int fromBlockZ, int maxRadiusChunks) {
|
||||
if (!isPlaced(engine, key)) {
|
||||
return null;
|
||||
return NOT_FOUND_RESULT;
|
||||
}
|
||||
int max = Math.max(1, Math.min(maxRadiusChunks, 2048));
|
||||
|
||||
List<int[]> gridParams = collectRandomSpreadParams(engine, key);
|
||||
if (gridParams == null || gridParams.isEmpty()) {
|
||||
// A non-grid distribution (DENSITY / CONCENTRIC_RINGS) matches this key, or no grid
|
||||
// params could be resolved; fall back to the exhaustive per-chunk scan.
|
||||
return locateByChunkScan(engine, key, fromBlockX, fromBlockZ, max);
|
||||
}
|
||||
|
||||
long seed = engine.getSeedManager().getMantle();
|
||||
int pcx = fromBlockX >> 4;
|
||||
int pcz = fromBlockZ >> 4;
|
||||
long maxDistSq = (long) max * (long) max;
|
||||
long bestDistSq = Long.MAX_VALUE;
|
||||
int[] best = null;
|
||||
LocatedCandidate best = null;
|
||||
PlacementCatalog catalog = collectPlacementCatalog(engine, key);
|
||||
if (catalog.randomSpread().isEmpty() && catalog.concentricRings().isEmpty() && !catalog.hasDensity()) {
|
||||
return NOT_FOUND_RESULT;
|
||||
}
|
||||
SeedManager seedManager = engine.getSeedManager();
|
||||
if (seedManager == null) {
|
||||
return NOT_FOUND_RESULT;
|
||||
}
|
||||
long seed = seedManager.getMantle();
|
||||
|
||||
for (int[] params : gridParams) {
|
||||
int spacing = Math.max(1, params[0]);
|
||||
int separation = params[1];
|
||||
int salt = params[2];
|
||||
for (RandomSpreadParameters parameters : catalog.randomSpread) {
|
||||
int spacing = Math.max(1, parameters.spacing());
|
||||
int centerCellX = Math.floorDiv(pcx, spacing);
|
||||
int centerCellZ = Math.floorDiv(pcz, spacing);
|
||||
int cellRadius = (max / spacing) + 2;
|
||||
int firstHitRing = -1;
|
||||
|
||||
for (int r = 0; r <= cellRadius; r++) {
|
||||
if (firstHitRing >= 0 && r > firstHitRing + 1) {
|
||||
long lowerBound = cellRingDistanceLowerBound(r, spacing, pcx, pcz, centerCellX, centerCellZ);
|
||||
if (best != null && lowerBound * lowerBound >= best.distanceSquared()) {
|
||||
break;
|
||||
}
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
@@ -139,98 +138,360 @@ public final class IrisStructureLocator {
|
||||
continue;
|
||||
}
|
||||
int[] candidate = StructurePlacementGrid.randomSpreadCellChunk(
|
||||
centerCellX + dx, centerCellZ + dz, spacing, separation, salt, seed);
|
||||
centerCellX + dx, centerCellZ + dz, spacing, parameters.separation(), parameters.salt(), seed);
|
||||
int cx = candidate[0];
|
||||
int cz = candidate[1];
|
||||
long ddx = (long) cx - pcx;
|
||||
long ddz = (long) cz - pcz;
|
||||
long distSq = ddx * ddx + ddz * ddz;
|
||||
if (distSq > maxDistSq || distSq >= bestDistSq) {
|
||||
long distSq = distanceSquared(cx, cz, pcx, pcz);
|
||||
if (distSq > maxDistSq || best != null && distSq >= best.distanceSquared()) {
|
||||
continue;
|
||||
}
|
||||
if (startsInChunk(engine, key, cx, cz)) {
|
||||
bestDistSq = distSq;
|
||||
best = new int[]{cx << 4, structureY(engine, key, cx, cz), cz << 4};
|
||||
if (firstHitRing < 0) {
|
||||
firstHitRing = r;
|
||||
}
|
||||
ResolvedPlacement resolved = resolveInChunk(engine, key, cx, cz);
|
||||
if (resolved != null) {
|
||||
best = new LocatedCandidate(resolved, distSq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
Set<Long> checkedRingChunks = new LinkedHashSet<>();
|
||||
for (IrisStructurePlacement placement : catalog.concentricRings) {
|
||||
int count = Math.max(1, placement.getRingCount());
|
||||
for (int placementIndex = 0; placementIndex < count; placementIndex++) {
|
||||
int[] candidate = StructurePlacementGrid.concentricRingChunk(placement, placementIndex, seed);
|
||||
if (candidate == null || !checkedRingChunks.add(chunkKey(candidate[0], candidate[1]))) {
|
||||
continue;
|
||||
}
|
||||
long distSq = distanceSquared(candidate[0], candidate[1], pcx, pcz);
|
||||
if (distSq > maxDistSq || best != null && distSq >= best.distanceSquared()) {
|
||||
continue;
|
||||
}
|
||||
ResolvedPlacement resolved = resolveInChunk(engine, key, candidate[0], candidate[1]);
|
||||
if (resolved != null) {
|
||||
best = new LocatedCandidate(resolved, distSq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] locateByChunkScan(Engine engine, String key, int fromBlockX, int fromBlockZ, int max) {
|
||||
int pcx = fromBlockX >> 4;
|
||||
int pcz = fromBlockZ >> 4;
|
||||
for (int r = 0; r <= max; r++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) {
|
||||
continue;
|
||||
}
|
||||
int cx = pcx + dx;
|
||||
int cz = pcz + dz;
|
||||
if (startsInChunk(engine, key, cx, cz)) {
|
||||
return new int[]{cx << 4, structureY(engine, key, cx, cz), cz << 4};
|
||||
if (catalog.hasDensity) {
|
||||
int checkedDensityCandidates = 0;
|
||||
for (int r = 0; r <= max; r++) {
|
||||
if (best != null && (long) r * r > best.distanceSquared()) {
|
||||
break;
|
||||
}
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) {
|
||||
continue;
|
||||
}
|
||||
int cx = pcx + dx;
|
||||
int cz = pcz + dz;
|
||||
long distSq = distanceSquared(cx, cz, pcx, pcz);
|
||||
if (distSq > maxDistSq || best != null && distSq >= best.distanceSquared()) {
|
||||
continue;
|
||||
}
|
||||
if (checkedDensityCandidates >= DENSITY_CANDIDATE_BUDGET) {
|
||||
return SEARCH_LIMIT_RESULT;
|
||||
}
|
||||
checkedDensityCandidates++;
|
||||
ResolvedPlacement resolved = resolveInChunk(engine, key, cx, cz);
|
||||
if (resolved != null) {
|
||||
best = new LocatedCandidate(resolved, distSq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best == null) {
|
||||
return NOT_FOUND_RESULT;
|
||||
}
|
||||
ResolvedPlacement resolved = best.resolved();
|
||||
return new LocateResult(LocateStatus.FOUND, resolved.originX(), resolved.baseY(), resolved.originZ());
|
||||
}
|
||||
|
||||
public static ResolvedPlacement resolvePlacement(Engine engine, IrisStructurePlacement placement, int cx, int cz, int placementOrdinal) {
|
||||
if (engine == null || placement == null || placement.getDistribution() == null
|
||||
|| placement.getStructures() == null || placement.getStructures().isEmpty()
|
||||
|| engine.getData() == null || engine.getSeedManager() == null) {
|
||||
return null;
|
||||
}
|
||||
long seed = engine.getSeedManager().getMantle();
|
||||
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, placementOrdinal)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG rng = StructurePlacementGrid.placementRng(placement, cx, cz, seed, placementOrdinal);
|
||||
int originX = (cx << 4) + rng.nextInt(16);
|
||||
int originZ = (cz << 4) + rng.nextInt(16);
|
||||
Integer baseY = resolveBaseY(engine, placement, originX, originZ, rng);
|
||||
if (baseY == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String selectedKey = selectStructureKey(placement, rng);
|
||||
if (selectedKey == null || selectedKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisStructure structure = IrisData.loadAnyStructure(selectedKey, engine.getData());
|
||||
if (structure == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StructureAssembler assembler = new StructureAssembler(engine.getData(), structure, originX, baseY, originZ);
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
pieces = alignSurfacePieces(pieces, placement, structure, baseY);
|
||||
boolean exactY = hasExactY(placement, structure, pieces);
|
||||
|
||||
int worldMin = engine.getMinHeight() + 1;
|
||||
int worldMax = engine.getMinHeight() + engine.getHeight() - 1;
|
||||
if (exactY) {
|
||||
Integer verticalShift = resolveVerticalShift(pieces, placement, baseY, worldMin, worldMax);
|
||||
if (verticalShift == null) {
|
||||
return null;
|
||||
}
|
||||
if (verticalShift != 0) {
|
||||
pieces = shiftPieces(pieces, verticalShift);
|
||||
baseY += verticalShift;
|
||||
}
|
||||
}
|
||||
|
||||
long configuredRadius = (long) Math.max(1, structure.getMaxSizeChunks()) * 16L;
|
||||
int structureRadius = (int) Math.min(Integer.MAX_VALUE, configuredRadius);
|
||||
if (!fitsHorizontalBounds(pieces, originX, originZ, structureRadius)
|
||||
|| (exactY && !fitsVerticalBounds(pieces, worldMin, worldMax))) {
|
||||
return null;
|
||||
}
|
||||
return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY);
|
||||
}
|
||||
|
||||
static boolean fitsWorldBounds(KList<PlacedStructurePiece> pieces, int worldMin, int worldMax,
|
||||
int originX, int originZ, int structureRadius) {
|
||||
int[] bounds = computeBounds(pieces);
|
||||
if (bounds == null) {
|
||||
return false;
|
||||
}
|
||||
return fitsVerticalBounds(bounds, worldMin, worldMax)
|
||||
&& fitsHorizontalBounds(bounds, originX, originZ, structureRadius);
|
||||
}
|
||||
|
||||
private static ResolvedPlacement resolveInChunk(Engine engine, String key, int cx, int cz) {
|
||||
IrisData data = engine.getData();
|
||||
KList<IrisStructurePlacement> placements = placementsAt(engine, cx, cz);
|
||||
for (int placementOrdinal = 0; placementOrdinal < placements.size(); placementOrdinal++) {
|
||||
IrisStructurePlacement placement = placements.get(placementOrdinal);
|
||||
if (!matches(placement, key, data)) {
|
||||
continue;
|
||||
}
|
||||
ResolvedPlacement resolved = resolvePlacement(engine, placement, cx, cz, placementOrdinal);
|
||||
if (resolved != null && matchesResolved(resolved, key)) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the distinct {spacing, separation, salt} tuples of every RANDOM_SPREAD placement that
|
||||
* matches the key across the dimension, its regions and biomes. Returns {@code null} if any
|
||||
* matching placement uses a non-grid distribution (DENSITY / CONCENTRIC_RINGS), signalling the
|
||||
* caller to fall back to the exhaustive per-chunk scan.
|
||||
*/
|
||||
private static List<int[]> collectRandomSpreadParams(Engine engine, String key) {
|
||||
IrisData data = engine.getData();
|
||||
List<int[]> params = new ArrayList<>();
|
||||
Set<Long> seen = new LinkedHashSet<>();
|
||||
|
||||
KList<IrisStructurePlacement> all = new KList<>();
|
||||
all.addAll(engine.getDimension().getStructures());
|
||||
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
|
||||
all.addAll(region.getStructures());
|
||||
private static Integer resolveBaseY(Engine engine, IrisStructurePlacement placement, int originX, int originZ, RNG rng) {
|
||||
int worldMin = engine.getMinHeight() + 1;
|
||||
int worldMax = engine.getMinHeight() + engine.getHeight() - 1;
|
||||
if (worldMin > worldMax) {
|
||||
return null;
|
||||
}
|
||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
||||
all.addAll(biome.getStructures());
|
||||
if (!placement.isUnderground()) {
|
||||
int surfaceY = engine.getHeight(originX, originZ, true) + engine.getMinHeight();
|
||||
return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
|
||||
}
|
||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
if (bandMin > bandMax) {
|
||||
return null;
|
||||
}
|
||||
return randomInclusive(rng, bandMin, bandMax);
|
||||
}
|
||||
|
||||
for (IrisStructurePlacement placement : all) {
|
||||
if (placement == null) {
|
||||
static Integer resolveVerticalShift(KList<PlacedStructurePiece> pieces, IrisStructurePlacement placement,
|
||||
int baseY, int worldMin, int worldMax) {
|
||||
int[] bounds = computeBounds(pieces);
|
||||
if (bounds == null) {
|
||||
return null;
|
||||
}
|
||||
if (!placement.isUnderground()) {
|
||||
return bounds[1] >= worldMin && bounds[4] <= worldMax ? 0 : null;
|
||||
}
|
||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
int bandMax = Math.min(worldMax, Math.max(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
int minimumShift = Math.max(worldMin - bounds[1], bandMin - baseY);
|
||||
int maximumShift = Math.min(worldMax - bounds[4], bandMax - baseY);
|
||||
if (minimumShift > maximumShift) {
|
||||
return null;
|
||||
}
|
||||
if (minimumShift > 0) {
|
||||
return minimumShift;
|
||||
}
|
||||
if (maximumShift < 0) {
|
||||
return maximumShift;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static String selectStructureKey(IrisStructurePlacement placement, RNG rng) {
|
||||
if (placement == null || placement.getStructures() == null || placement.getStructures().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return placement.getStructures().get(rng.nextInt(placement.getStructures().size()));
|
||||
}
|
||||
|
||||
private static KList<PlacedStructurePiece> alignSurfacePieces(KList<PlacedStructurePiece> pieces,
|
||||
IrisStructurePlacement placement,
|
||||
IrisStructure structure, int baseY) {
|
||||
if (placement.isUnderground() || pieces.size() <= 1
|
||||
|| structure.getPlaceMode() == ObjectPlaceMode.STRUCTURE_PIECE
|
||||
|| structure.getPlaceMode() == ObjectPlaceMode.FLOATING) {
|
||||
return pieces;
|
||||
}
|
||||
int[] bounds = computeBounds(pieces);
|
||||
return bounds == null ? pieces : shiftPieces(pieces, baseY - bounds[1]);
|
||||
}
|
||||
|
||||
static boolean hasExactY(IrisStructurePlacement placement, IrisStructure structure,
|
||||
KList<PlacedStructurePiece> pieces) {
|
||||
return placement.isUnderground() || pieces.size() > 1
|
||||
|| structure.getPlaceMode() == ObjectPlaceMode.STRUCTURE_PIECE
|
||||
|| structure.getPlaceMode() == ObjectPlaceMode.FLOATING;
|
||||
}
|
||||
|
||||
private static boolean fitsVerticalBounds(KList<PlacedStructurePiece> pieces, int worldMin, int worldMax) {
|
||||
int[] bounds = computeBounds(pieces);
|
||||
return bounds != null && fitsVerticalBounds(bounds, worldMin, worldMax);
|
||||
}
|
||||
|
||||
private static boolean fitsVerticalBounds(int[] bounds, int worldMin, int worldMax) {
|
||||
return bounds[1] >= worldMin && bounds[4] <= worldMax;
|
||||
}
|
||||
|
||||
private static boolean fitsHorizontalBounds(KList<PlacedStructurePiece> pieces, int originX, int originZ,
|
||||
int structureRadius) {
|
||||
int[] bounds = computeBounds(pieces);
|
||||
return bounds != null && fitsHorizontalBounds(bounds, originX, originZ, structureRadius);
|
||||
}
|
||||
|
||||
private static boolean fitsHorizontalBounds(int[] bounds, int originX, int originZ, int structureRadius) {
|
||||
return bounds[0] >= originX - structureRadius && bounds[3] <= originX + structureRadius
|
||||
&& bounds[2] >= originZ - structureRadius && bounds[5] <= originZ + structureRadius;
|
||||
}
|
||||
|
||||
private static KList<PlacedStructurePiece> shiftPieces(KList<PlacedStructurePiece> pieces, int shiftY) {
|
||||
KList<PlacedStructurePiece> shifted = new KList<>();
|
||||
for (PlacedStructurePiece piece : pieces) {
|
||||
shifted.add(new PlacedStructurePiece(
|
||||
piece.getPiece(), piece.getObject(), piece.getX(), piece.getY() + shiftY, piece.getZ(), piece.getRotation(),
|
||||
piece.getMinX(), piece.getMinY() + shiftY, piece.getMinZ(),
|
||||
piece.getMaxX(), piece.getMaxY() + shiftY, piece.getMaxZ()));
|
||||
}
|
||||
return shifted;
|
||||
}
|
||||
|
||||
private static int[] computeBounds(KList<PlacedStructurePiece> pieces) {
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (PlacedStructurePiece piece : pieces) {
|
||||
minX = Math.min(minX, piece.getMinX());
|
||||
minY = Math.min(minY, piece.getMinY());
|
||||
minZ = Math.min(minZ, piece.getMinZ());
|
||||
maxX = Math.max(maxX, piece.getMaxX());
|
||||
maxY = Math.max(maxY, piece.getMaxY());
|
||||
maxZ = Math.max(maxZ, piece.getMaxZ());
|
||||
}
|
||||
return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
|
||||
}
|
||||
|
||||
private static int randomInclusive(RNG rng, int minimum, int maximum) {
|
||||
if (minimum == maximum) {
|
||||
return minimum;
|
||||
}
|
||||
return minimum + rng.nextInt((maximum - minimum) + 1);
|
||||
}
|
||||
|
||||
private static PlacementCatalog collectPlacementCatalog(Engine engine, String key) {
|
||||
Set<RandomSpreadParameters> randomSpread = new LinkedHashSet<>();
|
||||
List<IrisStructurePlacement> concentricRings = new ArrayList<>();
|
||||
boolean hasDensity = false;
|
||||
for (IrisStructurePlacement placement : index(engine).placements) {
|
||||
if (!matches(placement, key, engine.getData())) {
|
||||
continue;
|
||||
}
|
||||
if (!matches(placement, key, data)) {
|
||||
continue;
|
||||
}
|
||||
if (placement.getDistribution() != StructureDistribution.RANDOM_SPREAD) {
|
||||
return null;
|
||||
}
|
||||
int spacing = Math.max(1, placement.getSpacing());
|
||||
int separation = placement.getSeparation();
|
||||
int salt = placement.getSalt();
|
||||
long packed = ((long) spacing << 42) ^ ((long) (separation & 0x1FFFFF) << 21) ^ (salt & 0x1FFFFFL);
|
||||
if (seen.add(packed)) {
|
||||
params.add(new int[]{spacing, separation, salt});
|
||||
if (placement.getDistribution() == StructureDistribution.RANDOM_SPREAD) {
|
||||
randomSpread.add(new RandomSpreadParameters(
|
||||
Math.max(1, placement.getSpacing()), placement.getSeparation(), placement.getSalt()));
|
||||
} else if (placement.getDistribution() == StructureDistribution.CONCENTRIC_RINGS) {
|
||||
concentricRings.add(placement);
|
||||
} else if (isSearchableDensityPlacement(engine, placement)) {
|
||||
hasDensity = true;
|
||||
}
|
||||
}
|
||||
return new PlacementCatalog(List.copyOf(randomSpread), List.copyOf(concentricRings), hasDensity);
|
||||
}
|
||||
|
||||
return params;
|
||||
static boolean isSearchableDensityPlacement(Engine engine, IrisStructurePlacement placement) {
|
||||
if (engine == null || placement == null || placement.getDistribution() != StructureDistribution.DENSITY
|
||||
|| !(placement.getDensity() > 0.0) || engine.getHeight() <= 0) {
|
||||
return false;
|
||||
}
|
||||
long worldMin = (long) engine.getMinHeight() + (placement.isUnderground() ? 1L : 0L);
|
||||
long worldMax = (long) engine.getMinHeight() + engine.getHeight() - 1L;
|
||||
long configuredMin = placement.isUnderground()
|
||||
? Math.min(placement.getMinHeight(), placement.getMaxHeight())
|
||||
: placement.getMinHeight();
|
||||
long configuredMax = placement.isUnderground()
|
||||
? Math.max(placement.getMinHeight(), placement.getMaxHeight())
|
||||
: placement.getMaxHeight();
|
||||
return configuredMin <= configuredMax && configuredMin <= worldMax && configuredMax >= worldMin;
|
||||
}
|
||||
|
||||
private static long distanceSquared(int cx, int cz, int pcx, int pcz) {
|
||||
long dx = (long) cx - pcx;
|
||||
long dz = (long) cz - pcz;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
|
||||
static long cellRingDistanceLowerBound(int ring, int spacing, int pcx, int pcz,
|
||||
int centerCellX, int centerCellZ) {
|
||||
if (ring <= 0) {
|
||||
return 0L;
|
||||
}
|
||||
int localX = pcx - (centerCellX * spacing);
|
||||
int localZ = pcz - (centerCellZ * spacing);
|
||||
long ringOffset = (long) ring * spacing;
|
||||
long positiveX = ringOffset - localX;
|
||||
long negativeX = ringOffset + localX - (spacing - 1L);
|
||||
long positiveZ = ringOffset - localZ;
|
||||
long negativeZ = ringOffset + localZ - (spacing - 1L);
|
||||
return Math.min(Math.min(positiveX, negativeX), Math.min(positiveZ, negativeZ));
|
||||
}
|
||||
|
||||
private static long chunkKey(int cx, int cz) {
|
||||
return ((long) cx << 32) ^ (cz & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static KList<IrisStructurePlacement> placementsAt(Engine engine, int cx, int cz) {
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (engine.getDimension() == null || engine.getComplex() == null) {
|
||||
return placements;
|
||||
}
|
||||
int bx = 8 + (cx << 4);
|
||||
int bz = 8 + (cz << 4);
|
||||
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz);
|
||||
IrisRegion region = engine.getComplex().getRegionStream().get(bx, bz);
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
}
|
||||
@@ -242,90 +503,140 @@ public final class IrisStructureLocator {
|
||||
}
|
||||
|
||||
private static boolean matches(IrisStructurePlacement placement, String key, IrisData data) {
|
||||
if (placement == null || placement.getStructures() == null || key == null || data == null) {
|
||||
return false;
|
||||
}
|
||||
String normalizedKey = normalize(key);
|
||||
for (String structureKey : placement.getStructures()) {
|
||||
if (structureKey == null) {
|
||||
if (structureKey == null || structureKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (structureKey.equalsIgnoreCase(key)) {
|
||||
return true;
|
||||
}
|
||||
IrisStructure structure = IrisData.loadAnyStructure(structureKey, data);
|
||||
if (structure != null) {
|
||||
String vanillaSource = structure.getVanillaSource();
|
||||
if (vanillaSource != null && !vanillaSource.isEmpty() && vanillaSource.equalsIgnoreCase(key)) {
|
||||
return true;
|
||||
}
|
||||
if (structure == null) {
|
||||
continue;
|
||||
}
|
||||
if (normalize(structureKey).equals(normalizedKey)
|
||||
|| normalize(structure.getLoadKey()).equals(normalizedKey)
|
||||
|| normalize(structure.getVanillaSource()).equals(normalizedKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int structureY(Engine engine, String key, int cx, int cz) {
|
||||
IrisData data = engine.getData();
|
||||
int bx = 8 + (cx << 4);
|
||||
int bz = 8 + (cz << 4);
|
||||
for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) {
|
||||
if (!matches(placement, key, data)) {
|
||||
continue;
|
||||
}
|
||||
if (placement.isUnderground()) {
|
||||
int lo = Math.max(engine.getMinHeight() + 1, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
int hi = Math.min(engine.getMinHeight() + engine.getHeight() - 1, Math.max(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
return (lo + hi) / 2;
|
||||
}
|
||||
}
|
||||
return engine.getHeight(bx, bz) + engine.getMinHeight();
|
||||
static boolean matchesResolved(ResolvedPlacement resolved, String key) {
|
||||
String normalizedKey = normalize(key);
|
||||
return normalize(resolved.structureKey()).equals(normalizedKey)
|
||||
|| normalize(resolved.structure().getLoadKey()).equals(normalizedKey)
|
||||
|| normalize(resolved.structure().getVanillaSource()).equals(normalizedKey);
|
||||
}
|
||||
|
||||
private static String normalize(String key) {
|
||||
return key == null ? "" : key.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static PlacementIndex index(Engine engine) {
|
||||
if (engine == null || engine.getData() == null || engine.getDimension() == null) {
|
||||
return EMPTY_INDEX;
|
||||
}
|
||||
return INDEX_CACHE.get(engine, ignored -> build(engine));
|
||||
}
|
||||
|
||||
private static PlacementIndex build(Engine engine) {
|
||||
IrisData data = engine.getData();
|
||||
return INDEX_CACHE.get(data, (IrisData keyData) -> build(engine, keyData));
|
||||
}
|
||||
|
||||
private static PlacementIndex build(Engine engine, IrisData data) {
|
||||
Set<String> loadKeys = new LinkedHashSet<>();
|
||||
Set<String> normalizedLoadKeys = new LinkedHashSet<>();
|
||||
Set<String> vanillaSources = new LinkedHashSet<>();
|
||||
collect(engine.getDimension().getStructures(), data, loadKeys, vanillaSources);
|
||||
List<IrisStructurePlacement> placements = new ArrayList<>();
|
||||
collect(engine.getDimension().getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
|
||||
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
|
||||
collect(region.getStructures(), data, loadKeys, vanillaSources);
|
||||
collect(region.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
|
||||
}
|
||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
||||
collect(biome.getStructures(), data, loadKeys, vanillaSources);
|
||||
for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
collect(biome.getStructures(), data, loadKeys, normalizedLoadKeys, vanillaSources, placements);
|
||||
}
|
||||
return new PlacementIndex(loadKeys, vanillaSources);
|
||||
return new PlacementIndex(
|
||||
Collections.unmodifiableSet(loadKeys),
|
||||
Collections.unmodifiableSet(normalizedLoadKeys),
|
||||
Collections.unmodifiableSet(vanillaSources),
|
||||
List.copyOf(placements));
|
||||
}
|
||||
|
||||
private static void collect(KList<IrisStructurePlacement> placements, IrisData data, Set<String> loadKeys, Set<String> vanillaSources) {
|
||||
if (placements == null) {
|
||||
private static void collect(KList<IrisStructurePlacement> source, IrisData data, Set<String> loadKeys,
|
||||
Set<String> normalizedLoadKeys, Set<String> vanillaSources,
|
||||
List<IrisStructurePlacement> placements) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
if (placement == null) {
|
||||
for (IrisStructurePlacement placement : source) {
|
||||
if (placement == null || placement.getStructures() == null) {
|
||||
continue;
|
||||
}
|
||||
boolean validPlacement = false;
|
||||
for (String structureKey : placement.getStructures()) {
|
||||
if (structureKey == null || structureKey.isEmpty()) {
|
||||
if (structureKey == null || structureKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
loadKeys.add(structureKey);
|
||||
IrisStructure structure = IrisData.loadAnyStructure(structureKey, data);
|
||||
if (structure != null) {
|
||||
String vanillaSource = structure.getVanillaSource();
|
||||
if (vanillaSource != null && !vanillaSource.isEmpty()) {
|
||||
vanillaSources.add(vanillaSource);
|
||||
}
|
||||
if (structure == null) {
|
||||
continue;
|
||||
}
|
||||
validPlacement = true;
|
||||
loadKeys.add(structureKey);
|
||||
normalizedLoadKeys.add(normalize(structureKey));
|
||||
if (structure.getLoadKey() != null && !structure.getLoadKey().isBlank()) {
|
||||
loadKeys.add(structure.getLoadKey());
|
||||
normalizedLoadKeys.add(normalize(structure.getLoadKey()));
|
||||
}
|
||||
if (structure.getVanillaSource() != null && !structure.getVanillaSource().isBlank()) {
|
||||
vanillaSources.add(normalize(structure.getVanillaSource()));
|
||||
}
|
||||
}
|
||||
if (validPlacement) {
|
||||
placements.add(placement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ResolvedPlacement(IrisStructurePlacement placement, String structureKey, IrisStructure structure,
|
||||
KList<PlacedStructurePiece> pieces, RNG rng,
|
||||
int originX, int baseY, int originZ, boolean exactY) {
|
||||
}
|
||||
|
||||
public enum LocateStatus {
|
||||
FOUND,
|
||||
NOT_FOUND,
|
||||
SEARCH_LIMIT_REACHED
|
||||
}
|
||||
|
||||
public record LocateResult(LocateStatus status, int originX, int baseY, int originZ) {
|
||||
public boolean found() {
|
||||
return status == LocateStatus.FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
private record RandomSpreadParameters(int spacing, int separation, int salt) {
|
||||
}
|
||||
|
||||
private record PlacementCatalog(List<RandomSpreadParameters> randomSpread,
|
||||
List<IrisStructurePlacement> concentricRings, boolean hasDensity) {
|
||||
}
|
||||
|
||||
private record LocatedCandidate(ResolvedPlacement resolved, long distanceSquared) {
|
||||
}
|
||||
|
||||
private static final class PlacementIndex {
|
||||
private final Set<String> loadKeys;
|
||||
private final Set<String> normalizedLoadKeys;
|
||||
private final Set<String> vanillaSources;
|
||||
private final List<IrisStructurePlacement> placements;
|
||||
|
||||
private PlacementIndex(Set<String> loadKeys, Set<String> vanillaSources) {
|
||||
private PlacementIndex(Set<String> loadKeys, Set<String> normalizedLoadKeys,
|
||||
Set<String> vanillaSources, List<IrisStructurePlacement> placements) {
|
||||
this.loadKeys = loadKeys;
|
||||
this.normalizedLoadKeys = normalizedLoadKeys;
|
||||
this.vanillaSources = vanillaSources;
|
||||
this.placements = placements;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,17 +22,23 @@ import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
public final class StructurePlacementGrid {
|
||||
private static final long DENSITY_SIGNATURE = 0x6A09E667F3BCC909L;
|
||||
|
||||
private StructurePlacementGrid() {
|
||||
}
|
||||
|
||||
public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed, RNG chunkRng) {
|
||||
public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
|
||||
return switch (placement.getDistribution()) {
|
||||
case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(), placement.getSalt(), seed);
|
||||
case DENSITY -> chunkRng.chance(placement.getDensity());
|
||||
case DENSITY -> densityStart(placement, cx, cz, seed, placementOrdinal);
|
||||
case CONCENTRIC_RINGS -> concentricRingsStart(cx, cz, placement, seed);
|
||||
};
|
||||
}
|
||||
|
||||
public static RNG placementRng(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
|
||||
return new RNG(placementSeed(placement, cx, cz, seed, placementOrdinal));
|
||||
}
|
||||
|
||||
public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) {
|
||||
int sp = Math.max(1, spacing);
|
||||
int cellX = Math.floorDiv(cx, sp);
|
||||
@@ -51,11 +57,36 @@ public final class StructurePlacementGrid {
|
||||
int sep = Math.max(0, Math.min(separation, sp - 1));
|
||||
RNG r = new RNG(mix(seed, cellX, cellZ, salt));
|
||||
int range = Math.max(1, sp - sep);
|
||||
int startCx = cellX * sp + r.i(0, range - 1);
|
||||
int startCz = cellZ * sp + r.i(0, range - 1);
|
||||
int startCx = cellX * sp + r.nextInt(range);
|
||||
int startCz = cellZ * sp + r.nextInt(range);
|
||||
return new int[]{startCx, startCz};
|
||||
}
|
||||
|
||||
public static int[] concentricRingChunk(IrisStructurePlacement placement, int placementIndex, long seed) {
|
||||
int count = Math.max(1, placement.getRingCount());
|
||||
if (placementIndex < 0 || placementIndex >= count) {
|
||||
return null;
|
||||
}
|
||||
int distance = Math.max(1, placement.getRingDistance());
|
||||
int spread = Math.max(1, placement.getRingSpread());
|
||||
int ring = Math.floorDiv(placementIndex, spread) + 1;
|
||||
int firstIndex = (ring - 1) * spread;
|
||||
int slots = Math.min(spread, count - firstIndex);
|
||||
int slot = placementIndex - firstIndex;
|
||||
double slotAngle = (2.0 * Math.PI) / slots;
|
||||
double offset = unitDouble(mix(seed, ring, count, placement.getSalt())) * slotAngle;
|
||||
double angle = offset + (slot * slotAngle);
|
||||
long configuredRadius = (long) ring * distance;
|
||||
if (configuredRadius > Integer.MAX_VALUE) {
|
||||
return null;
|
||||
}
|
||||
int radius = (int) configuredRadius;
|
||||
return new int[]{
|
||||
(int) Math.round(Math.cos(angle) * radius),
|
||||
(int) Math.round(Math.sin(angle) * radius)
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean concentricRingsStart(int cx, int cz, IrisStructurePlacement placement, long seed) {
|
||||
if (cx == 0 && cz == 0) {
|
||||
return false;
|
||||
@@ -64,23 +95,61 @@ public final class StructurePlacementGrid {
|
||||
int count = Math.max(1, placement.getRingCount());
|
||||
int spread = Math.max(1, placement.getRingSpread());
|
||||
double dist = Math.sqrt((double) cx * cx + (double) cz * cz);
|
||||
int ring = (int) Math.round(dist / distance);
|
||||
if (ring <= 0) {
|
||||
int estimatedRing = (int) Math.round(dist / distance);
|
||||
int ringCount = Math.ceilDiv(count, spread);
|
||||
int firstRing = Math.max(1, estimatedRing - 1);
|
||||
int lastRing = Math.min(ringCount, estimatedRing + 1);
|
||||
for (int ring = firstRing; ring <= lastRing; ring++) {
|
||||
int firstIndex = (ring - 1) * spread;
|
||||
int slots = Math.min(spread, count - firstIndex);
|
||||
if (slots <= 0) {
|
||||
continue;
|
||||
}
|
||||
double slotAngle = (2.0 * Math.PI) / slots;
|
||||
double offset = unitDouble(mix(seed, ring, count, placement.getSalt())) * slotAngle;
|
||||
double slotPosition = (Math.atan2(cz, cx) - offset) / slotAngle;
|
||||
int nearestSlot = Math.floorMod((int) Math.round(slotPosition), slots);
|
||||
for (int delta = -1; delta <= 1; delta++) {
|
||||
int slot = Math.floorMod(nearestSlot + delta, slots);
|
||||
int[] candidate = concentricRingChunk(placement, firstIndex + slot, seed);
|
||||
if (candidate != null && candidate[0] == cx && candidate[1] == cz) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean densityStart(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
|
||||
double density = placement.getDensity();
|
||||
if (density <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
int ringRadius = ring * distance;
|
||||
int slots = Math.min(spread * ring, count);
|
||||
if (slots <= 0) {
|
||||
return false;
|
||||
if (density >= 1.0) {
|
||||
return true;
|
||||
}
|
||||
double slotAngle = (2 * Math.PI) / slots;
|
||||
double offset = (mix(seed, ring, 0, 0) & 0xFFFFL) / 65536.0 * slotAngle;
|
||||
double angle = Math.atan2(cz, cx);
|
||||
int slotIndex = (int) Math.round((angle - offset) / slotAngle);
|
||||
double idealAngle = offset + slotIndex * slotAngle;
|
||||
int idealCx = (int) Math.round(Math.cos(idealAngle) * ringRadius);
|
||||
int idealCz = (int) Math.round(Math.sin(idealAngle) * ringRadius);
|
||||
return idealCx == cx && idealCz == cz;
|
||||
RNG rng = new RNG(placementSeed(placement, cx, cz, seed, placementOrdinal) ^ DENSITY_SIGNATURE);
|
||||
return rng.chance(density);
|
||||
}
|
||||
|
||||
private static long placementSeed(IrisStructurePlacement placement, int cx, int cz, long seed, int placementOrdinal) {
|
||||
long signature = 1469598103934665603L;
|
||||
signature = (signature ^ placement.getDistribution().ordinal()) * 1099511628211L;
|
||||
signature = (signature ^ placement.getSalt()) * 1099511628211L;
|
||||
signature = (signature ^ placementOrdinal) * 1099511628211L;
|
||||
for (String key : placement.getStructures()) {
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
signature = (signature ^ key.charAt(i)) * 1099511628211L;
|
||||
}
|
||||
}
|
||||
return mix(seed ^ signature, cx, cz, placement.getSalt() ^ placementOrdinal);
|
||||
}
|
||||
|
||||
private static double unitDouble(long value) {
|
||||
return (value >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
public static long mix(long seed, int a, int b, int salt) {
|
||||
|
||||
@@ -18,17 +18,15 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorld;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Determines which vanilla/datapack structures can ever generate in a world, using the same biome
|
||||
@@ -37,10 +35,10 @@ import java.util.WeakHashMap;
|
||||
* never produced cannot generate and must not be scanned for by {@code /locate} or {@code /iris find}
|
||||
* (an unbounded scan for an absent biome is what stalls the server).
|
||||
*
|
||||
* <p>The reachable set is fixed per pack/world, so it is cached per {@link IrisData}.
|
||||
* <p>The reachable set is fixed per live engine/world and is rebuilt after hotload.
|
||||
*/
|
||||
public final class StructureReachability {
|
||||
private static final Map<IrisData, Set<String>> REACHABLE_CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Cache<Engine, Set<String>> REACHABLE_CACHE = Caffeine.newBuilder().weakKeys().build();
|
||||
|
||||
private StructureReachability() {
|
||||
}
|
||||
@@ -49,17 +47,10 @@ public final class StructureReachability {
|
||||
if (engine == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
if (data == null) {
|
||||
if (engine.getData() == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> cached = REACHABLE_CACHE.get(data);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Set<String> built = build(engine);
|
||||
REACHABLE_CACHE.put(data, built);
|
||||
return built;
|
||||
return REACHABLE_CACHE.get(engine, ignored -> build(engine));
|
||||
}
|
||||
|
||||
public static boolean isReachable(Engine engine, String structureKey) {
|
||||
@@ -73,24 +64,21 @@ public final class StructureReachability {
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
if (data != null) {
|
||||
REACHABLE_CACHE.remove(data);
|
||||
}
|
||||
REACHABLE_CACHE.invalidate(engine);
|
||||
}
|
||||
|
||||
private static Set<String> build(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null || world.realWorld() == null) {
|
||||
if (world == null || world.platformWorld() == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
for (String key : IrisPlatforms.get().structureHooks().reachableStructureKeys(new BukkitWorld(world.realWorld()))) {
|
||||
for (String key : IrisPlatforms.get().structureHooks().reachableStructureKeys(world.platformWorld())) {
|
||||
if (key != null && !key.isEmpty()) {
|
||||
reachable.add(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
return Collections.unmodifiableSet(reachable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,11 +92,11 @@ public final class StructureReachability {
|
||||
return missing;
|
||||
}
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world == null || world.realWorld() == null) {
|
||||
if (world == null || world.platformWorld() == null) {
|
||||
return missing;
|
||||
}
|
||||
Set<String> possible = new LinkedHashSet<>();
|
||||
for (String key : IrisPlatforms.get().structureHooks().possibleBiomeKeys(new BukkitWorld(world.realWorld()))) {
|
||||
for (String key : IrisPlatforms.get().structureHooks().possibleBiomeKeys(world.platformWorld())) {
|
||||
if (key != null) {
|
||||
possible.add(key.toLowerCase());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.engine.framework;
|
||||
|
||||
public final class StructureVerticalBounds {
|
||||
private StructureVerticalBounds() {
|
||||
}
|
||||
|
||||
public static int clampOffset(
|
||||
int structureMinY,
|
||||
int structureMaxY,
|
||||
int requestedOffset,
|
||||
int worldMinY,
|
||||
int worldMaxYExclusive
|
||||
) {
|
||||
if (structureMaxY < structureMinY) {
|
||||
throw new IllegalArgumentException("Structure maximum Y cannot be below its minimum Y");
|
||||
}
|
||||
if (worldMaxYExclusive <= worldMinY) {
|
||||
throw new IllegalArgumentException("World maximum Y must be above its minimum Y");
|
||||
}
|
||||
|
||||
long structureHeight = (long) structureMaxY - structureMinY + 1L;
|
||||
long worldHeight = (long) worldMaxYExclusive - worldMinY;
|
||||
if (structureHeight > worldHeight) {
|
||||
throw new IllegalArgumentException("Structure height " + structureHeight
|
||||
+ " exceeds writable world height " + worldHeight);
|
||||
}
|
||||
|
||||
long minimumOffset = (long) worldMinY - structureMinY;
|
||||
long maximumOffset = (long) worldMaxYExclusive - 1L - structureMaxY;
|
||||
long clamped = Math.max(minimumOffset, Math.min(maximumOffset, (long) requestedOffset));
|
||||
return Math.toIntExact(clamped);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
@@ -83,13 +82,13 @@ public interface EngineMantle extends MatterGenerator {
|
||||
@ChunkCoordinates
|
||||
default KList<IrisPosition> findMarkers(int x, int z, MatterMarker marker) {
|
||||
KList<IrisPosition> p = new KList<>();
|
||||
if (J.isFolia() && getEngine().getWorld().hasRealWorld() && J.isOwnedByCurrentRegion(getEngine().getWorld().realWorld(), x, z)) {
|
||||
if (getEngine().getPlatformHooks().shouldSkipMantleMarkerRead(getEngine(), x, z)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
getMantle().iterateChunk(x, z, MatterMarker.class, (xx, yy, zz, mm) -> {
|
||||
if (marker.equals(mm)) {
|
||||
p.add(new IrisPosition(xx + (x << 4), yy, zz + (z << 4)));
|
||||
p.add(new IrisPosition(xx + (x << 4), yy + getEngine().getMinHeight(), zz + (z << 4)));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
@@ -33,7 +34,6 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -46,12 +46,12 @@ import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import lombok.Data;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -82,7 +82,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
|
||||
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
|
||||
}
|
||||
final var map = multicore ? cachedChunks : new KMap<Long, MantleChunk<Matter>>(d * d, 1f, parallelism);
|
||||
Map<Long, MantleChunk<Matter>> map = multicore
|
||||
? cachedChunks
|
||||
: new KMap<Long, MantleChunk<Matter>>(d * d, 1f, parallelism);
|
||||
mantle.getChunks(
|
||||
x - radius,
|
||||
x + radius,
|
||||
@@ -177,6 +179,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (chunk == null) return;
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (t instanceof PlatformBlockState) {
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
}
|
||||
Class<?> sliceType = t instanceof PlatformBlockState ? PlatformBlockState.class : matter.getClass(t);
|
||||
matter.slice(sliceType).set(x & 15, y & 15, z & 15, t);
|
||||
}
|
||||
@@ -208,6 +213,29 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean carveDataIfAbsent(int x, int y, int z, MatterCavern value) {
|
||||
if (value == null || y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
if (chunk == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.slice(MatterCavern.class);
|
||||
if (cavernSlice.get(x & 15, y & 15, z & 15) != null) {
|
||||
return false;
|
||||
}
|
||||
cavernSlice.set(x & 15, y & 15, z & 15, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clearBlock(int x, int y, int z) {
|
||||
if (y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return;
|
||||
@@ -224,7 +252,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (matter == null) {
|
||||
return;
|
||||
}
|
||||
matter.<PlatformBlockState>slice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
if (matter.hasSlice(PlatformBlockState.class)) {
|
||||
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
}
|
||||
|
||||
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||
@@ -245,6 +276,48 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
.get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
public <T> T getDataIfPresent(int x, int y, int z, Class<T> type) {
|
||||
if (y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
int section = y >> 4;
|
||||
if (chunk == null || !chunk.exists(section)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return null;
|
||||
}
|
||||
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
public void clearData(int x, int y, int z, Class<?> type) {
|
||||
if (y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
int section = y >> 4;
|
||||
if (chunk == null || !chunk.exists(section)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter == null || !matter.hasSlice(type)) {
|
||||
return;
|
||||
}
|
||||
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
|
||||
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
|
||||
if (matter.hasSlice(Identifier.class)) {
|
||||
matter.<Identifier>getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
public MantleChunk<Matter> acquireChunk(int cx, int cz) {
|
||||
if (cx < this.x - radius || cx > this.x + radius
|
||||
@@ -256,7 +329,7 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
MantleChunk<Matter> chunk = cachedChunks.get(key);
|
||||
if (chunk == null) {
|
||||
chunk = mantle.getChunk(cx, cz).use();
|
||||
var old = cachedChunks.put(key, chunk);
|
||||
MantleChunk<Matter> old = cachedChunks.put(key, chunk);
|
||||
if (old != null) old.release();
|
||||
}
|
||||
return chunk;
|
||||
@@ -277,9 +350,11 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
if (s.isCustom() && s.nativeHandle() instanceof IrisCustomData data) {
|
||||
setData(x, y, z, BukkitBlockState.of(data.getBase()));
|
||||
setData(x, y, z, data.getCustom());
|
||||
String placementKey = s.deferredPlacementKey();
|
||||
PlatformBlockState baseState = s.placementBaseState();
|
||||
if (s.isCustom() && placementKey != null && baseState != null) {
|
||||
setData(x, y, z, baseState);
|
||||
setData(x, y, z, Identifier.fromString(placementKey));
|
||||
return;
|
||||
}
|
||||
setData(x, y, z, s);
|
||||
@@ -842,7 +917,7 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
var iterator = cachedChunks.values().iterator();
|
||||
Iterator<MantleChunk<Matter>> iterator = cachedChunks.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next().release();
|
||||
iterator.remove();
|
||||
|
||||
+118
-72
@@ -21,10 +21,9 @@ package art.arcane.iris.engine.mantle.components;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||
@@ -47,10 +46,18 @@ import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@ComponentFlag(ReservedFlag.JIGSAW)
|
||||
public class IrisStructureComponent extends IrisMantleComponent {
|
||||
private static final long MAX_BORE_VOLUME = 6_000_000L;
|
||||
private static final long MAX_OVERBORE_VOLUME = 48_000_000L;
|
||||
private static final double OVERBORE_MIN_BOUNDARY = 0.2;
|
||||
private static final double OVERBORE_MIN_BOUNDARY_SQUARED = OVERBORE_MIN_BOUNDARY * OVERBORE_MIN_BOUNDARY;
|
||||
private static final double OVERBORE_BOUNDARY_SPAN = 0.8;
|
||||
private static final double OVERBORE_MAX_UP_REACH = 1.8;
|
||||
private static final int DYNAMIC_STRUCTURE_Y_TOLERANCE = 32;
|
||||
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
public IrisStructureComponent(EngineMantle engineMantle) {
|
||||
@@ -65,8 +72,6 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
int zzz = 8 + (z << 4);
|
||||
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||
RNG rng = new RNG(Cache.key(x, z) + seed());
|
||||
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
@@ -76,17 +81,16 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
}
|
||||
placements.addAll(getDimension().getStructures());
|
||||
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
placeFromPlacement(writer, placement, x, z, rng);
|
||||
for (int placementOrdinal = 0; placementOrdinal < placements.size(); placementOrdinal++) {
|
||||
placeFromPlacement(writer, placements.get(placementOrdinal), x, z, placementOrdinal);
|
||||
}
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, RNG rng) {
|
||||
if (placement.getStructures().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed(), rng)) {
|
||||
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, int placementOrdinal) {
|
||||
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
|
||||
getEngineMantle().getEngine(), placement, cx, cz, placementOrdinal);
|
||||
if (resolved == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,52 +100,19 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
+ " underground=" + placement.isUnderground() + " band=" + placement.getMinHeight() + ".." + placement.getMaxHeight());
|
||||
}
|
||||
|
||||
int sx = (cx << 4) + rng.i(0, 15);
|
||||
int sz = (cz << 4) + rng.i(0, 15);
|
||||
int baseY;
|
||||
if (placement.isUnderground()) {
|
||||
int worldMinY = getEngineMantle().getEngine().getMinHeight() + 1;
|
||||
int worldMaxY = getEngineMantle().getEngine().getMinHeight() + getEngineMantle().getEngine().getHeight() - 1;
|
||||
int bandMin = Math.max(worldMinY, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
int bandMax = Math.min(worldMaxY, Math.max(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
if (bandMin > bandMax) {
|
||||
if (trace) {
|
||||
IrisLogging.info("[StructTrace] BAIL band-inverted chunk=" + cx + "," + cz + " bandMin=" + bandMin + " bandMax=" + bandMax
|
||||
+ " worldMinY=" + worldMinY + " worldMaxY=" + worldMaxY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
baseY = bandMin == bandMax ? bandMin : rng.i(bandMin, bandMax);
|
||||
} else {
|
||||
int surfaceY = getEngineMantle().getEngine().getHeight(sx, sz, true) + getEngineMantle().getEngine().getMinHeight();
|
||||
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
|
||||
return;
|
||||
}
|
||||
baseY = surfaceY;
|
||||
}
|
||||
|
||||
String key = placement.getStructures().get(rng.i(0, placement.getStructures().size() - 1));
|
||||
IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData());
|
||||
if (structure == null) {
|
||||
if (trace) {
|
||||
IrisLogging.info("[StructTrace] BAIL structure-load-null chunk=" + cx + "," + cz + " key=" + key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, baseY, sz);
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
if (trace) {
|
||||
IrisLogging.info("[StructTrace] BAIL no-pieces chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY
|
||||
+ " pieces=" + (pieces == null ? "null" : "empty"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
String key = resolved.structureKey();
|
||||
IrisStructure structure = resolved.structure();
|
||||
KList<PlacedStructurePiece> pieces = resolved.pieces();
|
||||
RNG rng = resolved.rng();
|
||||
int baseY = resolved.baseY();
|
||||
if (trace) {
|
||||
IrisLogging.info("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size());
|
||||
}
|
||||
|
||||
if (!placement.isUnderground()) {
|
||||
clearIntersectingObjectTrees(writer, resolved);
|
||||
}
|
||||
|
||||
if (placement.isOverbore()) {
|
||||
overboreStructure(writer, pieces, placement.getOverboreRadius(), placement.getOverboreHeight(), placement.getOverboreFloor());
|
||||
} else if (placement.isBore()) {
|
||||
@@ -162,13 +133,8 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
} else if (pieces.size() == 1) {
|
||||
placeObject(writer, structure, pieces.getFirst(), mode, -1, rng);
|
||||
} else {
|
||||
int lowest = Integer.MAX_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
lowest = Math.min(lowest, p.getMinY());
|
||||
}
|
||||
int shift = baseY - lowest;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY() + shift, rng);
|
||||
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,14 +187,13 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
|
||||
double freq = 0.07;
|
||||
double rollFreq = 0.03;
|
||||
double clearMax = 1.45;
|
||||
double reachSide = margin;
|
||||
double reachUp = head < 1 ? 1.0 : head;
|
||||
double reachDown = floorCut < 1 ? 1.0 : floorCut;
|
||||
double upReachMin = 0.4;
|
||||
double upReachSpan = 1.4;
|
||||
int sideExt = (int) Math.ceil(reachSide * clearMax);
|
||||
int upExt = (int) Math.ceil(reachUp * (upReachMin + upReachSpan) * clearMax);
|
||||
int sideExt = overboreSideExtension(margin);
|
||||
int upExt = overboreUpExtension(reachUp);
|
||||
|
||||
long work = 0L;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
@@ -270,6 +235,9 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
double dz = bz < pMinZ ? pMinZ - bz : bz > pMaxZ ? bz - pMaxZ : 0;
|
||||
double nz = dz / reachSide;
|
||||
double nxz = nx * nx + nz * nz;
|
||||
if (nxz > 1.0) {
|
||||
continue;
|
||||
}
|
||||
double w = roll.fitDouble(0.0, 1.0, bx * rollFreq, bz * rollFreq) * 0.7
|
||||
+ roll.fitDouble(0.0, 1.0, bx * rollFreq * 3.0, bz * rollFreq * 3.0) * 0.3;
|
||||
double contrast = (w - 0.5) * 2.6 + 0.5;
|
||||
@@ -291,25 +259,103 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
} else {
|
||||
ny = 0.0;
|
||||
}
|
||||
double nd = Math.sqrt(nxz + ny * ny);
|
||||
if (nd > clearMax) {
|
||||
double distanceSquared = nxz + ny * ny;
|
||||
if (distanceSquared > 1.0) {
|
||||
continue;
|
||||
}
|
||||
boolean carve = nd <= 0.45;
|
||||
if (!carve) {
|
||||
if (distanceSquared > OVERBORE_MIN_BOUNDARY_SQUARED) {
|
||||
double n = blob.fitDouble(0.0, 1.0, bx * freq, by * freq, bz * freq);
|
||||
carve = nd <= 0.5 + 0.5 * n;
|
||||
}
|
||||
writer.clearBlock(bx, by - mantleOffset, bz);
|
||||
if (carve) {
|
||||
writer.setDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
if (!shouldCarveOverboreCell(distanceSquared, n)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
writer.carveDataIfAbsent(bx, by - mantleOffset, bz, CARVE_CAVERN);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int overboreSideExtension(int radius) {
|
||||
return Math.max(1, radius);
|
||||
}
|
||||
|
||||
static int overboreUpExtension(double reachUp) {
|
||||
return (int) Math.ceil(Math.max(1.0, reachUp) * OVERBORE_MAX_UP_REACH);
|
||||
}
|
||||
|
||||
static double overboreBoundaryLimit(double noise) {
|
||||
double clampedNoise = Math.max(0.0, Math.min(1.0, noise));
|
||||
return OVERBORE_MIN_BOUNDARY + OVERBORE_BOUNDARY_SPAN * clampedNoise;
|
||||
}
|
||||
|
||||
static boolean shouldCarveOverboreCell(double distanceSquared, double noise) {
|
||||
if (distanceSquared <= 0.0) {
|
||||
return true;
|
||||
}
|
||||
if (distanceSquared > 1.0) {
|
||||
return false;
|
||||
}
|
||||
double limit = overboreBoundaryLimit(noise);
|
||||
return distanceSquared <= limit * limit;
|
||||
}
|
||||
|
||||
private void clearIntersectingObjectTrees(MantleWriter writer, IrisStructureLocator.ResolvedPlacement resolved) {
|
||||
int mantleOffset = getEngineMantle().getEngine().getMinHeight();
|
||||
int worldHeight = getEngineMantle().getEngine().getHeight();
|
||||
int verticalTolerance = resolved.exactY() ? 0 : DYNAMIC_STRUCTURE_Y_TOLERANCE;
|
||||
for (PlacedStructurePiece piece : resolved.pieces()) {
|
||||
int minY = Math.max(0, piece.getMinY() - mantleOffset - verticalTolerance);
|
||||
int maxY = Math.min(worldHeight - 1, piece.getMaxY() - mantleOffset + verticalTolerance);
|
||||
if (minY > maxY) {
|
||||
continue;
|
||||
}
|
||||
for (int x = piece.getMinX(); x <= piece.getMaxX(); x++) {
|
||||
for (int z = piece.getMinZ(); z <= piece.getMaxZ(); z++) {
|
||||
clearIntersectingObjectTreeColumn(writer, x, z, minY, maxY, worldHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearIntersectingObjectTreeColumn(MantleWriter writer, int x, int z,
|
||||
int minY, int maxY, int worldHeight) {
|
||||
Set<String> intersectingMarkers = null;
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
PlatformBlockState state = writer.getDataIfPresent(x, y, z, PlatformBlockState.class);
|
||||
if (state == null || !state.isTreeBlock()) {
|
||||
continue;
|
||||
}
|
||||
String marker = writer.getDataIfPresent(x, y, z, String.class);
|
||||
if (isOrdinaryObjectMarker(marker)) {
|
||||
if (intersectingMarkers == null) {
|
||||
intersectingMarkers = new HashSet<>();
|
||||
}
|
||||
intersectingMarkers.add(marker);
|
||||
}
|
||||
}
|
||||
if (intersectingMarkers == null) {
|
||||
return;
|
||||
}
|
||||
for (int y = 0; y < worldHeight; y++) {
|
||||
String marker = writer.getDataIfPresent(x, y, z, String.class);
|
||||
if (!intersectingMarkers.contains(marker)) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState state = writer.getDataIfPresent(x, y, z, PlatformBlockState.class);
|
||||
if (state == null || !state.isTreeBlock()) {
|
||||
continue;
|
||||
}
|
||||
writer.clearBlock(x, y, z);
|
||||
writer.clearData(x, y, z, String.class);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isOrdinaryObjectMarker(String marker) {
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
return decoded != null && !decoded.structureAware();
|
||||
}
|
||||
|
||||
private int[] computePieceBounds(KList<PlacedStructurePiece> pieces) {
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
return null;
|
||||
@@ -383,7 +429,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
for (IrisRegion region : dimension.getAllRegions(this::getData)) {
|
||||
maxBlocks = Math.max(maxBlocks, maxBlocksFrom(region.getStructures()));
|
||||
}
|
||||
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(this::getData)) {
|
||||
maxBlocks = Math.max(maxBlocks, maxBlocksFrom(biome.getStructures()));
|
||||
}
|
||||
maxBlocks = Math.max(maxBlocks, maxBlocksFrom(dimension.getStructures()));
|
||||
|
||||
+1
-1
@@ -541,7 +541,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
Set<String> objectKeys = new HashSet<>();
|
||||
try {
|
||||
IrisData data = getData();
|
||||
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
|
||||
for (IrisBiome biome : getDimension().getReachableBiomes(this::getData)) {
|
||||
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
continue;
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class MantleFluidBodyComponent extends IrisMantleComponent {
|
||||
max = Math.max(max, i.getFluidBodies().getMaxRange(getData()));
|
||||
}
|
||||
|
||||
for (IrisBiome i : getDimension().getAllBiomes(this::getData)) {
|
||||
for (IrisBiome i : getDimension().getReachableBiomes(this::getData)) {
|
||||
max = Math.max(max, i.getFluidBodies().getMaxRange(getData()));
|
||||
}
|
||||
|
||||
|
||||
+15
-275
@@ -42,7 +42,6 @@ import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.IrisObjectScale;
|
||||
import art.arcane.iris.engine.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.engine.object.IrisObjectVacuum;
|
||||
import art.arcane.iris.engine.object.IrisProceduralObjects;
|
||||
import art.arcane.iris.engine.object.IrisProceduralPlacement;
|
||||
@@ -53,9 +52,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.project.context.ChunkedDoubleDataCache;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
@@ -64,7 +61,6 @@ import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.noise.NoiseType;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -83,7 +79,6 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L;
|
||||
private static final int BEDROCK_CLEARANCE = 6;
|
||||
private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128;
|
||||
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
|
||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
||||
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
|
||||
@@ -156,7 +151,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
|
||||
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
||||
SurfaceHeightLookup surfaceHeightLookup = new SurfaceHeightLookup(context);
|
||||
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
|
||||
int carvedBlocks = 0;
|
||||
int minY = 1;
|
||||
@@ -186,7 +180,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " regionSurfacePlacers=" + region.getSurfaceObjects().size()
|
||||
+ " regionCavePlacers=" + region.getCarvingObjects().size());
|
||||
}
|
||||
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen, surfaceHeightLookup);
|
||||
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen);
|
||||
placeProceduralObjects(writer, rng, x, z, surfaceBiome, caveBiome, region);
|
||||
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
|
||||
IrisDimension dimension = getDimension();
|
||||
@@ -252,7 +246,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex, boolean traceRegen, SurfaceHeightLookup surfaceHeightLookup) {
|
||||
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex, boolean traceRegen) {
|
||||
int biomeSurfaceChecked = 0;
|
||||
int biomeSurfaceTriggered = 0;
|
||||
int biomeCaveChecked = 0;
|
||||
@@ -268,9 +262,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int errors = 0;
|
||||
IrisCaveProfile biomeCaveProfile = resolveCaveProfile(caveBiome.getCaveProfile(), region.getCaveProfile());
|
||||
IrisCaveProfile regionCaveProfile = resolveCaveProfile(region.getCaveProfile(), caveBiome.getCaveProfile());
|
||||
int biomeSurfaceExclusionDepth = resolveSurfaceObjectExclusionDepth(biomeCaveProfile);
|
||||
int regionSurfaceExclusionDepth = resolveSurfaceObjectExclusionDepth(regionCaveProfile);
|
||||
SurfaceExposureCache surfaceExposureCache = new SurfaceExposureCache();
|
||||
CaveAnchorCache caveAnchorCache = new CaveAnchorCache();
|
||||
|
||||
for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) {
|
||||
@@ -287,7 +278,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
if (chance) {
|
||||
biomeSurfaceTriggered++;
|
||||
try {
|
||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, complex, traceRegen, x, z, "biome-surface", surfaceHeightLookup, surfaceExposureCache, caveAnchorCache);
|
||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, complex, traceRegen, x, z, "biome-surface");
|
||||
attempts += result.attempts();
|
||||
placed += result.placed();
|
||||
rejected += result.rejected();
|
||||
@@ -352,7 +343,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
if (chance) {
|
||||
regionSurfaceTriggered++;
|
||||
try {
|
||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, complex, traceRegen, x, z, "region-surface", surfaceHeightLookup, surfaceExposureCache, caveAnchorCache);
|
||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, complex, traceRegen, x, z, "region-surface");
|
||||
attempts += result.attempts();
|
||||
placed += result.placed();
|
||||
rejected += result.rejected();
|
||||
@@ -538,15 +529,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int x,
|
||||
int z,
|
||||
IrisObjectPlacement objectPlacement,
|
||||
int surfaceObjectExclusionBaseDepth,
|
||||
IrisComplex complex,
|
||||
boolean traceRegen,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
String scope,
|
||||
SurfaceHeightLookup surfaceHeightLookup,
|
||||
SurfaceExposureCache surfaceExposureCache,
|
||||
CaveAnchorCache caveAnchorCache
|
||||
String scope
|
||||
) {
|
||||
int attempts = 0;
|
||||
int placed = 0;
|
||||
@@ -572,12 +559,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
int xx = rng.i(x, x + 15);
|
||||
int zz = rng.i(z, z + 15);
|
||||
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v, objectPlacement);
|
||||
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v, objectPlacement);
|
||||
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
|
||||
boolean overCave = effectivePlacement.getMode() != ObjectPlaceMode.FLOATING
|
||||
&& surfaceObjectExclusionDepth > 0
|
||||
&& hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius, surfaceExposureCache);
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
|
||||
if (golden) {
|
||||
@@ -587,57 +569,18 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " densityIndex=" + i
|
||||
+ " xx=" + xx
|
||||
+ " zz=" + zz
|
||||
+ " overCave=" + overCave
|
||||
+ " exclusionDepth=" + surfaceObjectExclusionDepth
|
||||
+ " exclusionRadius=" + surfaceObjectExclusionRadius
|
||||
+ " mode=" + effectivePlacement.getMode());
|
||||
}
|
||||
try {
|
||||
int result = -1;
|
||||
String fallbackPath = "surface";
|
||||
|
||||
if (overCave) {
|
||||
int caveFloorY = findNearestCaveFloor(writer, xx, zz, caveAnchorCache);
|
||||
if (caveFloorY > 0) {
|
||||
IrisObjectPlacement floorPlacement = effectivePlacement.toPlacement(v.getLoadKey());
|
||||
floorPlacement.setMode(ObjectPlaceMode.FAST_MIN_HEIGHT);
|
||||
result = v.place(xx, caveFloorY, zz, placePlacer, floorPlacement, rng, (b, data) -> {
|
||||
String marker = placementMarker(v, id, "cave-floor");
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
}
|
||||
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
|
||||
}
|
||||
}, null, getData());
|
||||
fallbackPath = "cave-floor";
|
||||
int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
|
||||
String marker = placementMarker(v, id, "surface");
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
}
|
||||
|
||||
if (result < 0) {
|
||||
IrisObjectPlacement stiltPlacement = effectivePlacement.toPlacement(v.getLoadKey());
|
||||
stiltPlacement.setMode(ObjectPlaceMode.FAST_MIN_STILT);
|
||||
result = v.place(xx, -1, zz, placePlacer, stiltPlacement, rng, (b, data) -> {
|
||||
String marker = placementMarker(v, id, "stilt");
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
}
|
||||
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
|
||||
}
|
||||
}, null, getData());
|
||||
fallbackPath = "stilt";
|
||||
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
|
||||
}
|
||||
} else {
|
||||
result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
|
||||
String marker = placementMarker(v, id, "surface");
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
}
|
||||
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
|
||||
}
|
||||
}, null, getData());
|
||||
}
|
||||
}, null, getData());
|
||||
|
||||
if (result >= 0) {
|
||||
placed++;
|
||||
@@ -650,7 +593,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " scope=" + scope
|
||||
+ " object=" + v.getLoadKey()
|
||||
+ " resultY=" + result
|
||||
+ " fallback=" + fallbackPath);
|
||||
+ " fallback=surface");
|
||||
}
|
||||
|
||||
if (traceRegen) {
|
||||
@@ -660,8 +603,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " resultY=" + result
|
||||
+ " px=" + xx
|
||||
+ " pz=" + zz
|
||||
+ " overCave=" + overCave
|
||||
+ " fallback=" + fallbackPath
|
||||
+ " fallback=surface"
|
||||
+ " densityIndex=" + i
|
||||
+ " density=" + density);
|
||||
}
|
||||
@@ -1466,40 +1408,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return Math.max(0, caveProfile.getObjectMinDepthBelowSurface());
|
||||
}
|
||||
|
||||
private int resolveSurfaceObjectExclusionDepth(IrisCaveProfile caveProfile) {
|
||||
if (caveProfile == null) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
return Math.max(0, caveProfile.getSurfaceObjectExclusionDepth());
|
||||
}
|
||||
|
||||
private int resolveSurfaceObjectExclusionDepth(int baseDepth, IrisObject object, IrisObjectPlacement placement) {
|
||||
if (object == null) {
|
||||
return baseDepth;
|
||||
}
|
||||
|
||||
int horizontalReach = resolveSurfaceObjectExclusionRadius(object, placement) + 2;
|
||||
int verticalReach = Math.max(4, Math.min(16, Math.floorDiv(Math.max(1, object.getH()), 2)));
|
||||
return Math.max(baseDepth, Math.max(horizontalReach, verticalReach));
|
||||
}
|
||||
|
||||
static int computeSurfaceExclusionRadius(int maxDimension, int absTranslateX, int absTranslateZ) {
|
||||
return Math.max(1, Math.floorDiv(Math.max(1, maxDimension), 2) + absTranslateX + absTranslateZ + 1);
|
||||
}
|
||||
|
||||
private int resolveSurfaceObjectExclusionRadius(IrisObject object, IrisObjectPlacement placement) {
|
||||
if (object == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int maxDimension = Math.max(object.getW(), object.getD());
|
||||
IrisObjectTranslate t = placement != null ? placement.getTranslate() : null;
|
||||
int absX = t != null ? Math.abs(t.getX()) : 0;
|
||||
int absZ = t != null ? Math.abs(t.getZ()) : 0;
|
||||
return computeSurfaceExclusionRadius(maxDimension, absX, absZ);
|
||||
}
|
||||
|
||||
private int resolveAnchorSearchAttempts(IrisCaveProfile caveProfile) {
|
||||
if (caveProfile == null) {
|
||||
return 6;
|
||||
@@ -1508,66 +1416,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return Math.max(1, caveProfile.getAnchorSearchAttempts());
|
||||
}
|
||||
|
||||
private boolean hasSurfaceCarveExposure(MantleWriter writer, SurfaceHeightLookup surfaceHeightLookup, int x, int z, int depth, int radius, SurfaceExposureCache surfaceExposureCache) {
|
||||
int horizontalRadius = Math.max(0, radius);
|
||||
int maxY = getEngineMantle().getEngine().getHeight() - 1;
|
||||
if (surfaceExposureCache.get(writer, surfaceHeightLookup, x, z, depth, maxY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int sampleCount = surfaceExposureSampleCount(horizontalRadius);
|
||||
for (int sx = 0; sx < sampleCount; sx++) {
|
||||
int dx = surfaceExposureSampleOffset(sx, sampleCount, horizontalRadius);
|
||||
for (int sz = 0; sz < sampleCount; sz++) {
|
||||
int dz = surfaceExposureSampleOffset(sz, sampleCount, horizontalRadius);
|
||||
if (dx == 0 && dz == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int columnX = x + dx;
|
||||
int columnZ = z + dz;
|
||||
if (surfaceExposureCache.get(writer, surfaceHeightLookup, columnX, columnZ, depth, maxY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int surfaceExposureSampleCount(int radius) {
|
||||
if (radius <= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (radius <= 3) {
|
||||
return (radius << 1) + 1;
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
private int surfaceExposureSampleOffset(int sample, int sampleCount, int radius) {
|
||||
if (sampleCount <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -radius + Math.floorDiv((sample * radius * 2) + ((sampleCount - 1) >> 1), sampleCount - 1);
|
||||
}
|
||||
|
||||
private boolean hasSurfaceCarveExposureColumn(MantleWriter writer, SurfaceHeightLookup surfaceHeightLookup, int x, int z, int depth, int maxY) {
|
||||
int surfaceY = surfaceHeightLookup.getRoundedHeight(x, z);
|
||||
int fromY = Math.max(1, surfaceY - Math.max(0, depth));
|
||||
int toY = Math.min(maxY, surfaceY + 1);
|
||||
for (int y = fromY; y <= toY; y++) {
|
||||
if (writer.isCarved(x, y, z)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isRegenTraceThread() {
|
||||
return Thread.currentThread().getName().startsWith("Iris-Regen-")
|
||||
&& IrisSettings.get().getGeneral().isDebug();
|
||||
@@ -1656,7 +1504,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
updateProceduralRadiusBounds(region.getProceduralObjects(), xg, zg);
|
||||
}
|
||||
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(this::getData)) {
|
||||
updateProceduralRadiusBounds(biome.getProceduralObjects(), xg, zg);
|
||||
for (IrisObjectPlacement j : biome.getObjects()) {
|
||||
if (j.getScale().canScaleBeyond()) {
|
||||
@@ -1781,34 +1629,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
});
|
||||
}
|
||||
|
||||
private final class SurfaceExposureCache {
|
||||
private final Long2ObjectOpenHashMap<Long2ByteOpenHashMap> depthCaches;
|
||||
|
||||
private SurfaceExposureCache() {
|
||||
this.depthCaches = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
private boolean get(MantleWriter writer, SurfaceHeightLookup surfaceHeightLookup, int x, int z, int depth, int maxY) {
|
||||
long depthKey = depth;
|
||||
Long2ByteOpenHashMap columnCache = depthCaches.get(depthKey);
|
||||
if (columnCache == null) {
|
||||
columnCache = new Long2ByteOpenHashMap();
|
||||
columnCache.defaultReturnValue((byte) -1);
|
||||
depthCaches.put(depthKey, columnCache);
|
||||
}
|
||||
|
||||
long columnKey = Cache.key(x, z);
|
||||
byte cached = columnCache.get(columnKey);
|
||||
if (cached != -1) {
|
||||
return cached == 1;
|
||||
}
|
||||
|
||||
boolean exposed = hasSurfaceCarveExposureColumn(writer, surfaceHeightLookup, x, z, depth, maxY);
|
||||
columnCache.put(columnKey, (byte) (exposed ? 1 : 0));
|
||||
return exposed;
|
||||
}
|
||||
}
|
||||
|
||||
private final class CaveAnchorCache {
|
||||
private final Long2ObjectOpenHashMap<Long2ObjectOpenHashMap<KList<Integer>>> settingCaches;
|
||||
private final Long2ObjectOpenHashMap<byte[]> carvedColumns;
|
||||
@@ -1869,84 +1689,4 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SurfaceHeightLookup {
|
||||
private final ChunkContext context;
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final Long2ObjectOpenHashMap<ForeignChunkHeights> foreignChunkHeights;
|
||||
|
||||
private SurfaceHeightLookup(ChunkContext context) {
|
||||
this.context = context;
|
||||
this.heightStream = context.getComplex().getHeightStream();
|
||||
this.foreignChunkHeights = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
private int getRoundedHeight(int worldX, int worldZ) {
|
||||
int chunkBlockX = worldX & ~15;
|
||||
int chunkBlockZ = worldZ & ~15;
|
||||
if (chunkBlockX == context.getX() && chunkBlockZ == context.getZ()) {
|
||||
return context.getRoundedHeight(worldX & 15, worldZ & 15);
|
||||
}
|
||||
|
||||
long chunkKey = Cache.key(chunkBlockX, chunkBlockZ);
|
||||
ForeignChunkHeights chunkHeights = foreignChunkHeights.get(chunkKey);
|
||||
if (chunkHeights == null) {
|
||||
chunkHeights = new ForeignChunkHeights(heightStream, chunkBlockX, chunkBlockZ);
|
||||
foreignChunkHeights.put(chunkKey, chunkHeights);
|
||||
}
|
||||
return chunkHeights.getRoundedHeight(worldX, worldZ);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ForeignChunkHeights {
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final int chunkBlockX;
|
||||
private final int chunkBlockZ;
|
||||
private final Long2IntOpenHashMap sparseColumnHeights;
|
||||
private int uniqueColumnCount;
|
||||
private int[] roundedHeights;
|
||||
|
||||
private ForeignChunkHeights(ProceduralStream<Double> heightStream, int chunkBlockX, int chunkBlockZ) {
|
||||
this.heightStream = heightStream;
|
||||
this.chunkBlockX = chunkBlockX;
|
||||
this.chunkBlockZ = chunkBlockZ;
|
||||
this.sparseColumnHeights = new Long2IntOpenHashMap();
|
||||
this.sparseColumnHeights.defaultReturnValue(Integer.MIN_VALUE);
|
||||
this.uniqueColumnCount = 0;
|
||||
}
|
||||
|
||||
private int getRoundedHeight(int worldX, int worldZ) {
|
||||
int[] localRoundedHeights = roundedHeights;
|
||||
if (localRoundedHeights != null) {
|
||||
int localX = worldX - chunkBlockX;
|
||||
int localZ = worldZ - chunkBlockZ;
|
||||
return localRoundedHeights[(localZ << 4) + localX];
|
||||
}
|
||||
|
||||
long columnKey = Cache.key(worldX, worldZ);
|
||||
int cachedHeight = sparseColumnHeights.get(columnKey);
|
||||
if (cachedHeight != Integer.MIN_VALUE) {
|
||||
return cachedHeight;
|
||||
}
|
||||
|
||||
int roundedHeight = (int) Math.round(heightStream.getDouble(worldX, worldZ));
|
||||
sparseColumnHeights.put(columnKey, roundedHeight);
|
||||
uniqueColumnCount++;
|
||||
if (uniqueColumnCount >= SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD) {
|
||||
promoteToChunkCache();
|
||||
}
|
||||
|
||||
return roundedHeight;
|
||||
}
|
||||
|
||||
private void promoteToChunkCache() {
|
||||
if (roundedHeights != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int[] filledHeights = new int[256];
|
||||
new ChunkedDoubleDataCache(heightStream, chunkBlockX, chunkBlockZ, true).fillRounded(filledHeights);
|
||||
roundedHeights = filledHeights;
|
||||
sparseColumnHeights.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
|
||||
package art.arcane.iris.engine.mode;
|
||||
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.actuator.IrisBiomeActuator;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.EngineStage;
|
||||
import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
import art.arcane.iris.engine.modifier.IrisCarveModifier;
|
||||
@@ -33,27 +33,28 @@ import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
|
||||
import art.arcane.iris.engine.modifier.IrisPerfectionModifier;
|
||||
import art.arcane.iris.engine.modifier.IrisPostModifier;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
private static final AtomicLong lastMaintenanceBypassLog = new AtomicLong(0L);
|
||||
private final EnginePlatformHooks platformHooks;
|
||||
|
||||
public ModeOverworld(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
var decorant = new IrisDecorantActuator(getEngine());
|
||||
var cave = new IrisCarveModifier(getEngine());
|
||||
var post = new IrisPostModifier(getEngine());
|
||||
var deposit = new IrisDepositModifier(getEngine());
|
||||
var perfection = new IrisPerfectionModifier(getEngine());
|
||||
var custom = new IrisCustomModifier(getEngine());
|
||||
var floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
|
||||
platformHooks = engine.getPlatformHooks();
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
IrisDecorantActuator decorant = new IrisDecorantActuator(getEngine());
|
||||
IrisCarveModifier cave = new IrisCarveModifier(getEngine());
|
||||
IrisPostModifier post = new IrisPostModifier(getEngine());
|
||||
IrisDepositModifier deposit = new IrisDepositModifier(getEngine());
|
||||
IrisPerfectionModifier perfection = new IrisPerfectionModifier(getEngine());
|
||||
IrisCustomModifier custom = new IrisCustomModifier(getEngine());
|
||||
IrisFloatingChildBiomeModifier floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
|
||||
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
|
||||
EngineStage sGenMatter = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
generateMatter(x >> 4, z >> 4, m, c);
|
||||
@@ -61,25 +62,25 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
|
||||
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
|
||||
EngineStage sCave = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
cave.modify(x >> 4, z >> 4, k, m, c);
|
||||
};
|
||||
EngineStage sDeposit = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
deposit.modify(x, z, k, m, c);
|
||||
};
|
||||
EngineStage sPost = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
post.modify(x, z, k, m, c);
|
||||
};
|
||||
EngineStage sInsertMatter = (x, z, K, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m);
|
||||
@@ -88,7 +89,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
|
||||
EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c);
|
||||
EngineStage sCustom = (x, z, k, p, m, c) -> {
|
||||
if (shouldBypassMantleStages(getEngine())) {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
custom.modify(x, z, k, m, c);
|
||||
@@ -114,13 +115,8 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
|
||||
private static boolean shouldBypassMantleStages(Engine engine) {
|
||||
if (!J.isFolia()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var world = engine.getWorld().realWorld();
|
||||
boolean active = world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
|
||||
private boolean shouldBypassMantleStages() {
|
||||
boolean active = platformHooks.shouldBypassMantleStages(getEngine());
|
||||
if (active) {
|
||||
long now = System.currentTimeMillis();
|
||||
long last = lastMaintenanceBypassLog.get();
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
public class IrisCustomModifier extends EngineAssignedModifier<PlatformBlockState> {
|
||||
public IrisCustomModifier(Engine engine) {
|
||||
super(engine, "Custom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModify(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
|
||||
var mc = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4);
|
||||
if (!mc.isFlagged(MantleFlag.CUSTOM_ACTIVE)) return;
|
||||
MantleChunk<Matter> mc = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4);
|
||||
if (!mc.isFlagged(MantleFlag.CUSTOM_ACTIVE)) {
|
||||
return;
|
||||
}
|
||||
mc.use();
|
||||
|
||||
BurstExecutor burst = MultiBurst.burst.burst(output.getHeight());
|
||||
@@ -28,12 +33,19 @@ public class IrisCustomModifier extends EngineAssignedModifier<PlatformBlockStat
|
||||
for (int rX = 0; rX < output.getWidth(); rX++) {
|
||||
for (int rZ = 0; rZ < output.getDepth(); rZ++) {
|
||||
PlatformBlockState b = output.get(rX, finalY, rZ);
|
||||
if (b == null || !(b.nativeHandle() instanceof IrisCustomData d)) continue;
|
||||
if (b == null || !b.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
String placementKey = b.deferredPlacementKey();
|
||||
PlatformBlockState baseState = b.placementBaseState();
|
||||
if (placementKey == null || baseState == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mc.getOrCreate(finalY >> 4)
|
||||
.slice(Identifier.class)
|
||||
.set(rX, finalY & 15, rZ, d.getCustom());
|
||||
output.set(rX, finalY, rZ, BukkitBlockState.of(d.getBase()));
|
||||
.set(rX, finalY & 15, rZ, Identifier.fromString(placementKey));
|
||||
output.set(rX, finalY, rZ, baseState);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -41,4 +53,4 @@ public class IrisCustomModifier extends EngineAssignedModifier<PlatformBlockStat
|
||||
burst.complete();
|
||||
mc.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +122,6 @@ public class IrisCaveProfile {
|
||||
@Desc("Minimum depth below terrain surface required for cave-only object anchor placement.")
|
||||
private int objectMinDepthBelowSurface = 6;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("Skip surface-object placement when carved cells exist this many blocks below terrain surface.")
|
||||
private int surfaceObjectExclusionDepth = 5;
|
||||
|
||||
@ArrayType(type = IrisCaveFieldModule.class, min = 1)
|
||||
@Desc("Additional layered cave-density modules.")
|
||||
private KList<IrisCaveFieldModule> modules = new KList<>();
|
||||
|
||||
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
@@ -50,8 +51,6 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.bukkit.World.Environment;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -422,19 +421,6 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return rad.aquire(() -> Math.toRadians(dimensionAngleDeg));
|
||||
}
|
||||
|
||||
public Environment getEnvironment() {
|
||||
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() {
|
||||
return !focusRegion.equals("");
|
||||
}
|
||||
@@ -454,8 +440,16 @@ public class IrisDimension extends IrisRegistrant {
|
||||
public KList<IrisRegion> getAllRegions(DataProvider g) {
|
||||
KList<IrisRegion> r = new KList<>();
|
||||
|
||||
if (g == null) {
|
||||
return r;
|
||||
}
|
||||
IrisData data = g.getData();
|
||||
if (data == null || data.getRegionLoader() == null) {
|
||||
return r;
|
||||
}
|
||||
|
||||
for (String i : getRegions()) {
|
||||
r.add(g.getData().getRegionLoader().load(i));
|
||||
r.add(data.getRegionLoader().load(i));
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -472,7 +466,31 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getAllBiomes(DataProvider g) {
|
||||
return g.getData().getBiomeLoader().loadAll(g.getData().getBiomeLoader().getPossibleKeys());
|
||||
if (g == null) {
|
||||
return new KList<>();
|
||||
}
|
||||
IrisData data = g.getData();
|
||||
if (data == null || data.getBiomeLoader() == null) {
|
||||
return new KList<>();
|
||||
}
|
||||
return data.getBiomeLoader().loadAll(data.getBiomeLoader().getPossibleKeys());
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getReachableBiomes(DataProvider g) {
|
||||
KMap<String, IrisBiome> biomes = new KMap<>();
|
||||
|
||||
for (IrisRegion region : getAllRegions(g)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiome biome : region.getAllBiomes(g)) {
|
||||
if (biome != null) {
|
||||
biomes.put(biome.getLoadKey(), biome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return biomes.v();
|
||||
}
|
||||
|
||||
public KList<IrisBiome> getAllAnyBiomes() {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.engine.object;
|
||||
|
||||
public final class IrisDimensionContractException extends IllegalStateException {
|
||||
public IrisDimensionContractException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.engine.object;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisDimensionRuntimeContract(
|
||||
String typeKey,
|
||||
int minHeight,
|
||||
int height,
|
||||
int logicalHeight
|
||||
) {
|
||||
public IrisDimensionRuntimeContract {
|
||||
Objects.requireNonNull(typeKey, "typeKey");
|
||||
if (typeKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Dimension type key cannot be blank");
|
||||
}
|
||||
if (height <= 0) {
|
||||
throw new IllegalArgumentException("Dimension height must be positive");
|
||||
}
|
||||
if (logicalHeight < 0 || logicalHeight > height) {
|
||||
throw new IllegalArgumentException("Logical height must be inside the dimension height");
|
||||
}
|
||||
}
|
||||
|
||||
public static IrisDimensionRuntimeContract expected(IrisDimension dimension, String namespace) {
|
||||
Objects.requireNonNull(dimension, "dimension");
|
||||
Objects.requireNonNull(namespace, "namespace");
|
||||
return new IrisDimensionRuntimeContract(
|
||||
namespace + ":" + dimension.getDimensionTypeKey(),
|
||||
dimension.getMinHeight(),
|
||||
dimension.getMaxHeight() - dimension.getMinHeight(),
|
||||
dimension.getLogicalHeight());
|
||||
}
|
||||
|
||||
public static void requireHotloadCompatible(
|
||||
String runtimeName,
|
||||
IrisDimension active,
|
||||
IrisDimension replacement,
|
||||
String namespace
|
||||
) {
|
||||
IrisDimensionRuntimeContract actual = expected(active, namespace);
|
||||
IrisDimensionRuntimeContract proposed = expected(replacement, namespace);
|
||||
proposed.requireExact(runtimeName, actual);
|
||||
}
|
||||
|
||||
public int maxHeight() {
|
||||
return Math.addExact(minHeight, height);
|
||||
}
|
||||
|
||||
public void requireExact(String runtimeName, IrisDimensionRuntimeContract actual) {
|
||||
Objects.requireNonNull(actual, "actual");
|
||||
if (equals(actual)) {
|
||||
return;
|
||||
}
|
||||
throw mismatch(runtimeName, actual.typeKey(), actual.minHeight(), actual.height(), actual.logicalHeight());
|
||||
}
|
||||
|
||||
public void requireHeight(String runtimeName, int actualMinHeight, int actualHeight) {
|
||||
if (minHeight == actualMinHeight && height == actualHeight) {
|
||||
return;
|
||||
}
|
||||
throw mismatch(runtimeName, "unknown", actualMinHeight, actualHeight, -1);
|
||||
}
|
||||
|
||||
private IrisDimensionContractException mismatch(
|
||||
String runtimeName,
|
||||
String actualTypeKey,
|
||||
int actualMinHeight,
|
||||
int actualHeight,
|
||||
int actualLogicalHeight
|
||||
) {
|
||||
String expectedRange = minHeight + ".." + maxHeight();
|
||||
String actualRange = actualMinHeight + ".." + Math.addExact(actualMinHeight, actualHeight);
|
||||
String actualLogical = actualLogicalHeight < 0 ? "unknown" : Integer.toString(actualLogicalHeight);
|
||||
return new IrisDimensionContractException(runtimeName + " requires Iris dimension type " + typeKey
|
||||
+ " with range " + expectedRange + " and logical height " + logicalHeight
|
||||
+ ", but the loaded runtime uses " + actualTypeKey + " with range " + actualRange
|
||||
+ " and logical height " + actualLogical
|
||||
+ ". Generation was refused before any chunk writes. Restart after installing the exact Iris dimension type; terrain clipping is not allowed.");
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,18 @@ public class IrisEffect {
|
||||
return latch.aquire(() -> new ChronoLatch(interval)).flip();
|
||||
}
|
||||
|
||||
public boolean shouldApplyNow() {
|
||||
return canTick() && RNG.r.nextInt(chance) == 0;
|
||||
}
|
||||
|
||||
public String getParticleEffectKey() {
|
||||
return particleEffect;
|
||||
}
|
||||
|
||||
public String getSoundKey() {
|
||||
return sound;
|
||||
}
|
||||
|
||||
public Particle getParticleEffect() {
|
||||
if (particleEffect == null) {
|
||||
return null;
|
||||
@@ -215,11 +227,7 @@ public class IrisEffect {
|
||||
}
|
||||
|
||||
public void apply(Player p, Engine g) {
|
||||
if (!canTick()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RNG.r.nextInt(chance) != 0) {
|
||||
if (!shouldApplyNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,11 +286,7 @@ public class IrisEffect {
|
||||
}
|
||||
|
||||
public void apply(Entity p) {
|
||||
if (!canTick()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RNG.r.nextInt(chance) != 0) {
|
||||
if (!shouldApplyNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -383,41 +383,40 @@ public class IrisEntity extends IrisRegistrant {
|
||||
|
||||
J.runEntity(e, () -> {
|
||||
if (isSpawnEffectRiseOutOfGround() && e instanceof LivingEntity && Chunks.hasPlayersNearby(finalAt1)) {
|
||||
LivingEntity living = (LivingEntity) e;
|
||||
Location start = finalAt1.clone();
|
||||
boolean originalInvulnerable = e.isInvulnerable();
|
||||
boolean originalAi = living.hasAI();
|
||||
boolean originalCollidable = living.isCollidable();
|
||||
int originalNoDamageTicks = living.getNoDamageTicks();
|
||||
e.setInvulnerable(true);
|
||||
((LivingEntity) e).setAI(false);
|
||||
((LivingEntity) e).setCollidable(false);
|
||||
((LivingEntity) e).setNoDamageTicks(100000);
|
||||
living.setAI(false);
|
||||
living.setCollidable(false);
|
||||
living.setNoDamageTicks(100000);
|
||||
AtomicInteger t = new AtomicInteger(0);
|
||||
Runnable[] loop = new Runnable[1];
|
||||
loop[0] = () -> {
|
||||
if (t.get() > 100 || e.isDead()) {
|
||||
((LivingEntity) e).setNoDamageTicks(0);
|
||||
((LivingEntity) e).setCollidable(true);
|
||||
((LivingEntity) e).setAI(true);
|
||||
e.setInvulnerable(false);
|
||||
restoreRiseState(e, living, originalInvulnerable, originalAi,
|
||||
originalCollidable, originalNoDamageTicks);
|
||||
return;
|
||||
}
|
||||
|
||||
t.incrementAndGet();
|
||||
if (e.getLocation().getBlock().getType().isSolid() || ((LivingEntity) e).getEyeLocation().getBlock().getType().isSolid()) {
|
||||
if (e.getLocation().getBlock().getType().isSolid() || living.getEyeLocation().getBlock().getType().isSolid()) {
|
||||
e.teleport(start.add(new Vector(0, 0.1, 0)));
|
||||
ItemStack itemCrackData = new ItemStack(((LivingEntity) e).getEyeLocation().clone().subtract(0, 2, 0).getBlock().getBlockData().getMaterial());
|
||||
e.getWorld().spawnParticle(ITEM, ((LivingEntity) e).getEyeLocation(), 6, 0.2, 0.4, 0.2, 0.06f, itemCrackData);
|
||||
ItemStack itemCrackData = new ItemStack(living.getEyeLocation().clone().subtract(0, 2, 0).getBlock().getBlockData().getMaterial());
|
||||
e.getWorld().spawnParticle(ITEM, living.getEyeLocation(), 6, 0.2, 0.4, 0.2, 0.06f, itemCrackData);
|
||||
if (M.r(0.2)) {
|
||||
e.getWorld().playSound(e.getLocation(), Sound.BLOCK_CHORUS_FLOWER_GROW, 0.8f, 0.1f);
|
||||
}
|
||||
if (!J.runEntity(e, loop[0], 1)) {
|
||||
((LivingEntity) e).setNoDamageTicks(0);
|
||||
((LivingEntity) e).setCollidable(true);
|
||||
((LivingEntity) e).setAI(true);
|
||||
e.setInvulnerable(false);
|
||||
restoreRiseState(e, living, originalInvulnerable, originalAi,
|
||||
originalCollidable, originalNoDamageTicks);
|
||||
}
|
||||
} else {
|
||||
((LivingEntity) e).setNoDamageTicks(0);
|
||||
((LivingEntity) e).setCollidable(true);
|
||||
((LivingEntity) e).setAI(true);
|
||||
e.setInvulnerable(false);
|
||||
restoreRiseState(e, living, originalInvulnerable, originalAi,
|
||||
originalCollidable, originalNoDamageTicks);
|
||||
}
|
||||
};
|
||||
J.runEntity(e, loop[0]);
|
||||
@@ -428,6 +427,14 @@ public class IrisEntity extends IrisRegistrant {
|
||||
return e;
|
||||
}
|
||||
|
||||
private static void restoreRiseState(Entity entity, LivingEntity living, boolean invulnerable,
|
||||
boolean ai, boolean collidable, int noDamageTicks) {
|
||||
living.setNoDamageTicks(noDamageTicks);
|
||||
living.setCollidable(collidable);
|
||||
living.setAI(ai);
|
||||
entity.setInvulnerable(invulnerable);
|
||||
}
|
||||
|
||||
private int surfaceY(Location l) {
|
||||
int m = l.getBlockY();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -76,8 +77,9 @@ public class IrisEntitySpawn implements IRare {
|
||||
for (int id = 0; id < spawns; id++) {
|
||||
int x = (c.getX() * 16) + rng.i(15);
|
||||
int z = (c.getZ() * 16) + rng.i(15);
|
||||
int h = gen.getHeight(x, z, true) + (gen.getWorld().tryGetRealWorld() ? gen.getWorld().realWorld().getMinHeight() : -64);
|
||||
int hf = gen.getHeight(x, z, false) + (gen.getWorld().tryGetRealWorld() ? gen.getWorld().realWorld().getMinHeight() : -64);
|
||||
World world = BukkitWorldBinding.tryBind(gen.getWorld()) ? BukkitWorldBinding.world(gen.getWorld()) : null;
|
||||
int h = gen.getHeight(x, z, true) + (world == null ? -64 : world.getMinHeight());
|
||||
int hf = gen.getHeight(x, z, false) + (world == null ? -64 : world.getMinHeight());
|
||||
Location l = switch (getReferenceSpawner().getGroup()) {
|
||||
case NORMAL -> new Location(c.getWorld(), x, hf + 1, z);
|
||||
case CAVE -> {
|
||||
@@ -114,11 +116,11 @@ public class IrisEntitySpawn implements IRare {
|
||||
int spawns = minSpawns == maxSpawns ? minSpawns : rng.i(Math.min(minSpawns, maxSpawns), Math.max(minSpawns, maxSpawns));
|
||||
int s = 0;
|
||||
|
||||
if (!gen.getWorld().tryGetRealWorld()) {
|
||||
if (!BukkitWorldBinding.tryBind(gen.getWorld())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
World world = gen.getWorld().realWorld();
|
||||
World world = BukkitWorldBinding.world(gen.getWorld());
|
||||
if (spawns > 0) {
|
||||
|
||||
if (referenceMarker != null && referenceMarker.shouldExhaust()) {
|
||||
|
||||
+37
-23
@@ -29,13 +29,15 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("Controls native vanilla & ingested datapack structure generation for this dimension (set as the dimension's 'importedStructures' field). Default mode is ALL_ON (everything generates). Blacklist a few: mode=ALL_ON + list them in 'disabled'. Whitelist a few: mode=ALL_OFF + list them in 'enabled'. Both lists autocomplete every live vanilla AND ingested datapack structure key. Key matching is exact OR prefix (an entry matches when the structure key equals it or starts with it), so 'minecraft:village' covers every village variant. Run '/iris structure list <dimension>' to dump every valid key. Only affects NEWLY generated chunks, and is separate from the Iris 'structures' placement system (imported structures are placed by biome/region/dimension 'structures' lists, not here).")
|
||||
@Desc("Controls native vanilla, mod, and ingested datapack structure generation for this dimension (set as the dimension's 'importedStructures' field). Default mode is ALL_ON. Blacklist keys with 'disabled', or use ALL_OFF with 'enabled' as a whitelist. Family matching uses namespace, slash, or underscore boundaries, so 'minecraft:village' covers every village variant without matching unrelated names. Run '/iris structure list <dimension>' to dump every valid key. Only affects newly generated chunks and is separate from Iris structure placements.")
|
||||
@Data
|
||||
public class IrisImportedStructureControl {
|
||||
@Desc("Master toggle. ALL_ON generates every vanilla & datapack structure except those in 'disabled'. ALL_OFF (or CUSTOM) generates nothing except those in 'enabled'.")
|
||||
@Desc("Master toggle. ALL_ON generates every native structure except those in 'disabled'. ALL_OFF generates nothing except those in 'enabled'.")
|
||||
private VanillaStructureMode mode = VanillaStructureMode.ALL_ON;
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@@ -45,19 +47,19 @@ public class IrisImportedStructureControl {
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListVanillaStructure
|
||||
@Desc("Structure keys to turn ON while mode is ALL_OFF (or CUSTOM), e.g. 'minecraft:village_plains'. A namespace:path prefix also matches, so 'minecraft:village' enables every village variant. Ignored when mode is ALL_ON.")
|
||||
@Desc("Structure keys to turn ON while mode is ALL_OFF, e.g. 'minecraft:village_plains'. A family prefix also matches at namespace, slash, or underscore boundaries, so 'minecraft:village' enables every village variant. Ignored when mode is ALL_ON.")
|
||||
private KList<String> enabled = new KList<>();
|
||||
|
||||
@MinNumber(-512)
|
||||
@MaxNumber(512)
|
||||
@Desc("Vertical block offset applied only to UNDERGROUND vanilla structures (the UNDERGROUND_STRUCTURES and STRONGHOLDS generation steps: strongholds, trial chambers, mineshafts, ancient cities, etc.). Surface structures (villages, outposts, etc.) are never shifted. Use a negative value to push deep structures lower when your dimension's sea/terrain level differs from vanilla's (e.g. -64 if you lowered the fluid height to 0). 0 = no shift.")
|
||||
@Desc("Vertical block offset applied only to UNDERGROUND vanilla structures (the UNDERGROUND_STRUCTURES, UNDERGROUND_DECORATION, and STRONGHOLDS generation steps: strongholds, trial chambers, mineshafts, ancient cities, etc.). Surface structures (villages, outposts, etc.) are never shifted. Use a negative value to push deep structures lower when your dimension's sea/terrain level differs from vanilla's (e.g. -64 if you lowered the fluid height to 0). 0 = no shift.")
|
||||
private int undergroundYShift = 0;
|
||||
|
||||
@Desc("When true (the default), ingested datapacks generate normally: a datapack that redefines a VANILLA structure key (e.g. an ancient-city datapack overriding 'minecraft:ancient_city') REPLACES the vanilla placement, structure definition, jigsaw pools and pieces exactly as the datapack intends, and the datapack's OWN new structures generate too. When false, datapack structures do NOT generate at all - Iris strips the vanilla-key overrides from the installed datapack copy so the original vanilla structures generate untouched, and holds every datapack-namespaced structure out of natural generation so it never appears over or beside the vanilla one. Datapacks stay installed and importable either way, so when false the only way to get a datapack structure is to run '/iris structure import' and place it manually from a biome/region/dimension 'structures' list. Resolved globally across every loaded pack: if ANY dimension sets this false, vanilla-key overrides are stripped for every installed datapack.")
|
||||
@Desc("Controls whether ingested datapacks may replace minecraft-namespaced structure definitions, sets, pools, and templates. When false, those overrides are stripped from installed datapack copies so vanilla definitions stay intact. Non-minecraft structures from datapacks and mods remain governed by mode, enabled, and disabled because namespace alone cannot identify their origin. Resolved globally across loaded packs: if any dimension sets this false, minecraft-namespaced overrides are stripped from every installed datapack copy.")
|
||||
private boolean datapackOverrides = true;
|
||||
|
||||
@ArrayType(type = IrisVanillaStructureAdjustment.class, min = 1)
|
||||
@Desc("Per-structure transforms applied to the vanilla & datapack structures that still generate natively. Each entry translates the matched structure by (xShift, yShift, zShift). Use this to relocate structures you do not own through the Iris 'structures' placement system - e.g. push 'minecraft:stronghold' down 64 blocks. Multiple matching entries stack, and any underground shift here adds on top of 'undergroundYShift'. A structure suppressed by an Iris placement is unaffected (Iris already controls its position).")
|
||||
@Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. Surface-intersecting structures clear logs and leaves automatically; a matching vegetation option can force clearing for unusual placements. The last matching entry with stilt settings controls foundation columns. A structure suppressed by an Iris placement is unaffected.")
|
||||
private KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
|
||||
|
||||
public boolean active() {
|
||||
@@ -65,31 +67,26 @@ public class IrisImportedStructureControl {
|
||||
}
|
||||
|
||||
public boolean shouldGenerate(String key) {
|
||||
if (!datapackOverrides && isDatapackKey(key)) {
|
||||
return false;
|
||||
}
|
||||
if (mode == VanillaStructureMode.ALL_ON) {
|
||||
return !matches(disabled, key);
|
||||
}
|
||||
return matches(enabled, key);
|
||||
}
|
||||
|
||||
public int[] resolveOffset(String key, boolean undergroundStep) {
|
||||
int x = 0;
|
||||
public IrisNativeStructureDecision resolve(String key, boolean undergroundStep) {
|
||||
int y = undergroundStep ? undergroundYShift : 0;
|
||||
int z = 0;
|
||||
boolean clearVegetation = false;
|
||||
IrisVanillaStructureStiltSettings stilt = null;
|
||||
for (IrisVanillaStructureAdjustment adjustment : adjustments) {
|
||||
if (adjustment != null && adjustment.matches(key)) {
|
||||
x += adjustment.getXShift();
|
||||
y += adjustment.getYShift();
|
||||
z += adjustment.getZShift();
|
||||
clearVegetation |= adjustment.isClearVegetation();
|
||||
if (adjustment.getStilt() != null) {
|
||||
stilt = adjustment.getStilt();
|
||||
}
|
||||
}
|
||||
}
|
||||
return new int[]{x, y, z};
|
||||
}
|
||||
|
||||
private static boolean isDatapackKey(String key) {
|
||||
return key != null && key.contains(":") && !key.startsWith("minecraft:");
|
||||
return new IrisNativeStructureDecision(shouldGenerate(key), y, clearVegetation, stilt);
|
||||
}
|
||||
|
||||
private boolean matches(KList<String> list, String key) {
|
||||
@@ -97,13 +94,30 @@ public class IrisImportedStructureControl {
|
||||
return false;
|
||||
}
|
||||
for (String entry : list) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (key.equals(entry) || key.startsWith(entry)) {
|
||||
if (matchesKey(entry, key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean matchesKey(String pattern, String key) {
|
||||
if (pattern == null || key == null) {
|
||||
return false;
|
||||
}
|
||||
String normalizedPattern = pattern.trim().toLowerCase(Locale.ROOT);
|
||||
String normalizedKey = key.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalizedPattern.isEmpty() || !normalizedKey.startsWith(normalizedPattern)) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedKey.length() == normalizedPattern.length()) {
|
||||
return true;
|
||||
}
|
||||
char patternEnd = normalizedPattern.charAt(normalizedPattern.length() - 1);
|
||||
if (patternEnd == ':' || patternEnd == '/' || patternEnd == '_') {
|
||||
return true;
|
||||
}
|
||||
char boundary = normalizedKey.charAt(normalizedPattern.length());
|
||||
return boundary == '/' || boundary == '_';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.object;
|
||||
|
||||
public record IrisNativeStructureDecision(
|
||||
boolean generate,
|
||||
int yShift,
|
||||
boolean clearVegetation,
|
||||
IrisVanillaStructureStiltSettings stilt
|
||||
) {
|
||||
}
|
||||
@@ -100,6 +100,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
protected transient volatile boolean smartBored = false;
|
||||
@Setter
|
||||
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
||||
private transient final AtomicCache<Boolean> treeBlockPresence = new AtomicCache<>();
|
||||
@Getter
|
||||
private VectorMap<PlatformBlockState> blocks;
|
||||
@Getter
|
||||
@@ -325,6 +326,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void readLegacy(InputStream in) throws IOException {
|
||||
treeBlockPresence.reset();
|
||||
DataInputStream din = new DataInputStream(in);
|
||||
this.w = din.readInt();
|
||||
this.h = din.readInt();
|
||||
@@ -356,6 +358,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void read(InputStream in) throws Throwable {
|
||||
treeBlockPresence.reset();
|
||||
DataInputStream din = new DataInputStream(in);
|
||||
this.w = din.readInt();
|
||||
this.h = din.readInt();
|
||||
@@ -621,6 +624,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void setUnsigned(int x, int y, int z, PlatformBlockState block) {
|
||||
treeBlockPresence.reset();
|
||||
IrisBlockVector v = getSigned(x, y, z);
|
||||
|
||||
if (block == null) {
|
||||
@@ -642,6 +646,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void setUnsigned(int x, int y, int z, Block block, boolean legacy) {
|
||||
treeBlockPresence.reset();
|
||||
IrisBlockVector v = getSigned(x, y, z);
|
||||
|
||||
if (block == null) {
|
||||
@@ -923,11 +928,13 @@ public class IrisObject extends IrisRegistrant {
|
||||
&& !config.isFromBottom()
|
||||
&& config.getMode() != ObjectPlaceMode.FLOATING
|
||||
&& !rawStructurePiece
|
||||
&& !stilting
|
||||
&& !vacuuming
|
||||
&& !config.isUnderwater()
|
||||
&& !config.isOnwater()
|
||||
&& config.getCarvingSupport().supportsSurface()
|
||||
&& y > 0
|
||||
&& lacksFootprintSupport(placer, config, x, y, z, spinx, spiny, spinz)) {
|
||||
&& hasTreeBlocks()
|
||||
&& IrisSurfaceOpening.isOpen(oplacer, getLoader(), x, z, config.getTranslate(),
|
||||
config.getRotation(), spinx, spiny, spinz)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1493,38 +1500,27 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
|
||||
private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) {
|
||||
boolean carved = isCarvedAnchor(placer, x, y, z);
|
||||
CarvingMode carvingMode = placement.getCarvingSupport();
|
||||
return switch (carvingMode) {
|
||||
case SURFACE_ONLY -> carved;
|
||||
case CARVING_ONLY -> !carved;
|
||||
case SURFACE_ONLY -> placer.isCarved(x, y, z);
|
||||
case CARVING_ONLY -> !isCarvedCaveAnchor(placer, x, y, z);
|
||||
case ANYWHERE -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean lacksFootprintSupport(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z, int spinx, int spiny, int spinz) {
|
||||
IrisBlockVector rot = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone();
|
||||
int halfW = Math.max(0, Math.abs(rot.getBlockX()) / 2);
|
||||
int halfD = Math.max(0, Math.abs(rot.getBlockZ()) / 2);
|
||||
if (halfW == 0 && halfD == 0) {
|
||||
return false;
|
||||
}
|
||||
int tx = config.getTranslate().getX();
|
||||
int tz = config.getTranslate().getZ();
|
||||
int cx = x + tx;
|
||||
int cz = z + tz;
|
||||
int[] sampleX = {cx, cx - halfW, cx + halfW, cx - halfW, cx + halfW};
|
||||
int[] sampleZ = {cz, cz - halfD, cz + halfD, cz + halfD, cz - halfD};
|
||||
for (int i = 0; i < sampleX.length; i++) {
|
||||
int sh = placer.getHighest(sampleX[i], sampleZ[i], getLoader(), true);
|
||||
if (isCarvedAnchor(placer, sampleX[i], sh, sampleZ[i])) {
|
||||
return true;
|
||||
boolean hasTreeBlocks() {
|
||||
Boolean present = treeBlockPresence.aquire(() -> {
|
||||
readLock.lock();
|
||||
try {
|
||||
return IrisSurfaceOpening.containsTreeBlocks(blocks.values());
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return Boolean.TRUE.equals(present);
|
||||
}
|
||||
|
||||
private boolean isCarvedAnchor(IObjectPlacer placer, int x, int y, int z) {
|
||||
private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) {
|
||||
return placer.isCarved(x, y, z)
|
||||
|| placer.isCarved(x, y - 1, z)
|
||||
|| placer.isCarved(x, y - 2, z)
|
||||
|
||||
@@ -325,17 +325,17 @@ public class IrisObjectPlacement {
|
||||
* @param dataManager Iris Data Manager
|
||||
* @return The loot table it should use.
|
||||
*/
|
||||
public IrisLootTable getTable(PlatformBlockState state, IrisData dataManager) {
|
||||
public IrisLootTable getTable(PlatformBlockState state, IrisData dataManager, RNG rng) {
|
||||
TableCache cache = getCache(dataManager);
|
||||
BlockData data = (BlockData) state.nativeHandle();
|
||||
if (BukkitBlockResolution.isStorageChest(data)) {
|
||||
IrisLootTable picked = null;
|
||||
if (cache.exact.containsKey(data.getMaterial()) && cache.exact.get(data.getMaterial()).containsKey(data)) {
|
||||
picked = cache.exact.get(data.getMaterial()).get(data).pullRandom();
|
||||
picked = cache.exact.get(data.getMaterial()).get(data).pullRandom(rng);
|
||||
} else if (cache.basic.containsKey(data.getMaterial())) {
|
||||
picked = cache.basic.get(data.getMaterial()).pullRandom();
|
||||
picked = cache.basic.get(data.getMaterial()).pullRandom(rng);
|
||||
} else if (cache.global.getSize() > 0) {
|
||||
picked = cache.global.pullRandom();
|
||||
picked = cache.global.pullRandom(rng);
|
||||
}
|
||||
|
||||
return picked;
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@@ -22,7 +22,6 @@ import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorld;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
@@ -95,10 +94,11 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public boolean canSpawn(Engine engine) {
|
||||
if (!isValid(new BukkitWorld(engine.getWorld().realWorld())))
|
||||
PlatformWorld world = engine.getWorld().platformWorld();
|
||||
if (world == null || !isValid(world))
|
||||
return false;
|
||||
|
||||
var rate = getMaximumRate();
|
||||
IrisRate rate = getMaximumRate();
|
||||
return rate.isInfinite() || engine.getEngineData().getCooldown(this).canSpawn(rate);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class IrisSpawner extends IrisRegistrant {
|
||||
if (!canSpawn(engine))
|
||||
return false;
|
||||
|
||||
var rate = getMaximumRatePerChunk();
|
||||
IrisRate rate = getMaximumRatePerChunk();
|
||||
return rate.isInfinite() || engine.getEngineData().getChunk(x, z).getCooldown(this).canSpawn(rate);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
|
||||
final class IrisSurfaceOpening {
|
||||
private static final int SUPPORT_RADIUS = 1;
|
||||
|
||||
private IrisSurfaceOpening() {
|
||||
}
|
||||
|
||||
static boolean isOpen(IObjectPlacer placer, IrisData data, int x, int z, IrisObjectTranslate translate,
|
||||
IrisObjectRotation rotation, int spinX, int spinY, int spinZ) {
|
||||
IrisBlockVector offset = new IrisBlockVector(0, 0, 0);
|
||||
if (translate != null) {
|
||||
offset = rotation == null
|
||||
? translate.translate(offset)
|
||||
: translate.translate(offset, rotation, spinX, spinY, spinZ);
|
||||
}
|
||||
int centerX = x + offset.getBlockX();
|
||||
int centerZ = z + offset.getBlockZ();
|
||||
for (int dx = -SUPPORT_RADIUS; dx <= SUPPORT_RADIUS; dx++) {
|
||||
for (int dz = -SUPPORT_RADIUS; dz <= SUPPORT_RADIUS; dz++) {
|
||||
int sampleX = centerX + dx;
|
||||
int sampleZ = centerZ + dz;
|
||||
int surfaceY = placer.getHighest(sampleX, sampleZ, data, true);
|
||||
if (placer.isCarved(sampleX, surfaceY, sampleZ)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean containsTreeBlocks(Iterable<PlatformBlockState> states) {
|
||||
for (PlatformBlockState state : states) {
|
||||
if (state != null && state.isTreeBlock()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+7
-17
@@ -32,7 +32,7 @@ import lombok.experimental.Accessors;
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A per-structure transform applied to vanilla & datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Every block the structure writes is translated by (xShift, yShift, zShift). Use it to relocate a structure you do not control through the Iris placement system - e.g. push 'minecraft:stronghold' down 64 blocks when your terrain sits lower than vanilla's. Listed under the dimension's importedStructures 'adjustments'.")
|
||||
@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Surface structures clear intersecting trees automatically; optional postprocessing can force vegetation clearing or build palette-driven foundation columns.")
|
||||
@Data
|
||||
public class IrisVanillaStructureAdjustment {
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@@ -42,28 +42,18 @@ public class IrisVanillaStructureAdjustment {
|
||||
|
||||
@MinNumber(-512)
|
||||
@MaxNumber(512)
|
||||
@Desc("Vertical block offset. Negative pushes the structure down, positive lifts it. Applied to every block the structure places.")
|
||||
@Desc("Vertical block offset. Negative pushes the structure down, positive lifts it. The resolved shift is clamped so the structure remains inside the world's vertical build bounds.")
|
||||
private int yShift = 0;
|
||||
|
||||
@MinNumber(-512)
|
||||
@MaxNumber(512)
|
||||
@Desc("East/west block offset (positive = +X). Keep small; large horizontal shifts move the structure far from the position vanilla chose for it.")
|
||||
private int xShift = 0;
|
||||
@Desc("When true, force logs and leaves out of the structure footprint even when the structure does not reach the detected tree base. Normal surface-intersecting structures are protected automatically.")
|
||||
private boolean clearVegetation = false;
|
||||
|
||||
@MinNumber(-512)
|
||||
@MaxNumber(512)
|
||||
@Desc("North/south block offset (positive = +Z). Keep small; large horizontal shifts move the structure far from the position vanilla chose for it.")
|
||||
private int zShift = 0;
|
||||
@Desc("Optional foundation columns placed beneath the native structure piece bases after placement.")
|
||||
private IrisVanillaStructureStiltSettings stilt = null;
|
||||
|
||||
public boolean matches(String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
for (String entry : match) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (key.equals(entry) || key.startsWith(entry)) {
|
||||
if (IrisImportedStructureControl.matchesKey(entry, key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("Defines the foundation columns placed beneath a native vanilla, mod, or datapack structure.")
|
||||
@Data
|
||||
public class IrisVanillaStructureStiltSettings {
|
||||
@MinNumber(1)
|
||||
@MaxNumber(4064)
|
||||
@Desc("Maximum number of blocks each foundation column may descend while searching for solid ground.")
|
||||
private int maxDepth = 64;
|
||||
|
||||
@Desc("Block palette used for foundation columns.")
|
||||
private IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:cobblestone");
|
||||
}
|
||||
@@ -18,35 +18,20 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@Accessors(chain = true, fluent = true)
|
||||
public class IrisWorld {
|
||||
private static final KList<Player> NO_PLAYERS = new KList<>();
|
||||
private static final KList<? extends Entity> NO_ENTITIES = new KList<>();
|
||||
private NamespacedKey key;
|
||||
private String platformIdentity;
|
||||
private String name;
|
||||
private File worldFolder;
|
||||
@@ -54,106 +39,24 @@ public class IrisWorld {
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private long seed;
|
||||
private World.Environment environment;
|
||||
private World realWorld;
|
||||
private PlatformWorld platformWorld;
|
||||
private int minHeight;
|
||||
private int maxHeight;
|
||||
|
||||
public static IrisWorld fromWorld(World world) {
|
||||
return bindWorld(IrisWorld.builder().build(), world);
|
||||
}
|
||||
|
||||
private static IrisWorld bindWorld(IrisWorld iw, World world) {
|
||||
return iw.key(WorldIdentity.key(world))
|
||||
.name(world.getName())
|
||||
.worldFolder(world.getWorldFolder())
|
||||
.minHeight(world.getMinHeight())
|
||||
.maxHeight(world.getMaxHeight())
|
||||
.realWorld(world)
|
||||
.environment(world.getEnvironment());
|
||||
}
|
||||
|
||||
public long getRawWorldSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public String identity() {
|
||||
return key == null ? platformIdentity : key.toString();
|
||||
return platformIdentity;
|
||||
}
|
||||
|
||||
public void setRawWorldSeed(long seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public boolean tryGetRealWorld() {
|
||||
if (hasRealWorld()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
World w = WorldIdentity.resolve(key).orElse(null);
|
||||
|
||||
if (w != null) {
|
||||
realWorld = w;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasRealWorld() {
|
||||
return realWorld != null;
|
||||
}
|
||||
|
||||
public List<Player> getPlayers() {
|
||||
|
||||
if (hasRealWorld()) {
|
||||
return realWorld().getPlayers();
|
||||
}
|
||||
|
||||
return NO_PLAYERS;
|
||||
}
|
||||
|
||||
public void evacuate() {
|
||||
if (hasRealWorld()) {
|
||||
IrisToolbelt.evacuate(realWorld());
|
||||
}
|
||||
}
|
||||
|
||||
public void bind(WorldInfo worldInfo) {
|
||||
key(WorldIdentity.key(worldInfo))
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldInfo))
|
||||
.minHeight(worldInfo.getMinHeight())
|
||||
.maxHeight(worldInfo.getMaxHeight())
|
||||
.environment(worldInfo.getEnvironment());
|
||||
}
|
||||
|
||||
public void bind(World world) {
|
||||
if (hasRealWorld()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindWorld(this, world);
|
||||
}
|
||||
|
||||
public Location spawnLocation() {
|
||||
if (hasRealWorld()) {
|
||||
return realWorld().getSpawnLocation();
|
||||
}
|
||||
|
||||
IrisLogging.error("This world is not real yet, cannot get spawn location! HEADLESS!");
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T extends Entity> Collection<? extends T> getEntitiesByClass(Class<T> t) {
|
||||
if (hasRealWorld()) {
|
||||
return realWorld().getEntitiesByClass(t);
|
||||
}
|
||||
|
||||
return (KList<? extends T>) NO_ENTITIES;
|
||||
public boolean hasPlatformWorld() {
|
||||
return platformWorld != null;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
|
||||
@@ -51,15 +51,24 @@ public class TileData implements Cloneable {
|
||||
private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
|
||||
private static final boolean BUKKIT_PRESENT = detectBukkit();
|
||||
private static volatile TileReader FALLBACK_READER = null;
|
||||
private static volatile TileFactory FALLBACK_FACTORY = null;
|
||||
|
||||
public interface TileReader {
|
||||
TileData read(DataInputStream in) throws IOException;
|
||||
}
|
||||
|
||||
public interface TileFactory {
|
||||
TileData create(PlatformBlockState state, KMap<String, Object> properties);
|
||||
}
|
||||
|
||||
public static void bindFallbackReader(TileReader reader) {
|
||||
FALLBACK_READER = reader;
|
||||
}
|
||||
|
||||
public static void bindFallbackFactory(TileFactory factory) {
|
||||
FALLBACK_FACTORY = factory;
|
||||
}
|
||||
|
||||
private static boolean detectBukkit() {
|
||||
try {
|
||||
Class.forName("org.bukkit.Bukkit", false, TileData.class.getClassLoader());
|
||||
@@ -84,7 +93,7 @@ public class TileData implements Cloneable {
|
||||
if (!BukkitPlatform.hasTile(block.getType()))
|
||||
return null;
|
||||
if (useLegacy) {
|
||||
var legacy = LegacyTileData.fromBukkit(block.getState());
|
||||
LegacyTileData legacy = LegacyTileData.fromBukkit(block.getState());
|
||||
if (legacy != null)
|
||||
return legacy;
|
||||
}
|
||||
@@ -93,6 +102,10 @@ public class TileData implements Cloneable {
|
||||
}
|
||||
|
||||
public static TileData of(PlatformBlockState state, KMap<String, Object> properties) {
|
||||
TileFactory factory = FALLBACK_FACTORY;
|
||||
if (factory != null) {
|
||||
return factory.create(state, properties);
|
||||
}
|
||||
if (!BUKKIT_PRESENT) {
|
||||
return null;
|
||||
}
|
||||
@@ -104,11 +117,12 @@ public class TileData implements Cloneable {
|
||||
}
|
||||
|
||||
public static TileData read(DataInputStream in) throws IOException {
|
||||
TileReader reader = FALLBACK_READER;
|
||||
if (reader != null) {
|
||||
return reader.read(in);
|
||||
}
|
||||
if (!BUKKIT_PRESENT) {
|
||||
TileReader reader = FALLBACK_READER;
|
||||
if (reader != null) {
|
||||
return reader.read(in);
|
||||
}
|
||||
throw new IOException("No tile data reader is available for this platform");
|
||||
}
|
||||
if (!in.markSupported())
|
||||
throw new IOException("Mark not supported");
|
||||
@@ -182,7 +196,7 @@ public class TileData implements Cloneable {
|
||||
|
||||
@Override
|
||||
public TileData clone() {
|
||||
var clone = new TileData();
|
||||
TileData clone = new TileData();
|
||||
clone.material = material;
|
||||
clone.properties = properties.copy(); //TODO make a deep copy
|
||||
return clone;
|
||||
|
||||
@@ -20,14 +20,11 @@ package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Master toggle for vanilla & datapack structure generation in a dimension.")
|
||||
@Desc("Master toggle for native vanilla, mod, and datapack structure generation in a dimension.")
|
||||
public enum VanillaStructureMode {
|
||||
@Desc("All vanilla & datapack structures generate, except any keys listed in 'disabled'. This is the default. Use this to blacklist a few structures.")
|
||||
@Desc("All native vanilla, mod, and datapack structures generate, except any keys listed in 'disabled'. This is the default. Use this to blacklist a few structures.")
|
||||
ALL_ON,
|
||||
|
||||
@Desc("No vanilla or datapack structures generate, except any keys listed in 'enabled'. Use this to whitelist a few structures.")
|
||||
ALL_OFF,
|
||||
|
||||
@Desc("Same as ALL_OFF: nothing generates except keys listed in 'enabled'.")
|
||||
CUSTOM
|
||||
@Desc("No native vanilla, mod, or datapack structures generate, except any keys listed in 'enabled'. Use this to whitelist a few structures.")
|
||||
ALL_OFF
|
||||
}
|
||||
|
||||
@@ -41,11 +41,13 @@ import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.object.IrisDimensionContractException;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.VanillaStructureMode;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.StudioMode;
|
||||
import art.arcane.iris.engine.platform.studio.StudioGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
@@ -110,6 +112,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private volatile Looper hotloader;
|
||||
private volatile StudioMode lastMode;
|
||||
private volatile DummyBiomeProvider dummyBiomeProvider;
|
||||
private volatile Throwable initializationFailure;
|
||||
private volatile IrisDimension validatedDimension;
|
||||
private volatile IrisDimensionRuntimeContract validatedWorldContract;
|
||||
private volatile boolean closing;
|
||||
@Setter
|
||||
private volatile StudioGenerator studioGenerator;
|
||||
@@ -132,15 +137,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
new KList<>(".iris"),
|
||||
new KList<>()
|
||||
);
|
||||
this.initializationFailure = null;
|
||||
this.validatedDimension = null;
|
||||
this.validatedWorldContract = null;
|
||||
this.closing = false;
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, BukkitPlatform.plugin());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onWorldInit(WorldInitEvent event) {
|
||||
if (!Objects.equals(world.key(), WorldIdentity.key(event.getWorld()))) return;
|
||||
if (!Objects.equals(world.identity(), WorldIdentity.key(event.getWorld()).toString())) return;
|
||||
BukkitPlatform.volmitPlugin().unregisterListener(this);
|
||||
world.bind(event.getWorld());
|
||||
world.setRawWorldSeed(event.getWorld().getSeed());
|
||||
if (initialize(event.getWorld())) return;
|
||||
|
||||
@@ -153,18 +160,29 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
private boolean initialize(World world) {
|
||||
Engine engine = getEngine(world);
|
||||
if (engine == null) return false;
|
||||
try {
|
||||
Engine engine = getEngine(world);
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
INMS.get().inject(world.getSeed(), engine, world);
|
||||
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
||||
if (!studio) {
|
||||
J.s(() -> updateSpawnLocation(world), 1);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
initializationFailure = e;
|
||||
spawnChunks.completeExceptionally(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("Failed to inject biome source into " + world.getName());
|
||||
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
|
||||
e.printStackTrace();
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Failed to initialize Iris for world '" + world.getName() + "'", e);
|
||||
}
|
||||
spawnChunks.complete(INMS.get().getSpawnChunkCount(world));
|
||||
BukkitPlatform.volmitPlugin().unregisterListener(this);
|
||||
@@ -280,6 +298,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
private Engine getEngine(WorldInfo world) {
|
||||
throwIfInitializationFailed();
|
||||
validateAndBindWorld(world);
|
||||
if (setup.get()) {
|
||||
return getEngine();
|
||||
}
|
||||
@@ -322,6 +342,40 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
}
|
||||
|
||||
private void throwIfInitializationFailed() {
|
||||
Throwable failure = initializationFailure;
|
||||
if (failure == null) {
|
||||
return;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Iris initialization previously failed for world '" + world.name() + "'", failure);
|
||||
}
|
||||
|
||||
private void validateAndBindWorld(WorldInfo worldInfo) {
|
||||
IrisDimension dimension = getTarget().getDimension();
|
||||
IrisDimensionRuntimeContract activeContract = validatedWorldContract;
|
||||
if (activeContract == null || validatedDimension != dimension) {
|
||||
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(dimension, "iris");
|
||||
if (activeContract != null) {
|
||||
expected.requireExact("Bukkit world '" + worldInfo.getName() + "'", activeContract);
|
||||
}
|
||||
int runtimeHeight = worldInfo.getMaxHeight() - worldInfo.getMinHeight();
|
||||
expected.requireHeight("Bukkit world '" + worldInfo.getName() + "'", worldInfo.getMinHeight(), runtimeHeight);
|
||||
BukkitWorldBinding.bind(world, worldInfo);
|
||||
validatedWorldContract = expected;
|
||||
validatedDimension = dimension;
|
||||
return;
|
||||
}
|
||||
if (!world.hasPlatformWorld() && worldInfo instanceof World) {
|
||||
BukkitWorldBinding.bind(world, worldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closeAsync();
|
||||
@@ -434,13 +488,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (closing) {
|
||||
return;
|
||||
}
|
||||
throwIfInitializationFailed();
|
||||
|
||||
try {
|
||||
Engine engine = getEngine(world);
|
||||
lastChunkGenTime = System.currentTimeMillis();
|
||||
computeStudioGenerator();
|
||||
TerrainChunk tc = TerrainChunk.create(d);
|
||||
this.world.bind(world);
|
||||
if (studioGenerator != null) {
|
||||
studioGenerator.generateChunk(engine, tc, x, z);
|
||||
} else {
|
||||
@@ -452,6 +506,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
IrisLogging.debug("Generated " + x + " " + z);
|
||||
} catch (IrisDimensionContractException e) {
|
||||
throw e;
|
||||
} catch (GenerationSessionException e) {
|
||||
if (closing || isExpectedTeardown(engine, e)) {
|
||||
return;
|
||||
@@ -494,7 +550,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
private boolean isMaintenanceActive() {
|
||||
World realWorld = this.world.realWorld();
|
||||
World realWorld = BukkitWorldBinding.world(this.world);
|
||||
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
|
||||
}
|
||||
|
||||
@@ -525,17 +581,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return true;
|
||||
}
|
||||
|
||||
World realWorld = this.world.realWorld();
|
||||
World realWorld = BukkitWorldBinding.world(this.world);
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorld(realWorld);
|
||||
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(realWorld));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
|
||||
Engine currentEngine = engine;
|
||||
if (currentEngine == null || !setup.get()) {
|
||||
currentEngine = getEngine(worldInfo);
|
||||
}
|
||||
Engine currentEngine = getEngine(worldInfo);
|
||||
|
||||
boolean ignoreFluid = switch (heightMap) {
|
||||
case OCEAN_FLOOR, OCEAN_FLOOR_WG -> true;
|
||||
@@ -579,14 +632,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
if (initializationFailure != null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Engine e = this.engine;
|
||||
if (e == null) {
|
||||
return true;
|
||||
}
|
||||
return e.getDimension().getImportedStructures().getMode() != VanillaStructureMode.ALL_OFF;
|
||||
return e.getDimension().getImportedStructures().active();
|
||||
} catch (Throwable t) {
|
||||
return true;
|
||||
IrisLogging.reportError("Iris could not resolve native structure generation policy for "
|
||||
+ world.name() + ".", t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.core.events.IrisLootEvent;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
@@ -343,7 +344,8 @@ public final class EngineBukkitOps {
|
||||
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
|
||||
if (po != null && po.getPlacement() != null) {
|
||||
if (BukkitBlockResolution.isStorageChest(b.getBlockData())) {
|
||||
IrisLootTable table = po.getPlacement().getTable(BukkitBlockState.of(b.getBlockData()), engine.getData());
|
||||
IrisLootTable table = po.getPlacement().getTable(
|
||||
BukkitBlockState.of(b.getBlockData()), engine.getData(), rng);
|
||||
if (table != null) {
|
||||
tables.add(table);
|
||||
if (po.getPlacement().isOverrideGlobalLoot()) {
|
||||
@@ -390,8 +392,8 @@ public final class EngineBukkitOps {
|
||||
if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z))
|
||||
return;
|
||||
|
||||
if (PaperLib.isPaper() && engine.getWorld().hasRealWorld()) {
|
||||
PaperLib.getChunkAtAsync(engine.getWorld().realWorld(), x >> 4, z >> 4).thenAccept((c) -> {
|
||||
if (PaperLib.isPaper() && engine.getWorld().hasPlatformWorld()) {
|
||||
PaperLib.getChunkAtAsync(BukkitWorldBinding.world(engine.getWorld()), x >> 4, z >> 4).thenAccept((c) -> {
|
||||
Runnable r = () -> {
|
||||
for (ItemStack i : items) {
|
||||
inv.addItem(i);
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -46,6 +47,7 @@ public final class BukkitBlockState implements PlatformBlockState {
|
||||
private volatile Boolean lit;
|
||||
private volatile Boolean updatable;
|
||||
private volatile Boolean foliage;
|
||||
private volatile Boolean treeBlock;
|
||||
private volatile Boolean foliagePlantable;
|
||||
private volatile Boolean decorant;
|
||||
private volatile Boolean storage;
|
||||
@@ -168,6 +170,16 @@ public final class BukkitBlockState implements PlatformBlockState {
|
||||
return data instanceof IrisCustomData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deferredPlacementKey() {
|
||||
return data instanceof IrisCustomData custom ? custom.getCustom().toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState placementBaseState() {
|
||||
return data instanceof IrisCustomData custom ? of(custom.getBase()) : this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
Boolean cached = fluid;
|
||||
@@ -228,6 +240,16 @@ public final class BukkitBlockState implements PlatformBlockState {
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
Boolean cached = treeBlock;
|
||||
if (cached == null) {
|
||||
cached = Tag.LOGS.isTagged(data.getMaterial()) || Tag.LEAVES.isTagged(data.getMaterial());
|
||||
treeBlock = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
Boolean cached = foliagePlantable;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisEnvironment;
|
||||
import org.bukkit.World;
|
||||
|
||||
public final class BukkitEnvironment {
|
||||
private BukkitEnvironment() {
|
||||
}
|
||||
|
||||
public static World.Environment from(IrisEnvironment environment) {
|
||||
if (environment == null) {
|
||||
return World.Environment.NORMAL;
|
||||
}
|
||||
return switch (environment) {
|
||||
case NORMAL -> World.Environment.NORMAL;
|
||||
case NETHER -> World.Environment.NETHER;
|
||||
case THE_END -> World.Environment.THE_END;
|
||||
case CUSTOM -> World.Environment.CUSTOM;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public final class BukkitWorldBinding {
|
||||
private BukkitWorldBinding() {
|
||||
}
|
||||
|
||||
public static IrisWorld bind(IrisWorld target, WorldInfo worldInfo) {
|
||||
target.platformIdentity(WorldIdentity.key(worldInfo).toString())
|
||||
.name(worldInfo.getName())
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldInfo))
|
||||
.minHeight(worldInfo.getMinHeight())
|
||||
.maxHeight(worldInfo.getMaxHeight());
|
||||
if (worldInfo instanceof World world) {
|
||||
target.platformWorld(new BukkitWorld(world));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
public static boolean tryBind(IrisWorld target) {
|
||||
if (target.hasPlatformWorld()) {
|
||||
return true;
|
||||
}
|
||||
NamespacedKey key = NamespacedKey.fromString(target.identity());
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
World world = WorldIdentity.resolve(key).orElse(null);
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
bind(target, world);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static World world(IrisWorld target) {
|
||||
if (target == null || !target.hasPlatformWorld()) {
|
||||
return null;
|
||||
}
|
||||
return BukkitPlatform.unwrapWorld(target.platformWorld());
|
||||
}
|
||||
|
||||
public static List<Player> players(IrisWorld target) {
|
||||
World world = world(target);
|
||||
return world == null ? List.of() : world.getPlayers();
|
||||
}
|
||||
|
||||
public static <T extends Entity> Collection<? extends T> entities(IrisWorld target, Class<T> type) {
|
||||
World world = world(target);
|
||||
return world == null ? List.of() : world.getEntitiesByClass(type);
|
||||
}
|
||||
|
||||
public static Location spawnLocation(IrisWorld target) {
|
||||
World world = world(target);
|
||||
if (world != null) {
|
||||
return world.getSpawnLocation();
|
||||
}
|
||||
IrisLogging.error("This world is not real yet, cannot get spawn location! HEADLESS!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,10 @@ public class B {
|
||||
return state != null && state.isFoliage();
|
||||
}
|
||||
|
||||
public static boolean isTreeBlock(PlatformBlockState state) {
|
||||
return state != null && state.isTreeBlock();
|
||||
}
|
||||
|
||||
public static boolean isFoliagePlantable(PlatformBlockState state) {
|
||||
return state != null && state.isFoliagePlantable();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.apache.commons.lang.Validate;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.DyeColor;
|
||||
|
||||
@@ -4,8 +4,6 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.volmlib.util.parallel.MultiBurstSupport;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import kotlinx.coroutines.CoroutineDispatcher;
|
||||
import kotlinx.coroutines.ExecutorsKt;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
@@ -15,8 +13,6 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
private static final long TIMEOUT = Long.getLong("iris.burst.timeout", 15000);
|
||||
public static final MultiBurst burst = new MultiBurst();
|
||||
public static final MultiBurst ioBurst = new MultiBurst("Iris IO", () -> IrisSettings.get().getConcurrency().getIoParallelism());
|
||||
private volatile CoroutineDispatcher dispatcher;
|
||||
private volatile ExecutorService dispatcherService;
|
||||
|
||||
public MultiBurst() {
|
||||
this("Iris");
|
||||
@@ -34,20 +30,6 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
super(name, priority, parallelism, IrisSettings::getThreadCount, M::ms, IrisLogging::reportError, IrisLogging::info, IrisLogging::warn, TIMEOUT);
|
||||
}
|
||||
|
||||
public CoroutineDispatcher getDispatcher() {
|
||||
ExecutorService service = service();
|
||||
if (dispatcherService != service || dispatcher == null) {
|
||||
synchronized (this) {
|
||||
if (dispatcherService != service || dispatcher == null) {
|
||||
dispatcher = ExecutorsKt.from(service);
|
||||
dispatcherService = service;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
public boolean ownsCurrentThread() {
|
||||
Thread thread = Thread.currentThread();
|
||||
if (!(thread instanceof ForkJoinWorkerThread worker)) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class PregeneratorJobPlatformIsolationTest {
|
||||
@Test
|
||||
public void pregeneratorJobHasNoBukkitSignatures() {
|
||||
for (Field field : PregeneratorJob.class.getDeclaredFields()) {
|
||||
assertPlatformNeutral(field.getType());
|
||||
}
|
||||
for (Constructor<?> constructor : PregeneratorJob.class.getDeclaredConstructors()) {
|
||||
for (Class<?> parameter : constructor.getParameterTypes()) {
|
||||
assertPlatformNeutral(parameter);
|
||||
}
|
||||
}
|
||||
for (Method method : PregeneratorJob.class.getDeclaredMethods()) {
|
||||
assertPlatformNeutral(method.getReturnType());
|
||||
for (Class<?> parameter : method.getParameterTypes()) {
|
||||
assertPlatformNeutral(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPlatformNeutral(Class<?> type) {
|
||||
Class<?> component = type;
|
||||
while (component.isArray()) {
|
||||
component = component.getComponentType();
|
||||
}
|
||||
assertFalse(component.getName(), component.getName().startsWith("org.bukkit."));
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,13 @@ public class WorldLifecycleStagingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stagedStemGeneratorIsConsumedWhenLevelIdDiffersFromWorldName() {
|
||||
public void stagedStemGeneratorCannotBeConsumedByDifferentWorldName() {
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||
|
||||
WorldLifecycleStaging.stageStemGenerator("iris_world", generator);
|
||||
|
||||
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
assertNull(WorldLifecycleStaging.consumeStemGenerator("iris_world"));
|
||||
assertNull(WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("iris_world"));
|
||||
assertNull(WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
}
|
||||
|
||||
|
||||
+48
-3
@@ -38,6 +38,9 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
Path root = Files.createTempDirectory("iris-bootstrap-cold");
|
||||
try {
|
||||
Path dataDirectory = root.resolve("plugins/Iris");
|
||||
Path legacyDatapack = dataDirectory.resolve("bootstrap/datapack");
|
||||
Files.createDirectories(legacyDatapack);
|
||||
Files.writeString(legacyDatapack.resolve("obsolete.txt"), "obsolete", StandardCharsets.UTF_8);
|
||||
DefaultPackBootstrapProvisioner.ProvisionOptions options = options(server, root, Duration.ofHours(1));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult installed = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
@@ -56,9 +59,11 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status());
|
||||
assertEquals(1, requests.get());
|
||||
assertTrue(Files.isRegularFile(installed.packRoot().resolve("dimensions/overworld.json")));
|
||||
assertEquals(root.resolve("datapacks/iris"), installed.datapackRoot());
|
||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("pack.mcmeta")));
|
||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/overworld/worldgen/biome/bootstrap_biome.json")));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory));
|
||||
assertFalse(Files.exists(dataDirectory.resolve("bootstrap/datapack")));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.wasProvisionedThisStartup());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
@@ -66,6 +71,46 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingArchiveRebuildsWorldDatapackFromValidatedInstalledPack() throws Exception {
|
||||
byte[] archive = packArchive("overworld", "bootstrap_biome");
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(archive, requests);
|
||||
boolean serverStopped = false;
|
||||
Path root = Files.createTempDirectory("iris-bootstrap-offline-migration");
|
||||
try {
|
||||
Path dataDirectory = root.resolve("plugins/Iris");
|
||||
DefaultPackBootstrapProvisioner.ProvisionOptions options = options(server, root, Duration.ofHours(1));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult installed = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
},
|
||||
options
|
||||
);
|
||||
Files.delete(dataDirectory.resolve("cache/bootstrap/default-overworld.zip"));
|
||||
delete(installed.datapackRoot());
|
||||
server.stop(0);
|
||||
serverStopped = true;
|
||||
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult rebuilt = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, rebuilt.status());
|
||||
assertEquals(1, requests.get());
|
||||
assertTrue(Files.isRegularFile(rebuilt.datapackRoot().resolve("pack.mcmeta")));
|
||||
assertTrue(Files.isRegularFile(rebuilt.datapackRoot().resolve("data/overworld/worldgen/biome/bootstrap_biome.json")));
|
||||
} finally {
|
||||
if (!serverStopped) {
|
||||
server.stop(0);
|
||||
}
|
||||
delete(root);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptCacheRedownloadsAndRepairsArchive() throws Exception {
|
||||
byte[] archive = packArchive("overworld", "bootstrap_biome");
|
||||
@@ -169,7 +214,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("data/overworld/worldgen/biome/local_biome.json")));
|
||||
|
||||
Files.writeString(target.resolve("biomes/local.json"), biomeJson("changed_biome"), StandardCharsets.UTF_8);
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
@@ -198,7 +243,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
writePack(dataDirectory.resolve("packs/second"), "second", "second_biome");
|
||||
writePack(root.resolve("dimensions/example/world/iris/pack"), "world_local", "world_local_biome");
|
||||
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackValidatorStructureGraphTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void acceptsCompleteStructureGraph() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"castle\"]}]}");
|
||||
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
|
||||
write(pack, "jigsaw-pools/castle/start.json", "{\"pieces\":[{\"piece\":\"castle/start\"}],\"fallback\":\"castle/end\"}");
|
||||
write(pack, "jigsaw-pools/castle/end.json", "{\"pieces\":[]}");
|
||||
write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/start\",\"connectors\":[{\"pool\":\"castle/end\"}]}");
|
||||
write(pack, "objects/castle/start.iob", "object");
|
||||
|
||||
assertTrue(PackValidator.validateStructureGraph(pack).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsMissingReferencesInDeterministicGraphOrder() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"missing-structure\"]}]}");
|
||||
write(pack, "structures/castle.json", "{\"startPool\":\"missing-start\"}");
|
||||
write(pack, "jigsaw-pools/castle/start.json", "{\"pieces\":[{\"piece\":\"missing-piece\"}],\"fallback\":\"missing-fallback\"}");
|
||||
write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/start\",\"connectors\":[{\"pool\":\"missing-connector-pool\"}]}");
|
||||
write(pack, "objects/castle/start.iob", "object");
|
||||
|
||||
List<String> errors = PackValidator.validateStructureGraph(pack);
|
||||
|
||||
assertEquals(List.of(
|
||||
"Dimension 'main' structures[0].structures[0] references missing structure 'missing-structure'.",
|
||||
"Structure 'castle' references missing start pool 'missing-start'.",
|
||||
"Jigsaw pool 'castle/start' pieces[0] references missing piece 'missing-piece'.",
|
||||
"Jigsaw pool 'castle/start' references missing fallback pool 'missing-fallback'.",
|
||||
"Jigsaw piece 'castle/start' connectors[0] references missing pool 'missing-connector-pool'."
|
||||
), errors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresLegacyGeneratedStructureIndex() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "structures/structure-index.json", "{\"counts\":{},\"structureSets\":{},\"iris\":[]}");
|
||||
|
||||
assertTrue(PackValidator.validateStructureGraph(pack).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsMalformedStructureJson() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "structures/castle.json", "{");
|
||||
|
||||
List<String> errors = PackValidator.validateStructureGraph(pack);
|
||||
|
||||
assertEquals(1, errors.size());
|
||||
assertTrue(errors.get(0).startsWith("Structure 'castle' has invalid JSON:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsMalformedJigsawPoolJson() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "jigsaw-pools/castle/start.json", "{");
|
||||
|
||||
List<String> errors = PackValidator.validateStructureGraph(pack);
|
||||
|
||||
assertEquals(1, errors.size());
|
||||
assertTrue(errors.get(0).startsWith("Jigsaw pool 'castle/start' has invalid JSON:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsMalformedJigsawPieceJson() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "jigsaw-pieces/castle/start.json", "{");
|
||||
|
||||
List<String> errors = PackValidator.validateStructureGraph(pack);
|
||||
|
||||
assertEquals(1, errors.size());
|
||||
assertTrue(errors.get(0).startsWith("Jigsaw piece 'castle/start' has invalid JSON:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsMissingJigsawPieceObject() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/missing\"}");
|
||||
|
||||
assertEquals(List.of(
|
||||
"Jigsaw piece 'castle/start' references missing object 'castle/missing'."
|
||||
), PackValidator.validateStructureGraph(pack));
|
||||
}
|
||||
|
||||
private void write(File root, String relative, String content) throws Exception {
|
||||
Path path = root.toPath().resolve(relative);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ExternalDataSVCRegistryTest {
|
||||
@Test
|
||||
public void registryLoadsWithoutResolvingOptionalProviderClasses() throws Exception {
|
||||
Class<?> serviceClass = Class.forName(ExternalDataSVC.class.getName(), true, ExternalDataSVC.class.getClassLoader());
|
||||
Field registryField = serviceClass.getDeclaredField("BUILT_IN_PROVIDERS");
|
||||
registryField.setAccessible(true);
|
||||
List<?> definitions = (List<?>) registryField.get(null);
|
||||
List<String> pluginIds = new ArrayList<>(definitions.size());
|
||||
|
||||
for (Object definition : definitions) {
|
||||
Method pluginIdMethod = definition.getClass().getDeclaredMethod("pluginId");
|
||||
Method classNameMethod = definition.getClass().getDeclaredMethod("className");
|
||||
pluginIdMethod.setAccessible(true);
|
||||
classNameMethod.setAccessible(true);
|
||||
pluginIds.add((String) pluginIdMethod.invoke(definition));
|
||||
assertTrue(((String) classNameMethod.invoke(definition)).startsWith("art.arcane.iris.core.link.data."));
|
||||
}
|
||||
|
||||
assertEquals(List.of(
|
||||
"CraftEngine",
|
||||
"Nexo",
|
||||
"ItemsAdder",
|
||||
"ExecutableItems",
|
||||
"MMOItems",
|
||||
"EcoItems",
|
||||
"MythicMobs",
|
||||
"MythicCrucible",
|
||||
"KGenerators"
|
||||
), pluginIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package art.arcane.iris.core.structure;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StructureIndexServiceTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisPlatform previousPlatform;
|
||||
|
||||
@Before
|
||||
public void detachPlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedIndexLivesOutsideStructureRegistrantDirectory() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
File index = StructureIndexService.indexFile(pack);
|
||||
|
||||
assertEquals(new File(pack, ".iris/structure-index.json"), index);
|
||||
assertFalse(index.toPath().startsWith(new File(pack, "structures").toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyIndexPathRemainsExplicitForCleanup() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
|
||||
assertEquals(new File(pack, "structures/structure-index.json"),
|
||||
StructureIndexService.legacyIndexFile(pack));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyIndexIsDeletedAndStructureKeysAreRefreshed() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
File legacyIndex = StructureIndexService.legacyIndexFile(pack);
|
||||
Files.createDirectories(legacyIndex.toPath().getParent());
|
||||
Files.writeString(legacyIndex.toPath(), "{}", StandardCharsets.UTF_8);
|
||||
IrisData data = mock(IrisData.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
KCache<String, IrisStructure> loadCache = mock(KCache.class);
|
||||
when(data.getDataFolder()).thenReturn(pack);
|
||||
when(data.getStructureLoader()).thenReturn(structureLoader);
|
||||
when(structureLoader.getLoadCache()).thenReturn(loadCache);
|
||||
|
||||
StructureIndexService.removeLegacyIndex(data);
|
||||
|
||||
assertFalse(legacyIndex.exists());
|
||||
verify(loadCache).invalidate("structure-index");
|
||||
verify(structureLoader).clearList();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeOnceWaitsForBoundHooksThenCleansBeforeEnumerating() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
File legacyIndex = StructureIndexService.legacyIndexFile(pack);
|
||||
Files.createDirectories(legacyIndex.toPath().getParent());
|
||||
Files.writeString(legacyIndex.toPath(), "{}", StandardCharsets.UTF_8);
|
||||
IrisData data = mock(IrisData.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
KCache<String, IrisStructure> loadCache = mock(KCache.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformStructureHooks structureHooks = mock(PlatformStructureHooks.class);
|
||||
when(data.getDataFolder()).thenReturn(pack);
|
||||
when(data.getStructureLoader()).thenReturn(structureLoader);
|
||||
when(structureLoader.getLoadCache()).thenReturn(loadCache);
|
||||
when(structureLoader.getPossibleKeys()).thenReturn(new String[]{"castle", "structure-index"});
|
||||
when(structureHooks.structureKeys()).thenReturn(List.of("minecraft:village", "example:castle"));
|
||||
when(structureHooks.structureSetKeys()).thenReturn(List.of("minecraft:villages"));
|
||||
|
||||
assertNull(StructureIndexService.writeOnce(data));
|
||||
assertTrue(legacyIndex.exists());
|
||||
|
||||
IrisPlatforms.bind(platform);
|
||||
assertNull(StructureIndexService.writeOnce(data));
|
||||
assertTrue(legacyIndex.exists());
|
||||
|
||||
when(platform.structureHooks()).thenReturn(structureHooks);
|
||||
File index = StructureIndexService.writeOnce(data);
|
||||
|
||||
assertEquals(StructureIndexService.indexFile(pack), index);
|
||||
assertTrue(index.isFile());
|
||||
assertFalse(legacyIndex.exists());
|
||||
InOrder order = inOrder(loadCache, structureLoader, structureHooks);
|
||||
order.verify(loadCache).invalidate("structure-index");
|
||||
order.verify(structureLoader).clearList();
|
||||
order.verify(structureHooks).structureKeys();
|
||||
order.verify(structureHooks).structureSetKeys();
|
||||
assertNull(StructureIndexService.writeOnce(data));
|
||||
verify(structureHooks, times(1)).structureKeys();
|
||||
verify(structureHooks, times(1)).structureSetKeys();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedServiceDoesNotLinkBukkitRuntimeClasses() throws Exception {
|
||||
InputStream stream = StructureIndexService.class.getResourceAsStream("StructureIndexService.class");
|
||||
assertNotNull(stream);
|
||||
String bytecode;
|
||||
try (InputStream input = stream) {
|
||||
bytecode = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
assertFalse(bytecode.contains("art/arcane/iris/util/project/matter/IrisMatterSupport"));
|
||||
assertFalse(bytecode.contains("org/bukkit/"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mode.ModeOverworld;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class IrisEnginePlatformHookIsolationTest {
|
||||
private static final List<String> BUKKIT_ENGINE_CLASSES = List.of(
|
||||
"art/arcane/iris/core/gui/PregeneratorJob",
|
||||
"art/arcane/iris/core/ServerConfigurator",
|
||||
"art/arcane/iris/core/datapack/DatapackIngestService",
|
||||
"art/arcane/iris/core/events/IrisEngineHotloadEvent",
|
||||
"art/arcane/iris/core/project/IrisProject",
|
||||
"art/arcane/iris/core/tools/IrisToolbelt",
|
||||
"art/arcane/iris/platform/bukkit/",
|
||||
"org/bukkit/"
|
||||
);
|
||||
private static final List<String> BUKKIT_MODE_CLASSES = List.of(
|
||||
"art/arcane/iris/core/tools/IrisToolbelt",
|
||||
"art/arcane/iris/spi/IrisServices",
|
||||
"art/arcane/iris/util/common/scheduling/J",
|
||||
"art/arcane/iris/platform/bukkit/",
|
||||
"org/bukkit/"
|
||||
);
|
||||
private static final List<String> ENGINE_POLICY_CLASSES = List.of(
|
||||
"art/arcane/iris/core/gui/PregeneratorJob",
|
||||
"art/arcane/iris/core/tools/WorldMaintenance",
|
||||
"art/arcane/iris/platform/bukkit/",
|
||||
"org/bukkit/"
|
||||
);
|
||||
private static final List<String> PLATFORM_POLICY_CLASSES = List.of(
|
||||
"art/arcane/iris/core/gui/PregeneratorJob",
|
||||
"art/arcane/iris/core/tools/WorldMaintenance",
|
||||
"art/arcane/iris/util/common/scheduling/J",
|
||||
"art/arcane/iris/platform/bukkit/",
|
||||
"org/bukkit/"
|
||||
);
|
||||
|
||||
@Test
|
||||
public void sharedGeneratorHotPathsDoNotLinkBukkitImplementations() throws IOException {
|
||||
assertNoClassLinks(IrisEngine.class, BUKKIT_ENGINE_CLASSES);
|
||||
assertNoClassLinks(Engine.class, ENGINE_POLICY_CLASSES);
|
||||
assertNoClassLinks(EngineMode.class, PLATFORM_POLICY_CLASSES);
|
||||
assertNoClassLinks(EngineMantle.class, PLATFORM_POLICY_CLASSES);
|
||||
assertNoClassLinks(ModeOverworld.class, BUKKIT_MODE_CLASSES);
|
||||
}
|
||||
|
||||
private static void assertNoClassLinks(Class<?> type, List<String> forbiddenClasses) throws IOException {
|
||||
InputStream stream = type.getResourceAsStream(type.getSimpleName() + ".class");
|
||||
assertNotNull(stream);
|
||||
String bytecode;
|
||||
try (InputStream input = stream) {
|
||||
bytecode = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
for (String className : forbiddenClasses) {
|
||||
assertFalse(type.getName() + " -> " + className, bytecode.contains(className));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisWorldManagerMarkerTest {
|
||||
@Test
|
||||
public void worldBlockHeightIsTranslatedToMantleHeight() {
|
||||
assertEquals(64, IrisWorldManager.toMantleY(0, -64));
|
||||
assertEquals(319, IrisWorldManager.toMantleY(255, -64));
|
||||
assertEquals(42, IrisWorldManager.toMantleY(42, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.engine.framework;
|
||||
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.math.RollingSequence;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class EngineComponentTest {
|
||||
@After
|
||||
public void removeCleanup() {
|
||||
IrisServices.remove(EngineComponentCleanup.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeDelegatesToRegisteredPlatformCleanup() {
|
||||
AtomicReference<EngineComponent> released = new AtomicReference<>();
|
||||
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) released::set);
|
||||
EngineComponent component = new TestComponent();
|
||||
|
||||
component.close();
|
||||
|
||||
assertSame(component, released.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeWithoutPlatformCleanupIsSafe() {
|
||||
new TestComponent().close();
|
||||
}
|
||||
|
||||
private static final class TestComponent implements EngineComponent {
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RollingSequence getMetrics() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class EnginePlayerTest {
|
||||
@Test
|
||||
public void samplesImmediatelyThenWaitsForTimeAndMovement() {
|
||||
assertTrue(EnginePlayer.needsSample(false, 0L, 0D));
|
||||
assertFalse(EnginePlayer.needsSample(true, 56L, 81D));
|
||||
assertFalse(EnginePlayer.needsSample(true, 55L, 82D));
|
||||
assertTrue(EnginePlayer.needsSample(true, 56L, 82D));
|
||||
}
|
||||
}
|
||||
+237
-5
@@ -18,12 +18,28 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.object.StructureDistribution;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisStructureLocatorContractTest {
|
||||
@Test
|
||||
@@ -61,14 +77,230 @@ public class IrisStructureLocatorContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void locateReturnsNullForNullEngine() {
|
||||
assertNull(IrisStructureLocator.locate(null, "minecraft:village_taiga", 0, 0, 100));
|
||||
public void locateReturnsNotFoundForNullEngine() {
|
||||
assertEquals(IrisStructureLocator.LocateStatus.NOT_FOUND,
|
||||
IrisStructureLocator.locate(null, "minecraft:village_taiga", 0, 0, 100).status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void locateReturnsNullForNullOrEmptyKey() {
|
||||
public void locateReturnsNotFoundForNullOrEmptyKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertNull(IrisStructureLocator.locate(engine, null, 0, 0, 100));
|
||||
assertNull(IrisStructureLocator.locate(engine, "", 0, 0, 100));
|
||||
assertEquals(IrisStructureLocator.LocateStatus.NOT_FOUND,
|
||||
IrisStructureLocator.locate(engine, null, 0, 0, 100).status());
|
||||
assertEquals(IrisStructureLocator.LocateStatus.NOT_FOUND,
|
||||
IrisStructureLocator.locate(engine, "", 0, 0, 100).status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroDensityLocateReturnsWithoutScanningPlacementGraphs() {
|
||||
Engine engine = densityEngine(0.0, false, -64, 384, -2032, 2032);
|
||||
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, "test:density", 0, 0, 100);
|
||||
|
||||
assertEquals(IrisStructureLocator.LocateStatus.NOT_FOUND, result.status());
|
||||
verify(engine, never()).getComplex();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void impossibleDensityHeightBandReturnsWithoutScanningPlacementGraphs() {
|
||||
Engine engine = densityEngine(1.0, true, -64, 384, 500, 600);
|
||||
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, "test:density", 0, 0, 100);
|
||||
|
||||
assertEquals(IrisStructureLocator.LocateStatus.NOT_FOUND, result.status());
|
||||
verify(engine, never()).getComplex();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityLocateReportsSafetyLimitAfterFixedCandidateBudget() {
|
||||
Engine engine = densityEngine(1.0, false, -64, 384, -2032, 2032);
|
||||
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, "test:density", 0, 0, 2048);
|
||||
|
||||
assertEquals(IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, result.status());
|
||||
assertFalse(result.found());
|
||||
verify(engine, times(4_096)).getComplex();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchableDensityRequiresPositiveProbabilityAndWorldHeightOverlap() {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getHeight()).thenReturn(384);
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
placement.setDistribution(StructureDistribution.DENSITY);
|
||||
placement.setMinHeight(-32);
|
||||
placement.setMaxHeight(128);
|
||||
|
||||
placement.setDensity(Double.NaN);
|
||||
assertFalse(IrisStructureLocator.isSearchableDensityPlacement(engine, placement));
|
||||
placement.setDensity(0.0);
|
||||
assertFalse(IrisStructureLocator.isSearchableDensityPlacement(engine, placement));
|
||||
placement.setDensity(0.01);
|
||||
assertTrue(IrisStructureLocator.isSearchableDensityPlacement(engine, placement));
|
||||
placement.setMinHeight(400);
|
||||
placement.setMaxHeight(500);
|
||||
assertFalse(IrisStructureLocator.isSearchableDensityPlacement(engine, placement));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidateIsNullSafe() {
|
||||
IrisStructureLocator.invalidate(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementIndexIsScopedAndInvalidatedPerEngine() {
|
||||
IrisData sharedData = mock(IrisData.class);
|
||||
Engine firstEngine = emptyEngine(sharedData);
|
||||
Engine secondEngine = emptyEngine(sharedData);
|
||||
Set<String> firstIndex = IrisStructureLocator.placedKeys(firstEngine);
|
||||
Set<String> secondIndex = IrisStructureLocator.placedKeys(secondEngine);
|
||||
assertNotSame(firstIndex, secondIndex);
|
||||
|
||||
IrisStructureLocator.invalidate(firstEngine);
|
||||
assertNotSame(firstIndex, IrisStructureLocator.placedKeys(firstEngine));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assembledBoundsMustFitVerticallyAndHorizontally() {
|
||||
KList<PlacedStructurePiece> fitting = new KList<>();
|
||||
fitting.add(piece(-4, 1, -4, 4, 20, 4));
|
||||
assertTrue(IrisStructureLocator.fitsWorldBounds(fitting, 0, 20, 0, 0, 4));
|
||||
|
||||
KList<PlacedStructurePiece> belowWorld = new KList<>();
|
||||
belowWorld.add(piece(-4, -1, -4, 4, 20, 4));
|
||||
assertFalse(IrisStructureLocator.fitsWorldBounds(belowWorld, 0, 20, 0, 0, 4));
|
||||
|
||||
KList<PlacedStructurePiece> outsideRadius = new KList<>();
|
||||
outsideRadius.add(piece(-4, 1, -4, 5, 20, 4));
|
||||
assertFalse(IrisStructureLocator.fitsWorldBounds(outsideRadius, 0, 20, 0, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyAssemblyDoesNotFit() {
|
||||
assertFalse(IrisStructureLocator.fitsWorldBounds(new KList<>(), 0, 20, 0, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectedStructureKeyDoesNotMatchUnselectedPlacementMember() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
placement.getStructures().add("test:first");
|
||||
placement.getStructures().add("test:second");
|
||||
RNG selectsLast = new RNG(1L) {
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return bound - 1;
|
||||
}
|
||||
};
|
||||
String selectedKey = IrisStructureLocator.selectStructureKey(placement, selectsLast);
|
||||
assertEquals("test:second", selectedKey);
|
||||
|
||||
IrisStructure selected = new IrisStructure();
|
||||
selected.setLoadKey("test:second");
|
||||
selected.setVanillaSource("minecraft:second");
|
||||
IrisStructureLocator.ResolvedPlacement resolved = new IrisStructureLocator.ResolvedPlacement(
|
||||
placement, selectedKey, selected, new KList<>(), selectsLast, 0, 64, 0, false);
|
||||
assertFalse(IrisStructureLocator.matchesResolved(resolved, "test:first"));
|
||||
assertTrue(IrisStructureLocator.matchesResolved(resolved, "test:second"));
|
||||
assertTrue(IrisStructureLocator.matchesResolved(resolved, "minecraft:second"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundAssemblyIsShiftedToFitOrRejectedWhole() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
placement.setUnderground(true);
|
||||
placement.setMinHeight(-10);
|
||||
placement.setMaxHeight(10);
|
||||
|
||||
KList<PlacedStructurePiece> movable = new KList<>();
|
||||
movable.add(piece(-4, -10, -4, 4, 5, 4));
|
||||
assertEquals(Integer.valueOf(10), IrisStructureLocator.resolveVerticalShift(movable, placement, -10, 0, 319));
|
||||
|
||||
KList<PlacedStructurePiece> tooTall = new KList<>();
|
||||
tooTall.add(piece(-4, -100, -4, 4, 400, 4));
|
||||
assertNull(IrisStructureLocator.resolveVerticalShift(tooTall, placement, 0, 0, 319));
|
||||
|
||||
placement.setUnderground(false);
|
||||
KList<PlacedStructurePiece> clippedSurface = new KList<>();
|
||||
clippedSurface.add(piece(-4, 300, -4, 4, 330, 4));
|
||||
assertNull(IrisStructureLocator.resolveVerticalShift(clippedSurface, placement, 300, -64, 319));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactYOnlyCoversPlacementPathsWithResolvedAbsoluteAnchors() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
IrisStructure structure = new IrisStructure();
|
||||
structure.setPlaceMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
KList<PlacedStructurePiece> onePiece = new KList<>();
|
||||
onePiece.add(piece(-4, 1, -4, 4, 20, 4));
|
||||
assertFalse(IrisStructureLocator.hasExactY(placement, structure, onePiece));
|
||||
|
||||
structure.setPlaceMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
assertTrue(IrisStructureLocator.hasExactY(placement, structure, onePiece));
|
||||
|
||||
structure.setPlaceMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
placement.setUnderground(true);
|
||||
assertTrue(IrisStructureLocator.hasExactY(placement, structure, onePiece));
|
||||
|
||||
placement.setUnderground(false);
|
||||
KList<PlacedStructurePiece> multiplePieces = new KList<>(onePiece);
|
||||
multiplePieces.add(piece(5, 1, -4, 12, 20, 4));
|
||||
assertTrue(IrisStructureLocator.hasExactY(placement, structure, multiplePieces));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomSpreadCellRingStopUsesGeometricLowerBound() {
|
||||
assertEquals(0L, IrisStructureLocator.cellRingDistanceLowerBound(0, 32, 31, 8, 0, 0));
|
||||
assertEquals(1L, IrisStructureLocator.cellRingDistanceLowerBound(1, 32, 31, 8, 0, 0));
|
||||
assertEquals(33L, IrisStructureLocator.cellRingDistanceLowerBound(2, 32, 31, 8, 0, 0));
|
||||
assertEquals(1L, IrisStructureLocator.cellRingDistanceLowerBound(1, 32, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
private PlacedStructurePiece piece(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return new PlacedStructurePiece(null, null, 0, 0, 0, null, minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
private Engine emptyEngine(IrisData data) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getStructures()).thenReturn(new KList<>());
|
||||
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||
return engine;
|
||||
}
|
||||
|
||||
private Engine densityEngine(double density, boolean underground, int worldMin, int worldHeight,
|
||||
int placementMin, int placementMax) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisStructure structure = new IrisStructure();
|
||||
structure.setLoadKey("test:density");
|
||||
when(data.load(IrisStructure.class, "test:density", false)).thenReturn(structure);
|
||||
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
placement.getStructures().add("test:density");
|
||||
placement.setDistribution(StructureDistribution.DENSITY);
|
||||
placement.setDensity(density);
|
||||
placement.setUnderground(underground);
|
||||
placement.setMinHeight(placementMin);
|
||||
placement.setMaxHeight(placementMax);
|
||||
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
placements.add(placement);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getSeedManager()).thenReturn(new SeedManager(77L));
|
||||
when(engine.getMinHeight()).thenReturn(worldMin);
|
||||
when(engine.getHeight()).thenReturn(worldHeight);
|
||||
when(dimension.getStructures()).thenReturn(placements);
|
||||
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@
|
||||
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.StructureDistribution;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StructurePlacementGridTest {
|
||||
@@ -101,4 +105,69 @@ public class StructurePlacementGridTest {
|
||||
assertEquals(1, total);
|
||||
assertTrue(matches <= total);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityStartIsDeterministicAndOrdinalScoped() {
|
||||
IrisStructurePlacement placement = placement(StructureDistribution.DENSITY);
|
||||
placement.setDensity(0.35);
|
||||
boolean differsByOrdinal = false;
|
||||
for (int cx = -32; cx <= 32; cx++) {
|
||||
for (int cz = -32; cz <= 32; cz++) {
|
||||
boolean first = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 0);
|
||||
boolean repeated = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 0);
|
||||
boolean secondPlacement = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 1);
|
||||
assertEquals(first, repeated);
|
||||
differsByOrdinal |= first != secondPlacement;
|
||||
}
|
||||
}
|
||||
assertTrue(differsByOrdinal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityBoundariesNeverAndAlwaysStart() {
|
||||
IrisStructurePlacement placement = placement(StructureDistribution.DENSITY);
|
||||
placement.setDensity(0.0);
|
||||
assertFalse(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L, 0));
|
||||
placement.setDensity(1.0);
|
||||
assertTrue(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concentricRingCountIsFiniteTotalPlacementCount() {
|
||||
IrisStructurePlacement placement = placement(StructureDistribution.CONCENTRIC_RINGS);
|
||||
placement.setRingCount(10);
|
||||
placement.setRingSpread(3);
|
||||
placement.setRingDistance(12);
|
||||
long seed = 484848L;
|
||||
int lastRing = Math.ceilDiv(placement.getRingCount(), placement.getRingSpread());
|
||||
int scanRadius = (lastRing + 2) * placement.getRingDistance();
|
||||
int starts = 0;
|
||||
for (int cx = -scanRadius; cx <= scanRadius; cx++) {
|
||||
for (int cz = -scanRadius; cz <= scanRadius; cz++) {
|
||||
if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, 0)) {
|
||||
starts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(placement.getRingCount(), starts);
|
||||
assertNull(StructurePlacementGrid.concentricRingChunk(placement, placement.getRingCount(), seed));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementRngIsStableAndOrdinalScoped() {
|
||||
IrisStructurePlacement placement = placement(StructureDistribution.RANDOM_SPREAD);
|
||||
long first = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 0).getSeed();
|
||||
long repeated = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 0).getSeed();
|
||||
long secondPlacement = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 1).getSeed();
|
||||
assertEquals(first, repeated);
|
||||
assertNotEquals(first, secondPlacement);
|
||||
}
|
||||
|
||||
private IrisStructurePlacement placement(StructureDistribution distribution) {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
placement.setDistribution(distribution);
|
||||
placement.getStructures().add("test:structure");
|
||||
placement.setSalt(31415926);
|
||||
return placement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class StructureVerticalBoundsTest {
|
||||
@Test
|
||||
public void trialChamberShiftCannotProducePurpurPoiSectionBelowLiveWorld() {
|
||||
int worldMinY = -64;
|
||||
int worldMaxYExclusive = 320;
|
||||
int structureMinY = -48;
|
||||
int structureMaxY = -16;
|
||||
|
||||
int offset = StructureVerticalBounds.clampOffset(
|
||||
structureMinY,
|
||||
structureMaxY,
|
||||
-64,
|
||||
worldMinY,
|
||||
worldMaxYExclusive);
|
||||
|
||||
int placedMinY = structureMinY + offset;
|
||||
int placedMaxY = structureMaxY + offset;
|
||||
int placedMinSection = Math.floorDiv(placedMinY, 16);
|
||||
assertEquals(-16, offset);
|
||||
assertEquals(-64, placedMinY);
|
||||
assertEquals(-32, placedMaxY);
|
||||
assertEquals(-4, placedMinSection);
|
||||
assertTrue(placedMinSection >= Math.floorDiv(worldMinY, 16));
|
||||
assertTrue(placedMinSection <= Math.floorDiv(worldMaxYExclusive - 1, 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void offsetIsClampedAtBothLiveWorldBoundaries() {
|
||||
assertEquals(-20, StructureVerticalBounds.clampOffset(20, 40, -100, 0, 256));
|
||||
assertEquals(215, StructureVerticalBounds.clampOffset(20, 40, 300, 0, 256));
|
||||
assertEquals(0, StructureVerticalBounds.clampOffset(20, 40, 0, 0, 256));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureAlreadyOutsideRangeIsMovedInside() {
|
||||
assertEquals(48, StructureVerticalBounds.clampOffset(-112, -80, 0, -64, 320));
|
||||
assertEquals(-81, StructureVerticalBounds.clampOffset(400, 400, 0, -64, 320));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedStructureIsRejected() {
|
||||
try {
|
||||
StructureVerticalBounds.clampOffset(-100, 400, 0, -64, 320);
|
||||
fail("Oversized structure must be rejected");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue(e.getMessage().contains("exceeds writable world height"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineMantleMarkerTest {
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenReturn(block);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void markerCoordinatesIncludeWorldMinimumHeight() {
|
||||
EngineMantle engineMantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
||||
Engine engine = mock(Engine.class);
|
||||
EnginePlatformHooks platformHooks = mock(EnginePlatformHooks.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MatterMarker marker = new MatterMarker("test");
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
when(engine.getPlatformHooks()).thenReturn(platformHooks);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
doAnswer(invocation -> {
|
||||
Consumer4<Integer, Integer, Integer, MatterMarker> consumer = invocation.getArgument(3);
|
||||
consumer.accept(2, 7, 3, marker);
|
||||
return null;
|
||||
}).when(mantle).iterateChunk(eq(4), eq(-2), eq(MatterMarker.class), any());
|
||||
|
||||
KList<IrisPosition> positions = engineMantle.findMarkers(4, -2, marker);
|
||||
|
||||
assertEquals(1, positions.size());
|
||||
assertEquals(66, positions.get(0).getX());
|
||||
assertEquals(-57, positions.get(0).getY());
|
||||
assertEquals(-29, positions.get(0).getZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void platformHookCanSkipUnsafeMarkerRead() {
|
||||
EngineMantle engineMantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
||||
Engine engine = mock(Engine.class);
|
||||
EnginePlatformHooks platformHooks = mock(EnginePlatformHooks.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MatterMarker marker = new MatterMarker("test");
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
when(engine.getPlatformHooks()).thenReturn(platformHooks);
|
||||
when(platformHooks.shouldSkipMantleMarkerRead(engine, 4, -2)).thenReturn(true);
|
||||
|
||||
KList<IrisPosition> positions = engineMantle.findMarkers(4, -2, marker);
|
||||
|
||||
assertTrue(positions.isEmpty());
|
||||
verifyNoInteractions(mantle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleWriterOverlayTest {
|
||||
private static final int X = 1;
|
||||
private static final int Y = 2;
|
||||
private static final int Z = 3;
|
||||
|
||||
private MantleWriter writer;
|
||||
private Matter matter;
|
||||
private MatterSlice<PlatformBlockState> blockSlice;
|
||||
private MatterSlice<Identifier> identifierSlice;
|
||||
private MatterSlice<MatterCavern> cavernSlice;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState defaultBlock = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(registries.block(anyString())).thenReturn(defaultBlock);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
matter = mock(Matter.class);
|
||||
blockSlice = mock(MatterSlice.class);
|
||||
identifierSlice = mock(MatterSlice.class);
|
||||
cavernSlice = mock(MatterSlice.class);
|
||||
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
when(mantle.getChunk(0, 0)).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
when(chunk.getOrCreate(0)).thenReturn(matter);
|
||||
when(matter.hasSlice(PlatformBlockState.class)).thenReturn(true);
|
||||
when(matter.hasSlice(Identifier.class)).thenReturn(true);
|
||||
when(matter.getSlice(PlatformBlockState.class)).thenReturn(blockSlice);
|
||||
when(matter.getSlice(Identifier.class)).thenReturn(identifierSlice);
|
||||
doReturn(blockSlice).when(matter).slice(PlatformBlockState.class);
|
||||
doReturn(cavernSlice).when(matter).slice(MatterCavern.class);
|
||||
|
||||
writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingCavernStillClearsBlockAndDeferredPlacement() {
|
||||
MatterCavern existing = new MatterCavern(true, "", (byte) 3);
|
||||
MatterCavern requested = new MatterCavern(true, "", (byte) 3);
|
||||
when(cavernSlice.get(X, Y, Z)).thenReturn(existing);
|
||||
|
||||
assertFalse(writer.carveDataIfAbsent(X, Y, Z, requested));
|
||||
|
||||
verify(blockSlice).set(X, Y, Z, null);
|
||||
verify(identifierSlice).set(X, Y, Z, null);
|
||||
verify(cavernSlice, never()).set(X, Y, Z, requested);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentCavernClearsOverlaysAndWritesMask() {
|
||||
MatterCavern requested = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
assertTrue(writer.carveDataIfAbsent(X, Y, Z, requested));
|
||||
|
||||
verify(blockSlice).set(X, Y, Z, null);
|
||||
verify(identifierSlice).set(X, Y, Z, null);
|
||||
verify(cavernSlice).set(X, Y, Z, requested);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalBlockReplacementClearsDeferredPlacement() {
|
||||
PlatformBlockState replacement = mock(PlatformBlockState.class);
|
||||
|
||||
writer.setData(X, Y, Z, replacement);
|
||||
|
||||
verify(identifierSlice).set(X, Y, Z, null);
|
||||
verify(blockSlice).set(X, Y, Z, replacement);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStructureComponentOverboreTest {
|
||||
@Test
|
||||
public void exactPieceInteriorAlwaysCarves() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 0.0));
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.0, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowNoiseRemovesTheFormerFlatShell() {
|
||||
double oneBlockAtRadiusSix = 1.0 / 6.0;
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(
|
||||
oneBlockAtRadiusSix * oneBlockAtRadiusSix, 0.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.25 * 0.25, 0.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maximumNoiseStopsAtConfiguredReach() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(1.000001, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalCornersUseEuclideanFalloff() {
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(1.0, 1.0));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.8 * 0.8 + 0.8 * 0.8, 1.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundaryNoiseIsClampedAndDeterministic() {
|
||||
assertEquals(0.2, IrisStructureComponent.overboreBoundaryLimit(-10.0), 0.0);
|
||||
assertEquals(1.0, IrisStructureComponent.overboreBoundaryLimit(10.0), 0.0);
|
||||
assertTrue(IrisStructureComponent.shouldCarveOverboreCell(0.36, 0.5));
|
||||
assertFalse(IrisStructureComponent.shouldCarveOverboreCell(0.360001, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void candidateExtensionsDoNotScanImpossibleOuterShell() {
|
||||
assertEquals(6, IrisStructureComponent.overboreSideExtension(6));
|
||||
assertEquals(18, IrisStructureComponent.overboreUpExtension(10.0));
|
||||
|
||||
long oldVolume = (41L + 18L) * (31L + 27L) * (35L + 18L);
|
||||
long newVolume = (41L + 12L) * (31L + 18L) * (35L + 12L);
|
||||
assertTrue(newVolume < oldVolume);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryMarkersAreSeparatedFromStructureMarkers() {
|
||||
assertTrue(IrisStructureComponent.isOrdinaryObjectMarker("trees/oak@42"));
|
||||
assertFalse(IrisStructureComponent.isOrdinaryObjectMarker(
|
||||
"@iris-structure:v1:dHJlZXMvb2Fr:42:bWluZWNyYWZ0Om1hbnNpb24"));
|
||||
assertFalse(IrisStructureComponent.isOrdinaryObjectMarker("not-a-marker"));
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class MantleObjectComponentCaveExposureTest {
|
||||
|
||||
@Test
|
||||
public void test_resolveSurfaceObjectExclusionRadius_largeObject_coversFullFootprint() {
|
||||
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(24, 0, 0);
|
||||
assertTrue("Expected radius >= 12 for a 24x24 object but got " + radius, radius >= 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_resolveSurfaceObjectExclusionRadius_withTranslateOffset_includesOffset() {
|
||||
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(16, 5, 3);
|
||||
int expected = 8 + 5 + 3 + 1;
|
||||
assertTrue("Expected radius >= " + expected + " for 16x16 object with translate offsets 5+3 but got " + radius, radius >= expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_resolveSurfaceObjectExclusionRadius_smallObject_atLeastOne() {
|
||||
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(2, 0, 0);
|
||||
assertTrue("Expected radius >= 1 for a 2x2 object but got " + radius, radius >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_resolveSurfaceObjectExclusionRadius_nullLike_returnsOne() {
|
||||
int radius = MantleObjectComponent.computeSurfaceExclusionRadius(0, 0, 0);
|
||||
assertTrue("Expected radius >= 1 for zero-dimension object but got " + radius, radius >= 1);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDimensionReachableBiomesTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void includesOnlyBiomesReachableThroughSelectedRegions() {
|
||||
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("reachable", "missing"));
|
||||
IrisRegion reachable = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("parent", "shared"))
|
||||
.setSeaBiomes(new KList<>("shared"));
|
||||
IrisBiome parent = biome("parent").setChildren(new KList<>("child", "shared")).setCarvingBiome("carve");
|
||||
IrisBiome child = biome("child").setChildren(new KList<>("parent"));
|
||||
IrisBiome shared = biome("shared");
|
||||
IrisBiome carve = biome("carve");
|
||||
IrisBiome unused = biome("unused");
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("reachable")).thenReturn(reachable);
|
||||
when(biomeLoader.load("parent")).thenReturn(parent);
|
||||
when(biomeLoader.load("child")).thenReturn(child);
|
||||
when(biomeLoader.load("shared")).thenReturn(shared);
|
||||
when(biomeLoader.load("carve")).thenReturn(carve);
|
||||
when(biomeLoader.load("unused")).thenReturn(unused);
|
||||
|
||||
KList<IrisBiome> biomes = dimension.getReachableBiomes(() -> data);
|
||||
Set<String> keys = biomes.stream().map(IrisBiome::getLoadKey).collect(Collectors.toSet());
|
||||
|
||||
assertEquals(Set.of("parent", "child", "shared", "carve"), keys);
|
||||
assertEquals(keys.size(), biomes.size());
|
||||
}
|
||||
|
||||
private IrisBiome biome(String loadKey) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(loadKey);
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class IrisDimensionRuntimeContractTest {
|
||||
@Test
|
||||
public void exactTypeAndHeightContractPasses() {
|
||||
IrisDimensionRuntimeContract expected = expectedContract();
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract("iris:overworld", -256, 768, 512);
|
||||
|
||||
expected.requireExact("test world", actual);
|
||||
expected.requireHeight("test world", -256, 768);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tallPackOnVanillaHeightWorldIsRejected() {
|
||||
IrisDimensionRuntimeContract expected = expectedContract();
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract("minecraft:overworld", -64, 384, 384);
|
||||
|
||||
try {
|
||||
expected.requireExact("Bukkit world 'world'", actual);
|
||||
fail("Vanilla height world must be rejected");
|
||||
} catch (IrisDimensionContractException e) {
|
||||
assertTrue(e.getMessage().contains("Bukkit world 'world'"));
|
||||
assertTrue(e.getMessage().contains("iris:overworld"));
|
||||
assertTrue(e.getMessage().contains("-256..512"));
|
||||
assertTrue(e.getMessage().contains("minecraft:overworld"));
|
||||
assertTrue(e.getMessage().contains("-64..320"));
|
||||
assertTrue(e.getMessage().contains("Generation was refused"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrongIrisTypeKeyIsRejectedEvenWhenHeightMatches() {
|
||||
IrisDimensionRuntimeContract expected = expectedContract();
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract("iris:other", -256, 768, 512);
|
||||
|
||||
try {
|
||||
expected.requireExact("test world", actual);
|
||||
fail("Wrong Iris dimension type must be rejected");
|
||||
} catch (IrisDimensionContractException e) {
|
||||
assertTrue(e.getMessage().contains("iris:other"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logicalHeightMismatchIsRejected() {
|
||||
IrisDimensionRuntimeContract expected = expectedContract();
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract("iris:overworld", -256, 768, 384);
|
||||
|
||||
try {
|
||||
expected.requireExact("test world", actual);
|
||||
fail("Wrong logical height must be rejected");
|
||||
} catch (IrisDimensionContractException e) {
|
||||
assertTrue(e.getMessage().contains("logical height 384"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadMismatchReportsReplacementAgainstLiveRuntime() {
|
||||
IrisDimension active = new IrisDimension();
|
||||
active.setLoadKey("overworld");
|
||||
active.setDimensionHeight(new IrisRange(-256, 512));
|
||||
active.setLogicalHeight(512);
|
||||
IrisDimension replacement = new IrisDimension();
|
||||
replacement.setLoadKey("overworld");
|
||||
replacement.setDimensionHeight(new IrisRange(-384, 640));
|
||||
replacement.setLogicalHeight(640);
|
||||
|
||||
try {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
|
||||
fail("A hotload cannot replace the live vertical contract");
|
||||
} catch (IrisDimensionContractException e) {
|
||||
assertTrue(e.getMessage().contains("requires Iris dimension type iris:overworld with range -384..640"));
|
||||
assertTrue(e.getMessage().contains("loaded runtime uses iris:overworld with range -256..512"));
|
||||
}
|
||||
}
|
||||
|
||||
private IrisDimensionRuntimeContract expectedContract() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("overworld");
|
||||
dimension.setDimensionHeight(new IrisRange(-256, 512));
|
||||
dimension.setLogicalHeight(512);
|
||||
return IrisDimensionRuntimeContract.expected(dimension, "iris");
|
||||
}
|
||||
}
|
||||
+106
-21
@@ -21,7 +21,10 @@ package art.arcane.iris.engine.object;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisImportedStructureControlTest {
|
||||
@@ -87,31 +90,23 @@ public class IrisImportedStructureControlTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customModeBehavesLikeWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.CUSTOM)
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesFalseBlocksDatapackKeysWhileAllOn() {
|
||||
public void datapackOverridesFalseDoesNotDisableModOrDatapackNamespaces() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDatapackOverrides(false);
|
||||
assertFalse(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertFalse(control.shouldGenerate("aquaculture:treasure_vault"));
|
||||
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertTrue(control.shouldGenerate("aquaculture:treasure_vault"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesFalseBlocksDatapackKeyEvenWhenWhitelisted() {
|
||||
public void allOffWhitelistStillControlsThirdPartyStructuresWhenOverridesAreFalse() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("nova_structures:desert_temple", "minecraft:village_plains"))
|
||||
.setDatapackOverrides(false);
|
||||
assertFalse(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate("aquaculture:treasure_vault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -155,13 +150,6 @@ public class IrisImportedStructureControlTest {
|
||||
assertTrue(control.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeFalseWhenCustomWithoutWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.CUSTOM);
|
||||
assertFalse(control.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyOrNullListEntriesNeverMatchEveryKey() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
@@ -177,4 +165,101 @@ public class IrisImportedStructureControlTest {
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalShiftDefaultsToZeroAndAppliesUndergroundBaseOnlyUnderground() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl().setUndergroundYShift(-32);
|
||||
|
||||
assertEquals(0, control.resolve("minecraft:village_plains", false).yShift());
|
||||
assertEquals(-32, control.resolve("minecraft:stronghold", true).yShift());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingVerticalShiftsStackWithoutAffectingOtherStructures() {
|
||||
IrisVanillaStructureAdjustment broad = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:trial"))
|
||||
.setYShift(-48);
|
||||
IrisVanillaStructureAdjustment exact = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:trial_chambers"))
|
||||
.setYShift(-16);
|
||||
KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
|
||||
adjustments.add(broad);
|
||||
adjustments.add(exact);
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setUndergroundYShift(8)
|
||||
.setAdjustments(adjustments);
|
||||
|
||||
assertEquals(-56, control.resolve("minecraft:trial_chambers", true).yShift());
|
||||
assertEquals(8, control.resolve("minecraft:stronghold", true).yShift());
|
||||
assertEquals(0, control.resolve("minecraft:village_plains", false).yShift());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postprocessingDefaultsAreDisabled() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
|
||||
assertNull(control.resolve("minecraft:village_plains", false).stilt());
|
||||
assertFalse(control.resolve(null, false).clearVegetation());
|
||||
assertNull(control.resolve(null, false).stilt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postprocessingUsesPrefixMatches() {
|
||||
IrisVanillaStructureStiltSettings stilt = new IrisVanillaStructureStiltSettings();
|
||||
IrisVanillaStructureAdjustment adjustment = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village"))
|
||||
.setClearVegetation(true)
|
||||
.setStilt(stilt);
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setAdjustments(new KList<IrisVanillaStructureAdjustment>().qadd(adjustment));
|
||||
|
||||
assertTrue(control.resolve("minecraft:village_plains", false).clearVegetation());
|
||||
assertSame(stilt, control.resolve("minecraft:village_taiga", false).stilt());
|
||||
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
|
||||
assertNull(control.resolve("minecraft:stronghold", true).stilt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleMatchesMergeVegetationAndUseLastConfiguredStilt() {
|
||||
IrisVanillaStructureStiltSettings broadStilt = new IrisVanillaStructureStiltSettings().setMaxDepth(32);
|
||||
IrisVanillaStructureStiltSettings specificStilt = new IrisVanillaStructureStiltSettings().setMaxDepth(96);
|
||||
IrisVanillaStructureAdjustment broad = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village"))
|
||||
.setClearVegetation(true)
|
||||
.setStilt(broadStilt);
|
||||
IrisVanillaStructureAdjustment exactWithoutStilt = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village_plains"));
|
||||
IrisVanillaStructureAdjustment exactWithStilt = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village_plains"))
|
||||
.setStilt(specificStilt);
|
||||
KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
|
||||
adjustments.add(broad);
|
||||
adjustments.add(exactWithoutStilt);
|
||||
adjustments.add(exactWithStilt);
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl().setAdjustments(adjustments);
|
||||
|
||||
IrisNativeStructureDecision plains = control.resolve("minecraft:village_plains", false);
|
||||
assertTrue(plains.clearVegetation());
|
||||
assertSame(specificStilt, plains.stilt());
|
||||
assertEquals(96, plains.stilt().getMaxDepth());
|
||||
assertSame(broadStilt, control.resolve("minecraft:village_desert", false).stilt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void familyPrefixRequiresAResourceBoundary() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDisabled(keys("minecraft:village"));
|
||||
|
||||
assertFalse(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:villager_outpost"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void namespacePrefixMatchesEveryKeyInNamespace() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDisabled(keys("example:"));
|
||||
|
||||
assertFalse(control.shouldGenerate("example:castle"));
|
||||
assertTrue(control.shouldGenerate("other:castle"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class IrisPlatformIsolationTest {
|
||||
@Test
|
||||
public void irisWorldHasNoBukkitSignatures() {
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.platformIdentity("minecraft:overworld")
|
||||
.name("overworld")
|
||||
.seed(1337L)
|
||||
.minHeight(-64)
|
||||
.maxHeight(320)
|
||||
.build();
|
||||
|
||||
assertEquals("minecraft:overworld", world.identity());
|
||||
assertEquals(1337L, world.getRawWorldSeed());
|
||||
assertEquals(384, world.getHeight());
|
||||
assertNoBukkitSignatures(IrisWorld.class);
|
||||
assertNoBukkitSignatures(IrisWorld.IrisWorldBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void irisDimensionEnvironmentAndHashCodeArePlatformNeutral() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
|
||||
assertEquals(IrisEnvironment.NORMAL, dimension.getEnvironment());
|
||||
dimension.hashCode();
|
||||
assertNoBukkitSignatures(IrisDimension.class);
|
||||
}
|
||||
|
||||
private static void assertNoBukkitSignatures(Class<?> type) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
assertPlatformNeutral(field.getType());
|
||||
}
|
||||
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
|
||||
for (Class<?> parameter : constructor.getParameterTypes()) {
|
||||
assertPlatformNeutral(parameter);
|
||||
}
|
||||
}
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
assertPlatformNeutral(method.getReturnType());
|
||||
for (Class<?> parameter : method.getParameterTypes()) {
|
||||
assertPlatformNeutral(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPlatformNeutral(Class<?> type) {
|
||||
Class<?> component = type;
|
||||
while (component.isArray()) {
|
||||
component = component.getComponentType();
|
||||
}
|
||||
assertFalse(component.getName(), component.getName().startsWith("org.bukkit."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSurfaceOpeningTest {
|
||||
@Test
|
||||
public void carvedSurfaceCellRejectsPlacement() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
placer.carve(1, 80, 1);
|
||||
|
||||
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0));
|
||||
assertEquals(9, placer.heightQueries());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sealedShallowCaveDoesNotRejectPlacement() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
placer.carve(0, 79, 0);
|
||||
placer.carve(0, 78, 0);
|
||||
placer.carve(0, 77, 0);
|
||||
|
||||
assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everySupportColumnUsesItsOwnSurfaceHeight() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
placer.height(dx, dz, 80 + dx + dz);
|
||||
}
|
||||
}
|
||||
placer.carve(1, 82, 1);
|
||||
|
||||
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openingOutsideSupportStencilDoesNotRejectPlacement() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
placer.carve(2, 80, 0);
|
||||
|
||||
assertFalse(IrisSurfaceOpening.isOpen(placer, null, 0, 0, null, null, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementTranslationMovesSupportStencil() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
IrisObjectTranslate translate = new IrisObjectTranslate().setX(5).setZ(-3);
|
||||
placer.carve(5, 80, -3);
|
||||
|
||||
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, null, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementTranslationUsesTheSameRotationAsObjectBlocks() {
|
||||
SurfacePlacer placer = new SurfacePlacer(80);
|
||||
IrisObjectTranslate translate = new IrisObjectTranslate().setX(5).setZ(-3);
|
||||
IrisObjectRotation rotation = IrisObjectRotation.of(0, 90, 0);
|
||||
placer.carve(-3, 80, -5);
|
||||
|
||||
assertTrue(IrisSurfaceOpening.isOpen(placer, null, 0, 0, translate, rotation, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void treeBlockClassificationDoesNotMatchUnrelatedObjects() {
|
||||
PlatformBlockState stone = mock(PlatformBlockState.class);
|
||||
PlatformBlockState log = mock(PlatformBlockState.class);
|
||||
when(log.isTreeBlock()).thenReturn(true);
|
||||
|
||||
assertFalse(IrisSurfaceOpening.containsTreeBlocks(List.of(stone)));
|
||||
assertTrue(IrisSurfaceOpening.containsTreeBlocks(List.of(stone, log)));
|
||||
}
|
||||
|
||||
private static final class SurfacePlacer implements IObjectPlacer {
|
||||
private final int defaultHeight;
|
||||
private final Map<String, Integer> heights = new HashMap<>();
|
||||
private final Set<String> carved = new HashSet<>();
|
||||
private int heightQueries;
|
||||
|
||||
private SurfacePlacer(int defaultHeight) {
|
||||
this.defaultHeight = defaultHeight;
|
||||
}
|
||||
|
||||
private void height(int x, int z, int y) {
|
||||
heights.put(columnKey(x, z), y);
|
||||
}
|
||||
|
||||
private void carve(int x, int y, int z) {
|
||||
carved.add(blockKey(x, y, z));
|
||||
}
|
||||
|
||||
private int heightQueries() {
|
||||
return heightQueries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return getHighest(x, z, data, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
heightQueries++;
|
||||
return heights.getOrDefault(columnKey(x, z), defaultHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return carved.contains(blockKey(x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int x, int y, int z, T data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String columnKey(int x, int z) {
|
||||
return x + ":" + z;
|
||||
}
|
||||
|
||||
private static String blockKey(int x, int y, int z) {
|
||||
return x + ":" + y + ":" + z;
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class IrisVanillaStructureStiltSettingsTest {
|
||||
@Test
|
||||
public void defaultsToCobblestoneFoundation() {
|
||||
IrisVanillaStructureStiltSettings settings = new IrisVanillaStructureStiltSettings();
|
||||
|
||||
assertEquals(64, settings.getMaxDepth());
|
||||
assertNotNull(settings.getPalette());
|
||||
assertEquals(1, settings.getPalette().getPalette().size());
|
||||
assertEquals("minecraft:cobblestone", settings.getPalette().getPalette().get(0).getBlock());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maxDepthSchemaIsBoundedToTheMaximumWorldHeight() throws NoSuchFieldException {
|
||||
Field maxDepth = IrisVanillaStructureStiltSettings.class.getDeclaredField("maxDepth");
|
||||
MinNumber minimum = maxDepth.getAnnotation(MinNumber.class);
|
||||
MaxNumber maximum = maxDepth.getAnnotation(MaxNumber.class);
|
||||
|
||||
assertNotNull(minimum);
|
||||
assertNotNull(maximum);
|
||||
assertEquals(1.0, minimum.value(), 0.0);
|
||||
assertEquals(4064.0, maximum.value(), 0.0);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisWorldIdentityTest {
|
||||
@Test
|
||||
public void usesBukkitKeyBeforePlatformIdentity() {
|
||||
public void usesPlatformIdentity() {
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.key(new NamespacedKey("iris", "bukkit"))
|
||||
.platformIdentity("iris:modded")
|
||||
.platformIdentity("iris:bukkit")
|
||||
.build();
|
||||
|
||||
assertEquals("iris:bukkit", world.identity());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesPlatformIdentityWithoutBukkitKey() {
|
||||
public void preservesModdedIdentity() {
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.platformIdentity("minecraft:the_nether")
|
||||
.build();
|
||||
|
||||
Reference in New Issue
Block a user