mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Full audit fixpass: 93 defect fixes, 14 perf wins, vestigial cleanup
This commit is contained in:
+70
-7
@@ -55,6 +55,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
|||||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||||
import net.bytebuddy.ByteBuddy;
|
import net.bytebuddy.ByteBuddy;
|
||||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||||
|
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||||
import net.bytebuddy.asm.Advice;
|
import net.bytebuddy.asm.Advice;
|
||||||
import net.bytebuddy.matcher.ElementMatchers;
|
import net.bytebuddy.matcher.ElementMatchers;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
@@ -172,6 +173,7 @@ public class NMSBinding implements INMSBinding {
|
|||||||
private final BlockData AIR = Material.AIR.createBlockData();
|
private final BlockData AIR = Material.AIR.createBlockData();
|
||||||
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
||||||
private final AtomicBoolean injected = new AtomicBoolean();
|
private final AtomicBoolean injected = new AtomicBoolean();
|
||||||
|
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||||
@@ -1523,7 +1525,7 @@ public class NMSBinding implements INMSBinding {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
IrisLogging.info("Injecting Bukkit");
|
IrisLogging.info("Injecting Bukkit");
|
||||||
new AgentBuilder.Default()
|
serverLevelTransformer = new AgentBuilder.Default()
|
||||||
.disableClassFormatChanges()
|
.disableClassFormatChanges()
|
||||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
.type(ElementMatchers.is(ServerLevel.class))
|
.type(ElementMatchers.is(ServerLevel.class))
|
||||||
@@ -1546,6 +1548,17 @@ public class NMSBinding implements INMSBinding {
|
|||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
IrisLogging.error(C.RED + "Failed to inject Bukkit");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
// The ServerLevel transformer may already be installed when the ChunkAccess
|
||||||
|
// redefine throws; remove it or a retry would stack a second one and orphan
|
||||||
|
// this one permanently.
|
||||||
|
ResettableClassFileTransformer partial = serverLevelTransformer;
|
||||||
|
serverLevelTransformer = null;
|
||||||
|
if (partial != null) {
|
||||||
|
try {
|
||||||
|
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1562,6 +1575,24 @@ public class NMSBinding implements INMSBinding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uninjectBukkit() {
|
||||||
|
synchronized (injected) {
|
||||||
|
ResettableClassFileTransformer transformer = serverLevelTransformer;
|
||||||
|
serverLevelTransformer = null;
|
||||||
|
injected.set(false);
|
||||||
|
if (transformer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.error(C.RED + "Failed to remove ServerLevel injection");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
||||||
@@ -1624,24 +1655,56 @@ public class NMSBinding implements INMSBinding {
|
|||||||
if (dimensionKey == null)
|
if (dimensionKey == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||||
|
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||||
|
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||||
|
String levelId;
|
||||||
|
ClassLoader pluginClassLoader;
|
||||||
|
Class<?> generatorType;
|
||||||
|
Class<?> stagingType;
|
||||||
try {
|
try {
|
||||||
String levelId = dimensionKey.identifier().getPath();
|
levelId = dimensionKey.identifier().getPath();
|
||||||
if (levelId == null || levelId.isBlank()) {
|
if (levelId == null || levelId.isBlank()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
|
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||||
Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
if (irisPlugin == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||||
|
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||||
|
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From here Iris is present and its classes resolve; a failure resolving the
|
||||||
|
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||||
|
// the vanilla stem corrupts generation.
|
||||||
|
ChunkGenerator gen = null;
|
||||||
|
try {
|
||||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||||
if (generator == null) {
|
if (generator == null) {
|
||||||
generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader)
|
generator = stagingType
|
||||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||||
.invoke(null, levelId);
|
.invoke(null, levelId);
|
||||||
}
|
}
|
||||||
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||||
return;
|
gen = owned;
|
||||||
}
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||||
|
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
|
}
|
||||||
|
if (gen == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||||
|
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||||
|
try {
|
||||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||||
.getDeclaredMethod("get")
|
.getDeclaredMethod("get")
|
||||||
.invoke(null);
|
.invoke(null);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ dependencies {
|
|||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
compileOnly(libs.paper.api)
|
compileOnly(libs.paper.api)
|
||||||
|
// LogFilterSVC (constructed in Iris.enable's explicit service list) implements the
|
||||||
|
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||||
|
compileOnly(libs.log4j.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
|
|||||||
@@ -51,9 +51,27 @@ import art.arcane.iris.core.link.MultiverseCoreLink;
|
|||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
|
import art.arcane.iris.core.service.BoardSVC;
|
||||||
|
import art.arcane.iris.core.service.CommandSVC;
|
||||||
|
import art.arcane.iris.core.service.DatapackStructureScopeSVC;
|
||||||
import art.arcane.iris.core.service.EditSVC;
|
import art.arcane.iris.core.service.EditSVC;
|
||||||
|
import art.arcane.iris.core.service.EntityRiseSVC;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.core.service.GlobalCacheSVC;
|
||||||
|
import art.arcane.iris.core.service.IrisApiEventSVC;
|
||||||
|
import art.arcane.iris.core.service.IrisEngineSVC;
|
||||||
|
import art.arcane.iris.core.service.IrisIntegrationService;
|
||||||
|
import art.arcane.iris.core.service.IrisProtocolService;
|
||||||
|
import art.arcane.iris.core.service.IrisTerrainSVC;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService;
|
||||||
|
import art.arcane.iris.core.service.LogFilterSVC;
|
||||||
|
import art.arcane.iris.core.service.ObjectSVC;
|
||||||
|
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.core.service.PreservationSVC;
|
import art.arcane.iris.core.service.PreservationSVC;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
|
import art.arcane.iris.core.service.TreeFellerSVC;
|
||||||
|
import art.arcane.iris.core.service.TreeSVC;
|
||||||
|
import art.arcane.iris.core.service.WandSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.EnginePanic;
|
import art.arcane.iris.engine.EnginePanic;
|
||||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||||
@@ -80,7 +98,6 @@ import art.arcane.volmlib.util.hud.HudBossBarLane;
|
|||||||
import art.arcane.volmlib.util.hud.HudSlotService;
|
import art.arcane.volmlib.util.hud.HudSlotService;
|
||||||
import art.arcane.volmlib.util.io.IO;
|
import art.arcane.volmlib.util.io.IO;
|
||||||
import art.arcane.volmlib.util.io.InstanceState;
|
import art.arcane.volmlib.util.io.InstanceState;
|
||||||
import art.arcane.volmlib.util.io.JarScanner;
|
|
||||||
import art.arcane.volmlib.util.math.M;
|
import art.arcane.volmlib.util.math.M;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.iris.util.common.misc.Bindings;
|
import art.arcane.iris.util.common.misc.Bindings;
|
||||||
@@ -98,7 +115,6 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.GameMode;
|
import org.bukkit.GameMode;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.data.BlockData;
|
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@@ -115,9 +131,9 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.lang.annotation.Annotation;
|
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -165,6 +181,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private volatile IrisPapiListener papiListener;
|
private volatile IrisPapiListener papiListener;
|
||||||
private volatile IrisPapiState papiState;
|
private volatile IrisPapiState papiState;
|
||||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||||
|
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
|
||||||
|
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
|
||||||
|
private final List<IrisService> enabledServices = new CopyOnWriteArrayList<>();
|
||||||
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
|
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
|
||||||
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
|
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
|
||||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||||
@@ -206,28 +225,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> KList<T> initialize(String s, Class<T> requiredType) {
|
|
||||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
|
||||||
KList<T> v = new KList<>();
|
|
||||||
J.attempt(js::scan);
|
|
||||||
for (Class<?> i : js.getClasses()) {
|
|
||||||
if (!isConcreteImplementation(i, requiredType)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
v.add(requiredType.cast(i.getDeclaredConstructor().newInstance()));
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
Iris.warn("Skipped class initialization for %s: %s%s",
|
|
||||||
i.getName(),
|
|
||||||
ex.getClass().getSimpleName(),
|
|
||||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
|
||||||
Iris.reportError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isConcreteImplementation(Class<?> candidate, Class<?> requiredType) {
|
static boolean isConcreteImplementation(Class<?> candidate, Class<?> requiredType) {
|
||||||
int modifiers = candidate.getModifiers();
|
int modifiers = candidate.getModifiers();
|
||||||
return requiredType.isAssignableFrom(candidate)
|
return requiredType.isAssignableFrom(candidate)
|
||||||
@@ -235,27 +232,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
&& !Modifier.isAbstract(modifiers);
|
&& !Modifier.isAbstract(modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
|
|
||||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
|
||||||
KList<Class<?>> v = new KList<>();
|
|
||||||
J.attempt(js::scan);
|
|
||||||
for (Class<?> i : js.getClasses()) {
|
|
||||||
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
|
|
||||||
try {
|
|
||||||
v.add(i);
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
Iris.warn("Skipped class discovery entry for %s: %s%s",
|
|
||||||
i.getName(),
|
|
||||||
ex.getClass().getSimpleName(),
|
|
||||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
|
||||||
Iris.reportError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sq(Runnable r) {
|
public static void sq(Runnable r) {
|
||||||
synchronized (syncJobs) {
|
synchronized (syncJobs) {
|
||||||
syncJobs.queue(r);
|
syncJobs.queue(r);
|
||||||
@@ -538,7 +514,21 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enable() {
|
/**
|
||||||
|
* @return false when the bootstrap was aborted (unsupported server version); the caller
|
||||||
|
* must bail out of onEnable without touching any further setup.
|
||||||
|
*/
|
||||||
|
private boolean enable() {
|
||||||
|
if (!INMS.isBound()) {
|
||||||
|
Throwable bindFailure = INMS.bindFailure();
|
||||||
|
Iris.error("Iris cannot start: " + (bindFailure == null
|
||||||
|
? "no NMS binding is available for this server version."
|
||||||
|
: bindFailure.getMessage()));
|
||||||
|
// Deferred one tick: disablePlugin from inside onEnable re-enters onDisable
|
||||||
|
// synchronously and the loader then continues registering the half-enabled plugin.
|
||||||
|
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
alreadyDrained.set(false);
|
alreadyDrained.set(false);
|
||||||
servicesDisabled.set(false);
|
servicesDisabled.set(false);
|
||||||
sharedRuntimeClosed.set(false);
|
sharedRuntimeClosed.set(false);
|
||||||
@@ -551,25 +541,50 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
setupAudience();
|
setupAudience();
|
||||||
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane());
|
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane());
|
||||||
Bindings.setupSentry();
|
Bindings.setupSentry();
|
||||||
initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
|
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
|
||||||
|
// enable/disable and paid a full-jar class sweep at boot. Infrastructure first,
|
||||||
|
// engine/world services next, commands last.
|
||||||
|
List<IrisService> orderedServices = List.of(
|
||||||
|
new PreservationSVC(),
|
||||||
|
new GlobalCacheSVC(),
|
||||||
|
new LogFilterSVC(),
|
||||||
|
new ExternalDataSVC(),
|
||||||
|
new EditSVC(),
|
||||||
|
new ObjectSVC(),
|
||||||
|
new ObjectStudioSaveService(),
|
||||||
|
new JigsawStudioService(),
|
||||||
|
new StudioSVC(),
|
||||||
|
new DatapackStructureScopeSVC(),
|
||||||
|
new IrisEngineSVC(),
|
||||||
|
new IrisTerrainSVC(),
|
||||||
|
new TreeSVC(),
|
||||||
|
new TreeFellerSVC(),
|
||||||
|
new EntityRiseSVC(),
|
||||||
|
new WandSVC(),
|
||||||
|
new BoardSVC(),
|
||||||
|
new IrisIntegrationService(),
|
||||||
|
new IrisProtocolService(),
|
||||||
|
new IrisApiEventSVC(),
|
||||||
|
new CommandSVC()
|
||||||
|
);
|
||||||
|
for (IrisService i : orderedServices) {
|
||||||
Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
|
Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
|
||||||
services.put(serviceType, i);
|
services.put(serviceType, i);
|
||||||
IrisServices.register(serviceType, i);
|
IrisServices.register(serviceType, i);
|
||||||
});
|
}
|
||||||
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
|
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
|
||||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||||
IO.delete(new File("iris"));
|
|
||||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||||
IrisServices.register(IrisCompat.class, compat);
|
IrisServices.register(IrisCompat.class, compat);
|
||||||
ServerConfigurator.configure();
|
ServerConfigurator.configure();
|
||||||
IrisToolbelt.applyPregenPerformanceProfile();
|
|
||||||
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
||||||
if (datapackValidation == StartupValidationOutcome.READY) {
|
if (datapackValidation == StartupValidationOutcome.READY) {
|
||||||
generatorResolver.validateAllPacks();
|
generatorResolver.validateAllPacks();
|
||||||
}
|
}
|
||||||
IrisSafeguard.execute();
|
IrisSafeguard.execute();
|
||||||
getSender().setTag(getTag());
|
getSender().setTag(getTag());
|
||||||
splash();
|
// A cosmetic banner must never abort the bootstrap.
|
||||||
|
J.attempt(this::splash);
|
||||||
IrisSafeguard.printReports();
|
IrisSafeguard.printReports();
|
||||||
IrisSafeguard.printFooter();
|
IrisSafeguard.printFooter();
|
||||||
tickets = new ChunkTickets();
|
tickets = new ChunkTickets();
|
||||||
@@ -594,20 +609,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
||||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
||||||
IO.delete(getTemp());
|
IO.delete(getTemp());
|
||||||
services.values().forEach(IrisService::onEnable);
|
// One throwing service must not abort the bootstrap: the steps after this loop
|
||||||
services.values().forEach(this::registerListener);
|
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
|
||||||
|
// Only services that actually enabled get listeners and a later onDisable.
|
||||||
|
enabledServices.clear();
|
||||||
|
IrisService firstFailedService = null;
|
||||||
|
for (IrisService service : orderedServices) {
|
||||||
|
try {
|
||||||
|
service.onEnable();
|
||||||
|
enabledServices.add(service);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (firstFailedService == null) {
|
||||||
|
firstFailedService = service;
|
||||||
|
}
|
||||||
|
Iris.reportError("Failed to enable " + service.getClass().getSimpleName() + "; continuing with a degraded runtime.", e);
|
||||||
|
// A failed service is excluded from the teardown loop, so clean up whatever
|
||||||
|
// its partial onEnable started right here, best-effort.
|
||||||
|
try {
|
||||||
|
service.onDisable();
|
||||||
|
} catch (Throwable cleanup) {
|
||||||
|
Iris.reportError("Failed to clean up partially enabled " + service.getClass().getSimpleName() + ".", cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstFailedService != null) {
|
||||||
|
IrisStartupValidation.markDatapacksInvalid("Iris service "
|
||||||
|
+ firstFailedService.getClass().getSimpleName()
|
||||||
|
+ " failed to enable; world creation and player admission are locked. Check the log above.");
|
||||||
|
}
|
||||||
|
for (IrisService service : enabledServices) {
|
||||||
|
try {
|
||||||
|
registerListener(service);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
addShutdownHook();
|
addShutdownHook();
|
||||||
pendingWorldReplacements.processPendingStartupReplacements();
|
pendingWorldReplacements.processPendingStartupReplacements();
|
||||||
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
||||||
WorldLifecycleService.get();
|
|
||||||
WorldRuntimeControlService.get();
|
|
||||||
|
|
||||||
if (J.isFolia() && IrisStartupValidation.isReady()) {
|
if (J.isFolia() && IrisStartupValidation.isReady()) {
|
||||||
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
|
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
pendingWorldReplacements.verifyLoadedPublishedWorlds();
|
pendingWorldReplacements.captureVanillaLevelContext();
|
||||||
|
// Off-main: the verify body takes the replacement-manager monitor and SHA-hashes
|
||||||
|
// whole pack trees; neither belongs on the tick thread.
|
||||||
|
J.a(pendingWorldReplacements::verifyLoadedPublishedWorlds);
|
||||||
J.a(this::bstats);
|
J.a(this::bstats);
|
||||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
||||||
J.sr(this::tickQueue, 0);
|
J.sr(this::tickQueue, 0);
|
||||||
@@ -620,20 +669,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
worldReconciler.checkForBukkitWorlds(s -> true);
|
worldReconciler.checkForBukkitWorlds(s -> true);
|
||||||
}
|
}
|
||||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||||
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
// The mantle stores block values as PlatformBlockState, so a BlockData retention can never
|
||||||
|
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
|
||||||
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addShutdownHook() {
|
public void addShutdownHook() {
|
||||||
if (shutdownHook != null) {
|
removeShutdownHook();
|
||||||
try {
|
|
||||||
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
Iris.debug("Skipping shutdown hook replacement because JVM shutdown is already in progress.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
|
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
|
||||||
try {
|
try {
|
||||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||||
@@ -642,6 +686,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The static-field guard in addShutdownHook is dead across a plugin reload (fresh
|
||||||
|
* classloader, fresh static), so onDisable must deregister the hook explicitly or each
|
||||||
|
* reload stacks another hook pinning the previous plugin classloader for the JVM's life.
|
||||||
|
*/
|
||||||
|
public void removeShutdownHook() {
|
||||||
|
Thread hook = shutdownHook;
|
||||||
|
shutdownHook = null;
|
||||||
|
if (hook == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().removeShutdownHook(hook);
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
|
Iris.debug("Skipping shutdown hook removal because JVM shutdown is already in progress.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public BukkitWorldReconciler worldReconciler() {
|
public BukkitWorldReconciler worldReconciler() {
|
||||||
return worldReconciler;
|
return worldReconciler;
|
||||||
}
|
}
|
||||||
@@ -692,16 +754,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisStartupValidation.begin();
|
IrisStartupValidation.begin();
|
||||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||||
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
||||||
enable();
|
boolean enabled = enable();
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
BukkitGuiHost.install();
|
BukkitGuiHost.install();
|
||||||
|
// super.onEnable() already registers this instance as a listener.
|
||||||
super.onEnable();
|
super.onEnable();
|
||||||
Bukkit.getPluginManager().registerEvents(this, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisSafeguard.isForceShutdown()) return;
|
|
||||||
teardownRuntime("onDisable", 30L);
|
teardownRuntime("onDisable", 30L);
|
||||||
|
removeShutdownHook();
|
||||||
|
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||||
if (BukkitPlatform.hasHud()) {
|
if (BukkitPlatform.hasHud()) {
|
||||||
BukkitPlatform.hudSlots().shutdown();
|
BukkitPlatform.hudSlots().shutdown();
|
||||||
BukkitPlatform.hudLanes().shutdown();
|
BukkitPlatform.hudLanes().shutdown();
|
||||||
@@ -710,12 +776,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
configHotloadEngine.clear();
|
configHotloadEngine.clear();
|
||||||
configHotloadEngine = null;
|
configHotloadEngine = null;
|
||||||
}
|
}
|
||||||
J.cancelPluginTasks();
|
|
||||||
HandlerList.unregisterAll((Plugin) this);
|
|
||||||
runPostShutdown();
|
runPostShutdown();
|
||||||
|
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||||
super.onDisable();
|
super.onDisable();
|
||||||
|
|
||||||
J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll);
|
|
||||||
IrisPlatforms.unbind();
|
IrisPlatforms.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +816,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
||||||
for (IrisService service : services.values()) {
|
// Only services whose onEnable actually completed; disabling a service that
|
||||||
|
// never initialized runs teardown against uninitialized state.
|
||||||
|
for (IrisService service : enabledServices) {
|
||||||
try {
|
try {
|
||||||
service.onDisable();
|
service.onDisable();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -768,6 +833,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
J.attempt(MultiBurst.burst::close);
|
J.attempt(MultiBurst.burst::close);
|
||||||
J.attempt(MultiBurst.ioBurst::close);
|
J.attempt(MultiBurst.ioBurst::close);
|
||||||
|
clearQueues();
|
||||||
IrisServices.clear();
|
IrisServices.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -169,7 +169,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME);
|
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
||||||
if (PackDownloader.isPackPresent(packsRoot, id)) {
|
if (PackDownloader.isPackPresent(packsRoot, id)) {
|
||||||
Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
|
Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
|
||||||
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
|
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
|
||||||
|
|||||||
+43
-9
@@ -254,7 +254,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
public void onWorldLoad(WorldLoadEvent event) {
|
public void onWorldLoad(WorldLoadEvent event) {
|
||||||
World world = event.getWorld();
|
World world = event.getWorld();
|
||||||
J.s(() -> verifyPublishedWorldIfPending(world), 1);
|
// One tick to let the load settle, then verify off-main: the body takes the manager
|
||||||
|
// monitor (held across pack staging by async threads) and SHA-hashes the whole pack
|
||||||
|
// tree — blocking the main thread on either froze the server.
|
||||||
|
J.s(() -> J.a(() -> verifyPublishedWorldIfPending(world)), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void verifyPublishedWorldIfPending(World world) {
|
private synchronized void verifyPublishedWorldIfPending(World world) {
|
||||||
@@ -506,10 +509,46 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the primary level context on the main thread at startup. resolveEffectiveSeed
|
||||||
|
* reads this snapshot so staging never blocks on a main-thread hop while holding the
|
||||||
|
* manager monitor — that inversion froze the server for the full 30s hop timeout whenever
|
||||||
|
* another world loaded during staging. The context is stable for the process lifetime:
|
||||||
|
* slot replacements only commit across a restart.
|
||||||
|
*/
|
||||||
|
public void captureVanillaLevelContext() {
|
||||||
|
try {
|
||||||
|
vanillaLevelContext = new VanillaLevelContext(
|
||||||
|
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
||||||
|
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
|
||||||
|
.getSeed(),
|
||||||
|
Iris.instance.getServer().getAllowNether(),
|
||||||
|
Iris.instance.getServer().getAllowEnd()
|
||||||
|
);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.debug("Could not capture the primary level context yet: " + detail(failure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static long resolveEffectiveSeed(SlotKind slotKind, long requestedSeed) throws IOException {
|
private static long resolveEffectiveSeed(SlotKind slotKind, long requestedSeed) throws IOException {
|
||||||
if (slotKind == SlotKind.IRIS_MANAGED) {
|
if (slotKind == SlotKind.IRIS_MANAGED) {
|
||||||
return requestedSeed;
|
return requestedSeed;
|
||||||
}
|
}
|
||||||
|
VanillaLevelContext context = vanillaLevelContext;
|
||||||
|
if (context == null) {
|
||||||
|
context = resolveVanillaLevelContext();
|
||||||
|
vanillaLevelContext = context;
|
||||||
|
}
|
||||||
|
if (slotKind == SlotKind.VANILLA_NETHER && !context.allowNether()) {
|
||||||
|
throw new IOException("allow-nether must be true before the vanilla Nether can be replaced.");
|
||||||
|
}
|
||||||
|
if (slotKind == SlotKind.VANILLA_END && !context.allowEnd()) {
|
||||||
|
throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced.");
|
||||||
|
}
|
||||||
|
return context.seed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VanillaLevelContext resolveVanillaLevelContext() throws IOException {
|
||||||
CompletableFuture<VanillaLevelContext> contextFuture = J.sfut(() -> new VanillaLevelContext(
|
CompletableFuture<VanillaLevelContext> contextFuture = J.sfut(() -> new VanillaLevelContext(
|
||||||
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
||||||
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
|
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
|
||||||
@@ -521,14 +560,7 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
throw new IOException("Could not schedule primary level-seed resolution.");
|
throw new IOException("Could not schedule primary level-seed resolution.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
VanillaLevelContext context = contextFuture.get(30L, TimeUnit.SECONDS);
|
return contextFuture.get(30L, TimeUnit.SECONDS);
|
||||||
if (slotKind == SlotKind.VANILLA_NETHER && !context.allowNether()) {
|
|
||||||
throw new IOException("allow-nether must be true before the vanilla Nether can be replaced.");
|
|
||||||
}
|
|
||||||
if (slotKind == SlotKind.VANILLA_END && !context.allowEnd()) {
|
|
||||||
throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced.");
|
|
||||||
}
|
|
||||||
return context.seed();
|
|
||||||
} catch (InterruptedException failure) {
|
} catch (InterruptedException failure) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new IOException("Primary level-seed resolution was interrupted.", failure);
|
throw new IOException("Primary level-seed resolution was interrupted.", failure);
|
||||||
@@ -580,6 +612,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static volatile VanillaLevelContext vanillaLevelContext;
|
||||||
|
|
||||||
private record VanillaLevelContext(long seed, boolean allowNether, boolean allowEnd) {
|
private record VanillaLevelContext(long seed, boolean allowNether, boolean allowEnd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -394,7 +394,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
|
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
|
||||||
public void goldenhash(
|
public void goldenhash(
|
||||||
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true)
|
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true)
|
||||||
World world,
|
World world,
|
||||||
@@ -404,7 +404,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
int centerX,
|
int centerX,
|
||||||
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
|
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
|
||||||
int centerZ,
|
int centerZ,
|
||||||
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
|
@Param(name = "reset-mantle", description = "Delete the world's entire mantle folder first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
|
||||||
boolean resetMantle,
|
boolean resetMantle,
|
||||||
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8")
|
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8")
|
||||||
int threads,
|
int threads,
|
||||||
|
|||||||
@@ -705,7 +705,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
public void download(
|
public void download(
|
||||||
@Param(name = "pack", description = "The pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", aliases = "project")
|
@Param(name = "pack", description = "The pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", aliases = "project")
|
||||||
String pack,
|
String pack,
|
||||||
@Param(name = "branch", description = "The branch to download from", descriptionKey = "iris.director.commandiris.param.branch_download_from", defaultValue = "stable")
|
@Param(name = "branch", description = "The branch to download from", descriptionKey = "iris.director.commandiris.param.branch_download_from", defaultValue = PackDownloader.DEFAULT_BRANCH)
|
||||||
String branch,
|
String branch,
|
||||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", descriptionKey = "iris.director.commandiris.param.whether_not_overwrite_pack_with_downloaded_one", aliases = "force", defaultValue = "false")
|
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", descriptionKey = "iris.director.commandiris.param.whether_not_overwrite_pack_with_downloaded_one", aliases = "force", defaultValue = "false")
|
||||||
boolean overwrite
|
boolean overwrite
|
||||||
@@ -803,7 +803,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
});
|
});
|
||||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||||
.whenComplete((unloaded, throwable) -> {
|
.whenComplete((unloaded, throwable) -> {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload");
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
lease.close();
|
lease.close();
|
||||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||||
@@ -812,7 +812,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
J.s(response);
|
J.s(response);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload");
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
lease.close();
|
lease.close();
|
||||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||||
|
|||||||
+3
-2
@@ -37,7 +37,6 @@ import art.arcane.iris.engine.object.IrisObject;
|
|||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
||||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||||
import art.arcane.iris.engine.object.StudioMode;
|
|
||||||
import art.arcane.iris.engine.object.TileData;
|
import art.arcane.iris.engine.object.TileData;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
@@ -144,7 +143,8 @@ public class CommandObject implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
|
// ObjectStudioActivation carries the buffet state; mutating the shared cached
|
||||||
|
// IrisDimension here leaked OBJECT_BUFFET into later pack exports of this dimension.
|
||||||
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
||||||
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
||||||
|
|
||||||
@@ -637,6 +637,7 @@ public class CommandObject implements DirectorExecutor {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||||
|
|||||||
+13
-4
@@ -42,9 +42,18 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
|||||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
|
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
|
||||||
public class CommandPack implements DirectorExecutor {
|
public class CommandPack implements DirectorExecutor {
|
||||||
|
/**
|
||||||
|
* Director treats a blank defaultValue as "required", which made the all-packs branch
|
||||||
|
* unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank
|
||||||
|
* value (the documented "pack=" escape hatch) still means every pack.
|
||||||
|
*/
|
||||||
|
static boolean wantsAllPacks(String pack) {
|
||||||
|
return pack == null || pack.isBlank() || "*".equals(pack.trim());
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"})
|
@Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"})
|
||||||
public void validate(
|
public void validate(
|
||||||
@Param(description = "The pack folder name to validate (leave empty for all)", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "")
|
@Param(description = "The pack folder name to validate, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "*")
|
||||||
String pack
|
String pack
|
||||||
) {
|
) {
|
||||||
VolmitSender s = sender();
|
VolmitSender s = sender();
|
||||||
@@ -54,7 +63,7 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pack == null || pack.isBlank()) {
|
if (wantsAllPacks(pack)) {
|
||||||
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||||
if (dirs.isEmpty()) {
|
if (dirs.isEmpty()) {
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE));
|
||||||
@@ -196,11 +205,11 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
|
|
||||||
@Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"})
|
@Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"})
|
||||||
public void status(
|
public void status(
|
||||||
@Param(description = "The pack folder name", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "")
|
@Param(description = "The pack folder name, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "*")
|
||||||
String pack
|
String pack
|
||||||
) {
|
) {
|
||||||
VolmitSender s = sender();
|
VolmitSender s = sender();
|
||||||
if (pack == null || pack.isBlank()) {
|
if (wantsAllPacks(pack)) {
|
||||||
if (PackValidationRegistry.snapshot().isEmpty()) {
|
if (PackValidationRegistry.snapshot().isEmpty()) {
|
||||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.bukkit.util.Vector;
|
|||||||
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||||
|
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
|
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
|
||||||
@@ -59,6 +60,12 @@ public class CommandPregen implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PregeneratorJob.getInstance() != null) {
|
||||||
|
// Reject like the modded adapter does; never silently kill a running job.
|
||||||
|
sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (sender().isPlayer() && access() == null) {
|
if (sender().isPlayer() && access() == null) {
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
||||||
|
|||||||
+47
-44
@@ -328,35 +328,50 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (radius <= 0 || radius > 2048) {
|
||||||
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||||
|
sender().sendMessage("Radius must be between 1 and 2048 chunks.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var sender = sender();
|
var sender = sender();
|
||||||
var player = player();
|
var player = player();
|
||||||
Thread.ofVirtual()
|
Thread.ofVirtual()
|
||||||
.start(() -> {
|
.start(() -> {
|
||||||
int d = radius * 2;
|
int d = radius * 2;
|
||||||
KMap<String, AtomicInteger> data = new KMap<>();
|
KMap<String, AtomicInteger> data = new KMap<>();
|
||||||
|
// Everything acquired below is released in the finally: a throw mid-scan
|
||||||
|
// previously leaked the whole sampler ForkJoinPool, the self-rescheduling
|
||||||
|
// progress task and both HUD claims for the rest of the server's uptime.
|
||||||
|
MultiBurst multiBurst = null;
|
||||||
|
HudSlotClaim titleClaim = null;
|
||||||
|
HudSlotClaim barClaim = null;
|
||||||
|
int c = -1;
|
||||||
|
try {
|
||||||
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
||||||
var multiBurst = new MultiBurst("Region Sampler");
|
multiBurst = new MultiBurst("Region Sampler");
|
||||||
var executor = multiBurst.burst(radius * radius);
|
var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20));
|
||||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
|
||||||
var loc = player.getLocation();
|
var loc = player.getLocation();
|
||||||
int totalTasks = d * d;
|
int totalTasks = d * d;
|
||||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||||
HudSlotClaim titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
|
titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
|
||||||
HudSlotClaim barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)));
|
barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)));
|
||||||
|
HudSlotClaim finalTitleClaim = titleClaim;
|
||||||
|
HudSlotClaim finalBarClaim = barClaim;
|
||||||
AtomicLong lastResolveMs = new AtomicLong(0L);
|
AtomicLong lastResolveMs = new AtomicLong(0L);
|
||||||
int c = J.ar(() -> {
|
c = J.ar(() -> {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (now - lastResolveMs.get() >= 250L) {
|
if (now - lastResolveMs.get() >= 250L) {
|
||||||
lastResolveMs.set(now);
|
lastResolveMs.set(now);
|
||||||
titleClaim.resolve();
|
finalTitleClaim.resolve();
|
||||||
barClaim.resolve();
|
finalBarClaim.resolve();
|
||||||
}
|
}
|
||||||
double jobProgress = (double) completedTasks.get() / totalTasks;
|
double jobProgress = (double) completedTasks.get() / totalTasks;
|
||||||
HudSurface barSurface = barClaim.granted();
|
HudSurface barSurface = finalBarClaim.granted();
|
||||||
sender.sendProgress(
|
sender.sendProgress(
|
||||||
jobProgress,
|
jobProgress,
|
||||||
IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS),
|
IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS),
|
||||||
titleClaim.granted(),
|
finalTitleClaim.granted(),
|
||||||
barSurface
|
barSurface
|
||||||
);
|
);
|
||||||
if (barSurface == HudSurface.BOSS_BAR) {
|
if (barSurface == HudSurface.BOSS_BAR) {
|
||||||
@@ -372,15 +387,28 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
completedTasks.incrementAndGet();
|
completedTasks.incrementAndGet();
|
||||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||||
executor.complete();
|
executor.complete();
|
||||||
multiBurst.close();
|
|
||||||
J.car(c);
|
|
||||||
titleClaim.release();
|
|
||||||
barClaim.release();
|
|
||||||
BukkitPlatform.hudLanes().hide(player, "iris:job");
|
|
||||||
|
|
||||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE));
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE));
|
||||||
var loader = engine.getData().getRegionLoader();
|
var loader = engine.getData().getRegionLoader();
|
||||||
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
|
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||||
|
} finally {
|
||||||
|
if (c != -1) {
|
||||||
|
J.car(c);
|
||||||
|
}
|
||||||
|
if (titleClaim != null) {
|
||||||
|
titleClaim.release();
|
||||||
|
}
|
||||||
|
if (barClaim != null) {
|
||||||
|
barClaim.release();
|
||||||
|
BukkitPlatform.hudLanes().hide(player, "iris:job");
|
||||||
|
}
|
||||||
|
if (multiBurst != null) {
|
||||||
|
multiBurst.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +465,6 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
||||||
KMap<String, Double> generatorTimings = new KMap<>();
|
KMap<String, Double> generatorTimings = new KMap<>();
|
||||||
KMap<String, Double> biomeTimings = new KMap<>();
|
KMap<String, Double> biomeTimings = new KMap<>();
|
||||||
KMap<String, Double> regionTimings = new KMap<>();
|
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS));
|
||||||
|
|
||||||
@@ -561,23 +588,6 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
|
|
||||||
fileText.add("");
|
fileText.add("");
|
||||||
|
|
||||||
for (String i : data.getRegionLoader().getPossibleKeys()) {
|
|
||||||
IrisRegion b = data.getRegionLoader().load(i);
|
|
||||||
double score = 0;
|
|
||||||
|
|
||||||
score += styleTimings.get(b.getLakeStyle().getStyle());
|
|
||||||
score += styleTimings.get(b.getRiverStyle().getStyle());
|
|
||||||
regionTimings.put(i, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
fileText.add("Project Region Performance Impacts: ");
|
|
||||||
|
|
||||||
for (String i : regionTimings.sortKNumber()) {
|
|
||||||
fileText.add(i + ": " + regionTimings.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
fileText.add("");
|
|
||||||
|
|
||||||
double m = 0;
|
double m = 0;
|
||||||
for (double i : biomeTimings.v()) {
|
for (double i : biomeTimings.v()) {
|
||||||
m += i;
|
m += i;
|
||||||
@@ -589,12 +599,6 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
mm /= generatorTimings.size();
|
mm /= generatorTimings.size();
|
||||||
m += mm;
|
m += mm;
|
||||||
double mmm = 0;
|
|
||||||
for (double i : regionTimings.v()) {
|
|
||||||
mmm += i;
|
|
||||||
}
|
|
||||||
mmm /= regionTimings.size();
|
|
||||||
m += mmm;
|
|
||||||
|
|
||||||
fileText.add("Average Score: " + m);
|
fileText.add("Average Score: " + m);
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0))));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0))));
|
||||||
@@ -763,9 +767,8 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
|
||||||
try {
|
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
try (PrintWriter pw = new PrintWriter(ff)) {
|
||||||
PrintWriter pw = new PrintWriter(ff);
|
|
||||||
pw.println("=== Iris Chunk Report ===");
|
pw.println("=== Iris Chunk Report ===");
|
||||||
pw.println("== General Info ==");
|
pw.println("== General Info ==");
|
||||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||||
@@ -820,7 +823,8 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length;
|
String[] regionFiles = new File(world.getWorldFolder(), "region").list();
|
||||||
|
regions = regionFiles == null ? 0 : regionFiles.length;
|
||||||
|
|
||||||
pw.println();
|
pw.println();
|
||||||
pw.println("== World Info ==");
|
pw.println("== World Info ==");
|
||||||
@@ -854,10 +858,9 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pw.println();
|
pw.println();
|
||||||
pw.close();
|
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
|
||||||
} catch (FileNotFoundException e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-15
@@ -26,7 +26,10 @@ import art.arcane.iris.core.edit.BlockSignal;
|
|||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||||
@@ -36,8 +39,10 @@ import art.arcane.volmlib.util.director.DirectorOrigin;
|
|||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||||
import org.bukkit.Chunk;
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.Location;
|
||||||
import org.bukkit.FluidCollisionMode;
|
import org.bukkit.FluidCollisionMode;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -47,7 +52,6 @@ import org.bukkit.block.data.BlockData;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||||
@@ -172,35 +176,73 @@ public class CommandWhat implements DirectorExecutor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches ModdedWhatCommands.MAX_MARKERS so both loaders truncate identically.
|
||||||
|
private static final int MAX_MARKER_HITS = 8_192;
|
||||||
|
private static final int MARKER_SIGNAL_BATCH = 128;
|
||||||
|
|
||||||
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
|
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
|
||||||
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
|
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
|
||||||
VolmitSender commandSender = sender();
|
VolmitSender commandSender = sender();
|
||||||
Player player = player();
|
Player player = player();
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
|
// Only the chunk coordinates need the owning thread; the 81-chunk mantle sweep loads
|
||||||
|
// tectonic plates from disk and must never run on the tick thread (the modded adapter
|
||||||
|
// already scans async, leased and capped — mirror it).
|
||||||
onPlayerThread(player, () -> {
|
onPlayerThread(player, () -> {
|
||||||
|
World world = player.getWorld();
|
||||||
Chunk c = player.getLocation().getChunk();
|
Chunk c = player.getLocation().getChunk();
|
||||||
|
int chunkX = c.getX();
|
||||||
|
int chunkZ = c.getZ();
|
||||||
|
|
||||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
AtomicInteger v = new AtomicInteger(0);
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
|
||||||
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
|
J.a(() -> {
|
||||||
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
|
KList<Location> hits = new KList<>();
|
||||||
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
|
MatterMarker matterMarker = new MatterMarker(marker);
|
||||||
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers");
|
||||||
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
v.incrementAndGet();
|
scan:
|
||||||
});
|
for (int xxx = chunkX - 4; xxx <= chunkX + 4; xxx++) {
|
||||||
|
for (int zzz = chunkZ - 4; zzz <= chunkZ + 4; zzz++) {
|
||||||
|
for (Location i : engine.getMantle().findMarkers(xxx, zzz, matterMarker)
|
||||||
|
.convert((p) -> BukkitPlatform.toLocation(p, world))) {
|
||||||
|
hits.add(i);
|
||||||
|
if (hits.size() >= MAX_MARKER_HITS) {
|
||||||
|
break scan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker)));
|
||||||
} else {
|
emitMarkerSignals(player, engine, hits, 0);
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void emitMarkerSignals(Player player, Engine engine, KList<Location> hits, int from) {
|
||||||
|
if (from >= hits.size() || !player.isOnline() || engine.isClosed() || engine.isClosing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int to = Math.min(from + MARKER_SIGNAL_BATCH, hits.size());
|
||||||
|
for (int i = from; i < to; i++) {
|
||||||
|
Location at = hits.get(i);
|
||||||
|
BlockSignal.of(at.getWorld(), at.getBlockX(), at.getBlockY(), at.getBlockZ(), 100);
|
||||||
|
}
|
||||||
|
J.s(() -> emitMarkerSignals(player, engine, hits, to), 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+19
-5
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core.service;
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.IrisStartupValidation;
|
||||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
|
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
|
||||||
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
|
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
|
||||||
@@ -29,13 +30,26 @@ public final class DatapackStructureScopeSVC implements IrisService {
|
|||||||
scopeIndex = DatapackStructureScopeIndex.create(
|
scopeIndex = DatapackStructureScopeIndex.create(
|
||||||
DatapackIngestService.installedStructureScopeResources());
|
DatapackIngestService.installedStructureScopeResources());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException(
|
// A missing manifest entry is the condition validateOnStartup already classified as
|
||||||
"Iris could not establish ownership for installed datapack structure sets", e);
|
// non-fatal; degrade to an empty scope and lock world creation instead of aborting
|
||||||
|
// the whole plugin bootstrap (which would skip listeners and journal reconciliation).
|
||||||
|
scopeIndex = DatapackStructureScopeIndex.create(null);
|
||||||
|
IrisLogging.error("Could not establish ownership for installed datapack structure sets; "
|
||||||
|
+ "datapack structure scoping is disabled until the manifest is repaired: " + e.getMessage());
|
||||||
|
IrisStartupValidation.markDatapacksInvalid(
|
||||||
|
"Iris could not establish ownership for installed datapack structure sets: " + e.getMessage());
|
||||||
}
|
}
|
||||||
for (World world : Bukkit.getWorlds()) {
|
for (World world : Bukkit.getWorlds()) {
|
||||||
IrisImportedStructureControl importedStructures = importedStructures(world);
|
try {
|
||||||
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
|
IrisImportedStructureControl importedStructures = importedStructures(world);
|
||||||
applyScope(world, importedStructures);
|
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
|
||||||
|
applyScope(world, importedStructures);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// Per-world containment: one world's scoping failure must not abort the
|
||||||
|
// service enable (and with it the whole bootstrap's containment contract).
|
||||||
|
IrisLogging.error("Could not scope datapack structures for world '" + world.getName() + "'.");
|
||||||
|
IrisLogging.reportError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import org.bukkit.event.world.WorldUnloadEvent;
|
|||||||
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
|
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
|
||||||
private KMap<World, BlockEditor> editors;
|
private KMap<World, BlockEditor> editors;
|
||||||
private int updateTaskId = -1;
|
private int updateTaskId = -1;
|
||||||
public static boolean deletingWorld = false;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -84,7 +83,7 @@ public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, B
|
|||||||
if (editors == null) {
|
if (editors == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
|
if (editors.containsKey(e.getWorld())) {
|
||||||
editors.remove(e.getWorld()).close();
|
editors.remove(e.getWorld()).close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ public class IrisStartupOrderingTest {
|
|||||||
@Test
|
@Test
|
||||||
public void externalDatapacksValidateBeforeDimensionPacks() throws Exception {
|
public void externalDatapacksValidateBeforeDimensionPacks() throws Exception {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
String enable = section(source, "private void enable()", "public void addShutdownHook()");
|
String enable = section(source, "private boolean enable()", "public void addShutdownHook()");
|
||||||
|
|
||||||
assertOrdered(enable,
|
assertOrdered(enable,
|
||||||
"DatapackIngestService.validateOnStartup();",
|
"DatapackIngestService.validateOnStartup();",
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class CommandIrisDownloadDefaultTest {
|
||||||
|
@Test
|
||||||
|
public void downloadBranchParamDefaultsToTheCoreConstant() throws Exception {
|
||||||
|
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/commands/CommandIris.java"));
|
||||||
|
int branchParam = source.indexOf("name = \"branch\"");
|
||||||
|
assertTrue("CommandIris must declare the branch param", branchParam >= 0);
|
||||||
|
String declaration = source.substring(branchParam, source.indexOf(')', branchParam));
|
||||||
|
assertTrue("branch param default must be PackDownloader.DEFAULT_BRANCH, not a literal",
|
||||||
|
declaration.contains("defaultValue = PackDownloader.DEFAULT_BRANCH"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility;
|
||||||
|
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
|
||||||
|
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class CommandPackOptionalPackContractTest {
|
||||||
|
private static DirectorParameterDescriptor packParam(String method) throws Exception {
|
||||||
|
DirectorNodeDescriptor node = DirectorAnnotationCompatibility
|
||||||
|
.fromMethod(CommandPack.class.getDeclaredMethod(method, String.class))
|
||||||
|
.orElseThrow();
|
||||||
|
DirectorParameterDescriptor pack = node.getParameters().get(0);
|
||||||
|
assertEquals("pack", pack.getName());
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bareValidateAndStatusAreInvocableWithoutAPackArgument() throws Exception {
|
||||||
|
for (String method : List.of("validate", "status")) {
|
||||||
|
DirectorParameterDescriptor pack = packParam(method);
|
||||||
|
assertFalse(method + " must not require a pack", pack.isRequired());
|
||||||
|
assertFalse(method + " needs a non-blank default for Director to treat it as optional",
|
||||||
|
pack.getDefaultValue().isBlank());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cleanupAndRestoreStillRequireAPack() throws Exception {
|
||||||
|
for (String method : List.of("cleanup", "restore")) {
|
||||||
|
DirectorNodeDescriptor node = DirectorAnnotationCompatibility
|
||||||
|
.fromMethod(findMethod(method))
|
||||||
|
.orElseThrow();
|
||||||
|
DirectorParameterDescriptor pack = node.getParameters().get(0);
|
||||||
|
assertEquals("pack", pack.getName());
|
||||||
|
assertTrue(method + " must keep requiring a pack", pack.isRequired());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allPacksPredicateAcceptsSentinelBlankAndNull() {
|
||||||
|
assertTrue(CommandPack.wantsAllPacks(null));
|
||||||
|
assertTrue(CommandPack.wantsAllPacks(""));
|
||||||
|
assertTrue(CommandPack.wantsAllPacks(" "));
|
||||||
|
assertTrue(CommandPack.wantsAllPacks("*"));
|
||||||
|
assertFalse(CommandPack.wantsAllPacks("overworld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.lang.reflect.Method findMethod(String name) {
|
||||||
|
for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) {
|
||||||
|
if (method.getName().equals(name)) {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("CommandPack must declare " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -46,7 +46,10 @@ public final class IrisClientPregenState {
|
|||||||
jobs.remove(jobId);
|
jobs.remove(jobId);
|
||||||
Long current = activeJobId;
|
Long current = activeJobId;
|
||||||
if (current != null && current == jobId) {
|
if (current != null && current == jobId) {
|
||||||
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
|
// Promote nothing: the server runs a single live job, so any remaining entry is
|
||||||
|
// dead or orphaned — and the access-ordered map's head is the STALEST entry,
|
||||||
|
// which is what the HUD used to promote.
|
||||||
|
activeJobId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -161,7 +161,9 @@ public final class NativeStructureVegetationClearer {
|
|||||||
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
|
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
// Field reads instead of a registry byValue map hit — this predicate runs per block
|
||||||
|
// probe on the hottest native-structure scans.
|
||||||
|
String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath();
|
||||||
return path.endsWith("_log") || path.endsWith("_wood")
|
return path.endsWith("_log") || path.endsWith("_wood")
|
||||||
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|
||||||
|| path.endsWith("_leaves");
|
|| path.endsWith("_leaves");
|
||||||
|
|||||||
+2
-2
@@ -815,10 +815,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
int localY = blockY & 15;
|
int localY = blockY & 15;
|
||||||
for (int z = 0; z < 16; z++) {
|
for (int z = 0; z < 16; z++) {
|
||||||
for (int x = 0; x < 16; x++) {
|
for (int x = 0; x < 16; x++) {
|
||||||
if (blocks.isAir(x, bufferY, z)) {
|
PlatformBlockState state = blocks.rawOrNull(x, bufferY, z);
|
||||||
|
if (state == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
|
|
||||||
BlockState blockState = (BlockState) state.nativeHandle();
|
BlockState blockState = (BlockState) state.nativeHandle();
|
||||||
section.setBlockState(x, localY, z, blockState, false);
|
section.setBlockState(x, localY, z, blockState, false);
|
||||||
if (blockState.hasBlockEntity()) {
|
if (blockState.hasBlockEntity()) {
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ public final class ModdedBlockBuffer implements Hunk<PlatformBlockState> {
|
|||||||
return data[index(x, y, z)] == null;
|
return data[index(x, y, z)] == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct slot read, null when unset — lets writeBlocks pay one index + one array load per
|
||||||
|
* block instead of the isAir + getRaw pair.
|
||||||
|
*/
|
||||||
|
public PlatformBlockState rawOrNull(int x, int y, int z) {
|
||||||
|
return data[index(x, y, z)];
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return 16;
|
return 16;
|
||||||
|
|||||||
+2
@@ -226,6 +226,8 @@ public final class ModdedDimensionManager {
|
|||||||
ModdedWorldEngines.evictOrThrow(level);
|
ModdedWorldEngines.evictOrThrow(level);
|
||||||
level.save(null, true, false);
|
level.save(null, true, false);
|
||||||
serverAccess.removeLevel(server, key);
|
serverAccess.removeLevel(server, key);
|
||||||
|
// Undo snapshots pin the ServerLevel and could replay into the dead level.
|
||||||
|
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
|
||||||
level.close();
|
level.close();
|
||||||
HANDLES.remove(dimensionId);
|
HANDLES.remove(dimensionId);
|
||||||
if (wipeStorage) {
|
if (wipeStorage) {
|
||||||
|
|||||||
+17
-1
@@ -47,6 +47,7 @@ import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
|||||||
import art.arcane.iris.spi.IrisPlatform;
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
@@ -99,6 +100,11 @@ public final class ModdedEngineBootstrap {
|
|||||||
captureInitialSpawn(server);
|
captureInitialSpawn(server);
|
||||||
currentServer = server;
|
currentServer = server;
|
||||||
bind();
|
bind();
|
||||||
|
// Pair of the stop() burst-pools stage. Load-bearing on integrated servers: once
|
||||||
|
// closed, MultiBurst falls back to a same-thread executor, so a second world load
|
||||||
|
// without reopen() would silently run every burst inline.
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
ModdedScheduler scheduler = schedulerOrNull();
|
ModdedScheduler scheduler = schedulerOrNull();
|
||||||
if (scheduler != null) {
|
if (scheduler != null) {
|
||||||
scheduler.reset();
|
scheduler.reset();
|
||||||
@@ -170,6 +176,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||||
|
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
||||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||||
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
|
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
|
||||||
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
|
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
|
||||||
@@ -183,6 +190,10 @@ public final class ModdedEngineBootstrap {
|
|||||||
failure = runStopStage(failure, "level snapshot", ModdedServerLevels::forget);
|
failure = runStopStage(failure, "level snapshot", ModdedServerLevels::forget);
|
||||||
}
|
}
|
||||||
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
|
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
|
||||||
|
failure = runStopStage(failure, "burst pools", () -> {
|
||||||
|
MultiBurst.burst.close();
|
||||||
|
MultiBurst.ioBurst.close();
|
||||||
|
});
|
||||||
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
|
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
|
||||||
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
|
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
|
||||||
failure = runStopStage(failure, "server state", () -> {
|
failure = runStopStage(failure, "server state", () -> {
|
||||||
@@ -386,7 +397,12 @@ public final class ModdedEngineBootstrap {
|
|||||||
ModdedCustomContentRegistry.Discovery customContentDiscovery =
|
ModdedCustomContentRegistry.Discovery customContentDiscovery =
|
||||||
ModdedCustomContentRegistry.discover();
|
ModdedCustomContentRegistry.discover();
|
||||||
rollback.add(customContentDiscovery::rollback);
|
rollback.add(customContentDiscovery::rollback);
|
||||||
ModdedIrisSplash.print(boundLoader);
|
try {
|
||||||
|
ModdedIrisSplash.print(boundLoader);
|
||||||
|
} catch (Throwable splashFailure) {
|
||||||
|
// A cosmetic banner must never roll back the platform bind.
|
||||||
|
LOGGER.warn("Iris splash could not be printed", splashFailure);
|
||||||
|
}
|
||||||
createdServices.enableAll();
|
createdServices.enableAll();
|
||||||
runtime = new BoundRuntime(created, createdServices);
|
runtime = new BoundRuntime(created, createdServices);
|
||||||
rollback.clear();
|
rollback.clear();
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||||
@@ -734,7 +735,7 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Path packsRoot() {
|
private static Path packsRoot() {
|
||||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
|
return IrisPlatforms.get().packsFolderNoCreate().toPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private record PublishedState(Path directory, String packsHash) {
|
private record PublishedState(Path directory, String packsHash) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.splash.IrisSplashComposer;
|
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||||
import art.arcane.iris.core.splash.IrisSplashRenderer;
|
import art.arcane.iris.core.splash.IrisSplashRenderer;
|
||||||
@@ -38,7 +39,7 @@ public final class ModdedIrisSplash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void printPacks(ModdedLoader loader) {
|
private static void printPacks(ModdedLoader loader) {
|
||||||
File packFolder = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
|
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
|
||||||
for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) {
|
for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) {
|
||||||
IrisLogging.info(line);
|
IrisLogging.info(line);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,13 +116,68 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modded packs live under config/irisworldgen/packs, not the config/iris data folder. The
|
||||||
|
* packsFolder overrides are the source of truth; the dataFolder/dataFile overrides below
|
||||||
|
* re-root any path whose FIRST segment is exactly "packs" so no call site can regress onto
|
||||||
|
* the empty config/iris/packs directory. Settings, worlds.json, parity/, cache/ and every
|
||||||
|
* other name stay under config/iris.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public File packsFolder(String... sub) {
|
||||||
|
File folder = packsFolderNoCreate(sub);
|
||||||
|
folder.mkdirs();
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File packsFolderNoCreate(String... sub) {
|
||||||
|
File root = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
|
||||||
|
if (sub == null || sub.length == 0) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
return new File(root, String.join(File.separator, sub));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File dataFolder(String... path) {
|
||||||
|
if (isPacksPath(path)) {
|
||||||
|
return packsFolder(stripPacksSegment(path));
|
||||||
|
}
|
||||||
|
return IrisPlatform.super.dataFolder(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File dataFolderNoCreate(String... path) {
|
||||||
|
if (isPacksPath(path)) {
|
||||||
|
return packsFolderNoCreate(stripPacksSegment(path));
|
||||||
|
}
|
||||||
|
return IrisPlatform.super.dataFolderNoCreate(path);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File dataFile(String... path) {
|
public File dataFile(String... path) {
|
||||||
|
if (isPacksPath(path)) {
|
||||||
|
File file = packsFolderNoCreate(stripPacksSegment(path));
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
File file = new File(dataFolder(), String.join(File.separator, path));
|
File file = new File(dataFolder(), String.join(File.separator, path));
|
||||||
file.getParentFile().mkdirs();
|
file.getParentFile().mkdirs();
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPacksPath(String... path) {
|
||||||
|
// Exact-segment match only: "packbenchmarks" and "packsx" must stay under config/iris.
|
||||||
|
return path != null && path.length > 0 && "packs".equals(path[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] stripPacksSegment(String... path) {
|
||||||
|
String[] sub = new String[path.length - 1];
|
||||||
|
System.arraycopy(path, 1, sub, 0, sub.length);
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File pluginJar() {
|
public File pluginJar() {
|
||||||
File jar = loader.modJar();
|
File jar = loader.modJar();
|
||||||
|
|||||||
+5
@@ -50,6 +50,11 @@ final class ModdedSpawnTableMerger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||||
|
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
|
||||||
|
// on the server thread, and the generator monitor is contended by binds/hotloads.
|
||||||
|
if (vanillaSpawnBiomesInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
synchronized (generator) {
|
synchronized (generator) {
|
||||||
if (vanillaSpawnBiomesInitialized) {
|
if (vanillaSpawnBiomesInitialized) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -59,9 +59,36 @@ public final class ModdedStartup {
|
|||||||
if (!PREPARED.compareAndSet(false, true)) {
|
if (!PREPARED.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
reportLegacyPacksDirectory();
|
||||||
validateAllPacks();
|
validateAllPacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
|
||||||
|
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
|
||||||
|
* the legacy directory holds packs, and quietly remove it when it is empty.
|
||||||
|
*/
|
||||||
|
private static void reportLegacyPacksDirectory() {
|
||||||
|
try {
|
||||||
|
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
|
||||||
|
if (!legacy.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
|
||||||
|
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
|
||||||
|
LOGGER.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
|
||||||
|
legacy.getAbsolutePath(), real.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] entries = legacy.list();
|
||||||
|
if (entries == null || entries.length == 0) {
|
||||||
|
legacy.delete();
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.debug("Iris legacy packs directory check failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
||||||
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
||||||
|
|||||||
+19
-9
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
@@ -26,6 +27,7 @@ import art.arcane.iris.engine.framework.EngineTarget;
|
|||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.dimension.DimensionType;
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
@@ -76,6 +78,8 @@ public final class ModdedWorldEngines {
|
|||||||
if (removed[0] == null) {
|
if (removed[0] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The GUI host holds strong Engine/ServerLevel references with no other remove path.
|
||||||
|
ModdedGuiHost.unbind(removed[0]);
|
||||||
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
|
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +93,7 @@ public final class ModdedWorldEngines {
|
|||||||
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
|
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
|
||||||
if (current != null && current != activeReplacement) {
|
if (current != null && current != activeReplacement) {
|
||||||
close(current);
|
close(current);
|
||||||
|
ModdedGuiHost.unbind(current);
|
||||||
}
|
}
|
||||||
return activeReplacement;
|
return activeReplacement;
|
||||||
});
|
});
|
||||||
@@ -96,6 +101,7 @@ public final class ModdedWorldEngines {
|
|||||||
|
|
||||||
static void closeUnregistered(Engine engine) {
|
static void closeUnregistered(Engine engine) {
|
||||||
close(engine);
|
close(engine);
|
||||||
|
ModdedGuiHost.unbind(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||||
@@ -167,11 +173,7 @@ public final class ModdedWorldEngines {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static File packFolder(String pack) {
|
public static File packFolder(String pack) {
|
||||||
return ModdedEngineBootstrap.loader().configDir()
|
return IrisPlatforms.get().packsFolderNoCreate(pack);
|
||||||
.resolve("irisworldgen")
|
|
||||||
.resolve("packs")
|
|
||||||
.resolve(pack)
|
|
||||||
.toFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static File resolvePack(String pack, String dimensionKey) {
|
static File resolvePack(String pack, String dimensionKey) {
|
||||||
@@ -200,10 +202,18 @@ public final class ModdedWorldEngines {
|
|||||||
ServerLevel level = entry.getKey();
|
ServerLevel level = entry.getKey();
|
||||||
Engine engine = entry.getValue();
|
Engine engine = entry.getValue();
|
||||||
try {
|
try {
|
||||||
close(engine);
|
// Latch the generator's unloading flag BEFORE closing (unbindEngine sets it,
|
||||||
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
|
// then evicts): chunk-system drain work running after this stage would
|
||||||
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
|
// otherwise see a closed engine and silently rebuild a fresh engine + Mantle
|
||||||
+ level.dimension().identifier());
|
// that no teardown stage ever closes, writing plates after the final save.
|
||||||
|
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||||
|
generator.unbindEngine(level);
|
||||||
|
} else {
|
||||||
|
close(engine);
|
||||||
|
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
|
||||||
|
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
|
||||||
|
+ level.dimension().identifier());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LOGGER.info("Iris engine closed for {}", level.dimension().identifier());
|
LOGGER.info("Iris engine closed for {}", level.dimension().identifier());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|||||||
+4
-18
@@ -565,9 +565,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
|
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -605,9 +603,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
int worldZ = position.getZ();
|
int worldZ = position.getZ();
|
||||||
int spawned = 0;
|
int spawned = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
|
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -774,18 +770,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
||||||
int totalRarity = 0;
|
KList<IrisEntitySpawn> weighted = IRare.expandWeighted(entries);
|
||||||
for (IrisEntitySpawn entry : entries) {
|
return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom();
|
||||||
totalRarity += IRare.get(entry);
|
|
||||||
}
|
|
||||||
if (totalRarity <= 0) {
|
|
||||||
return entries.getRandom();
|
|
||||||
}
|
|
||||||
KList<IrisEntitySpawn> weighted = new KList<>();
|
|
||||||
for (IrisEntitySpawn entry : entries) {
|
|
||||||
weighted.addMultiple(entry, totalRarity / IRare.get(entry));
|
|
||||||
}
|
|
||||||
return weighted.getRandom();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long pack(int x, int z) {
|
private static long pack(int x, int z) {
|
||||||
|
|||||||
@@ -178,10 +178,16 @@ public final class IrisModdedAPI {
|
|||||||
/**
|
/**
|
||||||
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
|
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
|
||||||
* <p>
|
* <p>
|
||||||
* Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod
|
* Iris drops slices it does not need once a region's generation data has served its purpose - both the
|
||||||
* writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
|
* normal per-chunk trim and pregeneration's forced cleanup honor this registry. Any type a mod writes
|
||||||
|
* with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
|
||||||
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
|
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
|
||||||
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored.
|
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored, and the
|
||||||
|
* block-state slice can never be retained.
|
||||||
|
* <p>
|
||||||
|
* Retained data lives for the world's lifetime: it persists into the mantle region files and unloads
|
||||||
|
* with them, so region files grow with everything you retain. The mod owns the cleanup - call
|
||||||
|
* {@code deleteMantleData} when a value is no longer needed.
|
||||||
*/
|
*/
|
||||||
public static void retainMantleDataForSlice(Class<?> sliceType) {
|
public static void retainMantleDataForSlice(Class<?> sliceType) {
|
||||||
if (sliceType == null) {
|
if (sliceType == null) {
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
@@ -230,7 +231,7 @@ final class ModdedCommandSuggestions {
|
|||||||
Set<String> names = new TreeSet<>();
|
Set<String> names = new TreeSet<>();
|
||||||
names.add("overworld");
|
names.add("overworld");
|
||||||
try {
|
try {
|
||||||
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
|
File packs = IrisPlatforms.get().packsFolderNoCreate();
|
||||||
for (File child : PackDirectoryResolver.listVisiblePackDirectories(packs)) {
|
for (File child : PackDirectoryResolver.listVisiblePackDirectories(packs)) {
|
||||||
String packName = child.getName();
|
String packName = child.getName();
|
||||||
names.add(packName);
|
names.add(packName);
|
||||||
|
|||||||
+4
-3
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||||
@@ -190,15 +191,15 @@ final class ModdedCommandTree {
|
|||||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
|
||||||
.executes((CommandContext<CommandSourceStack> context) ->
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
IrisModdedCommands.download(context.getSource(),
|
IrisModdedCommands.download(context.getSource(),
|
||||||
StringArgumentType.getString(context, "pack"), "stable", false))
|
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, false))
|
||||||
.then(Commands.literal("force")
|
.then(Commands.literal("force")
|
||||||
.executes((CommandContext<CommandSourceStack> context) ->
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
IrisModdedCommands.download(context.getSource(),
|
IrisModdedCommands.download(context.getSource(),
|
||||||
StringArgumentType.getString(context, "pack"), "stable", true)))
|
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, true)))
|
||||||
.then(Commands.argument("overwrite", BoolArgumentType.bool())
|
.then(Commands.argument("overwrite", BoolArgumentType.bool())
|
||||||
.executes((CommandContext<CommandSourceStack> context) ->
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
IrisModdedCommands.download(context.getSource(),
|
IrisModdedCommands.download(context.getSource(),
|
||||||
StringArgumentType.getString(context, "pack"), "stable",
|
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH,
|
||||||
BoolArgumentType.getBool(context, "overwrite"))))
|
BoolArgumentType.getBool(context, "overwrite"))))
|
||||||
.then(Commands.argument("branch", StringArgumentType.word())
|
.then(Commands.argument("branch", StringArgumentType.word())
|
||||||
.executes((CommandContext<CommandSourceStack> context) ->
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
|
|||||||
+3
@@ -188,6 +188,9 @@ public final class ModdedDustRevealer {
|
|||||||
private static void revealBatch(ModdedScheduler scheduler, RevealRun run,
|
private static void revealBatch(ModdedScheduler scheduler, RevealRun run,
|
||||||
List<BlockPos> hits, int from) {
|
List<BlockPos> hits, int from) {
|
||||||
if (!active(run)) {
|
if (!active(run)) {
|
||||||
|
// Drop the registry entry on abort too, or the run record pins the player, level
|
||||||
|
// and engine until server stop. No-op if a newer run already replaced it.
|
||||||
|
ACTIVE_RUNS.remove(run.playerId(), run);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int to = Math.min(hits.size(), from + PARTICLE_BATCH_SIZE);
|
int to = Math.min(hits.size(), from + PARTICLE_BATCH_SIZE);
|
||||||
|
|||||||
+23
@@ -55,6 +55,29 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the GUI binding for an evicted engine. Without this the host pinned every
|
||||||
|
* GUI-bound Engine, its ServerLevel and transitively the MinecraftServer for the process
|
||||||
|
* lifetime — there was no remove path at all.
|
||||||
|
*/
|
||||||
|
public static void unbind(Engine engine) {
|
||||||
|
if (engine == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
INSTANCE.levels.remove(engine);
|
||||||
|
INSTANCE.openers.remove(engine);
|
||||||
|
if (INSTANCE.active == engine) {
|
||||||
|
INSTANCE.active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
INSTANCE.levels.clear();
|
||||||
|
INSTANCE.openers.clear();
|
||||||
|
INSTANCE.active = null;
|
||||||
|
INSTANCE.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isGuiLaunchable() {
|
public static boolean isGuiLaunchable() {
|
||||||
return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis();
|
return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis();
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -543,6 +543,16 @@ final class ModdedLocateCommands {
|
|||||||
|
|
||||||
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
|
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||||
ServerPlayer player, String label, Position2 at) {
|
ServerPlayer player, String label, Position2 at) {
|
||||||
|
// Same liveness guards the structure completion path has: the search can take up to
|
||||||
|
// two minutes, and the captured ServerPlayer may be gone or elsewhere by then.
|
||||||
|
if (player.hasDisconnected() || player.isRemoved()) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (player.level() != level) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN));
|
||||||
|
return;
|
||||||
|
}
|
||||||
int blockX = (at.getX() << 4) + 8;
|
int blockX = (at.getX() << 4) + 8;
|
||||||
int blockZ = (at.getZ() << 4) + 8;
|
int blockZ = (at.getZ() << 4) + 8;
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
||||||
|
|||||||
+27
-22
@@ -291,30 +291,35 @@ public final class ModdedObjectCommands {
|
|||||||
}
|
}
|
||||||
int[] tilesSkipped = {0};
|
int[] tilesSkipped = {0};
|
||||||
int[] tilesSaved = {0};
|
int[] tilesSaved = {0};
|
||||||
|
// capture() must stay on the server thread (getBlockState/getBlockEntity are not
|
||||||
|
// async-safe), but the disk write of a local, unshared object is not tick work.
|
||||||
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
|
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
|
||||||
File parent = file.getParentFile();
|
MinecraftServer server = source.getServer();
|
||||||
if (parent != null) {
|
J.a(() -> {
|
||||||
parent.mkdirs();
|
File parent = file.getParentFile();
|
||||||
}
|
if (parent != null) {
|
||||||
try {
|
parent.mkdirs();
|
||||||
object.write(file);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
|
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
StringBuilder tileNote = new StringBuilder();
|
|
||||||
if (tilesSaved[0] > 0) {
|
|
||||||
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
|
|
||||||
if (tilesSkipped[0] > 0) {
|
|
||||||
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
|
|
||||||
}
|
}
|
||||||
tileNote.append(")");
|
try {
|
||||||
} else if (tilesSkipped[0] > 0) {
|
object.write(file);
|
||||||
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
|
} catch (IOException e) {
|
||||||
}
|
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, MessageArgument.untrusted("value", engine.getData().getDataFolder().getName()), MessageArgument.untrusted("name", name), MessageArgument.untrusted("w", w), MessageArgument.untrusted("h", h), MessageArgument.untrusted("d", d), MessageArgument.untrusted("value2", object.getBlocks().size()), MessageArgument.untrusted("tileNote", tileNote)));
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))));
|
||||||
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder tileNote = new StringBuilder();
|
||||||
|
if (tilesSaved[0] > 0) {
|
||||||
|
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
|
||||||
|
if (tilesSkipped[0] > 0) {
|
||||||
|
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
|
||||||
|
}
|
||||||
|
tileNote.append(")");
|
||||||
|
} else if (tilesSkipped[0] > 0) {
|
||||||
|
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
|
||||||
|
}
|
||||||
|
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, MessageArgument.untrusted("value", engine.getData().getDataFolder().getName()), MessageArgument.untrusted("name", name), MessageArgument.untrusted("w", w), MessageArgument.untrusted("h", h), MessageArgument.untrusted("d", d), MessageArgument.untrusted("value2", object.getBlocks().size()), MessageArgument.untrusted("tileNote", tileNote))));
|
||||||
|
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
|
||||||
|
});
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
@@ -88,10 +89,22 @@ public final class ModdedObjectUndo {
|
|||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Identity check, not just null: a studio closed and reopened under the same
|
||||||
|
// dimension id must never have blocks replayed into the dead ServerLevel.
|
||||||
|
MinecraftServer server = entry.level().getServer();
|
||||||
|
if (server == null || server.getLevel(entry.level().dimension()) != entry.level()) {
|
||||||
|
LOGGER.warn("Iris object undo: skipped a stale entry for removed dimension {}",
|
||||||
|
entry.level().dimension().identifier());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int writes = 0;
|
int writes = 0;
|
||||||
for (Map.Entry<BlockPos, BlockState> block : entry.blocks().entrySet()) {
|
for (Map.Entry<BlockPos, BlockState> block : entry.blocks().entrySet()) {
|
||||||
entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
|
try {
|
||||||
writes++;
|
entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
|
||||||
|
writes++;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.error("Iris object undo: failed to revert a block at {}", block.getKey(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LOGGER.info("Iris object undo: reverted {} block(s) in {}", writes, entry.level().dimension().identifier());
|
LOGGER.info("Iris object undo: reverted {} block(s) in {}", writes, entry.level().dimension().identifier());
|
||||||
reverted++;
|
reverted++;
|
||||||
@@ -99,6 +112,23 @@ public final class ModdedObjectUndo {
|
|||||||
return reverted;
|
return reverted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops every entry recorded against the given level. Called on dimension removal so a
|
||||||
|
* closed studio releases its block snapshots and the ServerLevel reference.
|
||||||
|
*/
|
||||||
|
public static void forget(ServerLevel level) {
|
||||||
|
if (level == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UNDOS.entrySet().removeIf((Map.Entry<UUID, Deque<Entry>> ownerEntry) -> {
|
||||||
|
Deque<Entry> queue = ownerEntry.getValue();
|
||||||
|
synchronized (queue) {
|
||||||
|
queue.removeIf((Entry entry) -> entry.level() == level);
|
||||||
|
return queue.isEmpty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static void clearAll() {
|
public static void clearAll() {
|
||||||
UNDOS.clear();
|
UNDOS.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
import art.arcane.iris.core.pack.PackResourceCleanup;
|
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
@@ -98,7 +99,7 @@ public final class ModdedPackCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static File packsRoot() {
|
public static File packsRoot() {
|
||||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
|
return IrisPlatforms.get().packsFolderNoCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int validate(CommandSourceStack source, String pack) {
|
private static int validate(CommandSourceStack source, String pack) {
|
||||||
|
|||||||
+47
-13
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.GuiHost;
|
|||||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||||
import art.arcane.iris.core.gui.VisionGUI;
|
import art.arcane.iris.core.gui.VisionGUI;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import art.arcane.iris.core.pack.StructurePackageClosure;
|
import art.arcane.iris.core.pack.StructurePackageClosure;
|
||||||
import art.arcane.iris.core.project.IrisProjectCopier;
|
import art.arcane.iris.core.project.IrisProjectCopier;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -31,7 +32,9 @@ import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
|||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.core.pack.PackExportClosure;
|
||||||
|
import art.arcane.iris.engine.object.IrisEntity;
|
||||||
|
import art.arcane.iris.engine.object.IrisMarker;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.IrisSpawner;
|
import art.arcane.iris.engine.object.IrisSpawner;
|
||||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
@@ -383,7 +386,7 @@ public final class ModdedStudioCommands {
|
|||||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||||
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||||
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
|
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
|
||||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
|
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
|
||||||
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
||||||
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
|
||||||
@@ -467,7 +470,9 @@ public final class ModdedStudioCommands {
|
|||||||
ServerPlayer player = source.getPlayer();
|
ServerPlayer player = source.getPlayer();
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
|
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
|
||||||
String dimensionId = STUDIOS.remove(owner);
|
// Commit the ownership drop only after removal succeeds: dropping it first orphaned a
|
||||||
|
// still-registered studio that no command could ever remove again.
|
||||||
|
String dimensionId = STUDIOS.get(owner);
|
||||||
if (dimensionId == null) {
|
if (dimensionId == null) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -479,6 +484,7 @@ public final class ModdedStudioCommands {
|
|||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
STUDIOS.remove(owner, dimensionId);
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, MessageArgument.untrusted("dimensionId", dimensionId)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, MessageArgument.untrusted("dimensionId", dimensionId)));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -588,7 +594,7 @@ public final class ModdedStudioCommands {
|
|||||||
File templateFolder = new File(packsRoot, template);
|
File templateFolder = new File(packsRoot, template);
|
||||||
if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
|
if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
|
||||||
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
|
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
|
||||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, "master", false, true,
|
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, PackDownloader.DEFAULT_BRANCH, false, true,
|
||||||
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
||||||
if (!installed || !new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
|
if (!installed || !new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
|
||||||
@@ -655,6 +661,7 @@ public final class ModdedStudioCommands {
|
|||||||
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
|
||||||
|
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
|
||||||
|
|
||||||
regionKeys.addAll(dimension.getRegions());
|
regionKeys.addAll(dimension.getRegions());
|
||||||
@@ -675,6 +682,8 @@ public final class ModdedStudioCommands {
|
|||||||
lootKeys.addAll(region.getLoot().getTables());
|
lootKeys.addAll(region.getLoot().getTables());
|
||||||
spawnerKeys.addAll(region.getEntitySpawners());
|
spawnerKeys.addAll(region.getEntitySpawners());
|
||||||
collectStructureKeys(structureKeys, region.getStructures());
|
collectStructureKeys(structureKeys, region.getStructures());
|
||||||
|
objectKeys.addAll(PackExportClosure.collectObjectKeys(region.getObjects()));
|
||||||
|
markerKeys.addAll(PackExportClosure.collectMarkerKeys(region.getObjects()));
|
||||||
}
|
}
|
||||||
for (String biomeKey : biomeKeys) {
|
for (String biomeKey : biomeKeys) {
|
||||||
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
|
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
|
||||||
@@ -685,9 +694,15 @@ public final class ModdedStudioCommands {
|
|||||||
lootKeys.addAll(biome.getLoot().getTables());
|
lootKeys.addAll(biome.getLoot().getTables());
|
||||||
spawnerKeys.addAll(biome.getEntitySpawners());
|
spawnerKeys.addAll(biome.getEntitySpawners());
|
||||||
collectStructureKeys(structureKeys, biome.getStructures());
|
collectStructureKeys(structureKeys, biome.getStructures());
|
||||||
for (IrisObjectPlacement placement : biome.getObjects()) {
|
objectKeys.addAll(PackExportClosure.collectObjectKeys(biome.getObjects()));
|
||||||
objectKeys.addAll(placement.getPlace());
|
markerKeys.addAll(PackExportClosure.collectMarkerKeys(biome.getObjects()));
|
||||||
|
}
|
||||||
|
for (String markerKey : markerKeys) {
|
||||||
|
IrisMarker marker = dm.getMarkerLoader().load(markerKey);
|
||||||
|
if (marker == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
spawnerKeys.addAll(marker.getSpawners());
|
||||||
}
|
}
|
||||||
for (String spawnerKey : spawnerKeys) {
|
for (String spawnerKey : spawnerKeys) {
|
||||||
IrisSpawner spawner = dm.getSpawnerLoader().load(spawnerKey);
|
IrisSpawner spawner = dm.getSpawnerLoader().load(spawnerKey);
|
||||||
@@ -695,6 +710,14 @@ public final class ModdedStudioCommands {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
spawner.getSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
|
spawner.getSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
|
||||||
|
spawner.getInitialSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
|
||||||
|
}
|
||||||
|
for (String entityKey : entityKeys) {
|
||||||
|
IrisEntity entity = dm.getEntityLoader().load(entityKey);
|
||||||
|
if (entity == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lootKeys.addAll(entity.getLoot().getTables());
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder hashes = new StringBuilder();
|
StringBuilder hashes = new StringBuilder();
|
||||||
@@ -732,6 +755,12 @@ public final class ModdedStudioCommands {
|
|||||||
for (String key : lootKeys) {
|
for (String key : lootKeys) {
|
||||||
hashes.append(copyJson(folder, "loot", key, dm.getLootLoader().findFile(key)));
|
hashes.append(copyJson(folder, "loot", key, dm.getLootLoader().findFile(key)));
|
||||||
}
|
}
|
||||||
|
for (String key : spawnerKeys) {
|
||||||
|
hashes.append(copyJson(folder, "spawners", key, dm.getSpawnerLoader().findFile(key)));
|
||||||
|
}
|
||||||
|
for (String key : markerKeys) {
|
||||||
|
hashes.append(copyJson(folder, "markers", key, dm.getMarkerLoader().findFile(key)));
|
||||||
|
}
|
||||||
|
|
||||||
JSONObject meta = new JSONObject();
|
JSONObject meta = new JSONObject();
|
||||||
meta.put("hash", IO.hash(hashes.toString()));
|
meta.put("hash", IO.hash(hashes.toString()));
|
||||||
@@ -794,14 +823,19 @@ public final class ModdedStudioCommands {
|
|||||||
int totalTasks = diameter * diameter;
|
int totalTasks = diameter * diameter;
|
||||||
KMap<String, AtomicInteger> counts = new KMap<>();
|
KMap<String, AtomicInteger> counts = new KMap<>();
|
||||||
engine.getDimension().getRegions().forEach((String key) -> counts.put(key, new AtomicInteger(0)));
|
engine.getDimension().getRegions().forEach((String key) -> counts.put(key, new AtomicInteger(0)));
|
||||||
|
// finally-scoped: a throw mid-scan previously leaked the sampler's whole
|
||||||
|
// ForkJoinPool (close() is the only thing that shuts it down).
|
||||||
MultiBurst burst = new MultiBurst("Region Sampler");
|
MultiBurst burst = new MultiBurst("Region Sampler");
|
||||||
BurstExecutor executor = burst.burst(totalTasks);
|
try {
|
||||||
new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> {
|
BurstExecutor executor = burst.burst(totalTasks);
|
||||||
IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> {
|
||||||
counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet();
|
IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
||||||
})).setOffset(blockX >> 4, blockZ >> 4).drain();
|
counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet();
|
||||||
executor.complete();
|
})).setOffset(blockX >> 4, blockZ >> 4).drain();
|
||||||
burst.close();
|
executor.complete();
|
||||||
|
} finally {
|
||||||
|
burst.close();
|
||||||
|
}
|
||||||
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
|
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
|
||||||
IrisRegion region = engine.getData().getRegionLoader().load(key);
|
IrisRegion region = engine.getData().getRegionLoader().load(key);
|
||||||
String rarity = region == null ? "?" : String.valueOf(region.getRarity());
|
String rarity = region == null ? "?" : String.valueOf(region.getRarity());
|
||||||
|
|||||||
+3
@@ -451,6 +451,9 @@ public final class ModdedWhatCommands {
|
|||||||
ModdedScheduler scheduler, MarkerRun run,
|
ModdedScheduler scheduler, MarkerRun run,
|
||||||
List<BlockPos> hits, int from) {
|
List<BlockPos> hits, int from) {
|
||||||
if (!active(run)) {
|
if (!active(run)) {
|
||||||
|
// Drop the registry entry on abort too, or the run record pins the player, level
|
||||||
|
// and engine until server stop. No-op if a newer run already replaced it.
|
||||||
|
ACTIVE_MARKER_RUNS.remove(run.playerId(), run);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int to = Math.min(hits.size(), from + MARKER_BATCH_SIZE);
|
int to = Math.min(hits.size(), from + MARKER_BATCH_SIZE);
|
||||||
|
|||||||
+3
-2
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
|
|||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.BrokenPackException;
|
import art.arcane.iris.core.pack.BrokenPackException;
|
||||||
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
@@ -177,7 +178,7 @@ public final class ModdedWorldCommands {
|
|||||||
}
|
}
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
|
||||||
Thread thread = new Thread(() -> {
|
Thread thread = new Thread(() -> {
|
||||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
|
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
|
||||||
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
||||||
server.execute(() -> {
|
server.execute(() -> {
|
||||||
if (!installed || !packFolder.isDirectory()) {
|
if (!installed || !packFolder.isDirectory()) {
|
||||||
@@ -280,7 +281,7 @@ public final class ModdedWorldCommands {
|
|||||||
}
|
}
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
|
||||||
Thread thread = new Thread(() -> {
|
Thread thread = new Thread(() -> {
|
||||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
|
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
|
||||||
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
||||||
server.execute(() -> {
|
server.execute(() -> {
|
||||||
if (!installed || !packFolder.isDirectory()) {
|
if (!installed || !packFolder.isDirectory()) {
|
||||||
|
|||||||
+12
-1
@@ -55,6 +55,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class ModdedChunkUpdateService implements ModdedTickableService {
|
public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||||
private static final long PASS_PERIOD_MILLIS = 3_000L;
|
private static final long PASS_PERIOD_MILLIS = 3_000L;
|
||||||
@@ -84,7 +85,17 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
ExecutorService active = warmupExecutor;
|
ExecutorService active = warmupExecutor;
|
||||||
warmupExecutor = null;
|
warmupExecutor = null;
|
||||||
if (active != null) {
|
if (active != null) {
|
||||||
active.shutdown();
|
// Drop queued warm-ups (pure prefetch) and AWAIT: the very next shutdown stage
|
||||||
|
// closes every Mantle, and an in-flight mantle.getChunk would fault a plate back
|
||||||
|
// in after the close-time flush.
|
||||||
|
active.shutdownNow();
|
||||||
|
try {
|
||||||
|
if (!active.awaitTermination(5L, TimeUnit.SECONDS)) {
|
||||||
|
IrisLogging.warn("Iris mantle warm-up did not stop before engine close");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
warmupQueue.clear();
|
warmupQueue.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedDownloadBranchParityTest {
|
||||||
|
private static final List<String> DOWNLOAD_SOURCES = List.of(
|
||||||
|
"art/arcane/iris/modded/command/ModdedWorldCommands.java",
|
||||||
|
"art/arcane/iris/modded/command/ModdedStudioCommands.java",
|
||||||
|
"art/arcane/iris/modded/command/ModdedCommandTree.java");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void implicitAndExplicitDownloadsShareTheCoreDefaultBranch() throws Exception {
|
||||||
|
for (String source : DOWNLOAD_SOURCES) {
|
||||||
|
Path path = Path.of(System.getProperty("iris.moddedCommonSources"), source);
|
||||||
|
String text = Files.readString(path);
|
||||||
|
assertFalse(source + " must not hardcode a \"master\" download branch",
|
||||||
|
text.contains("\"master\""));
|
||||||
|
assertFalse(source + " must not hardcode a \"stable\" download branch",
|
||||||
|
text.contains("\"stable\""));
|
||||||
|
assertTrue(source + " must use PackDownloader.DEFAULT_BRANCH",
|
||||||
|
text.contains("PackDownloader.DEFAULT_BRANCH"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -36,12 +36,12 @@ public class ModdedLootApplierTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void clearRemovesNativeAndIrisSourcesBeforeAdding() {
|
public void clearRemovesEverythingAndContributesNothing() {
|
||||||
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
|
||||||
|
|
||||||
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
|
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
|
||||||
|
|
||||||
assertEquals(List.of("biome-iris"), sources);
|
assertEquals(List.of(), sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class ModdedPlatformPathsTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private ModdedPlatform platform() {
|
||||||
|
Path configDir = temporaryFolder.getRoot().toPath();
|
||||||
|
return new ModdedPlatform(new ModdedLoader() {
|
||||||
|
@Override
|
||||||
|
public String platformName() {
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String minecraftVersion() {
|
||||||
|
return "26.2";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String modVersion() {
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MinecraftServer currentServer() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateLevelCache(MinecraftServer server) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean clientEnvironment() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path configDir() {
|
||||||
|
return configDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File modJar() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTreeFellerPermission(ServerPlayer player) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void packsResolveUnderIrisworldgen() {
|
||||||
|
ModdedPlatform platform = platform();
|
||||||
|
File root = temporaryFolder.getRoot();
|
||||||
|
File packsRoot = new File(new File(root, "irisworldgen"), "packs");
|
||||||
|
|
||||||
|
assertEquals(packsRoot, platform.packsFolder());
|
||||||
|
assertEquals(packsRoot, platform.dataFolder("packs"));
|
||||||
|
assertEquals(new File(packsRoot, "overworld"), platform.dataFolderNoCreate("packs", "overworld"));
|
||||||
|
assertEquals(new File(packsRoot, "overworld" + File.separator + "dimensions" + File.separator + "overworld.json"),
|
||||||
|
platform.dataFile("packs", "overworld", "dimensions", "overworld.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everythingElseStaysUnderIris() {
|
||||||
|
ModdedPlatform platform = platform();
|
||||||
|
File iris = new File(temporaryFolder.getRoot(), "iris");
|
||||||
|
|
||||||
|
assertEquals(iris, platform.dataFolder());
|
||||||
|
assertEquals(new File(iris, "settings.json"), platform.dataFile("settings.json"));
|
||||||
|
assertEquals(new File(iris, "parity"), platform.dataFolder("parity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void packsMatchIsExactSegmentOnly() {
|
||||||
|
ModdedPlatform platform = platform();
|
||||||
|
File iris = new File(temporaryFolder.getRoot(), "iris");
|
||||||
|
|
||||||
|
assertEquals(new File(iris, "packbenchmarks"), platform.dataFolder("packbenchmarks"));
|
||||||
|
assertEquals(new File(iris, "packsx"), platform.dataFolderNoCreate("packsx"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IRare;
|
||||||
|
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedSpawnerRarityParityTest {
|
||||||
|
private static IrisEntitySpawn spawn(int rarity) {
|
||||||
|
IrisEntitySpawn spawn = new IrisEntitySpawn();
|
||||||
|
spawn.setRarity(rarity);
|
||||||
|
return spawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rarityIsAppliedOnceAsPoolWeightOnly() {
|
||||||
|
IrisEntitySpawn common = spawn(1);
|
||||||
|
IrisEntitySpawn rare = spawn(4);
|
||||||
|
|
||||||
|
KList<IrisEntitySpawn> expanded = IRare.expandWeighted(List.of(common, rare));
|
||||||
|
|
||||||
|
// totalRarity 5 -> common appears 5/1 = 5 times, rare 5/4 = 1 time.
|
||||||
|
assertEquals(6, expanded.size());
|
||||||
|
assertEquals(5, expanded.stream().filter(entry -> entry == common).count());
|
||||||
|
assertEquals(1, expanded.stream().filter(entry -> entry == rare).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rarityZeroAndNegativeAreClampedToOne() {
|
||||||
|
assertEquals(1, IRare.get(spawn(0)));
|
||||||
|
assertEquals(1, IRare.get(spawn(-5)));
|
||||||
|
assertEquals(3, IRare.get(spawn(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void perPositionSpawnLoopsDoNotRerollEntryRarity() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.moddedCommonSources"),
|
||||||
|
"art/arcane/iris/modded/ModdedWorldManager.java"));
|
||||||
|
|
||||||
|
for (String method : List.of("private int spawnEntry(", "private int spawnEntryAt(")) {
|
||||||
|
String body = methodBody(source, method);
|
||||||
|
assertFalse(method + " must not re-roll rarity per position; rarityPick already weighted the pool",
|
||||||
|
body.contains("getRarity()"));
|
||||||
|
assertTrue(method + " must keep the min/max spawn-count roll",
|
||||||
|
body.contains("LootResolver.inclusive("));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String methodBody(String source, String declaration) {
|
||||||
|
int start = source.indexOf(declaration);
|
||||||
|
assertTrue("ModdedWorldManager must declare " + declaration, start >= 0);
|
||||||
|
int open = source.indexOf('{', start);
|
||||||
|
int depth = 0;
|
||||||
|
for (int index = open; index < source.length(); index++) {
|
||||||
|
char character = source.charAt(index);
|
||||||
|
if (character == '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (character == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
return source.substring(open + 1, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError(declaration + " is not brace balanced");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ public class IrisSettings {
|
|||||||
private IrisSettingsGUI gui = new IrisSettingsGUI();
|
private IrisSettingsGUI gui = new IrisSettingsGUI();
|
||||||
private IrisSettingsAutoconfiguration autoConfiguration = new IrisSettingsAutoconfiguration();
|
private IrisSettingsAutoconfiguration autoConfiguration = new IrisSettingsAutoconfiguration();
|
||||||
private IrisSettingsGenerator generator = new IrisSettingsGenerator();
|
private IrisSettingsGenerator generator = new IrisSettingsGenerator();
|
||||||
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
private transient IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
||||||
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
||||||
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
||||||
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ public class IrisWorlds {
|
|||||||
dimension = IrisData.loadAnyDimension(id, null);
|
dimension = IrisData.loadAnyDimension(id, null);
|
||||||
}
|
}
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME);
|
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
||||||
if (PackDownloader.isPackPresent(packsRoot, id)) {
|
if (PackDownloader.isPackPresent(packsRoot, id)) {
|
||||||
IrisLogging.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
|
IrisLogging.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
|
||||||
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
|
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
|
||||||
|
|||||||
@@ -892,7 +892,7 @@ public class ServerConfigurator {
|
|||||||
|
|
||||||
public static Stream<IrisData> allPacks() {
|
public static Stream<IrisData> allPacks() {
|
||||||
Stream<File> locals = PackDirectoryResolver.listVisiblePackDirectories(
|
Stream<File> locals = PackDirectoryResolver.listVisiblePackDirectories(
|
||||||
IrisPlatforms.get().dataFolder("packs")
|
IrisPlatforms.get().packsFolder()
|
||||||
).stream();
|
).stream();
|
||||||
return Stream.concat(locals
|
return Stream.concat(locals
|
||||||
.filter(base -> {
|
.filter(base -> {
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ public final class DatapackIngestService {
|
|||||||
if (autoIngest && !configured.isEmpty()) {
|
if (autoIngest && !configured.isEmpty()) {
|
||||||
IrisLogging.info("Validating " + configured.size()
|
IrisLogging.info("Validating " + configured.size()
|
||||||
+ " configured external datapack import(s) before player admission...");
|
+ " configured external datapack import(s) before player admission...");
|
||||||
Report report = ingest(null, configured, true);
|
Report report = ingest(null, configured, true, true);
|
||||||
if (!report.getFailed().isEmpty()) {
|
if (!report.getFailed().isEmpty()) {
|
||||||
String failure = report.getFailed().getFirst();
|
String failure = report.getFailed().getFirst();
|
||||||
IrisStartupValidation.markDatapacksInvalid(failure);
|
IrisStartupValidation.markDatapacksInvalid(failure);
|
||||||
@@ -513,15 +513,35 @@ public final class DatapackIngestService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Report ingest(VolmitSender sender, KList<String> urls, boolean restart) {
|
public static Report ingest(VolmitSender sender, KList<String> urls, boolean restart) {
|
||||||
IrisStartupValidation.beginDatapackValidation();
|
return ingest(sender, urls, restart, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only startup validation owns the global player-admission gate (gateAdmission=true). The
|
||||||
|
* runtime admin command must never flip it: a transient download failure there would lock
|
||||||
|
* every login until restart, and the PENDING window would close logins for the whole
|
||||||
|
* (potentially very slow) download. World-creation safety on the ungated path is carried
|
||||||
|
* by the loaded-runtime invalidation plus requireDatapackRestart when files change.
|
||||||
|
*/
|
||||||
|
private static Report ingest(VolmitSender sender, KList<String> urls, boolean restart, boolean gateAdmission) {
|
||||||
|
if (gateAdmission) {
|
||||||
|
IrisStartupValidation.beginDatapackValidation();
|
||||||
|
}
|
||||||
ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation =
|
ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation =
|
||||||
ServerConfigurator.invalidateLoadedDatapackRuntime();
|
ServerConfigurator.invalidateLoadedDatapackRuntime();
|
||||||
Report report;
|
Report report;
|
||||||
|
boolean settled = false;
|
||||||
TRANSACTION_LOCK.lock();
|
TRANSACTION_LOCK.lock();
|
||||||
try {
|
try {
|
||||||
report = ingestLocked(sender, urls, restart);
|
report = ingestLocked(sender, urls, restart);
|
||||||
|
settled = true;
|
||||||
} finally {
|
} finally {
|
||||||
TRANSACTION_LOCK.unlock();
|
TRANSACTION_LOCK.unlock();
|
||||||
|
if (!settled && gateAdmission) {
|
||||||
|
// Never strand the admission gate at PENDING on an unexpected throw.
|
||||||
|
IrisStartupValidation.markDatapacksInvalid(
|
||||||
|
"External datapack ingest failed unexpectedly; check the log above.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!report.changed() && report.getFailed().isEmpty()) {
|
if (!report.changed() && report.getFailed().isEmpty()) {
|
||||||
ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation);
|
ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation);
|
||||||
@@ -532,10 +552,12 @@ public final class DatapackIngestService {
|
|||||||
ServerConfigurator.requireDatapackRestart();
|
ServerConfigurator.requireDatapackRestart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!report.getFailed().isEmpty()) {
|
if (gateAdmission) {
|
||||||
IrisStartupValidation.markDatapacksInvalid(report.getFailed().getFirst());
|
if (!report.getFailed().isEmpty()) {
|
||||||
} else if (!report.changed()) {
|
IrisStartupValidation.markDatapacksInvalid(report.getFailed().getFirst());
|
||||||
IrisStartupValidation.markDatapacksReady();
|
} else if (!report.changed()) {
|
||||||
|
IrisStartupValidation.markDatapacksReady();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
@@ -944,45 +966,61 @@ public final class DatapackIngestService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RemoveOutcome {
|
||||||
|
REMOVED,
|
||||||
|
REJECTED,
|
||||||
|
FAILED_AFTER_MUTATION
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean remove(VolmitSender sender, String id) {
|
public static boolean remove(VolmitSender sender, String id) {
|
||||||
ServerConfigurator.invalidateLoadedDatapackRuntime();
|
ServerConfigurator.LoadedDatapackRuntimeInvalidation invalidation =
|
||||||
boolean removed;
|
ServerConfigurator.invalidateLoadedDatapackRuntime();
|
||||||
|
RemoveOutcome outcome;
|
||||||
TRANSACTION_LOCK.lock();
|
TRANSACTION_LOCK.lock();
|
||||||
try {
|
try {
|
||||||
removed = removeLocked(sender, id);
|
outcome = removeOutcomeLocked(sender, id);
|
||||||
} finally {
|
} finally {
|
||||||
TRANSACTION_LOCK.unlock();
|
TRANSACTION_LOCK.unlock();
|
||||||
}
|
}
|
||||||
if (removed) {
|
if (outcome == RemoveOutcome.REMOVED) {
|
||||||
ServerConfigurator.requireDatapackRestart();
|
ServerConfigurator.requireDatapackRestart();
|
||||||
|
} else if (outcome == RemoveOutcome.REJECTED) {
|
||||||
|
// Rejected before any mutation: nothing on disk changed, so the loaded runtime is
|
||||||
|
// still valid. Leaving it invalidated forced a full datapack reinstall on every
|
||||||
|
// later world creation for the rest of the session.
|
||||||
|
ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation);
|
||||||
}
|
}
|
||||||
return removed;
|
return outcome == RemoveOutcome.REMOVED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean removeLocked(VolmitSender sender, String id) {
|
private static RemoveOutcome removeOutcomeLocked(VolmitSender sender, String id) {
|
||||||
File root = IrisPlatforms.get().dataFolder("datapacks");
|
File root = IrisPlatforms.get().dataFolder("datapacks");
|
||||||
return removeLocked(sender, id, root, ServerConfigurator.getDatapacksFolder());
|
return removeOutcomeLocked(sender, id, root, ServerConfigurator.getDatapacksFolder());
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean removeLocked(VolmitSender sender, String id, File root, List<File> worldFolders) {
|
static boolean removeLocked(VolmitSender sender, String id, File root, List<File> worldFolders) {
|
||||||
|
return removeOutcomeLocked(sender, id, root, worldFolders) == RemoveOutcome.REMOVED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RemoveOutcome removeOutcomeLocked(VolmitSender sender, String id, File root, List<File> worldFolders) {
|
||||||
String requested = id == null ? "" : id.trim().toLowerCase(Locale.ROOT);
|
String requested = id == null ? "" : id.trim().toLowerCase(Locale.ROOT);
|
||||||
String cleaned = sanitizeId(id);
|
String cleaned = sanitizeId(id);
|
||||||
if (requested.isBlank() || !requested.equals(cleaned) || RESERVED_IDS.contains(cleaned)) {
|
if (requested.isBlank() || !requested.equals(cleaned) || RESERVED_IDS.contains(cleaned)) {
|
||||||
message(sender, C.RED + "Invalid Iris-managed datapack id '" + requested + "'. Run /iris datapack list and use the exact listed id.");
|
message(sender, C.RED + "Invalid Iris-managed datapack id '" + requested + "'. Run /iris datapack list and use the exact listed id.");
|
||||||
return false;
|
return RemoveOutcome.REJECTED;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
recoverTransactions(root, worldFolders);
|
recoverTransactions(root, worldFolders);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
message(sender, C.RED + "Datapack removal blocked by incomplete transaction recovery: " + e.getMessage());
|
message(sender, C.RED + "Datapack removal blocked by incomplete transaction recovery: " + e.getMessage());
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
return false;
|
return RemoveOutcome.REJECTED;
|
||||||
}
|
}
|
||||||
Manifest manifest = readManifest(root);
|
Manifest manifest = readManifest(root);
|
||||||
Entry ownedEntry = manifest.findById(cleaned);
|
Entry ownedEntry = manifest.findById(cleaned);
|
||||||
if (ownedEntry == null) {
|
if (ownedEntry == null) {
|
||||||
message(sender, C.YELLOW + "No Iris-managed datapack named '" + cleaned + "'. Unmanaged world datapacks are never removed by Iris.");
|
message(sender, C.YELLOW + "No Iris-managed datapack named '" + cleaned + "'. Unmanaged world datapacks are never removed by Iris.");
|
||||||
return false;
|
return RemoveOutcome.REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<File> targets;
|
List<File> targets;
|
||||||
@@ -991,7 +1029,7 @@ public final class DatapackIngestService {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
message(sender, C.RED + "Refused to remove datapack '" + cleaned + "': " + e.getMessage());
|
message(sender, C.RED + "Refused to remove datapack '" + cleaned + "': " + e.getMessage());
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
return false;
|
return RemoveOutcome.REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
EditableImportRemoval editableRemoval = null;
|
EditableImportRemoval editableRemoval = null;
|
||||||
@@ -1017,7 +1055,7 @@ public final class DatapackIngestService {
|
|||||||
removalFailure);
|
removalFailure);
|
||||||
message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN
|
message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN
|
||||||
+ "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone.");
|
+ "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone.");
|
||||||
return true;
|
return RemoveOutcome.REMOVED;
|
||||||
}
|
}
|
||||||
boolean restored = rollbackRemoval(manifestWrite, directoryRemoval, editableRemoval, removalFailure);
|
boolean restored = rollbackRemoval(manifestWrite, directoryRemoval, editableRemoval, removalFailure);
|
||||||
if (restored && coordinator != null) {
|
if (restored && coordinator != null) {
|
||||||
@@ -1030,12 +1068,14 @@ public final class DatapackIngestService {
|
|||||||
message(sender, C.RED + "Failed to remove datapack '" + cleaned
|
message(sender, C.RED + "Failed to remove datapack '" + cleaned
|
||||||
+ "'; Iris attempted to restore every prior location: " + removalFailure.getMessage());
|
+ "'; Iris attempted to restore every prior location: " + removalFailure.getMessage());
|
||||||
IrisLogging.reportError(removalFailure);
|
IrisLogging.reportError(removalFailure);
|
||||||
return false;
|
// A mutation was attempted; even a successful rollback is not provably identical
|
||||||
|
// (the fingerprint does not cover datapacks/), so stay conservatively invalidated.
|
||||||
|
return RemoveOutcome.FAILED_AFTER_MUTATION;
|
||||||
}
|
}
|
||||||
finishCommittedRemoval(cleaned, manifestWrite, directoryRemoval, editableRemoval, coordinator, null);
|
finishCommittedRemoval(cleaned, manifestWrite, directoryRemoval, editableRemoval, coordinator, null);
|
||||||
message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN
|
message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN
|
||||||
+ "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone.");
|
+ "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone.");
|
||||||
return true;
|
return RemoveOutcome.REMOVED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void finishCommittedRemoval(
|
private static void finishCommittedRemoval(
|
||||||
|
|||||||
@@ -358,15 +358,18 @@ public final class ModrinthResolver {
|
|||||||
connection.setReadTimeout(20000);
|
connection.setReadTimeout(20000);
|
||||||
connection.setInstanceFollowRedirects(true);
|
connection.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
int code = connection.getResponseCode();
|
// Everything past openConnection under one finally: a timeout/DNS/TLS throw inside
|
||||||
if (code != 200) {
|
// getResponseCode abandoned the connection undisconnected.
|
||||||
connection.disconnect();
|
try {
|
||||||
throw new IOException("HTTP " + code + " from " + url);
|
int code = connection.getResponseCode();
|
||||||
}
|
if (code != 200) {
|
||||||
|
throw new IOException("HTTP " + code + " from " + url);
|
||||||
|
}
|
||||||
|
|
||||||
try (InputStream input = connection.getInputStream();
|
try (InputStream input = connection.getInputStream();
|
||||||
InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
|
InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
|
||||||
return readBoundedApiResponse(reader, url, MAX_API_RESPONSE_CHARS);
|
return readBoundedApiResponse(reader, url, MAX_API_RESPONSE_CHARS);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,17 @@ public class BlockSignal {
|
|||||||
public static final AtomicInteger active = new AtomicInteger(0);
|
public static final AtomicInteger active = new AtomicInteger(0);
|
||||||
|
|
||||||
public BlockSignal(Block block, int ticks) {
|
public BlockSignal(Block block, int ticks) {
|
||||||
active.incrementAndGet();
|
// Count only after the entity actually exists, and release unconditionally: a refused
|
||||||
|
// Folia schedule or a spawn failure must not inflate the throttle counter forever.
|
||||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5);
|
Location tg = block.getLocation().clone().add(0.5, 0, 0.5);
|
||||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg, block.getBlockData());
|
FallingBlock e;
|
||||||
|
try {
|
||||||
|
e = block.getWorld().spawnFallingBlock(tg, block.getBlockData());
|
||||||
|
} catch (Throwable spawnFailure) {
|
||||||
|
sendBlockRefresh(block);
|
||||||
|
throw spawnFailure;
|
||||||
|
}
|
||||||
|
active.incrementAndGet();
|
||||||
e.setGravity(false);
|
e.setGravity(false);
|
||||||
e.setInvulnerable(true);
|
e.setInvulnerable(true);
|
||||||
e.setGlowing(true);
|
e.setGlowing(true);
|
||||||
@@ -55,10 +63,11 @@ public class BlockSignal {
|
|||||||
active.decrementAndGet();
|
active.decrementAndGet();
|
||||||
sendBlockRefresh(block);
|
sendBlockRefresh(block);
|
||||||
};
|
};
|
||||||
if (!J.runAt(blockLocation, removeTask, ticks)) {
|
boolean scheduled = J.runAt(blockLocation, removeTask, ticks);
|
||||||
if (!J.isFolia()) {
|
if (!scheduled && !J.isFolia()) {
|
||||||
J.s(removeTask, ticks);
|
J.s(removeTask, ticks);
|
||||||
}
|
} else if (!scheduled) {
|
||||||
|
removeTask.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import art.arcane.volmlib.util.math.RNG;
|
|||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import lombok.Data;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
@@ -47,74 +46,83 @@ import net.kyori.adventure.text.event.ClickEvent;
|
|||||||
import net.kyori.adventure.text.event.HoverEvent;
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
|
||||||
@Data
|
|
||||||
public class DustRevealer {
|
public class DustRevealer {
|
||||||
private final Engine engine;
|
// Matches the modded twin (ModdedDustRevealer) so both platforms truncate identically.
|
||||||
private final World world;
|
private static final int MAX_HITS = 2_048;
|
||||||
private final BlockPosition block;
|
private static final int PARTICLE_BATCH_SIZE = 64;
|
||||||
private final String key;
|
|
||||||
private final KList<BlockPosition> hits;
|
|
||||||
|
|
||||||
public DustRevealer(Engine engine, World world, BlockPosition block, String key, KList<BlockPosition> hits) {
|
/**
|
||||||
this.engine = engine;
|
* Bounded, single-threaded flood fill over the object's placement key, followed by batched
|
||||||
this.world = world;
|
* particle playback on the owning threads. The previous shape recursively spawned one
|
||||||
this.block = block;
|
* scheduler task + one burst task per matched block, all mutating one unsynchronized
|
||||||
this.key = key;
|
* ArrayList and sleep-spinning MultiBurst workers — unbounded fan-out on large objects.
|
||||||
this.hits = hits;
|
*/
|
||||||
|
private static void reveal(Engine engine, World world, BlockPosition origin, String key) {
|
||||||
|
KList<BlockPosition> hits = new KList<>();
|
||||||
|
Set<BlockPosition> visited = new HashSet<>();
|
||||||
|
ArrayDeque<BlockPosition> frontier = new ArrayDeque<>();
|
||||||
|
visited.add(origin);
|
||||||
|
frontier.add(origin);
|
||||||
|
hits.add(origin);
|
||||||
|
int minY = world.getMinHeight();
|
||||||
|
int maxY = world.getMaxHeight();
|
||||||
|
|
||||||
Location blockLocation = block.toBlock(world).getLocation();
|
try {
|
||||||
Runnable revealTask = () -> {
|
search:
|
||||||
BlockSignal.of(world, block.getX(), block.getY(), block.getZ(), 10);
|
while (!frontier.isEmpty()) {
|
||||||
|
BlockPosition at = frontier.poll();
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
if (dx == 0 && dy == 0 && dz == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockPosition next = new BlockPosition(at.getX() + dx, at.getY() + dy, at.getZ() + dz);
|
||||||
|
if (next.getY() < minY || next.getY() >= maxY || !visited.add(next)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key.equals(engine.getObjectPlacementKey(next.getX(), next.getY() - minY, next.getZ()))) {
|
||||||
|
hits.add(next);
|
||||||
|
frontier.add(next);
|
||||||
|
if (hits.size() >= MAX_HITS) {
|
||||||
|
break search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
revealBatch(world, hits, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void revealBatch(World world, KList<BlockPosition> hits, int from) {
|
||||||
|
if (from >= hits.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int to = Math.min(from + PARTICLE_BATCH_SIZE, hits.size());
|
||||||
|
Runnable batch = () -> {
|
||||||
|
for (int i = from; i < to; i++) {
|
||||||
|
BlockPosition p = hits.get(i);
|
||||||
|
BlockSignal.of(world, p.getX(), p.getY(), p.getZ(), 10);
|
||||||
|
}
|
||||||
|
BlockPosition first = hits.get(from);
|
||||||
if (M.r(0.25)) {
|
if (M.r(0.25)) {
|
||||||
world.playSound(block.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f));
|
world.playSound(first.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f));
|
||||||
}
|
}
|
||||||
J.a(() -> {
|
revealBatch(world, hits, to);
|
||||||
while (BlockSignal.active.get() > 128) {
|
|
||||||
J.sleep(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY(), block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY(), block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ()));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() + 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() - 1));
|
|
||||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() + 1));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
int delay = RNG.r.i(2, 8);
|
Location at = hits.get(from).toBlock(world).getLocation();
|
||||||
if (!J.runAt(blockLocation, revealTask, delay)) {
|
if (!J.runAt(at, batch, 1) && !J.isFolia()) {
|
||||||
if (!J.isFolia()) {
|
J.s(batch, 1);
|
||||||
J.s(revealTask, delay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +145,7 @@ public class DustRevealer {
|
|||||||
RuntimeUiMessages.DUST_FOUND_OBJECT,
|
RuntimeUiMessages.DUST_FOUND_OBJECT,
|
||||||
MessageArgument.untrusted("object", a)
|
MessageArgument.untrusted("object", a)
|
||||||
));
|
));
|
||||||
J.a(() -> {
|
J.a(() -> reveal(access, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a));
|
||||||
new DustRevealer(access, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a, new KList<>());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,24 +360,6 @@ public class DustRevealer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean is(BlockPosition a) {
|
|
||||||
if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int betterY = a.getY() - world.getMinHeight();
|
|
||||||
if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) {
|
|
||||||
hits.add(a);
|
|
||||||
new DustRevealer(engine, world, a, key, hits);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidTry(BlockPosition b) {
|
|
||||||
return !hits.contains(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record DustLine(String text, boolean emphasis) {
|
private record DustLine(String text, boolean emphasis) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
public final class PregenRenderer extends JPanel implements KeyListener {
|
public final class PregenRenderer extends JPanel implements KeyListener {
|
||||||
private static final long serialVersionUID = 2094606939770332040L;
|
private static final long serialVersionUID = 2094606939770332040L;
|
||||||
|
|
||||||
private final KList<Runnable> order = new KList<>();
|
// Backstop: if paint() ever stalls (iconified frame, EDT hiccup), the producer must not
|
||||||
|
// grow this without bound — one entry per drawn chunk adds up fast on a big pregen.
|
||||||
|
private static final int MAX_QUEUED_DRAWS = 250_000;
|
||||||
|
private KList<Runnable> order = new KList<>();
|
||||||
private final ReentrantLock lock = new ReentrantLock();
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
private final int res = 512;
|
private final int res = 512;
|
||||||
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
|
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
|
||||||
@@ -71,7 +74,9 @@ public final class PregenRenderer extends JPanel implements KeyListener {
|
|||||||
return (Position2 c, Color color) -> {
|
return (Position2 c, Color color) -> {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
order.add(() -> draw(c, color, bg));
|
if (order.size() < MAX_QUEUED_DRAWS) {
|
||||||
|
order.add(() -> draw(c, color, bg));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@@ -83,7 +88,11 @@ public final class PregenRenderer extends JPanel implements KeyListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVisibleFrame() {
|
public boolean isVisibleFrame() {
|
||||||
return frame != null && frame.isVisible();
|
// An iconified frame still reports isVisible() but AWT stops repainting it, which
|
||||||
|
// would stop the only queue drain while the producer keeps appending.
|
||||||
|
JFrame activeFrame = frame;
|
||||||
|
return activeFrame != null && activeFrame.isVisible()
|
||||||
|
&& (activeFrame.getExtendedState() & java.awt.Frame.ICONIFIED) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
@@ -99,18 +108,23 @@ public final class PregenRenderer extends JPanel implements KeyListener {
|
|||||||
public void paint(Graphics gx) {
|
public void paint(Graphics gx) {
|
||||||
Graphics2D g = (Graphics2D) gx;
|
Graphics2D g = (Graphics2D) gx;
|
||||||
bg = (Graphics2D) image.getGraphics();
|
bg = (Graphics2D) image.getGraphics();
|
||||||
|
// Swap the queue under the lock and drain outside it: generation threads must not
|
||||||
|
// block on the lock while the EDT walks a large batch, and pop()-per-entry was O(N^2).
|
||||||
|
KList<Runnable> batch;
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
while (order.isNotEmpty()) {
|
batch = order;
|
||||||
try {
|
order = new KList<>();
|
||||||
order.pop().run();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
for (Runnable r : batch) {
|
||||||
|
try {
|
||||||
|
r.run();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
|
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
|
||||||
g.setColor(Color.WHITE);
|
g.setColor(Color.WHITE);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import art.arcane.volmlib.util.function.Consumer2;
|
|||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import art.arcane.volmlib.util.math.Position2;
|
import art.arcane.volmlib.util.math.Position2;
|
||||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
@@ -46,12 +45,14 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||||
private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 15_000L;
|
// Must exceed the worker's own worst-case teardown budget (AsyncPregenMethod close:
|
||||||
|
// 60s permit drain + 120s flush + plate reclaim), or a routine drain trips the deadline
|
||||||
|
// and aborts the engine shutdown sequence mid-teardown.
|
||||||
|
private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 200_000L;
|
||||||
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
|
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
|
||||||
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
|
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
|
||||||
private static final Color COLOR_MANTLE = parseColor("#3c2773");
|
private static final Color COLOR_MANTLE = parseColor("#3c2773");
|
||||||
@@ -69,14 +70,12 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
private final IrisPregenerator pregenerator;
|
private final IrisPregenerator pregenerator;
|
||||||
private final Position2 min;
|
private final Position2 min;
|
||||||
private final Position2 max;
|
private final Position2 max;
|
||||||
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final ExecutorService service;
|
private final ExecutorService service;
|
||||||
private final Thread worker;
|
private final Thread worker;
|
||||||
private final PregenPhaseTracker apiPhases = new PregenPhaseTracker();
|
private final PregenPhaseTracker apiPhases = new PregenPhaseTracker();
|
||||||
private PregenRenderer renderer;
|
private PregenRenderer renderer;
|
||||||
private Consumer2<Position2, Color> drawFunction;
|
private Consumer2<Position2, Color> drawFunction;
|
||||||
private int rgc = 0;
|
|
||||||
private String[] info;
|
private String[] info;
|
||||||
private volatile double lastChunksPerSecond = 0D;
|
private volatile double lastChunksPerSecond = 0D;
|
||||||
private volatile long lastChunksRemaining = 0L;
|
private volatile long lastChunksRemaining = 0L;
|
||||||
@@ -87,14 +86,6 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
private volatile String lastMethod = IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD_PENDING);
|
private volatile String lastMethod = IrisLanguage.plain(DesktopUiMessages.PREGEN_METHOD_PENDING);
|
||||||
|
|
||||||
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
|
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
|
||||||
instance.updateAndGet(old -> {
|
|
||||||
if (old != null) {
|
|
||||||
old.pregenerator.close();
|
|
||||||
old.worker.interrupt();
|
|
||||||
old.close();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
});
|
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
monitor = new MemoryMonitor(50);
|
monitor = new MemoryMonitor(50);
|
||||||
saving = false;
|
saving = false;
|
||||||
@@ -120,6 +111,19 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
worker.setPriority(Thread.MIN_PRIORITY);
|
worker.setPriority(Thread.MIN_PRIORITY);
|
||||||
worker.setDaemon(true);
|
worker.setDaemon(true);
|
||||||
worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex));
|
worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex));
|
||||||
|
|
||||||
|
// Publish into the static only after every field is assigned (the volatile swap is
|
||||||
|
// what makes them visible to metrics/shutdown readers), and start the worker after
|
||||||
|
// publication so it also sees a complete object.
|
||||||
|
// CAS-or-throw: updateAndGet must be side-effect free (it can re-apply on
|
||||||
|
// contention), and silently killing the previous job overlapped its 60s+120s
|
||||||
|
// teardown with the new job's generation. Replacement goes through
|
||||||
|
// shutdownAndWait first, matching the modded adapter's rejection contract.
|
||||||
|
if (!instance.compareAndSet(null, this)) {
|
||||||
|
monitor.close();
|
||||||
|
service.shutdown();
|
||||||
|
throw new IllegalStateException("An Iris pregeneration job is already running; stop it first.");
|
||||||
|
}
|
||||||
worker.start();
|
worker.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +142,14 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!inst.worker.isAlive() && inst.worker.getState() != Thread.State.NEW) {
|
||||||
|
// The worker died without running onClose (early abort); clear the phantom job so
|
||||||
|
// it stops suppressing entity spawns and blocking future pregens. A NEW worker is
|
||||||
|
// a job mid-construction, not a dead one.
|
||||||
|
instance.compareAndSet(inst, null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
inst.pregenerator.close();
|
inst.pregenerator.close();
|
||||||
inst.worker.interrupt();
|
inst.worker.interrupt();
|
||||||
@@ -432,17 +444,11 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRegionGenerated(int x, int z) {
|
public void onRegionGenerated(int x, int z) {
|
||||||
shouldGc();
|
// No forced System.gc() here: a wall-clock full STW collection mid-generation stalled
|
||||||
rgc++;
|
// everything; MantleHeapPressure's 96% panic reclaim already owns heap pressure.
|
||||||
broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_DONE);
|
broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_DONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void shouldGc() {
|
|
||||||
if (cl.flip() && rgc > 16) {
|
|
||||||
System.gc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void broadcastRegionDelta(int regionX, int regionZ, int state) {
|
private void broadcastRegionDelta(int regionX, int regionZ, int state) {
|
||||||
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
|
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
|
||||||
if (protocolServer == null) {
|
if (protocolServer == null) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.render.IrisRenderer;
|
|||||||
import art.arcane.iris.engine.framework.render.RenderType;
|
import art.arcane.iris.engine.framework.render.RenderType;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -281,7 +282,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateEngine() {
|
public boolean updateEngine() {
|
||||||
if (engine.isClosed()) {
|
if (engine.isClosed() || engine.isClosing() || engine.getComplex() == null) {
|
||||||
try {
|
try {
|
||||||
Engine reacquired = GuiHost.get().findActiveEngine();
|
Engine reacquired = GuiHost.get().findActiveEngine();
|
||||||
if (reacquired != null && !reacquired.isClosed()) {
|
if (reacquired != null && !reacquired.isClosed()) {
|
||||||
@@ -451,12 +452,19 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
double mk = mscale;
|
double mk = mscale;
|
||||||
double mkd = scale;
|
double mkd = scale;
|
||||||
e.submit(() -> {
|
e.submit(() -> {
|
||||||
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
// finally: a render throw is swallowed by the discarded Future, and a
|
||||||
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType);
|
// leaked key permanently blocks the admission gate (working < 9).
|
||||||
rs.put(ps.getMilliseconds());
|
try {
|
||||||
working.remove(key);
|
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
||||||
if (mk == mscale && mkd == scale) {
|
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType);
|
||||||
positions.put(key, b);
|
rs.put(ps.getMilliseconds());
|
||||||
|
if (mk == mscale && mkd == scale) {
|
||||||
|
positions.put(key, b);
|
||||||
|
}
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
IrisLogging.debug("Vision tile render failed: " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
|
||||||
|
} finally {
|
||||||
|
working.remove(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -472,12 +480,17 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
double mk = mscale;
|
double mk = mscale;
|
||||||
double mkd = scale;
|
double mkd = scale;
|
||||||
eh.submit(() -> {
|
eh.submit(() -> {
|
||||||
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
try {
|
||||||
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType);
|
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
||||||
rs.put(ps.getMilliseconds());
|
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType);
|
||||||
workingfast.remove(key);
|
rs.put(ps.getMilliseconds());
|
||||||
if (mk == mscale && mkd == scale) {
|
if (mk == mscale && mkd == scale) {
|
||||||
fastpositions.put(key, b);
|
fastpositions.put(key, b);
|
||||||
|
}
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
IrisLogging.debug("Vision tile render failed: " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
|
||||||
|
} finally {
|
||||||
|
workingfast.remove(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@@ -507,12 +520,30 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void paint(Graphics gx) {
|
public void paint(Graphics gx) {
|
||||||
if (engine.isClosed() && !updateEngine()) {
|
// Cover the closing window too: releaseRuntime nulls the complex BEFORE closed flips,
|
||||||
|
// and a paint NPE in that window killed the self-driven repaint loop forever.
|
||||||
|
if ((engine.isClosed() || engine.isClosing() || engine.getComplex() == null) && !updateEngine()) {
|
||||||
EventQueue.invokeLater(() -> {
|
EventQueue.invokeLater(() -> {
|
||||||
try { setVisible(false); } catch (Throwable ignored) { }
|
try { setVisible(false); } catch (Throwable ignored) { }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
paintBody(gx);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.debug("Vision paint failed: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
// The repaint loop is entirely self-driven from here; it must survive any render
|
||||||
|
// exception or the window freezes on a stale frame permanently.
|
||||||
|
long sleepMs = eco ? 32 : 16;
|
||||||
|
J.a(() -> {
|
||||||
|
J.sleep(sleepMs);
|
||||||
|
repaint();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintBody(Graphics gx) {
|
||||||
|
|
||||||
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
|
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
|
||||||
oxp = lerp(oxp, ox, 0.36);
|
oxp = lerp(oxp, ox, 0.36);
|
||||||
@@ -587,6 +618,16 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bounded, not pruned-to-frame: the low-quality cache is the pan placeholder, but
|
||||||
|
// unbounded growth while panning leaked one BufferedImage per visited tile forever.
|
||||||
|
if (fastpositions.size() > 4096) {
|
||||||
|
for (BlockPosition i : fastpositions.k()) {
|
||||||
|
if (!gg.contains(i)) {
|
||||||
|
fastpositions.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleFollow();
|
handleFollow();
|
||||||
renderOverlays(g, p.getMilliseconds());
|
renderOverlays(g, p.getMilliseconds());
|
||||||
|
|
||||||
@@ -594,12 +635,6 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long targetMs = eco ? 32 : 16;
|
|
||||||
long sleepMs = Math.max(1, targetMs - (long) p.getMilliseconds());
|
|
||||||
J.a(() -> {
|
|
||||||
J.sleep(sleepMs);
|
|
||||||
repaint();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
|
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ public final class IrisWorldRemovalService {
|
|||||||
public CompletableFuture<Void> endMaintenance(ResolvedWorld resolvedWorld) {
|
public CompletableFuture<Void> endMaintenance(ResolvedWorld resolvedWorld) {
|
||||||
World world = resolvedWorld.loadedWorld();
|
World world = resolvedWorld.loadedWorld();
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-remove");
|
IrisToolbelt.endWorldMaintenance(world, "world-remove", true);
|
||||||
}
|
}
|
||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ package art.arcane.iris.core.link;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.volmlib.util.data.Cuboid;
|
import art.arcane.volmlib.util.data.Cuboid;
|
||||||
|
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
public class WorldEditLink {
|
public class WorldEditLink {
|
||||||
private static final AtomicCache<Boolean> active = new AtomicCache<>();
|
private static final AtomicCache<Boolean> active = new AtomicCache<>();
|
||||||
|
private static final ChronoLatch errorThrottle = new ChronoLatch(60_000);
|
||||||
|
|
||||||
public static Cuboid getSelection(Player p) {
|
public static Cuboid getSelection(Player p) {
|
||||||
if (!hasWorldEdit())
|
if (!hasWorldEdit())
|
||||||
@@ -42,16 +45,40 @@ public class WorldEditLink {
|
|||||||
(int) min.getClass().getDeclaredMethod("z").invoke(max)
|
(int) min.getClass().getDeclaredMethod("z").invoke(max)
|
||||||
);
|
);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.error("Could not get selection");
|
if (errorThrottle.flip()) {
|
||||||
e.printStackTrace();
|
IrisLogging.error("Could not get selection");
|
||||||
IrisLogging.reportError(e);
|
e.printStackTrace();
|
||||||
active.reset();
|
IrisLogging.reportError(e);
|
||||||
active.aquire(() -> false);
|
}
|
||||||
|
invalidate();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasWorldEdit() {
|
public static boolean hasWorldEdit() {
|
||||||
return active.aquire(() -> Bukkit.getPluginManager().isPluginEnabled("WorldEdit"));
|
return hasWorldEdit(() -> Bukkit.getPluginManager().isPluginEnabled("WorldEdit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hasWorldEdit(BooleanSupplier detector) {
|
||||||
|
if (Boolean.TRUE.equals(active.getIfPresent())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean present;
|
||||||
|
try {
|
||||||
|
present = detector.getAsBoolean();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (present) {
|
||||||
|
active.aquire(() -> Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void invalidate() {
|
||||||
|
active.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import art.arcane.iris.engine.object.IrisBlockData;
|
|||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisEntity;
|
import art.arcane.iris.engine.object.IrisEntity;
|
||||||
import art.arcane.iris.engine.object.IrisExpression;
|
import art.arcane.iris.engine.object.IrisExpression;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectScale;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisImage;
|
import art.arcane.iris.engine.object.IrisImage;
|
||||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||||
@@ -86,12 +87,20 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||||
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
||||||
|
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
|
||||||
|
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
|
||||||
|
// drifts while an engine runs, so a hashing set would silently fail removal and pin every
|
||||||
|
// engine-hosting IrisData (and its pack graph) for the JVM lifetime.
|
||||||
|
private static final Set<IrisData> ENGINE_HOLDERS =
|
||||||
|
Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||||
private final File dataFolder;
|
private final File dataFolder;
|
||||||
private final int id;
|
private final int id;
|
||||||
private final boolean datapackCompiler;
|
private final boolean datapackCompiler;
|
||||||
@@ -262,7 +271,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (File i : PackDirectoryResolver.listVisiblePackDirectories(
|
for (File i : PackDirectoryResolver.listVisiblePackDirectories(
|
||||||
IrisPlatforms.get().dataFolder("packs"))) {
|
IrisPlatforms.get().packsFolder())) {
|
||||||
IrisData dm = get(i);
|
IrisData dm = get(i);
|
||||||
if (dm == nearest) continue;
|
if (dm == nearest) continue;
|
||||||
T t = dm.load(type, key, false);
|
T t = dm.load(type, key, false);
|
||||||
@@ -338,6 +347,30 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when any loader — including detached {@link #openRuntime(File)} instances, which
|
||||||
|
* never enter the dataLoaders cache — has a live engine reading the given pack folder.
|
||||||
|
* The dataLoaders cache cannot answer this question: engines only ever attach to
|
||||||
|
* openRuntime instances.
|
||||||
|
*/
|
||||||
|
public static boolean hasActiveEngines(File dataFolder) {
|
||||||
|
Path target = dataFolder.toPath().toAbsolutePath().normalize();
|
||||||
|
IrisData[] holders;
|
||||||
|
synchronized (ENGINE_HOLDERS) {
|
||||||
|
holders = ENGINE_HOLDERS.toArray(new IrisData[0]);
|
||||||
|
}
|
||||||
|
for (IrisData data : holders) {
|
||||||
|
if (data.getEngines().isEmpty()) {
|
||||||
|
ENGINE_HOLDERS.remove(data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (data.getDataFolder().toPath().toAbsolutePath().normalize().equals(target)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void registerEngine(Engine engine) {
|
public void registerEngine(Engine engine) {
|
||||||
Objects.requireNonNull(engine, "engine");
|
Objects.requireNonNull(engine, "engine");
|
||||||
synchronized (engines) {
|
synchronized (engines) {
|
||||||
@@ -348,6 +381,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
}
|
}
|
||||||
engines.add(engine);
|
engines.add(engine);
|
||||||
}
|
}
|
||||||
|
ENGINE_HOLDERS.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unregisterEngine(Engine engine) {
|
public void unregisterEngine(Engine engine) {
|
||||||
@@ -359,9 +393,12 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
if (iterator.next() == engine) {
|
if (iterator.next() == engine) {
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (engines.isEmpty()) {
|
||||||
|
ENGINE_HOLDERS.remove(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +420,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
synchronized (engines) {
|
synchronized (engines) {
|
||||||
engines.clear();
|
engines.clear();
|
||||||
}
|
}
|
||||||
|
ENGINE_HOLDERS.remove(this);
|
||||||
dataLoaders.remove(dataFolder, this);
|
dataLoaders.remove(dataFolder, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +463,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
|
|
||||||
public synchronized void hotloaded() {
|
public synchronized void hotloaded() {
|
||||||
StructureGraphCatalog.invalidate(this);
|
StructureGraphCatalog.invalidate(this);
|
||||||
|
IrisObjectScale.invalidate();
|
||||||
closed = false;
|
closed = false;
|
||||||
possibleSnippets = new KMap<>();
|
possibleSnippets = new KMap<>();
|
||||||
builder = new GsonBuilder()
|
builder = new GsonBuilder()
|
||||||
@@ -490,6 +529,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
|
|
||||||
public void dump() {
|
public void dump() {
|
||||||
StructureGraphCatalog.invalidate(this);
|
StructureGraphCatalog.invalidate(this);
|
||||||
|
IrisObjectScale.invalidate();
|
||||||
for (ResourceLoader<?> i : loaders.values()) {
|
for (ResourceLoader<?> i : loaders.values()) {
|
||||||
i.clearCache();
|
i.clearCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ import java.util.Objects;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -79,14 +79,8 @@ import java.util.zip.GZIPOutputStream;
|
|||||||
public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||||
public static final AtomicDouble tlt = new AtomicDouble(0);
|
public static final AtomicDouble tlt = new AtomicDouble(0);
|
||||||
private static final int CACHE_SIZE = 100000;
|
private static final int CACHE_SIZE = 100000;
|
||||||
private static final ExecutorService schemaBuildExecutor = Executors.newSingleThreadExecutor(runnable -> {
|
private static volatile ExecutorService schemaBuildExecutor;
|
||||||
Thread thread = new Thread(runnable, "Iris-Schema-Builder");
|
|
||||||
thread.setDaemon(true);
|
|
||||||
thread.setPriority(Thread.MIN_PRIORITY);
|
|
||||||
return thread;
|
|
||||||
});
|
|
||||||
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
|
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
|
||||||
private static final AtomicBoolean schemaBuildExecutorRegistered = new AtomicBoolean();
|
|
||||||
protected final AtomicCache<KList<File>> folderCache;
|
protected final AtomicCache<KList<File>> folderCache;
|
||||||
protected volatile KSet<String> firstAccess;
|
protected volatile KSet<String> firstAccess;
|
||||||
protected File root;
|
protected File root;
|
||||||
@@ -124,11 +118,32 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
|||||||
IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath());
|
IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath());
|
||||||
if (options.registerPreservation()) {
|
if (options.registerPreservation()) {
|
||||||
IrisServices.get(PreservationRegistry.class).registerCache(this);
|
IrisServices.get(PreservationRegistry.class).registerCache(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily (re)creates the schema-build pool. PreservationSVC shutdownNow()s every
|
||||||
|
* registered executor on plugin disable; a static final pool registered once was dead for
|
||||||
|
* the rest of the JVM after a hot reload, breaking every studio workspace refresh. Each
|
||||||
|
* created pool is registered with the CURRENT cycle's preservation registry.
|
||||||
|
*/
|
||||||
|
private static synchronized ExecutorService schemaBuildExecutor() {
|
||||||
|
ExecutorService current = schemaBuildExecutor;
|
||||||
|
if (current == null || current.isShutdown()) {
|
||||||
|
schemaBuildQueue.clear();
|
||||||
|
current = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris-Schema-Builder");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
schemaBuildExecutor = current;
|
||||||
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
|
||||||
if (preservation != null && schemaBuildExecutorRegistered.compareAndSet(false, true)) {
|
if (preservation != null) {
|
||||||
preservation.register(schemaBuildExecutor);
|
preservation.register(current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject buildSchema() {
|
public JSONObject buildSchema() {
|
||||||
@@ -145,15 +160,22 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
|||||||
File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json");
|
File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json");
|
||||||
String schemaPath = a.getAbsolutePath();
|
String schemaPath = a.getAbsolutePath();
|
||||||
if (schemaBuildQueue.add(schemaPath)) {
|
if (schemaBuildQueue.add(schemaPath)) {
|
||||||
schemaBuildExecutor.execute(() -> {
|
try {
|
||||||
try {
|
schemaBuildExecutor().execute(() -> {
|
||||||
IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4));
|
try {
|
||||||
} catch (Throwable e) {
|
IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4));
|
||||||
IrisLogging.reportError(e);
|
} catch (Throwable e) {
|
||||||
} finally {
|
IrisLogging.reportError(e);
|
||||||
schemaBuildQueue.remove(schemaPath);
|
} finally {
|
||||||
}
|
schemaBuildQueue.remove(schemaPath);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
// Never let a dead pool leak the queue entry or escape into workspace
|
||||||
|
// generation (which would delete the user's .code-workspace on the way out).
|
||||||
|
schemaBuildQueue.remove(schemaPath);
|
||||||
|
IrisLogging.warn("Schema build skipped (executor unavailable): " + schemaPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return o;
|
return o;
|
||||||
@@ -475,7 +497,9 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KSet<String> set = firstAccess;
|
KSet<String> set = firstAccess;
|
||||||
if (set != null) set.add(name);
|
// contains-first: CHM.add takes the bin monitor even when the key is present, and
|
||||||
|
// every generation thread hammers the same few keys through this line.
|
||||||
|
if (set != null && !set.contains(name)) set.add(name);
|
||||||
return loadCache.get(name);
|
return loadCache.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public final class DirectorCommandMessages {
|
|||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_GENERATE_CHUNKS_INTO_BUFFERS_NO_WORLD_WRITES_HASH_BLOCKS_BIOMES_CAPTURES_GOLDEN = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_GENERATE_CHUNKS_INTO_BUFFERS_NO_WORLD_WRITES_HASH_BLOCKS_BIOMES_CAPTURES_GOLDEN = TextKey.of(
|
||||||
"iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden",
|
"iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden",
|
||||||
"Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds."
|
"Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds."
|
||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_SCAN = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_SCAN = TextKey.of(
|
||||||
"iris.director.commanddeveloper.param.world_scan",
|
"iris.director.commanddeveloper.param.world_scan",
|
||||||
@@ -168,7 +168,7 @@ public final class DirectorCommandMessages {
|
|||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_PARAM_DELETE_MANTLE_DATA_SCAN_AREA_FIRST_FULL_REGENERATION_FROM_SCRATCH = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_PARAM_DELETE_MANTLE_DATA_SCAN_AREA_FIRST_FULL_REGENERATION_FROM_SCRATCH = TextKey.of(
|
||||||
"iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch",
|
"iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch",
|
||||||
"Delete mantle data in the scan area first for full regeneration from scratch"
|
"Delete the world's entire mantle folder first for full regeneration from scratch"
|
||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_PARAM_CONCURRENT_CHUNK_GENERATIONS_1_STRICTLY_SERIAL_ORDER_DEPENDENCE_TESTING = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_PARAM_CONCURRENT_CHUNK_GENERATIONS_1_STRICTLY_SERIAL_ORDER_DEPENDENCE_TESTING = TextKey.of(
|
||||||
"iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing",
|
"iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing",
|
||||||
|
|||||||
@@ -25,9 +25,37 @@ import org.bukkit.Bukkit;
|
|||||||
|
|
||||||
public class INMS {
|
public class INMS {
|
||||||
//@done
|
//@done
|
||||||
private static final INMSBinding binding = bind();
|
private static final INMSBinding binding;
|
||||||
|
private static final Throwable bindFailure;
|
||||||
|
|
||||||
|
// A throwing field initializer would leave this class permanently erroneous — every
|
||||||
|
// later touch raises NoClassDefFoundError and buries the real "unsupported Minecraft
|
||||||
|
// version" message. Capture the failure so callers can report the actual cause.
|
||||||
|
static {
|
||||||
|
INMSBinding bound = null;
|
||||||
|
Throwable failure = null;
|
||||||
|
try {
|
||||||
|
bound = bind();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failure = t;
|
||||||
|
}
|
||||||
|
binding = bound;
|
||||||
|
bindFailure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBound() {
|
||||||
|
return binding != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Throwable bindFailure() {
|
||||||
|
return bindFailure;
|
||||||
|
}
|
||||||
|
|
||||||
public static INMSBinding get() {
|
public static INMSBinding get() {
|
||||||
|
if (binding == null) {
|
||||||
|
throw new IllegalStateException("Iris NMS binding is unavailable: "
|
||||||
|
+ (bindFailure == null ? "unknown failure" : bindFailure.getMessage()), bindFailure);
|
||||||
|
}
|
||||||
return binding;
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,13 @@ public interface INMSBinding {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any instrumentation installed by {@link #injectBukkit()}. Must be idempotent;
|
||||||
|
* called on plugin disable and pre-unload so the transformer cannot outlive the plugin.
|
||||||
|
*/
|
||||||
|
default void uninjectBukkit() {
|
||||||
|
}
|
||||||
|
|
||||||
KMap<Material, List<BlockProperty>> getBlockProperties();
|
KMap<Material, List<BlockProperty>> getBlockProperties();
|
||||||
|
|
||||||
private void validateDimensionTypes(WorldCreator c) {
|
private void validateDimensionTypes(WorldCreator c) {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class IrisPack {
|
|||||||
* @return the file path
|
* @return the file path
|
||||||
*/
|
*/
|
||||||
public static File packsPack(String name) {
|
public static File packsPack(String name) {
|
||||||
return IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME, name);
|
return IrisPlatforms.get().packsFolderNoCreate(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KList<File> collectFiles(File f, String fileExtension) {
|
private static KList<File> collectFiles(File f, String fileExtension) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class IrisPackRepository {
|
|||||||
private String repo = "overworld";
|
private String repo = "overworld";
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String branch = "stable";
|
private String branch = PackDownloader.DEFAULT_BRANCH;
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String tag = "";
|
private String tag = "";
|
||||||
@@ -96,7 +96,7 @@ public class IrisPackRepository {
|
|||||||
return IrisPackRepository.builder()
|
return IrisPackRepository.builder()
|
||||||
.user("IrisDimensions")
|
.user("IrisDimensions")
|
||||||
.repo(g)
|
.repo(g)
|
||||||
.branch("stable")
|
.branch(PackDownloader.DEFAULT_BRANCH)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ public class IrisPackRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException {
|
public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException {
|
||||||
File pack = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME, getRepo());
|
File pack = IrisPlatforms.get().packsFolderNoCreate(getRepo());
|
||||||
|
|
||||||
if (!pack.exists()) {
|
if (!pack.exists()) {
|
||||||
File dl = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "dltk-" + UUID.randomUUID() + ".zip");
|
File dl = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "dltk-" + UUID.randomUUID() + ".zip");
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates biome layer stacks. caveCeilingLayers reuses the height generators built from layers,
|
||||||
|
* so a biome with more ceiling entries than surface entries has no generator for the extras; the
|
||||||
|
* engine skips them, and this validator surfaces the mistake to the author at validate time.
|
||||||
|
*/
|
||||||
|
final class PackBiomeLayerValidator {
|
||||||
|
/** Both layers and caveCeilingLayers default to a single entry when absent (IrisBiome field initializers). */
|
||||||
|
private static final int DEFAULT_LAYER_COUNT = 1;
|
||||||
|
|
||||||
|
private PackBiomeLayerValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> validateCeilingLayerCounts(File biomesFolder) {
|
||||||
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
|
if (biomesFolder == null || !biomesFolder.isDirectory()) {
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder);
|
||||||
|
biomeFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
for (File biomeFile : biomeFiles) {
|
||||||
|
String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile);
|
||||||
|
JSONObject biome;
|
||||||
|
try {
|
||||||
|
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// Invalid JSON is reported by the graph validators; layer counts have nothing to add.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer layers = arrayLength(biome, "layers", biomeKey, blockingErrors);
|
||||||
|
Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, blockingErrors);
|
||||||
|
if (layers == null || ceiling == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ceiling > layers) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' declares " + ceiling + " caveCeilingLayers but only "
|
||||||
|
+ layers + " layers. caveCeilingLayers reuses the layers height generators and must not have more entries.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer arrayLength(JSONObject biome, String field, String biomeKey, List<String> blockingErrors) {
|
||||||
|
if (!biome.has(field) || biome.isNull(field)) {
|
||||||
|
return DEFAULT_LAYER_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray array = biome.optJSONArray(field);
|
||||||
|
if (array == null) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return array.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.pack;
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisDimensionType;
|
||||||
import art.arcane.volmlib.util.json.JSONArray;
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ final class PackDimensionValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
||||||
|
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||||
|
|
||||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||||
if (regionsArray == null || regionsArray.length() == 0) {
|
if (regionsArray == null || regionsArray.length() == 0) {
|
||||||
@@ -226,4 +228,84 @@ final class PackDimensionValidator {
|
|||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors the IrisDimensionType constructor checks so a pack that would throw at world creation
|
||||||
|
* fails validation instead. Defaults must match the POJOs exactly: dimensionHeight absent means
|
||||||
|
* the IrisDimension field initializer (-64..320); present-but-partial means the IrisRange field
|
||||||
|
* initializers (min 16, max 32); logicalHeight absent means 256.
|
||||||
|
*/
|
||||||
|
static void validateDimensionHeights(File packFolder, String dimensionKey, JSONObject dimJson, List<String> blockingErrors) {
|
||||||
|
JSONObject range = resolveDimensionHeight(packFolder, dimJson);
|
||||||
|
if (range == null && dimJson.has("dimensionHeight") && !dimJson.isNull("dimensionHeight")) {
|
||||||
|
if (!(dimJson.opt("dimensionHeight") instanceof String)) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight must be an object or a range snippet reference.");
|
||||||
|
}
|
||||||
|
// Unresolvable snippet references are reported by the content-key machinery.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minY;
|
||||||
|
int maxY;
|
||||||
|
if (range == null) {
|
||||||
|
minY = -64;
|
||||||
|
maxY = 320;
|
||||||
|
} else {
|
||||||
|
minY = (int) range.optDouble("min", 16D);
|
||||||
|
maxY = (int) range.optDouble("max", 32D);
|
||||||
|
}
|
||||||
|
int height = maxY - minY;
|
||||||
|
int logicalHeight = dimJson.optInt("logicalHeight", 256);
|
||||||
|
|
||||||
|
if (height < IrisDimensionType.MIN_HEIGHT || height > IrisDimensionType.MAX_HEIGHT) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight span (max - min) is " + height
|
||||||
|
+ "; it must be between " + IrisDimensionType.MIN_HEIGHT + " and " + IrisDimensionType.MAX_HEIGHT + ".");
|
||||||
|
} else if ((height & (IrisDimensionType.HEIGHT_STEP - 1)) != 0) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight span (max - min) is " + height
|
||||||
|
+ "; it must be a multiple of " + IrisDimensionType.HEIGHT_STEP + ".");
|
||||||
|
}
|
||||||
|
if (minY < IrisDimensionType.MIN_MIN_Y || minY > IrisDimensionType.MAX_MIN_Y) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight.min is " + minY
|
||||||
|
+ "; it must be between " + IrisDimensionType.MIN_MIN_Y + " and " + IrisDimensionType.MAX_MIN_Y + ".");
|
||||||
|
} else if ((minY & (IrisDimensionType.HEIGHT_STEP - 1)) != 0) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight.min is " + minY
|
||||||
|
+ "; it must be a multiple of " + IrisDimensionType.HEIGHT_STEP + ".");
|
||||||
|
}
|
||||||
|
if (logicalHeight < 0) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' logicalHeight is " + logicalHeight + "; it cannot be negative.");
|
||||||
|
} else if (logicalHeight > height) {
|
||||||
|
blockingErrors.add("Dimension '" + dimensionKey + "' logicalHeight is " + logicalHeight
|
||||||
|
+ "; it cannot be greater than the dimension height of " + height + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject resolveDimensionHeight(File packFolder, JSONObject dimJson) {
|
||||||
|
if (!dimJson.has("dimensionHeight") || dimJson.isNull("dimensionHeight")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject inline = dimJson.optJSONObject("dimensionHeight");
|
||||||
|
if (inline != null) {
|
||||||
|
return inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
String reference = dimJson.optString("dimensionHeight", null);
|
||||||
|
if (reference == null || !reference.startsWith("snippet/")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Mirror IrisData's snippet adapter: canonical snippet/range/... is used verbatim; any other
|
||||||
|
// snippet/... reference is re-rooted under this field's snippet folder.
|
||||||
|
if (!reference.startsWith("snippet/range/")) {
|
||||||
|
reference = "snippet/range/" + reference.substring("snippet/".length());
|
||||||
|
}
|
||||||
|
File snippet = new File(packFolder, reference + ".json");
|
||||||
|
if (!snippet.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
public final class PackDownloader {
|
public final class PackDownloader {
|
||||||
|
/**
|
||||||
|
* The branch every download path falls back to when the caller did not name one. Compile-time
|
||||||
|
* constant so Director {@code @Param(defaultValue = ...)} annotations can reference it.
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_BRANCH = "stable";
|
||||||
|
private static final String HEAD_REF = "HEAD";
|
||||||
private static final String DEFAULT_OVERWORLD_PACK = "overworld";
|
private static final String DEFAULT_OVERWORLD_PACK = "overworld";
|
||||||
private static final String DEFAULT_OVERWORLD_REPOSITORY = "IrisDimensions/overworld";
|
private static final String DEFAULT_OVERWORLD_REPOSITORY = "IrisDimensions/overworld";
|
||||||
private static final String DEFAULT_OVERWORLD_REF = "master";
|
private static final String DEFAULT_OVERWORLD_REF = "master";
|
||||||
@@ -234,6 +240,13 @@ public final class PackDownloader {
|
|||||||
String url = directUrl ? ref : resolveGithubArchiveUrl(repo, ref);
|
String url = directUrl ? ref : resolveGithubArchiveUrl(repo, ref);
|
||||||
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.DOWNLOADING, MessageArgument.untrusted("url", url)) + " ");
|
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.DOWNLOADING, MessageArgument.untrusted("url", url)) + " ");
|
||||||
File zip = WebCache.getNonCachedFile("pack-" + repo, url, ARCHIVE_LIMITS.maxArchiveBytes());
|
File zip = WebCache.getNonCachedFile("pack-" + repo, url, ARCHIVE_LIMITS.maxArchiveBytes());
|
||||||
|
if ((zip == null || !zip.exists()) && !directUrl && DEFAULT_BRANCH.equals(ref)) {
|
||||||
|
// A repo without the shared default branch still resolves via its HEAD; one retry, never recursive.
|
||||||
|
String headUrl = resolveGithubArchiveUrl(repo, HEAD_REF);
|
||||||
|
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.DOWNLOADING, MessageArgument.untrusted("url", headUrl)) + " ");
|
||||||
|
zip = WebCache.getNonCachedFile("pack-" + repo, headUrl, ARCHIVE_LIMITS.maxArchiveBytes());
|
||||||
|
url = headUrl;
|
||||||
|
}
|
||||||
File temp = WebCache.getTemp();
|
File temp = WebCache.getTemp();
|
||||||
File work = new File(temp, "dl-" + UUID.randomUUID());
|
File work = new File(temp, "dl-" + UUID.randomUUID());
|
||||||
|
|
||||||
@@ -436,17 +449,21 @@ public final class PackDownloader {
|
|||||||
+ " (required primary dimension is missing).");
|
+ " (required primary dimension is missing).");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<IrisData> loadedData = IrisData.getLoaded(new File(packsFolder, prepared.key()));
|
// A registered loader alone is not "active": startup validation registers a loader for
|
||||||
if (loadedData.isEmpty()) {
|
// every visible pack (permanently), which made force-updating any installed pack
|
||||||
loadedData = IrisData.getLoaded(target.toFile());
|
// impossible. Live engines only ever attach to detached openRuntime loaders, so the
|
||||||
}
|
// gate must go through the engine index, not the cached loader's own engine list.
|
||||||
if (loadedData.isPresent()) {
|
// Stale cached registrations are closed so the swap cannot race a loader holding the
|
||||||
|
// old tree.
|
||||||
|
if (IrisData.hasActiveEngines(target.toFile())) {
|
||||||
sendFeedback(
|
sendFeedback(
|
||||||
feedback,
|
feedback,
|
||||||
"Pack '" + prepared.key() + "' is active and cannot be replaced safely. Unload its worlds before retrying."
|
"Pack '" + prepared.key() + "' is active and cannot be replaced safely. Unload its worlds before retrying."
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
IrisData.getLoaded(new File(packsFolder, prepared.key())).ifPresent(IrisData::close);
|
||||||
|
IrisData.getLoaded(target.toFile()).ifPresent(IrisData::close);
|
||||||
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) {
|
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) {
|
||||||
publication.commit();
|
publication.commit();
|
||||||
try {
|
try {
|
||||||
@@ -471,7 +488,7 @@ public final class PackDownloader {
|
|||||||
Set<Path> roots = new LinkedHashSet<>();
|
Set<Path> roots = new LinkedHashSet<>();
|
||||||
roots.add(packsRoot);
|
roots.add(packsRoot);
|
||||||
if (IrisPlatforms.isBound()) {
|
if (IrisPlatforms.isBound()) {
|
||||||
roots.add(IrisPlatforms.get().dataFolder("packs").toPath().toAbsolutePath().normalize());
|
roots.add(IrisPlatforms.get().packsFolder().toPath().toAbsolutePath().normalize());
|
||||||
}
|
}
|
||||||
for (Path root : roots) {
|
for (Path root : roots) {
|
||||||
if (!Files.isDirectory(root)) {
|
if (!Files.isDirectory(root)) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the
|
||||||
|
* modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
|
||||||
|
* carry markers, markers carry spawners, spawners carry entities, entities carry loot. These
|
||||||
|
* helpers keep the two packagers walking placements identically.
|
||||||
|
*/
|
||||||
|
public final class PackExportClosure {
|
||||||
|
private PackExportClosure() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
|
||||||
|
KSet<String> markerKeys = new KSet<>();
|
||||||
|
if (placements == null) {
|
||||||
|
return markerKeys;
|
||||||
|
}
|
||||||
|
for (IrisObjectPlacement placement : placements) {
|
||||||
|
if (placement == null || placement.getMarkers() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisObjectMarker marker : placement.getMarkers()) {
|
||||||
|
if (marker == null || marker.getMarker() == null || marker.getMarker().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
markerKeys.add(marker.getMarker());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markerKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KSet<String> collectObjectKeys(Iterable<IrisObjectPlacement> placements) {
|
||||||
|
KSet<String> objectKeys = new KSet<>();
|
||||||
|
if (placements == null) {
|
||||||
|
return objectKeys;
|
||||||
|
}
|
||||||
|
for (IrisObjectPlacement placement : placements) {
|
||||||
|
if (placement == null || placement.getPlace() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String key : placement.getPlace()) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
objectKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns when two generator files with identical content are both referenced by biomes. IrisGenerator's
|
||||||
|
* equals ignores the load key and IrisComplex buckets generators into a HashSet, so only one of a
|
||||||
|
* content-identical pair survives; biomes linked to the losing key resolve 0/0 height bounds and
|
||||||
|
* contribute a flat band at fluid height.
|
||||||
|
*
|
||||||
|
* <p>The fingerprint is a conservative approximation of IrisGenerator.equals built from raw JSON
|
||||||
|
* (sorted keys, canonical number formatting). It can miss a collision where one file spells out a
|
||||||
|
* value the other leaves defaulted, and it ignores fields the POJO does not declare - acceptable
|
||||||
|
* for a warning aimed at the copy-the-file-and-forget-the-seed mistake.
|
||||||
|
*/
|
||||||
|
final class PackGeneratorDuplicateValidator {
|
||||||
|
private static final String GENERATOR_SNIPPET_FOLDER = "snippet/generator-layer/";
|
||||||
|
|
||||||
|
private PackGeneratorDuplicateValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> validateDuplicateGenerators(File packFolder) {
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
File generatorsFolder = new File(packFolder, "generators");
|
||||||
|
if (!generatorsFolder.isDirectory()) {
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> referenced = collectReferencedGeneratorKeys(packFolder);
|
||||||
|
Map<String, List<String>> byFingerprint = new TreeMap<>();
|
||||||
|
List<File> generatorFiles = PackValidationIo.listJsonRecursive(generatorsFolder);
|
||||||
|
generatorFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
for (File generatorFile : generatorFiles) {
|
||||||
|
String key = PackValidationIo.deriveKey(generatorsFolder, generatorFile);
|
||||||
|
String fingerprint;
|
||||||
|
try {
|
||||||
|
fingerprint = fingerprint(new JSONObject(Files.readString(generatorFile.toPath(), StandardCharsets.UTF_8)));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byFingerprint.computeIfAbsent(fingerprint, ignored -> new ArrayList<>()).add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (List<String> group : byFingerprint.values()) {
|
||||||
|
List<String> referencedKeys = group.stream().filter(referenced::contains).sorted().toList();
|
||||||
|
if (referencedKeys.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
warnings.add("Generators " + String.join(", ", referencedKeys)
|
||||||
|
+ " have identical content and are both referenced by biomes. Iris buckets generators by value"
|
||||||
|
+ " (IrisGenerator equals ignores the load key), so only one survives and biomes referencing the"
|
||||||
|
+ " others get a zero height band. Give each generator a distinct value (for example a different"
|
||||||
|
+ " seed) or point every biome at a single key.");
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> collectReferencedGeneratorKeys(File packFolder) {
|
||||||
|
Set<String> referenced = new HashSet<>();
|
||||||
|
File biomesFolder = new File(packFolder, "biomes");
|
||||||
|
if (!biomesFolder.isDirectory()) {
|
||||||
|
return referenced;
|
||||||
|
}
|
||||||
|
for (File biomeFile : PackValidationIo.listJsonRecursive(biomesFolder)) {
|
||||||
|
JSONObject biome;
|
||||||
|
try {
|
||||||
|
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONArray links = biome.optJSONArray("generators");
|
||||||
|
if (links == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < links.length(); i++) {
|
||||||
|
JSONObject link = links.optJSONObject(i);
|
||||||
|
if (link == null) {
|
||||||
|
String reference = links.optString(i, null);
|
||||||
|
link = resolveGeneratorLayerSnippet(packFolder, reference);
|
||||||
|
if (link == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referenced.add(link.optString("generator", "default"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return referenced;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject resolveGeneratorLayerSnippet(File packFolder, String reference) {
|
||||||
|
if (reference == null || !reference.startsWith("snippet/")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String resolved = reference.startsWith(GENERATOR_SNIPPET_FOLDER)
|
||||||
|
? reference
|
||||||
|
: GENERATOR_SNIPPET_FOLDER + reference.substring("snippet/".length());
|
||||||
|
File snippet = new File(packFolder, resolved + ".json");
|
||||||
|
if (!snippet.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fingerprint(Object value) {
|
||||||
|
if (value instanceof JSONObject object) {
|
||||||
|
Map<String, String> sorted = new TreeMap<>();
|
||||||
|
for (String key : object.keySet()) {
|
||||||
|
sorted.put(key, fingerprint(object.get(key)));
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder("{");
|
||||||
|
sorted.forEach((key, child) -> builder.append(key).append('=').append(child).append(';'));
|
||||||
|
return builder.append('}').toString();
|
||||||
|
}
|
||||||
|
if (value instanceof JSONArray array) {
|
||||||
|
StringBuilder builder = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < array.length(); i++) {
|
||||||
|
builder.append(fingerprint(array.get(i))).append(';');
|
||||||
|
}
|
||||||
|
return builder.append(']').toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
// Gson deserializes 1 and 1.0 into the same field value; fingerprint them alike.
|
||||||
|
return Double.toString(number.doubleValue());
|
||||||
|
}
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,10 +34,18 @@ final class PackLootValidator {
|
|||||||
private PackLootValidator() {
|
private PackLootValidator() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<String> validateLootGraph(File packFolder) {
|
record LootGraphIssues(List<String> errors, List<String> warnings) {
|
||||||
|
LootGraphIssues {
|
||||||
|
errors = List.copyOf(errors);
|
||||||
|
warnings = List.copyOf(warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LootGraphIssues validateLootGraph(File packFolder) {
|
||||||
List<String> blockingErrors = new ArrayList<>();
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
if (packFolder == null || !packFolder.isDirectory()) {
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
return blockingErrors;
|
return new LootGraphIssues(blockingErrors, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
File lootFolder = new File(packFolder, PackValidator.LOOT_FOLDER);
|
File lootFolder = new File(packFolder, PackValidator.LOOT_FOLDER);
|
||||||
@@ -69,10 +77,10 @@ final class PackLootValidator {
|
|||||||
}
|
}
|
||||||
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
|
String resourceType = PackStructurePlacementValidator.structureHostType(folderName);
|
||||||
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
|
String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile);
|
||||||
validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors);
|
validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors, warnings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return blockingErrors;
|
return new LootGraphIssues(blockingErrors, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateLootTable(String lootKey, JSONObject table, List<String> blockingErrors) {
|
private static void validateLootTable(String lootKey, JSONObject table, List<String> blockingErrors) {
|
||||||
@@ -158,17 +166,20 @@ final class PackLootValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot,
|
private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot,
|
||||||
Set<String> lootKeys, List<String> blockingErrors) {
|
Set<String> lootKeys, List<String> blockingErrors, List<String> warnings) {
|
||||||
String path = resourceType + " '" + resourceKey + "'.loot";
|
String path = resourceType + " '" + resourceKey + "'.loot";
|
||||||
if (!(rawLoot instanceof JSONObject reference)) {
|
if (!(rawLoot instanceof JSONObject reference)) {
|
||||||
blockingErrors.add(path + " must be an object.");
|
blockingErrors.add(path + " must be an object.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
boolean clearMode = false;
|
||||||
if (reference.has("mode")) {
|
if (reference.has("mode")) {
|
||||||
Object rawMode = reference.opt("mode");
|
Object rawMode = reference.opt("mode");
|
||||||
if (!(rawMode instanceof String mode)
|
if (!(rawMode instanceof String mode)
|
||||||
|| !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) {
|
|| !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) {
|
||||||
blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK.");
|
blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK.");
|
||||||
|
} else {
|
||||||
|
clearMode = "CLEAR".equals(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (reference.has("multiplier")) {
|
if (reference.has("multiplier")) {
|
||||||
@@ -189,6 +200,10 @@ final class PackLootValidator {
|
|||||||
blockingErrors.add(path + ".tables must be an array.");
|
blockingErrors.add(path + ".tables must be an array.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (clearMode && tables.length() > 0) {
|
||||||
|
warnings.add(path + " uses mode CLEAR and lists " + tables.length()
|
||||||
|
+ " table(s); CLEAR clears parent tables and contributes no tables of its own, so these entries are dead. Use REPLACE to substitute them.");
|
||||||
|
}
|
||||||
for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) {
|
for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) {
|
||||||
Object rawTableKey = tables.opt(tableIndex);
|
Object rawTableKey = tables.opt(tableIndex);
|
||||||
if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) {
|
if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) {
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catches the shared IrisStyledRange class default (min 16, max 32) leaking into pack content.
|
||||||
|
* The POJO cannot distinguish an empty object from an explicit 16/32 after Gson runs, so the raw
|
||||||
|
* JSON is the only place this mistake is still visible: an objects[].densityStyle of {} places
|
||||||
|
* 16-32 objects per chunk, and a caveProfile.densityThreshold of {} hollows the whole cave range.
|
||||||
|
*/
|
||||||
|
final class PackStyledRangeDefaultValidator {
|
||||||
|
private static final String STYLE_RANGE_SNIPPET_FOLDER = "snippet/style-range/";
|
||||||
|
|
||||||
|
record Validation(List<String> errors, List<String> warnings) {
|
||||||
|
Validation {
|
||||||
|
errors = List.copyOf(errors);
|
||||||
|
warnings = List.copyOf(warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackStyledRangeDefaultValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Validation validate(File packFolder) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
|
return new Validation(errors, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) {
|
||||||
|
File resourceFolder = new File(packFolder, folderName);
|
||||||
|
if (!resourceFolder.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<File> resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder);
|
||||||
|
resourceFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
String type = hostType(folderName);
|
||||||
|
for (File resourceFile : resourceFiles) {
|
||||||
|
String key = PackValidationIo.deriveKey(resourceFolder, resourceFile);
|
||||||
|
JSONObject json;
|
||||||
|
try {
|
||||||
|
json = new JSONObject(Files.readString(resourceFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateObjectPlacements(packFolder, type, key, json, errors, warnings);
|
||||||
|
validateCaveProfileThreshold(packFolder, type, key, json.optJSONObject("caveProfile"), errors, warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Validation(errors, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateObjectPlacements(File packFolder, String type, String key, JSONObject json,
|
||||||
|
List<String> errors, List<String> warnings) {
|
||||||
|
JSONArray objects = json.optJSONArray("objects");
|
||||||
|
if (objects == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < objects.length(); i++) {
|
||||||
|
JSONObject placement = objects.optJSONObject(i);
|
||||||
|
if (placement == null || !placement.has("densityStyle") || placement.isNull("densityStyle")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkRange(packFolder, placement.opt("densityStyle"),
|
||||||
|
type + " '" + key + "' objects[" + i + "].densityStyle",
|
||||||
|
"16-32 objects per chunk", "remove densityStyle to use the scalar density field",
|
||||||
|
errors, warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateCaveProfileThreshold(File packFolder, String type, String key, JSONObject caveProfile,
|
||||||
|
List<String> errors, List<String> warnings) {
|
||||||
|
if (caveProfile == null || !caveProfile.has("densityThreshold") || caveProfile.isNull("densityThreshold")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkRange(packFolder, caveProfile.opt("densityThreshold"),
|
||||||
|
type + " '" + key + "' caveProfile.densityThreshold",
|
||||||
|
"a carve threshold of 16-32, hollowing the entire vertical range",
|
||||||
|
"remove densityThreshold to keep the cave profile default",
|
||||||
|
errors, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkRange(File packFolder, Object raw, String path, String consequence, String removal,
|
||||||
|
List<String> errors, List<String> warnings) {
|
||||||
|
JSONObject range = null;
|
||||||
|
String via = "";
|
||||||
|
if (raw instanceof JSONObject inline) {
|
||||||
|
range = inline;
|
||||||
|
} else if (raw instanceof String reference && reference.startsWith("snippet/")) {
|
||||||
|
String resolved = reference.startsWith(STYLE_RANGE_SNIPPET_FOLDER)
|
||||||
|
? reference
|
||||||
|
: STYLE_RANGE_SNIPPET_FOLDER + reference.substring("snippet/".length());
|
||||||
|
File snippet = new File(packFolder, resolved + ".json");
|
||||||
|
if (!snippet.isFile()) {
|
||||||
|
return; // Missing snippets are reported by the content-key machinery.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
range = new JSONObject(Files.readString(snippet.toPath(), StandardCharsets.UTF_8));
|
||||||
|
via = " (via snippet '" + resolved + "')";
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (range == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasMin = range.has("min") && !range.isNull("min");
|
||||||
|
boolean hasMax = range.has("max") && !range.isNull("max");
|
||||||
|
if (!hasMin && !hasMax) {
|
||||||
|
errors.add(path + via + " omits both min and max, which resolves to the shared IrisStyledRange default of "
|
||||||
|
+ consequence + ". Set min and max explicitly, or " + removal + ".");
|
||||||
|
} else if (!hasMin || !hasMax) {
|
||||||
|
String missing = hasMin ? "max" : "min";
|
||||||
|
String defaultValue = hasMin ? "32" : "16";
|
||||||
|
warnings.add(path + via + " omits " + missing + ", which falls back to the shared IrisStyledRange default of "
|
||||||
|
+ defaultValue + ". Set it explicitly if that is not intended.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hostType(String folderName) {
|
||||||
|
String singular = folderName.endsWith("s") ? folderName.substring(0, folderName.length() - 1) : folderName;
|
||||||
|
return singular.substring(0, 1).toUpperCase(Locale.ROOT) + singular.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,7 +77,9 @@ public final class PackValidator {
|
|||||||
|
|
||||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||||
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
|
||||||
blockingErrors.addAll(PackLootValidator.validateLootGraph(packFolder));
|
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
|
||||||
|
addDistinct(blockingErrors, lootIssues.errors());
|
||||||
|
addDistinct(warnings, lootIssues.warnings());
|
||||||
blockingErrors.addAll(PackObjectSurfaceValidator.validateRemovedWorldgenFields(packFolder));
|
blockingErrors.addAll(PackObjectSurfaceValidator.validateRemovedWorldgenFields(packFolder));
|
||||||
blockingErrors.addAll(PackObjectSurfaceValidator.validateObjectSurfaceSupport(packFolder));
|
blockingErrors.addAll(PackObjectSurfaceValidator.validateObjectSurfaceSupport(packFolder));
|
||||||
blockingErrors.addAll(PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(packFolder));
|
blockingErrors.addAll(PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(packFolder));
|
||||||
@@ -96,6 +98,11 @@ public final class PackValidator {
|
|||||||
new File(packFolder, "spawners"), new File(packFolder, "entities")));
|
new File(packFolder, "spawners"), new File(packFolder, "entities")));
|
||||||
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
|
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
|
||||||
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory));
|
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory));
|
||||||
|
blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes")));
|
||||||
|
PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder);
|
||||||
|
addDistinct(blockingErrors, styledRanges.errors());
|
||||||
|
addDistinct(warnings, styledRanges.warnings());
|
||||||
|
addDistinct(warnings, PackGeneratorDuplicateValidator.validateDuplicateGenerators(packFolder));
|
||||||
|
|
||||||
// Strict content mode promotes unresolved keys and bad block properties from advisory to blocking. Palette
|
// Strict content mode promotes unresolved keys and bad block properties from advisory to blocking. Palette
|
||||||
// -sourced findings are exempt and stay warnings - see ContentKeyValidator.collectContentKeyIssues.
|
// -sourced findings are exempt and stay warnings - see ContentKeyValidator.collectContentKeyIssues.
|
||||||
|
|||||||
@@ -199,14 +199,17 @@ public class IrisPregenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
init();
|
|
||||||
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
|
|
||||||
startTime.set(M.ms());
|
|
||||||
ticker.start();
|
|
||||||
checkRegions();
|
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
boolean completed = false;
|
boolean completed = false;
|
||||||
|
// Everything runs inside the try: an early throw from init/checkRegions must still
|
||||||
|
// reach shutdown(), or the ticker/monitor threads leak and listener.onClose never
|
||||||
|
// fires — leaving a phantom job that suppresses spawns and blocks future pregens.
|
||||||
try {
|
try {
|
||||||
|
init();
|
||||||
|
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
|
||||||
|
startTime.set(M.ms());
|
||||||
|
ticker.start();
|
||||||
|
checkRegions();
|
||||||
int[] regionBounds = task.regionBounds();
|
int[] regionBounds = task.regionBounds();
|
||||||
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
|
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
|
||||||
task.iterateRegions((x, z) -> visitRegion(x, z, true));
|
task.iterateRegions((x, z) -> visitRegion(x, z, true));
|
||||||
@@ -371,10 +374,13 @@ public class IrisPregenerator {
|
|||||||
|
|
||||||
generator.generateChunk(xx, zz, listener);
|
generator.generateChunk(xx, zz, listener);
|
||||||
});
|
});
|
||||||
generator.onRegionSubmitted(x, z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit) {
|
if (hit) {
|
||||||
|
// Exactly once per visited region, on BOTH branches: a cache-completed region that
|
||||||
|
// never releases its sentinel wedges neighbor eviction for the whole resume
|
||||||
|
// frontier (allNeighborsDrained can never pass around it).
|
||||||
|
generator.onRegionSubmitted(x, z);
|
||||||
listener.onRegionGenerated(x, z);
|
listener.onRegionGenerated(x, z);
|
||||||
|
|
||||||
if (saveLatch.flip()) {
|
if (saveLatch.flip()) {
|
||||||
@@ -391,7 +397,9 @@ public class IrisPregenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generatedRegions.add(pos);
|
generatedRegions.add(pos);
|
||||||
checkRegions();
|
// No checkRegions() here: re-spiraling every region after every completed region
|
||||||
|
// was O(regions^2) dead work on the submitter thread (result discarded, and the
|
||||||
|
// only side effect was CachedPregenMethod faulting plates against eviction).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-5
@@ -57,7 +57,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class AsyncPregenMethod implements PregeneratorMethod {
|
public class AsyncPregenMethod implements PregeneratorMethod {
|
||||||
|
// THREAD_COUNT records the pool's original size while boosted; BOOST_HOLDERS counts live
|
||||||
|
// jobs holding the boost. Pairing them per instance stops a dying job (whose close can
|
||||||
|
// land minutes after its replacement started) from shrinking the pool under the live job.
|
||||||
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
|
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
|
||||||
|
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
||||||
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||||
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
|
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
|
||||||
@@ -102,6 +106,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private final AtomicLong failed = new AtomicLong();
|
private final AtomicLong failed = new AtomicLong();
|
||||||
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
||||||
private final AtomicBoolean closing = new AtomicBoolean();
|
private final AtomicBoolean closing = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
||||||
private final Object permitMonitor = new Object();
|
private final Object permitMonitor = new Object();
|
||||||
private volatile Engine metricsEngine;
|
private volatile Engine metricsEngine;
|
||||||
private volatile Mantle cachedMantle;
|
private volatile Mantle cachedMantle;
|
||||||
@@ -738,8 +743,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
|
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
|
||||||
+ ", urgent=" + urgent
|
+ ", urgent=" + urgent
|
||||||
+ ", timeout=" + timeoutSeconds + "s");
|
+ ", timeout=" + timeoutSeconds + "s");
|
||||||
if (workerPoolThreads > 0) {
|
if (workerPoolThreads > 0 && holdsWorkerBoost.compareAndSet(false, true)) {
|
||||||
increaseWorkerThreads();
|
acquireWorkerThreadBoost();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +780,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
flushAllRemainingChunks();
|
flushAllRemainingChunks();
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
resetWorkerThreads();
|
if (holdsWorkerBoost.compareAndSet(true, false)) {
|
||||||
|
releaseWorkerThreadBoost();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (interrupted) {
|
if (interrupted) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
@@ -945,7 +952,21 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void increaseWorkerThreads() {
|
public static void acquireWorkerThreadBoost() {
|
||||||
|
if (BOOST_HOLDERS.getAndIncrement() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
increaseWorkerThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void releaseWorkerThreadBoost() {
|
||||||
|
if (BOOST_HOLDERS.decrementAndGet() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetWorkerThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void increaseWorkerThreads() {
|
||||||
THREAD_COUNT.updateAndGet(i -> {
|
THREAD_COUNT.updateAndGet(i -> {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
return i;
|
return i;
|
||||||
@@ -974,7 +995,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resetWorkerThreads() {
|
private static void resetWorkerThreads() {
|
||||||
THREAD_COUNT.updateAndGet(i -> {
|
THREAD_COUNT.updateAndGet(i -> {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+38
-19
@@ -207,18 +207,28 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listener.onChunkGenerating(x, z);
|
listener.onChunkGenerating(x, z);
|
||||||
if (J.isFolia()) {
|
// Single choke point for failure accounting: without onChunkFailed a lossy run can
|
||||||
futures.add(PaperLib.getChunkAtAsync(world, x, z, true).thenAccept(c -> {
|
// never satisfy allVisitsComplete, so a finished pregen reports as aborted forever.
|
||||||
if (c != null) {
|
CompletableFuture<?> chunkFuture = J.isFolia()
|
||||||
lastUse.put(c, M.ms());
|
? PaperLib.getChunkAtAsync(world, x, z, true).thenAccept(c -> {
|
||||||
}
|
if (c != null) {
|
||||||
listener.onChunkGenerated(x, z);
|
lastUse.put(c, M.ms());
|
||||||
listener.onChunkCleaned(x, z);
|
}
|
||||||
}));
|
listener.onChunkGenerated(x, z);
|
||||||
return;
|
try {
|
||||||
}
|
listener.onChunkCleaned(x, z);
|
||||||
|
} catch (Throwable e) {
|
||||||
futures.add(scheduleChunkLoad(x, z, listener));
|
// Already counted as generated; a throw here must not also count the
|
||||||
|
// chunk failed through the whenComplete choke point below.
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: scheduleChunkLoad(x, z, listener);
|
||||||
|
futures.add(chunkFuture.whenComplete((r, err) -> {
|
||||||
|
if (err != null) {
|
||||||
|
listener.onChunkFailed(x, z);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<?> scheduleChunkLoad(int x, int z, PregenListener listener) {
|
private CompletableFuture<?> scheduleChunkLoad(int x, int z, PregenListener listener) {
|
||||||
@@ -294,12 +304,15 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
|||||||
IrisLogging.reportError(error);
|
IrisLogging.reportError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Chain instead of blocking: parking this MultiBurst worker on a 60s get()
|
||||||
generateChunkSync(x, z, listener).get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
// pinned a shared pool thread per failing chunk.
|
||||||
future.complete(null);
|
generateChunkSync(x, z, listener).whenComplete((r, fallbackError) -> {
|
||||||
} catch (Throwable fallbackError) {
|
if (fallbackError != null) {
|
||||||
future.completeExceptionally(fallbackError);
|
future.completeExceptionally(fallbackError);
|
||||||
}
|
} else {
|
||||||
|
future.complete(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,7 +327,13 @@ public class MedievalPregenMethod implements PregeneratorMethod {
|
|||||||
Chunk chunk = world.getChunkAt(x, z);
|
Chunk chunk = world.getChunkAt(x, z);
|
||||||
lastUse.put(chunk, M.ms());
|
lastUse.put(chunk, M.ms());
|
||||||
listener.onChunkGenerated(x, z);
|
listener.onChunkGenerated(x, z);
|
||||||
listener.onChunkCleaned(x, z);
|
try {
|
||||||
|
listener.onChunkCleaned(x, z);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// The chunk already counted as generated; a throw here must not also count it
|
||||||
|
// as failed through the whenComplete choke point.
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -131,16 +131,28 @@ public class IrisCodeWorkspace {
|
|||||||
project.getPath().mkdirs();
|
project.getPath().mkdirs();
|
||||||
File ws = getCodeWorkspaceFile();
|
File ws = getCodeWorkspaceFile();
|
||||||
|
|
||||||
|
// Render before touching the file: a config-generation failure has nothing to do with
|
||||||
|
// the existing workspace file, and deleting it before a rethrowing rebuild silently
|
||||||
|
// destroyed the author's workspace on every boot.
|
||||||
|
String rendered;
|
||||||
try {
|
try {
|
||||||
writeIfChanged(ws, createCodeWorkspaceConfig().toString(4));
|
rendered = createCodeWorkspaceConfig().toString(4);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
IrisLogging.warn("Could not generate the code workspace config for " + ws.getAbsolutePath() + "; leaving the existing workspace file untouched.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeIfChanged(ws, rendered);
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
|
IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
|
||||||
ws.delete();
|
ws.delete();
|
||||||
try {
|
try {
|
||||||
IO.writeAll(ws, createCodeWorkspaceConfig());
|
IO.writeAll(ws, rendered);
|
||||||
} catch (IOException e1) {
|
} catch (Throwable e1) {
|
||||||
IrisLogging.reportError(e1);
|
IrisLogging.reportError(e1);
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import art.arcane.iris.engine.object.IrisDimension;
|
|||||||
import art.arcane.iris.engine.object.IrisEntity;
|
import art.arcane.iris.engine.object.IrisEntity;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisLootTable;
|
import art.arcane.iris.engine.object.IrisLootTable;
|
||||||
|
import art.arcane.iris.core.pack.PackExportClosure;
|
||||||
|
import art.arcane.iris.engine.object.IrisMarker;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.IrisSpawner;
|
import art.arcane.iris.engine.object.IrisSpawner;
|
||||||
@@ -49,6 +51,7 @@ import org.zeroturnaround.zip.ZipUtil;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -62,8 +65,25 @@ public class IrisPackageCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) {
|
public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) {
|
||||||
|
// Detached loader on purpose: obfuscation rewrites the placement 'place' lists on the
|
||||||
|
// loaded resources, which must never leak into the shared cached pack state — a second
|
||||||
|
// export against a mutated cache produced archives with an empty objects/ directory.
|
||||||
|
IrisData dm = IrisData.openRuntime(project.getPath());
|
||||||
|
try {
|
||||||
|
return compilePackage(dm, sender, obfuscate, minify);
|
||||||
|
} finally {
|
||||||
|
dm.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void addIfPresent(KSet<T> set, T value) {
|
||||||
|
if (value != null) {
|
||||||
|
set.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File compilePackage(IrisData dm, VolmitSender sender, boolean obfuscate, boolean minify) {
|
||||||
String dimm = project.getName();
|
String dimm = project.getName();
|
||||||
IrisData dm = IrisData.get(project.getPath());
|
|
||||||
IrisDimension dimension = dm.getDimensionLoader().load(dimm);
|
IrisDimension dimension = dm.getDimensionLoader().load(dimm);
|
||||||
File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey());
|
File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey());
|
||||||
IO.delete(folder);
|
IO.delete(folder);
|
||||||
@@ -82,20 +102,35 @@ public class IrisPackageCompiler {
|
|||||||
KSet<IrisLootTable> loot = new KSet<>();
|
KSet<IrisLootTable> loot = new KSet<>();
|
||||||
KSet<IrisBlockData> blocks = new KSet<>();
|
KSet<IrisBlockData> blocks = new KSet<>();
|
||||||
|
|
||||||
|
// KSet is ConcurrentHashMap-backed: both add(null) and remove(null) throw NPE, so a
|
||||||
|
// failed loader lookup must be filtered at the add site, never stripped afterwards.
|
||||||
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
||||||
blocks.add(dm.getBlockLoader().load(i));
|
addIfPresent(blocks, dm.getBlockLoader().load(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i)));
|
dimension.getRegions().forEach((i) -> addIfPresent(regions, dm.getRegionLoader().load(i)));
|
||||||
dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)));
|
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||||
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
|
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
|
||||||
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
|
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
|
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||||
dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)));
|
dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||||
biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm))));
|
biomes.forEach((i) -> i.getGenerators().forEach((j) -> addIfPresent(generators, j.getCachedGenerator(() -> dm))));
|
||||||
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
|
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
|
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||||
collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i)));
|
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
||||||
|
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
||||||
|
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
||||||
|
KSet<IrisMarker> markers = new KSet<>();
|
||||||
|
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
||||||
|
IrisMarker marker = dm.getMarkerLoader().load(markerKey);
|
||||||
|
if (marker == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
markers.add(marker);
|
||||||
|
marker.getSpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||||
|
}
|
||||||
|
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
|
||||||
|
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||||
Set<String> structureKeys = new LinkedHashSet<>();
|
Set<String> structureKeys = new LinkedHashSet<>();
|
||||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
collectStructureKeys(structureKeys, dimension.getStructures());
|
||||||
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
||||||
@@ -106,25 +141,23 @@ public class IrisPackageCompiler {
|
|||||||
StringBuilder c = new StringBuilder();
|
StringBuilder c = new StringBuilder();
|
||||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS));
|
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS));
|
||||||
|
|
||||||
for (IrisBiome i : biomes) {
|
for (IrisObjectPlacement j : allPlacements) {
|
||||||
for (IrisObjectPlacement j : i.getObjects()) {
|
b.append(j.hashCode());
|
||||||
b.append(j.hashCode());
|
KList<String> newNames = new KList<>();
|
||||||
KList<String> newNames = new KList<>();
|
|
||||||
|
|
||||||
for (String k : j.getPlace()) {
|
for (String k : j.getPlace()) {
|
||||||
if (renameObjects.containsKey(k)) {
|
if (renameObjects.containsKey(k)) {
|
||||||
newNames.add(renameObjects.get(k));
|
newNames.add(renameObjects.get(k));
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", "");
|
|
||||||
b.append(name);
|
|
||||||
newNames.add(name);
|
|
||||||
renameObjects.put(k, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
j.setPlace(newNames);
|
String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", "");
|
||||||
|
b.append(name);
|
||||||
|
newNames.add(name);
|
||||||
|
renameObjects.put(k, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.setPlace(newNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
||||||
@@ -132,10 +165,18 @@ public class IrisPackageCompiler {
|
|||||||
ChronoLatch cl = new ChronoLatch(1000);
|
ChronoLatch cl = new ChronoLatch(1000);
|
||||||
O<Integer> ggg = new O<>();
|
O<Integer> ggg = new O<>();
|
||||||
ggg.set(0);
|
ggg.set(0);
|
||||||
biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) ->
|
O<Integer> missingObjects = new O<>();
|
||||||
|
missingObjects.set(0);
|
||||||
|
allPlacements.forEach((j) -> j.getPlace().forEach((k) ->
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0));
|
KList<String> sources = lookupObjects.get(k);
|
||||||
|
File f = sources == null || sources.isEmpty() ? null : dm.getObjectLoader().findFile(sources.get(0));
|
||||||
|
if (f == null) {
|
||||||
|
missingObjects.set(missingObjects.get() + 1);
|
||||||
|
IrisLogging.error("Missing object for placement key " + k);
|
||||||
|
return;
|
||||||
|
}
|
||||||
IO.copyFile(f, new File(folder, "objects/" + k + ".iob"));
|
IO.copyFile(f, new File(folder, "objects/" + k + ".iob"));
|
||||||
gb.append(IO.hash(f));
|
gb.append(IO.hash(f));
|
||||||
ggg.set(ggg.get() + 1);
|
ggg.set(ggg.get() + 1);
|
||||||
@@ -146,9 +187,15 @@ public class IrisPackageCompiler {
|
|||||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g))));
|
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g))));
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
missingObjects.set(missingObjects.get() + 1);
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
}
|
}
|
||||||
})));
|
}));
|
||||||
|
if (missingObjects.get() > 0) {
|
||||||
|
// A package missing objects must fail loudly, not ship as a clean "compiled".
|
||||||
|
throw new IllegalStateException(missingObjects.get()
|
||||||
|
+ " object(s) could not be exported for pack '" + dimm + "'; package compile aborted.");
|
||||||
|
}
|
||||||
|
|
||||||
b.append(IO.hash(gb.toString()));
|
b.append(IO.hash(gb.toString()));
|
||||||
c.append(IO.hash(b.toString()));
|
c.append(IO.hash(b.toString()));
|
||||||
@@ -205,6 +252,19 @@ public class IrisPackageCompiler {
|
|||||||
b.append(IO.hash(a));
|
b.append(IO.hash(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sorted so package.json.hash stays stable across runs.
|
||||||
|
for (IrisSpawner i : spawners.stream().sorted(Comparator.comparing(IrisSpawner::getLoadKey)).toList()) {
|
||||||
|
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||||
|
IO.writeAll(new File(folder, "spawners/" + i.getLoadKey() + ".json"), a);
|
||||||
|
b.append(IO.hash(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IrisMarker i : markers.stream().sorted(Comparator.comparing(IrisMarker::getLoadKey)).toList()) {
|
||||||
|
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||||
|
IO.writeAll(new File(folder, "markers/" + i.getLoadKey() + ".json"), a);
|
||||||
|
b.append(IO.hash(a));
|
||||||
|
}
|
||||||
|
|
||||||
c.append(IO.hash(b.toString()));
|
c.append(IO.hash(b.toString()));
|
||||||
String finalHash = IO.hash(c.toString());
|
String finalHash = IO.hash(c.toString());
|
||||||
JSONObject meta = new JSONObject();
|
JSONObject meta = new JSONObject();
|
||||||
|
|||||||
@@ -149,7 +149,10 @@ public final class GoldenHashEngine {
|
|||||||
|
|
||||||
if (request.resetMantle()) {
|
if (request.resetMantle()) {
|
||||||
progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_RESETTING_MANTLE));
|
progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_RESETTING_MANTLE));
|
||||||
resetMantleFull();
|
if (!resetMantleFull()) {
|
||||||
|
// A partial reset silently invalidates the capture; never scan over it.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int[]> targets = ChunkSpiral.centerOut(request.centerChunkX(), request.centerChunkZ(), radius);
|
List<int[]> targets = ChunkSpiral.centerOut(request.centerChunkX(), request.centerChunkZ(), radius);
|
||||||
@@ -184,29 +187,24 @@ public final class GoldenHashEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetMantleFull() {
|
private boolean resetMantleFull() {
|
||||||
|
Mantle mantle = engine.getMantle().getMantle();
|
||||||
try {
|
try {
|
||||||
Mantle mantle = engine.getMantle().getMantle();
|
// resetStorage invalidates the IOWorker channel cache before unlinking; deleting
|
||||||
mantle.saveAll();
|
// behind cached channels sent every post-reset plate write to an unlinked inode.
|
||||||
File folder = mantle.getDataFolder();
|
mantle.resetStorage();
|
||||||
File[] files = folder.listFiles();
|
|
||||||
if (files != null) {
|
|
||||||
for (File file : files) {
|
|
||||||
if (file.isFile()) {
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
feedback.ok(IrisLanguage.plain(
|
feedback.ok(IrisLanguage.plain(
|
||||||
RuntimeProgressMessages.GOLDEN_MANTLE_RESET,
|
RuntimeProgressMessages.GOLDEN_MANTLE_RESET,
|
||||||
MessageArgument.untrusted("path", folder.getAbsolutePath())
|
MessageArgument.untrusted("path", mantle.getDataFolder().getAbsolutePath())
|
||||||
));
|
));
|
||||||
|
return true;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
feedback.warn(IrisLanguage.plain(
|
feedback.warn(IrisLanguage.plain(
|
||||||
RuntimeProgressMessages.GOLDEN_MANTLE_RESET_FAILED,
|
RuntimeProgressMessages.GOLDEN_MANTLE_RESET_FAILED,
|
||||||
MessageArgument.untrusted("type", e.getClass().getSimpleName())
|
MessageArgument.untrusted("type", e.getClass().getSimpleName())
|
||||||
));
|
));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,11 +343,18 @@ public final class GoldenHashEngine {
|
|||||||
|
|
||||||
List<String> body = orderedBody(lines);
|
List<String> body = orderedBody(lines);
|
||||||
List<String> mismatches = new ArrayList<>();
|
List<String> mismatches = new ArrayList<>();
|
||||||
|
// Display strings and diagnosis input are different shapes: diagnose() parses a bare
|
||||||
|
// "<x> <z>" key, and feeding it a localized sentence aborted the diagnosis whenever
|
||||||
|
// the FIRST mismatch was a chunk missing from the golden capture.
|
||||||
|
String firstMismatchKey = null;
|
||||||
for (String line : body) {
|
for (String line : body) {
|
||||||
int second = line.indexOf(' ', line.indexOf(' ') + 1);
|
int second = line.indexOf(' ', line.indexOf(' ') + 1);
|
||||||
String key = line.substring(0, second);
|
String key = line.substring(0, second);
|
||||||
String golden = goldenChunks.get(key);
|
String golden = goldenChunks.get(key);
|
||||||
if (!line.equals(golden)) {
|
if (!line.equals(golden)) {
|
||||||
|
if (firstMismatchKey == null) {
|
||||||
|
firstMismatchKey = key;
|
||||||
|
}
|
||||||
mismatches.add(golden == null
|
mismatches.add(golden == null
|
||||||
? IrisLanguage.plain(
|
? IrisLanguage.plain(
|
||||||
RuntimeProgressMessages.GOLDEN_MISSING_IN_GOLDEN,
|
RuntimeProgressMessages.GOLDEN_MISSING_IN_GOLDEN,
|
||||||
@@ -400,7 +405,7 @@ public final class GoldenHashEngine {
|
|||||||
IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
|
IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
|
||||||
|
|
||||||
progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_DIAGNOSING));
|
progress.stage(IrisLanguage.plain(RuntimeProgressMessages.CHUNK_STAGE_DIAGNOSING));
|
||||||
diagnose(mismatches.getFirst());
|
diagnose(firstMismatchKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import org.bukkit.generator.ChunkGenerator.ChunkData;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public final class InPlaceChunkRegenerator {
|
public final class InPlaceChunkRegenerator {
|
||||||
private static final int BIOME_STEP = 4;
|
private static final int BIOME_STEP = 4;
|
||||||
@@ -111,36 +113,41 @@ public final class InPlaceChunkRegenerator {
|
|||||||
|
|
||||||
inFlight.acquire();
|
inFlight.acquire();
|
||||||
MultiBurst.burst.lazy(() -> {
|
MultiBurst.burst.lazy(() -> {
|
||||||
TerrainChunk buffer = TerrainChunk.create(world);
|
// Settle exactly once no matter which path fails: an unguarded throw here
|
||||||
try {
|
// (TerrainChunk.create, runRegion) parked the regen thread on the latch
|
||||||
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
|
// forever and stranded the player's boss bar. CAS is required — on Folia's
|
||||||
} catch (Throwable e) {
|
// owned-region path runRegion runs the apply inline before returning.
|
||||||
IrisLogging.reportError(e);
|
AtomicBoolean settled = new AtomicBoolean();
|
||||||
reporter.countApplied(false);
|
Consumer<Boolean> settle = ok -> {
|
||||||
inFlight.release();
|
if (settled.compareAndSet(false, true)) {
|
||||||
allApplied.countDown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
|
||||||
boolean ok = false;
|
|
||||||
try {
|
|
||||||
applyToLiveChunk(chunkX, chunkZ, buffer);
|
|
||||||
ok = true;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
} finally {
|
|
||||||
reporter.countApplied(ok);
|
reporter.countApplied(ok);
|
||||||
inFlight.release();
|
inFlight.release();
|
||||||
allApplied.countDown();
|
allApplied.countDown();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
try {
|
||||||
|
TerrainChunk buffer = TerrainChunk.create(world);
|
||||||
|
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
|
||||||
|
|
||||||
if (!scheduled) {
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
IrisLogging.warn("Regen could not schedule chunk apply at " + chunkX + "," + chunkZ + " in " + world.getName() + ".");
|
boolean ok = false;
|
||||||
reporter.countApplied(false);
|
try {
|
||||||
inFlight.release();
|
applyToLiveChunk(chunkX, chunkZ, buffer);
|
||||||
allApplied.countDown();
|
ok = true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
} finally {
|
||||||
|
settle.accept(ok);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!scheduled) {
|
||||||
|
IrisLogging.warn("Regen could not schedule chunk apply at " + chunkX + "," + chunkZ + " in " + world.getName() + ".");
|
||||||
|
settle.accept(false);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
settle.accept(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ public final class StudioOpenCoordinator {
|
|||||||
});
|
});
|
||||||
return operation.whenComplete((result, throwable) -> {
|
return operation.whenComplete((result, throwable) -> {
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "studio-close");
|
IrisToolbelt.endWorldMaintenance(world, "studio-close", true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import art.arcane.iris.core.safeguard.task.Tasks;
|
|||||||
import art.arcane.iris.core.safeguard.task.ValueWithDiagnostics;
|
import art.arcane.iris.core.safeguard.task.ValueWithDiagnostics;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -15,7 +14,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public final class IrisSafeguard {
|
public final class IrisSafeguard {
|
||||||
private static volatile boolean forceShutdown = false;
|
|
||||||
private static Map<Task, ValueWithDiagnostics<Mode>> results = Collections.emptyMap();
|
private static Map<Task, ValueWithDiagnostics<Mode>> results = Collections.emptyMap();
|
||||||
private static Map<String, String> context = Collections.emptyMap();
|
private static Map<String, String> context = Collections.emptyMap();
|
||||||
private static Map<String, List<String>> attachment = Collections.emptyMap();
|
private static Map<String, List<String>> attachment = Collections.emptyMap();
|
||||||
@@ -100,10 +98,6 @@ public final class IrisSafeguard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isForceShutdown() {
|
|
||||||
return forceShutdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void warning() {
|
private static void warning() {
|
||||||
IrisLogging.warn(C.GOLD + "Iris is running in Warning Mode");
|
IrisLogging.warn(C.GOLD + "Iris is running in Warning Mode");
|
||||||
IrisLogging.warn(C.GRAY + "Some startup checks need attention. Review the messages above for tuning suggestions.");
|
IrisLogging.warn(C.GRAY + "Some startup checks need attention. Review the messages above for tuning suggestions.");
|
||||||
@@ -115,9 +109,9 @@ public final class IrisSafeguard {
|
|||||||
IrisLogging.error(C.DARK_RED + "Iris is running in Danger Mode");
|
IrisLogging.error(C.DARK_RED + "Iris is running in Danger Mode");
|
||||||
IrisLogging.error("");
|
IrisLogging.error("");
|
||||||
IrisLogging.error(C.DARK_GRAY + "--==<" + C.RED + " IMPORTANT " + C.DARK_GRAY + ">==--");
|
IrisLogging.error(C.DARK_GRAY + "--==<" + C.RED + " IMPORTANT " + C.DARK_GRAY + ">==--");
|
||||||
IrisLogging.error("Critical startup checks failed. Iris will continue startup in 10 seconds.");
|
IrisLogging.error("Critical startup checks failed. Review and resolve the errors above as soon as possible.");
|
||||||
IrisLogging.error("Review and resolve the errors above as soon as possible.");
|
// No startup sleep: blocking the boot thread protected nothing — world creation and
|
||||||
J.sleep(10000L);
|
// player admission are already gated by IrisStartupValidation.
|
||||||
IrisLogging.info("");
|
IrisLogging.info("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import art.arcane.iris.core.IrisWorldStorage;
|
|||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||||
import art.arcane.iris.core.safeguard.Mode;
|
import art.arcane.iris.core.safeguard.Mode;
|
||||||
|
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.util.common.misc.getHardware;
|
import art.arcane.iris.util.common.misc.getHardware;
|
||||||
import art.arcane.iris.util.project.agent.Agent;
|
import art.arcane.iris.util.project.agent.Agent;
|
||||||
@@ -99,13 +100,21 @@ public final class Tasks {
|
|||||||
supportedVersions = BuildConstants.MINECRAFT_VERSION;
|
supportedVersions = BuildConstants.MINECRAFT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!INMS.isBound()) {
|
||||||
|
String cause = INMS.bindFailure() == null ? "Unknown NMS bind failure" : INMS.bindFailure().getMessage();
|
||||||
|
return withDiagnostics(Mode.UNSTABLE,
|
||||||
|
Diagnostic.Logger.ERROR.create("Server Version"),
|
||||||
|
Diagnostic.Logger.ERROR.create("- " + cause),
|
||||||
|
Diagnostic.Logger.ERROR.create("- Iris only supports " + supportedVersions));
|
||||||
|
}
|
||||||
|
|
||||||
if (!(INMS.get() instanceof NMSBinding1X)) {
|
if (!(INMS.get() instanceof NMSBinding1X)) {
|
||||||
return withDiagnostics(Mode.STABLE);
|
return withDiagnostics(Mode.STABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return withDiagnostics(Mode.UNSTABLE,
|
return withDiagnostics(Mode.UNSTABLE,
|
||||||
Diagnostic.Logger.ERROR.create("Server Version"),
|
Diagnostic.Logger.ERROR.create("NMS Disabled"),
|
||||||
Diagnostic.Logger.ERROR.create("- Iris only supports " + supportedVersions));
|
Diagnostic.Logger.ERROR.create("- NMS support is disabled (general.disableNMS); Iris world creation is unavailable."));
|
||||||
});
|
});
|
||||||
|
|
||||||
private static final Task INJECTION = Task.of("injection", () -> {
|
private static final Task INJECTION = Task.of("injection", () -> {
|
||||||
@@ -149,7 +158,13 @@ public final class Tasks {
|
|||||||
});
|
});
|
||||||
|
|
||||||
private static final Task JAVA = Task.of("java", () -> {
|
private static final Task JAVA = Task.of("java", () -> {
|
||||||
int version = javaVersion();
|
int version = IrisSplashComposer.javaVersion();
|
||||||
|
if (version < 0) {
|
||||||
|
return withDiagnostics(Mode.WARNING,
|
||||||
|
Diagnostic.Logger.WARN.create("Java Runtime"),
|
||||||
|
Diagnostic.Logger.WARN.create("- Java version could not be determined (java.version="
|
||||||
|
+ System.getProperty("java.version") + ")."));
|
||||||
|
}
|
||||||
if (version == 25) {
|
if (version == 25) {
|
||||||
return withDiagnostics(Mode.STABLE);
|
return withDiagnostics(Mode.STABLE);
|
||||||
}
|
}
|
||||||
@@ -211,16 +226,4 @@ public final class Tasks {
|
|||||||
return new ValueWithDiagnostics<>(mode, diagnostics);
|
return new ValueWithDiagnostics<>(mode, diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int javaVersion() {
|
|
||||||
String version = System.getProperty("java.version");
|
|
||||||
if (version.startsWith("1.")) {
|
|
||||||
version = version.substring(2, 3);
|
|
||||||
} else {
|
|
||||||
int dot = version.indexOf(".");
|
|
||||||
if (dot != -1) {
|
|
||||||
version = version.substring(0, dot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Integer.parseInt(version);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ public class ExternalDataSVC implements IrisService {
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
IrisLogging.info("Loading ExternalDataProvider...");
|
IrisLogging.info("Loading ExternalDataProvider...");
|
||||||
Bukkit.getPluginManager().registerEvents(this, BukkitPlatform.plugin());
|
// enable() registers every enabled service as a listener; self-registration here
|
||||||
|
// doubled every handler invocation.
|
||||||
|
|
||||||
for (ProviderDefinition definition : BUILT_IN_PROVIDERS) {
|
for (ProviderDefinition definition : BUILT_IN_PROVIDERS) {
|
||||||
activateConfiguredProvider(definition);
|
activateConfiguredProvider(definition);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package art.arcane.iris.core.service;
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
|
||||||
import art.arcane.iris.util.common.plugin.IrisService;
|
import art.arcane.iris.util.common.plugin.IrisService;
|
||||||
import org.apache.logging.log4j.Level;
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -10,17 +9,26 @@ import org.apache.logging.log4j.core.LogEvent;
|
|||||||
import org.apache.logging.log4j.core.Logger;
|
import org.apache.logging.log4j.core.Logger;
|
||||||
import org.apache.logging.log4j.message.Message;
|
import org.apache.logging.log4j.message.Message;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class LogFilterSVC implements IrisService, Filter {
|
public class LogFilterSVC implements IrisService, Filter {
|
||||||
|
|
||||||
private static final String HEIGHTMAP_MISMATCH = "Ignoring heightmap data for chunk";
|
private static final String HEIGHTMAP_MISMATCH = "Ignoring heightmap data for chunk";
|
||||||
private static final String RAID_PERSISTENCE = "Could not save data net.minecraft.world.entity.raid.PersistentRaid";
|
private static final String RAID_PERSISTENCE = "Could not save data net.minecraft.world.entity.raid.PersistentRaid";
|
||||||
private static final String DUPLICATE_ENTITY_UUID = "UUID of added entity already exists";
|
private static final String DUPLICATE_ENTITY_UUID = "UUID of added entity already exists";
|
||||||
|
|
||||||
private static final KList<String> FILTERS = new KList<>();
|
// Immutable: check() runs on arbitrary logger threads, and the root logger is JVM-global,
|
||||||
|
// so a mutable static here was both a CME hazard and grew by three entries per enable.
|
||||||
|
private static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
|
||||||
|
|
||||||
|
private boolean installed = false;
|
||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
FILTERS.add(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
|
if (installed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
((Logger) LogManager.getRootLogger()).addFilter(this);
|
((Logger) LogManager.getRootLogger()).addFilter(this);
|
||||||
|
installed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -33,6 +41,11 @@ public class LogFilterSVC implements IrisService, Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (!installed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
|
||||||
|
installed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isStarted() {
|
public boolean isStarted() {
|
||||||
@@ -116,8 +129,14 @@ public class LogFilterSVC implements IrisService, Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Result check(String string) {
|
private Result check(String string) {
|
||||||
if (FILTERS.stream().anyMatch(string::contains))
|
if (string == null) {
|
||||||
return Result.DENY;
|
return Result.NEUTRAL;
|
||||||
|
}
|
||||||
|
for (String filter : FILTERS) {
|
||||||
|
if (string.contains(filter)) {
|
||||||
|
return Result.DENY;
|
||||||
|
}
|
||||||
|
}
|
||||||
return Result.NEUTRAL;
|
return Result.NEUTRAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,17 @@ import lombok.Getter;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ObjectSVC implements IrisService {
|
public class ObjectSVC implements IrisService {
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Deque<Map<Block, BlockData>> undos = new ArrayDeque<>();
|
// Concurrent + global-thread pipeline: pastes publish via J.runGlobal while the undo
|
||||||
|
// command arrives on an async pool thread; a plain ArrayDeque was mutated from both.
|
||||||
|
private final Deque<Map<Block, BlockData>> undos = new ConcurrentLinkedDeque<>();
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -50,7 +52,9 @@ public class ObjectSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void revertChanges(int amount) {
|
public void revertChanges(int amount) {
|
||||||
loopChange(amount);
|
if (!J.runGlobal(() -> loopChange(amount))) {
|
||||||
|
loopChange(amount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loopChange(int amount) {
|
private void loopChange(int amount) {
|
||||||
@@ -68,22 +72,26 @@ public class ObjectSVC implements IrisService {
|
|||||||
* @param blocks The blocks to remove
|
* @param blocks The blocks to remove
|
||||||
*/
|
*/
|
||||||
private void revert(Map<Block, BlockData> blocks) {
|
private void revert(Map<Block, BlockData> blocks) {
|
||||||
Iterator<Map.Entry<Block, BlockData>> it = blocks.entrySet().iterator();
|
if (blocks == null || blocks.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
|
Iterator<Map.Entry<Block, BlockData>> it = blocks.entrySet().iterator();
|
||||||
int amount = 0;
|
int amount = 0;
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Map.Entry<Block, BlockData> entry = it.next();
|
Map.Entry<Block, BlockData> entry = it.next();
|
||||||
BlockData data = entry.getValue();
|
entry.getKey().setBlockData(entry.getValue(), false);
|
||||||
entry.getKey().setBlockData(data, false);
|
|
||||||
|
|
||||||
it.remove();
|
it.remove();
|
||||||
|
|
||||||
amount++;
|
if (++amount >= 200) {
|
||||||
|
break;
|
||||||
if (amount > 200) {
|
|
||||||
J.s(() -> revert(blocks), 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!blocks.isEmpty()) {
|
||||||
|
J.s(() -> revert(blocks), 1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,10 @@ public class StudioSVC implements IrisService {
|
|||||||
validatePublishedPack(target);
|
validatePublishedPack(target);
|
||||||
|
|
||||||
IrisData installedData;
|
IrisData installedData;
|
||||||
boolean activeRuntime = previousData != null && !previousData.getEngines().isEmpty();
|
// Live engines only ever attach to detached openRuntime loaders, never to the
|
||||||
|
// dataLoaders-cached previousData — so the old previousData.getEngines() test was
|
||||||
|
// always empty and a running world's pack could be swapped with no restart.
|
||||||
|
boolean activeRuntime = IrisData.hasActiveEngines(target.toFile());
|
||||||
if (previousData == null) {
|
if (previousData == null) {
|
||||||
createdData = IrisData.get(target.toFile());
|
createdData = IrisData.get(target.toFile());
|
||||||
installedData = createdData;
|
installedData = createdData;
|
||||||
@@ -780,7 +783,7 @@ public class StudioSVC implements IrisService {
|
|||||||
return generator.closeAsync().thenApply(ignored -> true);
|
return generator.closeAsync().thenApply(ignored -> true);
|
||||||
})
|
})
|
||||||
.whenComplete((unloaded, throwable) -> {
|
.whenComplete((unloaded, throwable) -> {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "studio-disable");
|
IrisToolbelt.endWorldMaintenance(world, "studio-disable", true);
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
IrisLogging.reportError("Failed to unload studio world \"" + world.getName()
|
IrisLogging.reportError("Failed to unload studio world \"" + world.getName()
|
||||||
+ "\" during disable cleanup; startup deletion remains queued.", throwable);
|
+ "\" during disable cleanup; startup deletion remains queued.", throwable);
|
||||||
@@ -790,7 +793,7 @@ public class StudioSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "studio-disable");
|
IrisToolbelt.endWorldMaintenance(world, "studio-disable", true);
|
||||||
IrisLogging.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
IrisLogging.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ import java.util.concurrent.ThreadLocalRandom;
|
|||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class TreeSVC implements IrisService {
|
public class TreeSVC implements IrisService {
|
||||||
private boolean block = false;
|
// Re-entrancy guard for the synthetic StructureGrowEvent this service fires from inside
|
||||||
|
// its own handler. callEvent dispatches inline on the calling thread, so a ThreadLocal
|
||||||
|
// is exact; a shared boolean let one region's growth suppress another's on Folia.
|
||||||
|
private static final ThreadLocal<Boolean> REENTRANT = ThreadLocal.withInitial(() -> false);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -88,7 +91,7 @@ public class TreeSVC implements IrisService {
|
|||||||
*/
|
*/
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
public void on(StructureGrowEvent event) {
|
public void on(StructureGrowEvent event) {
|
||||||
if (block || event.isCancelled()) {
|
if (REENTRANT.get() || event.isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisLogging.debug(this.getClass().getName() + " received a structure grow event");
|
IrisLogging.debug(this.getClass().getName() + " received a structure grow event");
|
||||||
@@ -161,11 +164,13 @@ public class TreeSVC implements IrisService {
|
|||||||
public void set(int x, int y, int z, PlatformBlockState s) {
|
public void set(int x, int y, int z, PlatformBlockState s) {
|
||||||
BlockData d = (BlockData) s.nativeHandle();
|
BlockData d = (BlockData) s.nativeHandle();
|
||||||
Block b = event.getWorld().getBlockAt(x, y, z);
|
Block b = event.getWorld().getBlockAt(x, y, z);
|
||||||
|
// Listeners get the post-growth tree, per the StructureGrowEvent contract;
|
||||||
|
// a fresh snapshot here handed every plugin an all-air "tree".
|
||||||
BlockState state = b.getState();
|
BlockState state = b.getState();
|
||||||
if (d instanceof IrisCustomData data)
|
if (d instanceof IrisCustomData data)
|
||||||
state.setBlockData(data.getBase());
|
state.setBlockData(data.getBase());
|
||||||
else state.setBlockData(d);
|
else state.setBlockData(d);
|
||||||
blockStateList.add(b.getState());
|
blockStateList.add(state);
|
||||||
dataCache.put(new Location(event.getWorld(), x, y, z), d);
|
dataCache.put(new Location(event.getWorld(), x, y, z), d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,9 +250,12 @@ public class TreeSVC implements IrisService {
|
|||||||
Runnable growTask = () -> {
|
Runnable growTask = () -> {
|
||||||
|
|
||||||
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
|
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
|
||||||
block = true;
|
REENTRANT.set(true);
|
||||||
Bukkit.getServer().getPluginManager().callEvent(iGrow);
|
try {
|
||||||
block = false;
|
Bukkit.getServer().getPluginManager().callEvent(iGrow);
|
||||||
|
} finally {
|
||||||
|
REENTRANT.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!iGrow.isCancelled()) {
|
if (!iGrow.isCancelled()) {
|
||||||
for (BlockState state : iGrow.getBlocks()) {
|
for (BlockState state : iGrow.getBlocks()) {
|
||||||
@@ -338,6 +346,11 @@ public class TreeSVC implements IrisService {
|
|||||||
public Cuboid getSaplings(Location at, Predicate<BlockData> valid, World world) {
|
public Cuboid getSaplings(Location at, Predicate<BlockData> valid, World world) {
|
||||||
KList<BlockPosition> blockPositions = new KList<>();
|
KList<BlockPosition> blockPositions = new KList<>();
|
||||||
grow(at.getWorld(), new BlockPosition(at.getBlockX(), at.getBlockY(), at.getBlockZ()), valid, blockPositions);
|
grow(at.getWorld(), new BlockPosition(at.getBlockX(), at.getBlockY(), at.getBlockZ()), valid, blockPositions);
|
||||||
|
if (blockPositions.isEmpty()) {
|
||||||
|
// No matching saplings (e.g. a giant-mushroom grow event): a 1x1 plane at the
|
||||||
|
// event location, never the MIN/MAX sentinel cuboid below.
|
||||||
|
return new Cuboid(at, at);
|
||||||
|
}
|
||||||
BlockPosition a = new BlockPosition(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
|
BlockPosition a = new BlockPosition(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
|
||||||
BlockPosition b = new BlockPosition(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
|
BlockPosition b = new BlockPosition(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public final class IrisSplashComposer {
|
|||||||
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
|
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
|
||||||
prefix + style.label(" Web: ") + style.value("VolmitSoftware.com"),
|
prefix + style.label(" Web: ") + style.value("VolmitSoftware.com"),
|
||||||
prefix + style.label(" Server: ") + style.value(serverLine),
|
prefix + style.label(" Server: ") + style.value(serverLine),
|
||||||
prefix + style.label(" Java: ") + style.value(String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()),
|
prefix + style.label(" Java: ") + style.value(javaVersion() < 0 ? "unknown" : String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()),
|
||||||
prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT),
|
prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT),
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
@@ -54,17 +54,22 @@ public final class IrisSplashComposer {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total parser: java.version values like "25-ea" or "26-internal" have no dot but a
|
||||||
|
* non-numeric suffix, and a cosmetic banner must never be able to abort startup.
|
||||||
|
*
|
||||||
|
* @return the feature version, or -1 when it cannot be determined
|
||||||
|
*/
|
||||||
public static int javaVersion() {
|
public static int javaVersion() {
|
||||||
String version = System.getProperty("java.version");
|
String version = System.getProperty("java.version", "");
|
||||||
if (version.startsWith("1.")) {
|
if (version.startsWith("1.")) {
|
||||||
version = version.substring(2, 3);
|
version = version.length() > 2 ? version.substring(2, 3) : "";
|
||||||
} else {
|
|
||||||
int dot = version.indexOf('.');
|
|
||||||
if (dot != -1) {
|
|
||||||
version = version.substring(0, dot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Integer.parseInt(version);
|
int end = 0;
|
||||||
|
while (end < version.length() && Character.isDigit(version.charAt(end))) {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return end == 0 ? -1 : Integer.parseInt(version, 0, end, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String releaseTrain(String version) {
|
public static String releaseTrain(String version) {
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ public class IrisConverter {
|
|||||||
HudSlotClaim barClaim = reportProgress
|
HudSlotClaim barClaim = reportProgress
|
||||||
? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)))
|
? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)))
|
||||||
: null;
|
: null;
|
||||||
|
// try/finally over the whole decode: a throw must never leak the
|
||||||
|
// self-rescheduling progress task or the HUD claims.
|
||||||
|
try {
|
||||||
if (mv > 2_000_000) {
|
if (mv > 2_000_000) {
|
||||||
largeObject = true;
|
largeObject = true;
|
||||||
IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
|
IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
|
||||||
@@ -149,13 +152,15 @@ public class IrisConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i != -1) J.car(i);
|
} finally {
|
||||||
if (titleClaim != null) {
|
if (i != -1) J.car(i);
|
||||||
titleClaim.release();
|
if (titleClaim != null) {
|
||||||
}
|
titleClaim.release();
|
||||||
if (barClaim != null) {
|
}
|
||||||
barClaim.release();
|
if (barClaim != null) {
|
||||||
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job");
|
barClaim.release();
|
||||||
|
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
object.shrinkwrap();
|
object.shrinkwrap();
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ public class IrisCreator {
|
|||||||
|
|
||||||
World world = null;
|
World world = null;
|
||||||
boolean bukkitRegistered = false;
|
boolean bukkitRegistered = false;
|
||||||
|
PlatformChunkGenerator stagedGenerator = null;
|
||||||
try {
|
try {
|
||||||
reportStudioProgress(0.08D, "resolve_dimension");
|
reportStudioProgress(0.08D, "resolve_dimension");
|
||||||
reportStudioProgress(0.16D, "prepare_world_pack");
|
reportStudioProgress(0.16D, "prepare_world_pack");
|
||||||
@@ -252,6 +253,7 @@ public class IrisCreator {
|
|||||||
if (access == null) {
|
if (access == null) {
|
||||||
throw new IrisException("Access is null. Something bad happened.");
|
throw new IrisException("Access is null. Something bad happened.");
|
||||||
}
|
}
|
||||||
|
stagedGenerator = access;
|
||||||
HudSlotClaim createClaim = !benchmark && studioProgressConsumer == null && sender.isPlayer()
|
HudSlotClaim createClaim = !benchmark && studioProgressConsumer == null && sender.isPlayer()
|
||||||
? openLoaderClaim("iris:world-create")
|
? openLoaderClaim("iris:world-create")
|
||||||
: null;
|
: null;
|
||||||
@@ -332,7 +334,7 @@ public class IrisCreator {
|
|||||||
}
|
}
|
||||||
return world;
|
return world;
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
rollbackWorldCreation(worldKey, world, dimensionRoot, bukkitRegistered, failure);
|
rollbackWorldCreation(worldKey, world, stagedGenerator, dimensionRoot, bukkitRegistered, failure);
|
||||||
if (failure instanceof IrisException irisException) {
|
if (failure instanceof IrisException irisException) {
|
||||||
throw irisException;
|
throw irisException;
|
||||||
}
|
}
|
||||||
@@ -655,12 +657,23 @@ public class IrisCreator {
|
|||||||
private void rollbackWorldCreation(
|
private void rollbackWorldCreation(
|
||||||
NamespacedKey worldKey,
|
NamespacedKey worldKey,
|
||||||
World createdWorld,
|
World createdWorld,
|
||||||
|
PlatformChunkGenerator stagedGenerator,
|
||||||
File dimensionRoot,
|
File dimensionRoot,
|
||||||
boolean bukkitRegistered,
|
boolean bukkitRegistered,
|
||||||
Throwable failure
|
Throwable failure
|
||||||
) {
|
) {
|
||||||
World activeWorld = createdWorld == null ? WorldIdentity.resolve(worldKey).orElse(null) : createdWorld;
|
World activeWorld = createdWorld == null ? WorldIdentity.resolve(worldKey).orElse(null) : createdWorld;
|
||||||
boolean safeToDelete = activeWorld != null || !containsTimeout(failure);
|
boolean safeToDelete = activeWorld != null || !containsTimeout(failure);
|
||||||
|
if (activeWorld == null && stagedGenerator != null) {
|
||||||
|
// The world never materialized, so no unload path will ever close the staged
|
||||||
|
// generator; without this it stays registered on WorldInitEvent and attaches a
|
||||||
|
// second engine when a same-name world is created later.
|
||||||
|
try {
|
||||||
|
stagedGenerator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (Throwable rollbackFailure) {
|
||||||
|
failure.addSuppressed(unwrapFailure(rollbackFailure));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (activeWorld != null) {
|
if (activeWorld != null) {
|
||||||
IrisToolbelt.beginWorldMaintenance(activeWorld, "world-create-rollback", true);
|
IrisToolbelt.beginWorldMaintenance(activeWorld, "world-create-rollback", true);
|
||||||
try {
|
try {
|
||||||
@@ -682,6 +695,11 @@ public class IrisCreator {
|
|||||||
if (safeToDelete && generator != null) {
|
if (safeToDelete && generator != null) {
|
||||||
generator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
generator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
// A staged generator the world never bound (e.g. the creation failed after a
|
||||||
|
// retry re-staged a fresh one) has no unload path either.
|
||||||
|
if (stagedGenerator != null && stagedGenerator != generator) {
|
||||||
|
stagedGenerator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
} catch (Throwable rollbackFailure) {
|
} catch (Throwable rollbackFailure) {
|
||||||
Throwable cause = unwrapFailure(rollbackFailure);
|
Throwable cause = unwrapFailure(rollbackFailure);
|
||||||
failure.addSuppressed(cause);
|
failure.addSuppressed(cause);
|
||||||
@@ -690,7 +708,7 @@ public class IrisCreator {
|
|||||||
ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\".");
|
ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\".");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
IrisToolbelt.endWorldMaintenance(activeWorld, "world-create-rollback");
|
IrisToolbelt.endWorldMaintenance(activeWorld, "world-create-rollback", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,18 @@ public class IrisReflectiveAPI {
|
|||||||
WorldMaintenance.retainMantleDataForSlice(classname);
|
WorldMaintenance.retainMantleDataForSlice(classname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These delegate to IrisToolbelt so the caller's WORLD Y is rebased into mantle space
|
||||||
|
// (0..worldHeight). Raw pass-through silently no-opped below Y=0 and read the wrong
|
||||||
|
// cell everywhere else, diverging from IrisToolbelt and IrisModdedAPI semantics.
|
||||||
public static void setMantleData(World world, int x, int y, int z, Object data) {
|
public static void setMantleData(World world, int x, int y, int z, Object data) {
|
||||||
IrisToolbelt.access(world).getEngine().getMantle().getMantle().set(x, y, z, data);
|
IrisToolbelt.setMantleData(world, x, y, z, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void deleteMantleData(World world, int x, int y, int z, Class c) {
|
public static void deleteMantleData(World world, int x, int y, int z, Class c) {
|
||||||
IrisToolbelt.access(world).getEngine().getMantle().getMantle().remove(x, y, z, c);
|
IrisToolbelt.deleteMantleData(world, x, y, z, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Object getMantleData(World world, int x, int y, int z, Class c) {
|
public static Object getMantleData(World world, int x, int y, int z, Class c) {
|
||||||
return IrisToolbelt.access(world).getEngine().getMantle().getMantle().get(x, y, z, c);
|
return IrisToolbelt.getMantleData(world, x, y, z, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class IrisToolbelt {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
File packsFolder = IrisPlatforms.get().dataFolder("packs");
|
File packsFolder = IrisPlatforms.get().packsFolder();
|
||||||
File pack = PackDirectoryResolver.resolveExisting(packsFolder, reference.pack());
|
File pack = PackDirectoryResolver.resolveExisting(packsFolder, reference.pack());
|
||||||
if (pack == null) {
|
if (pack == null) {
|
||||||
File found = findCaseInsensitivePack(packsFolder, reference.pack());
|
File found = findCaseInsensitivePack(packsFolder, reference.pack());
|
||||||
@@ -302,6 +302,13 @@ public class IrisToolbelt {
|
|||||||
* @return the pregenerator job (already started)
|
* @return the pregenerator job (already started)
|
||||||
*/
|
*/
|
||||||
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) {
|
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) {
|
||||||
|
// Match the modded adapter's contract: a running job is a rejection, never a silent
|
||||||
|
// kill whose teardown (60s drain + 120s flush) overlaps the new job's generation.
|
||||||
|
// Callers that genuinely want replacement must stop the old job first
|
||||||
|
// (PregeneratorJob.shutdownAndWait).
|
||||||
|
if (PregeneratorJob.getInstance() != null) {
|
||||||
|
throw new IllegalStateException("An Iris pregeneration job is already running; stop it first with /iris pregen stop.");
|
||||||
|
}
|
||||||
applyPregenPerformanceProfile(engine);
|
applyPregenPerformanceProfile(engine);
|
||||||
boolean useCachedWrapper = false;
|
boolean useCachedWrapper = false;
|
||||||
if (cached && engine != null) {
|
if (cached && engine != null) {
|
||||||
@@ -588,17 +595,25 @@ public class IrisToolbelt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void endWorldMaintenance(World world, String reason) {
|
public static void endWorldMaintenance(World world, String reason) {
|
||||||
|
endWorldMaintenance(world, reason, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void endWorldMaintenance(World world, String reason, boolean bypassMantleStages) {
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
endWorldMaintenance(WorldIdentity.serialize(world), reason);
|
endWorldMaintenance(WorldIdentity.serialize(world), reason, bypassMantleStages);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void endWorldMaintenance(String worldName, String reason) {
|
public static void endWorldMaintenance(String worldName, String reason) {
|
||||||
WorldMaintenance.endWorldMaintenance(worldName, reason);
|
WorldMaintenance.endWorldMaintenance(worldName, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void endWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) {
|
||||||
|
WorldMaintenance.endWorldMaintenance(worldName, reason, bypassMantleStages);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isWorldMaintenanceActive(World world) {
|
public static boolean isWorldMaintenanceActive(World world) {
|
||||||
return world != null && isWorldMaintenanceActive(WorldIdentity.serialize(world));
|
return world != null && isWorldMaintenanceActive(WorldIdentity.serialize(world));
|
||||||
}
|
}
|
||||||
@@ -639,6 +654,14 @@ public class IrisToolbelt {
|
|||||||
e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of);
|
e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> void setMantleData(World world, int x, int y, int z, T data) {
|
||||||
|
PlatformChunkGenerator e = access(world);
|
||||||
|
if (e == null || data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.getEngine().getMantle().getMantle().set(x, y - world.getMinHeight(), z, data);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean removeWorld(World world) throws IOException {
|
public static boolean removeWorld(World world) throws IOException {
|
||||||
return IrisCreator.removeFromBukkitYml(IrisWorldStorage.logicalName(world));
|
return IrisCreator.removeFromBukkitYml(IrisWorldStorage.logicalName(world));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
package art.arcane.iris.core.tools;
|
package art.arcane.iris.core.tools;
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.engine.mantle.MantleSliceRetention;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public final class WorldMaintenance {
|
public final class WorldMaintenance {
|
||||||
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
|
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
|
||||||
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
|
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
|
||||||
private static final Set<String> retainedMantleSlices = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
private WorldMaintenance() {
|
private WorldMaintenance() {
|
||||||
}
|
}
|
||||||
@@ -37,6 +36,10 @@ public final class WorldMaintenance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void endWorldMaintenance(String worldName, String reason) {
|
public static void endWorldMaintenance(String worldName, String reason) {
|
||||||
|
endWorldMaintenance(worldName, reason, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void endWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) {
|
||||||
if (worldName == null) {
|
if (worldName == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,13 +55,17 @@ public final class WorldMaintenance {
|
|||||||
depth = 0;
|
depth = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(worldName);
|
// Only a bypass-begin's paired end releases bypass credit: a plain end overlapping a
|
||||||
|
// bypassing operation stole its credit and re-enabled mantle stages under it.
|
||||||
int bypassDepth = 0;
|
int bypassDepth = 0;
|
||||||
if (bypassCounter != null) {
|
if (bypassMantleStages) {
|
||||||
bypassDepth = bypassCounter.decrementAndGet();
|
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(worldName);
|
||||||
if (bypassDepth <= 0) {
|
if (bypassCounter != null) {
|
||||||
worldMaintenanceMantleBypassDepth.remove(worldName, bypassCounter);
|
bypassDepth = bypassCounter.decrementAndGet();
|
||||||
bypassDepth = 0;
|
if (bypassDepth <= 0) {
|
||||||
|
worldMaintenanceMantleBypassDepth.remove(worldName, bypassCounter);
|
||||||
|
bypassDepth = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +95,10 @@ public final class WorldMaintenance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void retainMantleDataForSlice(String className) {
|
public static void retainMantleDataForSlice(String className) {
|
||||||
if (className == null) {
|
MantleSliceRetention.retain(className);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
retainedMantleSlices.add(className);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isRetainingMantleDataForSlice(String className) {
|
public static boolean isRetainingMantleDataForSlice(String className) {
|
||||||
return className != null && retainedMantleSlices.contains(className);
|
return MantleSliceRetention.isRetained(className);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,14 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
@@ -46,8 +50,10 @@ final class EngineBackgroundTasks {
|
|||||||
|
|
||||||
boolean scheduleTrackedTask(Runnable task) {
|
boolean scheduleTrackedTask(Runnable task) {
|
||||||
synchronized (backgroundTaskLock) {
|
synchronized (backgroundTaskLock) {
|
||||||
backgroundTasks.removeIf(tracked -> tracked.completion.isDone()
|
// A finished task is not outstanding work, however it finished. Retaining failed
|
||||||
&& !tracked.completion.isCompletedExceptionally());
|
// entries made the NEXT transition's drain re-report a long-settled failure and
|
||||||
|
// blocked close() from ever marking the engine closed.
|
||||||
|
backgroundTasks.removeIf(tracked -> tracked.completion.isDone());
|
||||||
if (!backgroundTaskAdmission) {
|
if (!backgroundTaskAdmission) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -60,6 +66,10 @@ final class EngineBackgroundTasks {
|
|||||||
return null;
|
return null;
|
||||||
} catch (Throwable exception) {
|
} catch (Throwable exception) {
|
||||||
tracked.completion.completeExceptionally(exception);
|
tracked.completion.completeExceptionally(exception);
|
||||||
|
// J.a(Callable) routes through a future nobody reads; report here or the
|
||||||
|
// failure is invisible.
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
IrisLogging.error("Iris background task failed.");
|
||||||
throw propagate(exception);
|
throw propagate(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,6 +89,15 @@ final class EngineBackgroundTasks {
|
|||||||
}
|
}
|
||||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS);
|
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS);
|
||||||
Throwable failure = null;
|
Throwable failure = null;
|
||||||
|
// Settled-set snapshot taken ONCE at drain entry: a per-iteration isDone() sample
|
||||||
|
// would also suppress tasks that failed while the drain was blocked on an earlier
|
||||||
|
// task, letting a real in-flight failure pass the transition unreported.
|
||||||
|
Set<TrackedBackgroundTask> settledAtEntry = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
for (TrackedBackgroundTask task : tasks) {
|
||||||
|
if (task.completion.isDone()) {
|
||||||
|
settledAtEntry.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (TrackedBackgroundTask task : tasks) {
|
for (TrackedBackgroundTask task : tasks) {
|
||||||
long remaining = deadline - System.nanoTime();
|
long remaining = deadline - System.nanoTime();
|
||||||
if (remaining <= 0L) {
|
if (remaining <= 0L) {
|
||||||
@@ -86,6 +105,7 @@ final class EngineBackgroundTasks {
|
|||||||
failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + "."));
|
failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + "."));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
boolean alreadyDone = settledAtEntry.contains(task);
|
||||||
try {
|
try {
|
||||||
task.completion.get(remaining, TimeUnit.NANOSECONDS);
|
task.completion.get(remaining, TimeUnit.NANOSECONDS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -94,7 +114,13 @@ final class EngineBackgroundTasks {
|
|||||||
failure = appendFailure(failure, e);
|
failure = appendFailure(failure, e);
|
||||||
} catch (ExecutionException | TimeoutException e) {
|
} catch (ExecutionException | TimeoutException e) {
|
||||||
cancelBackgroundTask(task, reason);
|
cancelBackgroundTask(task, reason);
|
||||||
failure = appendFailure(failure, e);
|
// A task that had settled before this drain began was already reported when it
|
||||||
|
// failed; only genuinely in-flight failures may poison this transition.
|
||||||
|
if (!alreadyDone) {
|
||||||
|
failure = appendFailure(failure, e);
|
||||||
|
} else {
|
||||||
|
IrisLogging.debug("Ignoring pre-settled background task failure during " + reason + ".");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boolean complete;
|
boolean complete;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import art.arcane.iris.spi.IrisServices;
|
|||||||
* having succeeded so a partially released engine never reports itself as closed.
|
* having succeeded so a partially released engine never reports itself as closed.
|
||||||
*/
|
*/
|
||||||
final class EngineShutdownSequence {
|
final class EngineShutdownSequence {
|
||||||
|
private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
|
||||||
|
|
||||||
private final IrisEngine engine;
|
private final IrisEngine engine;
|
||||||
private boolean runtimeReleased;
|
private boolean runtimeReleased;
|
||||||
private boolean targetReleased;
|
private boolean targetReleased;
|
||||||
@@ -55,11 +57,50 @@ final class EngineShutdownSequence {
|
|||||||
engine.getClosing().set(true);
|
engine.getClosing().set(true);
|
||||||
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
||||||
EngineTickRegistry.unregisterTicking(engine);
|
EngineTickRegistry.unregisterTicking(engine);
|
||||||
engine.getPlatformHooks().shutdownPregenerator(engine);
|
// Best-effort like every other step: a pregen join timeout must not escape the
|
||||||
|
// synchronized block before anything is saved or released. On failure, save what
|
||||||
|
// can be saved and leave the close incomplete-but-retryable (a live pregen writer
|
||||||
|
// may still hold the mantle, so the releases are skipped).
|
||||||
|
Throwable pregenFailure = runCleanup(null, () -> engine.getPlatformHooks().shutdownPregenerator(engine));
|
||||||
|
if (pregenFailure != null) {
|
||||||
|
engine.lifecycleState = LifecycleState.FAILED;
|
||||||
|
failure = pregenFailure;
|
||||||
|
failure = runCleanup(failure, engine::savePrefetchOnce);
|
||||||
|
failure = runCleanup(failure, engine::saveEngineData);
|
||||||
|
failure = runCleanup(failure, () -> engine.getMantle().saveAllNow());
|
||||||
|
reportIncompleteClose(failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable drainFailure = null;
|
||||||
try {
|
try {
|
||||||
engine.getGenerationSessions().sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true);
|
engine.getGenerationSessions().sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true);
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
throw new IllegalStateException("Failed to drain Iris generation for close.", e);
|
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.
|
||||||
|
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);
|
||||||
|
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
|
||||||
|
// dirty plates can still be flushed (saveAll is synchronized) so terrain since
|
||||||
|
// the last periodic save is not lost. The close stays incomplete and retryable.
|
||||||
|
engine.lifecycleState = LifecycleState.FAILED;
|
||||||
|
failure = runCleanup(drainFailure, () -> engine.getMantle().saveAllNow());
|
||||||
|
reportIncompleteClose(failure);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("close");
|
BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("close");
|
||||||
@@ -108,13 +149,17 @@ final class EngineShutdownSequence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
|
reportIncompleteClose(failure);
|
||||||
IrisLogging.reportError(failure);
|
|
||||||
failure.printStackTrace();
|
|
||||||
throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reportIncompleteClose(Throwable failure) {
|
||||||
|
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
failure.printStackTrace();
|
||||||
|
throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure);
|
||||||
|
}
|
||||||
|
|
||||||
void cleanupFailedConstruction(Throwable original) {
|
void cleanupFailedConstruction(Throwable original) {
|
||||||
engine.getClosing().set(true);
|
engine.getClosing().set(true);
|
||||||
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ public class IrisComplex implements DataProvider {
|
|||||||
private IrisRegion focusRegion;
|
private IrisRegion focusRegion;
|
||||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||||
private Set<IrisBiome> generatorBiomes;
|
private Set<IrisBiome> generatorBiomes;
|
||||||
private final Map<IrisBiome, ChildSelectionPlan> childSelectionPlans = Collections.synchronizedMap(new IdentityHashMap<>());
|
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
||||||
|
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
|
||||||
|
// per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed).
|
||||||
|
private volatile IdentityHashMap<IrisBiome, ChildSelectionPlan> childSelectionPlans = new IdentityHashMap<>();
|
||||||
|
private final Object childSelectionPlanLock = new Object();
|
||||||
|
|
||||||
public IrisComplex(Engine engine) {
|
public IrisComplex(Engine engine) {
|
||||||
this(engine, false);
|
this(engine, false);
|
||||||
@@ -169,7 +173,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
generatorBounds = buildGeneratorBounds(engine);
|
generatorBounds = buildGeneratorBounds(engine);
|
||||||
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
|
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
|
||||||
overlayStream = overlayNoise.isEmpty()
|
overlayStream = overlayNoise.isEmpty()
|
||||||
? ProceduralStream.ofDouble((x, z) -> 0.0D).waste("Overlay Stream")
|
? ProceduralStream.ofDouble((x, z) -> 0.0D)
|
||||||
: ProceduralStream.ofDouble((x, z) -> {
|
: ProceduralStream.ofDouble((x, z) -> {
|
||||||
double value = 0D;
|
double value = 0D;
|
||||||
|
|
||||||
@@ -178,22 +182,22 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}).waste("Overlay Stream");
|
});
|
||||||
rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
||||||
.select(engine.getDimension().getRockPalette().getBlockData(data)).waste("Rock Stream");
|
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||||
.select(engine.getDimension().getFluidPalette().getBlockData(data)).waste("Fluid Stream");
|
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||||
.zoom(engine.getDimension().getRegionZoom()).waste("Region Style");
|
.zoom(engine.getDimension().getRegionZoom());
|
||||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE).waste("Region Identity Stream");
|
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
regionStream = focusRegion != null ?
|
regionStream = focusRegion != null ?
|
||||||
ProceduralStream.of((x, z) -> focusRegion,
|
ProceduralStream.of((x, z) -> focusRegion,
|
||||||
Interpolated.of(a -> 0D, a -> focusRegion))
|
Interpolated.of(a -> 0D, a -> focusRegion))
|
||||||
: regionStyleStream
|
: regionStyleStream
|
||||||
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()))
|
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()))
|
||||||
.cache2D("regionStream", engine, cacheSize).waste("Region Stream");
|
.cache2D("regionStream", engine, cacheSize);
|
||||||
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
|
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
|
||||||
String.valueOf(i * 38445).hashCode() * 3245556666L)).waste("Region ID Stream");
|
String.valueOf(i * 38445).hashCode() * 3245556666L));
|
||||||
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r)
|
.convert((r)
|
||||||
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
||||||
@@ -201,7 +205,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(r.getCaveBiomeZoom())
|
.zoom(r.getCaveBiomeZoom())
|
||||||
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
|
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
|
||||||
.onNull(emptyBiome)
|
.onNull(emptyBiome)
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
|
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
|
||||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||||
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r)
|
.convert((r)
|
||||||
@@ -211,7 +215,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(r.getLandBiomeZoom())
|
.zoom(r.getLandBiomeZoom())
|
||||||
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
|
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
|
||||||
).convertAware2D(ProceduralStream::get)
|
).convertAware2D(ProceduralStream::get)
|
||||||
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
|
.cache2D("landBiomeStream", engine, cacheSize);
|
||||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||||
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r)
|
.convert((r)
|
||||||
@@ -221,7 +225,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(r.getSeaBiomeZoom())
|
.zoom(r.getSeaBiomeZoom())
|
||||||
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
|
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
|
||||||
).convertAware2D(ProceduralStream::get)
|
).convertAware2D(ProceduralStream::get)
|
||||||
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
|
.cache2D("seaBiomeStream", engine, cacheSize);
|
||||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||||
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r)
|
.convert((r)
|
||||||
@@ -229,60 +233,60 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(r.getShoreBiomeZoom())
|
.zoom(r.getShoreBiomeZoom())
|
||||||
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
|
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
|
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
|
||||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||||
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
||||||
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
|
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
|
||||||
engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData())
|
engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData())
|
||||||
.bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream()
|
.bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream()
|
||||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND)
|
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND)
|
||||||
.cache2D("bridgeStream", engine, cacheSize).waste("Bridge Stream");
|
.cache2D("bridgeStream", engine, cacheSize);
|
||||||
baseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome,
|
baseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome,
|
||||||
Interpolated.of(a -> 0D, a -> focusBiome)) :
|
Interpolated.of(a -> 0D, a -> focusBiome)) :
|
||||||
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||||
.convertAware2D(this::implode)
|
.convertAware2D(this::implode)
|
||||||
.cache2D("baseBiomeStream", engine, cacheSize).waste("Base Biome Stream");
|
.cache2D("baseBiomeStream", engine, cacheSize);
|
||||||
heightStream = ProceduralStream.of((x, z) -> {
|
heightStream = ProceduralStream.of((x, z) -> {
|
||||||
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
||||||
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||||
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize).waste("Height Stream");
|
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
|
||||||
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.round().waste("Rounded Height Stream");
|
.round();
|
||||||
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize).waste("Slope Stream");
|
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
||||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||||
b -> focusBiome))
|
b -> focusBiome))
|
||||||
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
|
||||||
.convertAware2D((h, x, z) ->
|
.convertAware2D((h, x, z) ->
|
||||||
fixBiomeType(h, baseBiomeStream.get(x, z),
|
fixBiomeType(h, baseBiomeStream.get(x, z),
|
||||||
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
|
regionStream.contextInjecting(engine, (c, xx, zz) -> c.getRegion().get(xx, zz)).get(x, z), x, z, fluidHeight))
|
||||||
.cache2D("trueBiomeStream", engine, cacheSize).waste("True Biome Stream");
|
.cache2D("trueBiomeStream", engine, cacheSize);
|
||||||
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize).waste("True Biome Derivative Stream");
|
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
|
||||||
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize).waste("Height Fluid Stream");
|
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
|
||||||
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height).waste("Max Height Stream");
|
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
|
||||||
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize).waste("Surface Decoration Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
|
||||||
terrainCeilingDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
terrainCeilingDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCeilingDecoration", engine, cacheSize).waste("Ceiling Decoration Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCeilingDecoration", engine, cacheSize);
|
||||||
terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
|
terrainCaveSurfaceDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainCaveSurfaceDecoration", engine, cacheSize).waste("Cave Surface Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainCaveSurfaceDecoration", engine, cacheSize);
|
||||||
terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
|
terrainCaveCeilingDecoration = caveBiomeStream.contextInjecting(engine, (c, x, z) -> c.getCave().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCaveCeilingDecoration", engine, cacheSize).waste("Cave Ceiling Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)).cache2D("terrainCaveCeilingDecoration", engine, cacheSize);
|
||||||
shoreSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
shoreSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)).cache2D("shoreSurfaceDecoration", engine, cacheSize).waste("Shore Surface Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)).cache2D("shoreSurfaceDecoration", engine, cacheSize);
|
||||||
seaSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
seaSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)).cache2D("seaSurfaceDecoration", engine, cacheSize).waste("Sea Surface Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)).cache2D("seaSurfaceDecoration", engine, cacheSize);
|
||||||
seaFloorDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
seaFloorDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)).cache2D("seaFloorDecoration", engine, cacheSize).waste("Sea Floor Stream");
|
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)).cache2D("seaFloorDecoration", engine, cacheSize);
|
||||||
baseBiomeIDStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
baseBiomeIDStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||||
.convertAware2D((b, x, z) -> {
|
.convertAware2D((b, x, z) -> {
|
||||||
UUID d = regionIDStream.get(x, z);
|
UUID d = regionIDStream.get(x, z);
|
||||||
return new UUID(b.getLoadKey().hashCode() * 818223L,
|
return new UUID(b.getLoadKey().hashCode() * 818223L,
|
||||||
d.hashCode());
|
d.hashCode());
|
||||||
})
|
})
|
||||||
.cache2D("", engine, cacheSize).waste("Biome ID Stream");
|
.cache2D("", engine, cacheSize);
|
||||||
//@done
|
//@done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +602,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return cachedPlan;
|
return cachedPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (childSelectionPlans) {
|
synchronized (childSelectionPlanLock) {
|
||||||
ChildSelectionPlan synchronizedPlan = childSelectionPlans.get(biome);
|
ChildSelectionPlan synchronizedPlan = childSelectionPlans.get(biome);
|
||||||
if (synchronizedPlan != null) {
|
if (synchronizedPlan != null) {
|
||||||
return synchronizedPlan;
|
return synchronizedPlan;
|
||||||
@@ -614,7 +618,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
options.add(biome);
|
options.add(biome);
|
||||||
|
|
||||||
ChildSelectionPlan createdPlan = ChildSelectionPlan.create(options);
|
ChildSelectionPlan createdPlan = ChildSelectionPlan.create(options);
|
||||||
childSelectionPlans.put(biome, createdPlan);
|
IdentityHashMap<IrisBiome, ChildSelectionPlan> next = new IdentityHashMap<>(childSelectionPlans);
|
||||||
|
next.put(biome, createdPlan);
|
||||||
|
childSelectionPlans = next;
|
||||||
return createdPlan;
|
return createdPlan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,12 +389,21 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
return worker.read(name, (regionName, in) ->
|
return worker.read(name, (regionName, in) ->
|
||||||
TectonicPlate.read(worldHeight, in, regionName.startsWith("pv."), adapter, hooks));
|
TectonicPlate.read(worldHeight, in, regionName.startsWith("pv."), adapter, hooks));
|
||||||
} finally {
|
} finally {
|
||||||
if (TectonicPlate.hasError() && IrisSettings.get().getGeneral().isDumpMantleOnError()) {
|
// hasError() is a consuming ThreadLocal read: evaluate exactly once. The
|
||||||
File dump = IrisPlatforms.get().dataFile("dump", name + ".bin");
|
// dump must never throw out of this finally — that would replace the
|
||||||
worker.dumpDecoded(name, dump.toPath());
|
// successfully parsed plate with the dump failure, and the caller would
|
||||||
} else {
|
// overwrite 1024 good chunks with a fresh empty plate.
|
||||||
IrisLogging.debug("Read Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2));
|
boolean errored = TectonicPlate.hasError();
|
||||||
|
if (errored && IrisSettings.get().getGeneral().isDumpMantleOnError()) {
|
||||||
|
try {
|
||||||
|
File dump = IrisPlatforms.get().dataFile("dump", name + ".bin");
|
||||||
|
worker.dumpDecoded(name, dump.toPath());
|
||||||
|
} catch (Throwable dumpFailure) {
|
||||||
|
IrisLogging.warn("Failed to dump mantle region " + name + " for diagnostics");
|
||||||
|
IrisLogging.reportError(dumpFailure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
IrisLogging.debug("Read Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -519,18 +519,7 @@ final class WorldEntitySpawner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private KList<IrisEntitySpawn> spawnRandomly(List<IrisEntitySpawn> types) {
|
private KList<IrisEntitySpawn> spawnRandomly(List<IrisEntitySpawn> types) {
|
||||||
KList<IrisEntitySpawn> rarityTypes = new KList<>();
|
return IRare.expandWeighted(types);
|
||||||
int totalRarity = 0;
|
|
||||||
|
|
||||||
for (IrisEntitySpawn i : types) {
|
|
||||||
totalRarity += IRare.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (IrisEntitySpawn i : types) {
|
|
||||||
rarityTypes.addMultiple(i, totalRarity / IRare.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rarityTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user