mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
IA Fixes
- Thsi fixes an integration between Itemsadder and Oraxen. - Waiting on oraxen folks
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+335
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user