mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
dwa
This commit is contained in:
+8
-4
@@ -57,6 +57,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
private volatile int cacheRuntimeId;
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -67,6 +68,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
this.cacheRuntimeId = engine.getCacheID();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
@@ -246,11 +248,11 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris spawn biome lookup was rejected during an engine transition");
|
||||
return null;
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris spawn biome lookup has no active engine runtime");
|
||||
return null;
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return vanillaSpawnBiomes.get(biome.value());
|
||||
@@ -434,11 +436,12 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (cacheDimension == dimension) {
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension) {
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
@@ -451,6 +454,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
cacheRuntimeId = runtimeId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -57,6 +57,7 @@ final class ImportedFeatureStage {
|
||||
private final Engine engine;
|
||||
private volatile FeatureTable featureTable;
|
||||
private volatile IrisDimension inertDimension;
|
||||
private volatile int settledRuntimeId;
|
||||
|
||||
ImportedFeatureStage(Engine engine) {
|
||||
this.engine = engine;
|
||||
@@ -86,18 +87,23 @@ final class ImportedFeatureStage {
|
||||
*/
|
||||
void prepare(WorldGenLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (settled(dimension)) {
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (settled(dimension, runtimeId)) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (settled(dimension)) {
|
||||
if (settled(dimension, runtimeId)) {
|
||||
return;
|
||||
}
|
||||
build(level, dimension);
|
||||
settledRuntimeId = runtimeId;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settled(IrisDimension dimension) {
|
||||
private boolean settled(IrisDimension dimension, int runtimeId) {
|
||||
if (settledRuntimeId != runtimeId) {
|
||||
return false;
|
||||
}
|
||||
FeatureTable current = featureTable;
|
||||
if (current != null && current.dimension() == dimension) {
|
||||
return true;
|
||||
|
||||
+18
-4
@@ -128,6 +128,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private final int runtimeHeight;
|
||||
private final int runtimeSeaLevel;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile IrisDimension spawnTableDimension;
|
||||
private volatile int spawnTableRuntimeId;
|
||||
private final ImportedFeatureStage importedFeatures;
|
||||
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
@@ -310,18 +312,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
private Set<String> reachableStructureKeys(ServerLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
ReachableStructureCache cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) {
|
||||
return cached.keys();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = reachableStructureCache;
|
||||
if (cached != null && cached.dimension() == dimension) {
|
||||
if (cached != null && cached.dimension() == dimension && cached.runtimeId() == runtimeId) {
|
||||
return cached.keys();
|
||||
}
|
||||
Set<String> reachable = Set.copyOf(
|
||||
VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
|
||||
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
|
||||
reachableStructureCache = new ReachableStructureCache(dimension, runtimeId, reachable);
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
@@ -738,6 +741,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
IrisDimension spawnDimension = engine.getDimension();
|
||||
int spawnRuntimeId = engine.getCacheID();
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
synchronized (mergedSpawnTables) {
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
mergedSpawnTables.clear();
|
||||
spawnTableDimension = spawnDimension;
|
||||
spawnTableRuntimeId = spawnRuntimeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
@@ -1160,7 +1174,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
|
||||
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
|
||||
+3
-2
@@ -26,8 +26,9 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
|
||||
assertTrue(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||
assertTrue(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||
|
||||
@@ -613,18 +613,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
|
||||
// Only services that actually enabled get listeners and a later onDisable.
|
||||
enabledServices.clear();
|
||||
IrisService firstFailedService = null;
|
||||
for (IrisService service : orderedServices) {
|
||||
try {
|
||||
service.onEnable();
|
||||
enabledServices.add(service);
|
||||
} catch (Throwable e) {
|
||||
if (firstFailedService == null) {
|
||||
firstFailedService = service;
|
||||
}
|
||||
// A service failure is NOT a datapack validation failure: the admission gate
|
||||
// must never lock every login over a broken cosmetic service. Log loudly,
|
||||
// continue degraded, and clean up whatever the partial onEnable started
|
||||
// (a failed service is excluded from the teardown loop).
|
||||
Iris.reportError("Failed to enable " + service.getClass().getSimpleName() + "; continuing with a degraded runtime.", e);
|
||||
// A failed service is excluded from the teardown loop, so clean up whatever
|
||||
// its partial onEnable started right here, best-effort.
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable cleanup) {
|
||||
@@ -632,11 +630,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstFailedService != null) {
|
||||
IrisStartupValidation.markDatapacksInvalid("Iris service "
|
||||
+ firstFailedService.getClass().getSimpleName()
|
||||
+ " failed to enable; world creation and player admission are locked. Check the log above.");
|
||||
}
|
||||
for (IrisService service : enabledServices) {
|
||||
try {
|
||||
registerListener(service);
|
||||
|
||||
+15
-1
@@ -468,7 +468,21 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
|
||||
private List<Transaction> loadTransactions() throws IOException {
|
||||
return WorldReplacementJournal.load(dataDirectory(), IrisWorldStorage.levelRoot().toPath());
|
||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||
List<Transaction> transactions = WorldReplacementJournal.load(dataDirectory(), levelRoot);
|
||||
List<Transaction> applicable = new ArrayList<>(transactions.size());
|
||||
for (Transaction transaction : transactions) {
|
||||
// A journal staged against another level root is not this server's transaction;
|
||||
// skip it (the bootstrap already told the operator how to resolve it).
|
||||
if (WorldReplacementJournal.appliesTo(transaction, levelRoot)) {
|
||||
applicable.add(transaction);
|
||||
} else {
|
||||
Iris.warn("Ignoring pending world replacement " + transaction.id() + " for "
|
||||
+ transaction.worldKey() + "; it was staged against level root "
|
||||
+ transaction.levelRoot() + ".");
|
||||
}
|
||||
}
|
||||
return applicable;
|
||||
}
|
||||
|
||||
private void writeTransaction(Transaction transaction) throws IOException {
|
||||
|
||||
@@ -263,6 +263,10 @@ public class CommandPack implements DirectorExecutor {
|
||||
for (File packDirectory : packDirectories) {
|
||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
||||
if (result == null) {
|
||||
// The boot cache only loads when it covers every pack, so a partial write is
|
||||
// pointless - but say so instead of silently skipping the persist forever.
|
||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
||||
return;
|
||||
}
|
||||
results.add(result);
|
||||
|
||||
+4
-4
@@ -329,8 +329,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
if (radius <= 0 || radius > 2048) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||
sender().sendMessage("Radius must be between 1 and 2048 chunks.");
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE));
|
||||
return;
|
||||
}
|
||||
var sender = sender();
|
||||
@@ -393,7 +392,8 @@ public class CommandStudio implements DirectorExecutor {
|
||||
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_SCAN_FAILED,
|
||||
MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
} finally {
|
||||
if (c != -1) {
|
||||
J.car(c);
|
||||
@@ -712,7 +712,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks", descriptionKey = "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks")
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks", descriptionKey = "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks", origin = DirectorOrigin.PLAYER)
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD));
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
/**
|
||||
* Shared waiter for the racy build caches in this package: the loser of a putIfAbsent race
|
||||
* must surface the same exception the builder threw, not a CompletionException wrapper.
|
||||
*/
|
||||
final class NativeBuildFutures {
|
||||
private NativeBuildFutures() {
|
||||
}
|
||||
|
||||
static <T> T awaitBuild(CompletableFuture<T> future, String what) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException error) {
|
||||
Throwable cause = error.getCause();
|
||||
if (cause instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
if (cause instanceof Error fatal) {
|
||||
throw fatal;
|
||||
}
|
||||
if (cause == null) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(what + " failed", cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-23
@@ -39,11 +39,13 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -73,8 +75,11 @@ public final class NativeStructureTerrainIntegrator {
|
||||
Blocks.SOUL_SOIL, Blocks.END_STONE, Blocks.GRAVEL, Blocks.CLAY,
|
||||
Blocks.CALCITE, Blocks.DRIPSTONE_BLOCK, Blocks.SANDSTONE,
|
||||
Blocks.RED_SANDSTONE, Blocks.SCULK);
|
||||
private static final Map<CarveFootprintKey, CachedCarveFootprint> CARVE_FOOTPRINTS =
|
||||
new LinkedHashMap<>(16, 0.75F, true);
|
||||
// Weakly keyed on the StructureStart so retired starts (world unload, generation done)
|
||||
// are collectable instead of pinned forever by a process-wide static; values hold only
|
||||
// primitives, so there is no value-to-key cycle defeating the weak keys.
|
||||
private static final Map<StructureStart, Map<Integer, CachedCarveFootprint>> CARVE_FOOTPRINTS =
|
||||
new WeakHashMap<>(16);
|
||||
private static final ConcurrentHashMap<CarveFootprintKey, CompletableFuture<StructureCarvingFootprint>>
|
||||
CARVE_FOOTPRINT_BUILDS = new ConcurrentHashMap<>();
|
||||
private static int cachedCarveCells;
|
||||
@@ -531,6 +536,15 @@ public final class NativeStructureTerrainIntegrator {
|
||||
if (active != null) {
|
||||
return awaitCarveFootprint(active);
|
||||
}
|
||||
// Close the check-then-claim window: a racer whose cache check missed before the
|
||||
// previous builder cached can claim the build slot after that builder retired it,
|
||||
// and would rebuild a second instance without this re-check.
|
||||
StructureCarvingFootprint published = cachedCarveFootprint(key);
|
||||
if (published != null) {
|
||||
build.complete(published);
|
||||
CARVE_FOOTPRINT_BUILDS.remove(key, build);
|
||||
return published;
|
||||
}
|
||||
try {
|
||||
StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns(
|
||||
sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS);
|
||||
@@ -551,53 +565,64 @@ public final class NativeStructureTerrainIntegrator {
|
||||
|
||||
static int cachedCarveFootprintCells() {
|
||||
synchronized (CARVE_FOOTPRINTS) {
|
||||
cachedCarveCells = liveCarveCells();
|
||||
return cachedCarveCells;
|
||||
}
|
||||
}
|
||||
|
||||
private static int liveCarveCells() {
|
||||
int total = 0;
|
||||
for (Map<Integer, CachedCarveFootprint> perStart : CARVE_FOOTPRINTS.values()) {
|
||||
for (CachedCarveFootprint cached : perStart.values()) {
|
||||
total += cached.cells();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static int maximumCachedCarveFootprintCells() {
|
||||
return MAX_CACHED_CARVE_CELLS;
|
||||
}
|
||||
|
||||
private static StructureCarvingFootprint cachedCarveFootprint(CarveFootprintKey key) {
|
||||
synchronized (CARVE_FOOTPRINTS) {
|
||||
CachedCarveFootprint cached = CARVE_FOOTPRINTS.get(key);
|
||||
Map<Integer, CachedCarveFootprint> perStart = CARVE_FOOTPRINTS.get(key.start());
|
||||
CachedCarveFootprint cached = perStart == null ? null : perStart.get(key.padding());
|
||||
return cached == null ? null : cached.footprint();
|
||||
}
|
||||
}
|
||||
|
||||
private static StructureCarvingFootprint awaitCarveFootprint(
|
||||
CompletableFuture<StructureCarvingFootprint> future) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException error) {
|
||||
Throwable cause = error.getCause();
|
||||
if (cause instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
if (cause instanceof Error fatal) {
|
||||
throw fatal;
|
||||
}
|
||||
throw new IllegalStateException("Native structure carve footprint build failed", cause);
|
||||
}
|
||||
return NativeBuildFutures.awaitBuild(future, "Native structure carve footprint build");
|
||||
}
|
||||
|
||||
private static void cacheCarveFootprint(CarveFootprintKey key,
|
||||
StructureCarvingFootprint footprint) {
|
||||
int cells = Math.multiplyExact(footprint.width(), footprint.depth());
|
||||
synchronized (CARVE_FOOTPRINTS) {
|
||||
CachedCarveFootprint previous = CARVE_FOOTPRINTS.remove(key);
|
||||
// Weak keys expunge silently as the GC drops retired starts; re-derive the live
|
||||
// total so the cell budget tracks reality instead of drifting.
|
||||
cachedCarveCells = liveCarveCells();
|
||||
Map<Integer, CachedCarveFootprint> perStart =
|
||||
CARVE_FOOTPRINTS.computeIfAbsent(key.start(), ignored -> new LinkedHashMap<>(4));
|
||||
CachedCarveFootprint previous = perStart.remove(key.padding());
|
||||
if (previous != null) {
|
||||
cachedCarveCells -= previous.cells();
|
||||
}
|
||||
while (!CARVE_FOOTPRINTS.isEmpty()
|
||||
&& cachedCarveCells + cells > MAX_CACHED_CARVE_CELLS) {
|
||||
Map.Entry<CarveFootprintKey, CachedCarveFootprint> eldest =
|
||||
CARVE_FOOTPRINTS.entrySet().iterator().next();
|
||||
cachedCarveCells -= eldest.getValue().cells();
|
||||
CARVE_FOOTPRINTS.remove(eldest.getKey());
|
||||
Iterator<Map.Entry<StructureStart, Map<Integer, CachedCarveFootprint>>> eviction =
|
||||
CARVE_FOOTPRINTS.entrySet().iterator();
|
||||
while (cachedCarveCells + cells > MAX_CACHED_CARVE_CELLS && eviction.hasNext()) {
|
||||
Map.Entry<StructureStart, Map<Integer, CachedCarveFootprint>> victim = eviction.next();
|
||||
if (victim.getKey() == key.start()) {
|
||||
continue;
|
||||
}
|
||||
for (CachedCarveFootprint evicted : victim.getValue().values()) {
|
||||
cachedCarveCells -= evicted.cells();
|
||||
}
|
||||
eviction.remove();
|
||||
}
|
||||
CARVE_FOOTPRINTS.put(key, new CachedCarveFootprint(footprint, cells));
|
||||
perStart.put(key.padding(), new CachedCarveFootprint(footprint, cells));
|
||||
cachedCarveCells += cells;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -329,7 +329,18 @@ public final class NativeStructureVolumeIndex {
|
||||
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
|
||||
CompletableFuture<KList<NativeStructureVolume>> existing = builds.putIfAbsent(key, future);
|
||||
if (existing != null) {
|
||||
return existing.join();
|
||||
return NativeBuildFutures.awaitBuild(existing, "Native structure volume build");
|
||||
}
|
||||
|
||||
// Close the check-then-claim window: a racer whose cache check missed before the
|
||||
// previous builder cached could claim the build slot after it retired and rebuild.
|
||||
synchronized (cache) {
|
||||
KList<NativeStructureVolume> published = cache.get(key);
|
||||
if (published != null) {
|
||||
future.complete(published);
|
||||
builds.remove(key, future);
|
||||
return published;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -236,6 +236,19 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
*/
|
||||
@Override
|
||||
public void reportError(Throwable error) {
|
||||
reportThrottled("Iris reported error", error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual reports run the same throttle and emit exactly one trace through the modded
|
||||
* log; the SPI default's raw stderr copy would bypass the per-signature suppression.
|
||||
*/
|
||||
@Override
|
||||
public void reportError(String context, Throwable error) {
|
||||
reportThrottled(context == null || context.isBlank() ? "Iris reported error" : context, error);
|
||||
}
|
||||
|
||||
private void reportThrottled(String message, Throwable error) {
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
@@ -247,7 +260,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
sink.accept(error);
|
||||
return;
|
||||
}
|
||||
ModdedIrisLog.error("Iris reported error", error);
|
||||
ModdedIrisLog.error(message, error);
|
||||
Consumer<Throwable> capture = CAPTURE_SINK;
|
||||
if (capture != null) {
|
||||
try {
|
||||
|
||||
@@ -800,6 +800,18 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
if (!mantleWarmupExecutorStopped) {
|
||||
try {
|
||||
// Drain, don't interrupt: an interrupt inside a FileChannel plate read closes
|
||||
// the channel and the read-failure fallback installs an empty plate that a
|
||||
// later flush persists over real data. Queued tasks no-op on the closed flag,
|
||||
// so the await only covers a single in-flight load; escalate on timeout only.
|
||||
mantleWarmupExecutor.shutdown();
|
||||
if (!mantleWarmupExecutor.awaitTermination(5L, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||
IrisLogging.warn("Iris mantle warm-up did not stop before world manager close; forcing interrupt");
|
||||
mantleWarmupExecutor.shutdownNow();
|
||||
}
|
||||
mantleWarmupExecutorStopped = true;
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
mantleWarmupExecutor.shutdownNow();
|
||||
mantleWarmupExecutorStopped = true;
|
||||
} catch (Throwable e) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -130,14 +131,17 @@ public final class IrisModdedAPI {
|
||||
* never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's
|
||||
* height range reads as null rather than throwing.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
|
||||
*/
|
||||
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
|
||||
// Lease-gated so the engine shutdown drain covers this public accessor; a mantle
|
||||
// mid-close refuses the lease instead of racing the region flush.
|
||||
return EngineLifecycleTasks.call(engine, "modded_api_mantle_get",
|
||||
() -> engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type), null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,28 +155,30 @@ public final class IrisModdedAPI {
|
||||
* Values written under a custom type are discarded when Iris trims a mantle region unless the type is
|
||||
* declared with {@link #retainMantleDataForSlice(Class)}.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
|
||||
*/
|
||||
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null || data == null) {
|
||||
return;
|
||||
}
|
||||
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
|
||||
EngineLifecycleTasks.run(engine, "modded_api_mantle_set",
|
||||
() -> engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range
|
||||
* {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent.
|
||||
*
|
||||
* @throws IllegalStateException if the engine's mantle has already been closed
|
||||
* A closing or closed engine refuses the operation quietly (null / no-op) instead of racing its shutdown.
|
||||
*/
|
||||
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
|
||||
EngineLifecycleTasks.run(engine, "modded_api_mantle_delete",
|
||||
() -> engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
@@ -69,6 +69,9 @@ final class ModdedCommandSuggestions {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final int TAB_FAILURE_KEYS_MAX = 256;
|
||||
private static final Set<String> REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet();
|
||||
private static final long PACK_NAME_CACHE_TTL_MS = 3_000L;
|
||||
private static volatile Set<String> cachedPackNames;
|
||||
private static volatile long cachedPackNamesAt;
|
||||
|
||||
private ModdedCommandSuggestions() {
|
||||
}
|
||||
@@ -228,6 +231,13 @@ final class ModdedCommandSuggestions {
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
// Suggestion packets arrive per keystroke; a short-lived snapshot keeps the directory
|
||||
// walk off the hot path without ever serving stale names for more than a few seconds.
|
||||
long now = System.currentTimeMillis();
|
||||
Set<String> cached = cachedPackNames;
|
||||
if (cached != null && now - cachedPackNamesAt < PACK_NAME_CACHE_TTL_MS) {
|
||||
return SharedSuggestionProvider.suggest(cached, builder);
|
||||
}
|
||||
Set<String> names = new TreeSet<>();
|
||||
names.add("overworld");
|
||||
try {
|
||||
@@ -249,6 +259,8 @@ final class ModdedCommandSuggestions {
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("pack names", context.getSource(), e);
|
||||
}
|
||||
cachedPackNames = names;
|
||||
cachedPackNamesAt = now;
|
||||
return SharedSuggestionProvider.suggest(names, builder);
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -70,9 +70,10 @@ final class ModdedCommandTree {
|
||||
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
|
||||
// ModdedWhatCommands.tree() gates itself at LEVEL_GAMEMASTERS; the /iris what overlays are read-only,
|
||||
// so the root builder relaxes the whole subtree here instead of forking that file.
|
||||
root.then(ModdedWhatCommands.tree().requires(READ_ONLY));
|
||||
// ModdedWhatCommands.tree() gates itself at LEVEL_GAMEMASTERS and keeps that gate:
|
||||
// "what markers" drives lease-gated 9x9 mantle scans, and the Bukkit twin puts the
|
||||
// whole tree behind an op-default permission. requires() would overwrite, not AND.
|
||||
root.then(ModdedWhatCommands.tree());
|
||||
|
||||
root.then(teleportTree("teleport"));
|
||||
root.then(teleportTree("tp"));
|
||||
|
||||
+19
-5
@@ -285,7 +285,20 @@ public final class ModdedObjectCommands {
|
||||
return 0;
|
||||
}
|
||||
File file = new File(engine.getData().getDataFolder(), "objects" + File.separator + name.replace('/', File.separatorChar) + ".iob");
|
||||
if (file.exists() && !overwrite) {
|
||||
// Atomic path claim ON the server thread: the async write below turned a plain
|
||||
// exists() check into a TOCTOU where two rapid saves interleaved into one file.
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
boolean claimed;
|
||||
try {
|
||||
claimed = file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
return 0;
|
||||
}
|
||||
if (!claimed && !overwrite) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FILE_ALREADY_EXISTS_USE_IRIS_OBJECT_SAVE_OVERWRITE, MessageArgument.untrusted("name", name)));
|
||||
return 0;
|
||||
}
|
||||
@@ -295,15 +308,16 @@ public final class ModdedObjectCommands {
|
||||
// async-safe), but the disk write of a local, unshared object is not tick work.
|
||||
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
|
||||
MinecraftServer server = source.getServer();
|
||||
boolean finalClaimed = claimed;
|
||||
J.a(() -> {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
try {
|
||||
object.write(file);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
|
||||
if (finalClaimed) {
|
||||
// Never leave a 0-byte claim file permanently blocking non-overwrite saves.
|
||||
file.delete();
|
||||
}
|
||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))));
|
||||
return;
|
||||
}
|
||||
|
||||
+14
-5
@@ -85,13 +85,16 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
ExecutorService active = warmupExecutor;
|
||||
warmupExecutor = null;
|
||||
if (active != null) {
|
||||
// Drop queued warm-ups (pure prefetch) and AWAIT: the very next shutdown stage
|
||||
// closes every Mantle, and an in-flight mantle.getChunk would fault a plate back
|
||||
// in after the close-time flush.
|
||||
active.shutdownNow();
|
||||
// NEVER interrupt first: an interrupt inside a FileChannel plate read closes the
|
||||
// channel (ClosedByInterruptException) and the read-failure fallback installs an
|
||||
// EMPTY plate that the very next shutdown stage flushes over real data. Queued
|
||||
// warm-ups self-cancel (warmupExecutor is already null), so shutdown() + await
|
||||
// only waits on the single in-flight load; escalate only on timeout.
|
||||
active.shutdown();
|
||||
try {
|
||||
if (!active.awaitTermination(5L, TimeUnit.SECONDS)) {
|
||||
IrisLogging.warn("Iris mantle warm-up did not stop before engine close");
|
||||
IrisLogging.warn("Iris mantle warm-up did not stop before engine close; forcing interrupt");
|
||||
active.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
@@ -279,6 +282,12 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
try {
|
||||
active.execute(() -> {
|
||||
// Self-cancel on shutdown: warmupExecutor is nulled first in onDisable, so
|
||||
// queued prefetches no-op instantly and only an in-flight load is awaited.
|
||||
if (warmupExecutor == null || mantle.isClosed()) {
|
||||
warmupQueue.remove(key);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mantle.getChunk(chunkX, chunkZ);
|
||||
} catch (Throwable e) {
|
||||
|
||||
Reference in New Issue
Block a user