mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Improve generation and lifecycle safety
- Reduce terrain, noise, hydrology, and decorator sampling overhead. - Reduce object transaction allocations while preserving rollback behavior. - Optimize structure placement and cave anchor sampling. - Parallelize Studio cache warmup and protect world entry and hotload recovery. - Validate pack fingerprints and publish pack replacements atomically. - Notify modded operators about outdated packs without synchronizing pack data. - Harden world creation, loading, evacuation, and teleport failure recovery. - Coalesce queued region flushes while preserving chunk save ordering. - Remove obsolete mantle, pack, and cave planning code. - Add regression tests and reusable gameplay fixtures.
This commit is contained in:
1 parent
a2bcd7a302
commit
7f5ce70f49
197 files changed
+7560
-4420
No files matched your search
+1
-1
@@ -50,7 +50,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
|
||||
int xx = rX + dx;
|
||||
int yy = y + dy;
|
||||
int zz = rZ + dz;
|
||||
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()
|
||||
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()
|
||||
? null : hunk.get(xx, yy, zz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
|
||||
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
|
||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||
@@ -477,6 +478,7 @@ public final class ModdedEngineBootstrap {
|
||||
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
createdServices.register(ModdedPackUpdateService.class, new ModdedPackUpdateService());
|
||||
ModdedStudioHotloadService studioHotloadService = createdServices.register(
|
||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Owns the Iris generation pool and the single decision of whether this loader already runs chunk
|
||||
@@ -43,7 +42,7 @@ public final class ModdedGenPool {
|
||||
private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L;
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem();
|
||||
private static final AtomicReference<ExecutorService> GEN_POOL = new AtomicReference<>(createGenPool());
|
||||
private static ExecutorService generationPool = createGenPool();
|
||||
|
||||
private ModdedGenPool() {
|
||||
}
|
||||
@@ -63,39 +62,32 @@ public final class ModdedGenPool {
|
||||
return CHUNK_SYSTEM.description();
|
||||
}
|
||||
|
||||
static ExecutorService pool() {
|
||||
ExecutorService pool = GEN_POOL.get();
|
||||
if (pool != null && !pool.isShutdown()) {
|
||||
return pool;
|
||||
}
|
||||
start();
|
||||
ExecutorService restarted = GEN_POOL.get();
|
||||
if (restarted == null) {
|
||||
static synchronized ExecutorService pool() {
|
||||
if (generationPool.isShutdown()) {
|
||||
throw new RejectedExecutionException("Iris gen pool is shut down");
|
||||
}
|
||||
return restarted;
|
||||
return generationPool;
|
||||
}
|
||||
|
||||
static void start() {
|
||||
while (true) {
|
||||
ExecutorService current = GEN_POOL.get();
|
||||
if (current != null && !current.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
ExecutorService created = createGenPool();
|
||||
if (GEN_POOL.compareAndSet(current, created)) {
|
||||
return;
|
||||
}
|
||||
created.shutdownNow();
|
||||
static synchronized void start() {
|
||||
if (!generationPool.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
if (!generationPool.isTerminated()) {
|
||||
throw new IllegalStateException("Iris gen pool cannot restart while prior workers are still active");
|
||||
}
|
||||
generationPool = createGenPool();
|
||||
}
|
||||
|
||||
static void shutdown() {
|
||||
ExecutorService pool = GEN_POOL.getAndSet(null);
|
||||
if (pool == null) {
|
||||
return;
|
||||
ExecutorService pool;
|
||||
synchronized (ModdedGenPool.class) {
|
||||
pool = generationPool;
|
||||
if (pool.isTerminated()) {
|
||||
return;
|
||||
}
|
||||
pool.shutdown();
|
||||
}
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) {
|
||||
return;
|
||||
|
||||
@@ -20,13 +20,11 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashRenderer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class ModdedIrisSplash {
|
||||
|
||||
@@ -34,18 +32,11 @@ public final class ModdedIrisSplash {
|
||||
}
|
||||
|
||||
public static void print(NativeModdedLoader loader) {
|
||||
printPacks(loader);
|
||||
if (isLogoEnabled()) {
|
||||
printLogo(loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printPacks(NativeModdedLoader loader) {
|
||||
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
|
||||
IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)
|
||||
.thenAccept(lines -> lines.forEach(IrisLogging::info));
|
||||
}
|
||||
|
||||
private static void printLogo(NativeModdedLoader loader) {
|
||||
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
|
||||
String[] splash = IrisSplashRenderer.renderPlain();
|
||||
|
||||
+53
-32
@@ -27,16 +27,15 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public final class ModdedPrimaryWorldRouter {
|
||||
private static final int TICK_INTERVAL = 20;
|
||||
|
||||
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
|
||||
private static final ConcurrentMap<UUID, CompletableFuture<Boolean>> routes = new ConcurrentHashMap<>();
|
||||
private static int tickCounter = 0;
|
||||
private static volatile String loginRefusal;
|
||||
private static volatile String absentPrimary;
|
||||
@@ -45,8 +44,12 @@ public final class ModdedPrimaryWorldRouter {
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
routed.clear();
|
||||
inFlight.clear();
|
||||
routes.forEach((id, route) -> {
|
||||
if (routes.remove(id, route)) {
|
||||
route.cancel(false);
|
||||
}
|
||||
});
|
||||
tickCounter = 0;
|
||||
loginRefusal = null;
|
||||
absentPrimary = null;
|
||||
}
|
||||
@@ -57,8 +60,10 @@ public final class ModdedPrimaryWorldRouter {
|
||||
*/
|
||||
public static void forget(UUID player) {
|
||||
if (player != null) {
|
||||
routed.remove(player);
|
||||
inFlight.remove(player);
|
||||
CompletableFuture<Boolean> route = routes.remove(player);
|
||||
if (route != null) {
|
||||
route.cancel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,36 +115,52 @@ public final class ModdedPrimaryWorldRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
server.forEachPlayer(player -> {
|
||||
UUID id = player.id();
|
||||
if (routed.contains(id) || !inFlight.add(id)) {
|
||||
return;
|
||||
server.forEachPlayer(player -> routePlayer(server, player, overworld, target, primary));
|
||||
}
|
||||
|
||||
private static void routePlayer(NativeModdedServer server, NativeProtocolPlayer player,
|
||||
NativeWorld overworld, NativeWorld target, String primary) {
|
||||
UUID id = player.id();
|
||||
if (routes.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<Boolean> route = new CompletableFuture<>();
|
||||
if (routes.putIfAbsent(id, route) != null) {
|
||||
return;
|
||||
}
|
||||
route.whenComplete((success, failure) -> {
|
||||
if (!Boolean.TRUE.equals(success)) {
|
||||
routes.remove(id, route);
|
||||
}
|
||||
if (!player.isInWorld(overworld)) {
|
||||
inFlight.remove(id);
|
||||
routed.add(id);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
|
||||
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
|
||||
ModdedTeleportDeadline.fromNow()));
|
||||
teleport.whenComplete((success, failure) -> {
|
||||
inFlight.remove(id);
|
||||
if (Boolean.TRUE.equals(success) && failure == null) {
|
||||
routed.add(id);
|
||||
return;
|
||||
}
|
||||
if (failure != null) {
|
||||
});
|
||||
if (!player.isInWorld(overworld)) {
|
||||
route.complete(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
|
||||
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
|
||||
ModdedTeleportDeadline.fromNow()));
|
||||
route.whenComplete((success, failure) -> {
|
||||
if (!Boolean.TRUE.equals(success) && !teleport.isDone()) {
|
||||
teleport.cancel(false);
|
||||
}
|
||||
});
|
||||
teleport.whenComplete((success, failure) -> {
|
||||
if (failure != null) {
|
||||
if (route.completeExceptionally(failure)) {
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
|
||||
id, primary, failure);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
inFlight.remove(id);
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
||||
} else {
|
||||
route.complete(Boolean.TRUE.equals(success));
|
||||
}
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
if (route.completeExceptionally(failure)) {
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.platform.protocol.IrisVisionRequestService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -128,6 +129,10 @@ public final class ModdedProtocolHandler {
|
||||
}
|
||||
String sessionId = sessionId(player);
|
||||
ModdedStartup.warnStartupFailuresTo(player);
|
||||
ModdedPackUpdateService packUpdates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||
if (packUpdates != null) {
|
||||
packUpdates.notifyPlayer(player);
|
||||
}
|
||||
IrisSessionRegistry current = registry;
|
||||
ModdedProtocolTransport currentTransport = transport;
|
||||
if (current == null || currentTransport == null) {
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackDownloader;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||
@@ -31,15 +34,11 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedStartup {
|
||||
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
||||
@@ -60,36 +59,9 @@ public final class ModdedStartup {
|
||||
if (!PREPARED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reportLegacyPacksDirectory();
|
||||
validateAllPacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
|
||||
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
|
||||
* the legacy directory holds packs, and quietly remove it when it is empty.
|
||||
*/
|
||||
private static void reportLegacyPacksDirectory() {
|
||||
try {
|
||||
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
|
||||
if (!legacy.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
|
||||
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
|
||||
ModdedIrisLog.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
|
||||
legacy.getAbsolutePath(), real.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
String[] entries = legacy.list();
|
||||
if (entries == null || entries.length == 0) {
|
||||
legacy.delete();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.debug("Iris legacy packs directory check failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
||||
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
||||
@@ -140,8 +112,7 @@ public final class ModdedStartup {
|
||||
}
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
PackValidationResult result = validatePack(packDir);
|
||||
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
||||
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
||||
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
|
||||
@@ -184,16 +155,15 @@ public final class ModdedStartup {
|
||||
throw new BrokenPackException(pack, List.of(
|
||||
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
|
||||
}
|
||||
PackValidationResult cached = PackValidationRegistry.get(pack);
|
||||
if (cached != null && cached.getValidatedAtMillis() >= newestModificationMillis(packDir.toPath())) {
|
||||
// prepareForStartup already validated this pack and nothing in it changed since; re-validating per
|
||||
// persistent dimension at boot costs a full pack parse each time.
|
||||
return PackValidationRegistry.requireLoadable(pack);
|
||||
}
|
||||
Path packRoot = packDir.toPath();
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
return PackValidationRegistry.requireLoadable(pack);
|
||||
PackValidationResult cached = PackValidationRegistry.getMatching(
|
||||
packRoot, PackFingerprints.computePackTreeFingerprint(packDir));
|
||||
if (cached != null) {
|
||||
return PackValidationRegistry.requireLoadable(packRoot);
|
||||
}
|
||||
validatePack(packDir);
|
||||
return PackValidationRegistry.requireLoadable(packRoot);
|
||||
} catch (BrokenPackException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
@@ -212,6 +182,42 @@ public final class ModdedStartup {
|
||||
}
|
||||
}
|
||||
|
||||
public static PackValidationResult validatePack(File packDir) {
|
||||
Path packRoot = packDir.toPath();
|
||||
PackValidationRegistry.ValidationTicket ticket = PackValidationRegistry.tryBeginValidation(packRoot);
|
||||
if (ticket == null) {
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content is currently being changed."));
|
||||
}
|
||||
String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packDir);
|
||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
PackValidationResult result;
|
||||
try {
|
||||
result = PackValidator.validate(packDir);
|
||||
} catch (Throwable failure) {
|
||||
ModdedIrisLog.error("Iris pack validation failed for '{}'", packDir.getName(), failure);
|
||||
String detail = failure.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = failure.getClass().getSimpleName();
|
||||
}
|
||||
result = new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack validation failed with " + failure.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packDir))
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
PackValidationRegistry.remove(packRoot);
|
||||
PackValidationRegistry.remove(packDir.getName());
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content or validation context changed during validation."));
|
||||
}
|
||||
if (!PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content changed during validation."));
|
||||
}
|
||||
PackValidationRegistry.publish(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
||||
ModdedDimensionRegistryStore.loadForStartup(server);
|
||||
@@ -266,23 +272,6 @@ public final class ModdedStartup {
|
||||
+ GenerationRefusalNotice.summary(failure));
|
||||
}
|
||||
|
||||
private static long newestModificationMillis(Path root) {
|
||||
long newest = 0L;
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
for (Path path : (Iterable<Path>) walk::iterator) {
|
||||
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
long modified = attributes.lastModifiedTime().toMillis();
|
||||
if (modified > newest) {
|
||||
newest = modified;
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException unreadable) {
|
||||
ModdedIrisLog.debug("Iris could not stat {} for validation reuse; revalidating", root, unreadable);
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
/**
|
||||
* True once the first tick with a player list has re-injected the persistent dimensions. Before that a runtime
|
||||
* dimension that is not loaded yet is still on its way.
|
||||
@@ -296,7 +285,7 @@ public final class ModdedStartup {
|
||||
* the console. Tell the operators who can actually act on it when they join.
|
||||
*/
|
||||
public static void warnStartupFailuresTo(NativeProtocolPlayer player) {
|
||||
if (player == null || !player.isGameMaster()) {
|
||||
if (player == null || (!player.isGameMaster() && !player.isServerOwner())) {
|
||||
return;
|
||||
}
|
||||
for (String failure : DIMENSION_FAILURES) {
|
||||
|
||||
+7
@@ -33,6 +33,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
@@ -445,6 +446,12 @@ public final class IrisModdedCommands {
|
||||
);
|
||||
String completionMessage = downloadCompletionMessage(result);
|
||||
if (result != null) {
|
||||
if (result.changed()) {
|
||||
ModdedPackUpdateService updates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||
if (updates != null) {
|
||||
dispatchDownloadFeedback(source, updates::refresh);
|
||||
}
|
||||
}
|
||||
if (completionMessage != null) {
|
||||
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
|
||||
}
|
||||
|
||||
+3
-3
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.modded.ModdedStartup;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -25,7 +27,6 @@ import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
@@ -138,8 +139,7 @@ public final class ModdedPackCommands {
|
||||
int broken = 0;
|
||||
for (File target : targets) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(target);
|
||||
PackValidationRegistry.publish(result);
|
||||
PackValidationResult result = ModdedStartup.validatePack(target);
|
||||
if (!result.isLoadable()) {
|
||||
broken++;
|
||||
}
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
|
||||
import art.arcane.iris.pack.BuiltInPackUpdates;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class ModdedPackUpdateService implements ModdedTickableService {
|
||||
private final Function<File, CompletableFuture<PackCheck>> checker;
|
||||
private final Set<UUID> notified = new HashSet<>();
|
||||
private CompletableFuture<PackCheck> pending;
|
||||
private List<String> notices = List.of();
|
||||
private File packsFolder;
|
||||
private boolean enabled;
|
||||
private boolean started;
|
||||
|
||||
public ModdedPackUpdateService() {
|
||||
this(ModdedPackUpdateService::checkPacks);
|
||||
}
|
||||
|
||||
ModdedPackUpdateService(Function<File, CompletableFuture<PackCheck>> checker) {
|
||||
this.checker = checker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
enabled = false;
|
||||
if (pending != null) {
|
||||
pending.cancel(true);
|
||||
pending = null;
|
||||
}
|
||||
started = false;
|
||||
notices = List.of();
|
||||
notified.clear();
|
||||
packsFolder = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(NativeModdedServer server) {
|
||||
if (!enabled || server == null || !server.hasPlayerList()) {
|
||||
return;
|
||||
}
|
||||
if (!started) {
|
||||
started = true;
|
||||
packsFolder = IrisPlatforms.get().packsFolderNoCreate().getAbsoluteFile();
|
||||
pending = checker.apply(packsFolder);
|
||||
}
|
||||
if (pending == null || !pending.isDone()) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<PackCheck> completed = pending;
|
||||
pending = null;
|
||||
try {
|
||||
publish(completed.join());
|
||||
server.forEachPlayer(this::notifyPlayer);
|
||||
} catch (CompletionException error) {
|
||||
IrisLogging.reportError("Iris pack update check failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (pending != null) {
|
||||
pending.cancel(true);
|
||||
pending = null;
|
||||
}
|
||||
started = false;
|
||||
notices = List.of();
|
||||
notified.clear();
|
||||
}
|
||||
|
||||
public void notifyPlayer(NativeProtocolPlayer player) {
|
||||
if (!enabled || notices.isEmpty() || player == null || !player.connected()
|
||||
|| (!player.isGameMaster() && !player.isServerOwner()) || !notified.add(player.id())) {
|
||||
return;
|
||||
}
|
||||
for (String notice : notices) {
|
||||
player.sendMessage(notice);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<PackCheck> checkPacks(File folder) {
|
||||
return CompletableFuture.supplyAsync(() -> IrisSplashPackScanner.collect(folder, IrisLogging::reportError))
|
||||
.thenCompose(packs -> BuiltInPackUpdates.check(packs.stream().map(SplashPackMetadata::name).toList())
|
||||
.thenApply(updates -> new PackCheck(packs, updates)));
|
||||
}
|
||||
|
||||
private void publish(PackCheck result) {
|
||||
List<String> updates = new ArrayList<>();
|
||||
IrisLogging.info("Iris packs: " + packsFolder);
|
||||
for (SplashPackMetadata pack : result.packs()) {
|
||||
BuiltInPackUpdates.Update update = result.updates().get(pack.name());
|
||||
String version = pack.version().matches("[0-9]+") ? "v" + pack.version() : "version unknown";
|
||||
IrisLogging.info(" " + pack.name() + " " + version
|
||||
+ (update == null ? "" : update.suffix(pack.version())));
|
||||
if (update != null && update.newerThan(pack.version())) {
|
||||
updates.add("Iris " + pack.name() + " release metadata is outdated: installed v" + pack.version()
|
||||
+ ", latest built-in release v" + update.version() + ".");
|
||||
updates.add("Back up and review custom edits in " + new File(packsFolder, pack.name())
|
||||
+ "; close Studio, then update with /iris download pack=" + pack.name() + " overwrite=true.");
|
||||
updates.add("Existing worlds retain their recorded production pack; this updates the authoring pack for new worlds.");
|
||||
}
|
||||
}
|
||||
notices = List.copyOf(updates);
|
||||
for (String notice : notices) {
|
||||
IrisLogging.warn(notice);
|
||||
}
|
||||
}
|
||||
|
||||
record PackCheck(List<SplashPackMetadata> packs, Map<String, BuiltInPackUpdates.Update> updates) {
|
||||
PackCheck {
|
||||
packs = List.copyOf(packs);
|
||||
updates = Map.copyOf(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-6
@@ -57,6 +57,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
|
||||
@@ -64,6 +65,8 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private static final long POLL_MILLIS = 250L;
|
||||
private static final long CHECK_LATCH_MILLIS = 1_000L;
|
||||
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
|
||||
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
|
||||
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
|
||||
|
||||
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
|
||||
private volatile ExecutorService executor;
|
||||
@@ -81,8 +84,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (executor != null) {
|
||||
return;
|
||||
ExecutorService current = executor;
|
||||
if (current != null && !current.isTerminated()) {
|
||||
if (!current.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Iris Studio hotload cannot restart while prior workers are still active");
|
||||
}
|
||||
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
|
||||
Thread thread = new Thread(task, "Iris Studio Hotload");
|
||||
@@ -96,10 +103,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = executor;
|
||||
executor = null;
|
||||
if (active != null) {
|
||||
active.shutdownNow();
|
||||
if (!shutdownAndDrain(active)) {
|
||||
throw new IllegalStateException("Iris Studio hotload worker remained active during service shutdown");
|
||||
}
|
||||
executor = null;
|
||||
for (Watch watch : watches.values()) {
|
||||
watch.close();
|
||||
}
|
||||
@@ -197,7 +204,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
@Override
|
||||
public void onServerTick(NativeModdedServer server) {
|
||||
ExecutorService active = executor;
|
||||
if (active == null) {
|
||||
if (active == null || active.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -284,6 +291,30 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
private static boolean shutdownAndDrain(ExecutorService active) {
|
||||
if (active == null) {
|
||||
return true;
|
||||
}
|
||||
active.shutdown();
|
||||
try {
|
||||
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
active.shutdownNow();
|
||||
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
ModdedIrisLog.error("Iris Studio hotload worker did not stop after shutdownNow",
|
||||
new IllegalStateException("Iris Studio hotload worker is still active"));
|
||||
return false;
|
||||
} catch (InterruptedException exception) {
|
||||
active.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
ModdedIrisLog.error("Interrupted while draining Iris Studio hotload", exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
||||
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user