mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
🧹
This commit is contained in:
@@ -27,28 +27,23 @@ import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.BukkitWorldReconciler;
|
||||
import art.arcane.iris.core.IrisWorldGeneratorResolver;
|
||||
import art.arcane.iris.core.PendingWorldDeleteQueue;
|
||||
import art.arcane.iris.core.SettingsHotloadWatch;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.core.link.IrisPapiInstaller;
|
||||
import art.arcane.iris.core.link.IrisPapiListener;
|
||||
import art.arcane.iris.core.link.IrisPapiState;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.link.MultiverseCoreLink;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.service.EditSVC;
|
||||
@@ -61,19 +56,14 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.object.IrisCompat;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.core.safeguard.IrisSafeguard;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.volmlib.integration.ReloadAware;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -96,17 +86,13 @@ import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.plugin.chunk.ChunkTickets;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.simd.SimdSupport;
|
||||
import art.arcane.volmlib.util.scheduling.Queue;
|
||||
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
@@ -121,23 +107,14 @@ import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -145,7 +122,6 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -165,8 +141,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public static ChunkTickets tickets;
|
||||
private static VolmitSender sender;
|
||||
private static Thread shutdownHook;
|
||||
private static File settingsFile;
|
||||
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
|
||||
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
|
||||
static {
|
||||
try {
|
||||
@@ -178,11 +152,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object TEARDOWN_LOCK = new Object();
|
||||
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
|
||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
|
||||
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
|
||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
if (sender == null) {
|
||||
@@ -292,69 +273,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
public static File getCached(String name, String url) {
|
||||
String h = IO.hash(name + "@" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
|
||||
if (!f.exists()) {
|
||||
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
Iris.verbose("Aquiring " + name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return f.exists() ? f : null;
|
||||
}
|
||||
|
||||
public static String getNonCached(String name, String url) {
|
||||
String h = IO.hash(name + "*" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
|
||||
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return IO.readAll(f);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url) {
|
||||
String h = IO.hash(name + "*" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
Iris.verbose("Download " + name + " -> " + url);
|
||||
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
fileOutputStream.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
public static void warn(String format, Object... objs) {
|
||||
msg(C.YELLOW + safeFormat(format, objs));
|
||||
}
|
||||
@@ -616,6 +534,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
private void enable() {
|
||||
alreadyDrained.set(false);
|
||||
servicesDisabled.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
IrisLanguage.initialize();
|
||||
PaperLibBootstrap.install();
|
||||
SimdSupport.install();
|
||||
@@ -635,7 +557,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisServices.register(IrisCompat.class, compat);
|
||||
ServerConfigurator.configure();
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
validateAllPacks();
|
||||
generatorResolver.validateAllPacks();
|
||||
IrisSafeguard.execute();
|
||||
getSender().setTag(getTag());
|
||||
splash();
|
||||
@@ -649,37 +571,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
|
||||
IrisServices.register(EngineWorldManagerProvider.class,
|
||||
(EngineWorldManagerProvider) IrisWorldManager::new);
|
||||
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
|
||||
settingsFile = getDataFile("settings.json");
|
||||
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) pendingWorldDeletes::queueWorldDeletionOnStartup);
|
||||
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
|
||||
settingsHotloadWatch = watch;
|
||||
configHotloadEngine = new ConfigHotloadEngine(
|
||||
Iris::isSettingsFile,
|
||||
Iris::knownSettingsFiles,
|
||||
Iris::readSettingsContent,
|
||||
Iris::normalizeSettingsContent
|
||||
watch::isSettingsFile,
|
||||
watch::knownSettingsFiles,
|
||||
watch::readSettingsContent,
|
||||
watch::normalizeSettingsContent
|
||||
);
|
||||
configHotloadEngine.configure(3_000L, List.of(settingsFile), List.of());
|
||||
configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of());
|
||||
services.values().forEach(IrisService::onEnable);
|
||||
services.values().forEach(this::registerListener);
|
||||
addShutdownHook();
|
||||
processPendingStartupWorldDeletes();
|
||||
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
||||
WorldLifecycleService.get();
|
||||
WorldRuntimeControlService.get();
|
||||
|
||||
if (J.isFolia()) {
|
||||
J.s(() -> checkForBukkitWorlds(s -> true), 1);
|
||||
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
|
||||
}
|
||||
|
||||
J.s(() -> {
|
||||
J.a(() -> IO.delete(getTemp()));
|
||||
J.a(this::bstats);
|
||||
J.ar(this::checkConfigHotload, 60);
|
||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
||||
J.sr(this::tickQueue, 0);
|
||||
J.s(this::setupPapi);
|
||||
J.a(DatapackIngestService::autoIngestOnStartup, 60);
|
||||
|
||||
autoStartStudio();
|
||||
if (!J.isFolia()) {
|
||||
checkForBukkitWorlds(s -> true);
|
||||
worldReconciler.checkForBukkitWorlds(s -> true);
|
||||
}
|
||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
||||
@@ -696,24 +619,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
}
|
||||
shutdownHook = new Thread(() -> {
|
||||
if (alreadyDrained.compareAndSet(false, true)) {
|
||||
try {
|
||||
Bukkit.getWorlds()
|
||||
.stream()
|
||||
.map(World::getGenerator)
|
||||
.filter(PlatformChunkGenerator.class::isInstance)
|
||||
.map(PlatformChunkGenerator.class::cast)
|
||||
.forEach(PlatformChunkGenerator::close);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to close Iris world generators from the JVM shutdown hook.", e);
|
||||
}
|
||||
}
|
||||
|
||||
MultiBurst.burst.close();
|
||||
MultiBurst.ioBurst.close();
|
||||
IrisServices.clear();
|
||||
}, "Iris-ShutdownHook");
|
||||
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
|
||||
try {
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
} catch (IllegalStateException ex) {
|
||||
@@ -721,247 +627,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
public void checkForBukkitWorlds(Predicate<String> filter) {
|
||||
try {
|
||||
KList<String> deferredStartupWorlds = new KList<>();
|
||||
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
|
||||
try {
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
|
||||
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
|
||||
|
||||
Iris.info("Loading World: %s | Generator: %s", s, generator);
|
||||
ChunkGenerator gen = getDefaultWorldGenerator(s, generator);
|
||||
IrisDimension dim = loadDimension(s, generator);
|
||||
assert dim != null && gen != null;
|
||||
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
||||
WorldCreator c = WorldCreator.ofKey(worldKey)
|
||||
.generator(gen)
|
||||
.environment(BukkitEnvironment.from(dim.getEnvironment()));
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||
if (stagedSeed != null) {
|
||||
c.seed(stagedSeed);
|
||||
}
|
||||
INMS.get().createWorld(c);
|
||||
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
||||
} catch (Throwable e) {
|
||||
if (containsCreateWorldUnsupportedOperation(e)) {
|
||||
if (J.isFolia()) {
|
||||
if (!deferredStartupWorlds.contains(s)) {
|
||||
deferredStartupWorlds.add(s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Iris.error("Failed to load world " + s + "!");
|
||||
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
|
||||
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
|
||||
reportError("Failed to load staged startup world \"" + s + "\".", e);
|
||||
return;
|
||||
}
|
||||
reportError("Failed to load startup world \"" + s + "\".", e);
|
||||
}
|
||||
});
|
||||
if (!deferredStartupWorlds.isEmpty()) {
|
||||
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
|
||||
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
reportError("Failed while loading startup Iris worlds.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
|
||||
Throwable cursor = throwable;
|
||||
while (cursor != null) {
|
||||
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
|
||||
for (StackTraceElement element : cursor.getStackTrace()) {
|
||||
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
|
||||
&& "createWorld".equals(element.getMethodName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor = cursor.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
|
||||
if (instance == null || worldNames == null || worldNames.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
||||
int before = queue.size();
|
||||
|
||||
for (String worldName : worldNames) {
|
||||
String normalized = normalizeWorldName(worldName);
|
||||
if (normalized == null) {
|
||||
continue;
|
||||
}
|
||||
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
|
||||
}
|
||||
|
||||
if (queue.size() != before) {
|
||||
writePendingWorldDeleteMap(queue);
|
||||
}
|
||||
|
||||
return queue.size() - before;
|
||||
}
|
||||
|
||||
private void processPendingStartupWorldDeletes() {
|
||||
try {
|
||||
try {
|
||||
int unregistered = art.arcane.iris.core.tools.IrisCreator.removeTransientStudioWorldsFromBukkitYml();
|
||||
if (unregistered > 0) {
|
||||
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
||||
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
|
||||
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
|
||||
for (String worldName : queue.values()) {
|
||||
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
|
||||
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
|
||||
continue;
|
||||
}
|
||||
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
|
||||
if (loaded != null) {
|
||||
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
|
||||
try {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
|
||||
if (generator != null) {
|
||||
generator.close();
|
||||
}
|
||||
IrisToolbelt.evacuate(loaded);
|
||||
Bukkit.unloadWorld(loaded, false);
|
||||
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
|
||||
}
|
||||
|
||||
if (WorldIdentity.resolve(worldKey).isPresent()) {
|
||||
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
boolean foundAny = false;
|
||||
boolean deletedAll = true;
|
||||
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
|
||||
if (!worldFolder.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foundAny = true;
|
||||
IO.delete(worldFolder);
|
||||
if (worldFolder.exists()) {
|
||||
deletedAll = false;
|
||||
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
|
||||
} else {
|
||||
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAny) {
|
||||
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deletedAll) {
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
writePendingWorldDeleteMap(remaining);
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to process queued startup world deletions.");
|
||||
reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
|
||||
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
|
||||
if (instance == null) {
|
||||
return queue;
|
||||
}
|
||||
|
||||
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
|
||||
if (!queueFile.exists()) {
|
||||
return queue;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String normalized = normalizeWorldName(line);
|
||||
if (normalized == null) {
|
||||
continue;
|
||||
}
|
||||
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
private static void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
|
||||
if (queue.isEmpty()) {
|
||||
if (queueFile.exists()) {
|
||||
IO.delete(queueFile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File parent = queueFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
|
||||
for (String worldName : queue.values()) {
|
||||
writer.println(worldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String normalizeWorldName(String worldName) {
|
||||
if (worldName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed = worldName.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
public BukkitWorldReconciler worldReconciler() {
|
||||
return worldReconciler;
|
||||
}
|
||||
|
||||
private void autoStartStudio() {
|
||||
@@ -1012,13 +679,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public void onDisable() {
|
||||
teardownPapi();
|
||||
if (IrisSafeguard.isForceShutdown()) return;
|
||||
if (alreadyDrained.compareAndSet(false, true)) {
|
||||
drainWorldGenerators("onDisable", 30L);
|
||||
}
|
||||
if (services != null) {
|
||||
services.values().forEach(IrisService::onDisable);
|
||||
}
|
||||
IrisServices.clear();
|
||||
teardownRuntime("onDisable", 30L);
|
||||
if (BukkitPlatform.hasHud()) {
|
||||
BukkitPlatform.hudSlots().shutdown();
|
||||
BukkitPlatform.hudLanes().shutdown();
|
||||
@@ -1039,12 +700,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
@Override
|
||||
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
|
||||
teardownPapi();
|
||||
if (!alreadyDrained.compareAndSet(false, true)) {
|
||||
if (alreadyDrained.get()) {
|
||||
Iris.info("Pre-unload hook skipped; Iris already drained.");
|
||||
return;
|
||||
}
|
||||
Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
|
||||
drainWorldGenerators("pre-unload:" + reason, 45L);
|
||||
drainOnce("pre-unload:" + reason, 45L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
||||
* second caller cannot rip the pools or services out from under an in-flight drain.
|
||||
*/
|
||||
private void drainOnce(String reason, long timeoutSeconds) {
|
||||
synchronized (TEARDOWN_LOCK) {
|
||||
if (alreadyDrained.compareAndSet(false, true)) {
|
||||
drainWorldGenerators(reason, timeoutSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full teardown: generators, then services, then the shared pools and the service map.
|
||||
* Both onDisable and the JVM shutdown hook route through here; whichever runs second is a no-op.
|
||||
*/
|
||||
private void teardownRuntime(String reason, long timeoutSeconds) {
|
||||
synchronized (TEARDOWN_LOCK) {
|
||||
if (alreadyDrained.compareAndSet(false, true)) {
|
||||
drainWorldGenerators(reason, timeoutSeconds);
|
||||
}
|
||||
|
||||
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
||||
for (IrisService service : services.values()) {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sharedRuntimeClosed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
J.attempt(MultiBurst.burst::close);
|
||||
J.attempt(MultiBurst.ioBurst::close);
|
||||
IrisServices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
@@ -1164,61 +867,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return IrisSafeguard.mode().tag(subTag);
|
||||
}
|
||||
|
||||
private void checkConfigHotload() {
|
||||
if (configHotloadEngine == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : configHotloadEngine.pollTouchedFiles()) {
|
||||
configHotloadEngine.processFileChange(file, ignored -> {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
IrisLanguage.reload();
|
||||
return true;
|
||||
}, ignored -> Iris.info("Hotloaded settings.json "));
|
||||
}
|
||||
IrisLanguage.update();
|
||||
}
|
||||
|
||||
private static boolean isSettingsFile(File file) {
|
||||
if (file == null || settingsFile == null) {
|
||||
return false;
|
||||
}
|
||||
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
|
||||
}
|
||||
|
||||
private static List<File> knownSettingsFiles() {
|
||||
if (settingsFile == null) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(settingsFile);
|
||||
}
|
||||
|
||||
private static String readSettingsContent(File file) {
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return IO.readAll(file);
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read settings file %s: %s%s",
|
||||
file.getAbsolutePath(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeSettingsContent(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return text.replace("\r\n", "\n").trim();
|
||||
}
|
||||
|
||||
private void tickQueue() {
|
||||
synchronized (Iris.syncJobs) {
|
||||
if (!Iris.syncJobs.hasNext()) {
|
||||
@@ -1256,117 +904,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
@Nullable
|
||||
@Override
|
||||
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
|
||||
org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
|
||||
if (stagedBiomeProvider != null) {
|
||||
Iris.debug("Using staged runtime biome provider for " + worldName);
|
||||
return stagedBiomeProvider;
|
||||
}
|
||||
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
|
||||
return super.getDefaultBiomeProvider(worldName, id);
|
||||
return generatorResolver.resolveDefaultBiomeProvider(worldName, id, () -> super.getDefaultBiomeProvider(worldName, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
||||
if (stagedGenerator != null) {
|
||||
Iris.debug("Using staged runtime generator for " + worldName);
|
||||
return stagedGenerator;
|
||||
}
|
||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||
|
||||
PackValidationResult validation = PackValidationRegistry.get(id);
|
||||
if (validation != null && !validation.isLoadable()) {
|
||||
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
|
||||
for (String reason : validation.getBlockingErrors()) {
|
||||
Iris.error(" - " + reason);
|
||||
}
|
||||
throw new BrokenPackException(id, validation.getBlockingErrors());
|
||||
}
|
||||
|
||||
IrisDimension dim = loadDimension(worldName, id);
|
||||
if (dim == null) {
|
||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
||||
}
|
||||
|
||||
Iris.debug("Assuming IrisDimension: " + dim.getName());
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
.build();
|
||||
|
||||
Iris.debug("Generator Config: " + w.toString());
|
||||
|
||||
File ff = new File(w.worldFolder(), "iris/pack");
|
||||
File[] files = ff.listFiles();
|
||||
if (files == null || files.length == 0)
|
||||
IO.delete(ff);
|
||||
|
||||
if (!ff.exists()) {
|
||||
ff.mkdirs();
|
||||
dim = service(StudioSVC.class).installIntoWorld(getSender(), dim, w.worldFolder());
|
||||
if (dim == null) {
|
||||
throw new IllegalStateException("Failed to install dimension pack for " + id);
|
||||
}
|
||||
}
|
||||
|
||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||
}
|
||||
|
||||
public static void validateAllPacks() {
|
||||
File packsRoot = Iris.instance.getDataFolder("packs");
|
||||
File[] packDirs = packsRoot.listFiles(File::isDirectory);
|
||||
if (packDirs == null || packDirs.length == 0) {
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.clear();
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
if (!result.isLoadable()) {
|
||||
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
Iris.error(" - " + reason);
|
||||
}
|
||||
} else if (!result.getWarnings().isEmpty()) {
|
||||
Iris.info("Pack '" + result.getPackName() + "' validated ("
|
||||
+ result.getWarnings().size() + " warning(s)).");
|
||||
for (String warning : result.getWarnings()) {
|
||||
Iris.warn(" [" + result.getPackName() + "] " + warning);
|
||||
}
|
||||
} else {
|
||||
Iris.success("Pack '" + result.getPackName() + "' validated.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
||||
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
|
||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||
if (dimension == null) {
|
||||
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
|
||||
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
|
||||
dimension = IrisData.loadAnyDimension(id, null);
|
||||
|
||||
if (dimension != null) {
|
||||
Iris.info("Resolved missing dimension, proceeding.");
|
||||
}
|
||||
}
|
||||
|
||||
return dimension;
|
||||
return generatorResolver.resolveDefaultWorldGenerator(worldName, id);
|
||||
}
|
||||
|
||||
public void splash() {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Loads Iris worlds that are staged in bukkit.yml but not yet present on the server.
|
||||
*/
|
||||
public final class BukkitWorldReconciler {
|
||||
private final Iris plugin;
|
||||
|
||||
public BukkitWorldReconciler(Iris plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void checkForBukkitWorlds(Predicate<String> filter) {
|
||||
try {
|
||||
KList<String> deferredStartupWorlds = new KList<>();
|
||||
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
|
||||
try {
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
|
||||
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
|
||||
|
||||
Iris.info("Loading World: %s | Generator: %s", s, generator);
|
||||
ChunkGenerator gen = plugin.getDefaultWorldGenerator(s, generator);
|
||||
IrisDimension dim = IrisWorldGeneratorResolver.loadDimension(s, generator);
|
||||
assert dim != null && gen != null;
|
||||
|
||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
||||
WorldCreator c = WorldCreator.ofKey(worldKey)
|
||||
.generator(gen)
|
||||
.environment(BukkitEnvironment.from(dim.getEnvironment()));
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||
if (stagedSeed != null) {
|
||||
c.seed(stagedSeed);
|
||||
}
|
||||
INMS.get().createWorld(c);
|
||||
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
||||
} catch (Throwable e) {
|
||||
if (containsCreateWorldUnsupportedOperation(e)) {
|
||||
if (J.isFolia()) {
|
||||
if (!deferredStartupWorlds.contains(s)) {
|
||||
deferredStartupWorlds.add(s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Iris.error("Failed to load world " + s + "!");
|
||||
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
|
||||
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
|
||||
Iris.reportError("Failed to load staged startup world \"" + s + "\".", e);
|
||||
return;
|
||||
}
|
||||
Iris.reportError("Failed to load startup world \"" + s + "\".", e);
|
||||
}
|
||||
});
|
||||
if (!deferredStartupWorlds.isEmpty()) {
|
||||
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
|
||||
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed while loading startup Iris worlds.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
|
||||
Throwable cursor = throwable;
|
||||
while (cursor != null) {
|
||||
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
|
||||
for (StackTraceElement element : cursor.getStackTrace()) {
|
||||
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
|
||||
&& "createWorld".equals(element.getMethodName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor = cursor.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
|
||||
* Bukkit plugin entry points delegate to.
|
||||
*/
|
||||
public final class IrisWorldGeneratorResolver {
|
||||
private final VolmitPlugin plugin;
|
||||
|
||||
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void validateAllPacks() {
|
||||
File packsRoot = plugin.getDataFolder("packs");
|
||||
File[] packDirs = packsRoot.listFiles(File::isDirectory);
|
||||
if (packDirs == null || packDirs.length == 0) {
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.clear();
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
if (!result.isLoadable()) {
|
||||
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
Iris.error(" - " + reason);
|
||||
}
|
||||
} else if (!result.getWarnings().isEmpty()) {
|
||||
Iris.info("Pack '" + result.getPackName() + "' validated ("
|
||||
+ result.getWarnings().size() + " warning(s)).");
|
||||
for (String warning : result.getWarnings()) {
|
||||
Iris.warn(" [" + result.getPackName() + "] " + warning);
|
||||
}
|
||||
} else {
|
||||
Iris.success("Pack '" + result.getPackName() + "' validated.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
||||
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
|
||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||
if (dimension == null) {
|
||||
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
|
||||
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
|
||||
dimension = IrisData.loadAnyDimension(id, null);
|
||||
|
||||
if (dimension != null) {
|
||||
Iris.info("Resolved missing dimension, proceeding.");
|
||||
}
|
||||
}
|
||||
|
||||
return dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
|
||||
* Iris has nothing staged.
|
||||
*/
|
||||
@Nullable
|
||||
public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier<BiomeProvider> fallback) {
|
||||
BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
|
||||
if (stagedBiomeProvider != null) {
|
||||
Iris.debug("Using staged runtime biome provider for " + worldName);
|
||||
return stagedBiomeProvider;
|
||||
}
|
||||
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, String id) {
|
||||
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
||||
if (stagedGenerator != null) {
|
||||
Iris.debug("Using staged runtime generator for " + worldName);
|
||||
return stagedGenerator;
|
||||
}
|
||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||
|
||||
PackValidationResult validation = PackValidationRegistry.get(id);
|
||||
if (validation != null && !validation.isLoadable()) {
|
||||
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
|
||||
for (String reason : validation.getBlockingErrors()) {
|
||||
Iris.error(" - " + reason);
|
||||
}
|
||||
throw new BrokenPackException(id, validation.getBlockingErrors());
|
||||
}
|
||||
|
||||
IrisDimension dim = loadDimension(worldName, id);
|
||||
if (dim == null) {
|
||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
||||
}
|
||||
|
||||
Iris.debug("Assuming IrisDimension: " + dim.getName());
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
.build();
|
||||
|
||||
Iris.debug("Generator Config: " + w.toString());
|
||||
|
||||
File ff = new File(w.worldFolder(), "iris/pack");
|
||||
File[] files = ff.listFiles();
|
||||
if (files == null || files.length == 0)
|
||||
IO.delete(ff);
|
||||
|
||||
if (!ff.exists()) {
|
||||
ff.mkdirs();
|
||||
dim = Iris.service(StudioSVC.class).installIntoWorld(Iris.getSender(), dim, w.worldFolder());
|
||||
if (dim == null) {
|
||||
throw new IllegalStateException("Failed to install dimension pack for " + id);
|
||||
}
|
||||
}
|
||||
|
||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.tools.IrisCreator;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Persistent queue of world folders that must be deleted on the next startup, plus the startup
|
||||
* drain that actually removes them.
|
||||
*/
|
||||
public final class PendingWorldDeleteQueue {
|
||||
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
|
||||
|
||||
private final VolmitPlugin plugin;
|
||||
|
||||
public PendingWorldDeleteQueue(VolmitPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
|
||||
if (worldNames == null || worldNames.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
||||
int before = queue.size();
|
||||
|
||||
for (String worldName : worldNames) {
|
||||
String normalized = normalizeWorldName(worldName);
|
||||
if (normalized == null) {
|
||||
continue;
|
||||
}
|
||||
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
|
||||
}
|
||||
|
||||
if (queue.size() != before) {
|
||||
writePendingWorldDeleteMap(queue);
|
||||
}
|
||||
|
||||
return queue.size() - before;
|
||||
}
|
||||
|
||||
public void processPendingStartupWorldDeletes() {
|
||||
try {
|
||||
try {
|
||||
int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml();
|
||||
if (unregistered > 0) {
|
||||
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
||||
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
|
||||
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
|
||||
for (String worldName : queue.values()) {
|
||||
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
|
||||
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
|
||||
continue;
|
||||
}
|
||||
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
|
||||
if (loaded != null) {
|
||||
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
|
||||
try {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
|
||||
if (generator != null) {
|
||||
generator.close();
|
||||
}
|
||||
IrisToolbelt.evacuate(loaded);
|
||||
Bukkit.unloadWorld(loaded, false);
|
||||
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
|
||||
}
|
||||
|
||||
if (WorldIdentity.resolve(worldKey).isPresent()) {
|
||||
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
boolean foundAny = false;
|
||||
boolean deletedAll = true;
|
||||
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
|
||||
if (!worldFolder.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foundAny = true;
|
||||
IO.delete(worldFolder);
|
||||
if (worldFolder.exists()) {
|
||||
deletedAll = false;
|
||||
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
|
||||
} else {
|
||||
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAny) {
|
||||
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deletedAll) {
|
||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
writePendingWorldDeleteMap(remaining);
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to process queued startup world deletions.");
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
|
||||
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
|
||||
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
|
||||
if (!queueFile.exists()) {
|
||||
return queue;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
String normalized = normalizeWorldName(line);
|
||||
if (normalized == null) {
|
||||
continue;
|
||||
}
|
||||
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
private void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
|
||||
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
|
||||
if (queue.isEmpty()) {
|
||||
if (queueFile.exists()) {
|
||||
IO.delete(queueFile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File parent = queueFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
|
||||
for (String worldName : queue.values()) {
|
||||
writer.println(worldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String normalizeWorldName(String worldName) {
|
||||
if (worldName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed = worldName.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Identity and hotload handling for settings.json. Supplies the predicates the
|
||||
* {@link ConfigHotloadEngine} is built from and drains the touched-file queue.
|
||||
*/
|
||||
public final class SettingsHotloadWatch {
|
||||
private final File settingsFile;
|
||||
|
||||
public SettingsHotloadWatch(File settingsFile) {
|
||||
this.settingsFile = settingsFile;
|
||||
}
|
||||
|
||||
public File settingsFile() {
|
||||
return settingsFile;
|
||||
}
|
||||
|
||||
public void checkConfigHotload(ConfigHotloadEngine engine) {
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : engine.pollTouchedFiles()) {
|
||||
engine.processFileChange(file, ignored -> {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
IrisLanguage.reload();
|
||||
return true;
|
||||
}, ignored -> Iris.info("Hotloaded settings.json "));
|
||||
}
|
||||
IrisLanguage.update();
|
||||
}
|
||||
|
||||
public boolean isSettingsFile(File file) {
|
||||
if (file == null || settingsFile == null) {
|
||||
return false;
|
||||
}
|
||||
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
|
||||
}
|
||||
|
||||
public List<File> knownSettingsFiles() {
|
||||
if (settingsFile == null) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(settingsFile);
|
||||
}
|
||||
|
||||
public String readSettingsContent(File file) {
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return IO.readAll(file);
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read settings file %s: %s%s",
|
||||
file.getAbsolutePath(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String normalizeSettingsContent(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return text.replace("\r\n", "\n").trim();
|
||||
}
|
||||
}
|
||||
+38
-20
@@ -51,6 +51,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
@@ -283,7 +284,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER)
|
||||
public void deleteChunk(
|
||||
@Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0")
|
||||
int radius
|
||||
@@ -293,25 +294,34 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
Player player = player();
|
||||
VolmitSender commandSender = sender();
|
||||
World world = player.getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
Engine engine = access.getEngine();
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
// The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself.
|
||||
if (!J.runEntity(player, () -> {
|
||||
int centerX = player.getLocation().getBlockX() >> 4;
|
||||
int centerZ = player.getLocation().getBlockZ() >> 4;
|
||||
|
||||
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
|
||||
new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start();
|
||||
})) {
|
||||
Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"})
|
||||
@@ -332,7 +342,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
|
||||
// --- Regen ---
|
||||
|
||||
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER)
|
||||
public void regen(
|
||||
@Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5")
|
||||
int radius
|
||||
@@ -342,29 +352,37 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
Player player = player();
|
||||
VolmitSender commandSender = sender();
|
||||
World world = player.getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
Iris.info("Regen run start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
// The player position must be read on the thread owning the player; the regenerator hops per chunk itself.
|
||||
if (!J.runEntity(player, () -> {
|
||||
int centerX = player.getLocation().getBlockX() >> 4;
|
||||
int centerZ = player.getLocation().getBlockZ() >> 4;
|
||||
|
||||
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
Iris.info("Regen run start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start();
|
||||
})) {
|
||||
Iris.warn("Could not schedule regen on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
+1
-14
@@ -316,19 +316,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
|
||||
}
|
||||
|
||||
/*
|
||||
/todo
|
||||
@Director(description = "Benchmark a pack", descriptionKey = "iris.director.commandiris.director.benchmark_pack", origin = DirectorOrigin.CONSOLE)
|
||||
public void packbenchmark(
|
||||
@Param(description = "Dimension to benchmark", descriptionKey = "iris.director.commandiris.param.dimension_benchmark")
|
||||
IrisDimension type
|
||||
) throws InterruptedException {
|
||||
|
||||
BenchDimension = type.getLoadKey();
|
||||
|
||||
IrisPackBenchmarking.runBenchmark();
|
||||
} */
|
||||
|
||||
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
|
||||
public void height() {
|
||||
if (sender().isPlayer()) {
|
||||
@@ -588,7 +575,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.instance.checkForBukkitWorlds(logicalWorldName::equals);
|
||||
Iris.instance.worldReconciler().checkForBukkitWorlds(logicalWorldName::equals);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
|
||||
}
|
||||
@Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
|
||||
+156
-104
@@ -428,8 +428,13 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d")
|
||||
public void dust() {
|
||||
player().getInventory().addItem(WandSVC.createDust());
|
||||
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
player.getInventory().addItem(WandSVC.createDust());
|
||||
commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-")
|
||||
@@ -437,28 +442,33 @@ public class CommandObject implements DirectorExecutor {
|
||||
@Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d.f(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
Location[] b = WandSVC.getCuboid(player);
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d.f(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player.updateInventory();
|
||||
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1")
|
||||
@@ -466,25 +476,30 @@ public class CommandObject implements DirectorExecutor {
|
||||
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (WandSVC.isHoldingWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[1] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
|
||||
if (WandSVC.isHoldingWand(player)) {
|
||||
Location[] g = WandSVC.getCuboid(player);
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[1] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2")
|
||||
@@ -492,26 +507,31 @@ public class CommandObject implements DirectorExecutor {
|
||||
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (WandSVC.isHoldingIrisWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
if (WandSVC.isHoldingIrisWand(player)) {
|
||||
Location[] g = WandSVC.getCuboid(player);
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true)
|
||||
@@ -540,8 +560,10 @@ public class CommandObject implements DirectorExecutor {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
|
||||
|
||||
ItemStack wand = player().getInventory().getItemInMainHand();
|
||||
Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
ItemStack wand = player.getInventory().getItemInMainHand();
|
||||
Location block = player.getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
|
||||
|
||||
Map<Block, BlockData> futureChanges = new HashMap<>();
|
||||
|
||||
@@ -549,30 +571,50 @@ public class CommandObject implements DirectorExecutor {
|
||||
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
|
||||
}
|
||||
|
||||
o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
|
||||
// Block writes must run on the thread owning the target chunk; the undo log stays global.
|
||||
final IrisObject placed = o;
|
||||
if (!J.runAt(block, () -> {
|
||||
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
|
||||
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
|
||||
|
||||
Iris.service(ObjectSVC.class).addChanges(futureChanges);
|
||||
|
||||
if (edit) {
|
||||
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
|
||||
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
|
||||
if (WandSVC.isWand(wand)) {
|
||||
wand = newWand;
|
||||
player().getInventory().setItemInMainHand(wand);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
|
||||
} else {
|
||||
int slot = WandSVC.findWand(player().getInventory());
|
||||
if (slot == -1) {
|
||||
player().getInventory().addItem(newWand);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
|
||||
} else {
|
||||
player().getInventory().setItem(slot, newWand);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", o.getLoadKey())));
|
||||
}
|
||||
if (!edit) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
Vector center = new Vector(placed.getCenter().getX(), placed.getCenter().getY(), placed.getCenter().getZ());
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(placed.getW() - 1,
|
||||
placed.getH() + center.getY() - 1, placed.getD() - 1), block.clone().subtract(center.clone().setY(0)));
|
||||
if (WandSVC.isWand(wand)) {
|
||||
player.getInventory().setItemInMainHand(newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
} else {
|
||||
int slot = WandSVC.findWand(player.getInventory());
|
||||
if (slot == -1) {
|
||||
player.getInventory().addItem(newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
} else {
|
||||
player.getInventory().setItem(slot, newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
}
|
||||
}
|
||||
});
|
||||
})) {
|
||||
Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
||||
*/
|
||||
private void onPlayerThread(Player player, Runnable body) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.runEntity(player, body)) {
|
||||
Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,30 +658,35 @@ public class CommandObject implements DirectorExecutor {
|
||||
@Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
a1.add(d.toVector().multiply(amount));
|
||||
a2.add(d.toVector().multiply(amount));
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player);
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
a1.add(d.toVector().multiply(amount));
|
||||
a2.add(d.toVector().multiply(amount));
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player.updateInventory();
|
||||
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u")
|
||||
@@ -655,20 +702,25 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER)
|
||||
public void we() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
|
||||
return;
|
||||
}
|
||||
|
||||
Cuboid locs = WorldEditLink.getSelection(sender().player());
|
||||
Cuboid locs = WorldEditLink.getSelection(player);
|
||||
|
||||
if (locs == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
|
||||
onPlayerThread(player, () -> {
|
||||
player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true)
|
||||
|
||||
+51
-13
@@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.core.service.BoardSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
@@ -104,6 +105,8 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
@@ -114,6 +117,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
@@ -662,10 +666,18 @@ public class CommandStudio implements DirectorExecutor {
|
||||
@Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true)
|
||||
Vector location
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Engine spawnEngine = engine();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
|
||||
}
|
||||
|
||||
// Entity creation must run on the thread owning the destination chunk.
|
||||
Location at = new Location(world(), location.getX(), location.getY(), location.getZ());
|
||||
if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) {
|
||||
Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + ".");
|
||||
}
|
||||
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
@@ -697,7 +709,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
|
||||
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
|
||||
if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName())));
|
||||
@@ -718,20 +730,46 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
KList<Chunk> chunks = new KList<>();
|
||||
int bx = player().getLocation().getChunk().getX();
|
||||
int bz = player().getLocation().getChunk().getZ();
|
||||
Player reporter = player();
|
||||
CountDownLatch gathered = new CountDownLatch(1);
|
||||
|
||||
try {
|
||||
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
// The raycast and the chunk loads need the thread owning the player; the report itself stays off it.
|
||||
boolean scheduled = J.runEntity(reporter, () -> {
|
||||
try {
|
||||
int bx = reporter.getLocation().getChunk().getX();
|
||||
int bz = reporter.getLocation().getChunk().getZ();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
try {
|
||||
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
} finally {
|
||||
gathered.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
||||
Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + ".");
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
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");
|
||||
|
||||
+92
-55
@@ -25,10 +25,13 @@ import art.arcane.iris.Iris;
|
||||
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.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -38,8 +41,10 @@ import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -97,83 +102,115 @@ public class CommandWhat implements DirectorExecutor {
|
||||
|
||||
@Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER)
|
||||
public void region() {
|
||||
try {
|
||||
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
World world = world();
|
||||
Engine engine = engine();
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
|
||||
}
|
||||
// Chunk access must happen on the thread owning the player's chunk.
|
||||
onPlayerThread(player, () -> {
|
||||
try {
|
||||
Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4);
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER)
|
||||
public void block() {
|
||||
BlockData bd;
|
||||
try {
|
||||
bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
|
||||
bd = null;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (bd != null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
|
||||
|
||||
if (BukkitBlockResolution.isStorage(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
|
||||
// The raycast reads blocks, so it has to run on the thread owning the player.
|
||||
onPlayerThread(player, () -> {
|
||||
BlockData bd;
|
||||
try {
|
||||
bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
|
||||
bd = null;
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
|
||||
}
|
||||
if (bd != null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
|
||||
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
|
||||
}
|
||||
if (BukkitBlockResolution.isStorage(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
|
||||
}
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
|
||||
}
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
|
||||
}
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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) {
|
||||
Chunk c = player().getLocation().getChunk();
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
int m = 1;
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
|
||||
onPlayerThread(player, () -> {
|
||||
Chunk c = player.getLocation().getChunk();
|
||||
|
||||
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();
|
||||
});
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
||||
*/
|
||||
private void onPlayerThread(Player player, Runnable body) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.runEntity(player, body)) {
|
||||
Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -23,21 +23,24 @@ import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class BukkitBlockEditor implements BlockEditor {
|
||||
private final World world;
|
||||
private volatile long last;
|
||||
|
||||
public BukkitBlockEditor(World world) {
|
||||
this.world = world;
|
||||
this.last = M.ms();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
touch();
|
||||
world.getBlockAt(x, y, z).setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
touch();
|
||||
return world.getBlockAt(x, y, z).getBlockData();
|
||||
}
|
||||
|
||||
@@ -48,11 +51,12 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
|
||||
@Override
|
||||
public long last() {
|
||||
return M.ms();
|
||||
return last;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
touch();
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
@@ -62,16 +66,23 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, Biome b) {
|
||||
touch();
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int y, int z) {
|
||||
touch();
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
touch();
|
||||
return world.getBiome(x, world.getMinHeight(), z);
|
||||
}
|
||||
|
||||
private void touch() {
|
||||
last = M.ms();
|
||||
}
|
||||
}
|
||||
|
||||
+97
-27
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -26,72 +27,141 @@ import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final Engine engine;
|
||||
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
||||
private volatile List<GuiMarker> playerMarkers = List.of();
|
||||
|
||||
public BukkitVisionOverlay(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the AWT event thread, so it may only hand back the last snapshot
|
||||
* built by a server thread.
|
||||
*/
|
||||
@Override
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : BukkitWorldBinding.players(world)) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
queuePlayerRefresh();
|
||||
return playerMarkers;
|
||||
}
|
||||
|
||||
private void queuePlayerRefresh() {
|
||||
if (!playerRefreshQueued.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
|
||||
Location at = player.getLocation();
|
||||
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ()));
|
||||
}
|
||||
playerMarkers = List.copyOf(markers);
|
||||
} finally {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
|
||||
entity.getHealth(), maxHealth));
|
||||
J.runGlobal(() -> {
|
||||
IrisWorld target = engine.getWorld();
|
||||
World world = BukkitWorldBinding.world(target);
|
||||
if (world == null) {
|
||||
sink.accept(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
List<LivingEntity> living = new ArrayList<>();
|
||||
for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) {
|
||||
if (!(entity instanceof Player)) {
|
||||
living.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (living.isEmpty()) {
|
||||
sink.accept(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
||||
AtomicInteger pending = new AtomicInteger(living.size());
|
||||
Runnable complete = () -> {
|
||||
if (pending.decrementAndGet() == 0) {
|
||||
sink.accept(List.copyOf(collected));
|
||||
}
|
||||
};
|
||||
|
||||
for (LivingEntity entity : living) {
|
||||
Location at = entity.getLocation();
|
||||
Runnable read = () -> {
|
||||
try {
|
||||
collected.add(marker(entity, at));
|
||||
} catch (Throwable ignored) {
|
||||
} finally {
|
||||
complete.run();
|
||||
}
|
||||
};
|
||||
|
||||
if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) {
|
||||
complete.run();
|
||||
}
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
}
|
||||
|
||||
private GuiMarker marker(LivingEntity entity, Location at) {
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasPlatformWorld()) {
|
||||
IrisWorld target = engine.getWorld();
|
||||
if (!target.hasPlatformWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = BukkitWorldBinding.players(world);
|
||||
J.runGlobal(() -> {
|
||||
List<Player> players = BukkitWorldBinding.players(target);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player player = players.get(0);
|
||||
World world = player.getWorld();
|
||||
int xx = (int) worldX;
|
||||
int zz = (int) worldZ;
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
J.runRegion(world, xx >> 4, zz >> 4, () -> {
|
||||
int yy = world.getHighestBlockYAt(xx, zz) + 1;
|
||||
Location destination = new Location(world, xx, yy, zz);
|
||||
J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||
import art.arcane.iris.core.service.IrisApiEventSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
@@ -41,7 +42,7 @@ import org.bukkit.World;
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
|
||||
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.tree.TreeFellerRunHooks;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
final class FellingRun {
|
||||
final TreeClaim claim;
|
||||
final TreeCandidate candidate;
|
||||
final int preservationChance;
|
||||
final TreeFellerRunHooks runHooks;
|
||||
final int heldSlot;
|
||||
final TreeFellerPresentation presentation;
|
||||
final AtomicBoolean finished = new AtomicBoolean();
|
||||
final AtomicInteger cursor = new AtomicInteger();
|
||||
final AtomicInteger processed = new AtomicInteger();
|
||||
volatile int blocksPerPulse = 1;
|
||||
volatile int effectStride = 1;
|
||||
volatile ItemStack expectedTool;
|
||||
volatile List<TreeMember> work = List.of();
|
||||
|
||||
FellingRun(
|
||||
TreeClaim claim,
|
||||
TreeCandidate candidate,
|
||||
int preservationChance,
|
||||
TreeFellerRunHooks runHooks,
|
||||
int heldSlot,
|
||||
Location fallbackLocation
|
||||
) {
|
||||
this.claim = claim;
|
||||
this.candidate = candidate;
|
||||
this.preservationChance = preservationChance;
|
||||
this.runHooks = runHooks;
|
||||
this.heldSlot = heldSlot;
|
||||
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
|
||||
this.expectedTool = candidate.tool().clone();
|
||||
}
|
||||
}
|
||||
@@ -279,6 +279,11 @@ public final class IrisEngineSVC implements IrisService {
|
||||
}
|
||||
closing.completion().complete(null);
|
||||
} else {
|
||||
// A failed close must still stop conflicting with future registrations,
|
||||
// otherwise the world never regains its maintenance task after a reload.
|
||||
synchronized (registrationLock) {
|
||||
closingGenerators.remove(closing);
|
||||
}
|
||||
closing.completion().completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.service.tree.BlockDropRouter;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
|
||||
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||
private final TreeFellerSVC service;
|
||||
private final FellingRun run;
|
||||
|
||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
||||
super(block, player);
|
||||
this.run = run;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean routeDrop(Object drop) {
|
||||
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.tree.TreeFellerAccess;
|
||||
import art.arcane.iris.api.tree.TreeFellerRunHooks;
|
||||
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
final class TreeFellerModel {
|
||||
private TreeFellerModel() {
|
||||
}
|
||||
|
||||
record PendingFell(
|
||||
TreeCandidate candidate,
|
||||
int preservationChance,
|
||||
TreeFellerRunHooks runHooks
|
||||
) {
|
||||
PendingFell withAccess(TreeFellerAccess access) {
|
||||
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
|
||||
}
|
||||
}
|
||||
|
||||
record TreeCandidate(
|
||||
TreeContext context,
|
||||
World world,
|
||||
Player player,
|
||||
ItemStack tool,
|
||||
TreeFellerAccess access,
|
||||
TreeMarkerTraversal.Position trigger
|
||||
) {
|
||||
TreeCandidate withAccess(TreeFellerAccess access) {
|
||||
return new TreeCandidate(context, world, player, tool, access, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
record TreeContext(
|
||||
Engine engine,
|
||||
String marker,
|
||||
TreeBlockMaterial expectedMaterial,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
}
|
||||
|
||||
record TreeClaim(UUID worldId, String marker) {
|
||||
}
|
||||
|
||||
record ProvenanceSnapshot(
|
||||
Engine engine,
|
||||
World world,
|
||||
int minimumY,
|
||||
TreeMarkerTraversal.Position position,
|
||||
String marker,
|
||||
TreeBlockMaterial material
|
||||
) {
|
||||
}
|
||||
|
||||
record ChunkPosition(int x, int z) {
|
||||
}
|
||||
|
||||
record TreeMember(
|
||||
TreeMarkerTraversal.Position position,
|
||||
boolean log,
|
||||
TreeBlockMaterial expectedMaterial,
|
||||
int erosionOrder
|
||||
) {
|
||||
}
|
||||
|
||||
record DamageReservation(
|
||||
ItemStack toolForDrops,
|
||||
boolean charged,
|
||||
boolean broke,
|
||||
boolean logCostReserved
|
||||
) {
|
||||
}
|
||||
}
|
||||
+34
-814
@@ -5,27 +5,22 @@ import art.arcane.iris.api.tree.TreeFellerAccess;
|
||||
import art.arcane.iris.api.tree.TreeFellerOptions;
|
||||
import art.arcane.iris.api.tree.TreeFellerRunHooks;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.service.tree.BlockDropRouter;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.PendingFell;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
|
||||
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
|
||||
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.ExperienceOrb;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -36,40 +31,31 @@ import org.bukkit.event.player.PlayerItemHeldEvent;
|
||||
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
|
||||
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.inventory.meta.Damageable;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
private static final String PERMISSION = "iris.treefeller";
|
||||
|
||||
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
|
||||
private final Map<BlockBreakEvent, PendingFell> pending = Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
|
||||
final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
|
||||
Collections.newSetFromMap(new IdentityHashMap<>())
|
||||
);
|
||||
private final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
|
||||
private final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
|
||||
final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
|
||||
final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
|
||||
private final Map<Engine, TreeDefinitionIndex> definitions = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private final TreeProvenance provenance = new TreeProvenance(definitions);
|
||||
private final TreeFellingRunner runner = new TreeFellingRunner(this, provenance);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -92,7 +78,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
managedEvents.clear();
|
||||
for (Set<FellingRun> runs : activeRuns.values()) {
|
||||
for (FellingRun run : runs) {
|
||||
finish(run);
|
||||
runner.finish(run);
|
||||
}
|
||||
}
|
||||
activeRuns.clear();
|
||||
@@ -105,8 +91,12 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
if (!serviceEnabled.get()
|
||||
|| event == null
|
||||
|| options == null
|
||||
|| event.isCancelled()
|
||||
|| isManagedBreak(event)) {
|
||||
|| event.isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
// An event that already carries a pending fell may still be upgraded to
|
||||
// INTEGRATION_OVERRIDE; only internal probes are refused outright.
|
||||
if (!pending.containsKey(event) && isManagedBreak(event)) {
|
||||
return false;
|
||||
}
|
||||
if (!canUse(event.getPlayer(), options.access())) {
|
||||
@@ -115,7 +105,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
|
||||
TreeCandidate candidate;
|
||||
try {
|
||||
candidate = resolveCandidate(event.getBlock(), event.getPlayer());
|
||||
candidate = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to resolve an Iris tree-feller request.", error);
|
||||
return false;
|
||||
@@ -153,7 +143,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return resolveTreeContext(block) != null;
|
||||
return provenance.resolveTreeContext(block) != null;
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to inspect Iris tree provenance.", error);
|
||||
return false;
|
||||
@@ -186,7 +176,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
|
||||
TreeCandidate current;
|
||||
try {
|
||||
current = resolveCandidate(event.getBlock(), event.getPlayer());
|
||||
current = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to finalize an Iris tree-feller request.", error);
|
||||
deferSuccessfulBreakCleanup(event);
|
||||
@@ -224,38 +214,38 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
activeRuns.computeIfAbsent(event.getPlayer().getUniqueId(), ignored -> ConcurrentHashMap.newKeySet()).add(run);
|
||||
notifyActivationAccepted(run.runHooks);
|
||||
run.presentation.activate(event.getBlock());
|
||||
discover(run);
|
||||
runner.discover(run);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
|
||||
if (!event.isSneaking()) {
|
||||
finishRuns(event.getPlayer().getUniqueId());
|
||||
runner.finishRuns(event.getPlayer().getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void haltWhenHeldSlotChanges(PlayerItemHeldEvent event) {
|
||||
if (event.getNewSlot() != event.getPreviousSlot()) {
|
||||
finishRuns(event.getPlayer().getUniqueId());
|
||||
runner.finishRuns(event.getPlayer().getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void haltWhenHandsSwap(PlayerSwapHandItemsEvent event) {
|
||||
finishRuns(event.getPlayer().getUniqueId());
|
||||
runner.finishRuns(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void clearPlacedProvenance(BlockPlaceEvent event) {
|
||||
ProvenanceSnapshot snapshot = captureProvenance(event.getBlockPlaced());
|
||||
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlockPlaced());
|
||||
if (snapshot == null) {
|
||||
return;
|
||||
}
|
||||
Location location = event.getBlockPlaced().getLocation();
|
||||
Runnable cleanup = () -> {
|
||||
if (!event.isCancelled()) {
|
||||
clearProvenanceIfMatching(snapshot);
|
||||
provenance.clearProvenanceIfMatching(snapshot);
|
||||
}
|
||||
};
|
||||
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
|
||||
@@ -263,6 +253,10 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isServiceEnabled() {
|
||||
return serviceEnabled.get();
|
||||
}
|
||||
|
||||
private boolean canUse(Player player, TreeFellerAccess access) {
|
||||
if (access == TreeFellerAccess.INTEGRATION_OVERRIDE) {
|
||||
return true;
|
||||
@@ -287,616 +281,19 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
}
|
||||
}
|
||||
|
||||
private TreeCandidate resolveCandidate(Block block, Player player) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
|
||||
return null;
|
||||
}
|
||||
if (!Tag.LOGS.isTagged(block.getType())) {
|
||||
return null;
|
||||
}
|
||||
ItemStack tool = player.getInventory().getItemInMainHand();
|
||||
if (!isAxe(tool)) {
|
||||
return null;
|
||||
}
|
||||
TreeContext context = resolveTreeContext(block);
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
return new TreeCandidate(
|
||||
context,
|
||||
block.getWorld(),
|
||||
player,
|
||||
tool.clone(),
|
||||
TreeFellerAccess.STANDALONE,
|
||||
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
|
||||
);
|
||||
}
|
||||
|
||||
private TreeContext resolveTreeContext(Block block) {
|
||||
if (block.getType().isAir()) {
|
||||
return null;
|
||||
}
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
|
||||
if (access == null || access.getEngine() == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
World world = block.getWorld();
|
||||
int minimumY = world.getMinHeight();
|
||||
int maximumY = world.getMaxHeight();
|
||||
int relativeY = block.getY() - minimumY;
|
||||
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
if (decoded == null || decoded.structureAware()) {
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
|
||||
block.getX(),
|
||||
relativeY,
|
||||
block.getZ(),
|
||||
TreeBlockMaterial.class
|
||||
);
|
||||
if (expected != null && !matchesExpectedMaterial(block, expected)) {
|
||||
return null;
|
||||
}
|
||||
if (expected == null
|
||||
&& !decoded.objectKey().startsWith("trees/")
|
||||
&& !definitionIndex(engine).isTreeMarker(marker)) {
|
||||
return null;
|
||||
}
|
||||
return new TreeContext(engine, marker, expected, minimumY, maximumY);
|
||||
}
|
||||
|
||||
private TreeDefinitionIndex definitionIndex(Engine engine) {
|
||||
synchronized (definitions) {
|
||||
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
|
||||
}
|
||||
}
|
||||
|
||||
private void discover(FellingRun run) {
|
||||
J.a(() -> {
|
||||
if (run.candidate.context().engine().isClosed()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
|
||||
run.candidate.trigger(),
|
||||
run.candidate.context().marker(),
|
||||
run.candidate.context().minimumY(),
|
||||
run.candidate.context().maximumY(),
|
||||
(x, y, z) -> markerAt(
|
||||
run.candidate.context().engine(),
|
||||
run.candidate.context().minimumY(),
|
||||
x,
|
||||
y,
|
||||
z
|
||||
)
|
||||
);
|
||||
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
|
||||
preflight(run, positions, discovery.complete());
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
|
||||
preflight(run, List.of(run.candidate.trigger()), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
|
||||
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
|
||||
if (grouped.isEmpty()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
|
||||
for (int index = 0; index < positions.size(); index++) {
|
||||
erosionOrder.put(positions.get(index), index);
|
||||
}
|
||||
|
||||
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
|
||||
AtomicBoolean failed = new AtomicBoolean();
|
||||
AtomicInteger remaining = new AtomicInteger(grouped.size());
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
|
||||
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
|
||||
ChunkPosition chunk = entry.getKey();
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
|
||||
failed.set(true);
|
||||
return;
|
||||
}
|
||||
for (TreeMarkerTraversal.Position position : entry.getValue()) {
|
||||
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
|
||||
if (member != null) {
|
||||
members.add(member);
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
failed.set(true);
|
||||
IrisLogging.reportError(
|
||||
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
|
||||
}
|
||||
};
|
||||
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
|
||||
failed.set(true);
|
||||
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<TreeMarkerTraversal.Position> positionsForFelling(
|
||||
TreeMarkerTraversal.Discovery discovery,
|
||||
TreeMarkerTraversal.Position trigger
|
||||
) {
|
||||
return discovery.complete() ? discovery.members() : List.of(trigger);
|
||||
}
|
||||
|
||||
private void completePreflightGroup(
|
||||
FellingRun run,
|
||||
List<TreeMember> members,
|
||||
AtomicBoolean failed,
|
||||
AtomicInteger remaining,
|
||||
AtomicBoolean completed,
|
||||
boolean allowFallback
|
||||
) {
|
||||
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (failed.get() && allowFallback) {
|
||||
preflight(run, List.of(run.candidate.trigger()), false);
|
||||
return;
|
||||
}
|
||||
if (failed.get()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
|
||||
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
run.work = ordered;
|
||||
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
|
||||
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
|
||||
processNext(run);
|
||||
}
|
||||
|
||||
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
|
||||
World world = run.candidate.world();
|
||||
Block block = world.getBlockAt(position.x(), position.y(), position.z());
|
||||
TreeContext context = run.candidate.context();
|
||||
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
|
||||
if (expected != null && !matchesExpectedMaterial(block, expected)) {
|
||||
clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
if (block.getType().isAir()) {
|
||||
clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
|
||||
}
|
||||
|
||||
private List<TreeMember> orderMembers(
|
||||
TreeMarkerTraversal.Position trigger,
|
||||
Collection<TreeMember> discovered
|
||||
) {
|
||||
Comparator<TreeMember> erosionOrder = Comparator
|
||||
.comparingInt(TreeMember::erosionOrder)
|
||||
.thenComparingInt(member -> member.position().y())
|
||||
.thenComparingInt(member -> member.position().x())
|
||||
.thenComparingInt(member -> member.position().z());
|
||||
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
|
||||
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
|
||||
return List.of();
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private void processNext(FellingRun run) {
|
||||
if (run.finished.get()) {
|
||||
return;
|
||||
}
|
||||
int index = run.cursor.getAndIncrement();
|
||||
if (index >= run.work.size()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
TreeMember member = run.work.get(index);
|
||||
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
|
||||
Runnable task = () -> runTask(
|
||||
run,
|
||||
"Failed to prepare an Iris tree-feller block.",
|
||||
() -> prepareBreak(run, member)
|
||||
);
|
||||
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
|
||||
if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareBreak(FellingRun run, TreeMember member) {
|
||||
Block block = liveMemberBlock(run, member);
|
||||
if (block == null) {
|
||||
if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!member.log()) {
|
||||
runMutationTask(
|
||||
run,
|
||||
member,
|
||||
new DamageReservation(run.expectedTool.clone(), false, false, false),
|
||||
new AtomicBoolean()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable task = () -> runTask(
|
||||
run,
|
||||
"Failed to reserve Iris tree-feller tool durability.",
|
||||
() -> reserveDamage(run, member)
|
||||
);
|
||||
if (!J.runEntity(run.candidate.player(), task)) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void reserveDamage(FellingRun run, TreeMember member) {
|
||||
Player player = run.candidate.player();
|
||||
if (!isRunControlActive(run, player)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
ItemStack current = inventory.getItem(run.heldSlot);
|
||||
if (current == null
|
||||
|| inventory.getHeldItemSlot() != run.heldSlot
|
||||
|| !current.isSimilar(run.expectedTool)
|
||||
|| !isAxe(current)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reserveLogCost(run)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack before = current.clone();
|
||||
ItemMeta meta = current.getItemMeta();
|
||||
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
|
||||
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
|
||||
return;
|
||||
}
|
||||
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
|
||||
refundAndFinish(run, new DamageReservation(before, false, false, true));
|
||||
return;
|
||||
}
|
||||
|
||||
int nextDamage = damageable.getDamage() + 1;
|
||||
boolean broke = nextDamage >= current.getType().getMaxDurability();
|
||||
if (broke) {
|
||||
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
|
||||
run.expectedTool = new ItemStack(Material.AIR);
|
||||
} else {
|
||||
damageable.setDamage(nextDamage);
|
||||
current.setItemMeta(meta);
|
||||
inventory.setItem(run.heldSlot, current);
|
||||
run.expectedTool = current.clone();
|
||||
}
|
||||
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
|
||||
}
|
||||
|
||||
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
|
||||
TreeMarkerTraversal.Position position = member.position();
|
||||
AtomicBoolean mutationSucceeded = new AtomicBoolean();
|
||||
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
|
||||
boolean scheduled;
|
||||
try {
|
||||
scheduled = J.runRegion(
|
||||
run.candidate.world(),
|
||||
position.x() >> 4,
|
||||
position.z() >> 4,
|
||||
task
|
||||
);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
if (!scheduled) {
|
||||
refundAndFinish(run, reservation);
|
||||
}
|
||||
}
|
||||
|
||||
private void runMutationTask(
|
||||
FellingRun run,
|
||||
TreeMember member,
|
||||
DamageReservation reservation,
|
||||
AtomicBoolean mutationSucceeded
|
||||
) {
|
||||
if (run.finished.get()) {
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
probeAndMutate(run, member, reservation, mutationSucceeded);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
|
||||
if (mutationSucceeded.get()) {
|
||||
finish(run);
|
||||
} else {
|
||||
refundAndFinish(run, reservation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void probeAndMutate(
|
||||
FellingRun run,
|
||||
TreeMember member,
|
||||
DamageReservation reservation,
|
||||
AtomicBoolean mutationSucceeded
|
||||
) {
|
||||
Block block = liveMemberBlock(run, member);
|
||||
if (block == null) {
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run);
|
||||
managedEvents.add(probe);
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(probe);
|
||||
} catch (Throwable error) {
|
||||
probe.setCancelled(true);
|
||||
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
|
||||
} finally {
|
||||
managedEvents.remove(probe);
|
||||
}
|
||||
|
||||
try {
|
||||
if (probe.isCancelled()) {
|
||||
if (reservation.charged() || reservation.logCostReserved()) {
|
||||
refundAndFinish(run, reservation);
|
||||
} else if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
block = liveMemberBlock(run, member);
|
||||
if (run.finished.get() || block == null) {
|
||||
probe.setCancelled(true);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
|
||||
BlockData visualData = block.getBlockData().clone();
|
||||
List<ItemStack> vanillaDrops = probe.isDropItems()
|
||||
? block.getDrops(reservation.toolForDrops()).stream()
|
||||
.map(ItemStack::clone)
|
||||
.toList()
|
||||
: List.of();
|
||||
block.setType(Material.AIR, false);
|
||||
if (!block.getType().isAir()) {
|
||||
probe.setCancelled(true);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
mutationSucceeded.set(true);
|
||||
run.presentation.erode(
|
||||
source,
|
||||
visualData,
|
||||
member.erosionOrder(),
|
||||
run.processed.get(),
|
||||
run.blocksPerPulse,
|
||||
run.effectStride,
|
||||
run.work.size()
|
||||
);
|
||||
|
||||
clearProvenance(
|
||||
run.candidate.context().engine(),
|
||||
run.candidate.context().minimumY(),
|
||||
member.position()
|
||||
);
|
||||
routeDrops(run, vanillaDrops, source);
|
||||
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
|
||||
dropExperience(source, probe.getExpToDrop());
|
||||
}
|
||||
if (reservation.logCostReserved()) {
|
||||
completeLogCost(run, reservation);
|
||||
return;
|
||||
}
|
||||
completeSuccessfulMutation(run, reservation);
|
||||
} catch (RuntimeException | Error error) {
|
||||
if (!mutationSucceeded.get()) {
|
||||
probe.setCancelled(true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private Block liveMemberBlock(FellingRun run, TreeMember member) {
|
||||
World world = run.candidate.world();
|
||||
TreeMarkerTraversal.Position position = member.position();
|
||||
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
|
||||
return null;
|
||||
}
|
||||
Block block = world.getBlockAt(position.x(), position.y(), position.z());
|
||||
TreeContext context = run.candidate.context();
|
||||
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
|
||||
return null;
|
||||
}
|
||||
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
|
||||
clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
|
||||
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
|
||||
clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
if (expected != null && !matchesExpectedMaterial(block, expected)) {
|
||||
clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
|
||||
if (!reservation.charged() && !reservation.logCostReserved()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
if (!J.runEntity(run.candidate.player(), () -> {
|
||||
if (reservation.charged()) {
|
||||
PlayerInventory inventory = run.candidate.player().getInventory();
|
||||
ItemStack current = inventory.getItem(run.heldSlot);
|
||||
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
|
||||
boolean currentMatches = expectedAir
|
||||
? current == null || current.getType() == Material.AIR
|
||||
: current != null && current.isSimilar(run.expectedTool);
|
||||
if (currentMatches) {
|
||||
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
|
||||
run.expectedTool = reservation.toolForDrops().clone();
|
||||
}
|
||||
}
|
||||
if (reservation.logCostReserved()) {
|
||||
refundLogCost(run);
|
||||
}
|
||||
finish(run);
|
||||
})) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunControlActive(FellingRun run, Player player) {
|
||||
return player.isOnline()
|
||||
&& player.getGameMode() == GameMode.SURVIVAL
|
||||
&& player.isSneaking()
|
||||
&& player.getWorld().equals(run.candidate.world());
|
||||
}
|
||||
|
||||
private boolean reserveLogCost(FellingRun run) {
|
||||
try {
|
||||
return run.runHooks.reserveLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void completeLogCost(FellingRun run, DamageReservation reservation) {
|
||||
if (!J.runEntity(run.candidate.player(), () -> {
|
||||
try {
|
||||
run.runHooks.commitLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
completeSuccessfulMutation(run, reservation);
|
||||
})) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void refundLogCost(FellingRun run) {
|
||||
try {
|
||||
run.runHooks.refundLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
|
||||
if (reservation.broke()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
continueRun(run);
|
||||
}
|
||||
|
||||
private void continueRun(FellingRun run) {
|
||||
int processed = run.processed.incrementAndGet();
|
||||
if (processed % run.blocksPerPulse == 0) {
|
||||
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
|
||||
} else {
|
||||
processNext(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void runTask(FellingRun run, String context, Runnable task) {
|
||||
if (run.finished.get() || !serviceEnabled.get()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
task.run();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(context, error);
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void finish(FellingRun run) {
|
||||
if (run.finished.compareAndSet(false, true)) {
|
||||
activeClaims.remove(run.claim);
|
||||
Set<FellingRun> runs = activeRuns.get(run.candidate.player().getUniqueId());
|
||||
if (runs != null) {
|
||||
runs.remove(run);
|
||||
if (runs.isEmpty()) {
|
||||
activeRuns.remove(run.candidate.player().getUniqueId(), runs);
|
||||
}
|
||||
}
|
||||
run.presentation.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishRuns(UUID playerId) {
|
||||
Set<FellingRun> runs = activeRuns.get(playerId);
|
||||
if (runs == null) {
|
||||
return;
|
||||
}
|
||||
for (FellingRun run : List.copyOf(runs)) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseManagedLater(BlockBreakEvent event) {
|
||||
J.s(() -> managedEvents.remove(event), 1);
|
||||
}
|
||||
|
||||
private void deferSuccessfulBreakCleanup(BlockBreakEvent event) {
|
||||
ProvenanceSnapshot snapshot = captureProvenance(event.getBlock());
|
||||
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlock());
|
||||
if (snapshot == null) {
|
||||
return;
|
||||
}
|
||||
Location location = event.getBlock().getLocation();
|
||||
Runnable cleanup = () -> {
|
||||
if (!event.isCancelled()) {
|
||||
clearProvenanceIfMatching(snapshot);
|
||||
provenance.clearProvenanceIfMatching(snapshot);
|
||||
}
|
||||
};
|
||||
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
|
||||
@@ -904,77 +301,11 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
|
||||
List<TreeMarkerTraversal.Position> positions
|
||||
) {
|
||||
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
|
||||
for (TreeMarkerTraversal.Position position : positions) {
|
||||
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
|
||||
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private String markerAt(Engine engine, int minimumY, int x, int y, int z) {
|
||||
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
|
||||
}
|
||||
|
||||
private TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
return engine.getMantle().getMantle().get(
|
||||
position.x(),
|
||||
position.y() - minimumY,
|
||||
position.z(),
|
||||
TreeBlockMaterial.class
|
||||
);
|
||||
}
|
||||
|
||||
private ProvenanceSnapshot captureProvenance(Block block) {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
|
||||
if (access == null || access.getEngine() == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
|
||||
block.getX(),
|
||||
block.getY(),
|
||||
block.getZ()
|
||||
);
|
||||
int minimumY = block.getWorld().getMinHeight();
|
||||
String marker = markerAt(engine, minimumY, position);
|
||||
TreeBlockMaterial material = materialAt(engine, minimumY, position);
|
||||
if (marker == null && material == null) {
|
||||
return null;
|
||||
}
|
||||
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
|
||||
}
|
||||
|
||||
private void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
|
||||
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
|
||||
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
}
|
||||
}
|
||||
|
||||
private void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
int relativeY = position.y() - minimumY;
|
||||
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
|
||||
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
|
||||
}
|
||||
|
||||
private boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
|
||||
return expected.matches(block.getBlockData().getAsString());
|
||||
}
|
||||
|
||||
private boolean isAxe(ItemStack item) {
|
||||
static boolean isAxe(ItemStack item) {
|
||||
return item != null && item.getType() != Material.AIR && item.getType().name().endsWith("_AXE");
|
||||
}
|
||||
|
||||
private void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
|
||||
void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
|
||||
World world = source.getWorld();
|
||||
if (world == null) {
|
||||
return;
|
||||
@@ -986,7 +317,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
}
|
||||
}
|
||||
|
||||
private void dropExperience(Location location, int experience) {
|
||||
void dropExperience(Location location, int experience) {
|
||||
if (experience <= 0 || location.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
@@ -996,115 +327,4 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
|
||||
);
|
||||
orb.setExperience(experience);
|
||||
}
|
||||
|
||||
private record PendingFell(
|
||||
TreeCandidate candidate,
|
||||
int preservationChance,
|
||||
TreeFellerRunHooks runHooks
|
||||
) {
|
||||
private PendingFell withAccess(TreeFellerAccess access) {
|
||||
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
|
||||
}
|
||||
}
|
||||
|
||||
private record TreeCandidate(
|
||||
TreeContext context,
|
||||
World world,
|
||||
Player player,
|
||||
ItemStack tool,
|
||||
TreeFellerAccess access,
|
||||
TreeMarkerTraversal.Position trigger
|
||||
) {
|
||||
private TreeCandidate withAccess(TreeFellerAccess access) {
|
||||
return new TreeCandidate(context, world, player, tool, access, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
private record TreeContext(
|
||||
Engine engine,
|
||||
String marker,
|
||||
TreeBlockMaterial expectedMaterial,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
}
|
||||
|
||||
private record TreeClaim(UUID worldId, String marker) {
|
||||
}
|
||||
|
||||
private record ProvenanceSnapshot(
|
||||
Engine engine,
|
||||
World world,
|
||||
int minimumY,
|
||||
TreeMarkerTraversal.Position position,
|
||||
String marker,
|
||||
TreeBlockMaterial material
|
||||
) {
|
||||
}
|
||||
|
||||
private record ChunkPosition(int x, int z) {
|
||||
}
|
||||
|
||||
private record TreeMember(
|
||||
TreeMarkerTraversal.Position position,
|
||||
boolean log,
|
||||
TreeBlockMaterial expectedMaterial,
|
||||
int erosionOrder
|
||||
) {
|
||||
}
|
||||
|
||||
private record DamageReservation(
|
||||
ItemStack toolForDrops,
|
||||
boolean charged,
|
||||
boolean broke,
|
||||
boolean logCostReserved
|
||||
) {
|
||||
}
|
||||
|
||||
private final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||
private final FellingRun run;
|
||||
|
||||
private RoutedBlockBreakEvent(Block block, Player player, FellingRun run) {
|
||||
super(block, player);
|
||||
this.run = run;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean routeDrop(Object drop) {
|
||||
return serviceEnabled.get() && run.presentation.routeDrop(drop);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FellingRun {
|
||||
private final TreeClaim claim;
|
||||
private final TreeCandidate candidate;
|
||||
private final int preservationChance;
|
||||
private final TreeFellerRunHooks runHooks;
|
||||
private final int heldSlot;
|
||||
private final TreeFellerPresentation presentation;
|
||||
private final AtomicBoolean finished = new AtomicBoolean();
|
||||
private final AtomicInteger cursor = new AtomicInteger();
|
||||
private final AtomicInteger processed = new AtomicInteger();
|
||||
private volatile int blocksPerPulse = 1;
|
||||
private volatile int effectStride = 1;
|
||||
private volatile ItemStack expectedTool;
|
||||
private volatile List<TreeMember> work = List.of();
|
||||
|
||||
private FellingRun(
|
||||
TreeClaim claim,
|
||||
TreeCandidate candidate,
|
||||
int preservationChance,
|
||||
TreeFellerRunHooks runHooks,
|
||||
int heldSlot,
|
||||
Location fallbackLocation
|
||||
) {
|
||||
this.claim = claim;
|
||||
this.candidate = candidate;
|
||||
this.preservationChance = preservationChance;
|
||||
this.runHooks = runHooks;
|
||||
this.heldSlot = heldSlot;
|
||||
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
|
||||
this.expectedTool = candidate.tool().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.service.TreeFellerModel.ChunkPosition;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.DamageReservation;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
|
||||
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.bukkit.inventory.meta.Damageable;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
final class TreeFellingRunner {
|
||||
private final TreeFellerSVC service;
|
||||
private final TreeProvenance provenance;
|
||||
|
||||
TreeFellingRunner(TreeFellerSVC service, TreeProvenance provenance) {
|
||||
this.service = service;
|
||||
this.provenance = provenance;
|
||||
}
|
||||
|
||||
void discover(FellingRun run) {
|
||||
J.a(() -> {
|
||||
if (run.candidate.context().engine().isClosed()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
|
||||
run.candidate.trigger(),
|
||||
run.candidate.context().marker(),
|
||||
run.candidate.context().minimumY(),
|
||||
run.candidate.context().maximumY(),
|
||||
(x, y, z) -> provenance.markerAt(
|
||||
run.candidate.context().engine(),
|
||||
run.candidate.context().minimumY(),
|
||||
x,
|
||||
y,
|
||||
z
|
||||
)
|
||||
);
|
||||
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
|
||||
preflight(run, positions, discovery.complete());
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
|
||||
preflight(run, List.of(run.candidate.trigger()), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
|
||||
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
|
||||
if (grouped.isEmpty()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
|
||||
for (int index = 0; index < positions.size(); index++) {
|
||||
erosionOrder.put(positions.get(index), index);
|
||||
}
|
||||
|
||||
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
|
||||
AtomicBoolean failed = new AtomicBoolean();
|
||||
AtomicInteger remaining = new AtomicInteger(grouped.size());
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
|
||||
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
|
||||
ChunkPosition chunk = entry.getKey();
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
|
||||
failed.set(true);
|
||||
return;
|
||||
}
|
||||
for (TreeMarkerTraversal.Position position : entry.getValue()) {
|
||||
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
|
||||
if (member != null) {
|
||||
members.add(member);
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
failed.set(true);
|
||||
IrisLogging.reportError(
|
||||
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
|
||||
}
|
||||
};
|
||||
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
|
||||
failed.set(true);
|
||||
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<TreeMarkerTraversal.Position> positionsForFelling(
|
||||
TreeMarkerTraversal.Discovery discovery,
|
||||
TreeMarkerTraversal.Position trigger
|
||||
) {
|
||||
return discovery.complete() ? discovery.members() : List.of(trigger);
|
||||
}
|
||||
|
||||
private void completePreflightGroup(
|
||||
FellingRun run,
|
||||
List<TreeMember> members,
|
||||
AtomicBoolean failed,
|
||||
AtomicInteger remaining,
|
||||
AtomicBoolean completed,
|
||||
boolean allowFallback
|
||||
) {
|
||||
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (failed.get() && allowFallback) {
|
||||
preflight(run, List.of(run.candidate.trigger()), false);
|
||||
return;
|
||||
}
|
||||
if (failed.get()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
|
||||
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
run.work = ordered;
|
||||
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
|
||||
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
|
||||
processNext(run);
|
||||
}
|
||||
|
||||
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
|
||||
World world = run.candidate.world();
|
||||
Block block = world.getBlockAt(position.x(), position.y(), position.z());
|
||||
TreeContext context = run.candidate.context();
|
||||
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
|
||||
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
|
||||
provenance.clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
if (block.getType().isAir()) {
|
||||
provenance.clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
|
||||
}
|
||||
|
||||
private List<TreeMember> orderMembers(
|
||||
TreeMarkerTraversal.Position trigger,
|
||||
Collection<TreeMember> discovered
|
||||
) {
|
||||
Comparator<TreeMember> erosionOrder = Comparator
|
||||
.comparingInt(TreeMember::erosionOrder)
|
||||
.thenComparingInt(member -> member.position().y())
|
||||
.thenComparingInt(member -> member.position().x())
|
||||
.thenComparingInt(member -> member.position().z());
|
||||
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
|
||||
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
|
||||
return List.of();
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private void processNext(FellingRun run) {
|
||||
if (run.finished.get()) {
|
||||
return;
|
||||
}
|
||||
int index = run.cursor.getAndIncrement();
|
||||
if (index >= run.work.size()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
TreeMember member = run.work.get(index);
|
||||
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
|
||||
Runnable task = () -> runTask(
|
||||
run,
|
||||
"Failed to prepare an Iris tree-feller block.",
|
||||
() -> prepareBreak(run, member)
|
||||
);
|
||||
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
|
||||
if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareBreak(FellingRun run, TreeMember member) {
|
||||
Block block = liveMemberBlock(run, member);
|
||||
if (block == null) {
|
||||
if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!member.log()) {
|
||||
runMutationTask(
|
||||
run,
|
||||
member,
|
||||
new DamageReservation(run.expectedTool.clone(), false, false, false),
|
||||
new AtomicBoolean()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable task = () -> runTask(
|
||||
run,
|
||||
"Failed to reserve Iris tree-feller tool durability.",
|
||||
() -> reserveDamage(run, member)
|
||||
);
|
||||
if (!J.runEntity(run.candidate.player(), task)) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void reserveDamage(FellingRun run, TreeMember member) {
|
||||
Player player = run.candidate.player();
|
||||
if (!isRunControlActive(run, player)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
ItemStack current = inventory.getItem(run.heldSlot);
|
||||
if (current == null
|
||||
|| inventory.getHeldItemSlot() != run.heldSlot
|
||||
|| !current.isSimilar(run.expectedTool)
|
||||
|| !TreeFellerSVC.isAxe(current)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reserveLogCost(run)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack before = current.clone();
|
||||
ItemMeta meta = current.getItemMeta();
|
||||
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
|
||||
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
|
||||
return;
|
||||
}
|
||||
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
|
||||
refundAndFinish(run, new DamageReservation(before, false, false, true));
|
||||
return;
|
||||
}
|
||||
|
||||
int nextDamage = damageable.getDamage() + 1;
|
||||
boolean broke = nextDamage >= current.getType().getMaxDurability();
|
||||
if (broke) {
|
||||
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
|
||||
run.expectedTool = new ItemStack(Material.AIR);
|
||||
} else {
|
||||
damageable.setDamage(nextDamage);
|
||||
current.setItemMeta(meta);
|
||||
inventory.setItem(run.heldSlot, current);
|
||||
run.expectedTool = current.clone();
|
||||
}
|
||||
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
|
||||
}
|
||||
|
||||
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
|
||||
TreeMarkerTraversal.Position position = member.position();
|
||||
AtomicBoolean mutationSucceeded = new AtomicBoolean();
|
||||
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
|
||||
boolean scheduled;
|
||||
try {
|
||||
scheduled = J.runRegion(
|
||||
run.candidate.world(),
|
||||
position.x() >> 4,
|
||||
position.z() >> 4,
|
||||
task
|
||||
);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
if (!scheduled) {
|
||||
refundAndFinish(run, reservation);
|
||||
}
|
||||
}
|
||||
|
||||
private void runMutationTask(
|
||||
FellingRun run,
|
||||
TreeMember member,
|
||||
DamageReservation reservation,
|
||||
AtomicBoolean mutationSucceeded
|
||||
) {
|
||||
if (run.finished.get()) {
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
probeAndMutate(run, member, reservation, mutationSucceeded);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
|
||||
if (mutationSucceeded.get()) {
|
||||
finish(run);
|
||||
} else {
|
||||
refundAndFinish(run, reservation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void probeAndMutate(
|
||||
FellingRun run,
|
||||
TreeMember member,
|
||||
DamageReservation reservation,
|
||||
AtomicBoolean mutationSucceeded
|
||||
) {
|
||||
Block block = liveMemberBlock(run, member);
|
||||
if (block == null) {
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
|
||||
service.managedEvents.add(probe);
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(probe);
|
||||
} catch (Throwable error) {
|
||||
probe.setCancelled(true);
|
||||
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
|
||||
} finally {
|
||||
service.managedEvents.remove(probe);
|
||||
}
|
||||
|
||||
try {
|
||||
if (probe.isCancelled()) {
|
||||
if (reservation.charged() || reservation.logCostReserved()) {
|
||||
refundAndFinish(run, reservation);
|
||||
} else if (member.log()) {
|
||||
finish(run);
|
||||
} else {
|
||||
continueRun(run);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
block = liveMemberBlock(run, member);
|
||||
if (run.finished.get() || block == null) {
|
||||
probe.setCancelled(true);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
|
||||
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
|
||||
BlockData visualData = block.getBlockData().clone();
|
||||
List<ItemStack> vanillaDrops = probe.isDropItems()
|
||||
? block.getDrops(reservation.toolForDrops()).stream()
|
||||
.map(ItemStack::clone)
|
||||
.toList()
|
||||
: List.of();
|
||||
block.setType(Material.AIR, false);
|
||||
if (!block.getType().isAir()) {
|
||||
probe.setCancelled(true);
|
||||
refundAndFinish(run, reservation);
|
||||
return;
|
||||
}
|
||||
mutationSucceeded.set(true);
|
||||
run.presentation.erode(
|
||||
source,
|
||||
visualData,
|
||||
member.erosionOrder(),
|
||||
run.processed.get(),
|
||||
run.blocksPerPulse,
|
||||
run.effectStride,
|
||||
run.work.size()
|
||||
);
|
||||
|
||||
provenance.clearProvenance(
|
||||
run.candidate.context().engine(),
|
||||
run.candidate.context().minimumY(),
|
||||
member.position()
|
||||
);
|
||||
service.routeDrops(run, vanillaDrops, source);
|
||||
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
|
||||
service.dropExperience(source, probe.getExpToDrop());
|
||||
}
|
||||
if (reservation.logCostReserved()) {
|
||||
completeLogCost(run, reservation);
|
||||
return;
|
||||
}
|
||||
completeSuccessfulMutation(run, reservation);
|
||||
} catch (RuntimeException | Error error) {
|
||||
if (!mutationSucceeded.get()) {
|
||||
probe.setCancelled(true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private Block liveMemberBlock(FellingRun run, TreeMember member) {
|
||||
World world = run.candidate.world();
|
||||
TreeMarkerTraversal.Position position = member.position();
|
||||
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
|
||||
return null;
|
||||
}
|
||||
Block block = world.getBlockAt(position.x(), position.y(), position.z());
|
||||
TreeContext context = run.candidate.context();
|
||||
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
|
||||
return null;
|
||||
}
|
||||
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
|
||||
provenance.clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
|
||||
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
|
||||
provenance.clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
|
||||
provenance.clearProvenance(context.engine(), context.minimumY(), position);
|
||||
return null;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
|
||||
if (!reservation.charged() && !reservation.logCostReserved()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
if (!J.runEntity(run.candidate.player(), () -> {
|
||||
if (reservation.charged()) {
|
||||
PlayerInventory inventory = run.candidate.player().getInventory();
|
||||
ItemStack current = inventory.getItem(run.heldSlot);
|
||||
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
|
||||
boolean currentMatches = expectedAir
|
||||
? current == null || current.getType() == Material.AIR
|
||||
: current != null && current.isSimilar(run.expectedTool);
|
||||
if (currentMatches) {
|
||||
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
|
||||
run.expectedTool = reservation.toolForDrops().clone();
|
||||
}
|
||||
}
|
||||
if (reservation.logCostReserved()) {
|
||||
refundLogCost(run);
|
||||
}
|
||||
finish(run);
|
||||
})) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunControlActive(FellingRun run, Player player) {
|
||||
return player.isOnline()
|
||||
&& player.getGameMode() == GameMode.SURVIVAL
|
||||
&& player.isSneaking()
|
||||
&& player.getWorld().equals(run.candidate.world());
|
||||
}
|
||||
|
||||
private boolean reserveLogCost(FellingRun run) {
|
||||
try {
|
||||
return run.runHooks.reserveLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void completeLogCost(FellingRun run, DamageReservation reservation) {
|
||||
if (!J.runEntity(run.candidate.player(), () -> {
|
||||
try {
|
||||
run.runHooks.commitLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
completeSuccessfulMutation(run, reservation);
|
||||
})) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void refundLogCost(FellingRun run) {
|
||||
try {
|
||||
run.runHooks.refundLogCost();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
|
||||
if (reservation.broke()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
continueRun(run);
|
||||
}
|
||||
|
||||
private void continueRun(FellingRun run) {
|
||||
int processed = run.processed.incrementAndGet();
|
||||
if (processed % run.blocksPerPulse == 0) {
|
||||
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
|
||||
} else {
|
||||
processNext(run);
|
||||
}
|
||||
}
|
||||
|
||||
private void runTask(FellingRun run, String context, Runnable task) {
|
||||
if (run.finished.get() || !service.isServiceEnabled()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
task.run();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(context, error);
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
void finish(FellingRun run) {
|
||||
if (run.finished.compareAndSet(false, true)) {
|
||||
service.activeClaims.remove(run.claim);
|
||||
Set<FellingRun> runs = service.activeRuns.get(run.candidate.player().getUniqueId());
|
||||
if (runs != null) {
|
||||
runs.remove(run);
|
||||
if (runs.isEmpty()) {
|
||||
service.activeRuns.remove(run.candidate.player().getUniqueId(), runs);
|
||||
}
|
||||
}
|
||||
run.presentation.finish();
|
||||
}
|
||||
}
|
||||
|
||||
void finishRuns(UUID playerId) {
|
||||
Set<FellingRun> runs = service.activeRuns.get(playerId);
|
||||
if (runs == null) {
|
||||
return;
|
||||
}
|
||||
for (FellingRun run : List.copyOf(runs)) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
|
||||
List<TreeMarkerTraversal.Position> positions
|
||||
) {
|
||||
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
|
||||
for (TreeMarkerTraversal.Position position : positions) {
|
||||
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
|
||||
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.tree.TreeFellerAccess;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
|
||||
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
|
||||
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
|
||||
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class TreeProvenance {
|
||||
private final Map<Engine, TreeDefinitionIndex> definitions;
|
||||
|
||||
TreeProvenance(Map<Engine, TreeDefinitionIndex> definitions) {
|
||||
this.definitions = definitions;
|
||||
}
|
||||
|
||||
TreeCandidate resolveCandidate(Block block, Player player) {
|
||||
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
|
||||
return null;
|
||||
}
|
||||
if (!Tag.LOGS.isTagged(block.getType())) {
|
||||
return null;
|
||||
}
|
||||
ItemStack tool = player.getInventory().getItemInMainHand();
|
||||
if (!TreeFellerSVC.isAxe(tool)) {
|
||||
return null;
|
||||
}
|
||||
TreeContext context = resolveTreeContext(block);
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
return new TreeCandidate(
|
||||
context,
|
||||
block.getWorld(),
|
||||
player,
|
||||
tool.clone(),
|
||||
TreeFellerAccess.STANDALONE,
|
||||
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
|
||||
);
|
||||
}
|
||||
|
||||
TreeContext resolveTreeContext(Block block) {
|
||||
if (block.getType().isAir()) {
|
||||
return null;
|
||||
}
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
|
||||
if (access == null || access.getEngine() == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
World world = block.getWorld();
|
||||
int minimumY = world.getMinHeight();
|
||||
int maximumY = world.getMaxHeight();
|
||||
int relativeY = block.getY() - minimumY;
|
||||
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
if (decoded == null || decoded.structureAware()) {
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
|
||||
block.getX(),
|
||||
relativeY,
|
||||
block.getZ(),
|
||||
TreeBlockMaterial.class
|
||||
);
|
||||
if (expected != null && !matchesExpectedMaterial(block, expected)) {
|
||||
return null;
|
||||
}
|
||||
if (expected == null
|
||||
&& !decoded.objectKey().startsWith("trees/")
|
||||
&& !definitionIndex(engine).isTreeMarker(marker)) {
|
||||
return null;
|
||||
}
|
||||
return new TreeContext(engine, marker, expected, minimumY, maximumY);
|
||||
}
|
||||
|
||||
TreeDefinitionIndex definitionIndex(Engine engine) {
|
||||
synchronized (definitions) {
|
||||
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
|
||||
}
|
||||
}
|
||||
|
||||
String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
String markerAt(Engine engine, int minimumY, int x, int y, int z) {
|
||||
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
|
||||
}
|
||||
|
||||
TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
return engine.getMantle().getMantle().get(
|
||||
position.x(),
|
||||
position.y() - minimumY,
|
||||
position.z(),
|
||||
TreeBlockMaterial.class
|
||||
);
|
||||
}
|
||||
|
||||
ProvenanceSnapshot captureProvenance(Block block) {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
|
||||
if (access == null || access.getEngine() == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
|
||||
block.getX(),
|
||||
block.getY(),
|
||||
block.getZ()
|
||||
);
|
||||
int minimumY = block.getWorld().getMinHeight();
|
||||
String marker = markerAt(engine, minimumY, position);
|
||||
TreeBlockMaterial material = materialAt(engine, minimumY, position);
|
||||
if (marker == null && material == null) {
|
||||
return null;
|
||||
}
|
||||
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
|
||||
}
|
||||
|
||||
void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
|
||||
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
|
||||
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
|
||||
}
|
||||
}
|
||||
|
||||
void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
|
||||
int relativeY = position.y() - minimumY;
|
||||
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
|
||||
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
|
||||
}
|
||||
|
||||
boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
|
||||
return expected.matches(block.getBlockData().getAsString());
|
||||
}
|
||||
}
|
||||
@@ -361,11 +361,7 @@ public class WandSVC implements IrisService {
|
||||
wand = createWand();
|
||||
dust = createDust();
|
||||
|
||||
J.ar(() -> {
|
||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||
tick(i);
|
||||
}
|
||||
}, 0);
|
||||
J.ar(this::tickAll, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -373,6 +369,22 @@ public class WandSVC implements IrisService {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Async driver tick. The online player list is only read from the thread that owns it,
|
||||
* and every wand draw is dispatched to the thread owning that player.
|
||||
*/
|
||||
private void tickAll() {
|
||||
try {
|
||||
J.runGlobal(() -> {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
J.runEntity(p, () -> tick(p));
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void tick(Player p) {
|
||||
try {
|
||||
try {
|
||||
|
||||
+5
-5
@@ -22,9 +22,9 @@ public final class IrisColumnWalk {
|
||||
int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4);
|
||||
int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15);
|
||||
|
||||
for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
|
||||
for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
|
||||
if (!visitor.visit(blockX, blockZ)) {
|
||||
for (long blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
|
||||
for (long blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
|
||||
if (!visitor.visit((int) blockX, (int) blockZ)) {
|
||||
return visited;
|
||||
}
|
||||
visited++;
|
||||
@@ -36,10 +36,10 @@ public final class IrisColumnWalk {
|
||||
return visited;
|
||||
}
|
||||
|
||||
private static int align(int origin, int lowerBound, int stride) {
|
||||
private static long align(int origin, int lowerBound, int stride) {
|
||||
long offset = (long) lowerBound - (long) origin;
|
||||
long steps = (offset + stride - 1L) / stride;
|
||||
return (int) (origin + steps * stride);
|
||||
return origin + steps * stride;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -41,6 +41,10 @@ public class WandSelection {
|
||||
|
||||
public void draw() {
|
||||
Location playerLoc = p.getLocation();
|
||||
if (c.getWorld() == null || !c.getWorld().equals(playerLoc.getWorld())) {
|
||||
return;
|
||||
}
|
||||
|
||||
double maxDistanceSquared = 256 * 256;
|
||||
int particleCount = 0;
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ public class TreeFellerEventOrderTest {
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
|
||||
assertEquals(List.of(trigger), TreeFellingRunner.positionsForFelling(incomplete, trigger));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user