mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
d
This commit is contained in:
+150
-64
@@ -19,8 +19,11 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -51,6 +54,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> visibleBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
|
||||
private volatile IrisModdedChunkGenerator generator;
|
||||
private volatile Set<String> possibleStructureBiomeKeys;
|
||||
|
||||
@@ -67,6 +71,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
possibleStructureBiomeKeys = null;
|
||||
for (StructureStateBiomeSource source : structureStateSources) {
|
||||
source.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
BiomeSource forStructureState(HolderLookup<StructureSet> structureSets) {
|
||||
@@ -91,7 +98,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
}
|
||||
});
|
||||
return new StructureStateBiomeSource(this, Set.copyOf(possible));
|
||||
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
|
||||
structureStateSources.add(source);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,10 +143,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
if (engine == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
|
||||
@@ -160,24 +178,46 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
if (engine == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
Holder<Biome> cached = visibleBiomeCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
|
||||
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
|
||||
visibleBiomeCache.clear();
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
Holder<Biome> cached = visibleBiomeCache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
|
||||
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
|
||||
visibleBiomeCache.clear();
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
return existing == null ? resolved : existing;
|
||||
}
|
||||
|
||||
boolean isStructureReachable(Holder<Structure> structure) {
|
||||
Set<String> possible = possibleStructureBiomeKeys();
|
||||
Engine engine = engineOrNull();
|
||||
if (engine == null) {
|
||||
return isStructureReachable(structure, possibleStructureBiomeKeys());
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_reachability");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure reachability was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
return isStructureReachable(structure, possibleStructureBiomeKeys());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStructureReachable(Holder<Structure> structure, Set<String> possible) {
|
||||
for (Holder<Biome> biome : structure.value().biomes()) {
|
||||
String key = holderKey(biome);
|
||||
if (isGeneratedBiomeKey(key, possible)) {
|
||||
@@ -191,26 +231,35 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
Engine engine = engineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
if (engine == null) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_biomes_within");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||
@@ -338,28 +387,37 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
||||
}
|
||||
Engine engine = current.awaitStructureEngine();
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
IrisBiome irisBiome;
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
} else {
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
}
|
||||
IrisBiome irisBiome;
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
} else {
|
||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
String derivativeKey = irisBiome.getStructureDerivativeKey();
|
||||
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
|
||||
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
String derivativeKey = irisBiome.getStructureDerivativeKey();
|
||||
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
|
||||
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static long biomeResolutionSeed(long worldSeed, int blockX, int blockY, int blockZ) {
|
||||
@@ -403,6 +461,21 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return current == null ? null : current.structureEngineOrNull();
|
||||
}
|
||||
|
||||
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return engine.acquireGenerationLease(operation);
|
||||
} catch (GenerationSessionException e) {
|
||||
if (engine.isClosing() || e.isExpectedTeardown()) {
|
||||
return null;
|
||||
}
|
||||
throw new IllegalStateException("Iris biome source could not acquire generation session for "
|
||||
+ operation + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> possibleStructureBiomeKeys() {
|
||||
Set<String> cached = possibleStructureBiomeKeys;
|
||||
if (cached != null) {
|
||||
@@ -429,24 +502,33 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return Set.of();
|
||||
}
|
||||
Engine engine = current.structureEngineOrNull();
|
||||
if (!isReady(engine)) {
|
||||
if (engine == null) {
|
||||
return current.configuredStructureBiomeKeys();
|
||||
}
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
possible.add(derivative);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome key lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
||||
}
|
||||
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
possible.add(derivative);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(possible);
|
||||
}
|
||||
return Set.copyOf(possible);
|
||||
}
|
||||
|
||||
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||
@@ -517,6 +599,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
this.possibleBiomes = possibleBiomes;
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
resolvedBiomes.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
||||
|
||||
+160
-78
@@ -22,6 +22,7 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -37,6 +38,7 @@ import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
@@ -185,6 +187,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile boolean unloading;
|
||||
private volatile Engine engine;
|
||||
private volatile String activePack;
|
||||
private volatile String activeDimensionKey;
|
||||
@@ -221,6 +224,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
||||
requireBindingAllowed();
|
||||
if (level.getChunkSource().getGenerator() != this) {
|
||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||
}
|
||||
@@ -258,11 +262,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
unloading = true;
|
||||
ServerLevel level = boundLevel();
|
||||
unbindEngine(level);
|
||||
}
|
||||
|
||||
synchronized void unbindEngine(ServerLevel level) {
|
||||
unloading = true;
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evictOrThrow(level);
|
||||
}
|
||||
@@ -322,13 +328,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
BlockPos pos, int radius,
|
||||
boolean findUnexplored) {
|
||||
Engine current = engine();
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
||||
? null
|
||||
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
||||
? null
|
||||
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNativeStructureReachable(Holder<Structure> structure) {
|
||||
@@ -412,8 +421,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private Engine engine() {
|
||||
requireBindingAllowed();
|
||||
Engine cached = engine;
|
||||
if (cached != null) {
|
||||
requireCompletedShutdown(cached);
|
||||
if (cached != null && !cached.isClosed()) {
|
||||
return cached;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
@@ -423,14 +434,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return bindEngine(level);
|
||||
}
|
||||
|
||||
void bindLevel(ServerLevel level) {
|
||||
synchronized void bindLevel(ServerLevel level) {
|
||||
if (level.getChunkSource().getGenerator() != this) {
|
||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||
}
|
||||
requireCompletedShutdown(engine);
|
||||
unloading = false;
|
||||
bindEngine(level);
|
||||
}
|
||||
|
||||
private Engine bindEngine(ServerLevel level) {
|
||||
requireBindingAllowed();
|
||||
try {
|
||||
requireGlobalStructureGeneration(
|
||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||
@@ -439,18 +453,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
throw error;
|
||||
}
|
||||
Engine cached = engine;
|
||||
requireCompletedShutdown(cached);
|
||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||
engineBinding.complete(cached);
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
requireBindingAllowed();
|
||||
Engine existing = engine;
|
||||
requireCompletedShutdown(existing);
|
||||
if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
|
||||
engineBinding.complete(existing);
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
requireCompletedShutdown(created);
|
||||
if (created.isClosed() || created.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' created an engine without a ready biome complex");
|
||||
@@ -473,6 +491,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCompletedShutdown(Engine current) {
|
||||
if (current != null && current.isClosing() && !current.isClosed()) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' cannot bind while its previous engine shutdown remains incomplete");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireBindingAllowed() {
|
||||
if (unloading) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' is unloading and cannot bind an engine");
|
||||
}
|
||||
}
|
||||
|
||||
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
|
||||
if (enabled) {
|
||||
return;
|
||||
@@ -483,8 +514,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private Engine engineOrNull() {
|
||||
requireBindingAllowed();
|
||||
Engine cached = engine;
|
||||
if (cached != null) {
|
||||
requireCompletedShutdown(cached);
|
||||
if (cached != null && !cached.isClosed()) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
@@ -495,8 +528,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
Engine structureEngineOrNull() {
|
||||
requireBindingAllowed();
|
||||
engineBinding.throwIfFailed(dimensionKey);
|
||||
Engine cached = engine;
|
||||
requireCompletedShutdown(cached);
|
||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||
return cached;
|
||||
}
|
||||
@@ -505,11 +540,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
Engine awaitStructureEngine() {
|
||||
requireBindingAllowed();
|
||||
Engine current = engine;
|
||||
requireCompletedShutdown(current);
|
||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||
return current;
|
||||
}
|
||||
Engine bound = engineBinding.await(dimensionKey);
|
||||
requireCompletedShutdown(bound);
|
||||
if (bound.isClosed() || bound.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' completed bootstrap without a ready biome complex");
|
||||
@@ -518,7 +556,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private Engine requireDataQueryEngine(String operation) {
|
||||
requireBindingAllowed();
|
||||
Engine current = engine;
|
||||
requireCompletedShutdown(current);
|
||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||
return current;
|
||||
}
|
||||
@@ -561,18 +601,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return configuredStructureBiomeKeys;
|
||||
}
|
||||
Engine current = engine;
|
||||
Iterable<IrisBiome> biomes;
|
||||
String namespace;
|
||||
if (current != null && !current.isClosed()) {
|
||||
biomes = current.getAllBiomes();
|
||||
namespace = current.getDimension().getLoadKey();
|
||||
} else {
|
||||
ConfiguredPack configured = configuredPack();
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
if (current != null && !current.isClosed() && !current.isClosing()) {
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(
|
||||
current.getAllBiomes(), current.getDimension().getLoadKey());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
} catch (GenerationSessionException e) {
|
||||
if (!current.isClosing() && !e.isExpectedTeardown()) {
|
||||
throw new IllegalStateException("Iris configured biome lookup could not acquire its engine runtime.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(biomes, namespace);
|
||||
ConfiguredPack configured = configuredPack();
|
||||
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
}
|
||||
@@ -654,7 +697,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
public Engine engineIfBound() {
|
||||
return engine;
|
||||
Engine current = engine;
|
||||
return unloading || current == null || current.isClosing() || current.isClosed() ? null : current;
|
||||
}
|
||||
|
||||
public Engine commandEngine() {
|
||||
@@ -710,23 +754,26 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return;
|
||||
}
|
||||
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(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);
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(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;
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
@@ -750,8 +797,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
||||
StructureManager structureManager, ChunkAccess chunk) {
|
||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -759,36 +810,46 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Engine generationEngine = engine();
|
||||
ChunkPos pos = chunk.getPos();
|
||||
lastChunkGenAt = System.currentTimeMillis();
|
||||
if (announced.compareAndSet(false, true)) {
|
||||
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
|
||||
}
|
||||
LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z());
|
||||
|
||||
int dimMinY = generationEngine.getMinHeight();
|
||||
int dimMaxY = generationEngine.getMaxHeight();
|
||||
int height = dimMaxY - dimMinY;
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
|
||||
if (PARALLEL_CHUNK_SYSTEM) {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air));
|
||||
generateTerrain(chunk, generationEngine, pos, air));
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air),
|
||||
() -> generateTerrain(chunk, generationEngine, pos, air),
|
||||
genPool);
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
int dimMinY, int height, PlatformBlockState air) {
|
||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
try {
|
||||
PlatformBlockState air) {
|
||||
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
||||
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
|
||||
if (announced.compareAndSet(false, true)) {
|
||||
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
|
||||
}
|
||||
int dimMinY = generationEngine.getMinHeight();
|
||||
int dimMaxY = generationEngine.getMaxHeight();
|
||||
int height = dimMaxY - dimMinY;
|
||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
|
||||
|
||||
writeBlocks(chunk, blocks, dimMinY, height);
|
||||
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(
|
||||
Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
return chunk;
|
||||
} catch (GenerationSessionException e) {
|
||||
if (e.isExpectedTeardown()) {
|
||||
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
||||
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
|
||||
return chunk;
|
||||
throw new IllegalStateException(
|
||||
"Iris chunk generation was rejected during an engine transition.", e);
|
||||
}
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
@@ -796,14 +857,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
}
|
||||
|
||||
writeBlocks(chunk, blocks, dimMinY, height);
|
||||
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(
|
||||
Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
||||
@@ -885,20 +938,31 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
placeVanillaStructures(level, chunk, structureManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
Engine current = engine();
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
||||
super.createReferences(level, structureManager, chunk);
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
super.createReferences(level, structureManager, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
|
||||
@@ -1168,7 +1232,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
Engine current = requireDataQueryEngine("base height");
|
||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1176,17 +1245,30 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
int minY = heightAccessor.getMinY();
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
Engine current = requireDataQueryEngine("base column");
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
int surface = current.getHeight(x, z, true);
|
||||
int fluid = current.getHeight(x, z, false);
|
||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||
BlockState water = Blocks.WATER.defaultBlockState();
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
|
||||
Arrays.fill(states, 0, solidEnd, stone);
|
||||
Arrays.fill(states, solidEnd, fluidEnd, water);
|
||||
Arrays.fill(states, fluidEnd, states.length, airState);
|
||||
return new NoiseColumn(minY, states);
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
int surface = current.getHeight(x, z, true);
|
||||
int fluid = current.getHeight(x, z, false);
|
||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||
BlockState water = Blocks.WATER.defaultBlockState();
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
|
||||
Arrays.fill(states, 0, solidEnd, stone);
|
||||
Arrays.fill(states, solidEnd, fluidEnd, water);
|
||||
Arrays.fill(states, fluidEnd, states.length, airState);
|
||||
return new NoiseColumn(minY, states);
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
||||
try {
|
||||
return current.acquireGenerationLease(operation);
|
||||
} catch (GenerationSessionException exception) {
|
||||
throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+143
-15
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
||||
@@ -28,11 +29,13 @@ import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
@@ -43,6 +46,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedBlockBreakHandler {
|
||||
@@ -52,12 +56,24 @@ public final class ModdedBlockBreakHandler {
|
||||
private ModdedBlockBreakHandler() {
|
||||
}
|
||||
|
||||
public static void prepare(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
public static void prepare(
|
||||
ServerLevel level,
|
||||
ServerPlayer player,
|
||||
BlockPos position,
|
||||
BlockState brokenState
|
||||
) {
|
||||
if (ModdedTreeFellerService.isBreakProbe()) {
|
||||
return;
|
||||
}
|
||||
if (engineFor(level) == null) {
|
||||
return;
|
||||
}
|
||||
BreakKey key = new BreakKey(level, position.asLong());
|
||||
PendingBreak pending = new PendingBreak(brokenState);
|
||||
ModdedTreeFellerService treeFeller = treeFellerService();
|
||||
ModdedTreeFellerService.PreparedOrigin preparedOrigin = treeFeller == null
|
||||
? null
|
||||
: treeFeller.prepare(level, player, position, brokenState);
|
||||
PendingBreak pending = new PendingBreak(brokenState, preparedOrigin);
|
||||
PENDING.put(key, pending);
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
@@ -69,11 +85,15 @@ public final class ModdedBlockBreakHandler {
|
||||
PENDING.clear();
|
||||
}
|
||||
|
||||
public static void cancel(ServerLevel level, BlockPos position) {
|
||||
PENDING.remove(new BreakKey(level, position.asLong()));
|
||||
}
|
||||
|
||||
public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) {
|
||||
BreakKey key = new BreakKey(level, position.asLong());
|
||||
PendingBreak pending = PENDING.remove(key);
|
||||
BlockState brokenState = pending == null ? fallbackState : pending.brokenState();
|
||||
return evaluateSafely(level, position, brokenState);
|
||||
PendingBreak resolved = pending == null ? new PendingBreak(fallbackState, null) : pending;
|
||||
return evaluateSafely(level, position, resolved);
|
||||
}
|
||||
|
||||
public static Result completePrepared(ServerLevel level, BlockPos position) {
|
||||
@@ -82,7 +102,7 @@ public final class ModdedBlockBreakHandler {
|
||||
if (pending == null || level.getBlockState(position).equals(pending.brokenState())) {
|
||||
return null;
|
||||
}
|
||||
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending.brokenState()) : null;
|
||||
return PENDING.remove(key, pending) ? evaluateSafely(level, position, pending) : null;
|
||||
}
|
||||
|
||||
public static void spawn(ServerLevel level, BlockPos position, KList<ItemStack> drops) {
|
||||
@@ -115,13 +135,15 @@ public final class ModdedBlockBreakHandler {
|
||||
if (level.getBlockState(position).equals(pending.brokenState())) {
|
||||
return;
|
||||
}
|
||||
Result result = evaluateSafely(level, position, pending.brokenState());
|
||||
spawn(level, position, result.drops());
|
||||
Result result = evaluateSafely(level, position, pending);
|
||||
if (!result.routeCombinedDrops(List.of())) {
|
||||
spawn(level, position, result.drops());
|
||||
}
|
||||
}
|
||||
|
||||
private static Result evaluateSafely(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
private static Result evaluateSafely(ServerLevel level, BlockPos position, PendingBreak pending) {
|
||||
try {
|
||||
return evaluate(level, position, brokenState);
|
||||
return evaluate(level, position, pending);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
|
||||
level.dimension().identifier(), error);
|
||||
@@ -129,13 +151,56 @@ public final class ModdedBlockBreakHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static Result evaluate(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
private static Result evaluate(ServerLevel level, BlockPos position, PendingBreak pending) {
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return Result.empty();
|
||||
}
|
||||
|
||||
removeMarker(engine, position);
|
||||
Result drops = evaluateDrops(level, position, pending.brokenState(), engine);
|
||||
ModdedTreeFellerService treeFeller = treeFellerService();
|
||||
ModdedTreeFellerService.OriginDropRoute route = treeFeller == null || pending.preparedOrigin() == null
|
||||
? null
|
||||
: treeFeller.completeOrigin(pending.preparedOrigin());
|
||||
if (route == null) {
|
||||
clearTreeProvenance(engine, position);
|
||||
}
|
||||
return drops.withRoute(route);
|
||||
}
|
||||
|
||||
public static Result evaluateManagedDrops(ServerLevel level, BlockPos position, BlockState brokenState) {
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return Result.empty();
|
||||
}
|
||||
try {
|
||||
return evaluateDrops(level, position, brokenState, engine);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris managed block-drop processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
|
||||
level.dimension().identifier(), error);
|
||||
return Result.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static void completeManagedBreak(ServerLevel level, BlockPos position) {
|
||||
Engine engine = engineFor(level);
|
||||
if (engine != null && !engine.isClosed()) {
|
||||
removeMarker(engine, position);
|
||||
clearTreeProvenance(engine, position);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearPlacedProvenance(ServerLevel level, BlockPos position) {
|
||||
completeManagedBreak(level, position);
|
||||
}
|
||||
|
||||
private static Result evaluateDrops(
|
||||
ServerLevel level,
|
||||
BlockPos position,
|
||||
BlockState brokenState,
|
||||
Engine engine
|
||||
) {
|
||||
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
|
||||
if (providers.isEmpty()) {
|
||||
return Result.empty();
|
||||
@@ -155,7 +220,7 @@ public final class ModdedBlockBreakHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Result(drops, replaceVanillaDrops);
|
||||
return new Result(drops, replaceVanillaDrops, null);
|
||||
}
|
||||
|
||||
private static void removeMarker(Engine engine, BlockPos position) {
|
||||
@@ -174,6 +239,12 @@ public final class ModdedBlockBreakHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearTreeProvenance(Engine engine, BlockPos position) {
|
||||
int mantleY = position.getY() - engine.getMinHeight();
|
||||
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), String.class);
|
||||
engine.getMantle().getMantle().remove(position.getX(), mantleY, position.getZ(), TreeBlockMaterial.class);
|
||||
}
|
||||
|
||||
private static KList<IrisBlockDrops> providers(Engine engine, BlockPos position, BlockState brokenState) {
|
||||
KList<IrisBlockDrops> providers = new KList<>();
|
||||
IrisData data = engine.getData();
|
||||
@@ -227,7 +298,7 @@ public final class ModdedBlockBreakHandler {
|
||||
return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock();
|
||||
}
|
||||
|
||||
private static Engine engineFor(ServerLevel level) {
|
||||
public static Engine engineFor(ServerLevel level) {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
||||
return null;
|
||||
@@ -244,15 +315,72 @@ public final class ModdedBlockBreakHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(KList<ItemStack> drops, boolean replaceVanillaDrops) {
|
||||
private static ModdedTreeFellerService treeFellerService() {
|
||||
return ModdedEngineBootstrap.services().service(ModdedTreeFellerService.class);
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
private final KList<ItemStack> drops;
|
||||
private final boolean replaceVanillaDrops;
|
||||
private final ModdedTreeFellerService.OriginDropRoute route;
|
||||
|
||||
private Result(
|
||||
KList<ItemStack> drops,
|
||||
boolean replaceVanillaDrops,
|
||||
ModdedTreeFellerService.OriginDropRoute route
|
||||
) {
|
||||
this.drops = drops;
|
||||
this.replaceVanillaDrops = replaceVanillaDrops;
|
||||
this.route = route;
|
||||
}
|
||||
|
||||
public KList<ItemStack> drops() {
|
||||
return drops;
|
||||
}
|
||||
|
||||
public boolean replaceVanillaDrops() {
|
||||
return replaceVanillaDrops;
|
||||
}
|
||||
|
||||
public boolean routeCombinedDrops(Iterable<ItemStack> vanillaDrops) {
|
||||
if (route == null) {
|
||||
return false;
|
||||
}
|
||||
return route.route(combinedDrops(vanillaDrops));
|
||||
}
|
||||
|
||||
public KList<ItemStack> combinedDrops(Iterable<ItemStack> vanillaDrops) {
|
||||
KList<ItemStack> combined = new KList<>();
|
||||
if (!replaceVanillaDrops) {
|
||||
for (ItemStack stack : vanillaDrops) {
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
combined.add(stack.copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ItemStack stack : drops) {
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
combined.add(stack.copy());
|
||||
}
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
private Result withRoute(ModdedTreeFellerService.OriginDropRoute route) {
|
||||
return new Result(drops, replaceVanillaDrops, route);
|
||||
}
|
||||
|
||||
private static Result empty() {
|
||||
return new Result(new KList<>(), false);
|
||||
return new Result(new KList<>(), false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private record BreakKey(ServerLevel level, long position) {
|
||||
}
|
||||
|
||||
private record PendingBreak(BlockState brokenState) {
|
||||
private record PendingBreak(
|
||||
BlockState brokenState,
|
||||
ModdedTreeFellerService.PreparedOrigin preparedOrigin
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -175,12 +175,18 @@ public final class ModdedDimensionManager {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
IrisModdedChunkGenerator generator = level.getChunkSource().getGenerator()
|
||||
instanceof IrisModdedChunkGenerator irisGenerator
|
||||
? irisGenerator
|
||||
: null;
|
||||
boolean generatorUnbound = false;
|
||||
try {
|
||||
evacuate(server, level);
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
generator.unbindEngine();
|
||||
if (generator != null) {
|
||||
generator.unbindEngine(level);
|
||||
generatorUnbound = true;
|
||||
}
|
||||
ModdedWorldEngines.evict(level);
|
||||
ModdedWorldEngines.evictOrThrow(level);
|
||||
level.save(null, true, false);
|
||||
serverAccess.removeLevel(server, key);
|
||||
level.close();
|
||||
@@ -191,12 +197,31 @@ public final class ModdedDimensionManager {
|
||||
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);
|
||||
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
||||
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess,
|
||||
ResourceKey<Level> key, ServerLevel level,
|
||||
IrisModdedChunkGenerator generator, boolean generatorUnbound,
|
||||
Throwable failure) {
|
||||
try {
|
||||
if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) {
|
||||
return;
|
||||
}
|
||||
generator.bindLevel(level);
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
LOGGER.error("Iris failed to restore the engine for retained runtime dimension '{}'",
|
||||
key.identifier(), rollbackFailure);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
|
||||
+78
-18
@@ -42,6 +42,7 @@ import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -135,29 +136,82 @@ public final class ModdedEngineBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
public static void levelUnloaded(ServerLevel level) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
generator.unbindEngine(level);
|
||||
} catch (Throwable exception) {
|
||||
LOGGER.error("Iris engine unload failed for {}", level.dimension().identifier(), exception);
|
||||
if (exception instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (exception instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris engine unload failed for "
|
||||
+ level.dimension().identifier(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
MinecraftServer stoppingServer = currentServer;
|
||||
ModdedWorldCheck.serverStopped(stoppingServer);
|
||||
ModdedProtocolHandler.stop();
|
||||
ModdedPregenJob.shutdown();
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedBlockBreakHandler.clear();
|
||||
ModdedStudioCommands.clear();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
services().disableAll();
|
||||
ModdedDimensionManager.clear();
|
||||
Throwable failure = null;
|
||||
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
|
||||
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
|
||||
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
|
||||
failure = runStopStage(failure, "object undo", ModdedObjectUndo::clearAll);
|
||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
|
||||
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
|
||||
failure = runStopStage(failure, "dimension manager", ModdedDimensionManager::clear);
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
failure = runStopStage(failure, "scheduler", scheduler::shutdown);
|
||||
}
|
||||
IrisModdedChunkGenerator.shutdownGenPool();
|
||||
ModdedSentry.flush();
|
||||
ModdedStartup.reset();
|
||||
currentServer = null;
|
||||
spawnCaptureServer = null;
|
||||
initialSpawnWasDefault = false;
|
||||
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
|
||||
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
|
||||
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
|
||||
failure = runStopStage(failure, "server state", () -> {
|
||||
currentServer = null;
|
||||
spawnCaptureServer = null;
|
||||
initialSpawnWasDefault = false;
|
||||
});
|
||||
if (failure != null) {
|
||||
LOGGER.error("Iris modded shutdown completed with failures", failure);
|
||||
throw propagateStopFailure(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable runStopStage(Throwable failure, String stage, StopAction action) {
|
||||
try {
|
||||
action.run();
|
||||
return failure;
|
||||
} catch (Throwable stageFailure) {
|
||||
LOGGER.error("Iris modded shutdown stage '{}' failed", stage, stageFailure);
|
||||
if (failure == null) {
|
||||
return stageFailure;
|
||||
}
|
||||
if (stageFailure != failure) {
|
||||
failure.addSuppressed(stageFailure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException propagateStopFailure(Throwable failure) {
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
return new IllegalStateException("Iris modded shutdown completed with failures", failure);
|
||||
}
|
||||
|
||||
private static void captureInitialSpawn(MinecraftServer server) {
|
||||
@@ -317,6 +371,7 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||
createdServices.register(ModdedTreeFellerService.class, new ModdedTreeFellerService());
|
||||
|
||||
bindService(PreservationRegistry.class, preservation, rollback);
|
||||
bindService(EngineEffectsProvider.class,
|
||||
@@ -377,6 +432,11 @@ public final class ModdedEngineBootstrap {
|
||||
void restore() throws Throwable;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface StopAction {
|
||||
void run() throws Throwable;
|
||||
}
|
||||
|
||||
private static final class BindRollback {
|
||||
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
@@ -39,4 +43,8 @@ public interface ModdedLoader {
|
||||
Path configDir();
|
||||
|
||||
File modJar();
|
||||
|
||||
boolean hasTreeFellerPermission(ServerPlayer player);
|
||||
|
||||
boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state);
|
||||
}
|
||||
|
||||
+18
-16
@@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedServiceManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
@@ -92,7 +91,24 @@ public final class ModdedServiceManager {
|
||||
return;
|
||||
}
|
||||
enabled = false;
|
||||
forEachReversed(this::disableService);
|
||||
Throwable failure = null;
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = ordered.length - 1; i >= 0; i--) {
|
||||
ModdedService service = ordered[i];
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable serviceFailure) {
|
||||
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), serviceFailure);
|
||||
if (failure == null) {
|
||||
failure = serviceFailure;
|
||||
} else if (serviceFailure != failure) {
|
||||
failure.addSuppressed(serviceFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("One or more Iris services failed to disable", failure);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void rollback(Throwable failure) {
|
||||
@@ -106,14 +122,6 @@ public final class ModdedServiceManager {
|
||||
services.clear();
|
||||
}
|
||||
|
||||
private void disableService(ModdedService service) {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickService(ModdedTickableService service, MinecraftServer server) {
|
||||
try {
|
||||
service.onServerTick(server);
|
||||
@@ -144,10 +152,4 @@ public final class ModdedServiceManager {
|
||||
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
|
||||
}
|
||||
|
||||
private void forEachReversed(Consumer<ModdedService> action) {
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = ordered.length - 1; i >= 0; i--) {
|
||||
action.accept(ordered[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -102,7 +102,7 @@ public final class ModdedWorldEngines {
|
||||
ModdedEngineBootstrap.bind();
|
||||
PackValidationRegistry.requireLoadable(pack);
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
IrisData data = IrisData.get(packDir);
|
||||
IrisData data = IrisData.openRuntime(packDir);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
||||
@@ -184,17 +184,28 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
|
||||
Throwable failure = null;
|
||||
for (Map.Entry<ServerLevel, Engine> entry : new ArrayList<>(ENGINES.entrySet())) {
|
||||
ServerLevel level = entry.getKey();
|
||||
Engine engine = entry.getValue();
|
||||
try {
|
||||
if (!engine.isClosed()) {
|
||||
engine.close();
|
||||
close(engine);
|
||||
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
|
||||
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
|
||||
+ level.dimension().identifier());
|
||||
}
|
||||
LOGGER.info("Iris engine closed for {}", entry.getKey().dimension().identifier());
|
||||
LOGGER.info("Iris engine closed for {}", level.dimension().identifier());
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris engine close failed for {}", entry.getKey().dimension().identifier(), e);
|
||||
LOGGER.error("Iris engine close failed for {}", level.dimension().identifier(), e);
|
||||
if (failure == null) {
|
||||
failure = e;
|
||||
} else if (e != failure) {
|
||||
failure.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ENGINES.clear();
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("One or more Iris engines failed to close; failed mappings were retained", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-6
@@ -22,6 +22,7 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.LootResolver;
|
||||
import art.arcane.iris.engine.object.IRare;
|
||||
@@ -79,6 +80,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
private long lastAmbientAt;
|
||||
private long lastCountAt;
|
||||
private long lastInitialRecoveryAt;
|
||||
private boolean initialSpawnQueueClosed;
|
||||
private boolean mantleWarmupExecutorStopped;
|
||||
private boolean mantleWarmupsCleared;
|
||||
private volatile boolean closed;
|
||||
private volatile int cachedEntityCount;
|
||||
private volatile int cachedConsideredChunks;
|
||||
@@ -116,6 +120,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
public void serverTick(ServerLevel level) {
|
||||
EngineLifecycleTasks.run(engine, "modded_world_manager_tick", () -> runServerTick(level));
|
||||
}
|
||||
|
||||
private void runServerTick(ServerLevel level) {
|
||||
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
return;
|
||||
}
|
||||
@@ -239,7 +247,12 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
|
||||
return;
|
||||
}
|
||||
scheduler.laterGlobal(() -> runInitialFollowUp(level, chunkX, chunkZ), RNG.r.i(5, 200));
|
||||
scheduler.laterGlobal(
|
||||
() -> EngineLifecycleTasks.run(
|
||||
engine,
|
||||
"modded_world_manager_initial_spawn_followup",
|
||||
() -> runInitialFollowUp(level, chunkX, chunkZ)),
|
||||
RNG.r.i(5, 200));
|
||||
}
|
||||
|
||||
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
|
||||
@@ -272,7 +285,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mantleWarmupExecutor.execute(() -> warmupMantleChunk(key, chunkX, chunkZ));
|
||||
mantleWarmupExecutor.execute(() -> {
|
||||
if (!EngineLifecycleTasks.run(
|
||||
engine,
|
||||
"modded_world_manager_mantle_warmup",
|
||||
() -> warmupMantleChunk(key, chunkX, chunkZ))) {
|
||||
mantleWarmups.remove(key);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException e) {
|
||||
mantleWarmups.remove(key);
|
||||
}
|
||||
@@ -727,11 +747,36 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public synchronized void close() {
|
||||
closed = true;
|
||||
initialSpawnQueue.close();
|
||||
mantleWarmupExecutor.shutdownNow();
|
||||
mantleWarmups.clear();
|
||||
Throwable failure = null;
|
||||
if (!initialSpawnQueueClosed) {
|
||||
try {
|
||||
initialSpawnQueue.close();
|
||||
initialSpawnQueueClosed = true;
|
||||
} catch (Throwable e) {
|
||||
failure = e;
|
||||
}
|
||||
}
|
||||
if (!mantleWarmupExecutorStopped) {
|
||||
try {
|
||||
mantleWarmupExecutor.shutdownNow();
|
||||
mantleWarmupExecutorStopped = true;
|
||||
} catch (Throwable e) {
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
}
|
||||
if (!mantleWarmupsCleared) {
|
||||
try {
|
||||
mantleWarmups.clear();
|
||||
mantleWarmupsCleared = true;
|
||||
} catch (Throwable e) {
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Failed to completely stop the modded Iris world manager.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -757,4 +802,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
public void onSave() {
|
||||
engine.getMantle().save();
|
||||
}
|
||||
|
||||
private static Throwable appendCloseFailure(Throwable failure, Throwable next) {
|
||||
if (failure == null) {
|
||||
return next;
|
||||
}
|
||||
if (failure != next) {
|
||||
failure.addSuppressed(next);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
|
||||
+79
-24
@@ -23,6 +23,8 @@ import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
@@ -40,6 +42,7 @@ import art.arcane.iris.modded.ModdedLoader;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
@@ -95,7 +98,11 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -104,6 +111,7 @@ public final class IrisModdedCommands {
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final long LOCATE_TIMEOUT_MS = 120000L;
|
||||
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
|
||||
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
|
||||
|
||||
private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
|
||||
@@ -1172,32 +1180,79 @@ public final class IrisModdedCommands {
|
||||
int chunkX = player.blockPosition().getX() >> 4;
|
||||
int chunkZ = player.blockPosition().getZ() >> 4;
|
||||
ok(source, "Searching for " + label + "...");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
Position2 at = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
|
||||
}).get();
|
||||
if (at == null) {
|
||||
server.execute(() -> fail(source, "Could not find " + label + " within the search timeout."));
|
||||
return;
|
||||
CompletableFuture<Position2> search;
|
||||
try {
|
||||
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
|
||||
});
|
||||
} catch (WrongEngineBroException e) {
|
||||
fail(source, "The engine for this world has been closed; rejoin the dimension and try again.");
|
||||
return;
|
||||
}
|
||||
UUID playerId = player.getUUID();
|
||||
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
|
||||
if (previous != null && previous != search) {
|
||||
previous.cancel(true);
|
||||
}
|
||||
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
|
||||
source, level, engine, player, label, server, playerId, search, at, error));
|
||||
}
|
||||
|
||||
private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||
ServerPlayer player, String label, MinecraftServer server, UUID playerId,
|
||||
CompletableFuture<Position2> search, Position2 at, Throwable error) {
|
||||
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
|
||||
return;
|
||||
}
|
||||
Throwable failure = unwrapCompletionFailure(error);
|
||||
if (failure instanceof CancellationException) {
|
||||
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
|
||||
return;
|
||||
}
|
||||
if (failure != null) {
|
||||
LOGGER.error("Iris locate failed for {}", label, failure);
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
fail(source, "Search failed: " + failure);
|
||||
}
|
||||
int blockX = (at.getX() << 4) + 8;
|
||||
int blockZ = (at.getZ() << 4) + 8;
|
||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
server.execute(() -> {
|
||||
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
|
||||
});
|
||||
} catch (WrongEngineBroException e) {
|
||||
server.execute(() -> fail(source, "The engine for this world has been closed; rejoin the dimension and try again."));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (ExecutionException e) {
|
||||
LOGGER.error("Iris locate failed for {}", label, e);
|
||||
server.execute(() -> fail(source, "Search failed: " + e.getCause()));
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (at == null) {
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
fail(source, "Could not find " + label + " within the search timeout.");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
server.execute(() -> {
|
||||
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||
teleportToLocateResult(source, level, engine, player, label, at);
|
||||
}
|
||||
}, "Iris Locator");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
});
|
||||
}
|
||||
|
||||
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||
ServerPlayer player, String label, Position2 at) {
|
||||
int blockX = (at.getX() << 4) + 8;
|
||||
int blockZ = (at.getZ() << 4) + 8;
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
|
||||
} catch (GenerationSessionException e) {
|
||||
fail(source, "The engine changed while locating " + label + "; try again.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable unwrapCompletionFailure(Throwable error) {
|
||||
Throwable failure = error;
|
||||
while ((failure instanceof CompletionException || failure instanceof ExecutionException)
|
||||
&& failure.getCause() != null) {
|
||||
failure = failure.getCause();
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static int seed(CommandSourceStack source) {
|
||||
|
||||
+92
-4
@@ -35,8 +35,13 @@ import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public final class ModdedPregenJob {
|
||||
private static final long SHUTDOWN_TIMEOUT_MILLIS = 10_000L;
|
||||
private static final AtomicReference<ActivePregen> ACTIVE = new AtomicReference<>();
|
||||
private static volatile String dimension = "?";
|
||||
|
||||
private ModdedPregenJob() {
|
||||
@@ -54,13 +59,26 @@ public final class ModdedPregenJob {
|
||||
.radiusX(radiusBlocks)
|
||||
.radiusZ(radiusBlocks)
|
||||
.build();
|
||||
PregeneratorMethod method = new ModdedPregenMethod(level, engine, sync);
|
||||
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
|
||||
PregeneratorMethod method = moddedMethod;
|
||||
if (cached) {
|
||||
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
|
||||
}
|
||||
ActivePregen active = new ActivePregen(engine, moddedMethod);
|
||||
ACTIVE.set(active);
|
||||
dimension = level.dimension().identifier().toString();
|
||||
new PregeneratorJob(task, method, engine);
|
||||
return true;
|
||||
try {
|
||||
PregeneratorJob job = new PregeneratorJob(task, method, engine);
|
||||
job.whenDone(() -> {
|
||||
if (!moddedMethod.hasPendingFinalSave()) {
|
||||
ACTIVE.compareAndSet(active, null);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (Throwable failure) {
|
||||
ACTIVE.compareAndSet(active, null);
|
||||
throw propagate(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static File cacheDirectory(ServerLevel level) {
|
||||
@@ -73,7 +91,17 @@ public final class ModdedPregenJob {
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
PregeneratorJob.shutdownAndWait(10_000L);
|
||||
ActivePregen active = ACTIVE.get();
|
||||
shutdownAndSave(active, () -> PregeneratorJob.shutdownAndWait(SHUTDOWN_TIMEOUT_MILLIS));
|
||||
}
|
||||
|
||||
public static boolean shutdownForWorld(String worldIdentity) {
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
if (job == null || !job.targetsWorldIdentity(worldIdentity)) {
|
||||
return false;
|
||||
}
|
||||
ActivePregen active = matchingActive(worldIdentity);
|
||||
return shutdownAndSave(active, () -> PregeneratorJob.shutdownInstanceForWorld(worldIdentity));
|
||||
}
|
||||
|
||||
public static Boolean pauseResume() {
|
||||
@@ -125,4 +153,64 @@ public final class ModdedPregenJob {
|
||||
status.append(ModdedCommandFeedback.footer());
|
||||
return status;
|
||||
}
|
||||
|
||||
private static ActivePregen matchingActive(String worldIdentity) {
|
||||
ActivePregen active = ACTIVE.get();
|
||||
return active != null && active.targets(worldIdentity) ? active : null;
|
||||
}
|
||||
|
||||
private static boolean shutdownAndSave(ActivePregen active, BooleanSupplier shutdown) {
|
||||
boolean deferred = active != null && active.method().deferFinalSaveToServerThread();
|
||||
boolean stopped = false;
|
||||
boolean result = false;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
result = shutdown.getAsBoolean();
|
||||
stopped = result;
|
||||
} catch (Throwable shutdownFailure) {
|
||||
failure = shutdownFailure;
|
||||
}
|
||||
|
||||
if (deferred) {
|
||||
try {
|
||||
if (stopped) {
|
||||
active.method().completeDeferredFinalSave();
|
||||
} else {
|
||||
active.method().cancelDeferredFinalSave();
|
||||
}
|
||||
} catch (Throwable saveFailure) {
|
||||
if (failure == null) {
|
||||
failure = saveFailure;
|
||||
} else if (saveFailure != failure) {
|
||||
failure.addSuppressed(saveFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failure == null && (stopped || PregeneratorJob.getInstance() == null)) {
|
||||
ACTIVE.compareAndSet(active, null);
|
||||
}
|
||||
if (failure != null) {
|
||||
throw propagate(failure);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static RuntimeException propagate(Throwable failure) {
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
return new IllegalStateException("Iris modded pregenerator shutdown failed", failure);
|
||||
}
|
||||
|
||||
private record ActivePregen(Engine engine, ModdedPregenMethod method) {
|
||||
private boolean targets(String worldIdentity) {
|
||||
return engine != null
|
||||
&& engine.getWorld() != null
|
||||
&& Objects.equals(engine.getWorld().identity(), worldIdentity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+184
-14
@@ -40,14 +40,18 @@ import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final TicketType PREGEN_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
|
||||
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||
private static final long ADAPTIVE_RECOVERY_INTERVAL = 64L;
|
||||
private static final long FINAL_SAVE_TIMEOUT_MILLIS = 10_000L;
|
||||
private static final long FINAL_SAVE_POLL_MILLIS = 50L;
|
||||
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||
|
||||
private final ServerLevel level;
|
||||
@@ -62,6 +66,10 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private final AtomicInteger adaptiveLimit;
|
||||
private final AtomicInteger timeoutStreak = new AtomicInteger();
|
||||
private final AtomicLong completed = new AtomicLong();
|
||||
private final AtomicBoolean finalSaveOnServerThread = new AtomicBoolean(false);
|
||||
private final AtomicBoolean finalSaveDeferred = new AtomicBoolean(false);
|
||||
private final AtomicBoolean finalSaveCompleted = new AtomicBoolean(false);
|
||||
private final AtomicReference<FinalSaveRequest> queuedFinalSave = new AtomicReference<>();
|
||||
private final int timeoutSeconds;
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
|
||||
@@ -114,6 +122,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
LOGGER.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
|
||||
level.dimension().identifier(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
|
||||
if (deferFinalSaveIfRequested()) {
|
||||
return;
|
||||
}
|
||||
saveLevel(true);
|
||||
}
|
||||
|
||||
@@ -122,24 +133,156 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
saveLevel(false);
|
||||
}
|
||||
|
||||
private void saveLevel(boolean wait) {
|
||||
CompletableFuture<Void> saved = new CompletableFuture<>();
|
||||
level.getServer().execute(() -> {
|
||||
try {
|
||||
level.save(null, false, false);
|
||||
} finally {
|
||||
saved.complete(null);
|
||||
boolean deferFinalSaveToServerThread() {
|
||||
if (!level.getServer().isSameThread()) {
|
||||
return false;
|
||||
}
|
||||
finalSaveOnServerThread.set(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void completeDeferredFinalSave() {
|
||||
requireServerThreadForFinalSave();
|
||||
cancelQueuedFinalSave();
|
||||
if (!finalSaveCompleted.get()) {
|
||||
saveLevelOnServerThread();
|
||||
finalSaveCompleted.set(true);
|
||||
}
|
||||
finalSaveDeferred.set(false);
|
||||
finalSaveOnServerThread.set(false);
|
||||
}
|
||||
|
||||
void cancelDeferredFinalSave() {
|
||||
requireServerThreadForFinalSave();
|
||||
finalSaveOnServerThread.set(false);
|
||||
if (finalSaveDeferred.get() || queuedFinalSave.get() != null) {
|
||||
cancelQueuedFinalSave();
|
||||
if (!finalSaveCompleted.get()) {
|
||||
saveLevelOnServerThread();
|
||||
finalSaveCompleted.set(true);
|
||||
}
|
||||
});
|
||||
if (!wait) {
|
||||
finalSaveDeferred.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasPendingFinalSave() {
|
||||
return finalSaveOnServerThread.get()
|
||||
|| finalSaveDeferred.get()
|
||||
|| queuedFinalSave.get() != null;
|
||||
}
|
||||
|
||||
private void saveLevel(boolean wait) {
|
||||
if (wait) {
|
||||
saveFinalLevel();
|
||||
return;
|
||||
}
|
||||
MinecraftServer server = level.getServer();
|
||||
if (server.isSameThread()) {
|
||||
saveLevelOnServerThread();
|
||||
return;
|
||||
}
|
||||
server.execute(this::saveLevelOnServerThread);
|
||||
}
|
||||
|
||||
private void saveFinalLevel() {
|
||||
MinecraftServer server = level.getServer();
|
||||
if (server.isSameThread()) {
|
||||
saveLevelOnServerThread();
|
||||
finalSaveCompleted.set(true);
|
||||
return;
|
||||
}
|
||||
if (deferFinalSaveIfRequested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FinalSaveRequest request = new FinalSaveRequest();
|
||||
if (!queuedFinalSave.compareAndSet(null, request)) {
|
||||
throw new IllegalStateException("Iris pregen final save is already queued for "
|
||||
+ level.dimension().identifier());
|
||||
}
|
||||
try {
|
||||
server.execute(() -> executeFinalSave(request));
|
||||
} catch (RuntimeException | Error failure) {
|
||||
queuedFinalSave.compareAndSet(request, null);
|
||||
request.fail(failure);
|
||||
throw failure;
|
||||
}
|
||||
awaitFinalSave(request);
|
||||
}
|
||||
|
||||
private void executeFinalSave(FinalSaveRequest request) {
|
||||
if (!request.claim()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
saved.get(10, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
|
||||
saveLevelOnServerThread();
|
||||
finalSaveCompleted.set(true);
|
||||
request.complete();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
request.fail(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
queuedFinalSave.compareAndSet(request, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitFinalSave(FinalSaveRequest request) {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||
while (true) {
|
||||
if (deferFinalSaveIfRequested()) {
|
||||
return;
|
||||
}
|
||||
long remainingNanos = deadline - System.nanoTime();
|
||||
if (remainingNanos <= 0L) {
|
||||
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
|
||||
return;
|
||||
}
|
||||
long waitMillis = Math.max(1L, Math.min(FINAL_SAVE_POLL_MILLIS,
|
||||
TimeUnit.NANOSECONDS.toMillis(remainingNanos)));
|
||||
try {
|
||||
request.completion().get(waitMillis, TimeUnit.MILLISECONDS);
|
||||
return;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (TimeoutException e) {
|
||||
continue;
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
LOGGER.error("Iris pregen level save failed for {}", level.dimension().identifier(), cause);
|
||||
throw new IllegalStateException("Iris pregen level save failed for "
|
||||
+ level.dimension().identifier(), cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deferFinalSaveIfRequested() {
|
||||
if (!finalSaveOnServerThread.get()) {
|
||||
return false;
|
||||
}
|
||||
finalSaveDeferred.set(true);
|
||||
if (finalSaveOnServerThread.get()) {
|
||||
return true;
|
||||
}
|
||||
finalSaveDeferred.set(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void cancelQueuedFinalSave() {
|
||||
FinalSaveRequest request = queuedFinalSave.getAndSet(null);
|
||||
if (request != null) {
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveLevelOnServerThread() {
|
||||
level.save(null, false, false);
|
||||
}
|
||||
|
||||
private void requireServerThreadForFinalSave() {
|
||||
if (!level.getServer().isSameThread()) {
|
||||
throw new IllegalStateException("Iris pregen final save must run on the Minecraft server thread for "
|
||||
+ level.dimension().identifier());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,4 +504,31 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
public Mantle getMantle() {
|
||||
return engine.getMantle().getMantle();
|
||||
}
|
||||
|
||||
private static final class FinalSaveRequest {
|
||||
private final CompletableFuture<Void> completion = new CompletableFuture<>();
|
||||
private final AtomicBoolean active = new AtomicBoolean(true);
|
||||
|
||||
private boolean claim() {
|
||||
return active.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
private void complete() {
|
||||
completion.complete(null);
|
||||
}
|
||||
|
||||
private void fail(Throwable failure) {
|
||||
active.set(false);
|
||||
completion.completeExceptionally(failure);
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
active.set(false);
|
||||
completion.complete(null);
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> completion() {
|
||||
return completion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+121
-101
@@ -19,13 +19,13 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
|
||||
import art.arcane.iris.core.runtime.GoldenHashEngine;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.core.service.EngineMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.modded.ModdedWorldEngines;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -33,71 +33,74 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long TRIM_PERIOD_MILLIS = 2_000L;
|
||||
private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L;
|
||||
private static final long SAVE_PERIOD_MILLIS = 60_000L;
|
||||
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
|
||||
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
|
||||
|
||||
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
|
||||
private final Set<Engine> inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private final Map<Engine, Long> lastSavedAt = Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
private volatile ExecutorService service;
|
||||
private long lastMaintenanceAt;
|
||||
private long lastSaveAt;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (service != null) {
|
||||
ExecutorService current = service;
|
||||
if (current != null && !current.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
|
||||
IrisSettings.IrisSettingsEngineSVC engineSettings = settings.getEngineSVC();
|
||||
ThreadFactory factory = (engineSettings.isUseVirtualThreads()
|
||||
|
||||
IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC();
|
||||
ThreadFactory factory = (settings.isUseVirtualThreads()
|
||||
? Thread.ofVirtual()
|
||||
: Thread.ofPlatform().priority(engineSettings.getPriority()))
|
||||
: Thread.ofPlatform().priority(settings.getPriority()))
|
||||
.name("Iris EngineSVC-", 0)
|
||||
.factory();
|
||||
service = Executors.newThreadPerTaskExecutor(factory);
|
||||
tectonicLimit.set(settings.getTectonicPlateSize());
|
||||
service = Executors.newFixedThreadPool(EngineMaintenance.workerParallelism(), factory);
|
||||
lastMaintenanceAt = 0L;
|
||||
lastSaveAt = System.currentTimeMillis();
|
||||
inFlight.clear();
|
||||
lastSavedAt.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = service;
|
||||
service = null;
|
||||
if (active != null) {
|
||||
active.shutdown();
|
||||
boolean drained = shutdownAndDrain(active);
|
||||
if (drained) {
|
||||
inFlight.clear();
|
||||
}
|
||||
lastSavedAt.clear();
|
||||
if (!drained) {
|
||||
throw new IllegalStateException("Iris engine maintenance workers remained active during service shutdown");
|
||||
}
|
||||
inFlight.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
ExecutorService active = service;
|
||||
if (active == null) {
|
||||
if (active == null || active.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastMaintenanceAt < TRIM_PERIOD_MILLIS) {
|
||||
if (now - lastMaintenanceAt < MAINTENANCE_PERIOD_MILLIS) {
|
||||
return;
|
||||
}
|
||||
lastMaintenanceAt = now;
|
||||
boolean flush = now - lastSaveAt >= SAVE_PERIOD_MILLIS;
|
||||
if (flush) {
|
||||
lastSaveAt = now;
|
||||
}
|
||||
|
||||
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
|
||||
int share = tectonicLimit.get() / Math.max(engines.size(), 1);
|
||||
reconcileSaveState(engines, now);
|
||||
for (Engine engine : engines) {
|
||||
if (!inFlight.add(engine)) {
|
||||
continue;
|
||||
@@ -105,101 +108,118 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi
|
||||
try {
|
||||
active.execute(() -> {
|
||||
try {
|
||||
maintain(engine, share, flush);
|
||||
maintain(engine, now);
|
||||
} finally {
|
||||
inFlight.remove(engine);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
} catch (RejectedExecutionException exception) {
|
||||
inFlight.remove(engine);
|
||||
if (active == service && !active.isShutdown()) {
|
||||
LOGGER.error("Iris rejected engine maintenance for {}", engineName(engine), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maintain(Engine engine, int share, boolean flush) {
|
||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
private void maintain(Engine engine, long scheduledAt) {
|
||||
if (engine == null || engine.isClosing() || engine.isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (pregenTargets(engine)) {
|
||||
return;
|
||||
}
|
||||
if (flush) {
|
||||
try {
|
||||
engine.save();
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
LOGGER.error("Iris engine save failed for {}", engine.getWorld().name(), e);
|
||||
}
|
||||
}
|
||||
if (!shouldReduce(engine) || shouldSkipForMaintenance(engine) || GoldenHashEngine.isActive()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
|
||||
engine.getMantle().trim(0L, 0);
|
||||
} else {
|
||||
engine.getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), activeTectonicLimit(engine, share));
|
||||
}
|
||||
long unloadStart = System.currentTimeMillis();
|
||||
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
|
||||
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine, share);
|
||||
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
|
||||
if (heapPressure && MantleHeapPressure.overPanicWater()) {
|
||||
MantleHeapPressure.requestPanicReclaim();
|
||||
}
|
||||
if (count > 0) {
|
||||
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}", count, System.currentTimeMillis() - unloadStart, engine.getWorld().name());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_engine_maintenance");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!EngineMaintenance.isAvailable(engine)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
LOGGER.error("Iris engine maintenance failed for {}", engine.getWorld().name(), e);
|
||||
|
||||
boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine);
|
||||
if (pregeneratorTargetsWorld) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveIfDue(engine, scheduledAt);
|
||||
if (!EngineMaintenance.shouldRun(engine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine);
|
||||
if (outcome.unloadedTectonicPlates() > 0) {
|
||||
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}",
|
||||
outcome.unloadedTectonicPlates(), outcome.unloadDurationMillis(), engineName(engine));
|
||||
}
|
||||
} catch (GenerationSessionException exception) {
|
||||
if (engine.isClosing() || exception.isExpectedTeardown()) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(exception);
|
||||
LOGGER.error("Iris engine maintenance session failed for {}", engineName(engine), exception);
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(exception);
|
||||
LOGGER.error("Iris engine maintenance failed for {}", engineName(engine), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReduce(Engine engine) {
|
||||
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
||||
private void saveIfDue(Engine engine, long scheduledAt) {
|
||||
Long lastSave = lastSavedAt.get(engine);
|
||||
if (lastSave == null || scheduledAt - lastSave < SAVE_PERIOD_MILLIS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
engine.save();
|
||||
lastSavedAt.put(engine, System.currentTimeMillis());
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(exception);
|
||||
LOGGER.error("Iris engine save failed for {}", engineName(engine), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void reconcileSaveState(Collection<Engine> engines, long now) {
|
||||
Set<Engine> activeEngines = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
activeEngines.addAll(engines);
|
||||
synchronized (lastSavedAt) {
|
||||
lastSavedAt.keySet().removeIf(engine -> !activeEngines.contains(engine));
|
||||
for (Engine engine : activeEngines) {
|
||||
lastSavedAt.putIfAbsent(engine, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shutdownAndDrain(ExecutorService active) {
|
||||
if (active == null) {
|
||||
return true;
|
||||
}
|
||||
return pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean shouldSkipForMaintenance(Engine engine) {
|
||||
if (engine.getWorld() == null || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity())) {
|
||||
return false;
|
||||
}
|
||||
return !pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean pregenTargets(Engine engine) {
|
||||
if (engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldIdentity(engine.getWorld().identity());
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(Engine engine, int share) {
|
||||
if (!pregenTargets(engine)) {
|
||||
return share;
|
||||
}
|
||||
return Math.max(share, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
|
||||
}
|
||||
|
||||
private static boolean isMantleClosed(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
|
||||
active.shutdown();
|
||||
try {
|
||||
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
active.shutdownNow();
|
||||
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
IllegalStateException failure = new IllegalStateException(
|
||||
"Iris engine maintenance workers did not stop after shutdownNow");
|
||||
IrisLogging.reportError(failure);
|
||||
LOGGER.error("Iris engine maintenance did not terminate; active engine lifecycle leases will block unsafe shutdown", failure);
|
||||
return false;
|
||||
} catch (InterruptedException exception) {
|
||||
active.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
IrisLogging.reportError(exception);
|
||||
LOGGER.error("Interrupted while draining Iris engine maintenance", exception);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String engineName(Engine engine) {
|
||||
return engine == null || engine.getWorld() == null ? "<unbound>" : engine.getWorld().name();
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import org.slf4j.Logger;
|
||||
@@ -55,6 +56,7 @@ public final class ModdedPreservationService implements ModdedService, Preservat
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
IrisData.dereference();
|
||||
threads.removeIf((Thread thread) -> !thread.isAlive());
|
||||
services.removeIf(ExecutorService::isShutdown);
|
||||
caches.removeIf((WeakReference<MeteredCache> ref) -> {
|
||||
|
||||
+5
-1
@@ -30,6 +30,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
@@ -180,7 +181,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
|
||||
@Override
|
||||
public void shutdownPregenerator(Engine engine) {
|
||||
PregeneratorJob.shutdownInstance();
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (world != null) {
|
||||
ModdedPregenJob.shutdownForWorld(world.identity());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.BlockParticleOption;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
final class ModdedTreeFellerPresentation {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final int MIN_BLOCKS_PER_PULSE = 4;
|
||||
private static final int MAX_BLOCKS_PER_PULSE = 64;
|
||||
private static final int TARGET_EROSION_PULSES = 60;
|
||||
private static final int MAX_EFFECT_ORIGINS_PER_PULSE = 16;
|
||||
|
||||
private final ServerPlayer player;
|
||||
private final ServerLevel sourceLevel;
|
||||
private final List<ItemStack> pendingDrops = new ArrayList<>();
|
||||
private final AtomicBoolean effectFailureReported = new AtomicBoolean();
|
||||
private final AtomicBoolean deliveryFailureReported = new AtomicBoolean();
|
||||
private boolean flushScheduled;
|
||||
private double fallbackX;
|
||||
private double fallbackY;
|
||||
private double fallbackZ;
|
||||
|
||||
ModdedTreeFellerPresentation(ServerPlayer player, ServerLevel sourceLevel) {
|
||||
this.player = player;
|
||||
this.sourceLevel = sourceLevel;
|
||||
this.fallbackX = player.getX();
|
||||
this.fallbackY = player.getY() + 0.15D;
|
||||
this.fallbackZ = player.getZ();
|
||||
}
|
||||
|
||||
static int blocksPerPulse(int blockCount) {
|
||||
int requested = Math.max(1, (blockCount + TARGET_EROSION_PULSES - 1) / TARGET_EROSION_PULSES);
|
||||
return Math.max(MIN_BLOCKS_PER_PULSE, Math.min(requested, MAX_BLOCKS_PER_PULSE));
|
||||
}
|
||||
|
||||
static int effectStride(int blocksPerPulse) {
|
||||
return Math.max(
|
||||
1,
|
||||
(blocksPerPulse + MAX_EFFECT_ORIGINS_PER_PULSE - 1) / MAX_EFFECT_ORIGINS_PER_PULSE
|
||||
);
|
||||
}
|
||||
|
||||
static List<ItemStack> consolidateDrops(Collection<ItemStack> drops) {
|
||||
List<ItemStack> consolidated = new ArrayList<>();
|
||||
for (ItemStack drop : drops) {
|
||||
mergeDrop(consolidated, drop);
|
||||
}
|
||||
return List.copyOf(consolidated);
|
||||
}
|
||||
|
||||
void activate(BlockPos position, BlockState state) {
|
||||
try {
|
||||
double x = position.getX() + 0.5D;
|
||||
double y = position.getY() + 0.5D;
|
||||
double z = position.getZ() + 0.5D;
|
||||
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 24, 0.45D, 0.45D, 0.45D, 0.18D);
|
||||
sourceLevel.sendParticles(ParticleTypes.END_ROD, x, y, z, 8, 0.25D, 0.25D, 0.25D, 0.035D);
|
||||
sourceLevel.sendParticles(
|
||||
new BlockParticleOption(ParticleTypes.BLOCK, state),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
8,
|
||||
0.25D,
|
||||
0.25D,
|
||||
0.25D,
|
||||
0.04D
|
||||
);
|
||||
sourceLevel.playSound(null, position, SoundEvents.ENCHANTMENT_TABLE_USE, SoundSource.PLAYERS, 0.55F, 1.35F);
|
||||
sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.4F, 0.8F);
|
||||
} catch (Throwable error) {
|
||||
reportEffectFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
void erode(BlockPos position, BlockState state, int processed, int effectStride, float pitch) {
|
||||
if (processed % effectStride != 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
double x = position.getX() + 0.5D;
|
||||
double y = position.getY() + 0.5D;
|
||||
double z = position.getZ() + 0.5D;
|
||||
sourceLevel.sendParticles(
|
||||
new BlockParticleOption(ParticleTypes.BLOCK, state),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
5,
|
||||
0.3D,
|
||||
0.3D,
|
||||
0.3D,
|
||||
0.04D
|
||||
);
|
||||
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y, z, 3, 0.28D, 0.28D, 0.28D, 0.12D);
|
||||
sourceLevel.playSound(null, position, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.22F, pitch);
|
||||
} catch (Throwable error) {
|
||||
reportEffectFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean route(Iterable<ItemStack> drops) {
|
||||
for (ItemStack drop : drops) {
|
||||
if (drop != null && !drop.isEmpty()) {
|
||||
pendingDrops.add(drop.copy());
|
||||
}
|
||||
}
|
||||
scheduleFlush();
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized void flush() {
|
||||
flushScheduled = false;
|
||||
if (pendingDrops.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<ItemStack> drops = consolidateDrops(pendingDrops);
|
||||
pendingDrops.clear();
|
||||
boolean atPlayer = !player.isRemoved() && player.level() == sourceLevel;
|
||||
double x = atPlayer ? player.getX() : fallbackX;
|
||||
double y = atPlayer ? player.getY() + 0.15D : fallbackY;
|
||||
double z = atPlayer ? player.getZ() : fallbackZ;
|
||||
if (atPlayer) {
|
||||
fallbackX = x;
|
||||
fallbackY = y;
|
||||
fallbackZ = z;
|
||||
}
|
||||
int delivered = 0;
|
||||
for (ItemStack drop : drops) {
|
||||
try {
|
||||
ItemEntity item = new ItemEntity(sourceLevel, x, y, z, drop, 0D, 0.08D, 0D);
|
||||
item.setDefaultPickUpDelay();
|
||||
if (sourceLevel.addFreshEntity(item)) {
|
||||
delivered++;
|
||||
} else {
|
||||
pendingDrops.add(drop.copy());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
pendingDrops.add(drop.copy());
|
||||
reportDeliveryFailure(error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
int particles = Math.min(32, 6 + (delivered * 2));
|
||||
sourceLevel.sendParticles(ParticleTypes.ENCHANT, x, y + 0.35D, z, particles, 0.3D, 0.25D, 0.3D, 0.1D);
|
||||
sourceLevel.playSound(null, x, y, z, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.28F, 1.75F);
|
||||
} catch (Throwable error) {
|
||||
reportEffectFailure(error);
|
||||
}
|
||||
if (!pendingDrops.isEmpty()) {
|
||||
scheduleFlush();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void finish() {
|
||||
flush();
|
||||
}
|
||||
|
||||
private synchronized void scheduleFlush() {
|
||||
if (pendingDrops.isEmpty() || flushScheduled) {
|
||||
return;
|
||||
}
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
flushScheduled = true;
|
||||
scheduler.laterGlobal(this::flush, 1);
|
||||
}
|
||||
|
||||
private static void mergeDrop(List<ItemStack> consolidated, ItemStack drop) {
|
||||
if (drop == null || drop.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ItemStack remaining = drop.copy();
|
||||
for (ItemStack existing : consolidated) {
|
||||
if (!ItemStack.isSameItemSameComponents(existing, remaining)) {
|
||||
continue;
|
||||
}
|
||||
int capacity = existing.getMaxStackSize() - existing.getCount();
|
||||
if (capacity <= 0) {
|
||||
continue;
|
||||
}
|
||||
int moved = Math.min(capacity, remaining.getCount());
|
||||
existing.grow(moved);
|
||||
remaining.shrink(moved);
|
||||
if (remaining.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (!remaining.isEmpty()) {
|
||||
int amount = Math.min(remaining.getCount(), remaining.getMaxStackSize());
|
||||
consolidated.add(remaining.copyWithCount(amount));
|
||||
remaining.shrink(amount);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportEffectFailure(Throwable error) {
|
||||
if (effectFailureReported.compareAndSet(false, true)) {
|
||||
LOGGER.error("Iris modded tree-feller presentation failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportDeliveryFailure(Throwable error) {
|
||||
if (deliveryFailureReported.compareAndSet(false, true)) {
|
||||
LOGGER.error("Iris modded tree-feller drop delivery failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
|
||||
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public final class ModdedTreeFellerService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final ThreadLocal<Integer> BREAK_PROBE_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
private final AtomicBoolean enabled = new AtomicBoolean();
|
||||
private final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
|
||||
private final Set<FellingRun> activeRuns = ConcurrentHashMap.newKeySet();
|
||||
private final Map<Engine, TreeDefinitionIndex> definitions = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public static boolean isBreakProbe() {
|
||||
return BREAK_PROBE_DEPTH.get() > 0;
|
||||
}
|
||||
|
||||
public static boolean runBreakProbe(BooleanSupplier probe) {
|
||||
int depth = BREAK_PROBE_DEPTH.get();
|
||||
BREAK_PROBE_DEPTH.set(depth + 1);
|
||||
try {
|
||||
return probe.getAsBoolean();
|
||||
} finally {
|
||||
if (depth == 0) {
|
||||
BREAK_PROBE_DEPTH.remove();
|
||||
} else {
|
||||
BREAK_PROBE_DEPTH.set(depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
enabled.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
enabled.set(false);
|
||||
for (FellingRun run : List.copyOf(activeRuns)) {
|
||||
finish(run);
|
||||
}
|
||||
activeRuns.clear();
|
||||
activeClaims.clear();
|
||||
definitions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
for (FellingRun run : List.copyOf(activeRuns)) {
|
||||
if (!isRunControlActive(run)) {
|
||||
finish(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedOrigin prepare(
|
||||
ServerLevel level,
|
||||
ServerPlayer player,
|
||||
BlockPos position,
|
||||
BlockState state
|
||||
) {
|
||||
IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller();
|
||||
if (!enabled.get()
|
||||
|| settings == null
|
||||
|| !settings.isEnabled()
|
||||
|| !player.gameMode().isSurvival()
|
||||
|| !player.isShiftKeyDown()
|
||||
|| !state.is(BlockTags.LOGS)
|
||||
|| !player.getInventory().getSelectedItem().is(ItemTags.AXES)
|
||||
|| !ModdedEngineBootstrap.loader().hasTreeFellerPermission(player)) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = ModdedBlockBreakHandler.engineFor(level);
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
int minimumY = level.getMinY();
|
||||
String marker = markerAt(engine, minimumY, position);
|
||||
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||
if (decoded == null || decoded.structureAware()) {
|
||||
return null;
|
||||
}
|
||||
TreeBlockMaterial expectedMaterial = materialAt(engine, minimumY, position);
|
||||
if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) {
|
||||
return null;
|
||||
}
|
||||
if (expectedMaterial == null
|
||||
&& !decoded.objectKey().startsWith("trees/")
|
||||
&& !definitionIndex(engine).isTreeMarker(marker)) {
|
||||
return null;
|
||||
}
|
||||
ItemStack tool = player.getInventory().getSelectedItem();
|
||||
return new PreparedOrigin(
|
||||
this,
|
||||
level,
|
||||
player,
|
||||
position.immutable(),
|
||||
state,
|
||||
engine,
|
||||
marker,
|
||||
minimumY,
|
||||
level.getMaxY(),
|
||||
player.getInventory().getSelectedSlot(),
|
||||
tool.copy(),
|
||||
settings.getDurabilityPreservationChance()
|
||||
);
|
||||
}
|
||||
|
||||
public OriginDropRoute completeOrigin(PreparedOrigin prepared) {
|
||||
if (prepared == null
|
||||
|| prepared.owner() != this
|
||||
|| !enabled.get()
|
||||
|| prepared.engine().isClosed()) {
|
||||
return null;
|
||||
}
|
||||
TreeClaim claim = new TreeClaim(prepared.level(), prepared.marker());
|
||||
if (!activeClaims.add(claim)) {
|
||||
return null;
|
||||
}
|
||||
ModdedTreeFellerPresentation presentation = new ModdedTreeFellerPresentation(
|
||||
prepared.player(),
|
||||
prepared.level()
|
||||
);
|
||||
FellingRun run = new FellingRun(claim, prepared, presentation);
|
||||
if (!normalizeOriginTool(run)) {
|
||||
activeClaims.remove(claim);
|
||||
return null;
|
||||
}
|
||||
activeRuns.add(run);
|
||||
presentation.activate(prepared.position(), prepared.state());
|
||||
OriginDropRoute route = presentation::route;
|
||||
if (run.toolBroken) {
|
||||
ModdedBlockBreakHandler.completeManagedBreak(prepared.level(), prepared.position());
|
||||
finish(run);
|
||||
return route;
|
||||
}
|
||||
discover(run);
|
||||
return route;
|
||||
}
|
||||
|
||||
private TreeDefinitionIndex definitionIndex(Engine engine) {
|
||||
synchronized (definitions) {
|
||||
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean normalizeOriginTool(FellingRun run) {
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
ItemStack before = prepared.toolBefore();
|
||||
ItemStack current = prepared.player().getInventory().getItem(prepared.heldSlot());
|
||||
boolean currentMatches = !current.isEmpty()
|
||||
&& ItemStack.matchesIgnoringComponents(
|
||||
before,
|
||||
current,
|
||||
(componentType) -> componentType == DataComponents.DAMAGE
|
||||
);
|
||||
if (!before.isDamageableItem()) {
|
||||
if (!currentMatches || !ItemStack.isSameItemSameComponents(before, current)) {
|
||||
return false;
|
||||
}
|
||||
run.expectedTool = current.copy();
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean preserve = ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance();
|
||||
int desiredDamage = before.getDamageValue() + (preserve ? 0 : 1);
|
||||
if (desiredDamage >= before.getMaxDamage()) {
|
||||
if (!current.isEmpty() && !currentMatches) {
|
||||
return false;
|
||||
}
|
||||
Item brokenItem = before.getItem();
|
||||
prepared.player().getInventory().setItem(prepared.heldSlot(), ItemStack.EMPTY);
|
||||
if (!current.isEmpty()) {
|
||||
prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND);
|
||||
}
|
||||
prepared.player().inventoryMenu.sendAllDataToRemote();
|
||||
run.expectedTool = ItemStack.EMPTY;
|
||||
run.toolBroken = true;
|
||||
return true;
|
||||
}
|
||||
if (!current.isEmpty() && !currentMatches) {
|
||||
return false;
|
||||
}
|
||||
ItemStack normalized = before.copy();
|
||||
normalized.setDamageValue(desiredDamage);
|
||||
prepared.player().getInventory().setItem(prepared.heldSlot(), normalized);
|
||||
prepared.player().inventoryMenu.sendAllDataToRemote();
|
||||
run.expectedTool = normalized.copy();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void discover(FellingRun run) {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
scheduler.async(() -> {
|
||||
TreeMarkerTraversal.Discovery discovery;
|
||||
try {
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
TreeMarkerTraversal.Position trigger = positionOf(prepared.position());
|
||||
discovery = TreeMarkerTraversal.discover(
|
||||
trigger,
|
||||
prepared.marker(),
|
||||
prepared.minimumY(),
|
||||
prepared.maximumY(),
|
||||
(x, y, z) -> markerAt(prepared.engine(), prepared.minimumY(), x, y, z)
|
||||
);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris modded tree-feller discovery failed", error);
|
||||
discovery = new TreeMarkerTraversal.Discovery(List.of(), false);
|
||||
}
|
||||
TreeMarkerTraversal.Discovery resolved = discovery;
|
||||
scheduler.global(() -> beginErosion(run, resolved));
|
||||
});
|
||||
}
|
||||
|
||||
private void beginErosion(FellingRun run, TreeMarkerTraversal.Discovery discovery) {
|
||||
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
|
||||
if (run.finished.get() || !discovery.complete()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
TreeMarkerTraversal.Position trigger = positionOf(run.prepared.position());
|
||||
run.work = discovery.members().stream()
|
||||
.filter((position) -> !position.equals(trigger))
|
||||
.toList();
|
||||
if (run.work.isEmpty()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
run.blocksPerPulse = ModdedTreeFellerPresentation.blocksPerPulse(run.work.size());
|
||||
run.effectStride = ModdedTreeFellerPresentation.effectStride(run.blocksPerPulse);
|
||||
scheduleNextPulse(run);
|
||||
}
|
||||
|
||||
private void scheduleNextPulse(FellingRun run) {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
scheduler.laterGlobal(() -> processPulse(run), 1);
|
||||
}
|
||||
|
||||
private void processPulse(FellingRun run) {
|
||||
if (run.finished.get() || !isRunControlActive(run)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
int processedThisPulse = 0;
|
||||
while (processedThisPulse < run.blocksPerPulse && run.cursor < run.work.size()) {
|
||||
if (!isRunControlActive(run)) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
TreeMarkerTraversal.Position position = run.work.get(run.cursor++);
|
||||
MemberResult result = processMember(run, position);
|
||||
if (result == MemberResult.STOP) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
processedThisPulse++;
|
||||
}
|
||||
run.presentation.flush();
|
||||
if (run.cursor >= run.work.size()) {
|
||||
finish(run);
|
||||
return;
|
||||
}
|
||||
scheduleNextPulse(run);
|
||||
}
|
||||
|
||||
private MemberResult processMember(FellingRun run, TreeMarkerTraversal.Position position) {
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
BlockPos blockPosition = blockPosition(position);
|
||||
ServerLevel level = prepared.level();
|
||||
if (!level.isLoaded(blockPosition)) {
|
||||
return MemberResult.STOP;
|
||||
}
|
||||
BlockState state = level.getBlockState(blockPosition);
|
||||
if (state.isAir()) {
|
||||
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
|
||||
return MemberResult.CONTINUE;
|
||||
}
|
||||
if (!prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) {
|
||||
return MemberResult.STOP;
|
||||
}
|
||||
TreeBlockMaterial expectedMaterial = materialAt(prepared.engine(), prepared.minimumY(), blockPosition);
|
||||
if (expectedMaterial != null && !expectedMaterial.matches(ModdedBlockState.serialize(state))) {
|
||||
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
|
||||
return state.is(BlockTags.LOGS) ? MemberResult.STOP : MemberResult.CONTINUE;
|
||||
}
|
||||
boolean log = state.is(BlockTags.LOGS);
|
||||
ServerPlayer player = prepared.player();
|
||||
if (!level.mayInteract(player, blockPosition)
|
||||
|| player.blockActionRestricted(level, blockPosition, player.gameMode())
|
||||
|| !player.getInventory().getSelectedItem().canDestroyBlock(
|
||||
state,
|
||||
level,
|
||||
blockPosition,
|
||||
player
|
||||
)) {
|
||||
return log ? MemberResult.STOP : MemberResult.CONTINUE;
|
||||
}
|
||||
if (!ModdedEngineBootstrap.loader().canTreeFellerBreak(
|
||||
level,
|
||||
player,
|
||||
blockPosition,
|
||||
state
|
||||
)) {
|
||||
return log ? MemberResult.STOP : MemberResult.CONTINUE;
|
||||
}
|
||||
if (!state.equals(level.getBlockState(blockPosition))
|
||||
|| !prepared.marker().equals(markerAt(prepared.engine(), prepared.minimumY(), blockPosition))) {
|
||||
return MemberResult.STOP;
|
||||
}
|
||||
|
||||
ItemStack toolForDrops = prepared.player().getInventory().getSelectedItem().copy();
|
||||
BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(blockPosition) : null;
|
||||
List<ItemStack> vanillaDrops = Block.getDrops(
|
||||
state,
|
||||
level,
|
||||
blockPosition,
|
||||
blockEntity,
|
||||
player,
|
||||
toolForDrops
|
||||
);
|
||||
ModdedBlockBreakHandler.Result customDrops = ModdedBlockBreakHandler.evaluateManagedDrops(
|
||||
level,
|
||||
blockPosition,
|
||||
state
|
||||
);
|
||||
ToolReservation reservation = log ? reserveToolDamage(run) : ToolReservation.free(toolForDrops);
|
||||
if (reservation == null) {
|
||||
return MemberResult.STOP;
|
||||
}
|
||||
if (!level.destroyBlock(blockPosition, false, player, 512)) {
|
||||
refundToolDamage(run, reservation);
|
||||
return MemberResult.STOP;
|
||||
}
|
||||
ModdedBlockBreakHandler.completeManagedBreak(level, blockPosition);
|
||||
int processed = run.processed++;
|
||||
float progress = run.work.size() <= 1 ? 1F : (float) processed / (float) (run.work.size() - 1);
|
||||
float pitch = Math.min(1.95F, 0.65F + (progress * 1.25F));
|
||||
run.presentation.erode(blockPosition, state, processed, run.effectStride, pitch);
|
||||
run.presentation.route(customDrops.combinedDrops(vanillaDrops));
|
||||
return reservation.broke() ? MemberResult.STOP : MemberResult.CONTINUE;
|
||||
}
|
||||
|
||||
private ToolReservation reserveToolDamage(FellingRun run) {
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
ItemStack current = prepared.player().getInventory().getSelectedItem();
|
||||
if (!ItemStack.isSameItemSameComponents(current, run.expectedTool) || !current.is(ItemTags.AXES)) {
|
||||
return null;
|
||||
}
|
||||
ItemStack before = current.copy();
|
||||
if (!current.isDamageableItem()
|
||||
|| ThreadLocalRandom.current().nextInt(100) < prepared.preservationChance()) {
|
||||
return ToolReservation.free(before);
|
||||
}
|
||||
int nextDamage = current.getDamageValue() + 1;
|
||||
if (nextDamage >= current.getMaxDamage()) {
|
||||
Item brokenItem = current.getItem();
|
||||
prepared.player().getInventory().setSelectedItem(ItemStack.EMPTY);
|
||||
prepared.player().onEquippedItemBroken(brokenItem, EquipmentSlot.MAINHAND);
|
||||
prepared.player().inventoryMenu.sendAllDataToRemote();
|
||||
run.expectedTool = ItemStack.EMPTY;
|
||||
return new ToolReservation(before, true, true);
|
||||
}
|
||||
current.setDamageValue(nextDamage);
|
||||
prepared.player().getInventory().setSelectedItem(current);
|
||||
prepared.player().inventoryMenu.sendAllDataToRemote();
|
||||
run.expectedTool = current.copy();
|
||||
return new ToolReservation(before, true, false);
|
||||
}
|
||||
|
||||
private void refundToolDamage(FellingRun run, ToolReservation reservation) {
|
||||
if (!reservation.charged()) {
|
||||
return;
|
||||
}
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
ItemStack current = prepared.player().getInventory().getSelectedItem();
|
||||
boolean expectedEmpty = run.expectedTool.isEmpty();
|
||||
if ((expectedEmpty && current.isEmpty())
|
||||
|| (!expectedEmpty && ItemStack.isSameItemSameComponents(current, run.expectedTool))) {
|
||||
ItemStack restored = reservation.before().copy();
|
||||
prepared.player().getInventory().setSelectedItem(restored);
|
||||
prepared.player().inventoryMenu.sendAllDataToRemote();
|
||||
run.expectedTool = restored.copy();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunControlActive(FellingRun run) {
|
||||
PreparedOrigin prepared = run.prepared;
|
||||
ServerPlayer player = prepared.player();
|
||||
IrisSettings.IrisSettingsTreeFeller settings = IrisSettings.get().getTreeFeller();
|
||||
if (!enabled.get()
|
||||
|| settings == null
|
||||
|| !settings.isEnabled()
|
||||
|| run.finished.get()
|
||||
|| player.isRemoved()
|
||||
|| player.hasDisconnected()
|
||||
|| player.level() != prepared.level()
|
||||
|| !player.gameMode().isSurvival()
|
||||
|| !player.isShiftKeyDown()
|
||||
|| player.getInventory().getSelectedSlot() != prepared.heldSlot()
|
||||
|| run.expectedTool.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
ItemStack current = player.getInventory().getSelectedItem();
|
||||
return current.is(ItemTags.AXES) && ItemStack.isSameItemSameComponents(current, run.expectedTool);
|
||||
}
|
||||
|
||||
private void finish(FellingRun run) {
|
||||
if (run.finished.compareAndSet(false, true)) {
|
||||
activeRuns.remove(run);
|
||||
activeClaims.remove(run.claim);
|
||||
ModdedBlockBreakHandler.completeManagedBreak(run.prepared.level(), run.prepared.position());
|
||||
run.presentation.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private String markerAt(Engine engine, int minimumY, BlockPos position) {
|
||||
return markerAt(engine, minimumY, position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
|
||||
private String markerAt(Engine engine, int minimumY, int x, int y, int z) {
|
||||
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
|
||||
}
|
||||
|
||||
private TreeBlockMaterial materialAt(Engine engine, int minimumY, BlockPos position) {
|
||||
return engine.getMantle().getMantle().get(
|
||||
position.getX(),
|
||||
position.getY() - minimumY,
|
||||
position.getZ(),
|
||||
TreeBlockMaterial.class
|
||||
);
|
||||
}
|
||||
|
||||
private TreeMarkerTraversal.Position positionOf(BlockPos position) {
|
||||
return new TreeMarkerTraversal.Position(position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
|
||||
private BlockPos blockPosition(TreeMarkerTraversal.Position position) {
|
||||
return new BlockPos(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
public record PreparedOrigin(
|
||||
ModdedTreeFellerService owner,
|
||||
ServerLevel level,
|
||||
ServerPlayer player,
|
||||
BlockPos position,
|
||||
BlockState state,
|
||||
Engine engine,
|
||||
String marker,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
int heldSlot,
|
||||
ItemStack toolBefore,
|
||||
int preservationChance
|
||||
) {
|
||||
public PreparedOrigin {
|
||||
Objects.requireNonNull(owner, "owner");
|
||||
Objects.requireNonNull(level, "level");
|
||||
Objects.requireNonNull(player, "player");
|
||||
Objects.requireNonNull(position, "position");
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
Objects.requireNonNull(marker, "marker");
|
||||
Objects.requireNonNull(toolBefore, "toolBefore");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OriginDropRoute {
|
||||
boolean route(Iterable<ItemStack> drops);
|
||||
}
|
||||
|
||||
private enum MemberResult {
|
||||
CONTINUE,
|
||||
STOP
|
||||
}
|
||||
|
||||
private record TreeClaim(ServerLevel level, String marker) {
|
||||
}
|
||||
|
||||
private record ToolReservation(ItemStack before, boolean charged, boolean broke) {
|
||||
private static ToolReservation free(ItemStack before) {
|
||||
return new ToolReservation(before, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FellingRun {
|
||||
private final TreeClaim claim;
|
||||
private final PreparedOrigin prepared;
|
||||
private final ModdedTreeFellerPresentation presentation;
|
||||
private final AtomicBoolean finished = new AtomicBoolean();
|
||||
private ItemStack expectedTool = ItemStack.EMPTY;
|
||||
private List<TreeMarkerTraversal.Position> work = List.of();
|
||||
private int cursor;
|
||||
private int processed;
|
||||
private int blocksPerPulse = 1;
|
||||
private int effectStride = 1;
|
||||
private boolean toolBroken;
|
||||
|
||||
private FellingRun(
|
||||
TreeClaim claim,
|
||||
PreparedOrigin prepared,
|
||||
ModdedTreeFellerPresentation presentation
|
||||
) {
|
||||
this.claim = claim;
|
||||
this.prepared = prepared;
|
||||
this.presentation = presentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedGenerationLeaseContractTest {
|
||||
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
||||
|
||||
@Test
|
||||
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/IrisModdedBiomeSource.java");
|
||||
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_visible_biome\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_biomes_within\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_reachability\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_state_biome\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(engine, \"modded_structure_biome_keys\")"));
|
||||
assertTrue(source.contains("engine == null || engine.isClosed()"));
|
||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||
assertTrue(source.contains("e.isExpectedTeardown()"));
|
||||
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
|
||||
assertTrue(source.contains("source.clearCache()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainAndHeightQueriesShareGenerationSessions() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/IrisModdedChunkGenerator.java");
|
||||
|
||||
assertTrue(source.contains("generationEngine.acquireGenerationLease(\"modded_chunk_pipeline\")"));
|
||||
assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_height\")"));
|
||||
assertTrue(source.contains("current.acquireGenerationLease(\"modded_base_column\")"));
|
||||
assertTrue(source.contains("current.acquireGenerationLease(\"modded_configured_biome_keys\")"));
|
||||
assertTrue(source.contains("generationEngine.isClosing() || e.isExpectedTeardown()"));
|
||||
String terrain = method(source, "private ChunkAccess generateTerrain(");
|
||||
int sessionFailure = terrain.indexOf("catch (GenerationSessionException e)");
|
||||
int generalFailure = terrain.indexOf("catch (Throwable e)", sessionFailure);
|
||||
String sessionHandler = terrain.substring(sessionFailure, generalFailure);
|
||||
assertTrue(sessionHandler.contains("throw new IllegalStateException"));
|
||||
assertFalse(sessionHandler.contains("return chunk;"));
|
||||
|
||||
String references = method(source, "public void createReferences(");
|
||||
assertTrue(references.contains("requireGenerationLease(current, \"modded_create_references\")"));
|
||||
assertTrue(references.contains("IrisContext.open(current, lease.sessionId(), null)"));
|
||||
assertTrue(references.contains("super.createReferences(level, structureManager, chunk);"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservationDereferencesSharedPackData() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/service/ModdedPreservationService.java");
|
||||
int dereference = source.indexOf("public void dereference()");
|
||||
int irisData = source.indexOf("IrisData.dereference();", dereference);
|
||||
int trackedResources = source.indexOf("threads.removeIf", dereference);
|
||||
|
||||
assertTrue(dereference >= 0);
|
||||
assertTrue(irisData > dereference);
|
||||
assertTrue(trackedResources > irisData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadShutdownOnlyTargetsItsOwnWorld() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/service/ModdedStudioHotloadService.java");
|
||||
int shutdown = source.indexOf("public void shutdownPregenerator(Engine engine)");
|
||||
int nextMethod = source.indexOf("private boolean throttled", shutdown);
|
||||
String method = source.substring(shutdown, nextMethod);
|
||||
|
||||
assertTrue(method.contains("ModdedPregenJob.shutdownForWorld(world.identity());"));
|
||||
assertFalse(method.contains("PregeneratorJob.shutdownInstance();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maintenanceOwnsOneGenerationSessionUntilWorkCompletes() throws IOException {
|
||||
String source = source("art/arcane/iris/modded/service/ModdedEngineMaintenanceService.java");
|
||||
String maintain = method(source, "private void maintain(Engine engine, long scheduledAt)");
|
||||
|
||||
assertTrue(maintain.contains("engine.acquireGenerationLease(\"modded_engine_maintenance\")"));
|
||||
assertTrue(maintain.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||
assertTrue(maintain.contains("exception.isExpectedTeardown()"));
|
||||
assertTrue(source.contains("active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockingPregenShutdownDefersItsFinalSaveToTheServerThread() throws IOException {
|
||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||
String shutdown = method(jobSource, "private static boolean shutdownAndSave(");
|
||||
assertTrue(shutdown.contains("deferFinalSaveToServerThread()"));
|
||||
assertTrue(shutdown.contains("completeDeferredFinalSave()"));
|
||||
assertTrue(shutdown.contains("cancelDeferredFinalSave()"));
|
||||
|
||||
String methodSource = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
|
||||
String close = method(methodSource, "public void close()");
|
||||
assertTrue(close.contains("deferFinalSaveIfRequested()"));
|
||||
|
||||
String await = method(methodSource, "private void awaitFinalSave(");
|
||||
assertTrue(await.contains("FINAL_SAVE_POLL_MILLIS"));
|
||||
assertTrue(await.contains("deferFinalSaveIfRequested()"));
|
||||
|
||||
String complete = method(methodSource, "void completeDeferredFinalSave()");
|
||||
int cancel = complete.indexOf("cancelQueuedFinalSave();");
|
||||
int directSave = complete.indexOf("saveLevelOnServerThread();");
|
||||
assertTrue(cancel >= 0);
|
||||
assertTrue(directSave > cancel);
|
||||
|
||||
String queuedSave = method(methodSource, "private void executeFinalSave(");
|
||||
assertTrue(queuedSave.contains("if (!request.claim())"));
|
||||
String cancelRequest = method(methodSource, "private void cancelQueuedFinalSave()");
|
||||
assertTrue(cancelRequest.contains("request.cancel();"));
|
||||
String pending = method(methodSource, "boolean hasPendingFinalSave()");
|
||||
assertTrue(pending.contains("queuedFinalSave.get() != null"));
|
||||
}
|
||||
|
||||
private static String source(String relativePath) throws IOException {
|
||||
String root = System.getProperty(SOURCE_ROOT_PROPERTY);
|
||||
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY, root != null && !root.isBlank());
|
||||
return Files.readString(Path.of(root, relativePath));
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = source.indexOf(signature);
|
||||
assertTrue("Missing source contract signature: " + signature, start >= 0);
|
||||
int openBrace = source.indexOf('{', start);
|
||||
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
|
||||
int depth = 0;
|
||||
for (int index = openBrace; index < source.length(); index++) {
|
||||
char current = source.charAt(index);
|
||||
if (current == '{') {
|
||||
depth++;
|
||||
} else if (current == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return source.substring(start, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
||||
}
|
||||
}
|
||||
+59
-1
@@ -141,7 +141,35 @@ public class ModdedLifecycleFailureContractTest {
|
||||
String source = source("ModdedEngineBootstrap.java");
|
||||
String stop = method(source, "public static void stop(");
|
||||
|
||||
assertEquals(1, occurrences(stop, "ModdedStartup.reset();"));
|
||||
assertEquals(1, occurrences(stop, "ModdedStartup::reset"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void engineBootstrapAttemptsEveryShutdownStageAndAggregatesFailures() throws IOException {
|
||||
String source = source("ModdedEngineBootstrap.java");
|
||||
String stop = method(source, "public static void stop(");
|
||||
|
||||
assertBefore(stop, "\"services\"", "\"world engines\"");
|
||||
assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
|
||||
assertBefore(stop, "\"server state\"", "if (failure != null)");
|
||||
assertTrue(stop.contains("throw propagateStopFailure(failure);"));
|
||||
|
||||
String runStage = method(source, "private static Throwable runStopStage(");
|
||||
assertTrue(runStage.contains("catch (Throwable stageFailure)"));
|
||||
assertTrue(runStage.contains("failure.addSuppressed(stageFailure);"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldShutdownRemovesOnlySuccessfullyClosedMappings() throws IOException {
|
||||
String source = source("ModdedWorldEngines.java");
|
||||
String shutdown = method(source, "public static void shutdown()");
|
||||
|
||||
assertTrue(shutdown.contains("new ArrayList<>(ENGINES.entrySet())"));
|
||||
assertFalse(shutdown.contains("ENGINES.clear()"));
|
||||
assertBefore(shutdown, "close(engine);", "ENGINES.remove(level, engine)");
|
||||
assertTrue(shutdown.contains("ENGINES.containsKey(level)"));
|
||||
assertTrue(shutdown.contains("failure.addSuppressed(e);"));
|
||||
assertTrue(shutdown.contains("failed mappings were retained"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,7 +188,37 @@ public class ModdedLifecycleFailureContractTest {
|
||||
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
|
||||
|
||||
assertFalse(unbind.contains("finally"));
|
||||
assertBefore(unbind, "unloading = true;", "ModdedWorldEngines.evictOrThrow(level);");
|
||||
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
|
||||
String binding = method(source, "private Engine bindEngine(ServerLevel level)");
|
||||
assertTrue(binding.contains("requireBindingAllowed();"));
|
||||
assertTrue(binding.contains("requireCompletedShutdown(cached);"));
|
||||
String bindLevel = method(source, "synchronized void bindLevel(ServerLevel level)");
|
||||
assertBefore(bindLevel, "requireCompletedShutdown(engine);", "unloading = false;");
|
||||
String boundEngine = method(source, "public Engine engineIfBound()");
|
||||
assertTrue(boundEngine.contains("unloading || current == null || current.isClosing() || current.isClosed()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loaderUnloadSurfacesCloseFailureAndDynamicRemovalUsesStrictEviction() throws IOException {
|
||||
String bootstrapSource = source("ModdedEngineBootstrap.java");
|
||||
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
|
||||
String failure = catchBlock(unload);
|
||||
assertTrue(failure.contains("LOGGER.error("));
|
||||
assertTrue(failure.contains("throw "));
|
||||
|
||||
String managerSource = source("ModdedDimensionManager.java");
|
||||
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
|
||||
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
|
||||
assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
|
||||
assertTrue(remove.contains("generatorUnbound = true;"));
|
||||
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);"));
|
||||
|
||||
String rollback = method(managerSource, "private static void rollbackRemoval(");
|
||||
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
|
||||
assertTrue(rollback.contains("generator.bindLevel(level);"));
|
||||
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
|
||||
assertTrue(rollback.contains("LOGGER.error("));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+24
@@ -48,7 +48,28 @@ public class ModdedServiceManagerTest {
|
||||
assertNull(manager.service(SecondService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures() {
|
||||
ModdedServiceManager manager = new ModdedServiceManager();
|
||||
RuntimeException firstFailure = new RuntimeException("first disable failed");
|
||||
RuntimeException secondFailure = new RuntimeException("second disable failed");
|
||||
FirstService first = manager.register(FirstService.class, new FirstService());
|
||||
first.disableFailure = firstFailure;
|
||||
SecondService second = manager.register(
|
||||
SecondService.class, new SecondService(null, secondFailure));
|
||||
|
||||
manager.enableAll();
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class, manager::disableAll);
|
||||
|
||||
assertEquals(1, first.disableCount);
|
||||
assertEquals(1, second.disableCount);
|
||||
assertSame(secondFailure, thrown.getCause());
|
||||
assertEquals(1, secondFailure.getSuppressed().length);
|
||||
assertSame(firstFailure, secondFailure.getSuppressed()[0]);
|
||||
}
|
||||
|
||||
private static final class FirstService implements ModdedService {
|
||||
private RuntimeException disableFailure;
|
||||
private int enableCount;
|
||||
private int disableCount;
|
||||
|
||||
@@ -60,6 +81,9 @@ public class ModdedServiceManagerTest {
|
||||
@Override
|
||||
public void onDisable() {
|
||||
disableCount++;
|
||||
if (disableFailure != null) {
|
||||
throw disableFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.component.DataComponentMap;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ModdedTreeFellerPresentationTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
DataComponentMap stackComponents = DataComponentMap.builder()
|
||||
.set(DataComponents.MAX_STACK_SIZE, 64)
|
||||
.build();
|
||||
if (!Items.OAK_LOG.builtInRegistryHolder().areComponentsBound()) {
|
||||
Items.OAK_LOG.builtInRegistryHolder().bindComponents(stackComponents);
|
||||
}
|
||||
if (!Items.BIRCH_LOG.builtInRegistryHolder().areComponentsBound()) {
|
||||
Items.BIRCH_LOG.builtInRegistryHolder().bindComponents(stackComponents);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pulseSizingKeepsErosionReadableAndBounded() {
|
||||
assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(1));
|
||||
assertEquals(4, ModdedTreeFellerPresentation.blocksPerPulse(240));
|
||||
assertEquals(10, ModdedTreeFellerPresentation.blocksPerPulse(600));
|
||||
assertEquals(64, ModdedTreeFellerPresentation.blocksPerPulse(100_000));
|
||||
assertEquals(4, ModdedTreeFellerPresentation.effectStride(64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compatibleDropsConsolidateWithoutExceedingStackLimits() {
|
||||
List<ItemStack> drops = ModdedTreeFellerPresentation.consolidateDrops(List.of(
|
||||
new ItemStack(Items.OAK_LOG, 48),
|
||||
new ItemStack(Items.OAK_LOG, 48),
|
||||
new ItemStack(Items.BIRCH_LOG, 3)
|
||||
));
|
||||
|
||||
assertEquals(3, drops.size());
|
||||
assertEquals(64, drops.get(0).getCount());
|
||||
assertEquals(32, drops.get(1).getCount());
|
||||
assertEquals(3, drops.get(2).getCount());
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ModdedTreeFellerServiceTest {
|
||||
@Test
|
||||
public void syntheticBreakProbeDepthIsNestedAndRestored() {
|
||||
assertFalse(ModdedTreeFellerService.isBreakProbe());
|
||||
|
||||
boolean accepted = ModdedTreeFellerService.runBreakProbe(() -> {
|
||||
assertTrue(ModdedTreeFellerService.isBreakProbe());
|
||||
return ModdedTreeFellerService.runBreakProbe(() -> {
|
||||
assertTrue(ModdedTreeFellerService.isBreakProbe());
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
assertTrue(accepted);
|
||||
assertFalse(ModdedTreeFellerService.isBreakProbe());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void syntheticBreakProbeDepthIsRestoredAfterFailure() {
|
||||
try {
|
||||
ModdedTreeFellerService.runBreakProbe(() -> {
|
||||
throw new IllegalStateException("probe failure");
|
||||
});
|
||||
fail("Expected probe failure");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertFalse(ModdedTreeFellerService.isBreakProbe());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user