mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Full audit fixpass: 93 defect fixes, 14 perf wins, vestigial cleanup
This commit is contained in:
@@ -14,6 +14,9 @@ dependencies {
|
||||
transitive = false
|
||||
}
|
||||
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('org.mockito:mockito-core:5.23.0')
|
||||
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.gui.BukkitGuiHost;
|
||||
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.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.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.engine.EnginePanic;
|
||||
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.io.IO;
|
||||
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.RNG;
|
||||
import art.arcane.iris.util.common.misc.Bindings;
|
||||
@@ -98,7 +115,6 @@ import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -115,9 +131,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -165,6 +181,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
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 BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(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) {
|
||||
int modifiers = candidate.getModifiers();
|
||||
return requiredType.isAssignableFrom(candidate)
|
||||
@@ -235,27 +232,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
&& !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) {
|
||||
synchronized (syncJobs) {
|
||||
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);
|
||||
servicesDisabled.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
@@ -551,25 +541,50 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
setupAudience();
|
||||
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane());
|
||||
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);
|
||||
services.put(serviceType, i);
|
||||
IrisServices.register(serviceType, i);
|
||||
});
|
||||
}
|
||||
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
|
||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||
IO.delete(new File("iris"));
|
||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||
IrisServices.register(IrisCompat.class, compat);
|
||||
ServerConfigurator.configure();
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
||||
if (datapackValidation == StartupValidationOutcome.READY) {
|
||||
generatorResolver.validateAllPacks();
|
||||
}
|
||||
IrisSafeguard.execute();
|
||||
getSender().setTag(getTag());
|
||||
splash();
|
||||
// A cosmetic banner must never abort the bootstrap.
|
||||
J.attempt(this::splash);
|
||||
IrisSafeguard.printReports();
|
||||
IrisSafeguard.printFooter();
|
||||
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
|
||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
||||
IO.delete(getTemp());
|
||||
services.values().forEach(IrisService::onEnable);
|
||||
services.values().forEach(this::registerListener);
|
||||
// One throwing service must not abort the bootstrap: the steps after this loop
|
||||
// (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();
|
||||
pendingWorldReplacements.processPendingStartupReplacements();
|
||||
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
||||
WorldLifecycleService.get();
|
||||
WorldRuntimeControlService.get();
|
||||
|
||||
if (J.isFolia() && IrisStartupValidation.isReady()) {
|
||||
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
|
||||
}
|
||||
|
||||
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.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
||||
J.sr(this::tickQueue, 0);
|
||||
@@ -620,20 +669,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
worldReconciler.checkForBukkitWorlds(s -> true);
|
||||
}
|
||||
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());
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addShutdownHook() {
|
||||
if (shutdownHook != null) {
|
||||
try {
|
||||
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
||||
} catch (IllegalStateException ex) {
|
||||
Iris.debug("Skipping shutdown hook replacement because JVM shutdown is already in progress.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
removeShutdownHook();
|
||||
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
|
||||
try {
|
||||
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() {
|
||||
return worldReconciler;
|
||||
}
|
||||
@@ -692,16 +754,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisStartupValidation.begin();
|
||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
|
||||
enable();
|
||||
boolean enabled = enable();
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
BukkitGuiHost.install();
|
||||
// super.onEnable() already registers this instance as a listener.
|
||||
super.onEnable();
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
teardownPapi();
|
||||
if (IrisSafeguard.isForceShutdown()) return;
|
||||
teardownRuntime("onDisable", 30L);
|
||||
removeShutdownHook();
|
||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||
if (BukkitPlatform.hasHud()) {
|
||||
BukkitPlatform.hudSlots().shutdown();
|
||||
BukkitPlatform.hudLanes().shutdown();
|
||||
@@ -710,12 +776,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
configHotloadEngine.clear();
|
||||
configHotloadEngine = null;
|
||||
}
|
||||
J.cancelPluginTasks();
|
||||
HandlerList.unregisterAll((Plugin) this);
|
||||
runPostShutdown();
|
||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||
super.onDisable();
|
||||
|
||||
J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@@ -753,7 +816,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
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 {
|
||||
service.onDisable();
|
||||
} catch (Throwable e) {
|
||||
@@ -768,6 +833,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
J.attempt(MultiBurst.burst::close);
|
||||
J.attempt(MultiBurst.ioBurst::close);
|
||||
clearQueues();
|
||||
IrisServices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||
if (dimension == null) {
|
||||
File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME);
|
||||
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
||||
if (PackDownloader.isPackPresent(packsRoot, id)) {
|
||||
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.");
|
||||
|
||||
+43
-9
@@ -254,7 +254,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
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) {
|
||||
@@ -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 {
|
||||
if (slotKind == SlotKind.IRIS_MANAGED) {
|
||||
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(
|
||||
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
||||
.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.");
|
||||
}
|
||||
try {
|
||||
VanillaLevelContext context = 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();
|
||||
return contextFuture.get(30L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
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) {
|
||||
}
|
||||
|
||||
|
||||
+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(
|
||||
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true)
|
||||
World world,
|
||||
@@ -404,7 +404,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
int centerX,
|
||||
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
|
||||
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,
|
||||
@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,
|
||||
|
||||
@@ -705,7 +705,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", aliases = "project")
|
||||
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,
|
||||
@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
|
||||
@@ -803,7 +803,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
});
|
||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||
.whenComplete((unloaded, throwable) -> {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload");
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
lease.close();
|
||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||
@@ -812,7 +812,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
J.s(response);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload");
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
lease.close();
|
||||
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);
|
||||
|
||||
+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.IrisObjectPlacementScaleInterpolator;
|
||||
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.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -144,7 +143,8 @@ public class CommandObject implements DirectorExecutor {
|
||||
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.setSources(hostDimension.getLoadKey(), sources);
|
||||
|
||||
@@ -637,6 +637,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
Iris.reportError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
|
||||
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"})
|
||||
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
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
@@ -54,7 +63,7 @@ public class CommandPack implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pack == null || pack.isBlank()) {
|
||||
if (wantsAllPacks(pack)) {
|
||||
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
if (dirs.isEmpty()) {
|
||||
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"})
|
||||
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
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (pack == null || pack.isBlank()) {
|
||||
if (wantsAllPacks(pack)) {
|
||||
if (PackValidationRegistry.snapshot().isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
||||
return;
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.bukkit.util.Vector;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
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")
|
||||
@@ -59,6 +60,12 @@ public class CommandPregen implements DirectorExecutor {
|
||||
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 {
|
||||
if (sender().isPlayer() && access() == 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));
|
||||
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 player = player();
|
||||
Thread.ofVirtual()
|
||||
.start(() -> {
|
||||
int d = radius * 2;
|
||||
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)));
|
||||
var multiBurst = new MultiBurst("Region Sampler");
|
||||
var executor = multiBurst.burst(radius * radius);
|
||||
multiBurst = new MultiBurst("Region Sampler");
|
||||
var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
|
||||
var loc = player.getLocation();
|
||||
int totalTasks = d * d;
|
||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||
HudSlotClaim 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)));
|
||||
titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
|
||||
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);
|
||||
int c = J.ar(() -> {
|
||||
c = J.ar(() -> {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastResolveMs.get() >= 250L) {
|
||||
lastResolveMs.set(now);
|
||||
titleClaim.resolve();
|
||||
barClaim.resolve();
|
||||
finalTitleClaim.resolve();
|
||||
finalBarClaim.resolve();
|
||||
}
|
||||
double jobProgress = (double) completedTasks.get() / totalTasks;
|
||||
HudSurface barSurface = barClaim.granted();
|
||||
HudSurface barSurface = finalBarClaim.granted();
|
||||
sender.sendProgress(
|
||||
jobProgress,
|
||||
IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS),
|
||||
titleClaim.granted(),
|
||||
finalTitleClaim.granted(),
|
||||
barSurface
|
||||
);
|
||||
if (barSurface == HudSurface.BOSS_BAR) {
|
||||
@@ -372,15 +387,28 @@ public class CommandStudio implements DirectorExecutor {
|
||||
completedTasks.incrementAndGet();
|
||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||
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));
|
||||
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)))));
|
||||
} 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<String, Double> generatorTimings = 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));
|
||||
|
||||
@@ -561,23 +588,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
|
||||
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;
|
||||
for (double i : biomeTimings.v()) {
|
||||
m += i;
|
||||
@@ -589,12 +599,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
mm /= generatorTimings.size();
|
||||
m += mm;
|
||||
double mmm = 0;
|
||||
for (double i : regionTimings.v()) {
|
||||
mmm += i;
|
||||
}
|
||||
mmm /= regionTimings.size();
|
||||
m += mmm;
|
||||
|
||||
fileText.add("Average Score: " + m);
|
||||
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())));
|
||||
try {
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
PrintWriter pw = new PrintWriter(ff);
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
try (PrintWriter pw = new PrintWriter(ff)) {
|
||||
pw.println("=== Iris Chunk Report ===");
|
||||
pw.println("== General Info ==");
|
||||
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("== World Info ==");
|
||||
@@ -854,10 +858,9 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
|
||||
pw.println();
|
||||
pw.close();
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
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.tools.IrisToolbelt;
|
||||
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.util.project.context.IrisContext;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
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.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -47,7 +52,6 @@ import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
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)
|
||||
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();
|
||||
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, () -> {
|
||||
World world = player.getWorld();
|
||||
Chunk c = player.getLocation().getChunk();
|
||||
int chunkX = c.getX();
|
||||
int chunkZ = c.getZ();
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
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++) {
|
||||
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
|
||||
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
|
||||
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
|
||||
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
|
||||
v.incrementAndGet();
|
||||
});
|
||||
J.a(() -> {
|
||||
KList<Location> hits = new KList<>();
|
||||
MatterMarker matterMarker = new MatterMarker(marker);
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
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)));
|
||||
} else {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||
}
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker)));
|
||||
emitMarkerSignals(player, engine, hits, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
+19
-5
@@ -1,5 +1,6 @@
|
||||
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.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
|
||||
@@ -29,13 +30,26 @@ public final class DatapackStructureScopeSVC implements IrisService {
|
||||
scopeIndex = DatapackStructureScopeIndex.create(
|
||||
DatapackIngestService.installedStructureScopeResources());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
"Iris could not establish ownership for installed datapack structure sets", e);
|
||||
// A missing manifest entry is the condition validateOnStartup already classified as
|
||||
// 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()) {
|
||||
IrisImportedStructureControl importedStructures = importedStructures(world);
|
||||
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
|
||||
applyScope(world, importedStructures);
|
||||
try {
|
||||
IrisImportedStructureControl importedStructures = importedStructures(world);
|
||||
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> {
|
||||
private KMap<World, BlockEditor> editors;
|
||||
private int updateTaskId = -1;
|
||||
public static boolean deletingWorld = false;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -84,7 +83,7 @@ public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, B
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
|
||||
if (editors.containsKey(e.getWorld())) {
|
||||
editors.remove(e.getWorld()).close();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class IrisStartupOrderingTest {
|
||||
@Test
|
||||
public void externalDatapacksValidateBeforeDimensionPacks() throws Exception {
|
||||
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,
|
||||
"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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user