mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
d
This commit is contained in:
1 parent
d76dadec30
commit
f6f06c1ab9
141 files changed
+10713
-1721
No files matched your search
+126
-59
@@ -5,9 +5,12 @@ import com.mojang.serialization.MapCodec;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
@@ -148,17 +151,26 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||||
ensureCachesCurrent();
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
|
||||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
if (lease == null) {
|
||||||
if (world == null) {
|
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
|
||||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
}
|
||||||
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isRuntimeAvailable()) {
|
||||||
|
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
ensureCachesCurrent();
|
||||||
|
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||||
|
if (world == null) {
|
||||||
|
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||||
|
}
|
||||||
|
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
||||||
|
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||||
|
.lookup(Registries.BIOME).orElse(null);
|
||||||
|
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||||
|
.lookup(Registries.BIOME).orElse(null);
|
||||||
|
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
|
||||||
}
|
}
|
||||||
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
|
||||||
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
|
||||||
.lookup(Registries.BIOME).orElse(null);
|
|
||||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
|
||||||
.lookup(Registries.BIOME).orElse(null);
|
|
||||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||||
@@ -212,11 +224,20 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||||
ensureCachesCurrent();
|
|
||||||
if (biome == null) {
|
if (biome == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return vanillaSpawnBiomes.get(biome.value());
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
|
||||||
|
if (lease == null) {
|
||||||
|
throw new IllegalStateException("Iris spawn biome lookup was rejected during an engine transition");
|
||||||
|
}
|
||||||
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isRuntimeAvailable()) {
|
||||||
|
throw new IllegalStateException("Iris spawn biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
ensureCachesCurrent();
|
||||||
|
return vanillaSpawnBiomes.get(biome.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RegistryAccess registry() {
|
private RegistryAccess registry() {
|
||||||
@@ -230,51 +251,69 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
ensureCachesCurrent();
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
|
||||||
if (isGuaranteedSurfaceBiome(y)) {
|
if (lease == null) {
|
||||||
return getSurfaceStructureBiomeHolder(x, z);
|
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 (!isRuntimeAvailable()) {
|
||||||
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
ensureCachesCurrent();
|
||||||
|
if (isGuaranteedSurfaceBiome(y)) {
|
||||||
|
return getSurfaceStructureBiomeHolder(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
long cacheKey = packNoiseKey(x, y, z);
|
long cacheKey = packNoiseKey(x, y, z);
|
||||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||||
if (cachedHolder != null) {
|
if (cachedHolder != null) {
|
||||||
return cachedHolder;
|
return cachedHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||||
|
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||||
|
if (existingHolder != null) {
|
||||||
|
return existingHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||||
|
structureBiomeCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
|
||||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
|
||||||
if (existingHolder != null) {
|
|
||||||
return existingHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
|
||||||
structureBiomeCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedHolder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||||
ensureCachesCurrent();
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within");
|
||||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
if (lease == null) {
|
||||||
boolean monumentQuery = radius == 29
|
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
|
||||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
|
||||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
|
||||||
}
|
}
|
||||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
if (!isRuntimeAvailable()) {
|
||||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||||
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(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
|
||||||
}
|
}
|
||||||
|
ensureCachesCurrent();
|
||||||
|
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||||
|
boolean monumentQuery = radius == 29
|
||||||
|
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||||
|
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
||||||
|
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(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return biomes;
|
||||||
}
|
}
|
||||||
return biomes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||||
@@ -322,24 +361,52 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
ensureCachesCurrent();
|
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
||||||
long cacheKey = packNoiseKey(x, y, z);
|
if (lease == null) {
|
||||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||||
if (cachedHolder != null) {
|
|
||||||
return cachedHolder;
|
|
||||||
}
|
}
|
||||||
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isRuntimeAvailable()) {
|
||||||
|
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
ensureCachesCurrent();
|
||||||
|
long cacheKey = packNoiseKey(x, y, z);
|
||||||
|
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||||
|
if (cachedHolder != null) {
|
||||||
|
return cachedHolder;
|
||||||
|
}
|
||||||
|
|
||||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||||
if (existingHolder != null) {
|
if (existingHolder != null) {
|
||||||
return existingHolder;
|
return existingHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||||
|
noiseBiomeCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedHolder;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
|
||||||
noiseBiomeCache.clear();
|
if (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return resolvedHolder;
|
private boolean isRuntimeAvailable() {
|
||||||
|
return !engine.isClosed() && engine.getComplex() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureCachesCurrent() {
|
private void ensureCachesCurrent() {
|
||||||
|
|||||||
+90
-43
@@ -1,6 +1,8 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
package art.arcane.iris.core.nms.v26_2_R1;
|
||||||
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -14,6 +16,7 @@ import art.arcane.iris.spi.PlatformBlockState;
|
|||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import com.mojang.datafixers.util.Pair;
|
import com.mojang.datafixers.util.Pair;
|
||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
@@ -90,6 +93,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
private final CustomBiomeSource customBiomeSource;
|
private final CustomBiomeSource customBiomeSource;
|
||||||
private final int runtimeMinY;
|
private final int runtimeMinY;
|
||||||
private final int runtimeHeight;
|
private final int runtimeHeight;
|
||||||
|
private final int runtimeSeaLevel;
|
||||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||||
private volatile ReachableStructureCache reachableStructureCache;
|
private volatile ReachableStructureCache reachableStructureCache;
|
||||||
private volatile StructureStepCache structureStepCache;
|
private volatile StructureStepCache structureStepCache;
|
||||||
@@ -106,17 +110,21 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||||
this.runtimeMinY = level.getMinY();
|
this.runtimeMinY = level.getMinY();
|
||||||
this.runtimeHeight = level.getHeight();
|
this.runtimeHeight = level.getHeight();
|
||||||
|
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
|
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
|
||||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||||
level, holders, pos, Math.max(1, radius), findUnexplored);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
|
level, holders, pos, Math.max(1, radius), findUnexplored);
|
||||||
? null
|
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||||
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
|
||||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
? null
|
||||||
|
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||||
|
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
|
||||||
@@ -231,14 +239,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSeaLevel() {
|
public int getSeaLevel() {
|
||||||
return runtimeMinY + engine.getDimension().getFluidHeight();
|
return runtimeSeaLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
|
||||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||||
|
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||||
|
adjustGeneratedStructures(registryAccess, access, previousStarts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
|
||||||
@@ -298,8 +309,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||||
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||||
return CompletableFuture.completedFuture(ichunkaccess);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
||||||
|
return CompletableFuture.completedFuture(ichunkaccess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -355,9 +369,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||||
|
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||||
|
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||||
@@ -525,29 +542,35 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||||
BlockPos blockPos = sectionPos.origin();
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||||
|
BlockPos blockPos = sectionPos.origin();
|
||||||
|
|
||||||
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||||
|
int minHeight = engine.getMinHeight();
|
||||||
|
|
||||||
for (int x = 0; x < 16; x++) {
|
for (int x = 0; x < 16; x++) {
|
||||||
for (int z = 0; z < 16; z++) {
|
for (int z = 0; z < 16; z++) {
|
||||||
int wX = x + blockPos.getX();
|
int wX = x + blockPos.getX();
|
||||||
int wZ = z + blockPos.getZ();
|
int wZ = z + blockPos.getZ();
|
||||||
|
|
||||||
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
|
int terrainTop = engine.getHeight(wX, wZ, false) + minHeight + 1;
|
||||||
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
|
int terrainNoFluid = engine.getHeight(wX, wZ, true) + minHeight + 1;
|
||||||
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
||||||
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
||||||
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
||||||
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
|
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw new IllegalStateException("Iris heightmap generation could not acquire its engine runtime.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -585,20 +608,44 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
return levelheightaccessor.getMinY()
|
||||||
|
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
|
||||||
|
+ 1;
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
int block = engine.getHeight(i, j, true);
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||||
int water = engine.getHeight(i, j, false);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
int block = engine.getHeight(i, j, true);
|
||||||
for (int k = 0; k < column.length; k++) {
|
int water = engine.getHeight(i, j, false);
|
||||||
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
|
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||||
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
|
for (int k = 0; k < column.length; k++) {
|
||||||
else column[k] = Blocks.AIR.defaultBlockState();
|
if (k <= block) {
|
||||||
|
column[k] = Blocks.STONE.defaultBlockState();
|
||||||
|
} else if (k <= water) {
|
||||||
|
column[k] = Blocks.WATER.defaultBlockState();
|
||||||
|
} else {
|
||||||
|
column[k] = Blocks.AIR.defaultBlockState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw new IllegalStateException("Iris base column query could not acquire its engine runtime.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationSessionLease requireGenerationLease(String operation) {
|
||||||
|
try {
|
||||||
|
return engine.acquireGenerationLease(operation);
|
||||||
|
} catch (GenerationSessionException exception) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " could not acquire its engine runtime.", exception);
|
||||||
}
|
}
|
||||||
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+17
@@ -19,4 +19,21 @@ public class CustomBiomeSourceStructureContractTest {
|
|||||||
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
|
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
|
||||||
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
|
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||||
|
|
||||||
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
|
||||||
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
|
||||||
|
assertTrue(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||||
|
assertTrue(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||||
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
|
||||||
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
||||||
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||||
|
assertTrue(source.contains("if (engine.isClosed())"));
|
||||||
|
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
|
||||||
|
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||||
|
assertTrue(source.contains("e.isExpectedTeardown()"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+10
@@ -54,4 +54,14 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
< placement.indexOf("for (NativePlacementGroup group"));
|
< placement.indexOf("for (NativePlacementGroup group"));
|
||||||
assertFalse(placement.contains("IrisLogging.reportError"));
|
assertFalse(placement.contains("IrisLogging.reportError"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void heightmapRuntimeReadsAreGenerationLeased() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||||
|
|
||||||
|
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_heightmaps\")"));
|
||||||
|
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_height\")"));
|
||||||
|
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")"));
|
||||||
|
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -46,4 +46,8 @@ tasks.named('jar', Jar).configure {
|
|||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
||||||
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
||||||
|
systemProperty('iris.bukkitChunkGeneratorSource', rootProject.file('core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
|
||||||
|
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
|
||||||
|
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
|
||||||
|
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,7 @@ import art.arcane.iris.engine.EnginePanic;
|
|||||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
|
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||||
import art.arcane.iris.engine.object.IrisCompat;
|
import art.arcane.iris.engine.object.IrisCompat;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
@@ -634,11 +635,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
|
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
|
||||||
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
|
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
|
||||||
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
|
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
|
||||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
|
IrisServices.register(EngineWorldManagerProvider.class,
|
||||||
IrisWorldManager manager = new IrisWorldManager(engine);
|
(EngineWorldManagerProvider) IrisWorldManager::new);
|
||||||
manager.startManager();
|
|
||||||
return manager;
|
|
||||||
});
|
|
||||||
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
|
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
|
||||||
settingsFile = getDataFile("settings.json");
|
settingsFile = getDataFile("settings.json");
|
||||||
configHotloadEngine = new ConfigHotloadEngine(
|
configHotloadEngine = new ConfigHotloadEngine(
|
||||||
@@ -673,6 +671,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||||
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
||||||
|
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
|
||||||
|
public interface IrisTreeFellerService {
|
||||||
|
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
|
||||||
|
|
||||||
|
boolean isManagedBreak(BlockBreakEvent event);
|
||||||
|
|
||||||
|
boolean isTreeBlock(Block block);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
|
public enum TreeFellerAccess {
|
||||||
|
STANDALONE,
|
||||||
|
INTEGRATION_OVERRIDE
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TreeFellerOptions(
|
||||||
|
TreeFellerAccess access,
|
||||||
|
int durabilityPreservationChance,
|
||||||
|
TreeFellerRunHooks runHooks
|
||||||
|
) {
|
||||||
|
public TreeFellerOptions {
|
||||||
|
Objects.requireNonNull(access, "access");
|
||||||
|
Objects.requireNonNull(runHooks, "runHooks");
|
||||||
|
if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) {
|
||||||
|
throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeFellerOptions standalone() {
|
||||||
|
return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeFellerOptions integrationOverride(
|
||||||
|
int durabilityPreservationChance,
|
||||||
|
TreeFellerRunHooks runHooks
|
||||||
|
) {
|
||||||
|
return new TreeFellerOptions(
|
||||||
|
TreeFellerAccess.INTEGRATION_OVERRIDE,
|
||||||
|
durabilityPreservationChance,
|
||||||
|
runHooks
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
|
public interface TreeFellerRunHooks {
|
||||||
|
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
|
||||||
|
@Override
|
||||||
|
public void onActivationAccepted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean reserveLogCost() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commitLogCost() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refundLogCost() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void onActivationAccepted();
|
||||||
|
|
||||||
|
boolean reserveLogCost();
|
||||||
|
|
||||||
|
void commitLogCost();
|
||||||
|
|
||||||
|
void refundLogCost();
|
||||||
|
}
|
||||||
+4
-1
@@ -80,7 +80,10 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdownPregenerator(Engine engine) {
|
public void shutdownPregenerator(Engine engine) {
|
||||||
PregeneratorJob.shutdownInstance();
|
IrisWorld world = engine.getWorld();
|
||||||
|
if (world != null) {
|
||||||
|
PregeneratorJob.shutdownInstanceForWorld(world.identity());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+608
-363
File diff suppressed because it is too large.
Load diff
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
|
import art.arcane.iris.engine.framework.MeteredCache;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.format.C;
|
||||||
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||||
|
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||||
|
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
|
||||||
|
import art.arcane.volmlib.util.format.Form;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class IrisEngineStatus {
|
||||||
|
private IrisEngineStatus() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send(VolmitSender sender, Snapshot snapshot) {
|
||||||
|
CacheSummary caches = summarizeCaches();
|
||||||
|
MaintenanceMetrics metrics = snapshot.metrics();
|
||||||
|
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "Status:");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (snapshot.serviceRunning() ? "Running" : "Stopped"));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Metrics: " + C.LIGHT_PURPLE + (snapshot.metricsRunning() ? "Running" : "Stopped"));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Maintenance Period: " + C.LIGHT_PURPLE + Form.duration(snapshot.maintenancePeriodMillis()));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Worker Parallelism: " + C.LIGHT_PURPLE + snapshot.workerParallelism());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Active World Tasks: " + C.LIGHT_PURPLE + metrics.activeTasks());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Configured Retention: " + C.LIGHT_PURPLE + Form.duration(snapshot.retentionMillis()));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Heap Usage: " + C.LIGHT_PURPLE + Form.pc(snapshot.heapUsage()));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Resident: " + C.LIGHT_PURPLE + metrics.residentTectonicPlates());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + metrics.queuedTectonicPlates());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Average Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.averageIdleDuration(), 2));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.maxIdleDuration(), 2));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(metrics.minIdleDuration(), 2));
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "Caches:");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + caches.sizes()[0] + " (" + caches.counts()[0] + ")");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + caches.sizes()[1] + " (" + caches.counts()[1] + ")");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + caches.sizes()[2] + " (" + caches.counts()[2] + ")");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + caches.sizes()[3] + " (" + caches.counts()[3] + ")");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "Other:");
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + metrics.worlds());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + metrics.loadedChunks());
|
||||||
|
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CacheSummary summarizeCaches() {
|
||||||
|
long[] sizes = new long[4];
|
||||||
|
long[] counts = new long[4];
|
||||||
|
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
|
||||||
|
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
|
||||||
|
|
||||||
|
for (MeteredCache cache : caches) {
|
||||||
|
int type = switch (cache) {
|
||||||
|
case ResourceLoader<?> ignored -> 0;
|
||||||
|
case CachedStream2D<?> ignored -> 1;
|
||||||
|
case CachedDoubleStream2D ignored -> 1;
|
||||||
|
case CachedStream3D<?> ignored -> 2;
|
||||||
|
default -> 3;
|
||||||
|
};
|
||||||
|
sizes[type] += cache.getSize();
|
||||||
|
counts[type]++;
|
||||||
|
}
|
||||||
|
return new CacheSummary(sizes, counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Snapshot(boolean serviceRunning,
|
||||||
|
boolean metricsRunning,
|
||||||
|
long maintenancePeriodMillis,
|
||||||
|
int workerParallelism,
|
||||||
|
long retentionMillis,
|
||||||
|
double heapUsage,
|
||||||
|
MaintenanceMetrics metrics) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MaintenanceMetrics(long residentTectonicPlates,
|
||||||
|
long queuedTectonicPlates,
|
||||||
|
long loadedChunks,
|
||||||
|
int worlds,
|
||||||
|
int activeTasks,
|
||||||
|
double averageIdleDuration,
|
||||||
|
double maxIdleDuration,
|
||||||
|
double minIdleDuration) {
|
||||||
|
static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CacheSummary(long[] sizes, long[] counts) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+251
-76
@@ -1,12 +1,14 @@
|
|||||||
package art.arcane.iris.core.service;
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.util.common.plugin.IrisService;
|
import art.arcane.iris.util.common.plugin.IrisService;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||||
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||||
@@ -15,25 +17,59 @@ import art.arcane.volmlib.integration.IntegrationServiceContract;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.plugin.ServicePriority;
|
import org.bukkit.plugin.ServicePriority;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
||||||
|
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||||
new IntegrationProtocolVersion(1, 0),
|
new IntegrationProtocolVersion(1, 0),
|
||||||
new IntegrationProtocolVersion(1, 1)
|
new IntegrationProtocolVersion(1, 1),
|
||||||
|
CURRENT_PROTOCOL
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final Set<String> CAPABILITIES = Set.of(
|
private static final Set<String> CAPABILITIES = Set.of(
|
||||||
"handshake",
|
"handshake",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
"metrics",
|
"metrics",
|
||||||
"iris-engine-metrics"
|
"metric-groups",
|
||||||
|
"iris-engine-metrics",
|
||||||
|
"iris-world-metrics"
|
||||||
|
);
|
||||||
|
private static final Map<String, String> TIMING_KEYS = Map.ofEntries(
|
||||||
|
Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS),
|
||||||
|
Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS),
|
||||||
|
Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS),
|
||||||
|
Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS),
|
||||||
|
Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS),
|
||||||
|
Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS),
|
||||||
|
Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS),
|
||||||
|
Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS),
|
||||||
|
Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS),
|
||||||
|
Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS),
|
||||||
|
Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS),
|
||||||
|
Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS),
|
||||||
|
Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS),
|
||||||
|
Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS)
|
||||||
);
|
);
|
||||||
|
|
||||||
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
|
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||||
|
|
||||||
|
public IrisIntegrationService() {
|
||||||
|
this(IrisIntegrationService::currentTelemetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisIntegrationService(Supplier<IrisTelemetrySnapshot> telemetrySupplier) {
|
||||||
|
this.telemetrySupplier = telemetrySupplier == null
|
||||||
|
? () -> IrisTelemetrySnapshot.EMPTY
|
||||||
|
: telemetrySupplier;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -68,9 +104,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<IntegrationMetricDescriptor> metricDescriptors() {
|
public Set<IntegrationMetricDescriptor> metricDescriptors() {
|
||||||
return IntegrationMetricSchema.descriptors().stream()
|
Set<IntegrationMetricDescriptor> descriptors = new LinkedHashSet<>();
|
||||||
.filter(descriptor -> descriptor.key().startsWith("iris."))
|
for (String key : IntegrationMetricSchema.irisKeys()) {
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
descriptors.add(IntegrationMetricSchema.descriptor(key));
|
||||||
|
}
|
||||||
|
return Set.copyOf(descriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -106,12 +144,11 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
negotiatedProtocol = negotiated.get();
|
|
||||||
return new IntegrationHandshakeResponse(
|
return new IntegrationHandshakeResponse(
|
||||||
pluginId(),
|
pluginId(),
|
||||||
pluginVersion(),
|
pluginVersion(),
|
||||||
true,
|
true,
|
||||||
negotiatedProtocol,
|
negotiated.get(),
|
||||||
SUPPORTED_PROTOCOLS,
|
SUPPORTED_PROTOCOLS,
|
||||||
CAPABILITIES,
|
CAPABILITIES,
|
||||||
"ok",
|
"ok",
|
||||||
@@ -121,90 +158,228 @@ public class IrisIntegrationService implements IrisService, IntegrationServiceCo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IntegrationHeartbeat heartbeat() {
|
public IntegrationHeartbeat heartbeat() {
|
||||||
long now = System.currentTimeMillis();
|
return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok");
|
||||||
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
|
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
|
||||||
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
|
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
|
||||||
? IntegrationMetricSchema.irisKeys()
|
? IntegrationMetricSchema.irisKeys()
|
||||||
: metricKeys;
|
: Set.copyOf(metricKeys);
|
||||||
long now = System.currentTimeMillis();
|
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||||
Map<String, IntegrationMetricSample> out = new HashMap<>();
|
long sampledAtMs = snapshot.sampledAtMs() > 0L
|
||||||
|
? snapshot.sampledAtMs()
|
||||||
|
: System.currentTimeMillis();
|
||||||
|
if (snapshot.sampledAtMs() <= 0L) {
|
||||||
|
return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, IntegrationMetricSample> available = globalSamples(snapshot);
|
||||||
|
Map<String, IntegrationMetricSample> selected = new LinkedHashMap<>(requested.size());
|
||||||
for (String key : requested) {
|
for (String key : requested) {
|
||||||
switch (key) {
|
IntegrationMetricSample sample = available.get(key);
|
||||||
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
|
selected.put(key, sample == null
|
||||||
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
|
? unavailable(key, "unsupported-key", sampledAtMs)
|
||||||
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
|
: sample);
|
||||||
default -> out.put(key, IntegrationMetricSample.unavailable(
|
}
|
||||||
IntegrationMetricSchema.descriptor(key),
|
return Map.copyOf(selected);
|
||||||
"unsupported-key",
|
}
|
||||||
now
|
|
||||||
));
|
@Override
|
||||||
|
public List<IntegrationMetricGroup> metricGroups() {
|
||||||
|
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||||
|
if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EngineTelemetrySnapshot> worlds = new ArrayList<>(snapshot.worlds());
|
||||||
|
worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity));
|
||||||
|
List<IntegrationMetricGroup> groups = new ArrayList<>(worlds.size());
|
||||||
|
for (EngineTelemetrySnapshot world : worlds) {
|
||||||
|
groups.add(new IntegrationMetricGroup(
|
||||||
|
"world",
|
||||||
|
world.worldIdentity(),
|
||||||
|
world.worldName(),
|
||||||
|
Map.of(
|
||||||
|
"plugin", "iris",
|
||||||
|
"dimension", world.dimensionKey()
|
||||||
|
),
|
||||||
|
worldSamples(world, snapshot.pregenerator())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, IntegrationMetricSample> globalSamples(IrisTelemetrySnapshot snapshot) {
|
||||||
|
long sampledAtMs = snapshot.sampledAtMs();
|
||||||
|
EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate();
|
||||||
|
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs);
|
||||||
|
putCacheSamples(samples, snapshot.caches(), sampledAtMs);
|
||||||
|
putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs);
|
||||||
|
putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs);
|
||||||
|
return Map.copyOf(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, IntegrationMetricSample> worldSamples(
|
||||||
|
EngineTelemetrySnapshot world,
|
||||||
|
IrisTelemetrySnapshot.PregenSnapshot pregenerator
|
||||||
|
) {
|
||||||
|
long sampledAtMs = world.sampledAtMs();
|
||||||
|
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs);
|
||||||
|
boolean targetsWorld = pregenerator.active()
|
||||||
|
&& world.worldIdentity().equals(pregenerator.worldIdentity());
|
||||||
|
putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs);
|
||||||
|
putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs);
|
||||||
|
samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys());
|
||||||
|
return Map.copyOf(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putCacheSamples(
|
||||||
|
Map<String, IntegrationMetricSample> samples,
|
||||||
|
IrisTelemetrySnapshot.CacheSnapshot caches,
|
||||||
|
long sampledAtMs
|
||||||
|
) {
|
||||||
|
putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs);
|
||||||
|
putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs);
|
||||||
|
putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs);
|
||||||
|
putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs);
|
||||||
|
putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putCacheBucket(
|
||||||
|
Map<String, IntegrationMetricSample> samples,
|
||||||
|
IrisTelemetrySnapshot.CacheBucket bucket,
|
||||||
|
String countKey,
|
||||||
|
String entriesKey,
|
||||||
|
String capacityKey,
|
||||||
|
String usageKey,
|
||||||
|
long sampledAtMs
|
||||||
|
) {
|
||||||
|
put(samples, countKey, bucket.count(), sampledAtMs);
|
||||||
|
put(samples, entriesKey, bucket.entries(), sampledAtMs);
|
||||||
|
put(samples, capacityKey, bucket.capacity(), sampledAtMs);
|
||||||
|
put(samples, usageKey, bucket.usage(), sampledAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putPregenSamples(
|
||||||
|
Map<String, IntegrationMetricSample> samples,
|
||||||
|
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
|
||||||
|
long sampledAtMs
|
||||||
|
) {
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs);
|
||||||
|
if (!pregenerator.active()) {
|
||||||
|
for (String key : Set.of(
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_PAUSED,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_PROGRESS,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_FAILED)) {
|
||||||
|
samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs));
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
|
||||||
|
put(samples, IntegrationMetricSchema.IRIS_PREGEN_FAILED, pregenerator.failed(), sampledAtMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
|
private void putTimingSamples(
|
||||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
|
Map<String, IntegrationMetricSample> samples,
|
||||||
|
Map<String, Double> timings,
|
||||||
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
|
long sampledAtMs
|
||||||
|
) {
|
||||||
if (chunksPerSecond > 0D) {
|
for (Map.Entry<String, String> entry : TIMING_KEYS.entrySet()) {
|
||||||
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
|
Double value = timings.get(entry.getKey());
|
||||||
|
samples.put(entry.getValue(), value == null
|
||||||
|
? unavailable(entry.getValue(), "timing-not-available", sampledAtMs)
|
||||||
|
: available(entry.getValue(), value, sampledAtMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
|
||||||
if (engineService != null) {
|
|
||||||
double idle = engineService.getAverageIdleDuration();
|
|
||||||
if (idle > 0D && Double.isFinite(idle)) {
|
|
||||||
return IntegrationMetricSample.available(descriptor, idle, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IntegrationMetricSample.available(descriptor, 0D, now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntegrationMetricSample samplePregenQueueMetric(long now) {
|
private void put(Map<String, IntegrationMetricSample> samples, String key, double value, long sampledAtMs) {
|
||||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
|
samples.put(key, available(key, value, sampledAtMs));
|
||||||
long totalQueue = 0L;
|
|
||||||
boolean hasAnySource = false;
|
|
||||||
|
|
||||||
long pregenRemaining = PregeneratorJob.chunksRemaining();
|
|
||||||
if (pregenRemaining >= 0L) {
|
|
||||||
totalQueue += pregenRemaining;
|
|
||||||
hasAnySource = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
|
||||||
if (engineService != null) {
|
|
||||||
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
|
|
||||||
hasAnySource = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasAnySource) {
|
|
||||||
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IntegrationMetricSample.available(descriptor, totalQueue, now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
|
private IntegrationMetricSample available(String key, double value, long sampledAtMs) {
|
||||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
|
return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs);
|
||||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
}
|
||||||
if (engineService == null) {
|
|
||||||
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
|
|
||||||
}
|
|
||||||
|
|
||||||
double ratio = engineService.getBiomeCacheUsageRatio();
|
private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) {
|
||||||
if (!Double.isFinite(ratio)) {
|
return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs);
|
||||||
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
|
private Map<String, IntegrationMetricSample> unavailableMetrics(
|
||||||
|
Set<String> keys,
|
||||||
|
String reason,
|
||||||
|
long sampledAtMs
|
||||||
|
) {
|
||||||
|
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>(keys.size());
|
||||||
|
for (String key : keys) {
|
||||||
|
samples.put(key, unavailable(key, reason, sampledAtMs));
|
||||||
|
}
|
||||||
|
return Map.copyOf(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisTelemetrySnapshot safeSnapshot() {
|
||||||
|
IrisTelemetrySnapshot snapshot = telemetrySupplier.get();
|
||||||
|
return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisTelemetrySnapshot currentTelemetry() {
|
||||||
|
IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class);
|
||||||
|
return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record IrisTelemetrySnapshot(
|
||||||
|
long sampledAtMs,
|
||||||
|
List<EngineTelemetrySnapshot> worlds,
|
||||||
|
EngineTelemetrySnapshot.Aggregate aggregate,
|
||||||
|
int maintenanceActiveTasks,
|
||||||
|
int maintenanceWorkers,
|
||||||
|
int closingEngines,
|
||||||
|
int pendingRegistrations,
|
||||||
|
double heapUsage,
|
||||||
|
double reclaimUrgency,
|
||||||
|
CacheSnapshot caches,
|
||||||
|
PregenSnapshot pregenerator
|
||||||
|
) {
|
||||||
|
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
|
||||||
|
0L,
|
||||||
|
List.of(),
|
||||||
|
EngineTelemetrySnapshot.aggregate(List.of()),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0D,
|
||||||
|
0D,
|
||||||
|
CacheSnapshot.EMPTY,
|
||||||
|
PregenSnapshot.INACTIVE
|
||||||
|
);
|
||||||
|
|
||||||
|
IrisTelemetrySnapshot {
|
||||||
|
worlds = worlds == null ? List.of() : List.copyOf(worlds);
|
||||||
|
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
|
||||||
|
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
|
||||||
|
maintenanceWorkers = Math.max(0, maintenanceWorkers);
|
||||||
|
closingEngines = Math.max(0, closingEngines);
|
||||||
|
pendingRegistrations = Math.max(0, pendingRegistrations);
|
||||||
|
heapUsage = finiteRatio(heapUsage);
|
||||||
|
reclaimUrgency = finiteRatio(reclaimUrgency);
|
||||||
|
caches = caches == null ? CacheSnapshot.EMPTY : caches;
|
||||||
|
pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double finiteRatio(double value) {
|
||||||
|
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
record CacheSnapshot(
|
||||||
|
CacheBucket total,
|
||||||
|
CacheBucket resource,
|
||||||
|
CacheBucket stream2d,
|
||||||
|
CacheBucket stream3d,
|
||||||
|
CacheBucket other
|
||||||
|
) {
|
||||||
|
static final CacheSnapshot EMPTY = new CacheSnapshot(
|
||||||
|
CacheBucket.EMPTY,
|
||||||
|
CacheBucket.EMPTY,
|
||||||
|
CacheBucket.EMPTY,
|
||||||
|
CacheBucket.EMPTY,
|
||||||
|
CacheBucket.EMPTY
|
||||||
|
);
|
||||||
|
|
||||||
|
CacheSnapshot {
|
||||||
|
total = total == null ? CacheBucket.EMPTY : total;
|
||||||
|
resource = resource == null ? CacheBucket.EMPTY : resource;
|
||||||
|
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
|
||||||
|
stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d;
|
||||||
|
other = other == null ? CacheBucket.EMPTY : other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CacheBucket(int count, long entries, long capacity) {
|
||||||
|
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
|
||||||
|
|
||||||
|
CacheBucket {
|
||||||
|
count = Math.max(0, count);
|
||||||
|
entries = Math.max(0L, entries);
|
||||||
|
capacity = Math.max(0L, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
double usage() {
|
||||||
|
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheBucket plus(CacheBucket other) {
|
||||||
|
if (other == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record PregenSnapshot(
|
||||||
|
boolean active,
|
||||||
|
String worldIdentity,
|
||||||
|
String worldName,
|
||||||
|
boolean paused,
|
||||||
|
double progressPercent,
|
||||||
|
long generated,
|
||||||
|
long total,
|
||||||
|
long remaining,
|
||||||
|
double chunksPerSecond,
|
||||||
|
long etaMs,
|
||||||
|
long elapsedMs,
|
||||||
|
long failed
|
||||||
|
) {
|
||||||
|
static final PregenSnapshot INACTIVE = new PregenSnapshot(
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
0D,
|
||||||
|
0L,
|
||||||
|
0L,
|
||||||
|
0L,
|
||||||
|
0D,
|
||||||
|
0L,
|
||||||
|
0L,
|
||||||
|
0L
|
||||||
|
);
|
||||||
|
|
||||||
|
PregenSnapshot {
|
||||||
|
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
|
||||||
|
worldName = worldName == null ? "" : worldName.trim();
|
||||||
|
progressPercent = Double.isFinite(progressPercent)
|
||||||
|
? Math.max(0D, Math.min(100D, progressPercent))
|
||||||
|
: 0D;
|
||||||
|
generated = Math.max(0L, generated);
|
||||||
|
total = Math.max(0L, total);
|
||||||
|
remaining = Math.max(0L, remaining);
|
||||||
|
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||||
|
etaMs = Math.max(0L, etaMs);
|
||||||
|
elapsedMs = Math.max(0L, elapsedMs);
|
||||||
|
failed = Math.max(0L, failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+308
@@ -0,0 +1,308 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.entity.ExperienceOrb;
|
||||||
|
import org.bukkit.entity.Item;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
final class TreeFellerPresentation {
|
||||||
|
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 Player player;
|
||||||
|
private final World sourceWorld;
|
||||||
|
private final Queue<PendingDrop> pendingDrops = new ConcurrentLinkedQueue<>();
|
||||||
|
private final AtomicBoolean flushScheduled = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean fallbackScheduled = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean completed = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean effectFailureReported = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean deliveryFailureReported = new AtomicBoolean();
|
||||||
|
private final AtomicInteger deliveryPulses = new AtomicInteger();
|
||||||
|
private volatile Location fallbackLocation;
|
||||||
|
|
||||||
|
TreeFellerPresentation(Player player, World sourceWorld, Location fallbackLocation) {
|
||||||
|
this.player = player;
|
||||||
|
this.sourceWorld = sourceWorld;
|
||||||
|
this.fallbackLocation = fallbackLocation.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(Block block) {
|
||||||
|
try {
|
||||||
|
Location center = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
|
||||||
|
World world = block.getWorld();
|
||||||
|
world.spawnParticle(Particle.ENCHANT, center, 24, 0.45D, 0.45D, 0.45D, 0.18D);
|
||||||
|
world.spawnParticle(Particle.END_ROD, center, 8, 0.25D, 0.25D, 0.25D, 0.035D);
|
||||||
|
world.playSound(center, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.55F, 1.35F);
|
||||||
|
world.playSound(center, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.4F, 0.8F);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
reportEffectFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void erode(
|
||||||
|
Location source,
|
||||||
|
BlockData visualData,
|
||||||
|
int erosionOrder,
|
||||||
|
int processed,
|
||||||
|
int blocksPerPulse,
|
||||||
|
int effectStride,
|
||||||
|
int totalBlocks
|
||||||
|
) {
|
||||||
|
if (processed % effectStride != 0 || source.getWorld() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
World world = source.getWorld();
|
||||||
|
world.spawnParticle(Particle.BLOCK, source, 5, 0.3D, 0.3D, 0.3D, 0.04D, visualData);
|
||||||
|
world.spawnParticle(Particle.ENCHANT, source, 3, 0.28D, 0.28D, 0.28D, 0.12D);
|
||||||
|
if (processed % blocksPerPulse == 0) {
|
||||||
|
double progress = totalBlocks <= 1 ? 1D : (double) erosionOrder / (double) (totalBlocks - 1);
|
||||||
|
float pitch = (float) Math.min(1.95D, 0.65D + (progress * 1.25D));
|
||||||
|
world.playSound(source, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.22F, pitch);
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
reportEffectFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean routeDrop(Object drop) {
|
||||||
|
if (!(drop instanceof ItemStack item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item.getType() == Material.AIR || item.getAmount() <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return enqueue(new PendingDrop(item.clone(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean routeExperience(int experience) {
|
||||||
|
return experience <= 0 || enqueue(new PendingDrop(null, experience));
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
completed.set(true);
|
||||||
|
if (!pendingDrops.isEmpty()) {
|
||||||
|
scheduleFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enqueue(PendingDrop drop) {
|
||||||
|
pendingDrops.add(drop);
|
||||||
|
if (scheduleFlush()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !pendingDrops.remove(drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scheduleFlush() {
|
||||||
|
if (pendingDrops.isEmpty() || !flushScheduled.compareAndSet(false, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fallbackScheduled.set(false);
|
||||||
|
Runnable retired = () -> {
|
||||||
|
if (!scheduleFallbackFlush()) {
|
||||||
|
flushScheduled.set(false);
|
||||||
|
IrisLogging.error("Unable to deliver queued Iris tree-feller drops after the player retired.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (J.runEntity(player, this::flushAtPlayer, 1, retired)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return scheduleFallbackFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean scheduleFallbackFlush() {
|
||||||
|
if (!fallbackScheduled.compareAndSet(false, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Location fallback = fallbackLocation.clone();
|
||||||
|
if (J.runAt(fallback, () -> flushAtFallback(fallback), 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fallbackScheduled.set(false);
|
||||||
|
flushScheduled.set(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushAtPlayer() {
|
||||||
|
if (!player.isOnline() || !player.getWorld().equals(sourceWorld)) {
|
||||||
|
if (!scheduleFallbackFlush()) {
|
||||||
|
IrisLogging.error("Unable to deliver queued Iris tree-feller drops at their fallback location.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location target = player.getLocation().clone().add(0D, 0.15D, 0D);
|
||||||
|
fallbackLocation = target.clone();
|
||||||
|
deliverBatch(target, drainPendingDrops(), true);
|
||||||
|
completeFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushAtFallback(Location fallback) {
|
||||||
|
deliverBatch(fallback, drainPendingDrops(), false);
|
||||||
|
completeFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DropBatch drainPendingDrops() {
|
||||||
|
List<ItemStack> items = new ArrayList<>();
|
||||||
|
long experience = 0L;
|
||||||
|
PendingDrop pending;
|
||||||
|
while ((pending = pendingDrops.poll()) != null) {
|
||||||
|
if (pending.item() != null) {
|
||||||
|
items.add(pending.item());
|
||||||
|
}
|
||||||
|
experience = Math.min(Integer.MAX_VALUE, experience + pending.experience());
|
||||||
|
}
|
||||||
|
return new DropBatch(consolidateDrops(items), (int) experience);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverBatch(Location target, DropBatch batch, boolean atPlayer) {
|
||||||
|
World world = target.getWorld();
|
||||||
|
if (world == null) {
|
||||||
|
requeueBatch(batch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int deliveredItems = 0;
|
||||||
|
for (int index = 0; index < batch.items().size(); index++) {
|
||||||
|
ItemStack item = batch.items().get(index);
|
||||||
|
Item dropped;
|
||||||
|
try {
|
||||||
|
dropped = world.dropItem(target, item);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
requeueUndelivered(batch, index);
|
||||||
|
reportDeliveryFailure(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deliveredItems++;
|
||||||
|
try {
|
||||||
|
dropped.setVelocity(new Vector(0D, 0.08D, 0D));
|
||||||
|
} catch (Throwable error) {
|
||||||
|
reportEffectFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batch.experience() > 0) {
|
||||||
|
try {
|
||||||
|
ExperienceOrb orb = world.spawn(target.clone().add(0D, 0.35D, 0D), ExperienceOrb.class);
|
||||||
|
orb.setExperience(batch.experience());
|
||||||
|
} catch (Throwable error) {
|
||||||
|
pendingDrops.add(new PendingDrop(null, batch.experience()));
|
||||||
|
reportDeliveryFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int enchantParticles = Math.min(32, 6 + (deliveredItems * 2));
|
||||||
|
Location effectLocation = target.clone().add(0D, 0.35D, 0D);
|
||||||
|
world.spawnParticle(Particle.ENCHANT, effectLocation, enchantParticles, 0.35D, 0.25D, 0.35D, 0.15D);
|
||||||
|
world.spawnParticle(Particle.END_ROD, effectLocation, Math.min(8, 2 + deliveredItems), 0.2D, 0.2D, 0.2D, 0.025D);
|
||||||
|
int deliveryPulse = deliveryPulses.incrementAndGet();
|
||||||
|
if (atPlayer && (deliveryPulse == 1 || deliveryPulse % 4 == 0 || completed.get())) {
|
||||||
|
world.playSound(target, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.2F, 1.65F);
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
reportEffectFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeFlush() {
|
||||||
|
fallbackScheduled.set(false);
|
||||||
|
flushScheduled.set(false);
|
||||||
|
if (!pendingDrops.isEmpty() && !scheduleFlush()) {
|
||||||
|
IrisLogging.error("Unable to schedule the remaining Iris tree-feller drops.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requeueBatch(DropBatch batch) {
|
||||||
|
requeueUndelivered(batch, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requeueUndelivered(DropBatch batch, int firstUndeliveredItem) {
|
||||||
|
for (int index = firstUndeliveredItem; index < batch.items().size(); index++) {
|
||||||
|
pendingDrops.add(new PendingDrop(batch.items().get(index), 0));
|
||||||
|
}
|
||||||
|
if (batch.experience() > 0) {
|
||||||
|
pendingDrops.add(new PendingDrop(null, batch.experience()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportEffectFailure(Throwable error) {
|
||||||
|
if (effectFailureReported.compareAndSet(false, true)) {
|
||||||
|
IrisLogging.reportError("Failed to render an Iris tree-feller effect.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportDeliveryFailure(Throwable error) {
|
||||||
|
if (deliveryFailureReported.compareAndSet(false, true)) {
|
||||||
|
IrisLogging.reportError("Failed to deliver an Iris tree-feller drop; it remains queued for retry.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mergeDrop(List<ItemStack> consolidated, ItemStack source) {
|
||||||
|
if (source == null || source.getType() == Material.AIR || source.getAmount() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int remaining = source.getAmount();
|
||||||
|
for (ItemStack existing : consolidated) {
|
||||||
|
if (!existing.isSimilar(source) || existing.getAmount() >= existing.getMaxStackSize()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int transferable = Math.min(remaining, existing.getMaxStackSize() - existing.getAmount());
|
||||||
|
existing.setAmount(existing.getAmount() + transferable);
|
||||||
|
remaining -= transferable;
|
||||||
|
if (remaining == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int maximumStackSize = Math.max(1, source.getMaxStackSize());
|
||||||
|
while (remaining > 0) {
|
||||||
|
ItemStack stack = source.clone();
|
||||||
|
stack.setAmount(Math.min(remaining, maximumStackSize));
|
||||||
|
consolidated.add(stack);
|
||||||
|
remaining -= stack.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PendingDrop(ItemStack item, int experience) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DropBatch(List<ItemStack> items, int experience) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+1110
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,10 @@ load: STARTUP
|
|||||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||||
website: volmit.com
|
website: volmit.com
|
||||||
description: More than a Dimension!
|
description: More than a Dimension!
|
||||||
|
permissions:
|
||||||
|
iris.treefeller:
|
||||||
|
description: Allows survival players to fell Iris-managed trees with an axe.
|
||||||
|
default: op
|
||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
PlaceholderAPI:
|
PlaceholderAPI:
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ loadbefore: [ Multiverse-Core ]
|
|||||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||||
website: volmit.com
|
website: volmit.com
|
||||||
description: More than a Dimension!
|
description: More than a Dimension!
|
||||||
commands:
|
commands:
|
||||||
iris:
|
iris:
|
||||||
aliases: [ ir, irs ]
|
aliases: [ ir, irs ]
|
||||||
|
permissions:
|
||||||
|
iris.treefeller:
|
||||||
|
description: Allows survival players to fell Iris-managed trees with an axe.
|
||||||
|
default: op
|
||||||
api-version: '${apiVersion}'
|
api-version: '${apiVersion}'
|
||||||
@@ -2,6 +2,8 @@ package art.arcane.iris;
|
|||||||
|
|
||||||
import org.bukkit.plugin.PluginDescriptionFile;
|
import org.bukkit.plugin.PluginDescriptionFile;
|
||||||
import org.bukkit.plugin.PluginLoadOrder;
|
import org.bukkit.plugin.PluginLoadOrder;
|
||||||
|
import org.bukkit.permissions.Permission;
|
||||||
|
import org.bukkit.permissions.PermissionDefault;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -54,6 +56,9 @@ public class PaperPluginMetadataTest {
|
|||||||
assertTrue(metadata.contains("folia-supported: true"));
|
assertTrue(metadata.contains("folia-supported: true"));
|
||||||
assertTrue(metadata.contains("load: STARTUP"));
|
assertTrue(metadata.contains("load: STARTUP"));
|
||||||
assertFalse(metadata.contains("commands:"));
|
assertFalse(metadata.contains("commands:"));
|
||||||
|
assertTrue(metadata.contains("permissions:\n iris.treefeller:\n"
|
||||||
|
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
|
||||||
|
+ " default: op"));
|
||||||
for (String pluginId : JOINED_PLUGIN_IDS) {
|
for (String pluginId : JOINED_PLUGIN_IDS) {
|
||||||
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
|
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
|
||||||
}
|
}
|
||||||
@@ -76,6 +81,13 @@ public class PaperPluginMetadataTest {
|
|||||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||||
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
|
||||||
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
|
||||||
|
Permission treeFeller = metadata.getPermissions().stream()
|
||||||
|
.filter(permission -> "iris.treefeller".equals(permission.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
assertNotNull(treeFeller);
|
||||||
|
assertEquals("Allows survival players to fell Iris-managed trees with an axe.", treeFeller.getDescription());
|
||||||
|
assertEquals(PermissionDefault.OP, treeFeller.getDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.api.tree;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class TreeFellerOptionsTest {
|
||||||
|
@Test
|
||||||
|
public void factoriesExposeStableAccessModes() {
|
||||||
|
TreeFellerOptions standalone = TreeFellerOptions.standalone();
|
||||||
|
TreeFellerOptions override = TreeFellerOptions.integrationOverride(75, TreeFellerRunHooks.NONE);
|
||||||
|
|
||||||
|
assertEquals(TreeFellerAccess.STANDALONE, standalone.access());
|
||||||
|
assertEquals(0, standalone.durabilityPreservationChance());
|
||||||
|
assertEquals(TreeFellerAccess.INTEGRATION_OVERRIDE, override.access());
|
||||||
|
assertEquals(75, override.durabilityPreservationChance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void chanceBelowRangeIsRejected() {
|
||||||
|
TreeFellerOptions.integrationOverride(-1, TreeFellerRunHooks.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void chanceAboveRangeIsRejected() {
|
||||||
|
TreeFellerOptions.integrationOverride(101, TreeFellerRunHooks.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
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 BukkitEngineLifecycleContractTest {
|
||||||
|
@Test
|
||||||
|
public void closeFutureIsPublishedBeforeCloseWorkIsScheduled() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
|
||||||
|
String closeAsync = method(source, "public CompletableFuture<Void> closeAsync()");
|
||||||
|
|
||||||
|
assertBefore(closeAsync, "closeFuture.compareAndSet(null, future)", "withExclusiveControlFuture(");
|
||||||
|
assertTrue(closeAsync.contains("while (!closeFuture.compareAndSet(null, future))"));
|
||||||
|
assertTrue(closeAsync.contains("operation.whenComplete("));
|
||||||
|
assertFalse(closeAsync.contains("!existing.isDone()"));
|
||||||
|
|
||||||
|
String baseHeight = method(source, "public int getBaseHeight(");
|
||||||
|
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
|
||||||
|
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
|
||||||
|
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
|
||||||
|
|
||||||
|
String generation = method(source, "public void generateNoise(");
|
||||||
|
assertTrue(generation.contains("throw new IllegalStateException"));
|
||||||
|
assertTrue(generation.contains("engine.acquireGenerationLease(\"bukkit_terrain_stage\")"));
|
||||||
|
assertTrue(generation.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||||
|
assertBefore(generation, "engine.acquireGenerationLease(\"bukkit_terrain_stage\")", "blocks.apply()");
|
||||||
|
assertFalse(generation.contains("RED_GLAZED_TERRACOTTA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void targetedPregeneratorShutdownChecksWorldIdentity() throws IOException {
|
||||||
|
String jobSource = Files.readString(Path.of(System.getProperty("iris.pregeneratorJobSource")));
|
||||||
|
String targetedShutdown = method(jobSource,
|
||||||
|
"public static boolean shutdownInstanceForWorld(String worldIdentity)");
|
||||||
|
|
||||||
|
assertBefore(targetedShutdown, "inst.targetsWorldIdentity(worldIdentity)",
|
||||||
|
"shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS)");
|
||||||
|
|
||||||
|
String waitedShutdown = method(jobSource,
|
||||||
|
"private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs)");
|
||||||
|
assertBefore(waitedShutdown, "inst.worker.interrupt()", "inst.worker.join(");
|
||||||
|
assertTrue(waitedShutdown.contains("inst.worker.isAlive()"));
|
||||||
|
|
||||||
|
String hooksSource = Files.readString(Path.of(System.getProperty("iris.bukkitEnginePlatformHooksSource")));
|
||||||
|
String hookShutdown = method(hooksSource, "public void shutdownPregenerator(Engine engine)");
|
||||||
|
assertTrue(hookShutdown.contains("PregeneratorJob.shutdownInstanceForWorld(world.identity());"));
|
||||||
|
assertFalse(hookShutdown.contains("PregeneratorJob.shutdownInstance();"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBefore(String source, String first, String second) {
|
||||||
|
int firstIndex = source.indexOf(first);
|
||||||
|
int secondIndex = source.indexOf(second);
|
||||||
|
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
|
||||||
|
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
|
||||||
|
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.world.WorldUnloadEvent;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisEngineLifecycleContractTest {
|
||||||
|
@Test
|
||||||
|
public void engineServiceExclusivelyOwnsWorldUnload() throws NoSuchMethodException {
|
||||||
|
assertMonitorUnloadHandler(IrisEngineSVC.class.getMethod("onWorldUnload", WorldUnloadEvent.class));
|
||||||
|
for (Method method : EngineAssignedWorldManager.class.getDeclaredMethods()) {
|
||||||
|
boolean handlesWorldUnload = method.getParameterCount() == 1
|
||||||
|
&& method.getParameterTypes()[0] == WorldUnloadEvent.class
|
||||||
|
&& method.isAnnotationPresent(EventHandler.class);
|
||||||
|
assertFalse("EngineAssignedWorldManager must defer world unload ownership to IrisEngineSVC.", handlesWorldUnload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registrationIdentityConflictsByWorldIdentityOrFolder() {
|
||||||
|
Path sharedFolder = Path.of("build", "worlds", "shared");
|
||||||
|
IrisEngineSVC.RegistrationIdentity first =
|
||||||
|
new IrisEngineSVC.RegistrationIdentity("minecraft:first", sharedFolder);
|
||||||
|
IrisEngineSVC.RegistrationIdentity sameIdentity =
|
||||||
|
new IrisEngineSVC.RegistrationIdentity("minecraft:first", Path.of("build", "worlds", "other"));
|
||||||
|
IrisEngineSVC.RegistrationIdentity sameFolder =
|
||||||
|
new IrisEngineSVC.RegistrationIdentity("minecraft:other", sharedFolder);
|
||||||
|
IrisEngineSVC.RegistrationIdentity distinct =
|
||||||
|
new IrisEngineSVC.RegistrationIdentity("minecraft:distinct", Path.of("build", "worlds", "distinct"));
|
||||||
|
|
||||||
|
assertTrue(first.conflictsWith(sameIdentity));
|
||||||
|
assertTrue(first.conflictsWith(sameFolder));
|
||||||
|
assertFalse(first.conflictsWith(distinct));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registrationRetryWaitsAsynchronouslyForClose() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
|
||||||
|
String add = method(source, "private void add(World world)");
|
||||||
|
String remove = method(source, "private void remove(World world)");
|
||||||
|
String completeClose = method(source, "private void completeClose(");
|
||||||
|
String retry = method(source, "private void retryRegistrationAfterClose(");
|
||||||
|
String invokeClose = method(source, "private CompletableFuture<Void> invokeGeneratorClose()");
|
||||||
|
|
||||||
|
assertBefore(add, "findClosingGenerator(registrationIdentity)", "new Registered(");
|
||||||
|
assertBefore(remove, "reserveClose(registered)", "startClose(registered, closing)");
|
||||||
|
assertBefore(completeClose, "if (failure == null)", "closingGenerators.remove(closing)");
|
||||||
|
assertBefore(completeClose, "closingGenerators.remove(closing)", "closing.completion().complete(null)");
|
||||||
|
assertBefore(completeClose, "} else {", "closing.completion().completeExceptionally(failure)");
|
||||||
|
assertTrue(retry.contains("completion.whenComplete("));
|
||||||
|
assertTrue(retry.contains("J.s(() -> add(world), 1);"));
|
||||||
|
assertBefore(retry, "if (failure != null)", "J.s(() -> add(world), 1);");
|
||||||
|
assertFalse(retry.contains("completion.get("));
|
||||||
|
assertFalse(retry.contains("completion.join("));
|
||||||
|
assertTrue(invokeClose.contains("CompletableFuture.failedFuture("));
|
||||||
|
assertTrue(invokeClose.contains("future.whenComplete("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertMonitorUnloadHandler(Method method) {
|
||||||
|
EventHandler eventHandler = method.getAnnotation(EventHandler.class);
|
||||||
|
assertNotNull(eventHandler);
|
||||||
|
assertEquals(EventPriority.MONITOR, eventHandler.priority());
|
||||||
|
assertTrue(eventHandler.ignoreCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBefore(String source, String first, String second) {
|
||||||
|
int firstIndex = source.indexOf(first);
|
||||||
|
int secondIndex = source.indexOf(second);
|
||||||
|
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
|
||||||
|
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
|
||||||
|
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package art.arcane.iris.core.service;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
public class IrisEngineSVCTest {
|
|
||||||
@Test
|
|
||||||
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
|
|
||||||
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
|
|
||||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void noMaintenanceNeverSkipsReduction() {
|
|
||||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
|
|
||||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||||
|
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisIntegrationServiceTest {
|
||||||
|
@Test
|
||||||
|
public void unavailableTelemetryDoesNotPublishFalseZeroes() {
|
||||||
|
IrisIntegrationService service = new IrisIntegrationService(() -> IrisTelemetrySnapshot.EMPTY);
|
||||||
|
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
|
||||||
|
IntegrationMetricSchema.IRIS_WORLD_COUNT,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||||
|
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS));
|
||||||
|
|
||||||
|
for (IntegrationMetricSample sample : samples.values()) {
|
||||||
|
assertFalse(sample.available());
|
||||||
|
assertEquals("telemetry-not-ready", sample.message());
|
||||||
|
}
|
||||||
|
assertTrue(service.metricGroups().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void publishesAggregateAndPerWorldMetricsFromOneImmutableSnapshot() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
EngineTelemetrySnapshot first = world("minecraft:overworld", "world", 12L, 20L, 4D, now);
|
||||||
|
EngineTelemetrySnapshot second = world("minecraft:the_nether", "world_nether", 8L, 30L, 2D, now);
|
||||||
|
IrisTelemetrySnapshot snapshot = telemetry(
|
||||||
|
List.of(first, second),
|
||||||
|
new IrisTelemetrySnapshot.PregenSnapshot(
|
||||||
|
true,
|
||||||
|
first.worldIdentity(),
|
||||||
|
first.worldName(),
|
||||||
|
false,
|
||||||
|
50D,
|
||||||
|
100L,
|
||||||
|
200L,
|
||||||
|
100L,
|
||||||
|
8D,
|
||||||
|
2_000L,
|
||||||
|
5_000L,
|
||||||
|
1L
|
||||||
|
),
|
||||||
|
now
|
||||||
|
);
|
||||||
|
IrisIntegrationService service = new IrisIntegrationService(() -> snapshot);
|
||||||
|
|
||||||
|
Map<String, IntegrationMetricSample> samples = service.sampleMetrics(Set.of(
|
||||||
|
IntegrationMetricSchema.IRIS_WORLD_COUNT,
|
||||||
|
IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS,
|
||||||
|
IntegrationMetricSchema.IRIS_LOADED_CHUNKS,
|
||||||
|
IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL,
|
||||||
|
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||||
|
IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS,
|
||||||
|
"iris.unsupported"
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(2D, value(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT), 0D);
|
||||||
|
assertEquals(3D, value(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS), 0D);
|
||||||
|
assertEquals(20D, value(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS), 0D);
|
||||||
|
assertEquals(50D, value(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL), 0D);
|
||||||
|
assertEquals(8D, value(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
|
||||||
|
assertEquals(4D, value(samples, IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS), 0D);
|
||||||
|
assertFalse(samples.get("iris.unsupported").available());
|
||||||
|
assertEquals("unsupported-key", samples.get("iris.unsupported").message());
|
||||||
|
|
||||||
|
List<IntegrationMetricGroup> groups = service.metricGroups();
|
||||||
|
assertEquals(2, groups.size());
|
||||||
|
IntegrationMetricGroup overworld = groups.get(0);
|
||||||
|
IntegrationMetricGroup nether = groups.get(1);
|
||||||
|
assertEquals("minecraft:overworld", overworld.scopeId());
|
||||||
|
assertEquals(1D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
|
||||||
|
assertEquals(8D, value(overworld.samples(), IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT), 0D);
|
||||||
|
assertEquals("minecraft:the_nether", nether.scopeId());
|
||||||
|
assertEquals(0D, value(nether.samples(), IntegrationMetricSchema.IRIS_PREGEN_ACTIVE), 0D);
|
||||||
|
assertFalse(nether.samples().get(IntegrationMetricSchema.IRIS_PREGEN_QUEUE).available());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void publishesEveryManagedWorldWithoutAWorldCountCap() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
List<EngineTelemetrySnapshot> worlds = new ArrayList<>();
|
||||||
|
for (int index = 0; index < 48; index++) {
|
||||||
|
worlds.add(world("iris:world_" + index, "world_" + index, index, index, index, now));
|
||||||
|
}
|
||||||
|
IrisIntegrationService service = new IrisIntegrationService(
|
||||||
|
() -> telemetry(worlds, IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, now)
|
||||||
|
);
|
||||||
|
|
||||||
|
List<IntegrationMetricGroup> groups = service.metricGroups();
|
||||||
|
|
||||||
|
assertEquals(48, groups.size());
|
||||||
|
assertEquals(48, groups.stream().map(IntegrationMetricGroup::scopeId).distinct().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisTelemetrySnapshot telemetry(
|
||||||
|
List<EngineTelemetrySnapshot> worlds,
|
||||||
|
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
|
||||||
|
long now
|
||||||
|
) {
|
||||||
|
IrisTelemetrySnapshot.CacheBucket resource = new IrisTelemetrySnapshot.CacheBucket(2, 10L, 100L);
|
||||||
|
IrisTelemetrySnapshot.CacheSnapshot caches = new IrisTelemetrySnapshot.CacheSnapshot(
|
||||||
|
resource,
|
||||||
|
resource,
|
||||||
|
IrisTelemetrySnapshot.CacheBucket.EMPTY,
|
||||||
|
IrisTelemetrySnapshot.CacheBucket.EMPTY,
|
||||||
|
IrisTelemetrySnapshot.CacheBucket.EMPTY
|
||||||
|
);
|
||||||
|
return new IrisTelemetrySnapshot(
|
||||||
|
now,
|
||||||
|
worlds,
|
||||||
|
EngineTelemetrySnapshot.aggregate(worlds),
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
0.5D,
|
||||||
|
0.1D,
|
||||||
|
caches,
|
||||||
|
pregenerator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EngineTelemetrySnapshot world(
|
||||||
|
String identity,
|
||||||
|
String name,
|
||||||
|
long loadedChunks,
|
||||||
|
long generatedTotal,
|
||||||
|
double generationMs,
|
||||||
|
long now
|
||||||
|
) {
|
||||||
|
return new EngineTelemetrySnapshot(
|
||||||
|
now,
|
||||||
|
identity,
|
||||||
|
name,
|
||||||
|
"dimension",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
loadedChunks,
|
||||||
|
3L,
|
||||||
|
0.25D,
|
||||||
|
generatedTotal,
|
||||||
|
generatedTotal,
|
||||||
|
2D,
|
||||||
|
4L,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1L,
|
||||||
|
4L,
|
||||||
|
1L,
|
||||||
|
10D,
|
||||||
|
Map.of("total", generationMs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double value(Map<String, IntegrationMetricSample> samples, String key) {
|
||||||
|
IntegrationMetricSample sample = samples.get(key);
|
||||||
|
assertTrue(sample.available());
|
||||||
|
return sample.valueOr(-1D);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.player.PlayerItemHeldEvent;
|
||||||
|
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
|
||||||
|
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class TreeFellerEventOrderTest {
|
||||||
|
@Test
|
||||||
|
public void standaloneIntentPrecedesMonitorFinalization() throws Exception {
|
||||||
|
Method request = TreeFellerSVC.class.getMethod("requestStandalone", org.bukkit.event.block.BlockBreakEvent.class);
|
||||||
|
Method finalize = TreeFellerSVC.class.getMethod("finalizeBreak", org.bukkit.event.block.BlockBreakEvent.class);
|
||||||
|
EventHandler requestHandler = request.getAnnotation(EventHandler.class);
|
||||||
|
EventHandler finalizeHandler = finalize.getAnnotation(EventHandler.class);
|
||||||
|
|
||||||
|
assertEquals(EventPriority.HIGHEST, requestHandler.priority());
|
||||||
|
assertTrue(requestHandler.ignoreCancelled());
|
||||||
|
assertEquals(EventPriority.MONITOR, finalizeHandler.priority());
|
||||||
|
assertFalse(finalizeHandler.ignoreCancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void incompleteDiscoveryFallsOnlyTheTrigger() {
|
||||||
|
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(4, 80, -2);
|
||||||
|
TreeMarkerTraversal.Discovery incomplete = new TreeMarkerTraversal.Discovery(
|
||||||
|
List.of(trigger, new TreeMarkerTraversal.Position(4, 81, -2)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playerControlChangesHaltRunsAtMonitor() throws Exception {
|
||||||
|
Method sneak = TreeFellerSVC.class.getMethod("haltWhenSneakingStops", PlayerToggleSneakEvent.class);
|
||||||
|
Method held = TreeFellerSVC.class.getMethod("haltWhenHeldSlotChanges", PlayerItemHeldEvent.class);
|
||||||
|
Method hands = TreeFellerSVC.class.getMethod("haltWhenHandsSwap", PlayerSwapHandItemsEvent.class);
|
||||||
|
|
||||||
|
assertMonitorCancellationHandler(sneak);
|
||||||
|
assertMonitorCancellationHandler(held);
|
||||||
|
assertMonitorCancellationHandler(hands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMonitorCancellationHandler(Method method) {
|
||||||
|
EventHandler handler = method.getAnnotation(EventHandler.class);
|
||||||
|
|
||||||
|
assertEquals(EventPriority.MONITOR, handler.priority());
|
||||||
|
assertTrue(handler.ignoreCancelled());
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class TreeFellerPresentationTest {
|
||||||
|
@Test
|
||||||
|
public void pulseSizeKeepsSmallTreesReadableAndLargeTreesBounded() {
|
||||||
|
assertEquals(4, TreeFellerPresentation.blocksPerPulse(1));
|
||||||
|
assertEquals(4, TreeFellerPresentation.blocksPerPulse(240));
|
||||||
|
assertEquals(10, TreeFellerPresentation.blocksPerPulse(600));
|
||||||
|
assertEquals(64, TreeFellerPresentation.blocksPerPulse(100_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void effectSamplingStaysBoundedPerPulse() {
|
||||||
|
assertEquals(1, TreeFellerPresentation.effectStride(4));
|
||||||
|
assertEquals(1, TreeFellerPresentation.effectStride(16));
|
||||||
|
assertEquals(2, TreeFellerPresentation.effectStride(17));
|
||||||
|
assertEquals(2, TreeFellerPresentation.effectStride(31));
|
||||||
|
assertEquals(2, TreeFellerPresentation.effectStride(32));
|
||||||
|
assertEquals(3, TreeFellerPresentation.effectStride(33));
|
||||||
|
assertEquals(4, TreeFellerPresentation.effectStride(64));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -120,7 +120,8 @@ dependencies {
|
|||||||
libs.fabricApi.eventsInteraction,
|
libs.fabricApi.eventsInteraction,
|
||||||
libs.fabricApi.networking,
|
libs.fabricApi.networking,
|
||||||
libs.fabricApi.rendering,
|
libs.fabricApi.rendering,
|
||||||
libs.fabricApi.keyMapping
|
libs.fabricApi.keyMapping,
|
||||||
|
libs.fabricApi.permission
|
||||||
]
|
]
|
||||||
fabricApi.each { Object notation ->
|
fabricApi.each { Object notation ->
|
||||||
add('jij', notation)
|
add('jij', notation)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,95 @@
|
|||||||
[17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
|
[19:42:28] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
|
||||||
[17:42:11] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[19:42:28] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
|
java.lang.RuntimeException: second disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
|
[19:42:28] [Test worker/ERROR]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
|
||||||
|
java.lang.RuntimeException: first disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
|
[19:42:28] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: cleanup failed
|
java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
@@ -44,7 +134,7 @@ java.lang.RuntimeException: cleanup failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
[17:42:11] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[19:42:28] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: enable failed
|
java.lang.RuntimeException: enable failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
@@ -92,7 +182,7 @@ java.lang.RuntimeException: enable failed
|
|||||||
Suppressed: java.lang.RuntimeException: cleanup failed
|
Suppressed: java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
|
||||||
... 42 more
|
... 42 more
|
||||||
[17:42:11] [Test worker/ERROR]: [worldcheck] server stop request failed
|
[19:42:28] [Test worker/ERROR]: [worldcheck] server stop request failed
|
||||||
java.lang.IllegalStateException: stop request failed
|
java.lang.IllegalStateException: stop request failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179)
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179)
|
||||||
@@ -139,7 +229,7 @@ java.lang.IllegalStateException: stop request failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
[17:42:11] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
|
[19:42:28] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
|
||||||
java.lang.IllegalStateException: shutdown wait failed
|
java.lang.IllegalStateException: shutdown wait failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194)
|
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194)
|
||||||
@@ -186,7 +276,7 @@ java.lang.IllegalStateException: shutdown wait failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
[17:42:11] [Test worker/ERROR]: [worldcheck] check failed
|
[19:42:28] [Test worker/ERROR]: [worldcheck] check failed
|
||||||
java.lang.IllegalStateException: check failed
|
java.lang.IllegalStateException: check failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174)
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174)
|
||||||
@@ -233,8 +323,8 @@ java.lang.IllegalStateException: check failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
|
||||||
[17:42:11] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
|
[19:42:28] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
|
||||||
[17:42:11] [Test worker/ERROR]: Iris custom content provider discovery failed
|
[19:42:28] [Test worker/ERROR]: Iris custom content provider discovery failed
|
||||||
java.lang.RuntimeException: provider init failed
|
java.lang.RuntimeException: provider init failed
|
||||||
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
|
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
|
||||||
|
|||||||
@@ -19,15 +19,27 @@
|
|||||||
package art.arcane.iris.fabric;
|
package art.arcane.iris.fabric;
|
||||||
|
|
||||||
import art.arcane.iris.modded.ModdedLoader;
|
import art.arcane.iris.modded.ModdedLoader;
|
||||||
|
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||||
import net.fabricmc.api.EnvType;
|
import net.fabricmc.api.EnvType;
|
||||||
|
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||||
|
import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner;
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
import net.fabricmc.loader.api.ModContainer;
|
import net.fabricmc.loader.api.ModContainer;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.server.permissions.PermissionLevel;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public final class FabricModdedLoader implements ModdedLoader {
|
public final class FabricModdedLoader implements ModdedLoader {
|
||||||
|
private static final Identifier TREE_FELLER_PERMISSION = Identifier.fromNamespaceAndPath("iris", "treefeller");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String platformName() {
|
public String platformName() {
|
||||||
return "fabric";
|
return "fabric";
|
||||||
@@ -74,4 +86,24 @@ public final class FabricModdedLoader implements ModdedLoader {
|
|||||||
.map((Path p) -> p.toFile())
|
.map((Path p) -> p.toFile())
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTreeFellerPermission(ServerPlayer player) {
|
||||||
|
return ((PermissionContextOwner) player).checkPermission(
|
||||||
|
TREE_FELLER_PERMISSION,
|
||||||
|
PermissionLevel.GAMEMASTERS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canTreeFellerBreak(
|
||||||
|
ServerLevel level,
|
||||||
|
ServerPlayer player,
|
||||||
|
BlockPos position,
|
||||||
|
BlockState state
|
||||||
|
) {
|
||||||
|
BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(position) : null;
|
||||||
|
return ModdedTreeFellerService.runBreakProbe(() -> PlayerBlockBreakEvents.BEFORE.invoker()
|
||||||
|
.beforeBlockBreak(level, player, position, state, blockEntity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
|
|||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.InteractionResult;
|
import net.minecraft.world.InteractionResult;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
@@ -60,13 +61,19 @@ public final class IrisFabricBootstrap implements ModInitializer {
|
|||||||
ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
|
ServerLifecycleEvents.SERVER_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
|
||||||
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
|
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
|
||||||
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
|
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
|
||||||
|
ServerLevelEvents.UNLOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelUnloaded(level));
|
||||||
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
|
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
|
||||||
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
|
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
|
||||||
if (level instanceof ServerLevel serverLevel) {
|
if (level instanceof ServerLevel serverLevel && player instanceof ServerPlayer serverPlayer) {
|
||||||
ModdedBlockBreakHandler.prepare(serverLevel, pos, state);
|
ModdedBlockBreakHandler.prepare(serverLevel, serverPlayer, pos, state);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
PlayerBlockBreakEvents.CANCELED.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
|
||||||
|
if (level instanceof ServerLevel serverLevel) {
|
||||||
|
ModdedBlockBreakHandler.cancel(serverLevel, pos);
|
||||||
|
}
|
||||||
|
});
|
||||||
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
|
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
|
||||||
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||||
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
|
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package art.arcane.iris.fabric.mixin;
|
||||||
|
|
||||||
|
import art.arcane.iris.modded.ModdedBlockBreakHandler;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.item.BlockItem;
|
||||||
|
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(BlockItem.class)
|
||||||
|
public final class BlockItemMixin {
|
||||||
|
@Inject(method = "placeBlock", at = @At("RETURN"))
|
||||||
|
private void iris$clearTreeProvenance(
|
||||||
|
BlockPlaceContext context,
|
||||||
|
BlockState state,
|
||||||
|
CallbackInfoReturnable<Boolean> info
|
||||||
|
) {
|
||||||
|
if (info.getReturnValue() && context.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedBlockBreakHandler.clearPlacedProvenance(level, context.getClickedPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,10 @@ public class BlockMixin {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (result.routeCombinedDrops(info.getReturnValue())) {
|
||||||
|
info.setReturnValue(List.of());
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<ItemStack> drops = result.replaceVanillaDrops()
|
List<ItemStack> drops = result.replaceVanillaDrops()
|
||||||
? new ArrayList<>()
|
? new ArrayList<>()
|
||||||
: new ArrayList<>(info.getReturnValue());
|
: new ArrayList<>(info.getReturnValue());
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"package": "art.arcane.iris.fabric.mixin",
|
"package": "art.arcane.iris.fabric.mixin",
|
||||||
"compatibilityLevel": "JAVA_25",
|
"compatibilityLevel": "JAVA_25",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
"BlockItemMixin",
|
||||||
"BlockMixin",
|
"BlockMixin",
|
||||||
"ServerPacksSourceMixin"
|
"ServerPacksSourceMixin"
|
||||||
],
|
],
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+101
-11
@@ -1,8 +1,98 @@
|
|||||||
[18Jul2026 17:42:16.031] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
|
[20Jul2026 19:42:36.918] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
|
||||||
[18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
|
[20Jul2026 19:42:36.919] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
|
||||||
[18Jul2026 17:42:16.033] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
|
[20Jul2026 19:42:36.919] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
|
||||||
[18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
[20Jul2026 19:42:38.772] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
||||||
[18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[20Jul2026 19:42:38.831] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
|
java.lang.RuntimeException: second disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55) ~[test/:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
|
[20Jul2026 19:42:38.837] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
|
||||||
|
java.lang.RuntimeException: first disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54) ~[test/:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
|
[20Jul2026 19:42:38.840] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: cleanup failed
|
java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
@@ -47,7 +137,7 @@ java.lang.RuntimeException: cleanup failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[20Jul2026 19:42:38.843] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: enable failed
|
java.lang.RuntimeException: enable failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
@@ -95,7 +185,7 @@ java.lang.RuntimeException: enable failed
|
|||||||
Suppressed: java.lang.RuntimeException: cleanup failed
|
Suppressed: java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
||||||
... 42 more
|
... 42 more
|
||||||
[18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
[20Jul2026 19:42:38.875] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
||||||
java.lang.IllegalStateException: stop request failed
|
java.lang.IllegalStateException: stop request failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
|
||||||
@@ -142,7 +232,7 @@ java.lang.IllegalStateException: stop request failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
[20Jul2026 19:42:38.878] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
||||||
java.lang.IllegalStateException: shutdown wait failed
|
java.lang.IllegalStateException: shutdown wait failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
|
||||||
@@ -189,7 +279,7 @@ java.lang.IllegalStateException: shutdown wait failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
[20Jul2026 19:42:38.883] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
||||||
java.lang.IllegalStateException: check failed
|
java.lang.IllegalStateException: check failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
|
||||||
@@ -236,8 +326,8 @@ java.lang.IllegalStateException: check failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
|
[20Jul2026 19:42:38.889] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
|
||||||
[18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
|
[20Jul2026 19:42:38.889] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
|
||||||
java.lang.RuntimeException: provider init failed
|
java.lang.RuntimeException: provider init failed
|
||||||
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
|
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
|||||||
@@ -1,5 +1,95 @@
|
|||||||
[18Jul2026 17:42:18.043] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
[20Jul2026 19:42:38.772] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
|
||||||
[18Jul2026 17:42:18.104] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[20Jul2026 19:42:38.831] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
|
java.lang.RuntimeException: second disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:55) ~[test/:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
|
[20Jul2026 19:42:38.837] [Test worker/ERROR] [Iris/]: Iris service onDisable failed for art.arcane.iris.modded.ModdedServiceManagerTest$FirstService
|
||||||
|
java.lang.RuntimeException: first disable failed
|
||||||
|
at art.arcane.iris.modded.ModdedServiceManagerTest.disableAttemptsEveryServiceInReverseOrderAndAggregatesFailures(ModdedServiceManagerTest.java:54) ~[test/:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
|
||||||
|
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
|
||||||
|
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
|
||||||
|
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
|
||||||
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
|
[20Jul2026 19:42:38.840] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: cleanup failed
|
java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
@@ -44,7 +134,7 @@ java.lang.RuntimeException: cleanup failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.110] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
[20Jul2026 19:42:38.843] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
|
||||||
java.lang.RuntimeException: enable failed
|
java.lang.RuntimeException: enable failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
@@ -92,7 +182,7 @@ java.lang.RuntimeException: enable failed
|
|||||||
Suppressed: java.lang.RuntimeException: cleanup failed
|
Suppressed: java.lang.RuntimeException: cleanup failed
|
||||||
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
|
||||||
... 42 more
|
... 42 more
|
||||||
[18Jul2026 17:42:18.144] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
[20Jul2026 19:42:38.875] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
|
||||||
java.lang.IllegalStateException: stop request failed
|
java.lang.IllegalStateException: stop request failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:179) ~[main/:?]
|
||||||
@@ -139,7 +229,7 @@ java.lang.IllegalStateException: stop request failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.147] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
[20Jul2026 19:42:38.878] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
|
||||||
java.lang.IllegalStateException: shutdown wait failed
|
java.lang.IllegalStateException: shutdown wait failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:194) ~[main/:?]
|
||||||
@@ -186,7 +276,7 @@ java.lang.IllegalStateException: shutdown wait failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.154] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
[20Jul2026 19:42:38.883] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
|
||||||
java.lang.IllegalStateException: check failed
|
java.lang.IllegalStateException: check failed
|
||||||
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
|
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
|
||||||
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
|
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:174) ~[main/:?]
|
||||||
@@ -233,8 +323,8 @@ java.lang.IllegalStateException: check failed
|
|||||||
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
|
||||||
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
|
||||||
[18Jul2026 17:42:18.160] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
|
[20Jul2026 19:42:38.889] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
|
||||||
[18Jul2026 17:42:18.161] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
|
[20Jul2026 19:42:38.889] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
|
||||||
java.lang.RuntimeException: provider init failed
|
java.lang.RuntimeException: provider init failed
|
||||||
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
|
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
|
||||||
|
|||||||
@@ -19,18 +19,37 @@
|
|||||||
package art.arcane.iris.forge;
|
package art.arcane.iris.forge;
|
||||||
|
|
||||||
import art.arcane.iris.modded.ModdedLoader;
|
import art.arcane.iris.modded.ModdedLoader;
|
||||||
|
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
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 net.minecraftforge.common.util.Result;
|
||||||
|
import net.minecraftforge.event.level.BlockEvent;
|
||||||
import net.minecraftforge.fml.ModList;
|
import net.minecraftforge.fml.ModList;
|
||||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||||
import net.minecraftforge.fml.loading.FMLLoader;
|
import net.minecraftforge.fml.loading.FMLLoader;
|
||||||
import net.minecraftforge.fml.loading.FMLPaths;
|
import net.minecraftforge.fml.loading.FMLPaths;
|
||||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||||
import net.minecraftforge.server.ServerLifecycleHooks;
|
import net.minecraftforge.server.ServerLifecycleHooks;
|
||||||
|
import net.minecraftforge.server.permission.PermissionAPI;
|
||||||
|
import net.minecraftforge.server.permission.nodes.PermissionNode;
|
||||||
|
import net.minecraftforge.server.permission.nodes.PermissionTypes;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public final class ForgeModdedLoader implements ModdedLoader {
|
public final class ForgeModdedLoader implements ModdedLoader {
|
||||||
|
public static final PermissionNode<Boolean> TREE_FELLER_PERMISSION = new PermissionNode<>(
|
||||||
|
"iris",
|
||||||
|
"treefeller",
|
||||||
|
PermissionTypes.BOOLEAN,
|
||||||
|
(player, playerId, contexts) ->
|
||||||
|
player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions())
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String platformName() {
|
public String platformName() {
|
||||||
return "forge";
|
return "forge";
|
||||||
@@ -73,4 +92,29 @@ public final class ForgeModdedLoader implements ModdedLoader {
|
|||||||
IModFileInfo info = ModList.getModFileById("irisworldgen");
|
IModFileInfo info = ModList.getModFileById("irisworldgen");
|
||||||
return info == null ? null : info.getFile().getFilePath().toFile();
|
return info == null ? null : info.getFile().getFilePath().toFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTreeFellerPermission(ServerPlayer player) {
|
||||||
|
return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canTreeFellerBreak(
|
||||||
|
ServerLevel level,
|
||||||
|
ServerPlayer player,
|
||||||
|
BlockPos position,
|
||||||
|
BlockState state
|
||||||
|
) {
|
||||||
|
return ModdedTreeFellerService.runBreakProbe(() -> {
|
||||||
|
BlockEvent.BreakEvent event = new BlockEvent.BreakEvent(
|
||||||
|
level,
|
||||||
|
position,
|
||||||
|
state,
|
||||||
|
player,
|
||||||
|
Result.DEFAULT
|
||||||
|
);
|
||||||
|
BlockEvent.BreakEvent.BUS.post(event);
|
||||||
|
return !event.getResult().isDenied();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,10 @@ public final class IrisForgeBlockLootModifier extends LootModifier {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
return generatedLoot;
|
return generatedLoot;
|
||||||
}
|
}
|
||||||
|
if (result.routeCombinedDrops(generatedLoot)) {
|
||||||
|
generatedLoot.clear();
|
||||||
|
return generatedLoot;
|
||||||
|
}
|
||||||
if (result.replaceVanillaDrops()) {
|
if (result.replaceVanillaDrops()) {
|
||||||
generatedLoot.clear();
|
generatedLoot.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,15 @@ import net.minecraftforge.event.server.ServerStartingEvent;
|
|||||||
import net.minecraftforge.event.server.ServerStoppingEvent;
|
import net.minecraftforge.event.server.ServerStoppingEvent;
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.common.loot.IGlobalLootModifier;
|
import net.minecraftforge.common.loot.IGlobalLootModifier;
|
||||||
|
import net.minecraftforge.common.util.BlockSnapshot;
|
||||||
|
import net.minecraftforge.eventbus.api.listener.Priority;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||||
import net.minecraftforge.fml.loading.FMLLoader;
|
import net.minecraftforge.fml.loading.FMLLoader;
|
||||||
import net.minecraftforge.registries.DeferredRegister;
|
import net.minecraftforge.registries.DeferredRegister;
|
||||||
import net.minecraftforge.registries.ForgeRegistries;
|
import net.minecraftforge.registries.ForgeRegistries;
|
||||||
|
import net.minecraftforge.server.permission.events.PermissionGatherEvent;
|
||||||
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
@@ -80,6 +83,11 @@ public final class IrisForgeBootstrap {
|
|||||||
ModdedEngineBootstrap.levelLoaded(level);
|
ModdedEngineBootstrap.levelLoaded(level);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
LevelEvent.Unload.BUS.addListener((LevelEvent.Unload event) -> {
|
||||||
|
if (event.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedEngineBootstrap.levelUnloaded(level);
|
||||||
|
}
|
||||||
|
});
|
||||||
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
|
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
|
||||||
if (event.getEntity() instanceof ServerPlayer player) {
|
if (event.getEntity() instanceof ServerPlayer player) {
|
||||||
ModdedProtocolHandler.onPlayerJoin(player);
|
ModdedProtocolHandler.onPlayerJoin(player);
|
||||||
@@ -96,11 +104,32 @@ public final class IrisForgeBootstrap {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||||
|
PermissionGatherEvent.Nodes.BUS.addListener((PermissionGatherEvent.Nodes event) ->
|
||||||
|
event.addNodes(ForgeModdedLoader.TREE_FELLER_PERMISSION));
|
||||||
BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> {
|
BlockEvent.BreakEvent.BUS.addListener((BlockEvent.BreakEvent event) -> {
|
||||||
if (event.getLevel() instanceof ServerLevel level && !event.getResult().isDenied()) {
|
if (event.getLevel() instanceof ServerLevel level
|
||||||
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
|
&& event.getPlayer() instanceof ServerPlayer player
|
||||||
|
&& !event.getResult().isDenied()) {
|
||||||
|
ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
BlockEvent.EntityPlaceEvent.BUS.addListener(Priority.MONITOR, false, (BlockEvent.EntityPlaceEvent event) -> {
|
||||||
|
if (!(event instanceof BlockEvent.EntityMultiPlaceEvent)
|
||||||
|
&& event.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BlockEvent.EntityMultiPlaceEvent.BUS.addListener(
|
||||||
|
Priority.MONITOR,
|
||||||
|
false,
|
||||||
|
(BlockEvent.EntityMultiPlaceEvent event) -> {
|
||||||
|
for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) {
|
||||||
|
if (snapshot.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
|
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
|
||||||
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||||
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
|
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
|
||||||
|
|||||||
+148
-62
@@ -19,8 +19,11 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.minecraft.core.Holder;
|
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>> visibleBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||||
|
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
|
||||||
private volatile IrisModdedChunkGenerator generator;
|
private volatile IrisModdedChunkGenerator generator;
|
||||||
private volatile Set<String> possibleStructureBiomeKeys;
|
private volatile Set<String> possibleStructureBiomeKeys;
|
||||||
|
|
||||||
@@ -67,6 +71,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
structureBiomeCache.clear();
|
structureBiomeCache.clear();
|
||||||
surfaceStructureBiomeCache.clear();
|
surfaceStructureBiomeCache.clear();
|
||||||
possibleStructureBiomeKeys = null;
|
possibleStructureBiomeKeys = null;
|
||||||
|
for (StructureStateBiomeSource source : structureStateSources) {
|
||||||
|
source.clearCache();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BiomeSource forStructureState(HolderLookup<StructureSet> structureSets) {
|
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
|
@Override
|
||||||
@@ -134,10 +143,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
@Override
|
@Override
|
||||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = engineOrNull();
|
||||||
if (!isReady(engine)) {
|
if (engine == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
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,
|
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) {
|
Holder<Biome> getVisibleNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = engineOrNull();
|
||||||
if (!isReady(engine)) {
|
if (engine == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
||||||
Holder<Biome> cached = visibleBiomeCache.get(key);
|
if (lease == null) {
|
||||||
if (cached != null) {
|
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
Holder<Biome> existing = visibleBiomeCache.putIfAbsent(key, resolved);
|
if (!isReady(engine)) {
|
||||||
if (visibleBiomeCache.size() > BIOME_CACHE_MAX) {
|
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||||
visibleBiomeCache.clear();
|
}
|
||||||
|
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) {
|
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()) {
|
for (Holder<Biome> biome : structure.value().biomes()) {
|
||||||
String key = holderKey(biome);
|
String key = holderKey(biome);
|
||||||
if (isGeneratedBiomeKey(key, possible)) {
|
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) {
|
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||||
Engine engine = engineOrNull();
|
Engine engine = engineOrNull();
|
||||||
if (!isReady(engine)) {
|
if (engine == null) {
|
||||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||||
}
|
}
|
||||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_biomes_within");
|
||||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
if (lease == null) {
|
||||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
|
||||||
}
|
}
|
||||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
if (!isReady(engine)) {
|
||||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
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,
|
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");
|
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
||||||
}
|
}
|
||||||
Engine engine = current.awaitStructureEngine();
|
Engine engine = current.awaitStructureEngine();
|
||||||
Registry<Biome> registry = biomeRegistry();
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
||||||
if (registry == null) {
|
if (lease == null) {
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||||
}
|
}
|
||||||
IrisBiome irisBiome;
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
if (!isReady(engine)) {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
} else {
|
}
|
||||||
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
|
Registry<Biome> registry = biomeRegistry();
|
||||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
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) {
|
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();
|
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() {
|
private Set<String> possibleStructureBiomeKeys() {
|
||||||
Set<String> cached = possibleStructureBiomeKeys;
|
Set<String> cached = possibleStructureBiomeKeys;
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
@@ -429,24 +502,33 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
Engine engine = current.structureEngineOrNull();
|
Engine engine = current.structureEngineOrNull();
|
||||||
if (!isReady(engine)) {
|
if (engine == null) {
|
||||||
return current.configuredStructureBiomeKeys();
|
return current.configuredStructureBiomeKeys();
|
||||||
}
|
}
|
||||||
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
|
||||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
if (lease == null) {
|
||||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
throw new IllegalStateException("Iris structure biome key lookup was rejected during an engine transition");
|
||||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
}
|
||||||
if (derivative != null) {
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
possible.add(derivative);
|
if (!isReady(engine)) {
|
||||||
|
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
if (!irisBiome.isCustom()) {
|
LinkedHashSet<String> possible = new LinkedHashSet<>();
|
||||||
continue;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
return Set.copyOf(possible);
|
||||||
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Set.copyOf(possible);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
|
||||||
@@ -517,6 +599,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
this.possibleBiomes = possibleBiomes;
|
this.possibleBiomes = possibleBiomes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearCache() {
|
||||||
|
resolvedBiomes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected MapCodec<? extends BiomeSource> codec() {
|
protected MapCodec<? extends BiomeSource> codec() {
|
||||||
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
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.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
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.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
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.iris.util.project.hunk.Hunk;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import com.mojang.datafixers.util.Pair;
|
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 ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
|
||||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||||
|
private volatile boolean unloading;
|
||||||
private volatile Engine engine;
|
private volatile Engine engine;
|
||||||
private volatile String activePack;
|
private volatile String activePack;
|
||||||
private volatile String activeDimensionKey;
|
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) {
|
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
||||||
|
requireBindingAllowed();
|
||||||
if (level.getChunkSource().getGenerator() != this) {
|
if (level.getChunkSource().getGenerator() != this) {
|
||||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||||
}
|
}
|
||||||
@@ -258,11 +262,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void unbindEngine() {
|
public synchronized void unbindEngine() {
|
||||||
|
unloading = true;
|
||||||
ServerLevel level = boundLevel();
|
ServerLevel level = boundLevel();
|
||||||
unbindEngine(level);
|
unbindEngine(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void unbindEngine(ServerLevel level) {
|
synchronized void unbindEngine(ServerLevel level) {
|
||||||
|
unloading = true;
|
||||||
if (level != null) {
|
if (level != null) {
|
||||||
ModdedWorldEngines.evictOrThrow(level);
|
ModdedWorldEngines.evictOrThrow(level);
|
||||||
}
|
}
|
||||||
@@ -322,13 +328,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
BlockPos pos, int radius,
|
BlockPos pos, int radius,
|
||||||
boolean findUnexplored) {
|
boolean findUnexplored) {
|
||||||
Engine current = engine();
|
Engine current = engine();
|
||||||
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||||
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
|
||||||
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
level, holders, pos, Math.max(1, radius), findUnexplored, current);
|
||||||
? null
|
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
|
||||||
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
|
||||||
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
? null
|
||||||
|
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||||
|
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isNativeStructureReachable(Holder<Structure> structure) {
|
public boolean isNativeStructureReachable(Holder<Structure> structure) {
|
||||||
@@ -412,8 +421,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Engine engine() {
|
private Engine engine() {
|
||||||
|
requireBindingAllowed();
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
if (cached != null) {
|
requireCompletedShutdown(cached);
|
||||||
|
if (cached != null && !cached.isClosed()) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
ServerLevel level = boundLevel();
|
ServerLevel level = boundLevel();
|
||||||
@@ -423,14 +434,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
return bindEngine(level);
|
return bindEngine(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindLevel(ServerLevel level) {
|
synchronized void bindLevel(ServerLevel level) {
|
||||||
if (level.getChunkSource().getGenerator() != this) {
|
if (level.getChunkSource().getGenerator() != this) {
|
||||||
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
|
||||||
}
|
}
|
||||||
|
requireCompletedShutdown(engine);
|
||||||
|
unloading = false;
|
||||||
bindEngine(level);
|
bindEngine(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Engine bindEngine(ServerLevel level) {
|
private Engine bindEngine(ServerLevel level) {
|
||||||
|
requireBindingAllowed();
|
||||||
try {
|
try {
|
||||||
requireGlobalStructureGeneration(
|
requireGlobalStructureGeneration(
|
||||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||||
@@ -439,18 +453,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
|
requireCompletedShutdown(cached);
|
||||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||||
engineBinding.complete(cached);
|
engineBinding.complete(cached);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
requireBindingAllowed();
|
||||||
Engine existing = engine;
|
Engine existing = engine;
|
||||||
|
requireCompletedShutdown(existing);
|
||||||
if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
|
if (existing != null && !existing.isClosed() && existing.getComplex() != null) {
|
||||||
engineBinding.complete(existing);
|
engineBinding.complete(existing);
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||||
|
requireCompletedShutdown(created);
|
||||||
if (created.isClosed() || created.getComplex() == null) {
|
if (created.isClosed() || created.getComplex() == null) {
|
||||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||||
+ "' created an engine without a ready biome complex");
|
+ "' 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) {
|
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
return;
|
return;
|
||||||
@@ -483,8 +514,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Engine engineOrNull() {
|
private Engine engineOrNull() {
|
||||||
|
requireBindingAllowed();
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
if (cached != null) {
|
requireCompletedShutdown(cached);
|
||||||
|
if (cached != null && !cached.isClosed()) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -495,8 +528,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Engine structureEngineOrNull() {
|
Engine structureEngineOrNull() {
|
||||||
|
requireBindingAllowed();
|
||||||
engineBinding.throwIfFailed(dimensionKey);
|
engineBinding.throwIfFailed(dimensionKey);
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
|
requireCompletedShutdown(cached);
|
||||||
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -505,11 +540,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Engine awaitStructureEngine() {
|
Engine awaitStructureEngine() {
|
||||||
|
requireBindingAllowed();
|
||||||
Engine current = engine;
|
Engine current = engine;
|
||||||
|
requireCompletedShutdown(current);
|
||||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
Engine bound = engineBinding.await(dimensionKey);
|
Engine bound = engineBinding.await(dimensionKey);
|
||||||
|
requireCompletedShutdown(bound);
|
||||||
if (bound.isClosed() || bound.getComplex() == null) {
|
if (bound.isClosed() || bound.getComplex() == null) {
|
||||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||||
+ "' completed bootstrap without a ready biome complex");
|
+ "' completed bootstrap without a ready biome complex");
|
||||||
@@ -518,7 +556,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Engine requireDataQueryEngine(String operation) {
|
private Engine requireDataQueryEngine(String operation) {
|
||||||
|
requireBindingAllowed();
|
||||||
Engine current = engine;
|
Engine current = engine;
|
||||||
|
requireCompletedShutdown(current);
|
||||||
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
if (current != null && !current.isClosed() && current.getComplex() != null) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
@@ -561,18 +601,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
return configuredStructureBiomeKeys;
|
return configuredStructureBiomeKeys;
|
||||||
}
|
}
|
||||||
Engine current = engine;
|
Engine current = engine;
|
||||||
Iterable<IrisBiome> biomes;
|
if (current != null && !current.isClosed() && !current.isClosing()) {
|
||||||
String namespace;
|
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
||||||
if (current != null && !current.isClosed()) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
biomes = current.getAllBiomes();
|
Set<String> resolved = collectConfiguredBiomeKeys(
|
||||||
namespace = current.getDimension().getLoadKey();
|
current.getAllBiomes(), current.getDimension().getLoadKey());
|
||||||
} else {
|
configuredStructureBiomeKeys = resolved;
|
||||||
ConfiguredPack configured = configuredPack();
|
return resolved;
|
||||||
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
|
} catch (GenerationSessionException e) {
|
||||||
configuredStructureBiomeKeys = resolved;
|
if (!current.isClosing() && !e.isExpectedTeardown()) {
|
||||||
return resolved;
|
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;
|
configuredStructureBiomeKeys = resolved;
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
@@ -654,7 +697,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Engine engineIfBound() {
|
public Engine engineIfBound() {
|
||||||
return engine;
|
Engine current = engine;
|
||||||
|
return unloading || current == null || current.isClosing() || current.isClosed() ? null : current;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Engine commandEngine() {
|
public Engine commandEngine() {
|
||||||
@@ -710,23 +754,26 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
|
||||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||||
continue;
|
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||||
}
|
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
continue;
|
||||||
if (vanillaHolder == null) {
|
}
|
||||||
continue;
|
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||||
}
|
if (vanillaHolder == null) {
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
continue;
|
||||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
}
|
||||||
if (customHolder != null) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
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) {
|
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||||
@@ -750,8 +797,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
||||||
StructureManager structureManager, ChunkAccess chunk) {
|
StructureManager structureManager, ChunkAccess chunk) {
|
||||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
Engine current = engine();
|
||||||
return CompletableFuture.completedFuture(chunk);
|
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
|
@Override
|
||||||
@@ -759,36 +810,46 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
Engine generationEngine = engine();
|
Engine generationEngine = engine();
|
||||||
ChunkPos pos = chunk.getPos();
|
ChunkPos pos = chunk.getPos();
|
||||||
lastChunkGenAt = System.currentTimeMillis();
|
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());
|
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();
|
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||||
|
|
||||||
if (PARALLEL_CHUNK_SYSTEM) {
|
if (PARALLEL_CHUNK_SYSTEM) {
|
||||||
return CompletableFuture.completedFuture(
|
return CompletableFuture.completedFuture(
|
||||||
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air));
|
generateTerrain(chunk, generationEngine, pos, air));
|
||||||
}
|
}
|
||||||
return CompletableFuture.supplyAsync(
|
return CompletableFuture.supplyAsync(
|
||||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air),
|
() -> generateTerrain(chunk, generationEngine, pos, air),
|
||||||
genPool);
|
genPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||||
int dimMinY, int height, PlatformBlockState air) {
|
PlatformBlockState air) {
|
||||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
||||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
|
||||||
try {
|
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);
|
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) {
|
} 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());
|
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);
|
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);
|
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);
|
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);
|
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) {
|
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
||||||
@@ -885,20 +938,31 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
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
|
@Override
|
||||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||||
Engine current = engine();
|
Engine current = engine();
|
||||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
|
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||||
|
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||||
|
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
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,
|
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) {
|
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||||
Engine current = requireDataQueryEngine("base height");
|
Engine current = requireDataQueryEngine("base height");
|
||||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
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
|
@Override
|
||||||
@@ -1176,17 +1245,30 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
int minY = heightAccessor.getMinY();
|
int minY = heightAccessor.getMinY();
|
||||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||||
Engine current = requireDataQueryEngine("base column");
|
Engine current = requireDataQueryEngine("base column");
|
||||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||||
int surface = current.getHeight(x, z, true);
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
int fluid = current.getHeight(x, z, false);
|
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
int surface = current.getHeight(x, z, true);
|
||||||
BlockState water = Blocks.WATER.defaultBlockState();
|
int fluid = current.getHeight(x, z, false);
|
||||||
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
|
BlockState water = Blocks.WATER.defaultBlockState();
|
||||||
Arrays.fill(states, 0, solidEnd, stone);
|
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
|
||||||
Arrays.fill(states, solidEnd, fluidEnd, water);
|
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid + 1)));
|
||||||
Arrays.fill(states, fluidEnd, states.length, airState);
|
Arrays.fill(states, 0, solidEnd, stone);
|
||||||
return new NoiseColumn(minY, states);
|
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
|
@Override
|
||||||
|
|||||||
+143
-15
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
|
|||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.LootResolver;
|
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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBlockData;
|
import art.arcane.iris.engine.object.IrisBlockData;
|
||||||
import art.arcane.iris.engine.object.IrisBlockDrops;
|
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.IrisMarker;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
import net.minecraft.util.RandomSource;
|
import net.minecraft.util.RandomSource;
|
||||||
import net.minecraft.world.entity.EntityTypes;
|
import net.minecraft.world.entity.EntityTypes;
|
||||||
@@ -43,6 +46,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public final class ModdedBlockBreakHandler {
|
public final class ModdedBlockBreakHandler {
|
||||||
@@ -52,12 +56,24 @@ public final class ModdedBlockBreakHandler {
|
|||||||
private 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) {
|
if (engineFor(level) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BreakKey key = new BreakKey(level, position.asLong());
|
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);
|
PENDING.put(key, pending);
|
||||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||||
if (scheduler != null) {
|
if (scheduler != null) {
|
||||||
@@ -69,11 +85,15 @@ public final class ModdedBlockBreakHandler {
|
|||||||
PENDING.clear();
|
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) {
|
public static Result complete(ServerLevel level, BlockPos position, BlockState fallbackState) {
|
||||||
BreakKey key = new BreakKey(level, position.asLong());
|
BreakKey key = new BreakKey(level, position.asLong());
|
||||||
PendingBreak pending = PENDING.remove(key);
|
PendingBreak pending = PENDING.remove(key);
|
||||||
BlockState brokenState = pending == null ? fallbackState : pending.brokenState();
|
PendingBreak resolved = pending == null ? new PendingBreak(fallbackState, null) : pending;
|
||||||
return evaluateSafely(level, position, brokenState);
|
return evaluateSafely(level, position, resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result completePrepared(ServerLevel level, BlockPos position) {
|
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())) {
|
if (pending == null || level.getBlockState(position).equals(pending.brokenState())) {
|
||||||
return null;
|
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) {
|
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())) {
|
if (level.getBlockState(position).equals(pending.brokenState())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Result result = evaluateSafely(level, position, pending.brokenState());
|
Result result = evaluateSafely(level, position, pending);
|
||||||
spawn(level, position, result.drops());
|
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 {
|
try {
|
||||||
return evaluate(level, position, brokenState);
|
return evaluate(level, position, pending);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
|
LOGGER.error("Iris block-break processing failed at {},{},{} in {}", position.getX(), position.getY(), position.getZ(),
|
||||||
level.dimension().identifier(), error);
|
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);
|
Engine engine = engineFor(level);
|
||||||
if (engine == null || engine.isClosed()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
return Result.empty();
|
return Result.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMarker(engine, position);
|
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);
|
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
|
||||||
if (providers.isEmpty()) {
|
if (providers.isEmpty()) {
|
||||||
return Result.empty();
|
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) {
|
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) {
|
private static KList<IrisBlockDrops> providers(Engine engine, BlockPos position, BlockState brokenState) {
|
||||||
KList<IrisBlockDrops> providers = new KList<>();
|
KList<IrisBlockDrops> providers = new KList<>();
|
||||||
IrisData data = engine.getData();
|
IrisData data = engine.getData();
|
||||||
@@ -227,7 +298,7 @@ public final class ModdedBlockBreakHandler {
|
|||||||
return exact ? configuredState.equals(brokenState) : configuredState.getBlock() == brokenState.getBlock();
|
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();
|
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||||
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
||||||
return null;
|
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() {
|
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 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
IrisModdedChunkGenerator generator = level.getChunkSource().getGenerator()
|
||||||
|
instanceof IrisModdedChunkGenerator irisGenerator
|
||||||
|
? irisGenerator
|
||||||
|
: null;
|
||||||
|
boolean generatorUnbound = false;
|
||||||
try {
|
try {
|
||||||
evacuate(server, level);
|
evacuate(server, level);
|
||||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
if (generator != null) {
|
||||||
generator.unbindEngine();
|
generator.unbindEngine(level);
|
||||||
|
generatorUnbound = true;
|
||||||
}
|
}
|
||||||
ModdedWorldEngines.evict(level);
|
ModdedWorldEngines.evictOrThrow(level);
|
||||||
level.save(null, true, false);
|
level.save(null, true, false);
|
||||||
serverAccess.removeLevel(server, key);
|
serverAccess.removeLevel(server, key);
|
||||||
level.close();
|
level.close();
|
||||||
@@ -191,12 +197,31 @@ public final class ModdedDimensionManager {
|
|||||||
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
|
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);
|
||||||
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
||||||
throw new IllegalStateException("Iris runtime dimension removal failed for " + 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) {
|
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
|
||||||
ServerLevel level = level(server, dimensionId);
|
ServerLevel level = level(server, dimensionId);
|
||||||
if (level == null) {
|
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.ModdedPreservationService;
|
||||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
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.IrisPlatform;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
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() {
|
public static void stop() {
|
||||||
MinecraftServer stoppingServer = currentServer;
|
MinecraftServer stoppingServer = currentServer;
|
||||||
ModdedWorldCheck.serverStopped(stoppingServer);
|
Throwable failure = null;
|
||||||
ModdedProtocolHandler.stop();
|
failure = runStopStage(failure, "world check", () -> ModdedWorldCheck.serverStopped(stoppingServer));
|
||||||
ModdedPregenJob.shutdown();
|
failure = runStopStage(failure, "protocol", ModdedProtocolHandler::stop);
|
||||||
ModdedObjectUndo.clearAll();
|
failure = runStopStage(failure, "pregenerator", ModdedPregenJob::shutdown);
|
||||||
ModdedWandService.clearAll();
|
failure = runStopStage(failure, "object undo", ModdedObjectUndo::clearAll);
|
||||||
ModdedBlockBreakHandler.clear();
|
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||||
ModdedStudioCommands.clear();
|
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||||
ModdedWorldEngines.shutdown();
|
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||||
ModdedPrimaryWorldRouter.clear();
|
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||||
services().disableAll();
|
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
|
||||||
ModdedDimensionManager.clear();
|
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
|
||||||
|
failure = runStopStage(failure, "dimension manager", ModdedDimensionManager::clear);
|
||||||
ModdedScheduler scheduler = schedulerOrNull();
|
ModdedScheduler scheduler = schedulerOrNull();
|
||||||
if (scheduler != null) {
|
if (scheduler != null) {
|
||||||
scheduler.shutdown();
|
failure = runStopStage(failure, "scheduler", scheduler::shutdown);
|
||||||
}
|
}
|
||||||
IrisModdedChunkGenerator.shutdownGenPool();
|
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
|
||||||
ModdedSentry.flush();
|
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
|
||||||
ModdedStartup.reset();
|
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
|
||||||
currentServer = null;
|
failure = runStopStage(failure, "server state", () -> {
|
||||||
spawnCaptureServer = null;
|
currentServer = null;
|
||||||
initialSpawnWasDefault = false;
|
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) {
|
private static void captureInitialSpawn(MinecraftServer server) {
|
||||||
@@ -317,6 +371,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||||
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||||
|
createdServices.register(ModdedTreeFellerService.class, new ModdedTreeFellerService());
|
||||||
|
|
||||||
bindService(PreservationRegistry.class, preservation, rollback);
|
bindService(PreservationRegistry.class, preservation, rollback);
|
||||||
bindService(EngineEffectsProvider.class,
|
bindService(EngineEffectsProvider.class,
|
||||||
@@ -377,6 +432,11 @@ public final class ModdedEngineBootstrap {
|
|||||||
void restore() throws Throwable;
|
void restore() throws Throwable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface StopAction {
|
||||||
|
void run() throws Throwable;
|
||||||
|
}
|
||||||
|
|
||||||
private static final class BindRollback {
|
private static final class BindRollback {
|
||||||
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
|
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
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.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -39,4 +43,8 @@ public interface ModdedLoader {
|
|||||||
Path configDir();
|
Path configDir();
|
||||||
|
|
||||||
File modJar();
|
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.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public final class ModdedServiceManager {
|
public final class ModdedServiceManager {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||||
@@ -92,7 +91,24 @@ public final class ModdedServiceManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enabled = false;
|
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) {
|
synchronized void rollback(Throwable failure) {
|
||||||
@@ -106,14 +122,6 @@ public final class ModdedServiceManager {
|
|||||||
services.clear();
|
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) {
|
private void tickService(ModdedTickableService service, MinecraftServer server) {
|
||||||
try {
|
try {
|
||||||
service.onServerTick(server);
|
service.onServerTick(server);
|
||||||
@@ -144,10 +152,4 @@ public final class ModdedServiceManager {
|
|||||||
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
|
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();
|
ModdedEngineBootstrap.bind();
|
||||||
PackValidationRegistry.requireLoadable(pack);
|
PackValidationRegistry.requireLoadable(pack);
|
||||||
File packDir = resolvePack(pack, dimensionKey);
|
File packDir = resolvePack(pack, dimensionKey);
|
||||||
IrisData data = IrisData.get(packDir);
|
IrisData data = IrisData.openRuntime(packDir);
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
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() {
|
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();
|
Engine engine = entry.getValue();
|
||||||
try {
|
try {
|
||||||
if (!engine.isClosed()) {
|
close(engine);
|
||||||
engine.close();
|
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) {
|
} 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.core.gui.PregeneratorJob;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.EngineWorldManager;
|
||||||
import art.arcane.iris.engine.framework.LootResolver;
|
import art.arcane.iris.engine.framework.LootResolver;
|
||||||
import art.arcane.iris.engine.object.IRare;
|
import art.arcane.iris.engine.object.IRare;
|
||||||
@@ -79,6 +80,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
private long lastAmbientAt;
|
private long lastAmbientAt;
|
||||||
private long lastCountAt;
|
private long lastCountAt;
|
||||||
private long lastInitialRecoveryAt;
|
private long lastInitialRecoveryAt;
|
||||||
|
private boolean initialSpawnQueueClosed;
|
||||||
|
private boolean mantleWarmupExecutorStopped;
|
||||||
|
private boolean mantleWarmupsCleared;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
private volatile int cachedEntityCount;
|
private volatile int cachedEntityCount;
|
||||||
private volatile int cachedConsideredChunks;
|
private volatile int cachedConsideredChunks;
|
||||||
@@ -116,6 +120,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void serverTick(ServerLevel level) {
|
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()) {
|
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||||
return;
|
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.");
|
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
|
||||||
return;
|
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) {
|
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
|
||||||
@@ -272,7 +285,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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) {
|
} catch (RejectedExecutionException e) {
|
||||||
mantleWarmups.remove(key);
|
mantleWarmups.remove(key);
|
||||||
}
|
}
|
||||||
@@ -727,11 +747,36 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public synchronized void close() {
|
||||||
closed = true;
|
closed = true;
|
||||||
initialSpawnQueue.close();
|
Throwable failure = null;
|
||||||
mantleWarmupExecutor.shutdownNow();
|
if (!initialSpawnQueueClosed) {
|
||||||
mantleWarmups.clear();
|
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
|
@Override
|
||||||
@@ -757,4 +802,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
public void onSave() {
|
public void onSave() {
|
||||||
engine.getMantle().save();
|
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.loader.IrisRegistrant;
|
||||||
import art.arcane.iris.core.pack.PackDownloader;
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.Locator;
|
import art.arcane.iris.engine.framework.Locator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
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.modded.ModdedPackInstaller;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
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.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.Position2;
|
import art.arcane.volmlib.util.math.Position2;
|
||||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||||
@@ -95,7 +98,11 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.function.Predicate;
|
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 Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||||
private static final long LOCATE_TIMEOUT_MS = 120000L;
|
private static final long LOCATE_TIMEOUT_MS = 120000L;
|
||||||
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
|
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> 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);
|
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 chunkX = player.blockPosition().getX() >> 4;
|
||||||
int chunkZ = player.blockPosition().getZ() >> 4;
|
int chunkZ = player.blockPosition().getZ() >> 4;
|
||||||
ok(source, "Searching for " + label + "...");
|
ok(source, "Searching for " + label + "...");
|
||||||
Thread thread = new Thread(() -> {
|
CompletableFuture<Position2> search;
|
||||||
try {
|
try {
|
||||||
Position2 at = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
|
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
|
||||||
}).get();
|
});
|
||||||
if (at == null) {
|
} catch (WrongEngineBroException e) {
|
||||||
server.execute(() -> fail(source, "Could not find " + label + " within the search timeout."));
|
fail(source, "The engine for this world has been closed; rejoin the dimension and try again.");
|
||||||
return;
|
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;
|
return;
|
||||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
}
|
||||||
server.execute(() -> {
|
if (at == null) {
|
||||||
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
server.execute(() -> {
|
||||||
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
|
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||||
});
|
fail(source, "Could not find " + label + " within the search timeout.");
|
||||||
} catch (WrongEngineBroException e) {
|
}
|
||||||
server.execute(() -> fail(source, "The engine for this world has been closed; rejoin the dimension and try again."));
|
});
|
||||||
} catch (InterruptedException e) {
|
return;
|
||||||
Thread.currentThread().interrupt();
|
}
|
||||||
} catch (ExecutionException e) {
|
server.execute(() -> {
|
||||||
LOGGER.error("Iris locate failed for {}", label, e);
|
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
|
||||||
server.execute(() -> fail(source, "Search failed: " + e.getCause()));
|
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) {
|
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 net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
public final class ModdedPregenJob {
|
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 static volatile String dimension = "?";
|
||||||
|
|
||||||
private ModdedPregenJob() {
|
private ModdedPregenJob() {
|
||||||
@@ -54,13 +59,26 @@ public final class ModdedPregenJob {
|
|||||||
.radiusX(radiusBlocks)
|
.radiusX(radiusBlocks)
|
||||||
.radiusZ(radiusBlocks)
|
.radiusZ(radiusBlocks)
|
||||||
.build();
|
.build();
|
||||||
PregeneratorMethod method = new ModdedPregenMethod(level, engine, sync);
|
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
|
||||||
|
PregeneratorMethod method = moddedMethod;
|
||||||
if (cached) {
|
if (cached) {
|
||||||
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
|
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
|
||||||
}
|
}
|
||||||
|
ActivePregen active = new ActivePregen(engine, moddedMethod);
|
||||||
|
ACTIVE.set(active);
|
||||||
dimension = level.dimension().identifier().toString();
|
dimension = level.dimension().identifier().toString();
|
||||||
new PregeneratorJob(task, method, engine);
|
try {
|
||||||
return true;
|
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) {
|
private static File cacheDirectory(ServerLevel level) {
|
||||||
@@ -73,7 +91,17 @@ public final class ModdedPregenJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void shutdown() {
|
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() {
|
public static Boolean pauseResume() {
|
||||||
@@ -125,4 +153,64 @@ public final class ModdedPregenJob {
|
|||||||
status.append(ModdedCommandFeedback.footer());
|
status.append(ModdedCommandFeedback.footer());
|
||||||
return status;
|
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.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public final class ModdedPregenMethod implements PregeneratorMethod {
|
public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
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 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 int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||||
private static final long ADAPTIVE_RECOVERY_INTERVAL = 64L;
|
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 static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||||
|
|
||||||
private final ServerLevel level;
|
private final ServerLevel level;
|
||||||
@@ -62,6 +66,10 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
private final AtomicInteger adaptiveLimit;
|
private final AtomicInteger adaptiveLimit;
|
||||||
private final AtomicInteger timeoutStreak = new AtomicInteger();
|
private final AtomicInteger timeoutStreak = new AtomicInteger();
|
||||||
private final AtomicLong completed = new AtomicLong();
|
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 int timeoutSeconds;
|
||||||
private final PregenMantleBackpressure backpressure;
|
private final PregenMantleBackpressure backpressure;
|
||||||
|
|
||||||
@@ -114,6 +122,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
LOGGER.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
|
LOGGER.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
|
||||||
level.dimension().identifier(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
|
level.dimension().identifier(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
|
||||||
|
if (deferFinalSaveIfRequested()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
saveLevel(true);
|
saveLevel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,24 +133,156 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
saveLevel(false);
|
saveLevel(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveLevel(boolean wait) {
|
boolean deferFinalSaveToServerThread() {
|
||||||
CompletableFuture<Void> saved = new CompletableFuture<>();
|
if (!level.getServer().isSameThread()) {
|
||||||
level.getServer().execute(() -> {
|
return false;
|
||||||
try {
|
}
|
||||||
level.save(null, false, false);
|
finalSaveOnServerThread.set(true);
|
||||||
} finally {
|
return true;
|
||||||
saved.complete(null);
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
});
|
finalSaveDeferred.set(false);
|
||||||
if (!wait) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
saved.get(10, TimeUnit.SECONDS);
|
saveLevelOnServerThread();
|
||||||
} catch (InterruptedException e) {
|
finalSaveCompleted.set(true);
|
||||||
Thread.currentThread().interrupt();
|
request.complete();
|
||||||
} catch (TimeoutException | ExecutionException e) {
|
} catch (RuntimeException | Error failure) {
|
||||||
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
|
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() {
|
public Mantle getMantle() {
|
||||||
return engine.getMantle().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;
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.service.EngineMaintenance;
|
||||||
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.engine.framework.Engine;
|
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.modded.ModdedWorldEngines;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -33,71 +33,74 @@ import org.slf4j.LoggerFactory;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Locale;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
|
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
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 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 Set<Engine> inFlight = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||||
|
private final Map<Engine, Long> lastSavedAt = Collections.synchronizedMap(new IdentityHashMap<>());
|
||||||
private volatile ExecutorService service;
|
private volatile ExecutorService service;
|
||||||
private long lastMaintenanceAt;
|
private long lastMaintenanceAt;
|
||||||
private long lastSaveAt;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
if (service != null) {
|
ExecutorService current = service;
|
||||||
|
if (current != null && !current.isShutdown()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
|
|
||||||
IrisSettings.IrisSettingsEngineSVC engineSettings = settings.getEngineSVC();
|
IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC();
|
||||||
ThreadFactory factory = (engineSettings.isUseVirtualThreads()
|
ThreadFactory factory = (settings.isUseVirtualThreads()
|
||||||
? Thread.ofVirtual()
|
? Thread.ofVirtual()
|
||||||
: Thread.ofPlatform().priority(engineSettings.getPriority()))
|
: Thread.ofPlatform().priority(settings.getPriority()))
|
||||||
.name("Iris EngineSVC-", 0)
|
.name("Iris EngineSVC-", 0)
|
||||||
.factory();
|
.factory();
|
||||||
service = Executors.newThreadPerTaskExecutor(factory);
|
service = Executors.newFixedThreadPool(EngineMaintenance.workerParallelism(), factory);
|
||||||
tectonicLimit.set(settings.getTectonicPlateSize());
|
|
||||||
lastMaintenanceAt = 0L;
|
lastMaintenanceAt = 0L;
|
||||||
lastSaveAt = System.currentTimeMillis();
|
inFlight.clear();
|
||||||
|
lastSavedAt.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
ExecutorService active = service;
|
ExecutorService active = service;
|
||||||
service = null;
|
service = null;
|
||||||
if (active != null) {
|
boolean drained = shutdownAndDrain(active);
|
||||||
active.shutdown();
|
if (drained) {
|
||||||
|
inFlight.clear();
|
||||||
|
}
|
||||||
|
lastSavedAt.clear();
|
||||||
|
if (!drained) {
|
||||||
|
throw new IllegalStateException("Iris engine maintenance workers remained active during service shutdown");
|
||||||
}
|
}
|
||||||
inFlight.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServerTick(MinecraftServer server) {
|
public void onServerTick(MinecraftServer server) {
|
||||||
ExecutorService active = service;
|
ExecutorService active = service;
|
||||||
if (active == null) {
|
if (active == null || active.isShutdown()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (now - lastMaintenanceAt < TRIM_PERIOD_MILLIS) {
|
if (now - lastMaintenanceAt < MAINTENANCE_PERIOD_MILLIS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastMaintenanceAt = now;
|
lastMaintenanceAt = now;
|
||||||
boolean flush = now - lastSaveAt >= SAVE_PERIOD_MILLIS;
|
|
||||||
if (flush) {
|
|
||||||
lastSaveAt = now;
|
|
||||||
}
|
|
||||||
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
|
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
|
||||||
int share = tectonicLimit.get() / Math.max(engines.size(), 1);
|
reconcileSaveState(engines, now);
|
||||||
for (Engine engine : engines) {
|
for (Engine engine : engines) {
|
||||||
if (!inFlight.add(engine)) {
|
if (!inFlight.add(engine)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -105,101 +108,118 @@ public final class ModdedEngineMaintenanceService implements ModdedTickableServi
|
|||||||
try {
|
try {
|
||||||
active.execute(() -> {
|
active.execute(() -> {
|
||||||
try {
|
try {
|
||||||
maintain(engine, share, flush);
|
maintain(engine, now);
|
||||||
} finally {
|
} finally {
|
||||||
inFlight.remove(engine);
|
inFlight.remove(engine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (RejectedExecutionException rejected) {
|
} catch (RejectedExecutionException exception) {
|
||||||
inFlight.remove(engine);
|
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) {
|
private void maintain(Engine engine, long scheduledAt) {
|
||||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
if (engine == null || engine.isClosing() || engine.isClosed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pregenTargets(engine)) {
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_engine_maintenance");
|
||||||
return;
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
}
|
if (!EngineMaintenance.isAvailable(engine)) {
|
||||||
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)) {
|
|
||||||
return;
|
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) {
|
private void saveIfDue(Engine engine, long scheduledAt) {
|
||||||
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
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 true;
|
||||||
}
|
}
|
||||||
return pregenTargets(engine);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldSkipForMaintenance(Engine engine) {
|
active.shutdown();
|
||||||
if (engine.getWorld() == null || !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity())) {
|
try {
|
||||||
return false;
|
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||||
}
|
|
||||||
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")) {
|
|
||||||
return true;
|
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;
|
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.MeteredCache;
|
||||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -55,6 +56,7 @@ public final class ModdedPreservationService implements ModdedService, Preservat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dereference() {
|
public void dereference() {
|
||||||
|
IrisData.dereference();
|
||||||
threads.removeIf((Thread thread) -> !thread.isAlive());
|
threads.removeIf((Thread thread) -> !thread.isAlive());
|
||||||
services.removeIf(ExecutorService::isShutdown);
|
services.removeIf(ExecutorService::isShutdown);
|
||||||
caches.removeIf((WeakReference<MeteredCache> ref) -> {
|
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.ModdedDimensionManager;
|
||||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||||
@@ -180,7 +181,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdownPregenerator(Engine engine) {
|
public void shutdownPregenerator(Engine engine) {
|
||||||
PregeneratorJob.shutdownInstance();
|
IrisWorld world = engine.getWorld();
|
||||||
|
if (world != null) {
|
||||||
|
ModdedPregenJob.shutdownForWorld(world.identity());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 source = source("ModdedEngineBootstrap.java");
|
||||||
String stop = method(source, "public static void stop(");
|
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
|
@Test
|
||||||
@@ -160,7 +188,37 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
|
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
|
||||||
|
|
||||||
assertFalse(unbind.contains("finally"));
|
assertFalse(unbind.contains("finally"));
|
||||||
|
assertBefore(unbind, "unloading = true;", "ModdedWorldEngines.evictOrThrow(level);");
|
||||||
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
|
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
|
@Test
|
||||||
|
|||||||
+24
@@ -48,7 +48,28 @@ public class ModdedServiceManagerTest {
|
|||||||
assertNull(manager.service(SecondService.class));
|
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 static final class FirstService implements ModdedService {
|
||||||
|
private RuntimeException disableFailure;
|
||||||
private int enableCount;
|
private int enableCount;
|
||||||
private int disableCount;
|
private int disableCount;
|
||||||
|
|
||||||
@@ -60,6 +81,9 @@ public class ModdedServiceManagerTest {
|
|||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
disableCount++;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-4
@@ -27,6 +27,7 @@ import art.arcane.iris.modded.command.ModdedWandService;
|
|||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.server.packs.PackType;
|
import net.minecraft.server.packs.PackType;
|
||||||
import net.minecraft.world.InteractionResult;
|
import net.minecraft.world.InteractionResult;
|
||||||
import net.minecraft.world.entity.item.ItemEntity;
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
@@ -34,15 +35,18 @@ import net.minecraft.world.entity.player.Player;
|
|||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
import net.neoforged.api.distmarker.Dist;
|
import net.neoforged.api.distmarker.Dist;
|
||||||
|
import net.neoforged.bus.api.EventPriority;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
import net.neoforged.fml.loading.FMLEnvironment;
|
import net.neoforged.fml.loading.FMLEnvironment;
|
||||||
import net.neoforged.fml.loading.FMLLoader;
|
import net.neoforged.fml.loading.FMLLoader;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.common.util.BlockSnapshot;
|
||||||
import net.neoforged.neoforge.event.AddPackFindersEvent;
|
import net.neoforged.neoforge.event.AddPackFindersEvent;
|
||||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||||
import net.neoforged.neoforge.event.level.BlockDropsEvent;
|
import net.neoforged.neoforge.event.level.BlockDropsEvent;
|
||||||
|
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||||
import net.neoforged.neoforge.event.level.LevelEvent;
|
import net.neoforged.neoforge.event.level.LevelEvent;
|
||||||
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerAboutToStartEvent;
|
import net.neoforged.neoforge.event.server.ServerAboutToStartEvent;
|
||||||
@@ -51,6 +55,9 @@ import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
|||||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||||
|
import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mod("irisworldgen")
|
@Mod("irisworldgen")
|
||||||
public final class IrisNeoForgeBootstrap {
|
public final class IrisNeoForgeBootstrap {
|
||||||
@@ -82,18 +89,55 @@ public final class IrisNeoForgeBootstrap {
|
|||||||
ModdedEngineBootstrap.levelLoaded(level);
|
ModdedEngineBootstrap.levelLoaded(level);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
NeoForge.EVENT_BUS.addListener((LevelEvent.Unload event) -> {
|
||||||
NeoForge.EVENT_BUS.addListener((BreakBlockEvent event) -> {
|
if (event.getLevel() instanceof ServerLevel level) {
|
||||||
if (event.getLevel() instanceof ServerLevel level && !event.isCanceled()) {
|
ModdedEngineBootstrap.levelUnloaded(level);
|
||||||
ModdedBlockBreakHandler.prepare(level, event.getPos(), event.getState());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||||
|
NeoForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) ->
|
||||||
|
event.addNodes(NeoForgeModdedLoader.TREE_FELLER_PERMISSION));
|
||||||
|
NeoForge.EVENT_BUS.addListener((BreakBlockEvent event) -> {
|
||||||
|
if (event.getLevel() instanceof ServerLevel level
|
||||||
|
&& event.getPlayer() instanceof ServerPlayer player
|
||||||
|
&& !event.isCanceled()) {
|
||||||
|
ModdedBlockBreakHandler.prepare(level, player, event.getPos(), event.getState());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
NeoForge.EVENT_BUS.addListener(
|
||||||
|
EventPriority.LOWEST,
|
||||||
|
false,
|
||||||
|
(BlockEvent.EntityPlaceEvent event) -> {
|
||||||
|
if (!(event instanceof BlockEvent.EntityMultiPlaceEvent)
|
||||||
|
&& event.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedBlockBreakHandler.clearPlacedProvenance(level, event.getPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
NeoForge.EVENT_BUS.addListener(
|
||||||
|
EventPriority.LOWEST,
|
||||||
|
false,
|
||||||
|
(BlockEvent.EntityMultiPlaceEvent event) -> {
|
||||||
|
for (BlockSnapshot snapshot : event.getReplacedBlockSnapshots()) {
|
||||||
|
if (snapshot.getLevel() instanceof ServerLevel level) {
|
||||||
|
ModdedBlockBreakHandler.clearPlacedProvenance(level, snapshot.getPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
NeoForge.EVENT_BUS.addListener((BlockDropsEvent event) -> {
|
NeoForge.EVENT_BUS.addListener((BlockDropsEvent event) -> {
|
||||||
if (!(event.getBreaker() instanceof Player)) {
|
if (!(event.getBreaker() instanceof Player)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ServerLevel level = event.getLevel();
|
ServerLevel level = event.getLevel();
|
||||||
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.complete(level, event.getPos(), event.getState());
|
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.complete(level, event.getPos(), event.getState());
|
||||||
|
List<ItemStack> vanillaDrops = event.getDrops().stream()
|
||||||
|
.map(ItemEntity::getItem)
|
||||||
|
.toList();
|
||||||
|
if (result.routeCombinedDrops(vanillaDrops)) {
|
||||||
|
event.getDrops().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result.replaceVanillaDrops()) {
|
if (result.replaceVanillaDrops()) {
|
||||||
event.getDrops().clear();
|
event.getDrops().clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,18 +19,37 @@
|
|||||||
package art.arcane.iris.neoforge;
|
package art.arcane.iris.neoforge;
|
||||||
|
|
||||||
import art.arcane.iris.modded.ModdedLoader;
|
import art.arcane.iris.modded.ModdedLoader;
|
||||||
|
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
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 net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
import net.neoforged.fml.loading.FMLEnvironment;
|
import net.neoforged.fml.loading.FMLEnvironment;
|
||||||
import net.neoforged.fml.loading.FMLLoader;
|
import net.neoforged.fml.loading.FMLLoader;
|
||||||
import net.neoforged.fml.loading.FMLPaths;
|
import net.neoforged.fml.loading.FMLPaths;
|
||||||
import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
||||||
|
import net.neoforged.neoforge.server.permission.PermissionAPI;
|
||||||
|
import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
|
||||||
|
import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
|
||||||
import net.neoforged.neoforgespi.language.IModFileInfo;
|
import net.neoforged.neoforgespi.language.IModFileInfo;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public final class NeoForgeModdedLoader implements ModdedLoader {
|
public final class NeoForgeModdedLoader implements ModdedLoader {
|
||||||
|
public static final PermissionNode<Boolean> TREE_FELLER_PERMISSION = new PermissionNode<>(
|
||||||
|
"iris",
|
||||||
|
"treefeller",
|
||||||
|
PermissionTypes.BOOLEAN,
|
||||||
|
(player, playerId, contexts) ->
|
||||||
|
player != null && Commands.LEVEL_GAMEMASTERS.check(player.permissions())
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String platformName() {
|
public String platformName() {
|
||||||
return "neoforge";
|
return "neoforge";
|
||||||
@@ -73,4 +92,23 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
|
|||||||
IModFileInfo info = ModList.get().getModFileById("irisworldgen");
|
IModFileInfo info = ModList.get().getModFileById("irisworldgen");
|
||||||
return info == null ? null : info.getFile().getFilePath().toFile();
|
return info == null ? null : info.getFile().getFilePath().toFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTreeFellerPermission(ServerPlayer player) {
|
||||||
|
return PermissionAPI.getPermission(player, TREE_FELLER_PERMISSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canTreeFellerBreak(
|
||||||
|
ServerLevel level,
|
||||||
|
ServerPlayer player,
|
||||||
|
BlockPos position,
|
||||||
|
BlockState state
|
||||||
|
) {
|
||||||
|
return ModdedTreeFellerService.runBreakProbe(() -> {
|
||||||
|
BreakBlockEvent event = new BreakBlockEvent(level, position, state, player);
|
||||||
|
NeoForge.EVENT_BUS.post(event);
|
||||||
|
return !event.isCanceled();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ public class IrisSettings {
|
|||||||
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
||||||
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
||||||
private IrisSettingsSentry sentry = new IrisSettingsSentry();
|
private IrisSettingsSentry sentry = new IrisSettingsSentry();
|
||||||
|
private IrisSettingsTreeFeller treeFeller = new IrisSettingsTreeFeller();
|
||||||
|
|
||||||
public static int getThreadCount(int c) {
|
public static int getThreadCount(int c) {
|
||||||
return Math.max(switch (c) {
|
return Math.max(switch (c) {
|
||||||
@@ -229,16 +230,8 @@ public class IrisSettings {
|
|||||||
public int noiseCacheSize = 1_024;
|
public int noiseCacheSize = 1_024;
|
||||||
public int resourceLoaderCacheSize = 1_024;
|
public int resourceLoaderCacheSize = 1_024;
|
||||||
public int objectLoaderCacheSize = 4_096;
|
public int objectLoaderCacheSize = 4_096;
|
||||||
public int tectonicPlateSize = -1;
|
|
||||||
public int mantleCleanupDelay = 200;
|
public int mantleCleanupDelay = 200;
|
||||||
public boolean simdKernels = true;
|
public boolean simdKernels = true;
|
||||||
|
|
||||||
public int getTectonicPlateSize() {
|
|
||||||
if (tectonicPlateSize > 0)
|
|
||||||
return tectonicPlateSize;
|
|
||||||
|
|
||||||
return (int) (getHardware.getProcessMemory() / 512L);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -286,6 +279,16 @@ public class IrisSettings {
|
|||||||
public boolean preventLeafDecay = true;
|
public boolean preventLeafDecay = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IrisSettingsTreeFeller {
|
||||||
|
public boolean enabled = false;
|
||||||
|
public int durabilityPreservationChance = 0;
|
||||||
|
|
||||||
|
public int getDurabilityPreservationChance() {
|
||||||
|
return Math.max(0, Math.min(durabilityPreservationChance, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class IrisSettingsStudio {
|
public static class IrisSettingsStudio {
|
||||||
public boolean studio = true;
|
public boolean studio = true;
|
||||||
@@ -300,9 +303,22 @@ public class IrisSettings {
|
|||||||
public boolean useVirtualThreads = true;
|
public boolean useVirtualThreads = true;
|
||||||
public boolean forceMulticoreWrite = false;
|
public boolean forceMulticoreWrite = false;
|
||||||
public int priority = Thread.NORM_PRIORITY;
|
public int priority = Thread.NORM_PRIORITY;
|
||||||
|
public int parallelism = -1;
|
||||||
|
|
||||||
public int getPriority() {
|
public int getPriority() {
|
||||||
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
|
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getParallelism() {
|
||||||
|
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||||
|
if (parallelism > 0) {
|
||||||
|
int maximumParallelism = processors > Integer.MAX_VALUE / 2
|
||||||
|
? Integer.MAX_VALUE
|
||||||
|
: processors * 2;
|
||||||
|
return Math.min(parallelism, maximumParallelism);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(1, (int) Math.ceil(Math.sqrt(processors)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||||
|
private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 15_000L;
|
||||||
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
|
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
|
||||||
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
|
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
|
||||||
private static final Color COLOR_MANTLE = parseColor("#3c2773");
|
private static final Color COLOR_MANTLE = parseColor("#3c2773");
|
||||||
@@ -130,18 +131,36 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean shutdownInstanceForWorld(String worldIdentity) {
|
||||||
|
PregeneratorJob inst = instance.get();
|
||||||
|
if (inst == null || !inst.targetsWorldIdentity(worldIdentity)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean shutdownAndWait(long timeoutMs) {
|
public static boolean shutdownAndWait(long timeoutMs) {
|
||||||
PregeneratorJob inst = instance.get();
|
PregeneratorJob inst = instance.get();
|
||||||
if (inst == null) {
|
if (inst == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return shutdownAndWait(inst, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs) {
|
||||||
inst.pregenerator.close();
|
inst.pregenerator.close();
|
||||||
inst.worker.interrupt();
|
inst.worker.interrupt();
|
||||||
try {
|
try {
|
||||||
inst.worker.join(timeoutMs);
|
inst.worker.join(Math.max(1L, timeoutMs));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while stopping the Iris pregenerator.", e);
|
||||||
|
}
|
||||||
|
if (inst.worker.isAlive()) {
|
||||||
|
throw new IllegalStateException("Timed out while stopping the Iris pregenerator after "
|
||||||
|
+ Math.max(1L, timeoutMs) + "ms.");
|
||||||
}
|
}
|
||||||
instance.compareAndSet(inst, null);
|
instance.compareAndSet(inst, null);
|
||||||
return true;
|
return true;
|
||||||
@@ -184,7 +203,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
|
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName) {
|
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName, String worldIdentity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PregenProgress progressSnapshot() {
|
public static PregenProgress progressSnapshot() {
|
||||||
@@ -205,7 +224,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
inst.lastMethod,
|
inst.lastMethod,
|
||||||
inst.paused(),
|
inst.paused(),
|
||||||
inst.pregenerator.getFailedChunks(),
|
inst.pregenerator.getFailedChunks(),
|
||||||
inst.worldName());
|
inst.worldName(),
|
||||||
|
inst.worldIdentity());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String worldName() {
|
public String worldName() {
|
||||||
@@ -216,6 +236,13 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return engine.getWorld().name();
|
return engine.getWorld().name();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String worldIdentity() {
|
||||||
|
if (engine == null || engine.getWorld() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return engine.getWorld().identity();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean targetsWorldIdentity(String worldIdentity) {
|
public boolean targetsWorldIdentity(String worldIdentity) {
|
||||||
if (worldIdentity == null || engine == null || engine.getWorld() == null) {
|
if (worldIdentity == null || engine == null || engine.getWorld() == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
|
|||||||
import art.arcane.iris.util.common.reflect.KeyedType;
|
import art.arcane.iris.util.common.reflect.KeyedType;
|
||||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -73,15 +77,20 @@ import java.io.FileReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||||
private static final KMap<File, IrisData> dataLoaders = new KMap<>();
|
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
||||||
private final File dataFolder;
|
private final File dataFolder;
|
||||||
private final int id;
|
private final int id;
|
||||||
private final boolean datapackCompiler;
|
private final boolean datapackCompiler;
|
||||||
@@ -108,14 +117,15 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
private Gson snippetLoader;
|
private Gson snippetLoader;
|
||||||
private GsonBuilder builder;
|
private GsonBuilder builder;
|
||||||
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||||
private Engine engine;
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private final transient List<Engine> engines = new ArrayList<>();
|
||||||
|
|
||||||
private IrisData(File dataFolder) {
|
private IrisData(File dataFolder) {
|
||||||
this(dataFolder, false);
|
this(dataFolder, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisData(File dataFolder, boolean datapackCompiler) {
|
private IrisData(File dataFolder, boolean datapackCompiler) {
|
||||||
this.engine = null;
|
|
||||||
this.dataFolder = dataFolder;
|
this.dataFolder = dataFolder;
|
||||||
this.id = RNG.r.imax();
|
this.id = RNG.r.imax();
|
||||||
this.datapackCompiler = datapackCompiler;
|
this.datapackCompiler = datapackCompiler;
|
||||||
@@ -130,6 +140,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
return dataLoaders.computeIfAbsent(dataFolder, IrisData::new);
|
return dataLoaders.computeIfAbsent(dataFolder, IrisData::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IrisData openRuntime(File dataFolder) {
|
||||||
|
return new IrisData(dataFolder);
|
||||||
|
}
|
||||||
|
|
||||||
public static IrisData openDatapackCompiler(File dataFolder) {
|
public static IrisData openDatapackCompiler(File dataFolder) {
|
||||||
return new IrisData(dataFolder, true);
|
return new IrisData(dataFolder, true);
|
||||||
}
|
}
|
||||||
@@ -275,9 +289,72 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void cleanupEngine() {
|
public void cleanupEngine() {
|
||||||
if (engine != null && engine.isClosed()) {
|
int removed;
|
||||||
engine = null;
|
synchronized (engines) {
|
||||||
IrisLogging.debug("Dereferenced Data<Engine> " + getId() + " " + getDataFolder());
|
int previousSize = engines.size();
|
||||||
|
removeClosedEngines();
|
||||||
|
removed = previousSize - engines.size();
|
||||||
|
}
|
||||||
|
if (removed > 0) {
|
||||||
|
IrisLogging.debug("Dereferenced " + removed + " Data<Engine> registration(s) " + getId() + " " + getDataFolder());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Engine getEngine() {
|
||||||
|
IrisContext context = IrisContext.get();
|
||||||
|
if (context != null) {
|
||||||
|
Engine contextEngine = context.getEngine();
|
||||||
|
if (!contextEngine.isClosed() && contextEngine.getData() == this) {
|
||||||
|
return contextEngine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (engines) {
|
||||||
|
removeClosedEngines();
|
||||||
|
return engines.size() == 1 ? engines.get(0) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Engine> getEngines() {
|
||||||
|
synchronized (engines) {
|
||||||
|
removeClosedEngines();
|
||||||
|
return List.copyOf(engines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerEngine(Engine engine) {
|
||||||
|
Objects.requireNonNull(engine, "engine");
|
||||||
|
synchronized (engines) {
|
||||||
|
for (Engine registeredEngine : engines) {
|
||||||
|
if (registeredEngine == engine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engines.add(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterEngine(Engine engine) {
|
||||||
|
if (engine == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (engines) {
|
||||||
|
Iterator<Engine> iterator = engines.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
if (iterator.next() == engine) {
|
||||||
|
iterator.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeClosedEngines() {
|
||||||
|
Iterator<Engine> iterator = engines.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
if (iterator.next().isClosed()) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +364,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
public void close() {
|
public void close() {
|
||||||
closed = true;
|
closed = true;
|
||||||
dump();
|
dump();
|
||||||
|
synchronized (engines) {
|
||||||
|
engines.clear();
|
||||||
|
}
|
||||||
if (dataLoaders.get(dataFolder) == this) {
|
if (dataLoaders.get(dataFolder) == this) {
|
||||||
dataLoaders.remove(dataFolder);
|
dataLoaders.remove(dataFolder);
|
||||||
}
|
}
|
||||||
@@ -365,7 +445,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
|
|
||||||
gson = builder.create();
|
gson = builder.create();
|
||||||
|
|
||||||
if (engine != null) {
|
for (Engine engine : getEngines()) {
|
||||||
engine.hotload();
|
engine.hotload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +485,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
invalidateLoader(structureLoader);
|
invalidateLoader(structureLoader);
|
||||||
invalidateLoader(jigsawPoolLoader);
|
invalidateLoader(jigsawPoolLoader);
|
||||||
invalidateLoader(jigsawPieceLoader);
|
invalidateLoader(jigsawPieceLoader);
|
||||||
if (engine != null) {
|
for (Engine engine : getEngines()) {
|
||||||
IrisStructureLocator.invalidate(engine);
|
IrisStructureLocator.invalidate(engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ public final class MantleHeapPressure {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static double reclaimUrgency(double fraction) {
|
||||||
|
if (!Double.isFinite(fraction) || fraction <= LOW_WATER) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
if (fraction >= HIGH_WATER) {
|
||||||
|
return 1D;
|
||||||
|
}
|
||||||
|
return (fraction - LOW_WATER) / (HIGH_WATER - LOW_WATER);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean overPanicWater() {
|
public static boolean overPanicWater() {
|
||||||
return usedFraction() >= PANIC_WATER;
|
return usedFraction() >= PANIC_WATER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package art.arcane.iris.core.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.engine.framework.Engine;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public final class EngineMaintenance {
|
||||||
|
private EngineMaintenance() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAvailable(Engine engine) {
|
||||||
|
return engine != null
|
||||||
|
&& !engine.isClosing()
|
||||||
|
&& !engine.isClosed()
|
||||||
|
&& !engine.getMantle().getMantle().isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean pregeneratorTargets(Engine engine) {
|
||||||
|
if (engine == null || engine.getWorld() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||||
|
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldRun(Engine engine) {
|
||||||
|
if (engine.isStudio()
|
||||||
|
&& !IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (GoldenHashEngine.isActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.getWorld() == null
|
||||||
|
|| !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int workerParallelism() {
|
||||||
|
return IrisSettings.get().getPerformance().getEngineSVC().getParallelism();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Outcome run(Engine engine) {
|
||||||
|
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
|
||||||
|
double heapUsage = MantleHeapPressure.usedFraction();
|
||||||
|
Plan plan = plan(settings.getMantleKeepAlive(), heapUsage, settings.getEngineSVC().isForceMulticoreWrite());
|
||||||
|
|
||||||
|
engine.getMantle().trim(plan.idleDurationMillis());
|
||||||
|
|
||||||
|
long unloadStart = System.nanoTime();
|
||||||
|
int unloadedTectonicPlates = engine.getMantle().unloadTectonicPlate(
|
||||||
|
plan.multicoreUnload() ? 0 : Integer.MAX_VALUE);
|
||||||
|
if (plan.heapPressure() && MantleHeapPressure.overPanicWater()) {
|
||||||
|
MantleHeapPressure.requestPanicReclaim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Outcome(
|
||||||
|
unloadedTectonicPlates,
|
||||||
|
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - unloadStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Plan plan(int mantleKeepAliveSeconds, double heapUsage, boolean forceMulticoreWrite) {
|
||||||
|
long baseIdleDurationMillis = TimeUnit.SECONDS.toMillis(Math.max(0, mantleKeepAliveSeconds));
|
||||||
|
double reclaimUrgency = MantleHeapPressure.reclaimUrgency(heapUsage);
|
||||||
|
long idleDurationMillis = Math.round(baseIdleDurationMillis * (1D - reclaimUrgency));
|
||||||
|
boolean heapPressure = reclaimUrgency >= 1D;
|
||||||
|
return new Plan(idleDurationMillis, heapPressure || forceMulticoreWrite, heapPressure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Outcome(int unloadedTectonicPlates, long unloadDurationMillis) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Plan(long idleDurationMillis, boolean multicoreUnload, boolean heapPressure) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisServices;
|
|||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -59,6 +60,7 @@ import org.bukkit.event.world.StructureGrowEvent;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class TreeSVC implements IrisService {
|
public class TreeSVC implements IrisService {
|
||||||
@@ -138,6 +140,7 @@ public class TreeSVC implements IrisService {
|
|||||||
|
|
||||||
saplingPlane.forEach(block -> block.setType(Material.AIR));
|
saplingPlane.forEach(block -> block.setType(Material.AIR));
|
||||||
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r));
|
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r));
|
||||||
|
String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
|
||||||
List<BlockState> blockStateList = new KList<>();
|
List<BlockState> blockStateList = new KList<>();
|
||||||
KMap<Location, BlockData> dataCache = new KMap<>();
|
KMap<Location, BlockData> dataCache = new KMap<>();
|
||||||
// TODO: REAL CLASSES!!!!
|
// TODO: REAL CLASSES!!!!
|
||||||
@@ -234,7 +237,7 @@ public class TreeSVC implements IrisService {
|
|||||||
|
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
|
||||||
J.s(() -> {
|
Runnable growTask = () -> {
|
||||||
|
|
||||||
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
|
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
|
||||||
block = true;
|
block = true;
|
||||||
@@ -253,9 +256,20 @@ public class TreeSVC implements IrisService {
|
|||||||
block.setBlockData(data.getBase(), false);
|
block.setBlockData(data.getBase(), false);
|
||||||
IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom());
|
IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom());
|
||||||
} else block.setBlockData(d, false);
|
} else block.setBlockData(d, false);
|
||||||
|
int mantleY = block.getY() - event.getWorld().getMinHeight();
|
||||||
|
engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker);
|
||||||
|
engine.getMantle().getMantle().set(
|
||||||
|
block.getX(),
|
||||||
|
mantleY,
|
||||||
|
block.getZ(),
|
||||||
|
TreeBlockMaterial.of(block.getBlockData().getAsString())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (!J.runAt(event.getLocation(), growTask) && !J.isFolia()) {
|
||||||
|
J.s(growTask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.core.service.tree;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface BlockDropRouter {
|
||||||
|
boolean routeDrop(Object drop);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package art.arcane.iris.core.service.tree;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
|
import art.arcane.iris.engine.object.IrisProceduralObjects;
|
||||||
|
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||||
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class TreeDefinitionIndex {
|
||||||
|
private static final String PROCEDURAL_TREE_PREFIX = "procedural/tree/";
|
||||||
|
|
||||||
|
private final Set<String> explicitObjectKeys;
|
||||||
|
private final Set<String> proceduralTreeKeys;
|
||||||
|
|
||||||
|
private TreeDefinitionIndex(Set<String> explicitObjectKeys, Set<String> proceduralTreeKeys) {
|
||||||
|
this.explicitObjectKeys = Set.copyOf(explicitObjectKeys);
|
||||||
|
this.proceduralTreeKeys = Set.copyOf(proceduralTreeKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeDefinitionIndex build(Engine engine) {
|
||||||
|
Set<String> explicitObjectKeys = new HashSet<>();
|
||||||
|
Set<String> proceduralTreeKeys = new HashSet<>();
|
||||||
|
|
||||||
|
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
|
||||||
|
if (region == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectPlacements(region.getObjects(), explicitObjectKeys);
|
||||||
|
collectProceduralTrees(region.getProceduralObjects(), proceduralTreeKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) {
|
||||||
|
if (biome == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectPlacements(biome.getObjects(), explicitObjectKeys);
|
||||||
|
collectProceduralTrees(biome.getProceduralObjects(), proceduralTreeKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TreeDefinitionIndex(explicitObjectKeys, proceduralTreeKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTreeMarker(String marker) {
|
||||||
|
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
|
||||||
|
if (decoded == null || decoded.structureAware()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String objectKey = decoded.objectKey();
|
||||||
|
if (objectKey.startsWith(PROCEDURAL_TREE_PREFIX)) {
|
||||||
|
return proceduralTreeKeys.contains(objectKey);
|
||||||
|
}
|
||||||
|
if (objectKey.startsWith("procedural/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return objectKey.startsWith("trees/") || explicitObjectKeys.contains(objectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectPlacements(Iterable<IrisObjectPlacement> placements, Set<String> objectKeys) {
|
||||||
|
if (placements == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IrisObjectPlacement placement : placements) {
|
||||||
|
if (placement == null
|
||||||
|
|| placement.getTrees() == null
|
||||||
|
|| placement.getTrees().isEmpty()
|
||||||
|
|| placement.getPlace() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String objectKey : placement.getPlace()) {
|
||||||
|
if (objectKey != null && !objectKey.isBlank()) {
|
||||||
|
objectKeys.add(objectKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectProceduralTrees(IrisProceduralObjects proceduralObjects, Set<String> objectKeys) {
|
||||||
|
if (proceduralObjects == null || proceduralObjects.getTrees() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IrisProceduralTree tree : proceduralObjects.getTrees()) {
|
||||||
|
if (tree == null || tree.getName() == null || tree.getName().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int variants = Math.max(1, tree.getVariants());
|
||||||
|
for (int index = 0; index < variants; index++) {
|
||||||
|
objectKeys.add(tree.getVariantLoadKey(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package art.arcane.iris.core.service.tree;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class TreeMarkerTraversal {
|
||||||
|
public static final int MAX_MEMBERS = 131_072;
|
||||||
|
public static final int MAX_VISITED = 1_000_000;
|
||||||
|
public static final int MAX_AXIS_DISTANCE = 256;
|
||||||
|
|
||||||
|
private TreeMarkerTraversal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Discovery discover(Position trigger, String marker, int minimumY, int maximumY, MarkerLookup lookup) {
|
||||||
|
ArrayDeque<Position> pending = new ArrayDeque<>();
|
||||||
|
Set<Position> visited = new HashSet<>();
|
||||||
|
List<Position> members = new ArrayList<>();
|
||||||
|
pending.add(trigger);
|
||||||
|
visited.add(trigger);
|
||||||
|
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
Position current = pending.removeFirst();
|
||||||
|
if (!marker.equals(lookup.markerAt(current.x(), current.y(), current.z()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (members.size() >= MAX_MEMBERS) {
|
||||||
|
return new Discovery(List.copyOf(members), false);
|
||||||
|
}
|
||||||
|
members.add(current);
|
||||||
|
|
||||||
|
for (int xOffset = -1; xOffset <= 1; xOffset++) {
|
||||||
|
for (int yOffset = -1; yOffset <= 1; yOffset++) {
|
||||||
|
for (int zOffset = -1; zOffset <= 1; zOffset++) {
|
||||||
|
if (xOffset == 0 && yOffset == 0 && zOffset == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Position next = current.offset(xOffset, yOffset, zOffset);
|
||||||
|
if (next.y() < minimumY || next.y() >= maximumY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!withinAxisBounds(trigger, next)) {
|
||||||
|
if (marker.equals(lookup.markerAt(next.x(), next.y(), next.z()))) {
|
||||||
|
return new Discovery(List.copyOf(members), false);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!visited.add(next)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (visited.size() > MAX_VISITED) {
|
||||||
|
return new Discovery(List.copyOf(members), false);
|
||||||
|
}
|
||||||
|
pending.addLast(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Discovery(List.copyOf(members), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean withinAxisBounds(Position trigger, Position position) {
|
||||||
|
return Math.abs(position.x() - trigger.x()) <= MAX_AXIS_DISTANCE
|
||||||
|
&& Math.abs(position.y() - trigger.y()) <= MAX_AXIS_DISTANCE
|
||||||
|
&& Math.abs(position.z() - trigger.z()) <= MAX_AXIS_DISTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface MarkerLookup {
|
||||||
|
String markerAt(int x, int y, int z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Position(int x, int y, int z) {
|
||||||
|
public Position offset(int xOffset, int yOffset, int zOffset) {
|
||||||
|
return new Position(x + xOffset, y + yOffset, z + zOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Discovery(List<Position> members, boolean complete) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,30 +19,87 @@
|
|||||||
package art.arcane.iris.engine;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
|
||||||
|
|
||||||
public class EnginePanic {
|
import java.util.Collections;
|
||||||
private static final KMap<String, String> stuff = new KMap<>();
|
import java.util.LinkedHashMap;
|
||||||
private static KMap<String, String> last = new KMap<>();
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class EnginePanic {
|
||||||
|
private static final Diagnostics GLOBAL = scoped("global");
|
||||||
|
|
||||||
|
private EnginePanic() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Diagnostics scoped(String scope) {
|
||||||
|
return new Diagnostics(scope);
|
||||||
|
}
|
||||||
|
|
||||||
public static void add(String key, String value) {
|
public static void add(String key, String value) {
|
||||||
stuff.put(key, value);
|
GLOBAL.add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveLast() {
|
public static void saveLast() {
|
||||||
last = stuff.copy();
|
GLOBAL.saveLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lastPanic() {
|
public static void lastPanic() {
|
||||||
for (String i : last.keySet()) {
|
GLOBAL.lastPanic();
|
||||||
IrisLogging.error("Last Panic " + i + ": " + stuff.get(i));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void panic() {
|
public static void panic() {
|
||||||
lastPanic();
|
GLOBAL.panic();
|
||||||
for (String i : stuff.keySet()) {
|
}
|
||||||
IrisLogging.error("Engine Panic " + i + ": " + stuff.get(i));
|
|
||||||
|
public static final class Diagnostics {
|
||||||
|
private final String scope;
|
||||||
|
private final ThreadLocal<LinkedHashMap<String, String>> current = ThreadLocal.withInitial(LinkedHashMap::new);
|
||||||
|
private final AtomicReference<Map<String, String>> last = new AtomicReference<>(Map.of());
|
||||||
|
|
||||||
|
private Diagnostics(String scope) {
|
||||||
|
this.scope = scope == null || scope.isBlank() ? "unknown" : scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(String key, String value) {
|
||||||
|
current.get().put(String.valueOf(key), String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveLast() {
|
||||||
|
last.set(currentSnapshot());
|
||||||
|
current.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void lastPanic() {
|
||||||
|
log("Last Panic", last.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void panic() {
|
||||||
|
Map<String, String> currentSnapshot = currentSnapshot();
|
||||||
|
lastPanic();
|
||||||
|
log("Engine Panic", currentSnapshot);
|
||||||
|
current.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> currentSnapshot() {
|
||||||
|
return immutableSnapshot(current.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> lastSnapshot() {
|
||||||
|
return last.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(String prefix, Map<String, String> snapshot) {
|
||||||
|
for (Map.Entry<String, String> entry : snapshot.entrySet()) {
|
||||||
|
IrisLogging.error(prefix + " [" + scope + "] " + entry.getKey() + ": " + entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> immutableSnapshot(Map<String, String> values) {
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableMap(new LinkedHashMap<>(values));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ public final class GenerationCacheWarmer {
|
|||||||
KList<IrisBiome> biomes = engine.getAllBiomes();
|
KList<IrisBiome> biomes = engine.getAllBiomes();
|
||||||
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
||||||
for (IrisBiome biome : biomes) {
|
for (IrisBiome biome : biomes) {
|
||||||
warmPlacements(biome.getObjects(), root, counter, data);
|
warmPlacements(biome.getObjects(), root, counter, data, engine);
|
||||||
warmDecorators(biome.getDecorators(), root, counter, data);
|
warmDecorators(biome.getDecorators(), root, counter, data);
|
||||||
warmOres(biome.getOres(), root, counter, data);
|
warmOres(biome.getOres(), root, counter, data);
|
||||||
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
||||||
@@ -57,7 +57,7 @@ public final class GenerationCacheWarmer {
|
|||||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||||
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
|
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
|
||||||
for (IrisRegion region : regions) {
|
for (IrisRegion region : regions) {
|
||||||
warmPlacements(region.getObjects(), root, counter, data);
|
warmPlacements(region.getObjects(), root, counter, data, engine);
|
||||||
warmOres(region.getOres(), root, counter, data);
|
warmOres(region.getOres(), root, counter, data);
|
||||||
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,8 @@ public final class GenerationCacheWarmer {
|
|||||||
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
|
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter, IrisData data) {
|
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter,
|
||||||
|
IrisData data, Engine engine) {
|
||||||
if (placements == null) {
|
if (placements == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
RNG rng = root.nextParallelRNG(counter[0]++);
|
RNG rng = root.nextParallelRNG(counter[0]++);
|
||||||
placement.getSurfaceWarp(rng, data);
|
placement.getSurfaceWarp(rng, data, engine);
|
||||||
placement.getDensity(rng, 0, 0, data);
|
placement.getDensity(rng, 0, 0, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
|
|
||||||
public IrisComplex(Engine engine, boolean simple) {
|
public IrisComplex(Engine engine, boolean simple) {
|
||||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||||
IrisBiome emptyBiome = new IrisBiome();
|
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
||||||
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
||||||
this.rng = new RNG(engine.getSeedManager().getComplex());
|
this.rng = new RNG(engine.getSeedManager().getComplex());
|
||||||
this.data = engine.getData();
|
this.data = engine.getData();
|
||||||
@@ -124,19 +124,23 @@ public class IrisComplex implements DataProvider {
|
|||||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||||
|
|
||||||
if (focusBiome != null) {
|
if (focusBiome != null) {
|
||||||
focusBiome.setInferredType(InferredType.LAND);
|
focusBiome = focusBiome.withInferredType(InferredType.LAND);
|
||||||
focusRegion = findRegion(focusBiome, engine);
|
focusRegion = findRegion(focusBiome, engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
//@builder
|
//@builder
|
||||||
if (focusRegion != null) {
|
if (focusRegion != null) {
|
||||||
|
prepareInferredBiomes(focusRegion);
|
||||||
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
|
||||||
} else {
|
} else {
|
||||||
engine.getDimension()
|
engine.getDimension().getRegions().forEach(regionKey -> {
|
||||||
.getRegions()
|
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||||
.forEach(i -> data.getRegionLoader().load(i)
|
if (region == null) {
|
||||||
.getAllBiomes(this)
|
return;
|
||||||
.forEach(this::registerGenerators));
|
}
|
||||||
|
prepareInferredBiomes(region);
|
||||||
|
region.getAllBiomes(this).forEach(this::registerGenerators);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
generatorBounds = buildGeneratorBounds(engine);
|
generatorBounds = buildGeneratorBounds(engine);
|
||||||
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
|
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
|
||||||
@@ -171,7 +175,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(r.getCaveBiomeZoom())
|
.zoom(r.getCaveBiomeZoom())
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes()))
|
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
|
||||||
.onNull(emptyBiome)
|
.onNull(emptyBiome)
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
|
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
|
||||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||||
@@ -181,7 +185,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(engine.getDimension().getLandZoom())
|
.zoom(engine.getDimension().getLandZoom())
|
||||||
.zoom(r.getLandBiomeZoom())
|
.zoom(r.getLandBiomeZoom())
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)))
|
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
|
||||||
).convertAware2D(ProceduralStream::get)
|
).convertAware2D(ProceduralStream::get)
|
||||||
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
|
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
|
||||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||||
@@ -191,7 +195,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(engine.getDimension().getSeaZoom())
|
.zoom(engine.getDimension().getSeaZoom())
|
||||||
.zoom(r.getSeaBiomeZoom())
|
.zoom(r.getSeaBiomeZoom())
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)))
|
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
|
||||||
).convertAware2D(ProceduralStream::get)
|
).convertAware2D(ProceduralStream::get)
|
||||||
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
|
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
|
||||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||||
@@ -200,7 +204,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(r.getShoreBiomeZoom())
|
.zoom(r.getShoreBiomeZoom())
|
||||||
.selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)))
|
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
|
||||||
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
|
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
|
||||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||||
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
|
||||||
@@ -312,22 +316,49 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
||||||
|
IrisBiome resolved = resolveSurfaceBiome(
|
||||||
|
height,
|
||||||
|
biome,
|
||||||
|
region,
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
fluidHeight,
|
||||||
|
landBiomeStream,
|
||||||
|
seaBiomeStream,
|
||||||
|
shoreBiomeStream);
|
||||||
|
return resolved == biome ? biome : implode(resolved, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IrisBiome resolveSurfaceBiome(
|
||||||
|
double height,
|
||||||
|
IrisBiome biome,
|
||||||
|
IrisRegion region,
|
||||||
|
double x,
|
||||||
|
double z,
|
||||||
|
double fluidHeight,
|
||||||
|
ProceduralStream<IrisBiome> landBiomes,
|
||||||
|
ProceduralStream<IrisBiome> seaBiomes,
|
||||||
|
ProceduralStream<IrisBiome> shoreBiomes
|
||||||
|
) {
|
||||||
|
if (biome == null || region == null) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
double sh = region.getShoreHeight(x, z);
|
double sh = region.getShoreHeight(x, z);
|
||||||
|
|
||||||
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
||||||
return shoreBiomeStream.get(x, z);
|
return shoreBiomes.get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height > fluidHeight + sh && !biome.isLand()) {
|
if (height > fluidHeight + sh && !biome.isLand()) {
|
||||||
return landBiomeStream.get(x, z);
|
return landBiomes.get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height < fluidHeight && !biome.isAquatic()) {
|
if (height < fluidHeight && !biome.isAquatic()) {
|
||||||
return seaBiomeStream.get(x, z);
|
return seaBiomes.get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height == fluidHeight && !biome.isShore()) {
|
if (height == fluidHeight && !biome.isShore()) {
|
||||||
return shoreBiomeStream.get(x, z);
|
return shoreBiomes.get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
return biome;
|
return biome;
|
||||||
@@ -446,6 +477,21 @@ public class IrisComplex implements DataProvider {
|
|||||||
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
|
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void prepareInferredBiomes(IrisRegion region) {
|
||||||
|
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
|
||||||
|
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
|
||||||
|
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
|
||||||
|
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
|
||||||
|
KList<IrisBiome> inferred = new KList<>();
|
||||||
|
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||||
|
inferred.add(biome.withInferredType(type));
|
||||||
|
}
|
||||||
|
return inferred;
|
||||||
|
}
|
||||||
|
|
||||||
private void registerGenerators(IrisBiome biome) {
|
private void registerGenerators(IrisBiome biome) {
|
||||||
generatorBiomes.add(biome);
|
generatorBiomes.add(biome);
|
||||||
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
|
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
|
||||||
@@ -532,8 +578,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
|
|
||||||
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
|
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
|
||||||
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
||||||
IrisBiome biome = childSelectionPlan.select(childCell, x, z);
|
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
|
||||||
biome.setInferredType(b.getInferredType());
|
|
||||||
return implode(biome, x, z, max - 1);
|
return implode(biome, x, z, max - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +685,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ChildSelectionPlan {
|
static final class ChildSelectionPlan {
|
||||||
private final IrisBiome[] mappedBiomes;
|
private final IrisBiome[] mappedBiomes;
|
||||||
private final int maxIndex;
|
private final int maxIndex;
|
||||||
|
|
||||||
@@ -649,7 +694,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.maxIndex = mappedBiomes.length - 1;
|
this.maxIndex = mappedBiomes.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChildSelectionPlan create(KList<IrisBiome> options) {
|
static ChildSelectionPlan create(KList<IrisBiome> options) {
|
||||||
if (options.isEmpty()) {
|
if (options.isEmpty()) {
|
||||||
return new ChildSelectionPlan(new IrisBiome[0]);
|
return new ChildSelectionPlan(new IrisBiome[0]);
|
||||||
}
|
}
|
||||||
@@ -690,7 +735,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return new ChildSelectionPlan(mappedBiomes);
|
return new ChildSelectionPlan(mappedBiomes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome select(CNG childCell, double x, double z) {
|
IrisBiome select(CNG childCell, double x, double z) {
|
||||||
if (mappedBiomes.length == 0) {
|
if (mappedBiomes.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -36,19 +36,23 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
|
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
|
||||||
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
|
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
|
||||||
|
private static final long EMPTY_PLAYER_REFRESH_NANOS = 1_000_000_000L;
|
||||||
|
|
||||||
private final ConcurrentHashMap<UUID, EnginePlayer> players;
|
private final ConcurrentHashMap<UUID, EnginePlayer> players;
|
||||||
private final Semaphore limit;
|
private final Semaphore limit;
|
||||||
private final AtomicBoolean playerMapUpdateQueued;
|
private final AtomicBoolean playerMapUpdateQueued;
|
||||||
|
private final AtomicLong nextEmptyRefresh;
|
||||||
|
|
||||||
public IrisEngineEffects(Engine engine) {
|
public IrisEngineEffects(Engine engine) {
|
||||||
super(engine, "FX");
|
super(engine, "FX");
|
||||||
players = new ConcurrentHashMap<>();
|
players = new ConcurrentHashMap<>();
|
||||||
limit = new Semaphore(1);
|
limit = new Semaphore(1);
|
||||||
playerMapUpdateQueued = new AtomicBoolean(false);
|
playerMapUpdateQueued = new AtomicBoolean(false);
|
||||||
|
nextEmptyRefresh = new AtomicLong(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,7 +97,15 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (players.isEmpty() || M.r(0.02)) {
|
if (players.isEmpty()) {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
long next = nextEmptyRefresh.get();
|
||||||
|
if (now >= next && nextEmptyRefresh.compareAndSet(next, now + EMPTY_PLAYER_REFRESH_NANOS)) {
|
||||||
|
updatePlayerMap();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (M.r(0.02)) {
|
||||||
updatePlayerMap();
|
updatePlayerMap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisPlatforms;
|
|||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||||
import art.arcane.iris.engine.EnginePanic;
|
|
||||||
import art.arcane.iris.core.nms.container.Pair;
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -66,7 +65,9 @@ import java.io.IOException;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = "engine")
|
@EqualsAndHashCode(exclude = "engine")
|
||||||
@@ -182,8 +183,8 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
IrisMatterSupport.ensureRegistered();
|
IrisMatterSupport.ensureRegistered();
|
||||||
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
|
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
|
||||||
int worldHeight = engine.getTarget().getHeight();
|
int worldHeight = engine.getTarget().getHeight();
|
||||||
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter(engine.getData());
|
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
|
||||||
MantleHooks hooks = createRuntimeHooks();
|
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
|
||||||
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
|
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
|
||||||
createRegionIO(dataFolder, worldHeight, adapter, hooks);
|
createRegionIO(dataFolder, worldHeight, adapter, hooks);
|
||||||
return new Mantle<>(
|
return new Mantle<>(
|
||||||
@@ -199,14 +200,14 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static MantleDataAdapter<Matter> createRuntimeDataAdapter(IrisData data) {
|
public static MantleDataAdapter<Matter> createRuntimeDataAdapter(IrisData data) {
|
||||||
return createDataAdapter(data);
|
return createDataAdapter(() -> data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MantleHooks createRuntimeHooks() {
|
public static MantleHooks createRuntimeHooks() {
|
||||||
return createHooks();
|
return createHooks(EnginePanic.scoped("runtime mantle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MantleDataAdapter<Matter> createDataAdapter(IrisData data) {
|
private static MantleDataAdapter<Matter> createDataAdapter(Supplier<IrisData> dataSupplier) {
|
||||||
return new MantleDataAdapter<>() {
|
return new MantleDataAdapter<>() {
|
||||||
@Override
|
@Override
|
||||||
public Matter createSection() {
|
public Matter createSection() {
|
||||||
@@ -215,6 +216,7 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
|
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
|
||||||
|
IrisData data = Objects.requireNonNull(dataSupplier.get(), "Iris mantle data is unavailable.");
|
||||||
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
|
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
|
||||||
return Matter.readDin(din);
|
return Matter.readDin(din);
|
||||||
}
|
}
|
||||||
@@ -285,11 +287,11 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MantleHooks createHooks() {
|
private static MantleHooks createHooks(EnginePanic.Diagnostics panic) {
|
||||||
return new MantleHooks() {
|
return new MantleHooks() {
|
||||||
@Override
|
@Override
|
||||||
public void onBeforeReadSection(int index) {
|
public void onBeforeReadSection(int index) {
|
||||||
EnginePanic.add("read.section", "Section[" + index + "]");
|
panic.add("read.section", "Section[" + index + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -299,22 +301,22 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
art.arcane.volmlib.util.io.CountingDataInputStream din,
|
art.arcane.volmlib.util.io.CountingDataInputStream din,
|
||||||
IOException error) {
|
IOException error) {
|
||||||
IrisLogging.error("Failed to read chunk section, skipping it.");
|
IrisLogging.error("Failed to read chunk section, skipping it.");
|
||||||
EnginePanic.add("read.byte.range", start + " " + end);
|
panic.add("read.byte.range", start + " " + end);
|
||||||
EnginePanic.add("read.byte.current", din.count() + "");
|
panic.add("read.byte.current", din.count() + "");
|
||||||
IrisLogging.reportError(error);
|
IrisLogging.reportError(error);
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
EnginePanic.panic();
|
panic.panic();
|
||||||
TectonicPlate.addError();
|
TectonicPlate.addError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBeforeReadChunk(int index) {
|
public void onBeforeReadChunk(int index) {
|
||||||
EnginePanic.add("read-chunk", "Chunk[" + index + "]");
|
panic.add("read-chunk", "Chunk[" + index + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAfterReadChunk(int index) {
|
public void onAfterReadChunk(int index) {
|
||||||
EnginePanic.saveLast();
|
panic.saveLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -324,11 +326,11 @@ public class IrisEngineMantle implements EngineMantle {
|
|||||||
art.arcane.volmlib.util.io.CountingDataInputStream din,
|
art.arcane.volmlib.util.io.CountingDataInputStream din,
|
||||||
Throwable error) {
|
Throwable error) {
|
||||||
IrisLogging.error("Failed to read chunk, creating a new chunk instead.");
|
IrisLogging.error("Failed to read chunk, creating a new chunk instead.");
|
||||||
EnginePanic.add("read.byte.range", start + " " + end);
|
panic.add("read.byte.range", start + " " + end);
|
||||||
EnginePanic.add("read.byte.current", din.count() + "");
|
panic.add("read.byte.current", din.count() + "");
|
||||||
IrisLogging.reportError(error);
|
IrisLogging.reportError(error);
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
EnginePanic.panic();
|
panic.panic();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
|||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.service.tree.BlockDropRouter;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -82,9 +83,12 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@@ -95,7 +99,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
|
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
|
||||||
|
|
||||||
private final Looper looper;
|
private final Looper looper;
|
||||||
private final int id;
|
|
||||||
private final KList<Runnable> updateQueue = new KList<>();
|
private final KList<Runnable> updateQueue = new KList<>();
|
||||||
private final ChronoLatch cl;
|
private final ChronoLatch cl;
|
||||||
private final ChronoLatch clw;
|
private final ChronoLatch clw;
|
||||||
@@ -111,10 +114,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
|
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
|
||||||
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
|
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
|
||||||
private int entityCount = 0;
|
private final AtomicBoolean entityCountWarningReported = new AtomicBoolean();
|
||||||
private int actuallySpawned = 0;
|
private final AtomicBoolean entityCountErrorReported = new AtomicBoolean();
|
||||||
|
private boolean looperStopped;
|
||||||
|
private boolean cleanupServiceStopped;
|
||||||
|
private volatile int entityCount = 0;
|
||||||
|
private final AtomicInteger actuallySpawned = new AtomicInteger();
|
||||||
private int cooldown = 0;
|
private int cooldown = 0;
|
||||||
private int forcedChunkUpdateCursor = 0;
|
private int forcedChunkUpdateCursor = 0;
|
||||||
|
private volatile boolean entityCountValid = false;
|
||||||
private volatile boolean playersPresent = false;
|
private volatile boolean playersPresent = false;
|
||||||
private KSet<Position2> injectBiomes = new KSet<>();
|
private KSet<Position2> injectBiomes = new KSet<>();
|
||||||
private volatile int loadedChunkCount = 0;
|
private volatile int loadedChunkCount = 0;
|
||||||
@@ -128,7 +136,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
chunkUpdater = null;
|
chunkUpdater = null;
|
||||||
chunkDiscovery = null;
|
chunkDiscovery = null;
|
||||||
cleanupService = null;
|
cleanupService = null;
|
||||||
id = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisWorldManager(Engine engine) {
|
public IrisWorldManager(Engine engine) {
|
||||||
@@ -143,55 +150,82 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
thread.setPriority(Thread.MIN_PRIORITY);
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
return thread;
|
return thread;
|
||||||
});
|
});
|
||||||
id = engine.getCacheID();
|
|
||||||
looper = new Looper() {
|
looper = new Looper() {
|
||||||
@Override
|
@Override
|
||||||
protected long loop() {
|
protected long loop() {
|
||||||
if (getEngine().isClosed() || getEngine().getCacheID() != id) {
|
if (!isManagerStarted()) {
|
||||||
interrupt();
|
return -1L;
|
||||||
}
|
}
|
||||||
|
return callManagerTask(
|
||||||
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
|
"bukkit_world_manager_loop",
|
||||||
J.runGlobal(() -> BukkitWorldBinding.tryBind(getEngine().getWorld()));
|
IrisWorldManager.this::runLoop,
|
||||||
}
|
250L);
|
||||||
|
|
||||||
if (getEngine().getWorld().hasPlatformWorld()) {
|
|
||||||
if (chunkUpdater.flip()) {
|
|
||||||
updateChunks();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!playersPresent) {
|
|
||||||
return 5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunkDiscovery.flip()) {
|
|
||||||
discoverChunks();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cln.flip()) {
|
|
||||||
engine.getEngineData().cleanup(getEngine());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
|
|
||||||
return 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
onAsyncTick();
|
|
||||||
}
|
|
||||||
|
|
||||||
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
looper.setPriority(Thread.MIN_PRIORITY);
|
looper.setPriority(Thread.MIN_PRIORITY);
|
||||||
looper.setName("Iris World Manager " + getTarget().getWorld().name());
|
looper.setName("Iris World Manager " + getTarget().getWorld().name());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startManager() {
|
private long runLoop() {
|
||||||
|
if (getEngine().isClosed()) {
|
||||||
|
looper.interrupt();
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
|
||||||
|
J.runGlobal(() -> runManagerTask(
|
||||||
|
"bukkit_world_manager_bind",
|
||||||
|
() -> BukkitWorldBinding.tryBind(getEngine().getWorld())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getEngine().getWorld().hasPlatformWorld()) {
|
||||||
|
if (chunkUpdater.flip()) {
|
||||||
|
updateChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playersPresent) {
|
||||||
|
return 5000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkDiscovery.flip()) {
|
||||||
|
discoverChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cln.flip()) {
|
||||||
|
getEngine().getEngineData().cleanup(getEngine());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()
|
||||||
|
&& !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
|
||||||
|
return 3000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAsyncTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
super.start();
|
||||||
if (!looper.isAlive()) {
|
if (!looper.isAlive()) {
|
||||||
looper.start();
|
looper.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Runnable managedTask(String operation, Runnable task) {
|
||||||
|
return () -> runManagerTask(operation, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable managedTask(String operation, Runnable task, Runnable unavailable) {
|
||||||
|
return () -> {
|
||||||
|
if (!runManagerTask(operation, task)) {
|
||||||
|
unavailable.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void discoverChunks() {
|
private void discoverChunks() {
|
||||||
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
World world = BukkitWorldBinding.world(getEngine().getWorld());
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
@@ -206,7 +240,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean scheduled = J.runGlobal(() -> {
|
boolean scheduled = J.runGlobal(managedTask("bukkit_world_manager_discover_chunks", () -> {
|
||||||
try {
|
try {
|
||||||
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
|
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
|
||||||
return;
|
return;
|
||||||
@@ -217,7 +251,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runEntity(player, () -> {
|
J.runEntity(player, managedTask("bukkit_world_manager_discover_player", () -> {
|
||||||
if (!player.isOnline() || !world.equals(player.getWorld())) {
|
if (!player.isOnline() || !world.equals(player.getWorld())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -232,14 +266,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
|
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
} finally {
|
} finally {
|
||||||
chunkDiscoveryScanScheduled.set(false);
|
chunkDiscoveryScanScheduled.set(false);
|
||||||
}
|
}
|
||||||
});
|
}, () -> chunkDiscoveryScanScheduled.set(false)));
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
chunkDiscoveryScanScheduled.set(false);
|
chunkDiscoveryScanScheduled.set(false);
|
||||||
}
|
}
|
||||||
@@ -260,7 +294,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_discovered_flag", () -> {
|
||||||
try {
|
try {
|
||||||
Mantle<Matter> mantle = getMantle();
|
Mantle<Matter> mantle = getMantle();
|
||||||
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) {
|
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) {
|
||||||
@@ -271,7 +305,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
} finally {
|
} finally {
|
||||||
discoveredFlagQueue.remove(key);
|
discoveredFlagQueue.remove(key);
|
||||||
}
|
}
|
||||||
});
|
}, () -> discoveredFlagQueue.remove(key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChunks() {
|
private void updateChunks() {
|
||||||
@@ -288,7 +322,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean scheduled = J.runGlobal(() -> updateChunksOnGlobal(world));
|
boolean scheduled = J.runGlobal(managedTask(
|
||||||
|
"bukkit_world_manager_update_chunks",
|
||||||
|
() -> updateChunksOnGlobal(world),
|
||||||
|
() -> chunkUpdateScanScheduled.set(false)));
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
chunkUpdateScanScheduled.set(false);
|
chunkUpdateScanScheduled.set(false);
|
||||||
}
|
}
|
||||||
@@ -308,7 +345,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runEntity(player, () -> schedulePlayerChunkUpdates(world, player));
|
J.runEntity(player, managedTask(
|
||||||
|
"bukkit_world_manager_player_chunk_updates",
|
||||||
|
() -> schedulePlayerChunkUpdates(world, player)));
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleForcedChunkUpdates(world);
|
scheduleForcedChunkUpdates(world);
|
||||||
@@ -363,13 +402,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_chunk_update", () -> {
|
||||||
try {
|
try {
|
||||||
updateChunkRegion(world, chunkX, chunkZ);
|
updateChunkRegion(world, chunkX, chunkZ);
|
||||||
} finally {
|
} finally {
|
||||||
chunkUpdateQueue.remove(key);
|
chunkUpdateQueue.remove(key);
|
||||||
}
|
}
|
||||||
});
|
}, () -> chunkUpdateQueue.remove(key)));
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
chunkUpdateQueue.remove(key);
|
chunkUpdateQueue.remove(key);
|
||||||
}
|
}
|
||||||
@@ -409,12 +448,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
|
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
|
||||||
int delay = RNG.r.i(5, 200);
|
int delay = RNG.r.i(5, 200);
|
||||||
J.runRegion(world, chunkX, chunkZ, () -> {
|
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
spawnIn(world.getChunkAt(chunkX, chunkZ), true);
|
spawnIn(world.getChunkAt(chunkX, chunkZ), true);
|
||||||
}, delay);
|
}), delay);
|
||||||
|
|
||||||
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
|
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
|
||||||
forEachMarkerSpawner(markerChunk, (block, spawners) -> {
|
forEachMarkerSpawner(markerChunk, (block, spawners) -> {
|
||||||
@@ -442,7 +481,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
|
||||||
boolean raised = false;
|
boolean raised = false;
|
||||||
try {
|
try {
|
||||||
Mantle<Matter> mantle = getMantle();
|
Mantle<Matter> mantle = getMantle();
|
||||||
@@ -460,13 +499,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runRegion(world, chunkX, chunkZ, () -> {
|
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onFirstRaise.run();
|
onFirstRaise.run();
|
||||||
});
|
}));
|
||||||
});
|
}, () -> markerFlagQueue.remove(key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warmupMantleChunkAsync(int chunkX, int chunkZ) {
|
private void warmupMantleChunkAsync(int chunkX, int chunkZ) {
|
||||||
@@ -475,7 +514,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_mantle_warmup", () -> {
|
||||||
try {
|
try {
|
||||||
getMantle().getChunk(chunkX, chunkZ);
|
getMantle().getChunk(chunkX, chunkZ);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -483,11 +522,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
} finally {
|
} finally {
|
||||||
mantleWarmupQueue.remove(key);
|
mantleWarmupQueue.remove(key);
|
||||||
}
|
}
|
||||||
});
|
}, () -> mantleWarmupQueue.remove(key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onAsyncTick() {
|
private boolean onAsyncTick() {
|
||||||
if (getEngine().isClosed()) {
|
if (getEngine().isClosing() || getEngine().isClosed()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +535,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
actuallySpawned = 0;
|
actuallySpawned.set(0);
|
||||||
|
|
||||||
if (!getEngine().getWorld().hasPlatformWorld()) {
|
if (!getEngine().getWorld().hasPlatformWorld()) {
|
||||||
IrisLogging.debug("Can't spawn. No real world");
|
IrisLogging.debug("Can't spawn. No real world");
|
||||||
@@ -509,8 +548,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
World realWorld = BukkitWorldBinding.world(getEngine().getWorld());
|
World realWorld = BukkitWorldBinding.world(getEngine().getWorld());
|
||||||
if (realWorld == null) {
|
if (realWorld == null) {
|
||||||
entityCount = 0;
|
entityCount = 0;
|
||||||
|
entityCountValid = false;
|
||||||
} else if (J.isFolia()) {
|
} else if (J.isFolia()) {
|
||||||
entityCount = getFoliaLivingEntityCount(realWorld);
|
Integer count = getFoliaLivingEntityCount(realWorld);
|
||||||
|
if (count != null) {
|
||||||
|
entityCount = count;
|
||||||
|
entityCountValid = true;
|
||||||
|
resetEntityCountFailures();
|
||||||
|
} else {
|
||||||
|
entityCountValid = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||||
boolean scheduled = J.runGlobal(() -> {
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
@@ -526,14 +573,32 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
future.completeExceptionally(ex);
|
future.completeExceptionally(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
entityCount = scheduled ? future.get(2, TimeUnit.SECONDS) : 0;
|
if (scheduled) {
|
||||||
|
entityCount = future.get(2, TimeUnit.SECONDS);
|
||||||
|
entityCountValid = true;
|
||||||
|
resetEntityCountFailures();
|
||||||
|
} else {
|
||||||
|
reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
entityCountValid = false;
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||||
|
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!entityCountValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
double epx = getEntitySaturation();
|
double epx = getEntitySaturation();
|
||||||
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
||||||
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
|
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
|
||||||
@@ -549,16 +614,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
Position2[] cc = getLoadedChunkPositionsSnapshot(world);
|
Position2[] cc = getLoadedChunkPositionsSnapshot(world);
|
||||||
while (spawnBuffer-- > 0) {
|
while (spawnBuffer-- > 0) {
|
||||||
|
if (getEngine().isClosing() || getEngine().isClosed()) {
|
||||||
|
return actuallySpawned.get() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (cc.length == 0) {
|
if (cc.length == 0) {
|
||||||
IrisLogging.debug("Can't spawn. No chunks!");
|
IrisLogging.debug("Can't spawn. No chunks!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Position2 c = cc[RNG.r.nextInt(cc.length)];
|
Position2 c = cc[RNG.r.nextInt(cc.length)];
|
||||||
spawnChunkSafely(world, c.getX(), c.getZ(), false);
|
if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) {
|
||||||
|
return actuallySpawned.get() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return actuallySpawned > 0;
|
return actuallySpawned.get() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPregenActiveForThisWorld() {
|
private boolean isPregenActiveForThisWorld() {
|
||||||
@@ -604,13 +675,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return future.get(2, TimeUnit.SECONDS);
|
return future.get(2, TimeUnit.SECONDS);
|
||||||
} catch (Throwable e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return new Position2[0];
|
||||||
|
} catch (ExecutionException | TimeoutException e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
return new Position2[0];
|
return new Position2[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getFoliaLivingEntityCount(World world) {
|
private Integer getFoliaLivingEntityCount(World world) {
|
||||||
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
|
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
|
||||||
boolean scheduled = J.runGlobal(() -> {
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -620,18 +694,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
return 0;
|
reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Player> players;
|
List<Player> players;
|
||||||
try {
|
try {
|
||||||
players = playerFuture.get(2, TimeUnit.SECONDS);
|
players = playerFuture.get(2, TimeUnit.SECONDS);
|
||||||
} catch (Throwable e) {
|
} catch (InterruptedException e) {
|
||||||
IrisLogging.reportError(e);
|
entityCountValid = false;
|
||||||
return 0;
|
Thread.currentThread().interrupt();
|
||||||
|
return null;
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
|
||||||
|
return null;
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||||
|
reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Entity> candidates = new ConcurrentHashMap<>();
|
Map<String, Entity> candidates = new ConcurrentHashMap<>();
|
||||||
|
AtomicBoolean incomplete = new AtomicBoolean();
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
|
||||||
CountDownLatch latch = new CountDownLatch(players.size());
|
CountDownLatch latch = new CountDownLatch(players.size());
|
||||||
for (Player player : players) {
|
for (Player player : players) {
|
||||||
@@ -651,21 +736,28 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
candidates.put(nearby.getUniqueId().toString(), nearby);
|
candidates.put(nearby.getUniqueId().toString(), nearby);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
incomplete.set(true);
|
||||||
|
failure.compareAndSet(null, e);
|
||||||
} finally {
|
} finally {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
})) {
|
})) {
|
||||||
|
incomplete.set(true);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) {
|
||||||
latch.await(2, TimeUnit.SECONDS);
|
if (!Thread.currentThread().isInterrupted()) {
|
||||||
} catch (InterruptedException e) {
|
reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
|
||||||
Thread.currentThread().interrupt();
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicInteger count = new AtomicInteger();
|
AtomicInteger count = new AtomicInteger();
|
||||||
|
incomplete.set(false);
|
||||||
|
failure.set(null);
|
||||||
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
|
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
|
||||||
for (Entity entity : candidates.values()) {
|
for (Entity entity : candidates.values()) {
|
||||||
if (!J.runEntity(entity, () -> {
|
if (!J.runEntity(entity, () -> {
|
||||||
@@ -673,47 +765,117 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
|
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
|
||||||
count.incrementAndGet();
|
count.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
incomplete.set(true);
|
||||||
|
failure.compareAndSet(null, e);
|
||||||
} finally {
|
} finally {
|
||||||
entityLatch.countDown();
|
entityLatch.countDown();
|
||||||
}
|
}
|
||||||
})) {
|
})) {
|
||||||
|
incomplete.set(true);
|
||||||
entityLatch.countDown();
|
entityLatch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) {
|
||||||
entityLatch.await(2, TimeUnit.SECONDS);
|
if (!Thread.currentThread().isInterrupted()) {
|
||||||
} catch (InterruptedException e) {
|
reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
|
||||||
Thread.currentThread().interrupt();
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count.get();
|
return count.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
|
static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) {
|
||||||
|
try {
|
||||||
|
return latch.await(timeout, unit);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
J.runRegion(world, chunkX, chunkZ, () -> {
|
AtomicBoolean failureReported = new AtomicBoolean();
|
||||||
try {
|
future.whenComplete((ignored, failure) -> {
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
if (failure != null) {
|
||||||
return;
|
reportSpawnFailure(chunkX, chunkZ, failure, failureReported);
|
||||||
}
|
|
||||||
|
|
||||||
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
|
|
||||||
} finally {
|
|
||||||
future.complete(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
boolean scheduled;
|
||||||
|
try {
|
||||||
|
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||||
|
future.complete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
|
||||||
|
future.complete(null);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scheduled) {
|
||||||
|
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
future.get(5, TimeUnit.SECONDS);
|
future.get(5, TimeUnit.SECONDS);
|
||||||
} catch (Throwable e) {
|
return true;
|
||||||
IrisLogging.reportError(e);
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ);
|
||||||
|
return false;
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||||
|
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reportEntityCountFailure(String message, Throwable error) {
|
||||||
|
entityCountValid = false;
|
||||||
|
if (error != null) {
|
||||||
|
if (entityCountErrorReported.compareAndSet(false, true)) {
|
||||||
|
IrisLogging.reportError(message, error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityCountWarningReported.compareAndSet(false, true)) {
|
||||||
|
IrisLogging.warn(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetEntityCountFailures() {
|
||||||
|
entityCountWarningReported.set(false);
|
||||||
|
entityCountErrorReported.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) {
|
||||||
|
if (!failureReported.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
|
||||||
|
IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause);
|
||||||
|
}
|
||||||
|
|
||||||
private void spawnIn(Chunk c, boolean initial) {
|
private void spawnIn(Chunk c, boolean initial) {
|
||||||
if (getEngine().isClosed()) {
|
if (getEngine().isClosed()) {
|
||||||
return;
|
return;
|
||||||
@@ -736,8 +898,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawn(block, s, false);
|
spawn(block, s, false);
|
||||||
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
|
J.runRegion(c.getWorld(), c.getX(), c.getZ(), managedTask(
|
||||||
() -> spawn(block, s, true)));
|
"bukkit_world_manager_marker_spawn_followup",
|
||||||
|
() -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
|
||||||
|
() -> spawn(block, s, true))));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,17 +939,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
if (v == null || v.getReferenceSpawner() == null)
|
if (v == null || v.getReferenceSpawner() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
spawn(c, v);
|
||||||
spawn(c, v);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> spawn(c, v));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawn(Chunk c, IrisEntitySpawn i) {
|
private void spawn(Chunk c, IrisEntitySpawn i) {
|
||||||
IrisSpawner ref = i.getReferenceSpawner();
|
IrisSpawner ref = i.getReferenceSpawner();
|
||||||
int s = i.spawn(getEngine(), c, RNG.r);
|
int s = i.spawn(getEngine(), c, RNG.r);
|
||||||
actuallySpawned += s;
|
actuallySpawned.addAndGet(s);
|
||||||
if (s > 0) {
|
if (s > 0) {
|
||||||
ref.spawn(getEngine(), c.getX(), c.getZ());
|
ref.spawn(getEngine(), c.getX(), c.getZ());
|
||||||
}
|
}
|
||||||
@@ -797,7 +957,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
int s = i.spawn(getEngine(), pos, RNG.r);
|
int s = i.spawn(getEngine(), pos, RNG.r);
|
||||||
actuallySpawned += s;
|
actuallySpawned.addAndGet(s);
|
||||||
if (s > 0) {
|
if (s > 0) {
|
||||||
ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
|
ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
|
||||||
}
|
}
|
||||||
@@ -857,10 +1017,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
int cX = e.getX(), cZ = e.getZ();
|
int cX = e.getX(), cZ = e.getZ();
|
||||||
Long key = Cache.key(cX, cZ);
|
Long key = Cache.key(cX, cZ);
|
||||||
cleanup.put(key, cleanupService.schedule(() -> {
|
cleanup.put(key, cleanupService.schedule(managedTask("bukkit_world_manager_chunk_cleanup", () -> {
|
||||||
cleanup.remove(key);
|
cleanup.remove(key);
|
||||||
getEngine().cleanupMantleChunk(cX, cZ);
|
getEngine().cleanupMantleChunk(cX, cZ);
|
||||||
}, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
|
}, () -> cleanup.remove(key)), Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -898,15 +1058,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
@Override
|
@Override
|
||||||
public void teleportAsync(PlayerTeleportEvent e) {
|
public void teleportAsync(PlayerTeleportEvent e) {
|
||||||
e.setCancelled(true);
|
e.setCancelled(true);
|
||||||
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), () -> {
|
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), managedTask(
|
||||||
|
"bukkit_world_manager_teleport",
|
||||||
|
() -> {
|
||||||
ignoreTP.set(true);
|
ignoreTP.set(true);
|
||||||
e.getPlayer().teleport(e.getTo(), e.getCause());
|
e.getPlayer().teleport(e.getTo(), e.getCause());
|
||||||
ignoreTP.set(false);
|
ignoreTP.set(false);
|
||||||
}));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warmupAreaAsync(Player player, Location to, Runnable r) {
|
private void warmupAreaAsync(Player player, Location to, Runnable r) {
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_teleport_warmup", () -> {
|
||||||
int viewDistance = 2;
|
int viewDistance = 2;
|
||||||
KList<Future<Chunk>> futures = new KList<>();
|
KList<Future<Chunk>> futures = new KList<>();
|
||||||
for (int i = -viewDistance; i <= viewDistance; i++) {
|
for (int i = -viewDistance; i <= viewDistance; i++) {
|
||||||
@@ -945,7 +1107,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return "Loading Chunks";
|
return "Loading Chunks";
|
||||||
}
|
}
|
||||||
}.queue(futures).execute(new VolmitSender(player), true, r);
|
}.queue(futures).execute(new VolmitSender(player), true, r);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
|
public Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
|
||||||
@@ -1024,14 +1186,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_marker_scan", () -> {
|
||||||
try {
|
try {
|
||||||
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
|
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
|
||||||
if (markerData.isEmpty()) {
|
if (markerData.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runRegion(world, chunkX, chunkZ, () -> {
|
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_marker_scan_region", () -> {
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1050,13 +1212,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
|
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
} finally {
|
} finally {
|
||||||
markerScanQueue.remove(key);
|
markerScanQueue.remove(key);
|
||||||
}
|
}
|
||||||
});
|
}, () -> markerScanQueue.remove(key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
|
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
|
||||||
@@ -1107,13 +1269,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void removeMarkerAsync(IrisPosition marker) {
|
private void removeMarkerAsync(IrisPosition marker) {
|
||||||
J.a(() -> {
|
J.a(managedTask("bukkit_world_manager_remove_marker", () -> {
|
||||||
try {
|
try {
|
||||||
getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class);
|
getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MarkerSpawnData {
|
private static final class MarkerSpawnData {
|
||||||
@@ -1127,21 +1289,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
int blockX = e.getBlock().getX();
|
int blockX = e.getBlock().getX();
|
||||||
int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight());
|
int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight());
|
||||||
int blockZ = e.getBlock().getZ();
|
int blockZ = e.getBlock().getZ();
|
||||||
J.a(() -> {
|
|
||||||
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
|
|
||||||
|
|
||||||
if (marker != null) {
|
|
||||||
if (marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
|
|
||||||
|
|
||||||
if (mark == null || mark.isRemoveOnChange()) {
|
|
||||||
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
KList<ItemStack> d = new KList<>();
|
KList<ItemStack> d = new KList<>();
|
||||||
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation());
|
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation());
|
||||||
@@ -1159,15 +1306,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
e.setDropItems(false);
|
e.setDropItems(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d.isNotEmpty()) {
|
World w = e.getBlock().getWorld();
|
||||||
World w = e.getBlock().getWorld();
|
Location blockLocation = e.getBlock().getLocation();
|
||||||
Location dropLocation = e.getBlock().getLocation().clone().add(.5, .5, .5);
|
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
|
||||||
Runnable dropTask = () -> d.forEach(item -> w.dropItemNaturally(dropLocation, item));
|
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
|
||||||
if (!J.runAt(dropLocation, dropTask)) {
|
Runnable finalizedBreak = managedTask("bukkit_world_manager_block_break_finalize", () -> {
|
||||||
if (!J.isFolia()) {
|
if (e.isCancelled()) {
|
||||||
J.s(dropTask);
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
J.a(managedTask("bukkit_world_manager_block_break_marker", () -> {
|
||||||
|
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
|
||||||
|
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
|
||||||
|
if (mark == null || mark.isRemoveOnChange()) {
|
||||||
|
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
|
||||||
|
});
|
||||||
|
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
|
||||||
|
J.s(finalizedBreak, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1176,6 +1337,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return worldY - minHeight;
|
return worldY - minHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static <T> void routeDrops(Iterable<T> drops, BlockDropRouter router, Consumer<T> fallback) {
|
||||||
|
for (T drop : drops) {
|
||||||
|
boolean routed = false;
|
||||||
|
if (router != null) {
|
||||||
|
try {
|
||||||
|
routed = router.routeDrop(drop);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
IrisLogging.reportError("Failed to route a deferred Iris block drop.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!routed) {
|
||||||
|
fallback.accept(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int toWorldY(int mantleY, int minHeight) {
|
static int toWorldY(int mantleY, int minHeight) {
|
||||||
return mantleY + minHeight;
|
return mantleY + minHeight;
|
||||||
}
|
}
|
||||||
@@ -1190,11 +1367,35 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public synchronized void close() {
|
||||||
super.close();
|
Throwable failure = null;
|
||||||
looper.interrupt();
|
try {
|
||||||
if (cleanupService != null) {
|
super.close();
|
||||||
cleanupService.shutdownNow();
|
} catch (Throwable e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
if (!looperStopped) {
|
||||||
|
try {
|
||||||
|
if (looper != null) {
|
||||||
|
looper.interrupt();
|
||||||
|
}
|
||||||
|
looperStopped = true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure = appendCloseFailure(failure, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cleanupServiceStopped) {
|
||||||
|
try {
|
||||||
|
if (cleanupService != null) {
|
||||||
|
cleanupService.shutdownNow();
|
||||||
|
}
|
||||||
|
cleanupServiceStopped = true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure = appendCloseFailure(failure, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw new IllegalStateException("Failed to completely stop the Bukkit Iris world manager.", failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1212,6 +1413,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return (double) entityCount / (loadedChunkCount + 1) * 1.28;
|
return (double) entityCount / (loadedChunkCount + 1) * 1.28;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Throwable appendCloseFailure(Throwable failure, Throwable next) {
|
||||||
|
if (failure == null) {
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
if (failure != next) {
|
||||||
|
failure.addSuppressed(next);
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
private static class ChunkCounter implements Predicate<IrisSpawner> {
|
private static class ChunkCounter implements Predicate<IrisSpawner> {
|
||||||
private final Entity[] entities;
|
private final Entity[] entities;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
engine.getData(),
|
engine.getData(),
|
||||||
chunkHeight,
|
chunkHeight,
|
||||||
complex.getHeightStream(),
|
complex.getHeightStream(),
|
||||||
complex.getBaseBiomeStream(),
|
complex.getTrueBiomeStream(),
|
||||||
complex.getRegionStream(),
|
complex.getRegionStream(),
|
||||||
complex.getRockStream(),
|
complex.getRockStream(),
|
||||||
true
|
true
|
||||||
@@ -89,6 +89,8 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
|
|
||||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||||
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
|
||||||
|
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||||
upperDim.getRegions().forEach(regionKey -> {
|
upperDim.getRegions().forEach(regionKey -> {
|
||||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||||
if (region != null) {
|
if (region != null) {
|
||||||
@@ -135,8 +137,7 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
.zoom(upperDim.getBiomeZoom())
|
.zoom(upperDim.getBiomeZoom())
|
||||||
.zoom(upperDim.getLandZoom())
|
.zoom(upperDim.getLandZoom())
|
||||||
.zoom(r.getLandBiomeZoom())
|
.zoom(r.getLandBiomeZoom())
|
||||||
.selectRarity(upperData.getBiomeLoader().loadAll(r.getLandBiomes(),
|
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
|
||||||
t -> t.setInferredType(InferredType.LAND))))
|
|
||||||
.convertAware2D(ProceduralStream::get);
|
.convertAware2D(ProceduralStream::get);
|
||||||
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||||
.convert(r -> upperDim.getSeaBiomeStyle()
|
.convert(r -> upperDim.getSeaBiomeStyle()
|
||||||
@@ -144,16 +145,14 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
.zoom(upperDim.getBiomeZoom())
|
.zoom(upperDim.getBiomeZoom())
|
||||||
.zoom(upperDim.getSeaZoom())
|
.zoom(upperDim.getSeaZoom())
|
||||||
.zoom(r.getSeaBiomeZoom())
|
.zoom(r.getSeaBiomeZoom())
|
||||||
.selectRarity(upperData.getBiomeLoader().loadAll(r.getSeaBiomes(),
|
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
|
||||||
t -> t.setInferredType(InferredType.SEA))))
|
|
||||||
.convertAware2D(ProceduralStream::get);
|
.convertAware2D(ProceduralStream::get);
|
||||||
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||||
.convert(r -> upperDim.getShoreBiomeStyle()
|
.convert(r -> upperDim.getShoreBiomeStyle()
|
||||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
|
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
|
||||||
.zoom(upperDim.getBiomeZoom())
|
.zoom(upperDim.getBiomeZoom())
|
||||||
.zoom(r.getShoreBiomeZoom())
|
.zoom(r.getShoreBiomeZoom())
|
||||||
.selectRarity(upperData.getBiomeLoader().loadAll(r.getShoreBiomes(),
|
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
|
||||||
t -> t.setInferredType(InferredType.SHORE))))
|
|
||||||
.convertAware2D(ProceduralStream::get);
|
.convertAware2D(ProceduralStream::get);
|
||||||
|
|
||||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||||
@@ -170,7 +169,9 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
.convertAware2D((t, x, z) -> {
|
.convertAware2D((t, x, z) -> {
|
||||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
||||||
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
||||||
});
|
})
|
||||||
|
.convertAware2D((biome, x, z) -> implode(
|
||||||
|
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
|
||||||
|
|
||||||
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
||||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||||
@@ -235,6 +236,23 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
|
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
|
||||||
}, Interpolated.DOUBLE);
|
}, Interpolated.DOUBLE);
|
||||||
|
|
||||||
|
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
|
||||||
|
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||||
|
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
|
||||||
|
height,
|
||||||
|
baseBiome,
|
||||||
|
regionStream.get(x, z),
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
fluidHeight,
|
||||||
|
landBiomeStream,
|
||||||
|
seaBiomeStream,
|
||||||
|
shoreBiomeStream);
|
||||||
|
return resolved == baseBiome
|
||||||
|
? baseBiome
|
||||||
|
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||||
|
});
|
||||||
|
|
||||||
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
|
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
|
||||||
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
||||||
.select(upperDim.getRockPalette().getBlockData(upperData));
|
.select(upperDim.getRockPalette().getBlockData(upperData));
|
||||||
@@ -244,13 +262,67 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
upperData,
|
upperData,
|
||||||
chunkHeight,
|
chunkHeight,
|
||||||
heightStream,
|
heightStream,
|
||||||
baseBiomeStream,
|
finalBiomeStream,
|
||||||
regionStream,
|
regionStream,
|
||||||
rockStream,
|
rockStream,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
|
||||||
|
KList<IrisBiome> inferred = new KList<>();
|
||||||
|
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||||
|
inferred.add(biome.withInferredType(type));
|
||||||
|
}
|
||||||
|
return inferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisBiome implode(
|
||||||
|
IrisBiome biome,
|
||||||
|
double x,
|
||||||
|
double z,
|
||||||
|
RNG rng,
|
||||||
|
DataProvider dataProvider,
|
||||||
|
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
|
||||||
|
int remainingDepth
|
||||||
|
) {
|
||||||
|
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
|
||||||
|
if (selectionPlan == null) {
|
||||||
|
synchronized (childSelectionPlans) {
|
||||||
|
selectionPlan = childSelectionPlans.get(biome);
|
||||||
|
if (selectionPlan == null) {
|
||||||
|
KList<IrisBiome> options = new KList<>();
|
||||||
|
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
|
||||||
|
if (child != null) {
|
||||||
|
options.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.add(biome);
|
||||||
|
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
|
||||||
|
childSelectionPlans.put(biome, selectionPlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiome selected = selectionPlan.select(
|
||||||
|
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
|
||||||
|
if (selected == null) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
return implode(
|
||||||
|
selected.withInferredType(biome.getInferredType()),
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
rng,
|
||||||
|
dataProvider,
|
||||||
|
childSelectionPlans,
|
||||||
|
remainingDepth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
public int getUpperSurfaceY(int x, int z) {
|
public int getUpperSurfaceY(int x, int z) {
|
||||||
double rawHeight = heightStream.get((double) x, (double) z);
|
double rawHeight = heightStream.get((double) x, (double) z);
|
||||||
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import art.arcane.iris.spi.PlatformBlockState;
|
|||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.iris.util.common.data.DataProvider;
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
import art.arcane.iris.util.common.data.B;
|
import art.arcane.iris.util.common.data.B;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||||
@@ -127,6 +128,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return GenerationSessionLease.noop();
|
return GenerationSessionLease.noop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IrisContext context = IrisContext.get();
|
||||||
|
if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) {
|
||||||
|
return generationSessions.continueSession(operation, context.getGenerationSessionId());
|
||||||
|
}
|
||||||
|
if (isClosing() || isClosed()) {
|
||||||
|
throw new GenerationSessionException("Generation session rejected new work for " + operation
|
||||||
|
+ " while the Iris engine is closing.", isClosed());
|
||||||
|
}
|
||||||
|
|
||||||
return generationSessions.acquire(operation);
|
return generationSessions.acquire(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+147
-35
@@ -33,82 +33,194 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
|||||||
import org.bukkit.event.world.ChunkLoadEvent;
|
import org.bukkit.event.world.ChunkLoadEvent;
|
||||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||||
import org.bukkit.event.world.WorldSaveEvent;
|
import org.bukkit.event.world.WorldSaveEvent;
|
||||||
import org.bukkit.event.world.WorldUnloadEvent;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener {
|
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener {
|
||||||
private final int taskId;
|
private final Object managerLifecycleLock;
|
||||||
|
private final AtomicBoolean started;
|
||||||
|
private boolean listenerRegistered;
|
||||||
|
private boolean closeRequested;
|
||||||
|
private int taskId;
|
||||||
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
|
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
|
||||||
|
|
||||||
public EngineAssignedWorldManager() {
|
public EngineAssignedWorldManager() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
|
managerLifecycleLock = new Object();
|
||||||
|
started = new AtomicBoolean(false);
|
||||||
taskId = -1;
|
taskId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EngineAssignedWorldManager(Engine engine) {
|
public EngineAssignedWorldManager(Engine engine) {
|
||||||
super(engine, "World");
|
super(engine, "World");
|
||||||
BukkitPlatform.volmitPlugin().registerListener(this);
|
managerLifecycleLock = new Object();
|
||||||
taskId = J.sr(this::onTick, 1);
|
started = new AtomicBoolean(false);
|
||||||
|
taskId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
synchronized (managerLifecycleLock) {
|
||||||
|
if (started.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (closeRequested) {
|
||||||
|
throw new IllegalStateException("Cannot restart a closed Iris world manager.");
|
||||||
|
}
|
||||||
|
|
||||||
|
started.set(true);
|
||||||
|
try {
|
||||||
|
listenerRegistered = true;
|
||||||
|
registerManagerListener();
|
||||||
|
taskId = scheduleManagerTick(() -> runManagerTask("bukkit_world_manager_tick", this::onTick));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
started.set(false);
|
||||||
|
closeRequested = true;
|
||||||
|
Throwable cleanupFailure = closeManagerResources();
|
||||||
|
if (cleanupFailure != null) {
|
||||||
|
e.addSuppressed(cleanupFailure);
|
||||||
|
}
|
||||||
|
throw propagate(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(IrisEngineHotloadEvent e) {
|
public void on(IrisEngineHotloadEvent e) {
|
||||||
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
|
runManagerTask("bukkit_world_manager_hotload_event", () -> {
|
||||||
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
|
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
|
||||||
VolmitSender s = new VolmitSender(i);
|
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
|
||||||
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
|
VolmitSender s = new VolmitSender(i);
|
||||||
}
|
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(WorldSaveEvent e) {
|
public void on(WorldSaveEvent e) {
|
||||||
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
runManagerTask("bukkit_world_manager_save", () -> {
|
||||||
getEngine().save();
|
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||||
}
|
getEngine().save();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
@EventHandler
|
|
||||||
public void on(WorldUnloadEvent e) {
|
|
||||||
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
|
||||||
getEngine().close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(BlockBreakEvent e) {
|
public void on(BlockBreakEvent e) {
|
||||||
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
runManagerTask("bukkit_world_manager_block_break", () -> {
|
||||||
onBlockBreak(e);
|
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||||
}
|
onBlockBreak(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(BlockPlaceEvent e) {
|
public void on(BlockPlaceEvent e) {
|
||||||
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
runManagerTask("bukkit_world_manager_block_place", () -> {
|
||||||
onBlockPlace(e);
|
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||||
}
|
onBlockPlace(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(ChunkLoadEvent e) {
|
public void on(ChunkLoadEvent e) {
|
||||||
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
runManagerTask("bukkit_world_manager_chunk_load", () -> {
|
||||||
onChunkLoad(e.getChunk(), e.isNewChunk());
|
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||||
}
|
onChunkLoad(e.getChunk(), e.isNewChunk());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(ChunkUnloadEvent e) {
|
public void on(ChunkUnloadEvent e) {
|
||||||
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
runManagerTask("bukkit_world_manager_chunk_unload", () -> {
|
||||||
onChunkUnload(e.getChunk());
|
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
|
||||||
}
|
onChunkUnload(e.getChunk());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
super.close();
|
Throwable failure;
|
||||||
BukkitPlatform.volmitPlugin().unregisterListener(this);
|
synchronized (managerLifecycleLock) {
|
||||||
if (taskId != -1) {
|
closeRequested = true;
|
||||||
J.csr(taskId);
|
started.set(false);
|
||||||
|
failure = closeManagerResources();
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw new IllegalStateException("Failed to completely stop the Iris world manager.", failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void registerManagerListener() {
|
||||||
|
BukkitPlatform.volmitPlugin().registerListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int scheduleManagerTick(Runnable tick) {
|
||||||
|
return J.sr(tick, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void unregisterManagerListener() {
|
||||||
|
BukkitPlatform.volmitPlugin().unregisterListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void cancelManagerTick(int scheduledTaskId) {
|
||||||
|
J.csr(scheduledTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Throwable closeManagerResources() {
|
||||||
|
Throwable failure = null;
|
||||||
|
if (listenerRegistered) {
|
||||||
|
try {
|
||||||
|
unregisterManagerListener();
|
||||||
|
listenerRegistered = false;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (taskId != -1) {
|
||||||
|
try {
|
||||||
|
cancelManagerTick(taskId);
|
||||||
|
taskId = -1;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (failure == null) {
|
||||||
|
failure = e;
|
||||||
|
} else if (failure != e) {
|
||||||
|
failure.addSuppressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeException propagate(Throwable failure) {
|
||||||
|
if (failure instanceof RuntimeException runtimeException) {
|
||||||
|
return runtimeException;
|
||||||
|
}
|
||||||
|
if (failure instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean runManagerTask(String operation, Runnable task) {
|
||||||
|
if (!started.get()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return EngineLifecycleTasks.run(getEngine(), operation, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> T callManagerTask(String operation, Supplier<T> task, T unavailable) {
|
||||||
|
if (!started.get()) {
|
||||||
|
return unavailable;
|
||||||
|
}
|
||||||
|
return EngineLifecycleTasks.call(getEngine(), operation, task, unavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isManagerStarted() {
|
||||||
|
return started.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class EngineLifecycleTasks {
|
||||||
|
private EngineLifecycleTasks() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean run(Engine engine, String operation, Runnable task) {
|
||||||
|
Objects.requireNonNull(engine);
|
||||||
|
Objects.requireNonNull(operation);
|
||||||
|
Objects.requireNonNull(task);
|
||||||
|
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
task.run();
|
||||||
|
return true;
|
||||||
|
} catch (GenerationSessionException exception) {
|
||||||
|
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
|
||||||
|
Objects.requireNonNull(task);
|
||||||
|
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
return task.get();
|
||||||
|
} catch (GenerationSessionException exception) {
|
||||||
|
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
||||||
|
return unavailable;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,9 @@ import art.arcane.volmlib.util.atomics.AtomicRollingSequence;
|
|||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class EngineMetrics {
|
public class EngineMetrics {
|
||||||
private final AtomicRollingSequence total;
|
private final AtomicRollingSequence total;
|
||||||
@@ -111,4 +114,24 @@ public class EngineMetrics {
|
|||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Double> telemetryAverages() {
|
||||||
|
Map<String, Double> averages = new LinkedHashMap<>();
|
||||||
|
averages.put("total", total.getAverage());
|
||||||
|
averages.put("updates", updates.getAverage());
|
||||||
|
averages.put("terrain", terrain.getAverage());
|
||||||
|
averages.put("biome", biome.getAverage());
|
||||||
|
averages.put("post", post.getAverage());
|
||||||
|
averages.put("perfection", perfection.getAverage());
|
||||||
|
averages.put("decoration", decoration.getAverage());
|
||||||
|
averages.put("cave", cave.getAverage());
|
||||||
|
averages.put("deposit", deposit.getAverage());
|
||||||
|
averages.put("carve.resolve", carveResolve.getAverage());
|
||||||
|
averages.put("carve.apply", carveApply.getAverage());
|
||||||
|
averages.put("context.prefill", contextPrefill.getAverage());
|
||||||
|
averages.put("pregen.wait.permit", pregenWaitPermit.getAverage());
|
||||||
|
averages.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage());
|
||||||
|
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
|
||||||
|
return Map.copyOf(averages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,8 @@ import lombok.ToString;
|
|||||||
public class EngineTarget {
|
public class EngineTarget {
|
||||||
private final MultiBurst burster;
|
private final MultiBurst burster;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
private IrisDimension dimension;
|
private final IrisDimension dimension;
|
||||||
private IrisWorld world;
|
private final IrisWorld world;
|
||||||
|
|
||||||
public EngineTarget(IrisWorld world, IrisDimension dimension, IrisData data) {
|
public EngineTarget(IrisWorld world, IrisDimension dimension, IrisData data) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisEngineStatistics;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record EngineTelemetrySnapshot(
|
||||||
|
long sampledAtMs,
|
||||||
|
String worldIdentity,
|
||||||
|
String worldName,
|
||||||
|
String dimensionKey,
|
||||||
|
boolean active,
|
||||||
|
boolean studio,
|
||||||
|
boolean closing,
|
||||||
|
boolean failed,
|
||||||
|
long loadedChunks,
|
||||||
|
long loadedEntities,
|
||||||
|
double entitySaturation,
|
||||||
|
long generatedSession,
|
||||||
|
long generatedTotal,
|
||||||
|
double chunksPerSecond,
|
||||||
|
long blockUpdatesPerSecond,
|
||||||
|
int parallelism,
|
||||||
|
int activeGenerationLeases,
|
||||||
|
long hotloadsTotal,
|
||||||
|
long mantleResidentPlates,
|
||||||
|
long mantleQueuedPlates,
|
||||||
|
double mantleIdleMs,
|
||||||
|
Map<String, Double> generationTimingsMs
|
||||||
|
) {
|
||||||
|
public EngineTelemetrySnapshot {
|
||||||
|
if (worldIdentity == null || worldIdentity.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Engine telemetry requires a world identity");
|
||||||
|
}
|
||||||
|
worldIdentity = worldIdentity.trim();
|
||||||
|
worldName = worldName == null || worldName.isBlank() ? worldIdentity : worldName.trim();
|
||||||
|
dimensionKey = dimensionKey == null ? "" : dimensionKey.trim();
|
||||||
|
loadedChunks = Math.max(0L, loadedChunks);
|
||||||
|
loadedEntities = Math.max(0L, loadedEntities);
|
||||||
|
entitySaturation = finiteNonNegative(entitySaturation);
|
||||||
|
generatedSession = Math.max(0L, generatedSession);
|
||||||
|
generatedTotal = Math.max(0L, generatedTotal);
|
||||||
|
chunksPerSecond = finiteNonNegative(chunksPerSecond);
|
||||||
|
blockUpdatesPerSecond = Math.max(0L, blockUpdatesPerSecond);
|
||||||
|
parallelism = Math.max(0, parallelism);
|
||||||
|
activeGenerationLeases = Math.max(0, activeGenerationLeases);
|
||||||
|
hotloadsTotal = Math.max(0L, hotloadsTotal);
|
||||||
|
mantleResidentPlates = Math.max(0L, mantleResidentPlates);
|
||||||
|
mantleQueuedPlates = Math.max(0L, mantleQueuedPlates);
|
||||||
|
mantleIdleMs = finiteNonNegative(mantleIdleMs);
|
||||||
|
generationTimingsMs = sanitizeTimings(generationTimingsMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EngineTelemetrySnapshot capture(Engine engine, double chunksPerSecond, long sampledAtMs) {
|
||||||
|
if (engine == null) {
|
||||||
|
throw new IllegalArgumentException("Engine cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
EngineWorldManager worldManager = engine.getWorldManager();
|
||||||
|
GenerationSessionManager sessions = engine.getGenerationSessions();
|
||||||
|
IrisEngineStatistics statistics = engine.getEngineData().getStatistics();
|
||||||
|
boolean closing = engine.isClosing();
|
||||||
|
boolean closed = engine.isClosed();
|
||||||
|
boolean failed = engine.hasFailed();
|
||||||
|
return new EngineTelemetrySnapshot(
|
||||||
|
sampledAtMs,
|
||||||
|
engine.getWorld().identity(),
|
||||||
|
engine.getWorld().name(),
|
||||||
|
engine.getDimension().getLoadKey(),
|
||||||
|
!closing && !closed && !failed,
|
||||||
|
engine.isStudio(),
|
||||||
|
closing,
|
||||||
|
failed,
|
||||||
|
worldManager == null ? 0L : worldManager.getChunkCount(),
|
||||||
|
worldManager == null ? 0L : worldManager.getEntityCount(),
|
||||||
|
worldManager == null ? 0D : worldManager.getEntitySaturation(),
|
||||||
|
engine.getGenerated(),
|
||||||
|
statistics == null ? 0L : statistics.getChunksGenerated(),
|
||||||
|
chunksPerSecond,
|
||||||
|
engine.getBlockUpdatesPerSecond(),
|
||||||
|
engine.getParallelism(),
|
||||||
|
sessions == null ? 0 : sessions.activeLeases(),
|
||||||
|
statistics == null ? 0L : statistics.getTotalHotloads(),
|
||||||
|
engine.getMantle().getLoadedRegionCount(),
|
||||||
|
engine.getMantle().getUnloadRegionCount(),
|
||||||
|
engine.getMantle().getAdjustedIdleDuration(),
|
||||||
|
engine.getMetrics().telemetryAverages()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Aggregate aggregate(List<EngineTelemetrySnapshot> snapshots) {
|
||||||
|
List<EngineTelemetrySnapshot> safeSnapshots = snapshots == null ? List.of() : snapshots;
|
||||||
|
int active = 0;
|
||||||
|
int studio = 0;
|
||||||
|
int closing = 0;
|
||||||
|
int failed = 0;
|
||||||
|
long loadedChunks = 0L;
|
||||||
|
long loadedEntities = 0L;
|
||||||
|
double entitySaturation = 0D;
|
||||||
|
long generatedSession = 0L;
|
||||||
|
long generatedTotal = 0L;
|
||||||
|
double chunksPerSecond = 0D;
|
||||||
|
long blockUpdatesPerSecond = 0L;
|
||||||
|
int parallelism = 0;
|
||||||
|
int activeGenerationLeases = 0;
|
||||||
|
long hotloadsTotal = 0L;
|
||||||
|
long mantleResidentPlates = 0L;
|
||||||
|
long mantleQueuedPlates = 0L;
|
||||||
|
double mantleIdleTotal = 0D;
|
||||||
|
double mantleIdleMax = 0D;
|
||||||
|
double mantleIdleMin = Double.POSITIVE_INFINITY;
|
||||||
|
int mantleIdleCount = 0;
|
||||||
|
int worldCount = 0;
|
||||||
|
Map<String, Double> timingMaxima = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (EngineTelemetrySnapshot snapshot : safeSnapshots) {
|
||||||
|
if (snapshot == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
worldCount++;
|
||||||
|
active += snapshot.active() ? 1 : 0;
|
||||||
|
studio += snapshot.studio() ? 1 : 0;
|
||||||
|
closing += snapshot.closing() ? 1 : 0;
|
||||||
|
failed += snapshot.failed() ? 1 : 0;
|
||||||
|
loadedChunks += snapshot.loadedChunks();
|
||||||
|
loadedEntities += snapshot.loadedEntities();
|
||||||
|
entitySaturation = Math.max(entitySaturation, snapshot.entitySaturation());
|
||||||
|
generatedSession += snapshot.generatedSession();
|
||||||
|
generatedTotal += snapshot.generatedTotal();
|
||||||
|
chunksPerSecond += snapshot.chunksPerSecond();
|
||||||
|
blockUpdatesPerSecond += snapshot.blockUpdatesPerSecond();
|
||||||
|
parallelism += snapshot.parallelism();
|
||||||
|
activeGenerationLeases += snapshot.activeGenerationLeases();
|
||||||
|
hotloadsTotal += snapshot.hotloadsTotal();
|
||||||
|
mantleResidentPlates += snapshot.mantleResidentPlates();
|
||||||
|
mantleQueuedPlates += snapshot.mantleQueuedPlates();
|
||||||
|
mantleIdleTotal += snapshot.mantleIdleMs();
|
||||||
|
mantleIdleMax = Math.max(mantleIdleMax, snapshot.mantleIdleMs());
|
||||||
|
mantleIdleMin = Math.min(mantleIdleMin, snapshot.mantleIdleMs());
|
||||||
|
mantleIdleCount++;
|
||||||
|
for (Map.Entry<String, Double> entry : snapshot.generationTimingsMs().entrySet()) {
|
||||||
|
timingMaxima.merge(entry.getKey(), entry.getValue(), Math::max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Aggregate(
|
||||||
|
worldCount,
|
||||||
|
active,
|
||||||
|
studio,
|
||||||
|
closing,
|
||||||
|
failed,
|
||||||
|
loadedChunks,
|
||||||
|
loadedEntities,
|
||||||
|
entitySaturation,
|
||||||
|
generatedSession,
|
||||||
|
generatedTotal,
|
||||||
|
chunksPerSecond,
|
||||||
|
blockUpdatesPerSecond,
|
||||||
|
parallelism,
|
||||||
|
activeGenerationLeases,
|
||||||
|
hotloadsTotal,
|
||||||
|
mantleResidentPlates,
|
||||||
|
mantleQueuedPlates,
|
||||||
|
mantleIdleCount == 0 ? 0D : mantleIdleTotal / mantleIdleCount,
|
||||||
|
mantleIdleMax,
|
||||||
|
mantleIdleCount == 0 ? 0D : mantleIdleMin,
|
||||||
|
Map.copyOf(timingMaxima)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double finiteNonNegative(double value) {
|
||||||
|
return Double.isFinite(value) && value > 0D ? value : 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Double> sanitizeTimings(Map<String, Double> timings) {
|
||||||
|
if (timings == null || timings.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
Map<String, Double> sanitized = new LinkedHashMap<>(timings.size());
|
||||||
|
for (Map.Entry<String, Double> entry : timings.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Double value = entry.getValue();
|
||||||
|
if (key == null || key.isBlank() || value == null || !Double.isFinite(value) || value < 0D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sanitized.put(key, value);
|
||||||
|
}
|
||||||
|
return Map.copyOf(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Aggregate(
|
||||||
|
int worldCount,
|
||||||
|
int active,
|
||||||
|
int studio,
|
||||||
|
int closing,
|
||||||
|
int failed,
|
||||||
|
long loadedChunks,
|
||||||
|
long loadedEntities,
|
||||||
|
double entitySaturationMax,
|
||||||
|
long generatedSession,
|
||||||
|
long generatedTotal,
|
||||||
|
double chunksPerSecond,
|
||||||
|
long blockUpdatesPerSecond,
|
||||||
|
int parallelism,
|
||||||
|
int activeGenerationLeases,
|
||||||
|
long hotloadsTotal,
|
||||||
|
long mantleResidentPlates,
|
||||||
|
long mantleQueuedPlates,
|
||||||
|
double mantleIdleAverageMs,
|
||||||
|
double mantleIdleMaxMs,
|
||||||
|
double mantleIdleMinMs,
|
||||||
|
Map<String, Double> generationTimingMaximaMs
|
||||||
|
) {
|
||||||
|
public Aggregate {
|
||||||
|
generationTimingMaximaMs = generationTimingMaximaMs == null
|
||||||
|
? Map.of()
|
||||||
|
: Map.copyOf(generationTimingMaximaMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ package art.arcane.iris.engine.framework;
|
|||||||
|
|
||||||
@SuppressWarnings("EmptyMethod")
|
@SuppressWarnings("EmptyMethod")
|
||||||
public interface EngineWorldManager {
|
public interface EngineWorldManager {
|
||||||
|
default void start() {
|
||||||
|
}
|
||||||
|
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
int getEntityCount();
|
int getEntityCount();
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ public final class GenerationSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException {
|
||||||
|
while (true) {
|
||||||
|
GenerationSessionState state = current.get();
|
||||||
|
if (state == null || state.sessionId() != sessionId) {
|
||||||
|
throw rejected(operation, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.activeLeases().incrementAndGet();
|
||||||
|
if (state != current.get()) {
|
||||||
|
releaseLease(state);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenerationSessionLease(this, state, state.sessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long currentSessionId() {
|
public long currentSessionId() {
|
||||||
GenerationSessionState state = current.get();
|
GenerationSessionState state = current.get();
|
||||||
return state == null ? 0L : state.sessionId();
|
return state == null ? 0L : state.sessionId();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
|
|||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
@@ -32,7 +33,7 @@ import art.arcane.volmlib.util.math.Position2;
|
|||||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -105,18 +106,15 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
|
public CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
|
||||||
if (engine.isClosed()) {
|
if (engine.isClosed()) {
|
||||||
throw new WrongEngineBroException();
|
throw new WrongEngineBroException();
|
||||||
}
|
}
|
||||||
|
|
||||||
Locator.cancelSearch();
|
AtomicBoolean stop = new AtomicBoolean(false);
|
||||||
|
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
|
||||||
return MultiBurst.burst.completeValue(() -> {
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("hinted_locator_search");
|
||||||
AtomicBoolean stop = new AtomicBoolean(false);
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
LocatorCanceller.cancel = () -> stop.set(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
SearchPlan plan = planner.apply(engine);
|
SearchPlan plan = planner.apply(engine);
|
||||||
|
|
||||||
if (!plan.isPossible()) {
|
if (!plan.isPossible()) {
|
||||||
@@ -124,10 +122,9 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return search(engine, plan, pos, timeout, checks, stop);
|
return search(engine, plan, pos, timeout, checks, stop);
|
||||||
} finally {
|
|
||||||
LocatorCanceller.cancel = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return LocatorCanceller.requestScoped(search, stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
|
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
|
||||||
@@ -140,7 +137,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
KList<Position2> batch = new KList<>();
|
KList<Position2> batch = new KList<>();
|
||||||
|
|
||||||
for (int ring = 0; ring <= maxRing; ring++) {
|
for (int ring = 0; ring <= maxRing; ring++) {
|
||||||
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
|
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +170,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
int index = i;
|
int index = i;
|
||||||
Position2 sample = batch.get(i);
|
Position2 sample = batch.get(i);
|
||||||
executor.queue(() -> {
|
executor.queue(() -> {
|
||||||
if (stop.get() || (fine && matched.get())) {
|
if (stop.get() || engine.isClosing() || (fine && matched.get())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +207,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
return batch.get(i);
|
return batch.get(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
|
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +225,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
KList<Position2> cells = new KList<>();
|
KList<Position2> cells = new KList<>();
|
||||||
|
|
||||||
for (int ring = 0; ring <= stride; ring++) {
|
for (int ring = 0; ring <= stride; ring++) {
|
||||||
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
|
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +236,7 @@ public final class HintedLocator<T> implements Locator<T> {
|
|||||||
|
|
||||||
for (Position2 cell : cells) {
|
for (Position2 cell : cells) {
|
||||||
executor.queue(() -> {
|
executor.queue(() -> {
|
||||||
if (stop.get() || found.get() != null) {
|
if (stop.get() || engine.isClosing() || found.get() != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
|
|||||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -42,13 +42,6 @@ import java.util.function.Consumer;
|
|||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface Locator<T> {
|
public interface Locator<T> {
|
||||||
static void cancelSearch() {
|
|
||||||
if (LocatorCanceller.cancel != null) {
|
|
||||||
LocatorCanceller.cancel.run();
|
|
||||||
LocatorCanceller.cancel = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Locator<IrisRegion> region(String loadKey) {
|
static Locator<IrisRegion> region(String loadKey) {
|
||||||
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
|
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
|
||||||
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
|
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
|
||||||
@@ -107,55 +100,54 @@ public interface Locator<T> {
|
|||||||
|
|
||||||
boolean matches(Engine engine, Position2 chunk);
|
boolean matches(Engine engine, Position2 chunk);
|
||||||
|
|
||||||
default Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
|
default CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
|
||||||
if (engine.isClosed()) {
|
if (engine.isClosed()) {
|
||||||
throw new WrongEngineBroException();
|
throw new WrongEngineBroException();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelSearch();
|
AtomicBoolean stop = new AtomicBoolean(false);
|
||||||
|
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
|
||||||
|
try (GenerationSessionLease lease = engine.acquireGenerationLease("locator_search");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
|
||||||
|
MultiBurst burst = MultiBurst.burst;
|
||||||
|
AtomicBoolean found = new AtomicBoolean(false);
|
||||||
|
AtomicInteger searched = new AtomicInteger();
|
||||||
|
AtomicReference<Position2> foundPos = new AtomicReference<>();
|
||||||
|
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||||
|
AtomicReference<Position2> next = new AtomicReference<>(pos);
|
||||||
|
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
|
||||||
|
s.setOffset(pos.getX(), pos.getZ());
|
||||||
|
s.next();
|
||||||
|
while (!found.get() && !stop.get() && !engine.isClosing() && px.getMilliseconds() < timeout) {
|
||||||
|
BurstExecutor e = burst.burst(tc);
|
||||||
|
|
||||||
return MultiBurst.burst.completeValue(() -> {
|
for (int i = 0; i < tc; i++) {
|
||||||
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
|
Position2 p = next.get();
|
||||||
MultiBurst burst = MultiBurst.burst;
|
s.next();
|
||||||
AtomicBoolean found = new AtomicBoolean(false);
|
e.queue(() -> {
|
||||||
AtomicInteger searched = new AtomicInteger();
|
if (found.get() || stop.get() || engine.isClosing()) {
|
||||||
AtomicBoolean stop = new AtomicBoolean(false);
|
return;
|
||||||
AtomicReference<Position2> foundPos = new AtomicReference<>();
|
}
|
||||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
if (matches(engine, p)) {
|
||||||
LocatorCanceller.cancel = () -> stop.set(true);
|
foundPos.compareAndSet(null, p);
|
||||||
AtomicReference<Position2> next = new AtomicReference<>(pos);
|
found.set(true);
|
||||||
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
|
}
|
||||||
s.setOffset(pos.getX(), pos.getZ());
|
searched.incrementAndGet();
|
||||||
s.next();
|
});
|
||||||
while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) {
|
}
|
||||||
BurstExecutor e = burst.burst(tc);
|
|
||||||
|
|
||||||
for (int i = 0; i < tc; i++) {
|
e.complete();
|
||||||
Position2 p = next.get();
|
checks.accept(searched.get());
|
||||||
s.next();
|
|
||||||
e.queue(() -> {
|
|
||||||
if (found.get()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (matches(engine, p)) {
|
|
||||||
foundPos.compareAndSet(null, p);
|
|
||||||
found.set(true);
|
|
||||||
}
|
|
||||||
searched.incrementAndGet();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.complete();
|
if (found.get() && foundPos.get() != null) {
|
||||||
checks.accept(searched.get());
|
return foundPos.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocatorCanceller.cancel = null;
|
|
||||||
|
|
||||||
if (found.get() && foundPos.get() != null) {
|
|
||||||
return foundPos.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
return LocatorCanceller.requestScoped(search, stop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,49 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
public class LocatorCanceller {
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class LocatorCanceller {
|
||||||
protected static Runnable cancel = null;
|
protected static Runnable cancel = null;
|
||||||
|
|
||||||
|
private LocatorCanceller() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> CompletableFuture<T> requestScoped(CompletableFuture<T> future, AtomicBoolean stop) {
|
||||||
|
return new RequestFuture<>(Objects.requireNonNull(future), Objects.requireNonNull(stop));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RequestFuture<T> extends CompletableFuture<T> {
|
||||||
|
private final CompletableFuture<T> delegate;
|
||||||
|
private final AtomicBoolean stop;
|
||||||
|
|
||||||
|
private RequestFuture(CompletableFuture<T> delegate, AtomicBoolean stop) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.stop = stop;
|
||||||
|
delegate.whenComplete((value, exception) -> {
|
||||||
|
if (isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception == null) {
|
||||||
|
complete(value);
|
||||||
|
} else {
|
||||||
|
completeExceptionally(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||||
|
if (isDone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop.set(true);
|
||||||
|
boolean cancelled = super.cancel(mayInterruptIfRunning);
|
||||||
|
delegate.cancel(mayInterruptIfRunning);
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user