diff --git a/common/src/main/java/com/axalotl/async/common/commands/ConfigCommand.java b/common/src/main/java/com/axalotl/async/common/commands/ConfigCommand.java index 4b145e58..7d0952f2 100644 --- a/common/src/main/java/com/axalotl/async/common/commands/ConfigCommand.java +++ b/common/src/main/java/com/axalotl/async/common/commands/ConfigCommand.java @@ -30,6 +30,7 @@ public static LiteralArgumentBuilder registerConfig(LiteralA .then(buildReloadCommand()) .then(buildSynchronizedEntitiesCommand()) .then(buildAsyncEntitySpawnCommand()) + .then(buildAsyncMobSpawningCommand()) .then(buildAsyncRandomTicksCommand()) ); } @@ -155,6 +156,28 @@ private static LiteralArgumentBuilder buildAsyncRandomTicksC ); } + private static LiteralArgumentBuilder buildAsyncMobSpawningCommand() { + return literal("setAsyncMobSpawning") + .executes(ctx -> { + sendMessage(ctx, "Current value of async natural mob spawning: ", + String.valueOf(AsyncConfig.enableAsyncMobSpawning), + false); + return 1; + }) + .then(Commands.argument("value", BoolArgumentType.bool()) + .executes(ctx -> { + boolean value = BoolArgumentType.getBool(ctx, "value"); + AsyncConfig.enableAsyncMobSpawning = value; + PlatformUtils.saveConfig(); + + sendMessage(ctx, "Async Natural Mob Spawning set to ", + String.valueOf(value), + true); + return 1; + }) + ); + } + private static void displaySynchronizedEntities(CommandContext ctx) { Set entities = AsyncConfig.synchronizedEntities; MutableComponent message = prefix.copy() @@ -266,4 +289,4 @@ private static void sendErrorMessage(CommandContext ctx, Str .append(Component.literal(suffix).withStyle(style -> style.withColor(ChatFormatting.RED))); ctx.getSource().sendSuccess(() -> message, true); } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/axalotl/async/common/commands/StatsCommand.java b/common/src/main/java/com/axalotl/async/common/commands/StatsCommand.java index 17229d24..d1b34caa 100644 --- a/common/src/main/java/com/axalotl/async/common/commands/StatsCommand.java +++ b/common/src/main/java/com/axalotl/async/common/commands/StatsCommand.java @@ -60,6 +60,7 @@ private static void showGeneralStats(CommandSourceStack source) { boolean enabled = !AsyncConfig.disabled; boolean asyncSpawn = AsyncConfig.enableAsyncSpawn; + boolean asyncMobSpawning = AsyncConfig.enableAsyncMobSpawning; boolean asyncRandomTicks = AsyncConfig.enableAsyncRandomTicks; MutableComponent message = prefix.copy() @@ -73,6 +74,10 @@ private static void showGeneralStats(CommandSourceStack source) { .append(Component.literal(asyncSpawn ? "Enabled" : "Disabled") .withStyle(asyncSpawn ? ChatFormatting.GREEN : ChatFormatting.RED)) + .append(Component.literal("\nAsync Mob Spawning: ").withStyle(ChatFormatting.WHITE)) + .append(Component.literal(asyncMobSpawning ? "Enabled" : "Disabled") + .withStyle(asyncMobSpawning ? ChatFormatting.GREEN : ChatFormatting.RED)) + .append(Component.literal("\nAsync Random Ticks: ").withStyle(ChatFormatting.WHITE)) .append(Component.literal(asyncRandomTicks ? "Enabled" : "Disabled") .withStyle(asyncRandomTicks ? ChatFormatting.GREEN : ChatFormatting.RED)) @@ -163,4 +168,4 @@ private static void showEntityStats(CommandSourceStack source, int topCount) { source.sendSuccess(() -> message, false); }); } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/axalotl/async/common/config/AsyncConfig.java b/common/src/main/java/com/axalotl/async/common/config/AsyncConfig.java index 7dca7972..2dd8f94b 100644 --- a/common/src/main/java/com/axalotl/async/common/config/AsyncConfig.java +++ b/common/src/main/java/com/axalotl/async/common/config/AsyncConfig.java @@ -16,6 +16,7 @@ public class AsyncConfig { public static boolean disabled = false; public static int maxThreads = -1; public static boolean enableAsyncSpawn = true; + public static boolean enableAsyncMobSpawning = getDefaultAsyncMobSpawning(); public static boolean enableAsyncRandomTicks = false; public static Set synchronizedEntities = getDefaultSynchronizedEntities(); @@ -34,6 +35,10 @@ public static Set getDefaultSynchronizedEntities() { return defaultSynchronizedEntities; } + public static boolean getDefaultAsyncMobSpawning() { + return !PlatformUtils.isModLoaded("c2me"); + } + public static int getParallelism() { if (maxThreads <= 0) return Runtime.getRuntime().availableProcessors(); return Math.max(1, Math.min(Runtime.getRuntime().availableProcessors(), maxThreads)); @@ -121,4 +126,4 @@ public static void onConfigLoaded() { public static void clearCaches() { syncCache.clear(); } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/axalotl/async/common/mixin/server/ServerChunkCacheMixin.java b/common/src/main/java/com/axalotl/async/common/mixin/server/ServerChunkCacheMixin.java index cd57af3c..d64258bd 100644 --- a/common/src/main/java/com/axalotl/async/common/mixin/server/ServerChunkCacheMixin.java +++ b/common/src/main/java/com/axalotl/async/common/mixin/server/ServerChunkCacheMixin.java @@ -83,6 +83,9 @@ public abstract class ServerChunkCacheMixin extends ChunkSource { @Unique private final AtomicBoolean async$spawnCountsReady = new AtomicBoolean(false); + @Unique + private volatile boolean async$forceSyncMobSpawning = false; + @Shadow public abstract void tickSpawningChunk(LevelChunk chunk, long timeInhabited, List spawnCategories, NaturalSpawner.SpawnState spawnState); @@ -96,6 +99,16 @@ public abstract class ServerChunkCacheMixin extends ChunkSource { return; } + if (async$isHighRiskAsyncChunkRequest(leastStatus, create)) { + String message = String.format( + Locale.ROOT, + "Blocked async chunk request that could synchronously load or generate chunks on thread %s at [%d, %d] status %s create=%s", + Thread.currentThread().getName(), x, z, leastStatus, create + ); + ParallelProcessor.LOGGER.warn(message); + throw new IllegalStateException(message); + } + CompletableFuture> future = CompletableFuture.supplyAsync( () -> this.getChunkFutureMainThread(x, z, leastStatus, create), this.mainThreadProcessor @@ -118,6 +131,11 @@ public abstract class ServerChunkCacheMixin extends ChunkSource { cir.setReturnValue(chunk); } + @Unique + private boolean async$isHighRiskAsyncChunkRequest(ChunkStatus leastStatus, boolean create) { + return create || leastStatus.isOrAfter(ChunkStatus.STRUCTURE_STARTS); + } + @Unique private @Nullable ChunkAccess async$tryGetChunk(int x, int z, ChunkStatus leastStatus) { ChunkHolder holder = this.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); @@ -139,8 +157,10 @@ private void shortcutGetChunkNow(int chunkX, int chunkZ, CallbackInfoReturnable< if (Thread.currentThread() != this.mainThread) { final ChunkHolder holder = this.getVisibleChunkIfPresent(ChunkPos.asLong(chunkX, chunkZ)); if (holder != null) { - final CompletableFuture> future = holder.scheduleChunkGenerationTask(ChunkStatus.FULL, this.chunkMap); - ChunkAccess chunk = future.getNow(ChunkHolder.UNLOADED_CHUNK).orElse(null); + ChunkAccess chunk = holder.getChunkIfPresent(ChunkStatus.FULL); + if (chunk instanceof ImposterProtoChunk imposter) { + chunk = imposter.getWrapped(); + } if (chunk instanceof LevelChunk worldChunk) { cir.setReturnValue(worldChunk); } @@ -148,9 +168,14 @@ private void shortcutGetChunkNow(int chunkX, int chunkZ, CallbackInfoReturnable< } } + @Unique + private boolean async$canUseAsyncMobSpawning() { + return !AsyncConfig.disabled && AsyncConfig.enableAsyncSpawn && AsyncConfig.enableAsyncMobSpawning && !async$forceSyncMobSpawning; + } + @Inject(method = "tickChunks()V", at = @At("TAIL")) private void tickChunks(CallbackInfo ci) { - if (AsyncConfig.disabled || !AsyncConfig.enableAsyncSpawn) return; + if (!async$canUseAsyncMobSpawning()) return; if (async$firstRunSpawnCounts) { async$firstRunSpawnCounts = false; @@ -159,8 +184,14 @@ private void tickChunks(CallbackInfo ci) { if (async$spawnCountsReady.getAndSet(false)) { final int i = distanceManager.getNaturalSpawnChunkCount(); ParallelProcessor.tickPool.submit(() -> { - lastSpawnState = NaturalSpawner.createState(i, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); - async$spawnCountsReady.set(true); + try { + lastSpawnState = NaturalSpawner.createState(i, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); + } catch (Throwable throwable) { + async$forceSyncMobSpawning = true; + ParallelProcessor.LOGGER.warn("Async natural spawn state calculation failed, disabling async natural mob spawning for this level", throwable); + } finally { + async$spawnCountsReady.set(true); + } }); } } @@ -170,7 +201,7 @@ private void tickChunksSpawn(ProfilerFiller profiler, long timeInhabited, Operat profiler.push("naturalSpawnCount"); int i = this.distanceManager.getNaturalSpawnChunkCount(); - if (AsyncConfig.disabled || !AsyncConfig.enableAsyncSpawn || async$firstRunSpawnCounts) { + if (!async$canUseAsyncMobSpawning() || async$firstRunSpawnCounts) { lastSpawnState = NaturalSpawner.createState(i, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); } @@ -186,7 +217,7 @@ private void tickChunksSpawn(ProfilerFiller profiler, long timeInhabited, Operat profiler.popPush("tickSpawningChunks"); - if (!AsyncConfig.disabled && AsyncConfig.enableAsyncSpawn) { + if (async$canUseAsyncMobSpawning()) { NaturalSpawner.SpawnState currentState = lastSpawnState; if (currentState != null) { CompletableFuture.runAsync(() -> { @@ -199,21 +230,8 @@ private void tickChunksSpawn(ProfilerFiller profiler, long timeInhabited, Operat } } }, ParallelProcessor.tickPool).exceptionally(e -> { - ParallelProcessor.LOGGER.error("Error in async entity spawning, switching to synchronous", e); - List list1 = this.spawningChunks; - try { - profiler.popPush("filteringSpawningChunks"); - this.chunkMap.collectSpawningChunks(list1); - profiler.popPush("shuffleSpawningChunks"); - Util.shuffle(list1, this.level.random); - profiler.popPush("tickSpawningChunks"); - - for(LevelChunk levelchunk : list1) { - this.tickSpawningChunk(levelchunk, timeInhabited, list, lastSpawnState); - } - } finally { - list1.clear(); - } + async$forceSyncMobSpawning = true; + ParallelProcessor.LOGGER.error("Error in async natural mob spawning, disabling async natural mob spawning for this level", e); return null; }); } @@ -242,4 +260,4 @@ private void tickChunksSpawn(ProfilerFiller profiler, long timeInhabited, Operat } profiler.pop(); } -} \ No newline at end of file +} diff --git a/fabric/src/main/java/com/axalotl/async/fabric/config/AsyncConfig.java b/fabric/src/main/java/com/axalotl/async/fabric/config/AsyncConfig.java index 6214c8de..7930be43 100644 --- a/fabric/src/main/java/com/axalotl/async/fabric/config/AsyncConfig.java +++ b/fabric/src/main/java/com/axalotl/async/fabric/config/AsyncConfig.java @@ -28,6 +28,7 @@ public class AsyncConfig { "maxThreads", "synchronizedEntities", "enableAsyncSpawn", + "enableAsyncMobSpawning", "enableAsyncRandomTicks" ); @@ -63,6 +64,8 @@ List of entity IDs or namespaces (*): - 'minecraft:*' = all entities in namespace"""); setWithComment("enableAsyncSpawn", enableAsyncSpawn, "Enables async entity spawning. WARNING: incompatible with Carpet's lagFreeSpawning."); + setWithComment("enableAsyncMobSpawning", enableAsyncMobSpawning, + "Enables async natural mob spawning. Defaults to false when C2ME is detected to avoid chunk-load deadlocks."); setWithComment("enableAsyncRandomTicks", enableAsyncRandomTicks, "Experimental! Enables async random ticks."); @@ -81,6 +84,7 @@ private static void loadConfigValues() { disabled = CONFIG.getOrElse("disabled", disabled); maxThreads = CONFIG.getOrElse("maxThreads", maxThreads); enableAsyncSpawn = CONFIG.getOrElse("enableAsyncSpawn", enableAsyncSpawn); + enableAsyncMobSpawning = CONFIG.getOrElse("enableAsyncMobSpawning", enableAsyncMobSpawning); enableAsyncRandomTicks = CONFIG.getOrElse("enableAsyncRandomTicks", enableAsyncRandomTicks); List entries = CONFIG.get("synchronizedEntities"); @@ -100,6 +104,7 @@ List of entity IDs or namespaces (*): - 'minecraft:zombie' = specific entity - 'minecraft:*' = all entities in namespace"""); setCommentIfExists("enableAsyncSpawn", "Enables async entity spawning. WARNING: incompatible with Carpet's lagFreeSpawning."); + setCommentIfExists("enableAsyncMobSpawning", "Enables async natural mob spawning. Defaults to false when C2ME is detected to avoid chunk-load deadlocks."); setCommentIfExists("enableAsyncRandomTicks", "Experimental! Enables async random ticks."); } @@ -129,7 +134,8 @@ private static void setDefaultValues() { disabled = false; maxThreads = -1; enableAsyncSpawn = true; + enableAsyncMobSpawning = getDefaultAsyncMobSpawning(); enableAsyncRandomTicks = false; synchronizedEntities = getDefaultSynchronizedEntities(); } -} \ No newline at end of file +} diff --git a/neoforge/src/main/java/com/axalotl/async/neoforge/config/AsyncConfig.java b/neoforge/src/main/java/com/axalotl/async/neoforge/config/AsyncConfig.java index dad0a2d2..178de158 100644 --- a/neoforge/src/main/java/com/axalotl/async/neoforge/config/AsyncConfig.java +++ b/neoforge/src/main/java/com/axalotl/async/neoforge/config/AsyncConfig.java @@ -17,6 +17,7 @@ public class AsyncConfig { private static final ModConfigSpec.IntValue maxThreads; private static final ModConfigSpec.ConfigValue> synchronizedEntities; private static final ModConfigSpec.BooleanValue enableAsyncSpawn; + private static final ModConfigSpec.BooleanValue enableAsyncMobSpawning; private static final ModConfigSpec.BooleanValue enableAsyncRandomTicks; static { @@ -42,6 +43,9 @@ List of entity IDs or namespaces (*): enableAsyncSpawn = BUILDER.comment("Enables async entity spawning. WARNING: incompatible with Carpet's lagFreeSpawning.") .define("enableAsyncSpawn", com.axalotl.async.common.config.AsyncConfig.enableAsyncSpawn); + enableAsyncMobSpawning = BUILDER.comment("Enables async natural mob spawning. Defaults to false when C2ME is detected to avoid chunk-load deadlocks.") + .define("enableAsyncMobSpawning", com.axalotl.async.common.config.AsyncConfig.enableAsyncMobSpawning); + enableAsyncRandomTicks = BUILDER.comment("Experimental! Enables async random ticks.") .define("enableAsyncRandomTicks", com.axalotl.async.common.config.AsyncConfig.enableAsyncRandomTicks); @@ -54,6 +58,7 @@ public static void loadConfig() { com.axalotl.async.common.config.AsyncConfig.disabled = disabled.get(); com.axalotl.async.common.config.AsyncConfig.maxThreads = maxThreads.get(); com.axalotl.async.common.config.AsyncConfig.enableAsyncSpawn = enableAsyncSpawn.get(); + com.axalotl.async.common.config.AsyncConfig.enableAsyncMobSpawning = enableAsyncMobSpawning.get(); com.axalotl.async.common.config.AsyncConfig.enableAsyncRandomTicks = enableAsyncRandomTicks.get(); List entries = synchronizedEntities.get(); @@ -71,9 +76,10 @@ public static void saveConfig() { disabled.set(com.axalotl.async.common.config.AsyncConfig.disabled); maxThreads.set(com.axalotl.async.common.config.AsyncConfig.maxThreads); enableAsyncSpawn.set(com.axalotl.async.common.config.AsyncConfig.enableAsyncSpawn); + enableAsyncMobSpawning.set(com.axalotl.async.common.config.AsyncConfig.enableAsyncMobSpawning); enableAsyncRandomTicks.set(com.axalotl.async.common.config.AsyncConfig.enableAsyncRandomTicks); synchronizedEntities.set(new ArrayList<>(com.axalotl.async.common.config.AsyncConfig.synchronizedEntities)); SPEC.save(); com.axalotl.async.common.config.AsyncConfig.onConfigLoaded(); } -} \ No newline at end of file +}