mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-22 14:50:47 +00:00
dd
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
final class InitialSpawnQueue {
|
||||
private static final long DEFAULT_MAX_AGE_NANOS = TimeUnit.MINUTES.toNanos(5);
|
||||
|
||||
private final int capacity;
|
||||
private final long maxAgeNanos;
|
||||
private final LongSupplier nanoTime;
|
||||
private final ArrayDeque<Long> queue;
|
||||
private final Map<Long, Long> pending;
|
||||
private final Set<Long> queued;
|
||||
private boolean closed;
|
||||
|
||||
InitialSpawnQueue(int capacity) {
|
||||
this(capacity, DEFAULT_MAX_AGE_NANOS, System::nanoTime);
|
||||
}
|
||||
|
||||
InitialSpawnQueue(int capacity, long maxAgeNanos, LongSupplier nanoTime) {
|
||||
if (capacity < 1) {
|
||||
throw new IllegalArgumentException("capacity must be positive");
|
||||
}
|
||||
if (maxAgeNanos < 1L) {
|
||||
throw new IllegalArgumentException("maxAgeNanos must be positive");
|
||||
}
|
||||
this.capacity = capacity;
|
||||
this.maxAgeNanos = maxAgeNanos;
|
||||
this.nanoTime = nanoTime;
|
||||
this.queue = new ArrayDeque<>(Math.min(capacity, 256));
|
||||
this.pending = new HashMap<>();
|
||||
this.queued = new HashSet<>();
|
||||
}
|
||||
|
||||
synchronized boolean offer(long key) {
|
||||
if (closed || pending.containsKey(key)) {
|
||||
return false;
|
||||
}
|
||||
if (pending.size() >= capacity) {
|
||||
expire(nanoTime.getAsLong());
|
||||
}
|
||||
if (pending.size() >= capacity) {
|
||||
return false;
|
||||
}
|
||||
pending.put(key, nanoTime.getAsLong());
|
||||
queued.add(key);
|
||||
queue.addLast(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized int batchSize(int limit) {
|
||||
if (closed || limit <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(limit, queue.size());
|
||||
}
|
||||
|
||||
synchronized Long poll() {
|
||||
long now = nanoTime.getAsLong();
|
||||
Long key;
|
||||
while ((key = queue.pollFirst()) != null) {
|
||||
queued.remove(key);
|
||||
Long offeredAt = pending.get(key);
|
||||
if (offeredAt == null) {
|
||||
continue;
|
||||
}
|
||||
if (expired(offeredAt, now)) {
|
||||
pending.remove(key);
|
||||
continue;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized void retry(long key) {
|
||||
Long offeredAt = pending.get(key);
|
||||
if (closed || offeredAt == null || expired(offeredAt, nanoTime.getAsLong())) {
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
return;
|
||||
}
|
||||
if (!queued.add(key)) {
|
||||
return;
|
||||
}
|
||||
queue.addLast(key);
|
||||
}
|
||||
|
||||
synchronized void complete(long key) {
|
||||
pending.remove(key);
|
||||
if (queued.remove(key)) {
|
||||
queue.removeFirstOccurrence(key);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean isEmpty() {
|
||||
return queue.isEmpty();
|
||||
}
|
||||
|
||||
synchronized int size() {
|
||||
return pending.size();
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
queue.clear();
|
||||
pending.clear();
|
||||
queued.clear();
|
||||
}
|
||||
|
||||
synchronized void close() {
|
||||
closed = true;
|
||||
clear();
|
||||
}
|
||||
|
||||
private void expire(long now) {
|
||||
Set<Long> expired = new HashSet<>();
|
||||
for (Map.Entry<Long, Long> entry : pending.entrySet()) {
|
||||
if (expired(entry.getValue(), now)) {
|
||||
expired.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
if (expired.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long key : expired) {
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
}
|
||||
queue.removeIf(expired::contains);
|
||||
}
|
||||
|
||||
private boolean expired(long offeredAt, long now) {
|
||||
return now - offeredAt >= maxAgeNanos;
|
||||
}
|
||||
}
|
||||
+90
@@ -20,6 +20,8 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -38,6 +40,8 @@ import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -49,6 +53,7 @@ import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
@@ -65,6 +70,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -136,8 +142,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private final String defaultPack;
|
||||
private final String defaultDimensionKey;
|
||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile Engine engine;
|
||||
private volatile String activePack;
|
||||
private volatile String activeDimensionKey;
|
||||
@@ -166,6 +175,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
@@ -177,6 +187,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
@@ -259,6 +270,82 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public void onHotload() {
|
||||
biomeHolders.clear();
|
||||
missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
||||
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||
biome, structureManager, category, pos);
|
||||
if (resolvedSpawns != explicitSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
|
||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
|
||||
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
|
||||
}
|
||||
|
||||
private synchronized void resetVanillaSpawnBiomes() {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -491,4 +578,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
|
||||
info.add("Iris dimension: " + dimensionKey);
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedEntityType implements PlatformEntityType {
|
||||
@@ -51,6 +52,11 @@ public final class ModdedEntityType implements PlatformEntityType {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String spawnCategory() {
|
||||
return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return type;
|
||||
|
||||
@@ -88,8 +88,8 @@ public final class ModdedStartup {
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
LOGGER.error(" - {}", reason);
|
||||
}
|
||||
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
|
||||
LOGGER.info("Iris pack '{}' validated ({} unused file(s) quarantined to .iris-trash/, {} warning(s)).", result.getPackName(), result.getRemovedUnusedFiles().size(), result.getWarnings().size());
|
||||
} else if (!result.getWarnings().isEmpty()) {
|
||||
LOGGER.info("Iris pack '{}' validated ({} warning(s)).", result.getPackName(), result.getWarnings().size());
|
||||
for (String warning : result.getWarnings()) {
|
||||
LOGGER.warn(" [{}] {}", result.getPackName(), warning);
|
||||
}
|
||||
|
||||
@@ -40,49 +40,85 @@ import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedWorldCheck {
|
||||
private static final int EXIT_PASS = 0;
|
||||
private static final int EXIT_FAILURE = 1;
|
||||
private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L;
|
||||
private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final ProcessExit PROCESS_EXIT = Runtime.getRuntime()::exit;
|
||||
|
||||
private ModdedWorldCheck() {
|
||||
}
|
||||
|
||||
public static void schedule() {
|
||||
Thread thread = new Thread(ModdedWorldCheck::waitAndRun, "Iris World Check");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
coordinatorThread(() -> waitAndRun(PROCESS_EXIT)).start();
|
||||
}
|
||||
|
||||
private static void waitAndRun() {
|
||||
static Thread coordinatorThread(Runnable coordinator) {
|
||||
Thread thread = new Thread(coordinator, "Iris World Check");
|
||||
thread.setDaemon(false);
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static void waitAndRun(ProcessExit processExit) {
|
||||
long start = System.currentTimeMillis();
|
||||
MinecraftServer server = null;
|
||||
while (System.currentTimeMillis() - start < 600000L) {
|
||||
MinecraftServer candidate = ModdedEngineBootstrap.currentServer();
|
||||
if (candidate != null && candidate.isReady()) {
|
||||
server = candidate;
|
||||
break;
|
||||
boolean ready = false;
|
||||
int exitCode = EXIT_FAILURE;
|
||||
try {
|
||||
while (System.currentTimeMillis() - start < SERVER_WAIT_TIMEOUT_MILLIS) {
|
||||
MinecraftServer candidate = ModdedEngineBootstrap.currentServer();
|
||||
if (candidate != null) {
|
||||
server = candidate;
|
||||
if (candidate.isReady()) {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Thread.sleep(SERVER_WAIT_INTERVAL_MILLIS);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(250L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
if (!ready) {
|
||||
LOGGER.error("[worldcheck] server did not become ready within 10 minutes");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (server == null) {
|
||||
LOGGER.error("[worldcheck] server did not become ready within 10 minutes");
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftServer serverRef = server;
|
||||
AtomicBoolean pass = new AtomicBoolean(false);
|
||||
try {
|
||||
MinecraftServer serverRef = server;
|
||||
AtomicBoolean pass = new AtomicBoolean(false);
|
||||
serverRef.submit(() -> pass.set(run(serverRef))).join();
|
||||
exitCode = pass.get() ? EXIT_PASS : EXIT_FAILURE;
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error("[worldcheck] coordinator interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("[worldcheck] check failed", e);
|
||||
} finally {
|
||||
int resultCode = exitCode;
|
||||
LOGGER.info("[worldcheck] shutting down dev server (result={})", resultCode == EXIT_PASS ? "PASS" : "FAIL");
|
||||
MinecraftServer serverRef = server;
|
||||
stopAndExit(serverRef == null ? null : () -> serverRef.halt(true), resultCode, processExit);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("[worldcheck] shutting down dev server (result={})", pass.get() ? "PASS" : "FAIL");
|
||||
serverRef.halt(false);
|
||||
static void stopAndExit(Runnable serverStop, int requestedExitCode, ProcessExit processExit) {
|
||||
int exitCode = requestedExitCode;
|
||||
boolean interrupted = Thread.interrupted();
|
||||
if (interrupted) {
|
||||
exitCode = EXIT_FAILURE;
|
||||
}
|
||||
try {
|
||||
if (serverStop != null) {
|
||||
serverStop.run();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
exitCode = EXIT_FAILURE;
|
||||
LOGGER.error("[worldcheck] server shutdown failed", e);
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
processExit.exit(exitCode);
|
||||
}
|
||||
|
||||
private static boolean run(MinecraftServer server) {
|
||||
@@ -186,4 +222,9 @@ public final class ModdedWorldCheck {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ProcessExit {
|
||||
void exit(int status);
|
||||
}
|
||||
}
|
||||
|
||||
+138
-29
@@ -57,88 +57,167 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ModdedWorldManager implements EngineWorldManager {
|
||||
private static final ConcurrentHashMap<Engine, Queue<Long>> INITIAL_QUEUES = new ConcurrentHashMap<>();
|
||||
private static final int MAX_INITIAL_QUEUE = 8192;
|
||||
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
|
||||
private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128;
|
||||
private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256;
|
||||
private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L;
|
||||
private static final long COUNT_INTERVAL_MS = 3_000L;
|
||||
private static final int ENTITY_SCAN_RADIUS = 64;
|
||||
private static final int PLAYER_CHUNK_RADIUS = 4;
|
||||
private static final int MIN_TICK_INTERVAL_MS = 1_000;
|
||||
|
||||
private final Engine engine;
|
||||
private final InitialSpawnQueue initialSpawnQueue;
|
||||
private final Set<Long> mantleWarmups;
|
||||
private final ThreadPoolExecutor mantleWarmupExecutor;
|
||||
private long lastAmbientAt;
|
||||
private long lastCountAt;
|
||||
private long lastInitialRecoveryAt;
|
||||
private volatile boolean closed;
|
||||
private volatile int cachedEntityCount;
|
||||
private volatile int cachedConsideredChunks;
|
||||
private volatile double cachedSaturation;
|
||||
|
||||
public ModdedWorldManager(Engine engine) {
|
||||
this.engine = engine;
|
||||
INITIAL_QUEUES.putIfAbsent(engine, new ConcurrentLinkedQueue<>());
|
||||
this.initialSpawnQueue = new InitialSpawnQueue(MAX_INITIAL_QUEUE);
|
||||
this.mantleWarmups = ConcurrentHashMap.newKeySet();
|
||||
BlockingQueue<Runnable> warmupQueue = new ArrayBlockingQueue<>(MANTLE_WARMUP_QUEUE_CAPACITY);
|
||||
this.mantleWarmupExecutor = new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
30L,
|
||||
TimeUnit.SECONDS,
|
||||
warmupQueue,
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Initial Spawn Mantle Warmup");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
},
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
this.mantleWarmupExecutor.allowCoreThreadTimeOut(true);
|
||||
}
|
||||
|
||||
public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) {
|
||||
if (engine == null) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return;
|
||||
}
|
||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
||||
if (queue == null || queue.size() >= MAX_INITIAL_QUEUE) {
|
||||
return;
|
||||
EngineWorldManager worldManager = engine.getWorldManager();
|
||||
if (worldManager instanceof ModdedWorldManager moddedWorldManager) {
|
||||
moddedWorldManager.initialSpawnQueue.offer(pack(chunkX, chunkZ));
|
||||
}
|
||||
queue.add(pack(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
public void serverTick(ServerLevel level) {
|
||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (isPregenActive()) {
|
||||
return;
|
||||
}
|
||||
recoverLoadedInitialSpawns(level);
|
||||
drainInitialSpawns(level);
|
||||
ambientTick(level);
|
||||
}
|
||||
|
||||
private void drainInitialSpawns(ServerLevel level) {
|
||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
||||
if (queue == null || queue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
private void recoverLoadedInitialSpawns(ServerLevel level) {
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
queue.clear();
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastInitialRecoveryAt < INITIAL_RECOVERY_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastInitialRecoveryAt = now;
|
||||
|
||||
int budget = MAX_INITIAL_DRAIN_PER_TICK;
|
||||
while (budget-- > 0) {
|
||||
Long key = queue.poll();
|
||||
if (key == null) {
|
||||
return;
|
||||
Set<Long> candidates = new HashSet<>();
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int centerX = player.blockPosition().getX() >> 4;
|
||||
int centerZ = player.blockPosition().getZ() >> 4;
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
candidates.add(pack(centerX + dx, centerZ + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.addAll(level.getForceLoadedChunks());
|
||||
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
int recovered = 0;
|
||||
for (long key : candidates) {
|
||||
int chunkX = unpackX(key);
|
||||
int chunkZ = unpackZ(key);
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
initialSpawnChunk(level, chunkX, chunkZ);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
continue;
|
||||
}
|
||||
if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ) || mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
private void drainInitialSpawns(ServerLevel level) {
|
||||
if (initialSpawnQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
initialSpawnQueue.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int budget = initialSpawnQueue.batchSize(MAX_INITIAL_DRAIN_PER_TICK);
|
||||
while (budget-- > 0) {
|
||||
Long key = initialSpawnQueue.poll();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
int chunkX = unpackX(key);
|
||||
int chunkZ = unpackZ(key);
|
||||
boolean retry = false;
|
||||
try {
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||
retry = true;
|
||||
continue;
|
||||
}
|
||||
if (!initialSpawnChunk(level, chunkX, chunkZ)) {
|
||||
retry = true;
|
||||
warmupMantleChunkAsync(key, chunkX, chunkZ);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
retry = true;
|
||||
} finally {
|
||||
if (retry) {
|
||||
initialSpawnQueue.retry(key);
|
||||
} else {
|
||||
initialSpawnQueue.complete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return false;
|
||||
}
|
||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
@@ -153,6 +232,33 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
||||
if (closed || !mantleWarmups.add(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mantleWarmupExecutor.execute(() -> warmupMantleChunk(key, chunkX, chunkZ));
|
||||
} catch (RejectedExecutionException e) {
|
||||
mantleWarmups.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void warmupMantleChunk(long key, int chunkX, int chunkZ) {
|
||||
try {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!closed && !engine.isClosed() && !mantle.isClosed() && !mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
mantle.getChunk(chunkX, chunkZ);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (!closed && !engine.isClosed()) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
} finally {
|
||||
mantleWarmups.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void ambientTick(ServerLevel level) {
|
||||
@@ -575,7 +681,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
INITIAL_QUEUES.remove(engine);
|
||||
closed = true;
|
||||
initialSpawnQueue.close();
|
||||
mantleWarmupExecutor.shutdownNow();
|
||||
mantleWarmups.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class NativeSpawnTableMerger {
|
||||
private NativeSpawnTableMerger() {
|
||||
}
|
||||
|
||||
static WeightedList<MobSpawnSettings.SpawnerData> merge(
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
|
||||
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
|
||||
Set<EntityType<?>> explicitTypes = new HashSet<>();
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
|
||||
explicitTypes.add(entry.value().type());
|
||||
}
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
|
||||
if (!explicitTypes.contains(entry.value().type())) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
entries.addAll(explicitSpawns.unwrap());
|
||||
return WeightedList.of(entries);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -131,7 +131,8 @@ final class ModdedCommandHelp {
|
||||
SECTIONS.put("s", SECTIONS.get("studio"));
|
||||
SECTIONS.put("pack", List.of(
|
||||
Entry.command("validate", "[pack]", "Validate a pack or every pack", "v"),
|
||||
Entry.command("restore", "<pack>", "Restore the latest trashed files for a pack", "r"),
|
||||
Entry.command("cleanup", "<pack> [apply]", "Preview or quarantine unused-resource candidates", "c"),
|
||||
Entry.command("restore", "<pack> [apply]", "Preview or restore the latest quarantine", "r"),
|
||||
Entry.command("status", "[pack]", "Show cached validation status", "s")
|
||||
));
|
||||
SECTIONS.put("pk", SECTIONS.get("pack"));
|
||||
|
||||
+2
-2
@@ -63,7 +63,7 @@ public final class ModdedGoldenHash {
|
||||
File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile();
|
||||
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
|
||||
engine.getWorld().name(),
|
||||
level.getSeed(),
|
||||
engine.getSeedManager().getSeed(),
|
||||
ModdedEngineBootstrap.loader().minecraftVersion(),
|
||||
engine.getMinHeight(),
|
||||
engine.getMaxHeight(),
|
||||
@@ -88,7 +88,7 @@ public final class ModdedGoldenHash {
|
||||
int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1);
|
||||
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + Math.max(1, threads) + " mode=" + mode);
|
||||
LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}",
|
||||
engine.getDimension().getLoadKey(), level.getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
|
||||
engine.getDimension().getLoadKey(), engine.getSeedManager().getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
scan.hashEngine.run();
|
||||
|
||||
+134
-21
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
@@ -58,12 +60,27 @@ public final class ModdedPackCommands {
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
||||
|
||||
root.then(Commands.literal("cleanup")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||
.then(Commands.literal("apply")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||
root.then(Commands.literal("c")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||
.then(Commands.literal("apply")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||
|
||||
root.then(Commands.literal("restore")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||
.then(Commands.literal("apply")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||
root.then(Commands.literal("r")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||
.then(Commands.literal("apply")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||
|
||||
root.then(Commands.literal("status")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource(), null))
|
||||
@@ -99,8 +116,8 @@ public final class ModdedPackCommands {
|
||||
targets.add(dir);
|
||||
}
|
||||
} else {
|
||||
File target = new File(packsRoot, pack);
|
||||
if (!target.isDirectory()) {
|
||||
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||
if (target == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot.getAbsolutePath());
|
||||
return 0;
|
||||
}
|
||||
@@ -133,19 +150,45 @@ public final class ModdedPackCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int restore(CommandSourceStack source, String pack) {
|
||||
File packFolder = new File(packsRoot(), pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
private static int cleanup(CommandSourceStack source, String pack, boolean apply) {
|
||||
File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack);
|
||||
if (packFolder == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
|
||||
return 0;
|
||||
}
|
||||
int restored = PackValidator.restoreTrash(packFolder);
|
||||
if (restored == 0) {
|
||||
IrisModdedCommands.fail(source, "Nothing to restore for pack '" + pack + "'.");
|
||||
MinecraftServer server = source.getServer();
|
||||
Thread thread = new Thread(() -> {
|
||||
if (apply) {
|
||||
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||
server.execute(() -> reportCleanupApply(source, pack, result));
|
||||
} else {
|
||||
PackResourceCleanup.Preview result = PackResourceCleanup.preview(packFolder);
|
||||
server.execute(() -> reportCleanupPreview(source, pack, result));
|
||||
}
|
||||
}, "Iris Pack Cleanup");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int restore(CommandSourceStack source, String pack, boolean apply) {
|
||||
File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack);
|
||||
if (packFolder == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
|
||||
IrisModdedCommands.ok(source, "Re-run /iris pack validate " + pack + " to re-check.");
|
||||
MinecraftServer server = source.getServer();
|
||||
Thread thread = new Thread(() -> {
|
||||
if (apply) {
|
||||
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||
server.execute(() -> reportRestoreApply(source, pack, result));
|
||||
} else {
|
||||
PackResourceCleanup.RestorePreview result = PackResourceCleanup.previewRestore(packFolder);
|
||||
server.execute(() -> reportRestorePreview(source, pack, result));
|
||||
}
|
||||
}, "Iris Pack Restore");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -161,8 +204,7 @@ public final class ModdedPackCommands {
|
||||
String tag = result.isLoadable() ? "OK" : "BROKEN";
|
||||
IrisModdedCommands.ok(source, tag + " " + entry.getKey()
|
||||
+ " (blocking=" + result.getBlockingErrors().size()
|
||||
+ ", warnings=" + result.getWarnings().size()
|
||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
||||
+ ", warnings=" + result.getWarnings().size() + ")");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -178,8 +220,7 @@ public final class ModdedPackCommands {
|
||||
private static void report(CommandSourceStack source, PackValidationResult result) {
|
||||
if (result.isLoadable()) {
|
||||
IrisModdedCommands.ok(source, "Pack '" + result.getPackName() + "' is loadable."
|
||||
+ " (warnings=" + result.getWarnings().size()
|
||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
||||
+ " (warnings=" + result.getWarnings().size() + ")");
|
||||
} else {
|
||||
IrisModdedCommands.fail(source, "Pack '" + result.getPackName() + "' is BROKEN:");
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
@@ -193,12 +234,84 @@ public final class ModdedPackCommands {
|
||||
if (result.getWarnings().size() > warningMax) {
|
||||
IrisModdedCommands.ok(source, " ... and " + (result.getWarnings().size() - warningMax) + " more warning(s).");
|
||||
}
|
||||
int trashMax = Math.min(10, result.getRemovedUnusedFiles().size());
|
||||
for (int i = 0; i < trashMax; i++) {
|
||||
IrisModdedCommands.ok(source, " ~ trashed " + result.getRemovedUnusedFiles().get(i));
|
||||
}
|
||||
|
||||
private static void reportCleanupPreview(CommandSourceStack source, String pack, PackResourceCleanup.Preview result) {
|
||||
if (!result.success()) {
|
||||
IrisModdedCommands.fail(source, result.error());
|
||||
return;
|
||||
}
|
||||
if (result.getRemovedUnusedFiles().size() > trashMax) {
|
||||
IrisModdedCommands.ok(source, " ... and " + (result.getRemovedUnusedFiles().size() - trashMax) + " more trashed file(s).");
|
||||
if (!result.hasCandidates()) {
|
||||
IrisModdedCommands.ok(source, "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Cleanup preview for pack '" + pack + "': "
|
||||
+ result.candidatePaths().size() + " candidate(s). No files were changed.");
|
||||
reportPaths(source, result.candidatePaths(), "candidate");
|
||||
IrisModdedCommands.ok(source, "Run /iris pack cleanup " + pack + " apply to quarantine after a fresh scan.");
|
||||
}
|
||||
|
||||
private static void reportCleanupApply(CommandSourceStack source, String pack, PackResourceCleanup.ApplyResult result) {
|
||||
if (!result.success()) {
|
||||
IrisModdedCommands.fail(source, result.error());
|
||||
reportPaths(source, result.quarantinedPaths(), "still quarantined");
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
IrisModdedCommands.ok(source, "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Quarantined " + result.quarantinedPaths().size()
|
||||
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
|
||||
reportPaths(source, result.quarantinedPaths(), "quarantined");
|
||||
}
|
||||
|
||||
private static void reportRestorePreview(CommandSourceStack source, String pack, PackResourceCleanup.RestorePreview result) {
|
||||
if (!result.success()) {
|
||||
IrisModdedCommands.fail(source, result.error());
|
||||
return;
|
||||
}
|
||||
if (!result.hasFiles()) {
|
||||
IrisModdedCommands.ok(source, "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Restore preview for " + result.dumpPath() + ": "
|
||||
+ result.filePaths().size() + " file(s). No files were changed.");
|
||||
reportPaths(source, result.filePaths(), "file");
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
IrisModdedCommands.fail(source, "Restore is blocked by " + result.conflicts().size() + " existing destination(s).");
|
||||
reportPaths(source, result.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Run /iris pack restore " + pack + " apply to restore after a fresh conflict check.");
|
||||
}
|
||||
|
||||
private static void reportRestoreApply(CommandSourceStack source, String pack, PackResourceCleanup.RestoreResult result) {
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
IrisModdedCommands.fail(source, "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
|
||||
reportPaths(source, result.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
if (!result.success()) {
|
||||
IrisModdedCommands.fail(source, result.error());
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
IrisModdedCommands.ok(source, "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Restored " + result.restoredPaths().size()
|
||||
+ " file(s) from " + result.dumpPath() + ".");
|
||||
reportPaths(source, result.restoredPaths(), "restored");
|
||||
}
|
||||
|
||||
private static void reportPaths(CommandSourceStack source, List<String> paths, String label) {
|
||||
int max = Math.min(10, paths.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
IrisModdedCommands.ok(source, " - " + label + ": " + paths.get(i));
|
||||
}
|
||||
if (paths.size() > max) {
|
||||
IrisModdedCommands.ok(source, " ... and " + (paths.size() - max) + " more.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -77,7 +77,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
this.maxInFlight = Math.max(8, pregen.getModdedPregenInFlight());
|
||||
this.minInFlight = Math.max(4, Math.min(16, maxInFlight / 4));
|
||||
this.semaphore = new Semaphore(maxInFlight, true);
|
||||
this.adaptiveLimit = new AtomicInteger(maxInFlight);
|
||||
this.adaptiveLimit = new AtomicInteger(sync ? 1 : maxInFlight);
|
||||
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
||||
this.backpressure = new PregenMantleBackpressure(
|
||||
this::getMantle,
|
||||
@@ -179,6 +179,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
CompletableFuture<?> loadFuture = CompletableFuture
|
||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
|
||||
.thenCompose((CompletableFuture<?> inner) -> inner);
|
||||
markSubmitted();
|
||||
try {
|
||||
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
||||
@@ -196,6 +197,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
|
||||
listener.onChunkFailed(x, z);
|
||||
} finally {
|
||||
markFinished();
|
||||
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
|
||||
}
|
||||
}
|
||||
@@ -255,6 +257,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
private void markFinished() {
|
||||
inFlight.decrementAndGet();
|
||||
if (sync) {
|
||||
return;
|
||||
}
|
||||
synchronized (permitMonitor) {
|
||||
permitMonitor.notifyAll();
|
||||
}
|
||||
|
||||
+13
-1
@@ -44,6 +44,7 @@ import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
@@ -108,17 +109,22 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (level.players().isEmpty() || isPregenActive(engine)) {
|
||||
if (!hasUpdateTargets(!level.players().isEmpty(), !level.getForceLoadedChunks().isEmpty()) || isPregenActive(engine)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
updateNearPlayers(engine, level);
|
||||
updateForcedChunks(engine, level);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean hasUpdateTargets(boolean hasPlayers, boolean hasForcedChunks) {
|
||||
return hasPlayers || hasForcedChunks;
|
||||
}
|
||||
|
||||
private boolean isPregenActive(Engine engine) {
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||
@@ -136,6 +142,12 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateForcedChunks(Engine engine, ServerLevel level) {
|
||||
for (long chunkKey : level.getForceLoadedChunks()) {
|
||||
updateChunk(engine, level, ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class InitialSpawnQueueTest {
|
||||
@Test
|
||||
public void rejectsDuplicatesAndEnforcesCapacityWhileInFlight() {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||
|
||||
assertTrue(queue.offer(1L));
|
||||
assertFalse(queue.offer(1L));
|
||||
assertTrue(queue.offer(2L));
|
||||
assertEquals(Long.valueOf(1L), queue.poll());
|
||||
assertFalse(queue.offer(3L));
|
||||
queue.complete(1L);
|
||||
assertTrue(queue.offer(3L));
|
||||
assertEquals(2, queue.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryRotatesWithoutBlockingReadyEntries() {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(4);
|
||||
queue.offer(1L);
|
||||
queue.offer(2L);
|
||||
queue.offer(3L);
|
||||
|
||||
int budget = queue.batchSize(8);
|
||||
Long unavailable = queue.poll();
|
||||
queue.retry(unavailable);
|
||||
Long firstReady = queue.poll();
|
||||
queue.complete(firstReady);
|
||||
Long secondReady = queue.poll();
|
||||
queue.complete(secondReady);
|
||||
|
||||
assertEquals(3, budget);
|
||||
assertEquals(Long.valueOf(1L), unavailable);
|
||||
assertEquals(Long.valueOf(2L), firstReady);
|
||||
assertEquals(Long.valueOf(3L), secondReady);
|
||||
assertEquals(1, queue.batchSize(8));
|
||||
assertEquals(Long.valueOf(1L), queue.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryIsDeduplicated() {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||
queue.offer(7L);
|
||||
Long key = queue.poll();
|
||||
|
||||
queue.retry(key);
|
||||
queue.retry(key);
|
||||
|
||||
assertEquals(1, queue.batchSize(8));
|
||||
assertEquals(Long.valueOf(7L), queue.poll());
|
||||
assertNull(queue.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeClearsAndRejectsFurtherWork() {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||
queue.offer(1L);
|
||||
|
||||
queue.close();
|
||||
|
||||
assertTrue(queue.isEmpty());
|
||||
assertEquals(0, queue.size());
|
||||
assertFalse(queue.offer(2L));
|
||||
assertEquals(0, queue.batchSize(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearAllowsLaterWork() {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||
queue.offer(1L);
|
||||
|
||||
queue.clear();
|
||||
|
||||
assertTrue(queue.offer(2L));
|
||||
assertEquals(Long.valueOf(2L), queue.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expiredEntriesReleaseCapacity() {
|
||||
AtomicLong now = new AtomicLong();
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get);
|
||||
queue.offer(1L);
|
||||
queue.offer(2L);
|
||||
|
||||
now.set(100L);
|
||||
|
||||
assertTrue(queue.offer(3L));
|
||||
assertEquals(1, queue.size());
|
||||
assertEquals(Long.valueOf(3L), queue.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expiredInFlightEntryCannotRetry() {
|
||||
AtomicLong now = new AtomicLong();
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get);
|
||||
queue.offer(1L);
|
||||
Long inFlight = queue.poll();
|
||||
|
||||
now.set(100L);
|
||||
queue.retry(inFlight);
|
||||
|
||||
assertEquals(0, queue.size());
|
||||
assertNull(queue.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentOffersRemainDeduplicatedAndBounded() throws Exception {
|
||||
InitialSpawnQueue queue = new InitialSpawnQueue(256);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (int thread = 0; thread < 8; thread++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
for (long key = 0L; key < 512L; key++) {
|
||||
queue.offer(key);
|
||||
}
|
||||
}));
|
||||
}
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
}
|
||||
executor.shutdown();
|
||||
|
||||
assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS));
|
||||
assertEquals(256, queue.size());
|
||||
assertEquals(256, queue.batchSize(512));
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisModdedChunkGeneratorSpawnTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitSpawnsReplaceMatchingVanillaTypesAndPreserveOthers() {
|
||||
MobSpawnSettings.SpawnerData zombie = new MobSpawnSettings.SpawnerData(EntityTypes.ZOMBIE, 1, 4);
|
||||
MobSpawnSettings.SpawnerData vanillaSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 1, 1);
|
||||
MobSpawnSettings.SpawnerData explicitSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 2, 5);
|
||||
MobSpawnSettings.SpawnerData explicitCow = new MobSpawnSettings.SpawnerData(EntityTypes.COW, 2, 4);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanilla = WeightedList.of(List.of(
|
||||
new Weighted<>(zombie, 100),
|
||||
new Weighted<>(vanillaSlime, 1)));
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicit = WeightedList.of(List.of(
|
||||
new Weighted<>(explicitSlime, 7),
|
||||
new Weighted<>(explicitCow, 3)));
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> merged = NativeSpawnTableMerger.merge(vanilla, explicit);
|
||||
List<Weighted<MobSpawnSettings.SpawnerData>> entries = merged.unwrap();
|
||||
|
||||
assertEquals(3, entries.size());
|
||||
assertEquals(EntityTypes.ZOMBIE, entries.get(0).value().type());
|
||||
assertEquals(100, entries.get(0).weight());
|
||||
assertEquals(explicitSlime, entries.get(1).value());
|
||||
assertEquals(7, entries.get(1).weight());
|
||||
assertEquals(explicitCow, entries.get(2).value());
|
||||
assertEquals(3, entries.get(2).weight());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedWorldCheckTest {
|
||||
@Test
|
||||
public void coordinatorThreadIsNonDaemon() {
|
||||
Thread thread = ModdedWorldCheck.coordinatorThread(() -> {
|
||||
});
|
||||
|
||||
assertFalse(thread.isDaemon());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void passStopsServerBeforeZeroExit() {
|
||||
List<String> events = new ArrayList<>();
|
||||
|
||||
ModdedWorldCheck.stopAndExit(
|
||||
() -> events.add("stop"),
|
||||
0,
|
||||
status -> events.add("exit:" + status)
|
||||
);
|
||||
|
||||
assertEquals(List.of("stop", "exit:0"), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureStopsServerBeforeNonzeroExit() {
|
||||
List<String> events = new ArrayList<>();
|
||||
|
||||
ModdedWorldCheck.stopAndExit(
|
||||
() -> events.add("stop"),
|
||||
1,
|
||||
status -> events.add("exit:" + status)
|
||||
);
|
||||
|
||||
assertEquals(List.of("stop", "exit:1"), events);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownFailureForcesNonzeroExit() {
|
||||
AtomicInteger status = new AtomicInteger(-1);
|
||||
|
||||
ModdedWorldCheck.stopAndExit(
|
||||
() -> {
|
||||
throw new IllegalStateException("shutdown failed");
|
||||
},
|
||||
0,
|
||||
status::set
|
||||
);
|
||||
|
||||
assertEquals(1, status.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptionIsClearedForShutdownAndRestoredBeforeExit() {
|
||||
AtomicBoolean interruptedDuringStop = new AtomicBoolean(true);
|
||||
AtomicBoolean interruptedDuringExit = new AtomicBoolean(false);
|
||||
AtomicInteger status = new AtomicInteger(-1);
|
||||
Thread.currentThread().interrupt();
|
||||
try {
|
||||
ModdedWorldCheck.stopAndExit(
|
||||
() -> interruptedDuringStop.set(Thread.currentThread().isInterrupted()),
|
||||
0,
|
||||
exitStatus -> {
|
||||
status.set(exitStatus);
|
||||
interruptedDuringExit.set(Thread.currentThread().isInterrupted());
|
||||
}
|
||||
);
|
||||
|
||||
assertFalse(interruptedDuringStop.get());
|
||||
assertTrue(interruptedDuringExit.get());
|
||||
assertEquals(1, status.get());
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedChunkUpdateServiceTest {
|
||||
@Test
|
||||
public void scansWhenPlayersArePresent() {
|
||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scansHeadlessForceLoadedChunks() {
|
||||
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsLevelsWithoutPlayersOrForcedChunks() {
|
||||
assertFalse(ModdedChunkUpdateService.hasUpdateTargets(false, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user