- Thsi fixes an integration between Itemsadder and Oraxen.
- Waiting on oraxen folks
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-07 19:22:59 -04:00
1 parent 894cef73a6
commit d3245aa1c2
61 files changed
+3497 -160

No files matched your search

@@ -382,21 +382,24 @@ public final class ContentGate {
return merged.isEmpty() ? Map.of() : Collections.unmodifiableMap(merged);
}
/** {@code namespace:name[props]} lowercased with the default namespace applied; null for blank input. */
/** Normalizes block names and vanilla properties, preserving external property case; null for blank input. */
public static String normalizeState(String state) {
if (state == null) {
return null;
}
String trimmed = state.trim().toLowerCase(Locale.ROOT);
String trimmed = state.trim();
if (trimmed.isEmpty()) {
return null;
}
int props = trimmed.indexOf('[');
String base = props < 0 ? trimmed : trimmed.substring(0, props);
String base = (props < 0 ? trimmed : trimmed.substring(0, props)).toLowerCase(Locale.ROOT);
String rest = props < 0 ? "" : trimmed.substring(props);
if (base.indexOf(':') < 0) {
base = DEFAULT_NAMESPACE + ':' + base;
}
if (base.startsWith(DEFAULT_NAMESPACE + ':')) {
rest = rest.toLowerCase(Locale.ROOT);
}
return base + rest;
}
@@ -3,12 +3,15 @@ package art.arcane.iris.core.link.data;
import art.arcane.iris.spi.IrisLogging;
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.iris.spi.IrisServices;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.data.IrisCustomData;
import dev.lone.itemsadder.api.CustomBlock;
import dev.lone.itemsadder.api.CustomStack;
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
import dev.lone.itemsadder.api.ItemsAdder;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.EventHandler;
@@ -24,8 +27,7 @@ import java.util.stream.Collectors;
public class ItemAdderDataProvider extends ExternalDataProvider implements Listener {
private volatile Set<String> itemNamespaces = Set.of();
private volatile Set<String> blockNamespaces = Set.of();
private volatile ContentRegistry content = new ContentRegistry(Set.of(), Set.of(), Set.of(), false);
public ItemAdderDataProvider() {
super("ItemsAdder");
@@ -33,22 +35,42 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
@Override
public void init() {
updateNamespaces();
if (ItemsAdder.areItemsLoaded()) {
updateContent();
}
}
@Override
public boolean isReady() {
return super.isReady() && content.ready();
}
@EventHandler
public void onLoadData(ItemsAdderLoadDataEvent event) {
updateNamespaces();
if (!updateContent()) {
return;
}
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
if (service != null) {
service.notifyContentChanged();
}
}
@NotNull
@Override
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
if (!state.isEmpty()) {
throw new MissingResourceException("ItemsAdder blocks do not expose Iris block properties.", blockId.namespace(), blockId.key());
}
CustomBlock block = CustomBlock.getInstance(blockId.toString());
if (block == null) {
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
return IrisCustomData.of(block.getBaseBlockData(), blockId);
BlockData base = block.getBaseBlockData();
if (base == null) {
throw new MissingResourceException("Failed to find ItemsAdder block data.", blockId.namespace(), blockId.key());
}
return IrisCustomData.of(base, blockId);
}
@NotNull
@@ -63,46 +85,42 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
CustomBlock custom;
if ((custom = CustomBlock.place(blockId.toString(), block.getLocation())) == null)
return;
block.setBlockData(custom.getBaseBlockData(), false);
if (CustomBlock.place(blockId.toString(), block.getLocation()) == null) {
throw new MissingResourceException("Failed to place ItemsAdder block.", blockId.namespace(), blockId.key());
}
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
return switch (dataType) {
case ENTITY -> List.of();
case ITEM -> CustomStack.getNamespacedIdsInRegistry()
.stream()
.map(Identifier::fromString)
.toList();
case BLOCK -> CustomBlock.getNamespacedIdsInRegistry()
.stream()
.map(Identifier::fromString)
.toList();
case ITEM -> content.items();
case BLOCK -> content.blocks();
};
}
private void updateNamespaces() {
try {
updateNamespaces(DataType.ITEM);
updateNamespaces(DataType.BLOCK);
} catch (Throwable e) {
IrisLogging.warn("Failed to update ItemAdder namespaces: " + e.getMessage());
}
}
private void updateNamespaces(DataType dataType) {
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);
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
if (dataType == DataType.ENTITY) return false;
return dataType == DataType.ITEM ? itemNamespaces.contains(id.namespace()) : blockNamespaces.contains(id.namespace());
return dataType == DataType.ITEM ? content.itemNamespaces().contains(id.namespace()) : content.blocks().contains(id);
}
private boolean updateContent() {
try {
Set<Identifier> items = CustomStack.getNamespacedIdsInRegistry().stream()
.map(Identifier::fromString).collect(Collectors.toUnmodifiableSet());
Set<Identifier> blocks = CustomBlock.getNamespacedIdsInRegistry().stream()
.map(Identifier::fromString).collect(Collectors.toUnmodifiableSet());
Set<String> itemNamespaces = items.stream().map(Identifier::namespace).collect(Collectors.toUnmodifiableSet());
content = new ContentRegistry(items, blocks, itemNamespaces, true);
IrisLogging.debug("Updated ItemsAdder content registry: " + items.size() + " items, " + blocks.size() + " blocks");
return true;
} catch (RuntimeException | LinkageError e) {
IrisLogging.reportError("Failed to update ItemsAdder content registry.", e);
return false;
}
}
private record ContentRegistry(Set<Identifier> items, Set<Identifier> blocks, Set<String> itemNamespaces, boolean ready) {
}
}
@@ -71,7 +71,9 @@ public class KGeneratorsDataProvider extends ExternalDataProvider {
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
if (dataType == DataType.ENTITY) return false;
return "kgenerators".equalsIgnoreCase(id.namespace());
if (dataType == DataType.ENTITY || !"kgenerators".equalsIgnoreCase(id.namespace())) {
return false;
}
return dataType == DataType.ITEM || Main.getGenerators().get(id.key()) != null;
}
}
@@ -116,8 +116,20 @@ public class MMOItemsDataProvider extends ExternalDataProvider {
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
if (dataType == DataType.ENTITY) return false;
return dataType == DataType.ITEM ? id.namespace().split("_", 2).length == 2 : id.namespace().equals("mmoitems");
if (dataType == DataType.ENTITY) {
return false;
}
if (dataType == DataType.ITEM) {
return id.namespace().split("_", 2).length == 2;
}
if (!id.namespace().equals("mmoitems")) {
return false;
}
try {
return api().getCustomBlocks().getBlock(Integer.parseInt(id.key())) != null;
} catch (NumberFormatException ignored) {
return false;
}
}
private MMOItems api() {
@@ -138,7 +138,13 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
@Override
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
if (dataType == DataType.ENTITY) return false;
return key.namespace().equalsIgnoreCase("crucible");
if (dataType == DataType.ENTITY || !key.namespace().equalsIgnoreCase("crucible")) {
return false;
}
if (dataType == DataType.ITEM) {
return true;
}
CrucibleItem item = itemManager.getItem(key.key()).orElse(null);
return item != null && (item.getBlockData() != null || item.getFurnitureData() != null);
}
}
@@ -140,7 +140,13 @@ public class NexoDataProvider extends ExternalDataProvider {
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
if (dataType == DataType.ENTITY) return false;
return "nexo".equalsIgnoreCase(id.namespace());
if (!"nexo".equalsIgnoreCase(id.namespace())) {
return false;
}
return switch (dataType) {
case BLOCK -> NexoBlocks.isCustomBlock(id.key()) || NexoFurniture.isFurniture(id.key());
case ITEM -> NexoItems.exists(id.key());
case ENTITY -> false;
};
}
}
@@ -0,0 +1,90 @@
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.service.ExternalDataSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.collection.KMap;
import io.th0rgal.oraxen.api.OraxenBlocks;
import io.th0rgal.oraxen.api.OraxenItems;
import io.th0rgal.oraxen.api.events.OraxenItemsLoadedEvent;
import io.th0rgal.oraxen.items.ItemBuilder;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
public final class OraxenDataProvider extends ExternalDataProvider implements Listener {
public OraxenDataProvider() {
super("Oraxen");
}
@Override
public void init() {
}
@EventHandler
public void onItemsLoaded(OraxenItemsLoadedEvent event) {
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
if (service != null) {
service.notifyContentChanged();
}
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) {
if (!state.isEmpty()) {
throw new MissingResourceException("Oraxen blocks do not expose Iris block properties.", blockId.namespace(), blockId.key());
}
BlockData data = OraxenBlocks.getOraxenBlockData(blockId.key());
if (data == null) {
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
return IrisCustomData.of(data, blockId);
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) {
ItemBuilder builder = OraxenItems.getItemById(itemId.key());
if (builder == null) {
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
}
return builder.build();
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
OraxenBlocks.place(blockId.key(), block.getLocation());
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
if (dataType == DataType.ENTITY) {
return List.of();
}
return OraxenItems.getNames().stream()
.filter(id -> dataType == DataType.ITEM || OraxenBlocks.isOraxenBlock(id))
.map(id -> new Identifier("oraxen", id))
.toList();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
if (!"oraxen".equalsIgnoreCase(id.namespace())) {
return false;
}
return switch (dataType) {
case BLOCK -> OraxenBlocks.isOraxenBlock(id.key());
case ITEM -> OraxenItems.exists(id.key());
case ENTITY -> false;
};
}
}
@@ -198,6 +198,21 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
}
public static void invalidateLoadedContentRegistries(File source) {
Path requested = dataFolderIdentity(Objects.requireNonNull(source, "authoring pack source"));
for (Map.Entry<File, IrisData> entry : dataLoaders.entrySet()) {
IrisData data = entry.getValue();
if (dataFolderIdentity(entry.getKey()).equals(requested)) {
synchronized (data) {
if (data.generationRegistryContract == null && data.getEngines().isEmpty()) {
data.invalidateAuthoringResources();
data.compatReport.clear();
}
}
}
}
}
private synchronized void invalidateAuthoringResources() {
if (generationRegistryContract != null) {
throw new IllegalStateException("Cannot invalidate an immutable generation pack as an authoring source.");
@@ -88,7 +88,7 @@ public final class ContentKeyValidator {
return List.of();
}
List<String> blockKeys = normalizeKeyList(registries.blockKeys());
List<String> blockKeys = normalizeKeyList(registries.blockTypeKeys());
List<String> itemKeys = normalizeKeyList(registries.itemKeys());
List<String> entityKeys = normalizeKeyList(registries.entityKeys());
@@ -432,7 +432,7 @@ public final class ContentKeyValidator {
if (registries == null) {
return ContentKeyIssues.none();
}
List<String> blockKeys = registries.blockKeys();
List<String> blockKeys = registries.blockTypeKeys();
List<String> itemKeys = registries.itemKeys();
List<String> entityKeys = registries.entityKeys();
if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) {
@@ -57,6 +57,7 @@ public final class PackValidationCache {
PlatformRegistries registries = Objects.requireNonNull(
platform.registries(), "Pack validation platform registries");
updateSorted(digest, registries.blockKeys());
updateSorted(digest, registries.blockTypeKeys());
updateSorted(digest, registries.biomeKeys());
updateSorted(digest, registries.itemKeys());
updateSorted(digest, registries.entityKeys());
@@ -314,6 +314,10 @@ public class SchemaBuilder {
return a;
}
private static String definitionReference(String key) {
return "#/definitions/" + key.replace("~", "~0").replace("/", "~1");
}
private static String stripNamespace(String key) {
int idx = key.indexOf(':');
return idx < 0 ? key : key.substring(idx + 1);
@@ -566,12 +570,12 @@ public class SchemaBuilder {
.put("properties", new JSONObject()
.put(blockType, new JSONObject()
.put("type", "string")
.put("$ref", "#/definitions/" + enumKey))))
.put("$ref", definitionReference(enumKey)))))
.put("then", new JSONObject()
.put("properties", new JSONObject()
.put(k.getName(), new JSONObject()
.put("type", "object")
.put("$ref", "#/definitions/" + propertiesKey))))
.put("$ref", definitionReference(propertiesKey)))))
.put("else", false));
if (!definitions.containsKey(enumKey)) {
@@ -26,7 +26,7 @@ import art.arcane.iris.core.link.data.DataType;
import art.arcane.iris.core.nms.container.BlockProperty;
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.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.plugin.IrisService;
import org.bukkit.Bukkit;
@@ -41,10 +41,16 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.util.Collection;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
public class ExternalDataSVC implements IrisService {
@@ -52,6 +58,7 @@ public class ExternalDataSVC implements IrisService {
private static final List<ProviderDefinition> BUILT_IN_PROVIDERS = List.of(
new ProviderDefinition("CraftEngine", PROVIDER_PACKAGE + "CraftEngineDataProvider"),
new ProviderDefinition("Nexo", PROVIDER_PACKAGE + "NexoDataProvider"),
new ProviderDefinition("Oraxen", PROVIDER_PACKAGE + "OraxenDataProvider"),
new ProviderDefinition("ItemsAdder", PROVIDER_PACKAGE + "ItemAdderDataProvider"),
new ProviderDefinition("ExecutableItems", PROVIDER_PACKAGE + "ExecutableItemsDataProvider"),
new ProviderDefinition("MMOItems", PROVIDER_PACKAGE + "MMOItemsDataProvider"),
@@ -60,9 +67,13 @@ public class ExternalDataSVC implements IrisService {
new ProviderDefinition("MythicCrucible", PROVIDER_PACKAGE + "MythicCrucibleDataProvider"),
new ProviderDefinition("KGenerators", PROVIDER_PACKAGE + "KGeneratorsDataProvider")
);
private static final Set<String> BLOCK_PROVIDER_PLUGINS = Set.of(
"craftengine", "nexo", "oraxen", "itemsadder", "mmoitems", "mythiccrucible", "kgenerators");
private final KList<ExternalDataProvider> providers = new KList<>();
private final KList<ExternalDataProvider> activeProviders = new KList<>();
private final List<ExternalDataProvider> providers = new CopyOnWriteArrayList<>();
private final List<ExternalDataProvider> activeProviders = new CopyOnWriteArrayList<>();
private final AtomicLong contentRevision = new AtomicLong();
private volatile Runnable contentChangeListener;
@Override
public void onEnable() {
@@ -77,10 +88,67 @@ public class ExternalDataSVC implements IrisService {
@Override
public void onDisable() {
contentChangeListener = null;
activeProviders.clear();
providers.clear();
}
public void setContentChangeListener(Runnable listener) {
contentChangeListener = Objects.requireNonNull(listener);
}
public void notifyContentChanged() {
contentRevision.incrementAndGet();
Runnable listener = contentChangeListener;
if (listener != null) {
listener.run();
}
}
public long contentRevision() {
return contentRevision.get();
}
public boolean hasPendingBlockProvider(String blockKey) {
Identifier identifier = Identifier.fromString(blockKey);
if ("minecraft".equals(identifier.namespace())) {
return false;
}
String namespace = identifier.namespace().toLowerCase(Locale.ROOT);
Optional<ExternalDataProvider> qualified = findProvider(namespace);
if (qualified.isPresent()) {
return !qualified.get().isReady();
}
if (BLOCK_PROVIDER_PLUGINS.contains(namespace)) {
return isInstalledProviderPending(namespace);
}
for (String pluginId : BLOCK_PROVIDER_PLUGINS) {
if (isInstalledProviderPending(pluginId)) {
return true;
}
}
for (ExternalDataProvider provider : providers) {
if (!provider.isReady()) {
return true;
}
}
return false;
}
private boolean isInstalledProviderPending(String pluginId) {
String installedName = BUILT_IN_PROVIDERS.stream()
.map(ProviderDefinition::pluginId)
.filter(name -> name.equalsIgnoreCase(pluginId))
.findFirst()
.orElse(pluginId);
Plugin plugin = Bukkit.getPluginManager().getPlugin(installedName);
if (plugin == null) {
return false;
}
Optional<ExternalDataProvider> provider = findProvider(pluginId);
return !plugin.isEnabled() || provider.isEmpty() || !provider.get().isReady();
}
@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
String pluginId = event.getPlugin().getName();
@@ -106,7 +174,8 @@ public class ExternalDataSVC implements IrisService {
}
providers.add(registeredProvider);
if (registeredProvider.isReady()) {
Plugin plugin = registeredProvider.getPlugin();
if (plugin != null && plugin.isEnabled()) {
activateProvider(registeredProvider);
}
}
@@ -119,28 +188,35 @@ public class ExternalDataSVC implements IrisService {
IrisLogging.error(e.getMessage());
return Optional.empty();
}
Identifier mod = pair.getA();
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst();
if (provider.isEmpty())
return Optional.empty();
try {
return Optional.of(provider.get().getBlockData(mod, pair.getB()));
BlockProvider match = findBlockProvider(pair.getA());
if (match == null) {
return Optional.empty();
}
BlockData resolved = match.provider().getBlockData(match.identifier(), pair.getB());
if (resolved instanceof IrisCustomData custom) {
resolved = IrisCustomData.of(custom.getBase(), qualifyBlockId(match.provider(), custom.getCustom()));
}
return Optional.of(resolved);
} catch (MissingResourceException e) {
IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]");
return Optional.empty();
} catch (RuntimeException | LinkageError e) {
IrisLogging.reportError("Failed to resolve external block " + key + ".", e);
return Optional.empty();
}
}
public Optional<List<BlockProperty>> getBlockProperties(final Identifier key) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(key, DataType.BLOCK)).findFirst();
if (provider.isEmpty())
return Optional.empty();
try {
return Optional.of(provider.get().getBlockProperties(key));
BlockProvider match = findBlockProvider(parseState(key).getA());
return match == null ? Optional.empty() : Optional.of(match.provider().getBlockProperties(match.identifier()));
} catch (MissingResourceException e) {
IrisLogging.error(e.getMessage() + " - [" + e.getClassName() + ":" + e.getKey() + "]");
return Optional.empty();
} catch (RuntimeException | LinkageError e) {
IrisLogging.reportError("Failed to resolve external block properties for " + key + ".", e);
return Optional.empty();
}
}
@@ -160,13 +236,12 @@ public class ExternalDataSVC implements IrisService {
}
public void processUpdate(Engine engine, Block block, Identifier blockId) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(blockId, DataType.BLOCK)).findFirst();
if (provider.isEmpty()) {
IrisLogging.warnOnce("external-provider:block:" + blockId,
"No matching Provider found for modded material \"%s\"!", blockId);
return;
Pair<Identifier, KMap<String, String>> state = parseState(blockId);
BlockProvider match = findBlockProvider(state.getA());
if (match == null) {
throw new MissingResourceException("No matching provider found for external block placement.", blockId.namespace(), blockId.key());
}
provider.get().processUpdate(engine, block, blockId);
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
}
public Entity spawnMob(Location location, Identifier mobId) {
@@ -185,36 +260,61 @@ public class ExternalDataSVC implements IrisService {
}
public Collection<Identifier> getAllIdentifiers(DataType dataType) {
if (dataType == DataType.BLOCK) {
Set<Identifier> identifiers = new LinkedHashSet<>();
for (ExternalDataProvider provider : activeProviders) {
for (Identifier identifier : providerBlockTypes(provider)) {
identifiers.add(identifier);
identifiers.add(qualifyBlockId(provider, identifier));
}
}
return List.copyOf(identifiers);
}
return activeProviders.stream()
.flatMap(p -> p.getTypes(dataType).stream())
.toList();
}
private Collection<Identifier> providerBlockTypes(ExternalDataProvider provider) {
try {
return provider.getTypes(DataType.BLOCK);
} catch (RuntimeException | LinkageError error) {
IrisLogging.reportError("Failed to enumerate blocks from " + provider.getPluginId() + ".", error);
return List.of();
}
}
public Collection<Pair<Identifier, List<BlockProperty>>> getAllBlockProperties() {
return activeProviders.stream()
.flatMap(p -> p.getTypes(DataType.BLOCK)
.stream()
.map(id -> new Pair<>(id, p.getBlockProperties(id))))
.toList();
List<Pair<Identifier, List<BlockProperty>>> properties = new ArrayList<>();
for (Identifier identifier : getAllIdentifiers(DataType.BLOCK)) {
getBlockProperties(identifier).ifPresent(values -> properties.add(new Pair<>(identifier, values)));
}
return properties;
}
public static Pair<Identifier, KMap<String, String>> parseState(Identifier key) {
String raw = key.key();
int open = raw.indexOf('[');
int close = raw.lastIndexOf(']');
if (open < 0 || close < open) {
if (open < 0 && close < 0) {
return new Pair<>(key, new KMap<>());
}
if (open <= 0 || close != raw.length() - 1 || close < open
|| raw.indexOf('[', open + 1) >= 0 || raw.indexOf(']') != close) {
throw new IllegalArgumentException("Malformed block state \"" + key + "\" (expected block[key=value])");
}
String state = raw.substring(open + 1, close);
KMap<String, String> stateMap = new KMap<>();
if (!state.isEmpty()) {
for (String entry : state.split(",")) {
for (String entry : state.split(",", -1)) {
String[] pair = entry.split("=", 2);
if (pair.length != 2 || pair[0].isBlank() || pair[1].isBlank()) {
throw new IllegalArgumentException("Malformed block state \"" + entry + "\" in \"" + key + "\" (expected key=value)");
}
stateMap.put(pair[0], pair[1]);
if (stateMap.put(pair[0].trim(), pair[1].trim()) != null) {
throw new IllegalArgumentException("Duplicate block property \"" + pair[0] + "\" in \"" + key + "\"");
}
}
}
return new Pair<>(new Identifier(key.namespace(), raw.substring(0, open)), stateMap);
@@ -237,6 +337,45 @@ public class ExternalDataSVC implements IrisService {
.findFirst();
}
private BlockProvider findBlockProvider(Identifier identifier) {
int separator = identifier.key().indexOf('/');
if (separator > 0) {
for (ExternalDataProvider provider : activeProviders) {
if (!provider.getPluginId().equalsIgnoreCase(identifier.namespace())) {
continue;
}
Identifier nativeId = new Identifier(identifier.key().substring(0, separator), identifier.key().substring(separator + 1));
if (provider.isValidProvider(nativeId, DataType.BLOCK)) {
return new BlockProvider(provider, nativeId);
}
return provider.isValidProvider(identifier, DataType.BLOCK) ? new BlockProvider(provider, identifier) : null;
}
if (BUILT_IN_PROVIDERS.stream().anyMatch(definition -> definition.pluginId().equalsIgnoreCase(identifier.namespace()))
|| findProvider(identifier.namespace()).isPresent()) {
return null;
}
}
BlockProvider match = null;
for (ExternalDataProvider provider : activeProviders) {
if (!provider.isValidProvider(identifier, DataType.BLOCK)) {
continue;
}
if (match != null) {
IrisLogging.warnOnce("external-provider:ambiguous-block:" + identifier,
"Multiple providers own block %s. Use %s or %s to select one.", identifier,
qualifyBlockId(match.provider(), identifier), qualifyBlockId(provider, identifier));
return null;
}
match = new BlockProvider(provider, identifier);
}
return match;
}
private static Identifier qualifyBlockId(ExternalDataProvider provider, Identifier identifier) {
return new Identifier(provider.getPluginId().toLowerCase(Locale.ROOT), identifier.namespace() + "/" + identifier.key());
}
private void activateConfiguredProvider(ProviderDefinition definition) {
if (!isPluginEnabled(definition.pluginId()) || findProvider(definition.pluginId()).isPresent()) {
return;
@@ -267,6 +406,7 @@ public class ExternalDataSVC implements IrisService {
}
activeProviders.add(provider);
IrisLogging.debug("Enabled ExternalDataProvider for %s.", provider.getPluginId());
notifyContentChanged();
} catch (Throwable error) {
IrisLogging.reportError("Failed to enable Iris external data provider " + provider.getPluginId() + ".", error);
}
@@ -279,4 +419,7 @@ public class ExternalDataSVC implements IrisService {
private record ProviderDefinition(String pluginId, String className) {
}
private record BlockProvider(ExternalDataProvider provider, Identifier identifier) {
}
}
@@ -56,6 +56,7 @@ final class EngineShutdownSequence {
private final Set<EngineTarget> incompleteTargets = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
private boolean runtimeReleased;
private boolean worldManagerStopped;
private boolean targetReleased;
private boolean engineDataReleased;
private boolean preservationReleased;
@@ -72,7 +73,10 @@ final class EngineShutdownSequence {
if (!engine.beginShutdown()) {
return;
}
Throwable routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
Throwable routerFailure = stopWorldManager();
if (routerFailure == null) {
routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
}
if (routerFailure != null) {
synchronized (engine.lifecycleLock) {
engine.lifecycleState = LifecycleState.FAILED;
@@ -111,20 +115,16 @@ final class EngineShutdownSequence {
drainFailure = e;
}
if (drainFailure != null) {
// A drain timeout must not abandon teardown: the world manager is the lease
// producer, so stop it first, then re-drain briefly.
// A drain timeout must not abandon teardown: give remaining admitted work
// one final bounded drain before deciding whether resources can be released.
IrisLogging.warn("Iris generation did not drain for close on " + engine.getWorld().name()
+ "; stopping the world manager and retrying.");
Throwable managerFailure = engine.runtime == null
? null
: runCleanup(null, engine.runtime.worldManager()::close);
+ "; waiting briefly for remaining work.");
try {
engine.getGenerationSessions().sealAndAwait("close-retry", CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS, true);
drainFailure = null;
} catch (GenerationSessionException e) {
drainFailure = appendFailure(drainFailure, e);
}
drainFailure = appendFailure(drainFailure, managerFailure);
}
if (drainFailure != null) {
// A live lease may be mid-write, so the mantle must not be closed at it — but
@@ -322,7 +322,9 @@ final class EngineShutdownSequence {
if (engineRuntime == null) {
return failure;
}
Throwable serviceFailure = runCleanup(null, engineRuntime.worldManager()::close);
Throwable serviceFailure = engineRuntime == engine.runtime
? stopWorldManager()
: runCleanup(null, engineRuntime.worldManager()::close);
serviceFailure = runCleanup(serviceFailure, engineRuntime.effects()::close);
if (serviceFailure != null) {
return appendFailure(failure, serviceFailure);
@@ -414,6 +416,17 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable stopWorldManager() {
if (worldManagerStopped || engine.runtime == null) {
return null;
}
Throwable failure = runCleanup(null, engine.runtime.worldManager()::close);
if (failure == null) {
worldManagerStopped = true;
}
return failure;
}
private Throwable closeAssemblyResources(RuntimeAssembly assembly) {
Throwable failure = runCleanup(null, () -> {
if (assembly.worldManager != null) {
@@ -275,7 +275,7 @@ public class IrisEngineMantle implements EngineMantle {
return true;
}
private static Mantle<Matter> createMantle(
public static Mantle<Matter> createMantle(
Engine engine,
Path storageDirectory,
Supplier<IrisData> dataSupplier
@@ -213,6 +213,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
if (generated) {
chunkMaintenance.invalidateMaterialization(e.getX(), e.getZ());
}
INMS.get().reconcileNativeStructurePois(e);
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
@@ -274,10 +277,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
public synchronized void close() {
Throwable failure = null;
try {
super.close();
chunkMaintenance.close();
} catch (Throwable e) {
failure = e;
}
try {
super.close();
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
if (!looperStopped) {
try {
if (looper != null) {
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockDrops;
import art.arcane.iris.engine.object.IrisMarker;
@@ -52,21 +53,26 @@ final class WorldBlockDropRouter {
}
void onBlockBreak(BlockBreakEvent e) {
if (e.isCancelled()) {
return;
}
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) {
int blockX = e.getBlock().getX();
int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight());
int blockZ = e.getBlock().getZ();
KList<ItemStack> d = new KList<>();
IrisBiome b = EngineBukkitOps.getBiome(manager.getEngine(), e.getBlock().getLocation());
List<IrisBlockDrops> dropProviders = filterDrops(b.getBlockDrops(), e, manager.getData());
if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
IrisRegion r = EngineBukkitOps.getRegion(manager.getEngine(), e.getBlock().getLocation());
dropProviders.addAll(filterDrops(r.getBlockDrops(), e, manager.getData()));
dropProviders.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), e, manager.getData()));
List<IrisBlockDrops> dropProviders;
try {
dropProviders = resolveDropProviders(e);
} catch (SavedBiomeUnavailableException unavailable) {
e.setCancelled(true);
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
return;
}
KList<ItemStack> d = new KList<>();
dropProviders.forEach(provider -> provider.fillDrops(false, d));
if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) {
@@ -131,4 +137,15 @@ final class WorldBlockDropRouter {
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());
}
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) {
IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
IrisRegion region = EngineBukkitOps.getRegion(manager.getEngine(), event.getBlock().getLocation());
providers.addAll(filterDrops(region.getBlockDrops(), event, manager.getData()));
providers.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), event, manager.getData()));
}
return providers;
}
}
@@ -20,6 +20,8 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter.SavedChunkMantle;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
@@ -28,6 +30,7 @@ import art.arcane.iris.util.common.plugin.Chunks;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.Matter;
@@ -36,10 +39,14 @@ import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -50,21 +57,47 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
final class WorldChunkMaintenance {
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
private static final int MAX_PENDING_MATERIALIZATIONS = 128;
private static final int MAX_COMPLETED_MATERIALIZATIONS = 8192;
private final IrisWorldManager manager;
private final Set<Long> mantleWarmupQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
private final Map<Long, RoutedMaterialization> materializations = new ConcurrentHashMap<>();
private final Set<Long> completedMaterializations = Collections.synchronizedSet(new LinkedHashSet<>());
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
private volatile Position2[] loadedChunkPositions = new Position2[0];
private int forcedChunkUpdateCursor = 0;
private volatile boolean closed;
WorldChunkMaintenance(IrisWorldManager manager) {
this.manager = manager;
}
void close() {
synchronized (materializations) {
closed = true;
for (RoutedMaterialization materialization : materializations.values()) {
materialization.cancel();
}
}
completedMaterializations.clear();
}
void invalidateMaterialization(int chunkX, int chunkZ) {
long key = Cache.key(chunkX, chunkZ);
synchronized (materializations) {
RoutedMaterialization materialization = materializations.get(key);
if (materialization != null) {
materialization.cancel();
}
completedMaterializations.remove(key);
}
}
void discoverChunks() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
@@ -264,32 +297,84 @@ final class WorldChunkMaintenance {
}
void updateChunkRegion(World world, int chunkX, int chunkZ) {
if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
if (closed || world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) {
if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunkAsync(chunkX, chunkZ);
GenerationHistoryRuntimeRouter router = manager.getEngine() instanceof IrisEngine engine
? engine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
if (router != null && !completedMaterializations.contains(Cache.key(chunkX, chunkZ))) {
scheduleMaterialization(new MaterializationTarget(world, chunkX, chunkZ), router);
return;
}
try {
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
} catch (SavedBiomeUnavailableException unavailable) {
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
if (router == null && !updateCurrentChunk(chunk, chunkX, chunkZ)) {
return;
}
}
spawnInitially(chunk);
}
private boolean updateCurrentChunk(Chunk chunk, int chunkX, int chunkZ) {
if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunkAsync(chunkX, chunkZ);
return false;
}
return materializeChunk(chunk, manager.getMantle());
}
private boolean materializeChunk(Chunk chunk, Mantle<Matter> mantle) {
try {
EngineBukkitOps.updateChunk(manager.getEngine(), chunk, mantle);
} catch (SavedBiomeUnavailableException unavailable) {
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
return false;
}
return true;
}
private void spawnInitially(Chunk chunk) {
if (manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
manager.entitySpawner.spawnInitially(chunk);
}
}
private void scheduleMaterialization(MaterializationTarget target, GenerationHistoryRuntimeRouter router) {
RoutedMaterialization materialization;
synchronized (materializations) {
if (closed || materializations.size() >= MAX_PENDING_MATERIALIZATIONS
|| materializations.containsKey(target.key())) {
return;
}
materialization = new RoutedMaterialization(target, router);
materializations.put(target.key(), materialization);
}
try {
J.a(manager.managedTask("bukkit_world_manager_materialization_load",
materialization::prepare, materialization::cancel));
} catch (Throwable failure) {
materialization.cancel();
IrisLogging.reportError(failure);
}
}
private void recordMaterialized(long key) {
synchronized (completedMaterializations) {
if (closed) {
return;
}
if (completedMaterializations.size() >= MAX_COMPLETED_MATERIALIZATIONS) {
completedMaterializations.remove(completedMaterializations.iterator().next());
}
completedMaterializations.add(key);
}
}
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
if (world == null || onFirstRaise == null) {
return;
@@ -355,4 +440,156 @@ final class WorldChunkMaintenance {
}
return loadedChunkPositions;
}
private record MaterializationTarget(World world, int x, int z) {
private long key() {
return Cache.key(x, z);
}
}
private final class RoutedMaterialization {
private final MaterializationTarget target;
private final GenerationHistoryRuntimeRouter router;
private SavedChunkMantle savedMantle;
private MantleChunk<Matter> mantleChunk;
private boolean cancelled;
private boolean preparing;
private boolean applying;
private boolean releasing;
private RoutedMaterialization(MaterializationTarget target, GenerationHistoryRuntimeRouter router) {
this.target = target;
this.router = router;
}
private void prepare() {
SavedChunkMantle opened = null;
MantleChunk<Matter> loaded = null;
try {
synchronized (this) {
if (cancelled) {
return;
}
preparing = true;
}
opened = router.openSavedChunkMantle(target.x(), target.z());
loaded = opened.mantle().getChunk(target.x(), target.z()).use();
opened.detachThread();
synchronized (this) {
if (cancelled) {
return;
}
savedMantle = opened;
mantleChunk = loaded;
opened = null;
loaded = null;
}
boolean scheduled = J.runRegion(target.world(), target.x(), target.z(),
manager.managedTask("bukkit_world_manager_materialization_apply", this::apply, this::cancel));
if (!scheduled) {
cancel();
}
} catch (Throwable failure) {
cancel();
IrisLogging.reportError("Failed to load saved chunk materialization at " + target.x() + "," + target.z() + ".", failure);
} finally {
try {
try {
if (loaded != null) {
loaded.release();
}
} finally {
if (opened != null) {
opened.close();
}
}
} finally {
boolean cleanup;
synchronized (this) {
preparing = false;
cleanup = cancelled;
}
if (cleanup) {
cancel();
}
}
}
}
private void apply() {
SavedChunkMantle active;
synchronized (this) {
if (cancelled || savedMantle == null) {
return;
}
applying = true;
active = savedMantle;
}
try {
World world = target.world();
if (closed || !world.isChunkLoaded(target.x(), target.z())
|| !Chunks.isSafe(world, target.x(), target.z())) {
return;
}
Chunk chunk = world.getChunkAt(target.x(), target.z());
boolean completed;
try (SavedChunkMantle.Scope ignored = active.openScope()) {
Mantle<Matter> mantle = active.mantle();
if (!materializeChunk(chunk, mantle)) {
return;
}
completed = mantle.hasFlag(target.x(), target.z(), MantleFlag.ETCHED);
}
synchronized (this) {
if (completed && !cancelled) {
recordMaterialized(target.key());
}
}
spawnInitially(chunk);
} catch (Throwable failure) {
IrisLogging.reportError("Failed to materialize saved chunk at " + target.x() + "," + target.z() + ".", failure);
} finally {
synchronized (this) {
applying = false;
}
cancel();
}
}
private void cancel() {
SavedChunkMantle retained;
MantleChunk<Matter> loaded;
synchronized (this) {
cancelled = true;
if (preparing || applying || releasing) {
return;
}
retained = savedMantle;
loaded = mantleChunk;
savedMantle = null;
mantleChunk = null;
releasing = retained != null;
}
if (retained != null) {
ForkJoinPool.commonPool().execute(() -> release(retained, loaded));
} else {
materializations.remove(target.key(), this);
}
}
private void release(SavedChunkMantle retained, MantleChunk<Matter> loaded) {
try {
try {
loaded.release();
} finally {
retained.close();
}
} catch (Throwable failure) {
completedMaterializations.remove(target.key());
IrisLogging.reportError("Failed to release saved chunk materialization at " + target.x() + "," + target.z() + ".", failure);
} finally {
materializations.remove(target.key(), this);
}
}
}
}
@@ -138,6 +138,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
}
private static BoundaryColumnGeometry.Voxel encode(PlatformBlockState state) {
String stateKey = state.key();
boolean custom = state.isCustom();
PlatformBlockState nativeState = state.placementBaseState();
if (nativeState != null) {
@@ -149,7 +150,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
: state.isWaterLogged() ? "minecraft:water[level=0]" : "";
boolean protectedContent = custom || state.isDecorant() || state.isFoliage() || state.isTreeBlock()
|| state.hasTileEntity();
return new BoundaryColumnGeometry.Voxel(state.key(), phase, fluidKey, protectedContent);
return new BoundaryColumnGeometry.Voxel(stateKey, phase, fluidKey, protectedContent);
}
}
}
@@ -574,11 +574,19 @@ public final class GenerationHistory {
}
public GenerationStage openStage(int chunkX, int chunkZ) throws IOException {
return openStage(chunkX, chunkZ, false);
}
GenerationStage openSavedChunkStage(int chunkX, int chunkZ) throws IOException {
return openStage(chunkX, chunkZ, true);
}
private GenerationStage openStage(int chunkX, int chunkZ, boolean savedChunk) throws IOException {
GenerationAdmission.StageLease lease = admission.enterStage();
try {
GenerationManifest manifest = store.manifest();
GenerationActivation activation = manifest.activeActivation();
GenerationEpoch epoch = manifest.activeEpoch();
GenerationActivation activation = savedChunk ? resolveActivation(chunkX, chunkZ) : manifest.activeActivation();
GenerationEpoch epoch = savedChunk ? requireEpoch(activation.epochId()) : manifest.activeEpoch();
return new GenerationStage(
this,
chunkX,
@@ -2,9 +2,12 @@ package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import java.io.IOException;
import java.nio.file.Path;
@@ -28,6 +31,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
private final Map<Long, RuntimeRetirement> retiringBindings;
private final Map<Long, SavedMantleEntry> savedMantles = new LinkedHashMap<>();
private final ReentrantLock stateLock;
private final Condition inactive;
private final ThreadLocal<Integer> operationDepth;
@@ -265,6 +269,29 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
public SavedChunkMantle openSavedChunkMantle(int chunkX, int chunkZ) throws IOException {
GenerationTransitionGate.Participation participation = engine.getGenerationSessions().transitionGate().enter();
boolean operation = false;
try {
enterRouteOperation();
operation = true;
GenerationHistory.GenerationStage stage = history.openSavedChunkStage(chunkX, chunkZ);
try {
SavedMantleAccess access = acquireSavedMantle(stage.activation(), stage.epoch());
return new SavedChunkMantle(this, new SavedChunkResources(stage, participation, access));
} catch (Throwable failure) {
stage.close();
throw failure;
}
} catch (Throwable failure) {
if (operation) {
leaveRouteOperation();
}
participation.close();
throw propagate(failure, "Unable to open saved Iris chunk mantle.");
}
}
public RuntimeStage openStage(int chunkX, int chunkZ) throws IOException {
RuntimeRoute route = openRoute(chunkX, chunkZ);
try {
@@ -369,6 +396,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
throw new IllegalStateException("Cannot close the generation-history router from an active route.");
}
List<RuntimeRetirement> retired = null;
List<SavedMantleEntry> saved = null;
stateLock.lock();
try {
closed = true;
@@ -389,6 +417,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
bindings.clear();
saved = new ArrayList<>(savedMantles.values());
break;
}
} finally {
@@ -411,6 +440,13 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable retirementFailure) {
failure = appendFailure(failure, retirementFailure);
}
for (SavedMantleEntry entry : saved) {
try {
closeSavedMantle(entry);
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
}
stateLock.lock();
try {
@@ -431,6 +467,164 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return signatureSampler.open(engine);
}
private SavedMantleAccess acquireSavedMantle(GenerationActivation activation, GenerationEpoch epoch) throws IOException {
SavedMantleEntry saved = null;
RuntimeCacheEntry runtime;
boolean loader = false;
stateLock.lock();
try {
while (true) {
awaitRetirementLocked(activation.activationId());
saved = savedMantles.get(activation.activationId());
if (saved == null || !saved.retiring) {
break;
}
awaitSavedMantleChange(saved);
}
runtime = bindings.get(activation.activationId());
if (runtime != null) {
runtime.leases++;
} else {
if (saved == null) {
saved = new SavedMantleEntry(activation.activationId());
saved.loading = true;
savedMantles.put(activation.activationId(), saved);
activeLoads++;
loader = true;
}
saved.leases++;
}
} finally {
stateLock.unlock();
}
if (runtime != null) {
RuntimeLease lease = awaitRuntime(runtime, activation.activationId());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(lease.binding())) {
return new SavedMantleAccess(engine.getMantle().getMantle(), lease::close);
} catch (Throwable failure) {
lease.close();
throw failure;
}
}
SavedMantleEntry entry = saved;
if (loader) {
loadSavedMantle(entry, activation, epoch);
}
stateLock.lock();
try {
while (entry.loading) {
try {
inactive.await();
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
entry.leases--;
inactive.signalAll();
throw new IOException("Interrupted while loading saved mantle for activation " + entry.activationId + ".", failure);
}
}
if (entry.failure != null) {
entry.leases--;
throw propagate(entry.failure, "Unable to load saved mantle for activation " + entry.activationId + ".");
}
return new SavedMantleAccess(entry.mantle, () -> releaseSavedMantle(entry));
} finally {
stateLock.unlock();
}
}
private void loadSavedMantle(SavedMantleEntry entry, GenerationActivation activation, GenerationEpoch epoch) {
IrisData data = null;
Mantle<Matter> mantle = null;
Throwable failure = null;
try {
data = IrisData.openRuntime(history.packRoot(activation.activationId()).toFile());
data.bindGenerationRegistryContract(epoch.registryContract());
data.registerEngine(engine);
IrisData savedData = data;
mantle = IrisEngineMantle.createMantle(engine, history.paths().activationMantleRoot(activation.activationId()), () -> savedData);
} catch (Throwable loadFailure) {
failure = loadFailure;
if (data != null) {
try {
data.unregisterEngine(engine);
data.close();
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
}
}
stateLock.lock();
try {
entry.data = data;
entry.mantle = mantle;
entry.failure = failure;
entry.loading = false;
activeLoads--;
if (failure != null) {
savedMantles.remove(entry.activationId, entry);
}
inactive.signalAll();
} finally {
stateLock.unlock();
}
}
private void releaseSavedMantle(SavedMantleEntry entry) {
stateLock.lock();
try {
if (entry.leases <= 0) {
throw new IllegalStateException("Saved mantle has no active lease.");
}
entry.leases--;
if (entry.leases > 0) {
return;
}
entry.retiring = true;
} finally {
stateLock.unlock();
}
closeSavedMantle(entry);
}
private void closeSavedMantle(SavedMantleEntry entry) {
Throwable failure = null;
try {
entry.mantle.close();
entry.data.unregisterEngine(engine);
entry.data.close();
} catch (Throwable closeFailure) {
failure = closeFailure;
}
stateLock.lock();
try {
entry.failure = failure;
if (failure == null) {
savedMantles.remove(entry.activationId, entry);
}
inactive.signalAll();
} finally {
stateLock.unlock();
}
if (failure != null) {
throw new IllegalStateException("Failed to close saved mantle for activation " + entry.activationId + ".", failure);
}
}
private void awaitSavedMantleChange(SavedMantleEntry entry) throws IOException {
if (entry.failure != null) {
throw propagate(entry.failure, "Saved mantle for activation " + entry.activationId + " is unavailable.");
}
if (closed) {
throw new IllegalStateException("Generation-history runtime router is closed.");
}
try {
inactive.await();
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while awaiting saved mantle for activation " + entry.activationId + ".", failure);
}
}
private RuntimeLease acquireRuntime(
GenerationActivation activation,
GenerationEpoch epoch
@@ -439,7 +633,14 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
boolean loader = false;
stateLock.lock();
try {
awaitRetirementLocked(activation.activationId());
while (true) {
awaitRetirementLocked(activation.activationId());
SavedMantleEntry saved = savedMantles.get(activation.activationId());
if (saved == null) {
break;
}
awaitSavedMantleChange(saved);
}
entry = bindings.get(activation.activationId());
if (entry == null) {
entry = new RuntimeCacheEntry(activation.activationId());
@@ -823,6 +1024,139 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
throw new IllegalStateException("Failed to close the generation-history runtime router.", failure);
}
private static final class SavedMantleEntry {
private final long activationId;
private IrisData data;
private Mantle<Matter> mantle;
private Throwable failure;
private int leases;
private boolean loading;
private boolean retiring;
private SavedMantleEntry(long activationId) {
this.activationId = activationId;
}
}
private record SavedMantleAccess(Mantle<Matter> mantle, Runnable release) {
}
private record SavedChunkResources(GenerationHistory.GenerationStage stage,
GenerationTransitionGate.Participation participation,
SavedMantleAccess access) {
}
public static final class SavedChunkMantle implements AutoCloseable {
private final GenerationHistoryRuntimeRouter router;
private final SavedChunkResources resources;
private int activeScopes;
private boolean closed;
private SavedChunkMantle(GenerationHistoryRuntimeRouter router, SavedChunkResources resources) {
this.router = router;
this.resources = resources;
}
public Mantle<Matter> mantle() {
return resources.access().mantle();
}
public void detachThread() {
resources.participation().detachThread();
}
public Scope openScope() {
synchronized (this) {
if (closed) {
throw new IllegalStateException("Saved chunk mantle is closed.");
}
activeScopes++;
}
try {
GenerationTransitionGate.Participation participation = resources.participation().attach();
router.enterRuntimeScope();
return new Scope(this, participation);
} catch (Throwable failure) {
releaseScope();
throw failure;
}
}
@Override
public synchronized void close() {
if (closed) {
return;
}
if (activeScopes > 0) {
throw new IllegalStateException("Saved chunk mantle still has active scopes.");
}
closed = true;
Throwable failure = null;
try {
resources.access().release().run();
} catch (Throwable closeFailure) {
failure = closeFailure;
}
try {
resources.stage().close();
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
try {
router.leaveRouteOperation();
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
try {
resources.participation().close();
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
if (failure != null) {
throw new IllegalStateException("Failed to close saved chunk mantle.", failure);
}
}
private synchronized void releaseScope() {
if (activeScopes <= 0) {
throw new IllegalStateException("Saved chunk mantle has no active scope.");
}
activeScopes--;
}
public static final class Scope implements AutoCloseable {
private final SavedChunkMantle handle;
private final GenerationTransitionGate.Participation participation;
private final Thread owner = Thread.currentThread();
private boolean closed;
private Scope(SavedChunkMantle handle, GenerationTransitionGate.Participation participation) {
this.handle = handle;
this.participation = participation;
}
@Override
public void close() {
if (closed) {
return;
}
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Saved chunk mantle scope closed from a different thread.");
}
closed = true;
try {
handle.router.leaveRuntimeScope();
} finally {
try {
handle.releaseScope();
} finally {
participation.close();
}
}
}
}
}
private static final class RuntimeCacheEntry {
private final long activationId;
private IrisEngine.GenerationRuntimeBinding binding;
@@ -41,6 +41,7 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Locale;
import java.util.Map;
@Accessors(chain = true)
@@ -76,12 +77,17 @@ public class IrisBlockData extends IrisRegistrant {
public static IrisBlockData from(String j) {
IrisBlockData b = new IrisBlockData();
String v = j.toLowerCase().trim();
String v = j.trim();
int bracket = v.indexOf('[');
String block = (bracket < 0 ? v : v.substring(0, bracket)).toLowerCase(Locale.ROOT);
b.setBlock(block);
if (v.contains("[")) {
if (bracket >= 0) {
KList<String> props = new KList<>();
String rp = v.split("\\Q[\\E")[1].replaceAll("\\Q]\\E", "");
b.setBlock(v.split("\\Q[\\E")[0]);
String rp = v.substring(bracket + 1).replace("]", "");
if (!block.contains(":") || block.startsWith("minecraft:")) {
rp = rp.toLowerCase(Locale.ROOT);
}
if (rp.contains(",")) {
props.add(rp.split("\\Q,\\E"));
@@ -93,8 +99,6 @@ public class IrisBlockData extends IrisRegistrant {
Object kg = filter(i.split("\\Q=\\E")[1]);
b.data.put(i.split("\\Q=\\E")[0], kg);
}
} else {
b.setBlock(v);
}
return b;
@@ -116,7 +120,11 @@ public class IrisBlockData extends IrisRegistrant {
}
try {
return Double.valueOf(string).intValue();
double value = Double.parseDouble(string);
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE && value == Math.rint(value)) {
return (int) value;
}
return value;
} catch (Throwable ignored) {
// Checks
}
@@ -156,19 +164,13 @@ public class IrisBlockData extends IrisRegistrant {
PlatformBlockState customState = customData.getBlockData(data);
if (customState != null) {
String st = customState.key();
if (st.contains("[")) {
st = st.split("\\Q[\\E")[0];
if (getData().isEmpty()) {
return customState;
}
KMap<String, Object> cdata = customData.getData().copy();
for (String i : getData().keySet()) {
cdata.put(i, getData().get(i));
}
String sx = keyify(st) + computeProperties(cdata);
IrisBlockData merged = IrisBlockData.from(customState.key());
merged.getData().putAll(getData());
String sx = merged.stateKey();
if (debug) {
IrisLogging.debug("Block Data used " + sx + " (CUSTOM)");
@@ -146,6 +146,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private volatile Throwable initializationFailure;
private volatile IrisDimension validatedDimension;
private volatile IrisDimensionRuntimeContract validatedWorldContract;
private volatile EngineTarget startupTarget;
private volatile CompletableFuture<Void> startupContentReady = CompletableFuture.completedFuture(null);
private volatile CompletableFuture<Void> startupReady = CompletableFuture.completedFuture(null);
private volatile boolean closing;
@Setter
private volatile StudioGenerator studioGenerator;
@@ -195,6 +198,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
BukkitPlatform.volmitPlugin().unregisterListener(this);
world.setRawWorldSeed(event.getWorld().getSeed());
if (startupTarget != null) {
startupContentReady.whenComplete((ignored, failure) ->
J.s(() -> resumeDeferredStartup(event.getWorld(), failure)));
return;
}
if (initialize(event.getWorld())) return;
IrisLogging.warn("Failed to get Engine for " + event.getWorld().getName() + " re-trying...");
@@ -205,6 +213,73 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}, 10);
}
public void deferStartup(EngineTarget target, CompletableFuture<Void> contentReady) {
if (setup.get() || startupTarget != null) {
throw new IllegalStateException("Iris generator startup has already begun.");
}
startupTarget = Objects.requireNonNull(target);
startupContentReady = Objects.requireNonNull(contentReady);
startupReady = new CompletableFuture<>();
}
private void resumeDeferredStartup(World runtimeWorld, Throwable failure) {
lock.lock();
try {
closeStartupTarget();
if (closing) {
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
return;
}
if (failure != null) {
initializationFailure = failure;
spawnChunks.completeExceptionally(failure);
initialSpawnReady.completeExceptionally(failure);
startupReady.completeExceptionally(failure);
IrisLogging.reportError("External block providers did not become available for Iris world '"
+ world.name() + "'; generation remains locked.", failure);
Bukkit.shutdown();
return;
}
if (!initialize(runtimeWorld)) {
throw new IllegalStateException("Iris engine initialization did not complete for '" + world.name() + "'.");
}
startupReady.complete(null);
} catch (RuntimeException | Error initializationError) {
initializationFailure = initializationError;
spawnChunks.completeExceptionally(initializationError);
initialSpawnReady.completeExceptionally(initializationError);
startupReady.completeExceptionally(initializationError);
Bukkit.shutdown();
throw initializationError;
} finally {
lock.unlock();
}
}
private void closeStartupTarget() {
EngineTarget target = startupTarget;
startupTarget = null;
if (target != null) {
target.getData().close();
}
}
private void requireStartupContentReady() {
if (startupReady.isCompletedExceptionally()) {
startupReady.join();
}
boolean initializationPending = startupTarget != null
|| !startupReady.isDone() && !lock.isHeldByCurrentThread();
requireStartupContentReady(startupContentReady, initializationPending);
}
static void requireStartupContentReady(CompletableFuture<Void> ready, boolean initializationPending) {
if (!ready.isDone() || initializationPending) {
throw new IllegalStateException("Iris generation is waiting for external block providers to initialize.");
}
ready.join();
}
private boolean initialize(World world) {
try {
Engine engine = getEngine(world);
@@ -579,6 +654,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public EngineTarget getTarget() {
if (engine != null) return engine.getTarget();
EngineTarget pendingTarget = startupTarget;
if (pendingTarget != null) {
return pendingTarget;
}
return targetCache.aquireOrThrow(() -> {
if (generationHistory != null) {
@@ -621,6 +700,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private Engine getEngine(WorldInfo world) {
throwIfInitializationFailed();
requireStartupContentReady();
validateAndBindWorld(world);
if (setup.get()) {
return getEngine();
@@ -763,6 +843,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
folder.clear();
populators.clear();
closeStartupTarget();
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
});
} catch (Throwable throwable) {
if (!alreadyClosing) {
@@ -1079,6 +1161,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
public void touch(World world) {
if (!startupReady.isDone() || startupReady.isCompletedExceptionally()) {
return;
}
getEngine(world);
}
@@ -1274,7 +1359,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return shouldGenerateNativeStructures(
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null);
initializationFailure != null || startupTarget != null);
}
static boolean shouldGenerateNativeStructures(
@@ -101,6 +101,10 @@ public final class EngineBukkitOps {
}
public static void updateChunk(Engine engine, Chunk c) {
updateChunk(engine, c, engine.getMantle().getMantle());
}
public static void updateChunk(Engine engine, Chunk c, Mantle<Matter> mantle) {
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
if (c.getWorld().isChunkLoaded(c.getX() + x, c.getZ() + z))
@@ -110,7 +114,6 @@ public final class EngineBukkitOps {
return;
}
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isLoaded(c)) {
String msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded";
IrisLogging.debug(msg);
@@ -118,7 +121,7 @@ public final class EngineBukkitOps {
}
if (!J.isFolia() && !J.isPrimaryThread()) {
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c));
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c, mantle));
if (scheduled != null) {
try {
scheduled.join();
@@ -70,7 +70,7 @@ public final class BukkitBlockState implements PlatformBlockState {
public static BukkitBlockState of(BlockData data) {
if (data instanceof IrisCustomData custom) {
return new BukkitBlockState(data, custom.getAsString());
return new BukkitBlockState(data, custom.getCustom().toString());
}
BukkitBlockState fast = DATA_CACHE.get(data);
if (fast != null) {
@@ -369,15 +369,14 @@ public final class BukkitBlockState implements PlatformBlockState {
@Override
public PlatformBlockState withProperty(String name, String value) {
String merged = mergeProperty(key, name, value);
BlockData resolved = Bukkit.createBlockData(merged);
// Re-attach the custom identity (as the proxy's own merge/clone cases do): the key of
// a custom state is the BASE block's string, so rebuilding from it alone silently
// downgraded custom blocks to vanilla on e.g. auto-waterlogging.
// Re-attach the custom identity (as the proxy's own merge/clone cases do) after
// editing the base block, so auto-waterlogging cannot turn custom blocks into vanilla.
if (data instanceof IrisCustomData custom) {
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
BlockData resolved = Bukkit.createBlockData(merged);
return of(IrisCustomData.of(resolved, custom.getCustom()));
}
return of(resolved);
return of(Bukkit.createBlockData(mergeProperty(key, name, value)));
}
@Override