diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cb2b3ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Gradle 构建产物
+.gradle/
+build/
+out/
+
+# IDE
+.idea/
+.vscode/
+*.iml
+
+# 运行时数据
+runs/
+logs/
+*.log
+
+# 系统文件
+.DS_Store
+Thumbs.db
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..7402740
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,99 @@
+plugins {
+ id 'java-library'
+ id 'maven-publish'
+ id 'net.neoforged.gradle.userdev' version '7.0.170'
+}
+
+tasks.named('wrapper', Wrapper).configure {
+ distributionType = Wrapper.DistributionType.BIN
+ gradleVersion = '8.10'
+}
+
+version = mod_version
+group = mod_group_id
+
+base {
+ archivesName = mod_id
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+ withSourcesJar()
+}
+
+//minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
+//minecraft.accessTransformers.publish rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
+
+runs {
+ configureEach {
+ systemProperty 'forge.logging.markers', 'REGISTRIES'
+ //systemProperty 'forge.logging.console.level', 'debug'
+ systemProperty 'mixin.debug.verbose', 'true'
+ systemProperty 'mixin.debug.export', 'true'
+ modSource project.sourceSets.main
+ }
+
+ client {
+ systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
+ // TEMP-DEBUG: auto-join world for deadlock repro (REMOVE AFTER DEBUG)
+ argument '--quickPlaySingleplayer'
+ argument '新的世界'
+ // 关闭 JVM 热路径堆栈省略,便于定位并行子任务中反复抛出的 ClassCastException
+ jvmArguments '-XX:-OmitStackTraceInFastThrow'
+ }
+
+ server {
+ systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
+ argument '--nogui'
+ }
+
+ gameTestServer {
+ systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
+ }
+
+ data {
+ arguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
+ }
+}
+
+sourceSets.main.resources {
+ srcDir 'src/generated/resources'
+}
+
+dependencies {
+ implementation "net.neoforged:neoforge:${neo_version}"
+}
+
+tasks.withType(ProcessResources).configureEach {
+ var replaceProperties = [
+ minecraft_version : minecraft_version,
+ minecraft_version_range: minecraft_version_range,
+ neo_version : neo_version,
+ neo_version_range : neo_version_range,
+ loader_version_range: loader_version_range,
+ mod_id : mod_id,
+ mod_name : mod_name,
+ mod_license : mod_license,
+ mod_version : mod_version,
+ mod_authors : mod_authors,
+ mod_description : mod_description
+ ]
+ inputs.properties replaceProperties
+
+ filesMatching(['META-INF/neoforge.mods.toml']) {
+ expand replaceProperties
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = 'UTF-8'
+}
+
+idea {
+ module {
+ downloadSources = true
+ downloadJavadoc = true
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..65f25d3
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,55 @@
+# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
+# This is required to provide enough memory for the Minecraft decompilation process.
+# 本机 JDK 路径(机器特定,已从仓库中注释掉)。如本地构建报"无法找到 JDK",请取消注释并填你本机的 JDK 路径。
+#org.gradle.java.home=C:\\Program Files\\Java\\jdk-21
+org.gradle.jvmargs=-Xmx3G
+org.gradle.daemon=true
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configuration-cache=true
+
+# Environment Properties
+# You can setup the workspace by running: gradle sync
+# Note: the non-default values are documented in the NeoGradle user docs. You should probably be using them.
+neo_version=21.1.244
+# Parchment is an unofficial project maintained by ParchmentMC. It aims to provide better parameter names and javadocs.
+# You should use the latest version matching your Minecraft version.
+# parchment_minecraft_version=1.21.1
+# parchment_mappings_version=2024.11.17
+
+# The Minecraft version must agree with the NeoForge version to get a valid artifact
+minecraft_version=1.21.1
+# The Minecraft version range can use any release version of Minecraft as bounds.
+# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
+# as they do not follow standard versioning semantics.
+minecraft_version_range=[1.21.1,1.22)
+# The NeoForge version must agree with the Minecraft version to get a valid artifact
+neo_version_range=[21.1.0,21.2)
+# The loader version range can only use the major version of FML as bounds
+loader_version_range=[4,)
+
+## Mod Properties
+
+# The unique mod identifier for the mod. Must be lowercase, alphanumeric separated by underscores.
+# This is used to identify the mod in the mod list, for dependencies, and in the file name.
+mod_id=dimensionalripper
+# The human-readable display name for the mod.
+mod_name=Dimensional Ripper
+# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
+mod_license=All-Rights-Reserved
+# The mod version. See https://semver.org/
+mod_version=1.0.0
+# The group ID for the mod. It is only important when publishing build artifacts to Maven.
+mod_group_id=dev.modzuozhi
+# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
+mod_authors=YourNameHere
+# The description of the mod. This is a simple text string that is used for display purposes in the mod list.
+mod_description=Multi-threaded dimension parallel optimization: independent per-dimension tick loops, fine-grained parallel ticking for entities/block entities/chunks, collision optimization, and asynchronous portal teleportation support (vanilla nether/end and mod dimensions like Twilight Forest/Aether).
+
+# Proxy settings for VPN / system proxy
+# 说明:VPN 开启(系统代理模式)时取消下面 4 行注释;直连可访问时保持注释状态
+#systemProp.http.proxyHost=127.0.0.1
+#systemProp.http.proxyPort=57777
+#systemProp.https.proxyHost=127.0.0.1
+#systemProp.https.proxyPort=57777
+#systemProp.http.nonProxyHosts=localhost|127.*|192.168.*|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e644113
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9355b41
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..de98174
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,38 @@
+#!/bin/sh
+#
+# Gradle start up script for POSIX generated by the Gradle Wrapper project.
+#
+
+# Resolve APP_HOME
+APP_HOME=$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )
+APP_NAME="Gradle"
+APP_BASE_NAME=$(basename "$0")
+
+# Add default JVM options here.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use.
+if [ -n "$JAVA_HOME" ]; then
+ JAVA_CMD="$JAVA_HOME/bin/java"
+else
+ JAVA_CMD="java"
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then
+ MAX_FD_LIMIT=$(ulimit -H -n)
+ if [ $? -eq 0 ]; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then
+ MAX_FD=$MAX_FD_LIMIT
+ fi
+ ulimit -n $MAX_FD
+ fi
+fi
+
+exec "$JAVA_CMD" $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..50d06f3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c16ef86
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,13 @@
+pluginManagement {
+ repositories {
+ mavenLocal()
+ gradlePluginPortal()
+ maven { url = 'https://maven.neoforged.net/releases' }
+ }
+}
+
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
+}
+
+rootProject.name = 'dimensionalripper'
diff --git a/src/main/java/dev/modzuozhi/ModZuozhi.java b/src/main/java/dev/modzuozhi/ModZuozhi.java
new file mode 100644
index 0000000..3be8544
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/ModZuozhi.java
@@ -0,0 +1,45 @@
+package dev.modzuozhi;
+
+import com.mojang.logging.LogUtils;
+import dev.modzuozhi.command.MasterCommand;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.common.EventBusSubscriber;
+import net.neoforged.fml.common.Mod;
+import net.neoforged.neoforge.event.RegisterCommandsEvent;
+import org.slf4j.Logger;
+
+@Mod(ModZuozhi.MOD_ID)
+public class ModZuozhi {
+ public static final String MOD_ID = "dimensionalripper";
+ public static final Logger LOGGER = LogUtils.getLogger();
+
+ /**
+ * 全局主开关:false 时整个模组退化为原版(所有并行环节关闭),运行时可由
+ * {@code /dimensionalripper on|off} 切换,无需修改配置或重启。
+ */
+ public static volatile boolean ENABLED = true;
+
+ /** 全局主开关是否开启。所有 mixin 的并行入口都应先判断此开关。 */
+ public static boolean isEnabled() {
+ return ENABLED;
+ }
+
+ /**
+ * 是否禁用实体间碰撞(Mob Collision Off 模式,释放大量 TPS 红利)。
+ * 默认开启(已长时间测试稳定),可由 {@code /dimensionalripper nocollide on|off} 运行时切换。
+ */
+ public static volatile boolean MOB_COLLISION_OFF = true;
+
+ public ModZuozhi(IEventBus modEventBus) {
+ LOGGER.info("[ModZuozhi] 初始化多线程模组(dimthreads 式维度并行)...");
+ }
+
+ @EventBusSubscriber(modid = MOD_ID)
+ public static class CommandRegistration {
+ @SubscribeEvent
+ public static void onRegisterCommands(RegisterCommandsEvent event) {
+ MasterCommand.register(event.getDispatcher());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/command/MasterCommand.java b/src/main/java/dev/modzuozhi/command/MasterCommand.java
new file mode 100644
index 0000000..b3fe7c3
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/command/MasterCommand.java
@@ -0,0 +1,111 @@
+package dev.modzuozhi.command;
+
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import dev.modzuozhi.ModZuozhi;
+import dev.modzuozhi.core.dimthread.DimThreadCore;
+import dev.modzuozhi.core.dimthread.FaultGuard;
+import dev.modzuozhi.core.dimthread.FineGrainScheduler;
+import dev.modzuozhi.core.dimthread.TpsTracker;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.Level;
+
+import static net.minecraft.commands.Commands.literal;
+
+/**
+ * modzuozhi 管理命令。
+ *
+ * 全局多线程优化默认启用,不提供全局开关(运行时切换全局并行风险高,容易引发
+ * 状态不一致问题)。若触发自动降级(见 {@link FaultGuard}),默认退回原版串行,重启游戏
+ * 即恢复启用,也可执行 {@code /dimensionalripper fine on} 手动复位。
+ *
+ * /dimensionalripper fine on|off - 单独开关"细粒度并行"(维度内 TE/区块/实体并行,默认开启)。
+ * /dimensionalripper nocollide on|off - 单独开关"禁用实体间碰撞"(Mob Collision Off 模式,默认开启)。
+ * /dimensionalripper status - 查看当前运行状态、降级情况与故障原因。
+ */
+public final class MasterCommand {
+ private MasterCommand() {
+ }
+
+ public static void register(CommandDispatcher dispatcher) {
+ dispatcher.register(
+ literal("dimensionalripper")
+ .then(literal("status").executes(MasterCommand::status))
+ .then(literal("fine")
+ .then(literal("on").executes(ctx -> setFine(ctx, true)))
+ .then(literal("off").executes(ctx -> setFine(ctx, false))))
+ .then(literal("nocollide")
+ .then(literal("on").executes(ctx -> setNoCollide(ctx, true)))
+ .then(literal("off").executes(ctx -> setNoCollide(ctx, false)))));
+ }
+
+ private static int setFine(CommandContext ctx, boolean enabled) {
+ if (enabled && !ModZuozhi.isEnabled()) {
+ // 全局关闭时单独开细粒度没有意义(维度 loop 已停、并行入口被 isActive 拦截),
+ // 还会留下"全局关但细粒度开"的不一致状态。这里自动恢复全局开启,下一 tick
+ // 主线程会重新拉起维度 loop(killAllLoops 已清空 loops map,computeIfAbsent 重建)。
+ ModZuozhi.ENABLED = true;
+ ctx.getSource().sendSuccess(() ->
+ Component.literal("全局此前已关闭,已随细粒度一并恢复为开启"), false);
+ ModZuozhi.LOGGER.info("[ModZuozhi] 全局随 /dimensionalripper fine on 自动恢复为开启");
+ }
+ FineGrainScheduler.setEnabled(enabled);
+ if (!enabled) {
+ // 关闭细粒度后必须先排空 SUB_POOL 残留子任务:此时维度 loop 已退化为串行 tick,
+ // 残留实体子任务会与它并发改 EntitySection(靠 isLockDisabled 的残留保护仍持锁互斥,
+ // 见 EntityTickParallel),等排空后锁才回落到零开销纯串行。
+ FineGrainScheduler.awaitIdle(5000L);
+ }
+ if (enabled) {
+ FaultGuard.reset();
+ }
+ ctx.getSource().sendSuccess(() ->
+ Component.literal("细粒度并行已" + (enabled ? "开启" : "关闭")), false);
+ return 1;
+ }
+
+ private static int setNoCollide(CommandContext ctx, boolean enabled) {
+ ModZuozhi.MOB_COLLISION_OFF = enabled;
+ ctx.getSource().sendSuccess(() ->
+ Component.literal("实体间碰撞已" + (enabled ? "禁用" : "恢复")), false);
+ ModZuozhi.LOGGER.info("[ModZuozhi] 实体间碰撞已切换为 {}", enabled ? "禁用" : "恢复");
+ return 1;
+ }
+
+ private static int status(CommandContext ctx) {
+ String fine = FineGrainScheduler.isEnabled() ? "开启" : "关闭";
+ String noCollide = ModZuozhi.MOB_COLLISION_OFF ? "开启" : "关闭";
+ String fineMark = FaultGuard.isFineDegraded() ? "(已自动降级)" : "";
+ // 全局始终默认启用,不提供开关;FaultGuard 若触发全局降级,其信息已写入 reason。
+ String reason = FaultGuard.getReason();
+
+ ServerLevel level = ctx.getSource().getLevel();
+ ResourceKey dimension = level.dimension();
+ // 维度并行激活时:维度 loop 的真实 MSPT(recordDimension 持续更新);
+ // 全局被 FaultGuard 降级关闭(退回原版串行,维度 loop 已停、recordDimension 不再更新)时:
+ // 用主线程 tick 间隔样本,它才是当前真实 tick 周期。
+ double tps = DimThreadCore.MANAGER.isActive(level.getServer())
+ ? TpsTracker.tps(dimension)
+ : TpsTracker.tps();
+
+ String msg = String.format(
+ "modzuozhi 状态:细粒度=%s%s,禁碰撞=%s,故障原因:%s\n当前维度:%s,TPS:%.1f",
+ fine, fineMark, noCollide, reason,
+ dimensionName(dimension), tps);
+ ctx.getSource().sendSuccess(() -> Component.literal(msg), false);
+ return 1;
+ }
+
+ /** 把维度 key 映射成玩家可读的中文名。 */
+ private static String dimensionName(ResourceKey dimension) {
+ return switch (dimension.location().getPath()) {
+ case "overworld" -> "主世界";
+ case "the_nether" -> "下界";
+ case "the_end" -> "末地";
+ default -> dimension.location().toString();
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java
new file mode 100644
index 0000000..2ce961b
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java
@@ -0,0 +1,193 @@
+package dev.modzuozhi.core.collection;
+
+import it.unimi.dsi.fastutil.longs.AbstractLongSet;
+import it.unimi.dsi.fastutil.longs.Long2ObjectFunction;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongSet;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.LongFunction;
+
+/**
+ * 第11步:SECTION_LOCK → 并发集合——并发的 {@link Long2ObjectMap} 实现(借鉴 MCMT 思路,独立重写)。
+ *
+ * {@code EntitySectionStorage.sections} 字段类型是 fastutil {@code Long2ObjectMap},mixin 无法改字段类型,
+ * 只能替换实例。fastutil 没有现成的并发 {@code Long2ObjectMap},因此继承
+ * {@link Long2ObjectOpenHashMap}(保持字段类型兼容),内部改用 {@link ConcurrentHashMap} 作为实际存储,
+ * 仅覆盖被 {@code EntitySectionStorage} 实际调用的方法(get/remove/computeIfAbsent/keySet/size/...)。
+ *
+ * 弱一致性说明:{@link #keySet()} 返回的 {@code LongSet} 是 {@link ConcurrentHashMap#keySet()} 的
+ * 弱一致视图,并发增删期间迭代不会抛 {@code ConcurrentModificationException},可安全用于
+ * {@code getAllChunksWithExistingSections} 的结构遍历。
+ *
+ * 未覆盖的方法(entrySet/long2ObjectEntrySet/values/putAll 等)会落到父类的内部 fastutil 数组(恒空),
+ * 因 {@code EntitySectionStorage} 从不对 {@code sections} 调用它们,故无影响(若未来使用需补充覆盖)。
+ */
+public final class ConcurrentLong2ObjectMap extends Long2ObjectOpenHashMap {
+ private static final long serialVersionUID = 1L;
+
+ /** 实际存储:并发哈希表。 */
+ private final Map backing = new ConcurrentHashMap<>();
+
+ @Override
+ public V get(long key) {
+ V v = backing.get(key);
+ return (v == null && !backing.containsKey(key)) ? defaultReturnValue() : v;
+ }
+
+ @Override
+ public V get(Object key) {
+ return (key instanceof Long) ? get((long) (Long) key) : defaultReturnValue();
+ }
+
+ @Override
+ public boolean containsKey(long key) {
+ return backing.containsKey(key);
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return key instanceof Long && backing.containsKey(key);
+ }
+
+ @Override
+ public V put(long key, V value) {
+ return backing.put(key, value);
+ }
+
+ @Override
+ public V put(Long key, V value) {
+ return backing.put(key, value);
+ }
+
+ @Override
+ public V remove(long key) {
+ return backing.remove(key);
+ }
+
+ @Override
+ public V remove(Object key) {
+ return backing.remove(key);
+ }
+
+ @Override
+ public boolean remove(long key, Object value) {
+ return backing.remove(key, value);
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ return key instanceof Long && backing.remove(key, value);
+ }
+
+ @Override
+ public V computeIfAbsent(long key, Long2ObjectFunction extends V> mappingFunction) {
+ V v = backing.computeIfAbsent(key, l -> mappingFunction.get(l));
+ return v == null ? defaultReturnValue() : v;
+ }
+
+ @Override
+ public V computeIfAbsent(long key, LongFunction extends V> mappingFunction) {
+ V v = backing.computeIfAbsent(key, l -> mappingFunction.apply(l));
+ return v == null ? defaultReturnValue() : v;
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return backing.isEmpty();
+ }
+
+ @Override
+ public void clear() {
+ backing.clear();
+ }
+
+ @Override
+ public LongSet keySet() {
+ return new BackedKeySet(backing.keySet());
+ }
+
+ /** {@link ConcurrentHashMap#keySet()} 的 fastutil 弱一致 {@code LongSet} 视图。 */
+ private static final class BackedKeySet extends AbstractLongSet {
+ private static final long serialVersionUID = 1L;
+
+ private final Set backing;
+
+ BackedKeySet(Set backing) {
+ this.backing = backing;
+ }
+
+ @Override
+ public LongIterator iterator() {
+ return new IteratorView(backing.iterator());
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public boolean add(long k) {
+ return backing.add(k);
+ }
+
+ @Override
+ public boolean remove(long k) {
+ return backing.remove(k);
+ }
+
+ @Override
+ public boolean contains(long k) {
+ return backing.contains(k);
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return backing.contains(o);
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return backing.remove(o);
+ }
+
+ @Override
+ public void clear() {
+ backing.clear();
+ }
+ }
+
+ /** 把 {@code java.util.Iterator} 包装成 fastutil 原语迭代器。 */
+ private static final class IteratorView implements LongIterator {
+ private final java.util.Iterator delegate;
+
+ IteratorView(java.util.Iterator delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return delegate.hasNext();
+ }
+
+ @Override
+ public long nextLong() {
+ return delegate.next();
+ }
+
+ @Override
+ public Long next() {
+ return delegate.next();
+ }
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java
new file mode 100644
index 0000000..e6c8c23
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java
@@ -0,0 +1,156 @@
+package dev.modzuozhi.core.collection;
+
+import it.unimi.dsi.fastutil.longs.AbstractLongBidirectionalIterator;
+import it.unimi.dsi.fastutil.longs.AbstractLongSortedSet;
+import it.unimi.dsi.fastutil.longs.LongBidirectionalIterator;
+import it.unimi.dsi.fastutil.longs.LongComparator;
+import it.unimi.dsi.fastutil.longs.LongSortedSet;
+
+import java.util.NavigableSet;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+/**
+ * 第11步:SECTION_LOCK → 并发集合——并发的 {@link LongSortedSet} 实现(借鉴 MCMT 思路,独立重写)。
+ *
+ * {@code EntitySectionStorage.sectionIds} 字段类型是 fastutil {@code LongSortedSet},mixin 无法改字段类型,
+ * 只能替换实例。fastutil 没有现成的并发 {@code LongSortedSet},因此实现
+ * {@link LongSortedSet},内部用 {@link ConcurrentSkipListSet}(有序、弱一致迭代、无 CME)。
+ *
+ * 与 MCMT 参考实现的关键差异:{@link #subSet}/{@link #headSet}/{@link #tailSet} 返回的是
+ * 同一 {@link ConcurrentSkipListSet} 子集视图的弱一致包装(不拷贝快照),避免每查询一次
+ * {@code forEachAccessibleNonEmptySection} 就分配一棵新 AVL 树的 GC 开销;迭代器弱一致,
+ * 并发增删期间迭代不会抛 {@code ConcurrentModificationException}。
+ *
+ * {@code EntitySectionStorage.createSection} 先 {@code sectionIds.add} 再建 {@code EntitySection},
+ * 读方在 subSet 迭代中可能遇到尚未写入 {@code sections} 的 key——原版
+ * {@code forEachAccessibleNonEmptySection} 已有 {@code sections.get(...) != null} 检查,天然安全。
+ */
+public final class ConcurrentLongSortedSet extends AbstractLongSortedSet {
+ private static final long serialVersionUID = 1L;
+
+ /** 实际存储:有序并发跳表。 */
+ private final NavigableSet back;
+
+ public ConcurrentLongSortedSet() {
+ this.back = new ConcurrentSkipListSet<>();
+ }
+
+ private ConcurrentLongSortedSet(NavigableSet back) {
+ this.back = back;
+ }
+
+ @Override
+ public LongBidirectionalIterator iterator() {
+ return new SortedIteratorView(back.iterator());
+ }
+
+ @Override
+ public LongBidirectionalIterator iterator(long fromElement) {
+ return new SortedIteratorView(back.tailSet(fromElement, true).iterator());
+ }
+
+ @Override
+ public int size() {
+ return back.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return back.isEmpty();
+ }
+
+ @Override
+ public boolean add(long k) {
+ return back.add(k);
+ }
+
+ @Override
+ public boolean add(Long k) {
+ return back.add(k);
+ }
+
+ @Override
+ public boolean remove(long k) {
+ return back.remove(k);
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return back.remove(o);
+ }
+
+ @Override
+ public boolean contains(long k) {
+ return back.contains(k);
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return back.contains(o);
+ }
+
+ @Override
+ public void clear() {
+ back.clear();
+ }
+
+ @Override
+ public long firstLong() {
+ return back.first();
+ }
+
+ @Override
+ public long lastLong() {
+ return back.last();
+ }
+
+ @Override
+ public LongSortedSet subSet(long fromElement, long toElement) {
+ // ConcurrentSkipListSet 的 2 参 subSet 返回 SortedSet;用 4 参重载([from, to) 语义一致)拿到 NavigableSet 视图
+ return new ConcurrentLongSortedSet(back.subSet(fromElement, true, toElement, false));
+ }
+
+ @Override
+ public LongSortedSet headSet(long toElement) {
+ return new ConcurrentLongSortedSet(back.headSet(toElement, false));
+ }
+
+ @Override
+ public LongSortedSet tailSet(long fromElement) {
+ return new ConcurrentLongSortedSet(back.tailSet(fromElement, true));
+ }
+
+ @Override
+ public LongComparator comparator() {
+ return null;
+ }
+
+ /** 把 {@code java.util.Iterator} 包装成 fastutil 双向原语迭代器(previous 不用于本集合场景)。 */
+ private static final class SortedIteratorView extends AbstractLongBidirectionalIterator {
+ private final java.util.Iterator delegate;
+
+ SortedIteratorView(java.util.Iterator delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return delegate.hasNext();
+ }
+
+ @Override
+ public long nextLong() {
+ return delegate.next();
+ }
+
+ @Override
+ public boolean hasPrevious() {
+ return false;
+ }
+
+ @Override
+ public long previousLong() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java b/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java
new file mode 100644
index 0000000..bb8f8cb
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java
@@ -0,0 +1,111 @@
+package dev.modzuozhi.core.collection;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.AbstractObjectCollection;
+import it.unimi.dsi.fastutil.objects.ObjectCollection;
+import it.unimi.dsi.fastutil.objects.ObjectIterator;
+import it.unimi.dsi.fastutil.objects.ObjectIterators;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 第14步(对齐 Async 零锁路线):并发的 {@code Int2ObjectMap} 实现(借鉴 MCMT/Async 思路,独立重写)。
+ *
+ * {@code EntityLookup.byId} 字段类型是 fastutil {@code Int2ObjectMap},mixin 无法改字段类型,
+ * 只能替换实例。fastutil 无现成并发版,因此继承 {@link Int2ObjectOpenHashMap}(保持字段类型兼容),
+ * 内部用 {@link ConcurrentHashMap} 作为实际存储,仅覆盖 {@code EntityLookup} 用到的
+ * get/put/remove/values/size 等方法。
+ *
+ * {@link #values()} 返回 {@link ConcurrentHashMap#values()} 的弱一致视图(迭代并发增删不抛 CME),
+ * 供 {@code EntityLookup.getEntities/getAllEntities} 安全遍历。
+ */
+public final class Int2ObjectConcurrentHashMap extends Int2ObjectOpenHashMap {
+ private static final long serialVersionUID = 1L;
+
+ /** 实际存储:并发哈希表。 */
+ private final Map backing = new ConcurrentHashMap<>();
+
+ @Override
+ public V get(int key) {
+ V v = backing.get(key);
+ return (v == null && !backing.containsKey(key)) ? defaultReturnValue() : v;
+ }
+
+ @Override
+ public V get(Object key) {
+ return (key instanceof Integer) ? get((int) (Integer) key) : defaultReturnValue();
+ }
+
+ @Override
+ public boolean containsKey(int key) {
+ return backing.containsKey(key);
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return key instanceof Integer && backing.containsKey(key);
+ }
+
+ @Override
+ public V put(int key, V value) {
+ return backing.put(key, value);
+ }
+
+ @Override
+ public V put(Integer key, V value) {
+ return backing.put(key, value);
+ }
+
+ @Override
+ public V remove(int key) {
+ return backing.remove(key);
+ }
+
+ @Override
+ public V remove(Object key) {
+ return backing.remove(key);
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return backing.isEmpty();
+ }
+
+ @Override
+ public void clear() {
+ backing.clear();
+ }
+
+ @Override
+ public ObjectCollection values() {
+ return new ValuesView(backing.values());
+ }
+
+ /** {@link ConcurrentHashMap#values()} 的弱一致视图(迭代安全)。 */
+ private static final class ValuesView extends AbstractObjectCollection {
+ private static final long serialVersionUID = 1L;
+
+ private final Collection backing;
+
+ ValuesView(Collection backing) {
+ this.backing = backing;
+ }
+
+ @Override
+ public ObjectIterator iterator() {
+ return ObjectIterators.asObjectIterator(backing.iterator());
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java b/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java
new file mode 100644
index 0000000..24c4387
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java
@@ -0,0 +1,95 @@
+package dev.modzuozhi.core.dimthread;
+
+import net.minecraft.core.BlockPos;
+
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 按区块坐标的细粒度可重入锁(借鉴 MCMT 的 ChunkLock 思路,写我们自己的实现)。
+ *
+ * 细粒度并行(subtask)下,多个子任务可能并发访问同一区块内的方块实体 / 方块 /
+ * getChunk / 光照等结构。用「区块坐标 → {@link ReentrantLock}」做互斥:同一区块内的子任务
+ * 串行,不同区块并行,既保证安全又保留并行度。
+ *
+ * 第5步(6b):radius 多区块锁。某些子任务会跨区块访问(如破坏方块、爆炸、
+ * 大型实体),单区块锁不足。提供 {@link #lock(BlockPos, int)} / {@link #lock(int, int, int)},
+ * 锁定以目标区块为中心 {@code (1+2r)²} 个区块的正方形范围:先对目标区块坐标排序
+ * 再逐个加锁(排序保证多线程以相同顺序取锁,杜绝循环等待死锁),解锁时逆序释放。
+ * 仍保留单区块 {@link #lock(BlockPos)} 供普通子任务使用(等价 radius=0)。
+ *
+ * 线程安全:锁表用 {@link ConcurrentHashMap},{@code computeIfAbsent} 原子取锁,避免并发创建。
+ * 锁按需惰性创建;为控制内存,锁数量与活跃区块数同量级,可接受(如需要可在后续版本加入过期回收)。
+ */
+public final class ChunkLock {
+ private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>();
+
+ private ChunkLock() {
+ }
+
+ /** 锁定包含该世界坐标的区块(返回后必须 {@code close()} 释放)。 */
+ public static AutoCloseable lock(BlockPos pos) {
+ return lock(pos.getX() >> 4, pos.getZ() >> 4);
+ }
+
+ /** 锁定指定区块(返回后必须 {@code close()} 释放)。 */
+ public static AutoCloseable lock(int chunkX, int chunkZ) {
+ long key = ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL);
+ ReentrantLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantLock());
+ lock.lock();
+ return lock::unlock;
+ }
+
+ /**
+ * 锁定包含该世界坐标区块及其周围 radius 圈的区块({@code (1+2r)²} 个)。
+ * 排序加锁、逆序释放,防死锁。返回的 {@link ChunkLockHandle} 用后必须 {@code close()}。
+ *
+ * @param radius 区块半径(0=仅本区块,1=3×3=9 个区块)
+ */
+ public static ChunkLockHandle lock(BlockPos pos, int radius) {
+ return lock(pos.getX() >> 4, pos.getZ() >> 4, radius);
+ }
+
+ /**
+ * 锁定指定区块及其周围 radius 圈的区块({@code (1+2r)²} 个)。
+ * 排序加锁、逆序释放,防死锁。返回的 {@link ChunkLockHandle} 用后必须 {@code close()}。
+ */
+ public static ChunkLockHandle lock(int chunkX, int chunkZ, int radius) {
+ long[] targets = new long[(1 + radius * 2) * (1 + radius * 2)];
+ int idx = 0;
+ for (int dx = -radius; dx <= radius; dx++) {
+ for (int dz = -radius; dz <= radius; dz++) {
+ targets[idx++] = ((long) (chunkX + dx) << 32) ^ ((chunkZ + dz) & 0xFFFFFFFFL);
+ }
+ }
+ // 排序保证所有线程以相同顺序取锁,避免循环等待死锁
+ Arrays.sort(targets);
+ for (long key : targets) {
+ LOCKS.computeIfAbsent(key, k -> new ReentrantLock()).lock();
+ }
+ return new ChunkLockHandle(targets);
+ }
+
+ /**
+ * 多区块锁的句柄:持有已加锁的区块 key 数组,{@link #close()} 逆序释放。
+ */
+ public static final class ChunkLockHandle implements AutoCloseable {
+ private final long[] targets;
+
+ private ChunkLockHandle(long[] targets) {
+ this.targets = targets;
+ }
+
+ @Override
+ public void close() {
+ // 逆序释放(与加锁方向相反)
+ for (int i = targets.length - 1; i >= 0; i--) {
+ ReentrantLock lock = LOCKS.get(targets[i]);
+ if (lock != null) {
+ lock.unlock();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java b/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java
new file mode 100644
index 0000000..89ebc2a
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java
@@ -0,0 +1,174 @@
+package dev.modzuozhi.core.dimthread;
+
+import com.mojang.logging.LogUtils;
+import dev.modzuozhi.ModZuozhi;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.Unit;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 阶段 A(步骤 0):自适应区块预取。
+ *
+ * 原理:在玩家 tick(主线程 {@code playerList.tick()} → {@code ServerPlayer.doTick()})时,
+ * 根据玩家移动方向,对前方尚未加载的区块投递一个临时的 FULL 级 ticket,让原版
+ * 区块生成管线提前把该区块生成好;玩家到达时直接命中已生成缓存,无需现场等待。
+ *
+ * 安全 / 无泄漏:
+ *
+ * - 使用自定义 {@link TicketType}({@code timeout}=10s),到期由
+ * {@code DistanceManager.purgeStaleTickets()} 自动回收,无需手动 remove,不存在 ticket 泄漏;
+ * - 只在玩家实际移动时、且方向可确定时才预取,静止/回退不产生任何 ticket;
+ * - 每玩家每 {@link #PREFETCH_INTERVAL_TICKS} 才重算一次,避免逐 tick 开销;
+ * - 已 FULL 的区块直接跳过({@code hasChunk}),不重复下 ticket;
+ * - 全流程 try/catch,预取任何异常都不影响玩家 tick(纯增益,可安全回退)。
+ *
+ *
+ * 不碰生成管线、不碰地形生成 mod:这里只负责"提前下票触发原版生成",生成逻辑完全交给原版。
+ */
+public final class ChunkPrefetcher {
+
+ /** 预取总开关(与 {@link ModZuozhi#isEnabled()} 叠加,一键回退)。 */
+ public static volatile boolean ENABLED = true;
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ /** 预取 ticket 存活时长(tick),到期自动回收。10s 覆盖通常的前视时间窗。 */
+ private static final int PREFETCH_TIMEOUT_TICKS = 200;
+
+ /** 每次重算至少要移动这么多格(平方距离)才触发,过滤原地抖动。 */
+ private static final double MIN_MOVE_SQ = 1.0;
+
+ /** 预取重算间隔(tick),降低每个玩家的逐 tick 开销。 */
+ private static final int PREFETCH_INTERVAL_TICKS = 5;
+
+ /** 预取深度:在玩家视距之外,再向前预取的区块数(收敛为 4~6 格,避免过度预取浪费算力)。 */
+ private static final int PREFETCH_DEPTH = 6;
+
+ /** 自定义预取 ticket:FULL 级别 + 有限存活时间。 */
+ private static final TicketType PREFETCH_TICKET =
+ TicketType.create("dimensionalripper_prefetch", (a, b) -> 0, PREFETCH_TIMEOUT_TICKS);
+
+ /** 每玩家轻量状态(上次位置 / 上次所在 chunk / 上次重算游戏刻)。 */
+ private static final ConcurrentHashMap STATES = new ConcurrentHashMap<>();
+
+ /** 状态表容量兜底,超出即整体清空(丢失仅损失一次方向估计,无害)。 */
+ private static final int STATE_CAPACITY = 512;
+
+ private ChunkPrefetcher() {
+ }
+
+ /** 每次玩家 tick 调用一次(主线程)。 */
+ public static void onPlayerTick(ServerPlayer player) {
+ if (!ModZuozhi.isEnabled() || !ENABLED) {
+ return;
+ }
+ try {
+ tick(player);
+ } catch (Throwable t) {
+ // 预取是纯增益:任何异常都不应影响玩家 tick 或服务器稳定性
+ }
+ }
+
+ private static void tick(ServerPlayer player) {
+ ServerLevel level = player.serverLevel();
+ ServerChunkCache source = level.getChunkSource();
+
+ State state = STATES.computeIfAbsent(player.getUUID(), u -> new State());
+ if (STATES.size() > STATE_CAPACITY) {
+ STATES.clear();
+ }
+
+ // 首次记录位置快照,不产生方向估计
+ if (!state.initialized) {
+ state.init(player, level.getGameTime());
+ return;
+ }
+
+ long gameTime = level.getGameTime();
+ if (gameTime - state.lastGameTime < PREFETCH_INTERVAL_TICKS) {
+ return;
+ }
+ state.lastGameTime = gameTime;
+
+ double x = player.getX();
+ double z = player.getZ();
+ double dx = x - state.lastX;
+ double dz = z - state.lastZ;
+ state.lastX = x;
+ state.lastZ = z;
+
+ if (dx * dx + dz * dz < MIN_MOVE_SQ) {
+ return; // 原地不动,不预取
+ }
+
+ ChunkPos chunk = player.chunkPosition();
+ int dirX = Integer.compare(chunk.x, state.lastChunkX);
+ int dirZ = Integer.compare(chunk.z, state.lastChunkZ);
+ state.lastChunkX = chunk.x;
+ state.lastChunkZ = chunk.z;
+
+ if (dirX == 0 && dirZ == 0) {
+ return; // 尚未跨 chunk,方向不可定
+ }
+
+ // 原版把区块生成到 FULL(完整)的半径由“视距 viewDistance”决定,不是模拟距离 simDistance。
+ // 因此只对【视距外 1~PREFETCH_DEPTH 格】的固定窗口投递临时 ticket:视距内的区块已被玩家
+ // 自身 ticket 加载到 FULL(hasChunk 恒真、下不出去票),窗口内已 FULL 的跳过即可。
+ // 固定窗口(不再向前扫描)是为了把预取前沿钉死在 view+PREFETCH_DEPTH,避免生成速度过快时
+ // 前沿被自己持续向前推进到十几二十格外,导致无谓的算力浪费。
+ int viewDist = level.getServer().getPlayerList().getViewDistance();
+ int simDist = level.getServer().getPlayerList().getSimulationDistance();
+ int from = viewDist + 1;
+ int to = viewDist + PREFETCH_DEPTH;
+ int prefetched = 0;
+ for (int k = from; k <= to; k++) {
+ if (prefetch(source, chunk.x + dirX * k, chunk.z + dirZ * k)) {
+ prefetched++;
+ }
+ }
+ if (state.firstLog || gameTime - state.lastLogGameTime >= 20) {
+ state.firstLog = false;
+ state.lastLogGameTime = gameTime;
+ LOGGER.info("[ChunkPrefetcher] 玩家 {} 方向 ({},{}), view={}, sim={}, 预取区间 [{}~{}], 投递 {} 个ticket",
+ player.getGameProfile().getName(), dirX, dirZ, viewDist, simDist, from, to, prefetched);
+ }
+ }
+
+ private static boolean prefetch(ServerChunkCache source, int chunkX, int chunkZ) {
+ if (source.hasChunk(chunkX, chunkZ)) {
+ return false; // 已 FULL 加载,无需预取
+ }
+ // radius=0 → FULL 级别 ticket;到期自动回收,不阻塞、不泄漏
+ source.addRegionTicket(PREFETCH_TICKET, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE);
+ return true;
+ }
+
+ private static final class State {
+ boolean initialized;
+ double lastX;
+ double lastZ;
+ int lastChunkX;
+ int lastChunkZ;
+ long lastGameTime;
+ long lastLogGameTime;
+ boolean firstLog;
+
+ void init(ServerPlayer player, long gameTime) {
+ this.initialized = true;
+ this.lastX = player.getX();
+ this.lastZ = player.getZ();
+ ChunkPos c = player.chunkPosition();
+ this.lastChunkX = c.x;
+ this.lastChunkZ = c.z;
+ this.lastGameTime = gameTime;
+ this.firstLog = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java b/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java
new file mode 100644
index 0000000..ae91740
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java
@@ -0,0 +1,63 @@
+package dev.modzuozhi.core.dimthread;
+
+import dev.modzuozhi.ModZuozhi;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import org.slf4j.Logger;
+
+/**
+ * dimthreads 式核心入口:临时主线程交换 + worker 识别 + 线程池托管。
+ *
+ * 核心思想:不固定"每维度专属线程",而是所有维度共用一个线程池,主线程每 tick 把
+ * 每个 {@code level.tick()} 分发到池中并行,然后 {@code awaitCompletion()} 等待全部完成。
+ * 运行某维度 tick 前,将该维度的 {@code thread} 与 {@code chunkSource.mainThread} 临时
+ * 设为当前 worker,使原版线程检查自然通过,从根上消除 supplyAsync/join 死锁。
+ */
+public final class DimThreadCore {
+ public static final String MOD_ID = ModZuozhi.MOD_ID;
+ public static final Logger LOGGER = ModZuozhi.LOGGER;
+ public static final ServerManager MANAGER = new ServerManager();
+
+ private DimThreadCore() {
+ }
+
+ public static ThreadPool getThreadPool(MinecraftServer server) {
+ return MANAGER.getThreadPool(server);
+ }
+
+ /**
+ * 临时把一批对象(Level / ServerChunkCache)的 mainThread 换成当前线程,执行任务后还原。
+ * 必须在 try/finally 中调用,保证 worker 线程异常时也能还原。
+ */
+ public static void swapThreadsAndRun(Runnable task, Object... threadedObjects) {
+ Thread currentThread = Thread.currentThread();
+ Thread[] oldThreads = new Thread[threadedObjects.length];
+
+ for (int i = 0; i < oldThreads.length; i++) {
+ oldThreads[i] = ((IMutableMainThread) threadedObjects[i]).dimThreads$getMainThread();
+ ((IMutableMainThread) threadedObjects[i]).dimThreads$setMainThread(currentThread);
+ }
+
+ try {
+ task.run();
+ } finally {
+ for (int i = 0; i < oldThreads.length; i++) {
+ ((IMutableMainThread) threadedObjects[i]).dimThreads$setMainThread(oldThreads[i]);
+ }
+ }
+ }
+
+ /** 给 worker 线程命名,便于在崩溃报告中识别。 */
+ public static void attach(Thread thread, String name) {
+ thread.setName(MOD_ID + "_server_" + name);
+ }
+
+ public static void attach(Thread thread, ServerLevel world) {
+ attach(thread, world.dimension().location().getPath());
+ }
+
+ /** 判断线程是否为维度 worker(通过名字前缀识别)。 */
+ public static boolean owns(Thread thread) {
+ return thread.getName().startsWith(MOD_ID + "_server_");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java
new file mode 100644
index 0000000..59f4929
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java
@@ -0,0 +1,57 @@
+package dev.modzuozhi.core.dimthread;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.portal.DimensionTransition;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * 跨维度传送延期队列:维度 worker 上触发的 {@code changeDimension} 由本队列承接,
+ * 不依赖 {@code server.execute()}(跨 tick 消费),而是由主线程在维度 tick 收口后、
+ * {@code playerList.tick()} 之前统一处理,保证传送在同一 tick 内完成。
+ *
+ * 线程安全:维度 worker 写({@link #schedule}),主线程读({@link #flush}),
+ * 且主线程 flush 时所有维度 worker 已 awaitCompletion 完成,无竞争。
+ */
+public final class DimensionChangeQueue {
+ private static final Deque queue = new ArrayDeque<>();
+
+ private DimensionChangeQueue() {
+ }
+
+ /** 入队一个待执行的维度变更(由维度 worker 调用)。 */
+ public static void schedule(Entity entity, DimensionTransition transition) {
+ queue.add(new PendingChange(entity, transition));
+ }
+
+ /** 是否有待处理的维度变更。 */
+ public static boolean hasPending() {
+ return !queue.isEmpty();
+ }
+
+ /**
+ * 在主线程上执行所有排队的维度变更(由 {@code tickChildren} 收口处调用)。
+ *
+ * 直接调用 {@link Entity#changeDimension},该调用在主线程上执行时不会被
+ * {@code EntityChangeDimensionMixin} 拦截(主线程不是维度 worker),
+ * 完整的维度变更逻辑(移除旧维度、添加新维度、发数据包)同步完成。
+ */
+ @Nullable
+ public static Entity flush() {
+ PendingChange change;
+ Entity result = null;
+ while ((change = queue.pollFirst()) != null) {
+ try {
+ result = change.entity.changeDimension(change.transition);
+ } catch (Throwable t) {
+ DimThreadCore.LOGGER.error("[DimensionChangeQueue] 维度变更异常", t);
+ }
+ }
+ return result;
+ }
+
+ private record PendingChange(Entity entity, DimensionTransition transition) {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java
new file mode 100644
index 0000000..9cf868a
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java
@@ -0,0 +1,189 @@
+package dev.modzuozhi.core.dimthread;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.entity.Entity;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+
+/**
+ * 实体 tick 细粒度并行的上下文管理(借鉴 MCMT/Async 思路,写我们自己的实现)。
+ *
+ * 实体 tick 并行与方块实体/区块环境并行的关键区别:{@code ServerLevel.tick} 里对每个实体
+ * 的 {@code guardEntityTick(...)} 调用位于 {@code entityTickList.forEach(...)} 的 lambda 内部,
+ * 无法像 TE/区块那样在循环体内 {@code @Redirect}。因此这里用 ThreadLocal 屏障在
+ * {@code ServerLevelEntityTickMixin} 的 {@code forEach} 前后建立/收口,再由
+ * {@code EntityTickParallelMixin} 在 {@code guardEntityTick} 处读取当前屏障并提交并行。
+ *
+ * 第14步(对齐 Async 零锁路线):实体移动、生成、移除的共享状态({@code EntityLookup}、
+ * {@code knownUuids}、{@code chunkVisibility}、section 存储)已全部并发化,不再需要全局锁
+ * {@link #SECTION_LOCK} 保护。实体生命周期写入现在无锁并行,碰撞查询也走并行。
+ * 仅保留 {@code EntitySection} 内容({@code ClassInstanceMultiMap})的分片读写锁保护内容遍历
+ * 与增删的互斥(Async 未去掉分片锁,保留为安全边界;分片竞争在 256 片下极低,不是瓶颈)。
+ */
+public final class EntityTickParallel {
+ /** 当前维度 worker 线程正在并行 tick 的实体屏障(仅 worker 线程非 null)。 */
+ private static final ThreadLocal CURRENT = new ThreadLocal<>();
+
+ /** 本 tick 收集的待并行实体批(仅并行窗口 worker 线程非 null)。 */
+ private static final ThreadLocal BATCH = new ThreadLocal<>();
+
+ /** 分片锁数量(2 的幂)。越大锁竞争越小,内存开销约 256×对象头,可忽略。 */
+ private static final int STRIPE_COUNT = 256;
+ /** 分片读写锁数组:保护单个 {@code EntitySection} 的内容读写({@code ClassInstanceMultiMap})。 */
+ private static final ReentrantReadWriteLock[] SECTION_LOCKS = new ReentrantReadWriteLock[STRIPE_COUNT];
+
+ /**
+ * 当前线程的锁配对栈:记录每次 lock 调用时是否真正加了锁,unlock 时弹出对应决定。
+ *
+ * 背景:{@link #isLockDisabled()} 在「细粒度关闭且无残留子任务」时为 true(零锁纯串行)。
+ * 但 {@code /dimensionalripper fine on|off} 运行期切换瞬间,该判定会在一次 lock→unlock 之间翻转:
+ * lock 时跳过加锁、unlock 时却执行解锁 → 对未持有的锁 unlock,抛
+ * {@code IllegalMonitorStateException: attempt to unlock read lock}(实测:大量村民 tick 时
+ * 执行 {@code /dimensionalripper fine on} 触发)。用本栈把 lock 时的决定记录下来,unlock 严格按
+ * 记录配对(支持嵌套,LIFO),无论 {@code isLockDisabled()} 是否翻转都保持一致。
+ */
+ private static final ThreadLocal> LOCK_STACK = ThreadLocal.withInitial(ArrayDeque::new);
+
+ static {
+ for (int i = 0; i < STRIPE_COUNT; i++) {
+ SECTION_LOCKS[i] = new ReentrantReadWriteLock();
+ }
+ }
+
+ private EntityTickParallel() {
+ }
+
+ /**
+ * 将 section key({@code SectionPos.asLong(x,y,z)})映射到分片索引。
+ * 使用乘散列取高位,保证相邻 section 均匀分布到不同分片。
+ */
+ private static int stripe(long sectionKey) {
+ long hash = sectionKey * 0x9E3779B97F4A7C15L;
+ return (int) (hash >>> 32) & (STRIPE_COUNT - 1);
+ }
+
+ /** 对指定 section 分片加写锁(内容增删,独占);记录配对决定。 */
+ public static void lockSection(long sectionKey) {
+ boolean locked = !isLockDisabled();
+ LOCK_STACK.get().push(locked);
+ if (locked) {
+ SECTION_LOCKS[stripe(sectionKey)].writeLock().lock();
+ }
+ }
+
+ /** 对指定 section 分片解写锁;严格按 lock 时的配对决定解锁。 */
+ public static void unlockSection(long sectionKey) {
+ if (popLocked()) {
+ SECTION_LOCKS[stripe(sectionKey)].writeLock().unlock();
+ }
+ }
+
+ /** 对指定 section 分片加读锁(内容遍历/查询,共享);记录配对决定。 */
+ public static void lockSectionRead(long sectionKey) {
+ boolean locked = !isLockDisabled();
+ LOCK_STACK.get().push(locked);
+ if (locked) {
+ SECTION_LOCKS[stripe(sectionKey)].readLock().lock();
+ }
+ }
+
+ /** 对指定 section 分片解读锁;严格按 lock 时的配对决定解锁。 */
+ public static void unlockSectionRead(long sectionKey) {
+ if (popLocked()) {
+ SECTION_LOCKS[stripe(sectionKey)].readLock().unlock();
+ }
+ }
+
+ /** 弹出本线程最近一次 lock 的配对决定(LIFO);栈空视为未加锁(防御)。 */
+ private static boolean popLocked() {
+ Deque stack = LOCK_STACK.get();
+ Boolean locked = stack.pollFirst();
+ if (stack.isEmpty()) {
+ LOCK_STACK.remove();
+ }
+ return Boolean.TRUE.equals(locked);
+ }
+
+ /**
+ * 是否应跳过锁(零开销纯串行)。
+ *
+ * 锁的唯一目的是保护并行子任务对 {@code EntitySection} 的并发增删。而子任务是否
+ * 并行由 {@link FineGrainScheduler#isEnabled()} 决定({@code submitBatch} 关闭时同步串行)。
+ * 因此锁的启用必须与细粒度并行开关一致:若这里跟随全局 {@code ModZuozhi.ENABLED},
+ * 会出现"全局 off → 锁被跳过,但细粒度仍开 → 子任务并行增删而主线程无锁遍历"的
+ * {@code ConcurrentModificationException} 竞态(已实测崩溃)。
+ *
+ * 残留保护:关闭细粒度({@code /dimensionalripper fine off}、{@code /dimensionalripper off}、
+ * FaultGuard 降级)瞬间,{@code SUB_POOL} 里可能仍有已提交未跑完的实体子任务;
+ * 它们与退化为串行的维度 tick / 主线程并发改 {@code EntitySection}。因此只要还存在残留
+ * 子任务({@link FineGrainScheduler#activeSubTasks()} > 0),锁就必须保持启用,直到
+ * {@code awaitIdle} 把它们全部排空后才回到零锁纯串行。
+ *
+ * 注意:本判定可能随时间变化({@code fine on|off} 切换)。lock/unlock 的配对一致性
+ * 由 {@link #LOCK_STACK} 保证——lock 时快照本判定,unlock 严格按快照执行,不会因判定
+ * 在两次调用间翻转而对未持有的锁 unlock。
+ */
+ public static boolean isLockDisabled() {
+ return !FineGrainScheduler.isEnabled() && FineGrainScheduler.activeSubTasks() == 0;
+ }
+
+ /** 当前线程的实体屏障(worker 端);无则 null。 */
+ public static FineGrainScheduler.Barrier current() {
+ return CURRENT.get();
+ }
+
+ /**
+ * 本 tick 收集的一批待并行实体(同一 consumer,即 {@code tickNonPassenger})。
+ * 收集代替逐实体提交:循环结束后统一按线程池大小切成块提交,把每实体一次的
+ * 调度/屏障计数开销从 O(实体数) 降到 O(线程数)。
+ */
+ public static final class Batch {
+ public final MinecraftServer server;
+ public final Consumer consumer;
+ public final List entities = new ArrayList<>();
+
+ public Batch(MinecraftServer server, Consumer consumer) {
+ this.server = server;
+ this.consumer = consumer;
+ }
+ }
+
+ /** 收集一个待并行 tick 的实体(在 {@code guardEntityTick} 中调用,替代逐实体提交)。 */
+ public static void collect(MinecraftServer server, Consumer consumer, Entity entity) {
+ Batch batch = BATCH.get();
+ if (batch == null) {
+ batch = new Batch(server, consumer);
+ BATCH.set(batch);
+ }
+ batch.entities.add(entity);
+ }
+
+ /** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */
+ public static void begin() {
+ CURRENT.set(FineGrainScheduler.beginBarrier());
+ }
+
+ /**
+ * 实体循环结束(finally 中调用):先把收集的实体批按块提交并行,再等待全部子任务完成
+ * 并执行延迟回调,清除上下文。
+ */
+ public static void end() {
+ FineGrainScheduler.Barrier barrier = CURRENT.get();
+ if (barrier == null) {
+ return;
+ }
+ CURRENT.remove();
+ Batch batch = BATCH.get();
+ BATCH.remove();
+ if (batch != null && !batch.entities.isEmpty()) {
+ barrier.submitBatch(batch.server, batch.entities, batch.consumer);
+ }
+ barrier.await();
+ barrier.drain();
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java b/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java
new file mode 100644
index 0000000..3dcdd8f
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java
@@ -0,0 +1,148 @@
+package dev.modzuozhi.core.dimthread;
+
+import dev.modzuozhi.ModZuozhi;
+
+/**
+ * 第9步后半:故障监控与自动回退(fail-safe / graceful degradation)。
+ *
+ * 目标:运行期检测到并行故障时安全降级而非崩溃。采用两级回退,先关细粒度、再退回原版串行:
+ *
+ * - 细粒度降级:子任务连续异常达到 {@link #FINE_FAULT_THRESHOLD} 次 → 自动关闭
+ * {@link FineGrainScheduler}(退回纯维度并行)。子任务异常发生在独立子任务池内,不影响
+ * 维度 tick 收口,因此关闭是安全的。
+ * - 维度并行回退:维度 tick 连续异常达到 {@link #DIM_FAULT_THRESHOLD} 次,或维度 tick
+ * 连续超时(看门狗,{@link #AWAIT_TIMEOUT_MS})达到 {@link #AWAIT_TIMEOUT_THRESHOLD} 次 →
+ * 关闭全局 {@link ModZuozhi#ENABLED},退回原版串行。
+ *
+ * 计数均为连续语义(成功/按时完成会清零),容忍偶发抖动,只有持续故障才触发回退。
+ * 手动 {@code /dimensionalripper on|fine on} 重新启用时调用 {@link #reset()} 清除降级状态。
+ */
+public final class FaultGuard {
+ /** 子任务连续异常达到该次数,自动关闭细粒度并行。 */
+ private static final int FINE_FAULT_THRESHOLD = 5;
+ /** 维度 tick 连续异常达到该次数,自动回退原版串行。 */
+ private static final int DIM_FAULT_THRESHOLD = 3;
+ /** 维度 tick 连续超时达到该次数,自动回退原版串行。 */
+ private static final int AWAIT_TIMEOUT_THRESHOLD = 3;
+ /** 看门狗:维度 tick 最晚完成时限(主线程等待上限)。 */
+ public static final long AWAIT_TIMEOUT_MS = 30_000L;
+
+ /** 细粒度是否已被自动关闭。 */
+ private static volatile boolean fineDegraded = false;
+ /** 维度并行是否已被自动回退。 */
+ private static volatile boolean dimDegraded = false;
+ /** 最近的降级原因(status 命令展示)。 */
+ private static volatile String reason = "正常";
+ /** 连续失败计数(仅写入线程同步读写,读取方为 status/日志,volatile 已足够)。 */
+ private static int fineFailures;
+ private static int dimFailures;
+ private static int awaitTimeouts;
+
+ private FaultGuard() {
+ }
+
+ /** 上报一次子任务异常;达阈值自动关闭细粒度并行。 */
+ public static void onSubTaskFailure() {
+ if (fineDegraded) {
+ return;
+ }
+ fineFailures++;
+ if (fineFailures >= FINE_FAULT_THRESHOLD) {
+ fineDegraded = true;
+ reason = "子任务连续异常 " + fineFailures + " 次,已自动关闭细粒度并行(保留维度并行)";
+ ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
+ FineGrainScheduler.setEnabled(false);
+ }
+ }
+
+ /** 上报维度 tick 异常;达阈值自动回退原版串行。 */
+ public static void onDimTickFailure(Throwable t) {
+ if (dimDegraded) {
+ return;
+ }
+ dimFailures++;
+ ModZuozhi.LOGGER.error("[FaultGuard] 维度 tick 异常(第 {} 次)", dimFailures, t);
+ if (dimFailures >= DIM_FAULT_THRESHOLD) {
+ dimDegraded = true;
+ reason = "维度 tick 连续异常 " + dimFailures + " 次,已自动回退原版串行";
+ ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
+ // 关闭顺序与 setMaster 一致:先关细粒度并排空残留子任务(此时 ENABLED 仍 true,
+ // section 锁与碰撞拦截仍生效,残留任务安全跑完),再关全局,最后熄灭维度 loop
+ // (否则主线程退回原版串行 tick 时仍会经 mixin 提交并行子任务 → 双 tick/CME 竞态)。
+ FineGrainScheduler.setEnabled(false);
+ FineGrainScheduler.awaitIdle(5000L);
+ ModZuozhi.ENABLED = false;
+ DimThreadCore.MANAGER.killAllLoops();
+ }
+ }
+
+ /** 上报子任务屏障等待超时(子任务卡死/死锁):立即降级,避免维度 worker 永久阻塞导致整服冻结。 */
+ public static void onBarrierTimeout() {
+ if (dimDegraded) {
+ return;
+ }
+ dimDegraded = true;
+ reason = "子任务屏障等待超时(疑似死锁),已自动回退原版串行";
+ ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
+ // 与 onDimTickFailure 一致:先关细粒度并尽力排空残留子任务,再关全局,
+ // 最后熄灭维度 loop,释放可能被卡死子任务持有的锁,恢复主线程原版串行。
+ FineGrainScheduler.setEnabled(false);
+ FineGrainScheduler.awaitIdle(2000L);
+ ModZuozhi.ENABLED = false;
+ DimThreadCore.MANAGER.killAllLoops();
+ }
+
+ /**
+ * 看门狗:主线程等待维度 tick 完成后上报结果。
+ *
+ * @param done 是否在 {@link #AWAIT_TIMEOUT_MS} 内完成
+ * @return 本次是否触发维度并行回退(调用方需在返回 true 时等待残留 worker 收口)
+ */
+ public static boolean onAwaitCompleted(boolean done) {
+ if (dimDegraded) {
+ return false;
+ }
+ if (!done) {
+ awaitTimeouts++;
+ ModZuozhi.LOGGER.error("[FaultGuard] 维度 tick 超时(第 {} 次,>{}/tick)", awaitTimeouts, AWAIT_TIMEOUT_MS);
+ if (awaitTimeouts >= AWAIT_TIMEOUT_THRESHOLD) {
+ dimDegraded = true;
+ reason = "维度 tick 连续超时 " + awaitTimeouts + " 次,已自动回退原版串行";
+ ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
+ // 与 onDimTickFailure 一致:先关细粒度并排空残留子任务,再关全局,
+ // 避免主线程串行 tick 时仍提交并行子任务(双 tick / section 无锁遍历 CME 竞态)。
+ FineGrainScheduler.setEnabled(false);
+ FineGrainScheduler.awaitIdle(5000L);
+ ModZuozhi.ENABLED = false;
+ return true;
+ }
+ } else {
+ // 按时完成:重置连续超时计数
+ awaitTimeouts = 0;
+ }
+ return false;
+ }
+
+ public static boolean isFineDegraded() {
+ return fineDegraded;
+ }
+
+ public static boolean isDimDegraded() {
+ return dimDegraded;
+ }
+
+ public static String getReason() {
+ return reason;
+ }
+
+ /** 手动重新启用(/dimensionalripper on|fine on)时重置所有降级状态与计数。 */
+ public static void reset() {
+ fineDegraded = false;
+ dimDegraded = false;
+ reason = "正常";
+ fineFailures = 0;
+ dimFailures = 0;
+ awaitTimeouts = 0;
+ ModZuozhi.LOGGER.info("[FaultGuard] 降级状态已重置");
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java
new file mode 100644
index 0000000..b241a4d
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java
@@ -0,0 +1,304 @@
+package dev.modzuozhi.core.dimthread;
+
+import dev.modzuozhi.ModZuozhi;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.entity.Entity;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Consumer;
+
+import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID;
+
+/**
+ * 细粒度并行调度器(借鉴 MCMT 思路,写我们自己的实现)。
+ *
+ * 在现有 dimthreads 式「维度级并行」基础上,提供维度内更细粒度并行的能力:
+ * 维度 worker 在本维度的 tick 内部,把可并行的子单元(方块实体、区块环境、白名单实体)
+ * 提交到独立的细粒度子任务池并行执行,再用本维度自己的屏障 {@link Barrier} 收口,
+ * 保持该维度 tick 内部的顺序边界,同时兼容外层主线程的 {@code awaitCompletion()} 收口。
+ *
+ * 设计要点:
+ *
+ * - 独立线程池:子任务使用 {@link #SUB_POOL} 专用池,与维度 worker 池解耦。
+ * 否则"维度 worker 阻塞 await 等待子任务"会占满维度池线程,导致子任务永远无人执行
+ * 而死锁(这也是之前必现死锁的根因)。独立池固定 CPU 线程,从结构上不可能死锁。
+ * - 独立开关:{@link #isEnabled()} 默认关闭(保守)。关闭时 {@link Barrier#submit}
+ * 直接同步执行,行为完全退化为串行,绝不引入并发风险;由 {@code /dimensionalripper fine on|off}
+ * 运行时切换。
+ * - 异常隔离:子任务异常只记录日志,不中断本维度 tick,也不污染外层维度收口。
+ *
+ */
+public final class FineGrainScheduler {
+
+ /**
+ * 细粒度并行总开关(默认开启,已长时间测试稳定;出错时 {@link FaultGuard} 会自动降级关闭)。
+ * 可由 {@code /dimensionalripper fine on|off} 运行时切换。
+ */
+ private static volatile boolean enabled = true;
+
+ /** 细粒度子任务的独立线程池(daemon,固定 CPU 线程数)。 */
+ private static final ThreadPoolExecutor SUB_POOL = new ThreadPoolExecutor(
+ Math.max(2, Runtime.getRuntime().availableProcessors()),
+ Math.max(2, Runtime.getRuntime().availableProcessors()),
+ 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ t.setName(MOD_ID + "_subtask");
+ return t;
+ });
+
+ /** 当前并行子任务的延迟执行队列(仅并行子任务线程内非 null)。 */
+ private static final ThreadLocal CURRENT_QUEUE = new ThreadLocal<>();
+
+ /**
+ * 正在 {@link #SUB_POOL} 中运行或排队的子任务总数(含残留排空期)。
+ *
+ * 用于两点:
+ *
+ * - {@link #awaitIdle} 判断是否排空(比 {@code getActiveCount()+queue} 更准,覆盖排队任务);
+ * - {@link EntityTickParallel#isLockDisabled} 在「细粒度已关但仍有残留子任务」期间保持
+ * 分片锁启用,保证残留子任务与退化为串行的维度 tick 并发改 {@code EntitySection} 时仍互斥,
+ * 否则 `fine off`/`off` 瞬间锁被禁用 + 残留子任务无锁运行 → CME(已实测)。
+ *
+ */
+ private static final java.util.concurrent.atomic.AtomicInteger ACTIVE_SUBTASKS =
+ new java.util.concurrent.atomic.AtomicInteger();
+
+ private FineGrainScheduler() {
+ }
+
+ /** 当前仍在运行/排队的子任务数(关闭细粒度后的残留排空期也计入)。 */
+ public static int activeSubTasks() {
+ return ACTIVE_SUBTASKS.get();
+ }
+
+ public static boolean isEnabled() {
+ return enabled;
+ }
+
+ public static void setEnabled(boolean value) {
+ enabled = value;
+ // 子任务使用独立 SUB_POOL,无需再扩容维度线程池;只需让线程数目标在下次 tick 重算。
+ DimThreadCore.MANAGER.invalidateThreadCount();
+ ModZuozhi.LOGGER.info("[FineGrain] 细粒度并行已切换为 {}", value ? "开启" : "关闭");
+ }
+
+ /** 是否处于某个并行子任务上下文(用于拦截跨线程的 Level 结构写入)。 */
+ public static boolean inSubTask() {
+ return CURRENT_QUEUE.get() != null;
+ }
+
+ /**
+ * 等待已提交的细粒度子任务全部完成(全局关闭时调用,带超时兜底)。
+ *
+ * 目的:{@code /dimensionalripper off} 或自动降级后,{@link #SUB_POOL} 里可能残留上一拍提交、
+ * 尚未跑完的实体子任务;此时主线程已退回原版串行 tick 同一批实体,会与残留子任务
+ * 并发 tick 同一实体导致异常(表现为"关闭时反复出现子任务异常")。先排空
+ * SUB_POOL 再让主线程接管,消除该竞态。超时则强制继续,避免异常场景下永久阻塞。
+ */
+ public static void awaitIdle(long timeoutMs) {
+ long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+ while (System.nanoTime() < deadline) {
+ if (ACTIVE_SUBTASKS.get() == 0) {
+ return;
+ }
+ LockSupport.parkNanos(100_000L); // 0.1ms 退避
+ }
+ ModZuozhi.LOGGER.warn("[FineGrain] 等待子任务排空超时(>{}ms),强制继续", timeoutMs);
+ }
+
+ /** 当前并行子任务的延迟执行队列;非子任务上下文返回 null。 */
+ public static PostExecuteQueue currentQueue() {
+ return CURRENT_QUEUE.get();
+ }
+
+ /** 为本维度 tick 创建一个新的子任务屏障。 */
+ public static Barrier beginBarrier() {
+ return new Barrier();
+ }
+
+ /**
+ * 维度内子任务屏障:提交多个可并行子任务,{@link #await()} 等待全部完成。
+ *
+ * 仅在 {@link FineGrainScheduler#isEnabled()} 为 true 时真正并行;关闭时
+ * {@link #submit} 直接在当前线程同步执行,保证开关无关的正确性。
+ */
+ public static final class Barrier {
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private final PostExecuteQueue postQueue = new PostExecuteQueue();
+ private int pending;
+
+ /**
+ * 子任务屏障等待超时(毫秒)。一批子任务(方块实体/区块/实体)正常应在几十 ms 内完成,
+ * 超时意味着某个子任务卡死(例如阻塞在另一个线程持有的 {@link ChunkLock} 或 section 锁上)。
+ * 此时无条件 {@code latch.await()} 会让维度 worker 永久阻塞,进而死锁主线程保存流程。
+ */
+ private static final long AWAIT_TIMEOUT_MS = 10_000L;
+
+ /**
+ * 提交一个细粒度子任务。
+ *
+ * @param server 当前服务器(用于取线程池)
+ * @param task 子任务(如某个方块实体/实体的 tick)
+ */
+ public void submit(MinecraftServer server, Runnable task) {
+ submit(server, task, null);
+ }
+
+ /**
+ * 提交一个细粒度子任务,可选地锁定其所在区块(防止同一区块内并发访问)。
+ *
+ * @param server 当前服务器(保留参数,当前子任务使用独立线程池)
+ * @param task 子任务(如某个方块实体/实体的 tick)
+ * @param pos 子任务所在世界坐标;非 null 时执行前锁定所在区块
+ */
+ public void submit(MinecraftServer server, Runnable task, BlockPos pos) {
+ submit(server, task, pos, 0);
+ }
+
+ /**
+ * 提交一个细粒度子任务,可选地锁定其所在区块及其周围 radius 圈区块。
+ *
+ * @param server 当前服务器(保留参数,当前子任务使用独立线程池)
+ * @param task 子任务(如某个方块实体/实体的 tick)
+ * @param pos 子任务所在世界坐标;非 null 时执行前锁定区块
+ * @param radius 区块锁半径:0=仅本区块;>0 时锁定 {@code (1+2r)²} 个区块
+ * (排序加锁防死锁),供跨区块访问的子任务使用
+ */
+ public void submit(MinecraftServer server, Runnable task, BlockPos pos, int radius) {
+ if (!FineGrainScheduler.enabled) {
+ task.run();
+ return;
+ }
+ synchronized (this) {
+ pending++;
+ }
+ ACTIVE_SUBTASKS.incrementAndGet();
+ SUB_POOL.execute(() -> {
+ AutoCloseable chunkLock = (pos == null)
+ ? null
+ : (radius > 0 ? ChunkLock.lock(pos, radius) : ChunkLock.lock(pos));
+ PostExecuteQueue prev = CURRENT_QUEUE.get();
+ CURRENT_QUEUE.set(postQueue);
+ try {
+ task.run();
+ } catch (Throwable t) {
+ ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t);
+ FaultGuard.onSubTaskFailure();
+ } finally {
+ CURRENT_QUEUE.set(prev);
+ if (chunkLock != null) {
+ try {
+ chunkLock.close();
+ } catch (Exception ignore) {
+ }
+ }
+ synchronized (this) {
+ pending--;
+ if (pending == 0) {
+ latch.countDown();
+ }
+ }
+ ACTIVE_SUBTASKS.decrementAndGet();
+ }
+ });
+ }
+
+ /**
+ * 批量提交待并行实体(借鉴 Async 批量化思路,独立实现)。
+ *
+ * 按线程池大小把实体列表切成块,每块作为一个子任务提交并行执行,把调度/屏障计数
+ * 开销从 O(实体数) 降到 O(线程数)。块内逐实体 tick 并保持异常隔离。
+ *
+ * @param server 当前服务器
+ * @param entities 待并行实体列表
+ * @param consumer 实体 tick 函数(如 {@code tickNonPassenger})
+ */
+ public void submitBatch(MinecraftServer server, List entities, Consumer consumer) {
+ if (entities.isEmpty()) {
+ return;
+ }
+ if (!FineGrainScheduler.enabled) {
+ // 细粒度关闭:当前线程逐实体串行 tick(行为等价原版)
+ for (Entity entity : entities) {
+ if (!entity.isRemoved()) {
+ consumer.accept(entity);
+ }
+ }
+ return;
+ }
+ int poolSize = SUB_POOL.getCorePoolSize();
+ int chunkSize = Math.max(1, (entities.size() + poolSize - 1) / poolSize);
+ int chunkCount = (entities.size() + chunkSize - 1) / chunkSize;
+ synchronized (this) {
+ pending += chunkCount;
+ }
+ ACTIVE_SUBTASKS.addAndGet(chunkCount);
+ for (int i = 0; i < entities.size(); i += chunkSize) {
+ int end = Math.min(i + chunkSize, entities.size());
+ List chunk = entities.subList(i, end);
+ SUB_POOL.execute(() -> modzuozhi_runChunk(chunk, consumer));
+ }
+ }
+
+ /** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */
+ private void modzuozhi_runChunk(List chunk, Consumer consumer) {
+ PostExecuteQueue prev = CURRENT_QUEUE.get();
+ CURRENT_QUEUE.set(postQueue);
+ try {
+ for (Entity entity : chunk) {
+ try {
+ if (!entity.isRemoved()) {
+ consumer.accept(entity);
+ }
+ } catch (Throwable t) {
+ ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t);
+ FaultGuard.onSubTaskFailure();
+ }
+ }
+ } finally {
+ CURRENT_QUEUE.set(prev);
+ synchronized (this) {
+ pending--;
+ if (pending == 0) {
+ latch.countDown();
+ }
+ }
+ ACTIVE_SUBTASKS.decrementAndGet();
+ }
+ }
+
+ /** 阻塞等待本维度所有已提交子任务完成;超时触发 FaultGuard 降级,避免永久死锁。 */
+ public void await() {
+ // 关键:若从未提交任何子任务(pending==0),latch 永远不会 countDown,
+ // 无条件 await 会永久死锁。因此先检查 pending,为 0 直接返回。
+ synchronized (this) {
+ if (pending == 0) {
+ return;
+ }
+ }
+ boolean done;
+ try {
+ done = latch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ if (!done) {
+ ModZuozhi.LOGGER.error("[FineGrain] 子任务屏障等待超时(>{}ms),存在卡死的子任务,触发降级", AWAIT_TIMEOUT_MS);
+ FaultGuard.onBarrierTimeout();
+ }
+ }
+
+ /** 维度 worker 收口处:串行执行子任务投递的延迟回调。 */
+ public void drain() {
+ postQueue.drain();
+ }
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java b/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java
new file mode 100644
index 0000000..dee55d1
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java
@@ -0,0 +1,17 @@
+package dev.modzuozhi.core.dimthread;
+
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+/**
+ * 区块环境 tick(随机 tick / 流体)的并行版本入口,由 {@code ServerLevelChunkTickMixin} 注入。
+ *
+ * 并行子任务需要在线程本地随机源上执行随机 tick,但原版 {@code ServerLevel.tickChunk} 直接访问
+ * {@code Level.random}({@code public final} 共享字段)。由于在 NeoForge 开发环境无法对
+ * Minecraft 混淆字段做 {@code @Redirect}(无 refmap),这里改为在 {@code ServerLevel} 上提供
+ * 一个等价的自定义实现 {@link #modzuozhi_tickChunk},它接收线程本地随机源作为参数,
+ * 由 {@code ServerChunkCacheMixin} 在并行子任务中调用,从而完全绕开共享随机源。
+ */
+public interface IChunkEnvTick {
+ void modzuozhi_tickChunk(LevelChunk chunk, int randomTickSpeed, RandomSource random);
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java b/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java
new file mode 100644
index 0000000..5cdf6f2
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java
@@ -0,0 +1,14 @@
+package dev.modzuozhi.core.dimthread;
+
+/**
+ * 可迁移主线程引用的对象(Level.thread / ServerChunkCache.mainThread)。
+ *
+ * dimthreads 模型:运行某维度 tick 前,先把该维度 {@code Level.thread} 与
+ * {@code ServerChunkCache.mainThread} 临时设成当前 worker 线程,使原版所有
+ * {@code currentThread() == mainThread} 判断自然成立,杜绝 supplyAsync/join 死锁。
+ */
+public interface IMutableMainThread {
+ Thread dimThreads$getMainThread();
+
+ void dimThreads$setMainThread(Thread thread);
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java
new file mode 100644
index 0000000..81fff09
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java
@@ -0,0 +1,12 @@
+package dev.modzuozhi.core.dimthread;
+
+/**
+ * 第2步:网络包按玩家分派到维度线程——{@code ServerLevel} 的每维度玩家包队列访问接口。
+ *
+ * 由 {@code ServerLevelPlayerPacketMixin} 注入实现,供 {@code PacketUtilsDimMixin}
+ * (包处理重定向)与 {@code ServerLevelPlayerPacketMixin}(tick 收口 drain)使用。
+ */
+public interface IPlayerPacketQueue {
+ /** 获取本维度的玩家包处理队列。 */
+ PlayerPacketQueue modzuozhi$playerPacketQueue();
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java b/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java
new file mode 100644
index 0000000..a4799a5
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java
@@ -0,0 +1,18 @@
+package dev.modzuozhi.core.dimthread;
+
+/**
+ * 供 {@code EntitySectionMixin} 注入的 section key 访问器。
+ *
+ * {@code EntitySection} 自身不持有其在 {@code EntitySectionStorage} 中的 section key,
+ * 细粒度分片锁需要该 key 定位锁片。本接口由 {@code EntitySectionMixin} 实现,注入
+ * {@code @Unique} 字段保存 key,并由 {@code EntitySectionStorageMixin} 在
+ * {@code createSection} 时写入。
+ *
+ * 注意:该接口不能放在 mixin 包内——Mixin 禁止从被转换类直接引用 mixin 包中的
+ * 类型({@code IllegalClassLoadError})。因此放在核心包 {@code core.dimthread}。
+ */
+public interface ISectionKeyAccessor {
+ void modzuozhi_setSectionKey(long key);
+
+ long modzuozhi_getSectionKey();
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java b/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java
new file mode 100644
index 0000000..2269df3
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java
@@ -0,0 +1,288 @@
+package dev.modzuozhi.core.dimthread;
+
+import com.mojang.logging.LogUtils;
+import dev.modzuozhi.ModZuozhi;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.neoforged.neoforge.event.EventHooks;
+import org.slf4j.Logger;
+
+import java.util.Iterator;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * 维度独立 tick 循环(借鉴 Chlorophyll 的 {LevelTickLoop 思路,用我们自己的
+ * {@link DimThreadCore#swapThreadsAndRun} 机制独立重写)。
+ *
+ * 目标:每个 {@link ServerLevel} 一个常驻循环,各自按 tick 预算独立排下一次 tick。
+ * 某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再像旧「有界屏障」模型那样由
+ * 主线程 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。
+ *
+ * 每个循环在一"拍"里打包三件事(对应原版 {@code tickChildren} 里逐维度的部分):
+ *
+ * - 本维度 {@code level.tick()};
+ * - 本维度玩家的连接 tick(原版玩家 {@code doTick()} 走的就是网络连接路径);
+ * - 本维度玩家的区块发送 + 刷新恢复。
+ *
+ * 全局部分(命令函数、全局连接监听、playerList 延迟广播、自动保存)仍留在主线程,
+ * 见 {@code MinecraftServerDimMixin} 的后续改造。
+ */
+public final class LevelTickLoop implements Runnable, java.util.concurrent.Executor {
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private final ServerLevel level;
+ private final ScheduledThreadPoolExecutor masterPool;
+
+ /** 当前维度的玩家连接集合(玩家登录/断开时增删,见连接路由改造)。 */
+ private final Set connections = ConcurrentHashMap.newKeySet();
+
+ /** 从主线程/跨线程投递回本 loop 执行的任务队列。 */
+ private final Queue taskScope = new ConcurrentLinkedQueue<>();
+
+ /** 本 loop 正在调度中的下一次 tick 任务。 */
+ private volatile Future> currentTickTask;
+ /** 是否还在运行(false 表示已申请停止)。 */
+ private volatile boolean running = true;
+ /** 是否已启动过自排程(防止重复 schedule 出多个并列循环)。 */
+ private volatile boolean scheduled = false;
+ /** 是否正在执行一次 tick(health 检查用)。 */
+ private volatile boolean ticking = false;
+
+ /** 本循环独立的 tick 计数(用于每 20 tick 的时间同步)。 */
+ private int tickCount;
+
+ public LevelTickLoop(ServerLevel level, ScheduledThreadPoolExecutor masterPool) {
+ this.level = level;
+ this.masterPool = masterPool;
+ }
+
+ public ServerLevel getLevel() {
+ return this.level;
+ }
+
+ public boolean isTicking() {
+ return this.ticking;
+ }
+
+ public boolean isRunning() {
+ return this.running;
+ }
+
+ /** 是否已被主线程 {@code tickChildren} 启动过(即该 loop 已接管本维度 tick)。 */
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ /** 申请停止自排程(维度卸载 / 服务器停机时调用)。 */
+ public void kill() {
+ this.running = false;
+ Future> task = this.currentTickTask;
+ if (task != null && !task.isCancelled()) {
+ task.cancel(true);
+ }
+ }
+
+ /** 首次启动(或外部主动重排)本循环。 */
+ public void schedule() {
+ this.currentTickTask = this.masterPool.submit(this);
+ }
+
+ /** 每 tick 由主线程调用:保证本 loop 只启动一次自排程。 */
+ public void scheduleIfNeeded() {
+ if (!this.scheduled) {
+ this.scheduled = true;
+ this.schedule();
+ }
+ }
+
+ public void addConnection(Connection connection) {
+ this.connections.add(connection);
+ }
+
+ public void removeConnection(Connection connection) {
+ this.connections.remove(connection);
+ }
+
+ @Override
+ public void execute(java.lang.Runnable command) {
+ this.taskScope.offer(command);
+ }
+
+ @Override
+ public void run() {
+ DimThreadCore.attach(Thread.currentThread(), this.level);
+ this.ticking = true;
+
+ MinecraftServer server = this.level.getServer();
+ long nspt = server.tickRateManager().nanosecondsPerTick();
+ if (server.tickRateManager().isSprinting()) {
+ nspt = 0L;
+ }
+
+ long tickStart = System.nanoTime();
+ long deadline = tickStart + nspt;
+ try {
+ this.processPendingTasks();
+ DimThreadCore.swapThreadsAndRun(() -> {
+ long workStart = System.nanoTime();
+ this.internalTick();
+ // 只统计真实 tick 工作量(MSPT),不含后面 drainChunkTasks 的按预算轮询等待,
+ // 避免把定时器唤醒延迟等「非负载时间」算进 TPS(否则空闲也会显示 19.7)。
+ TpsTracker.recordDimension(this.level.dimension(), System.nanoTime() - workStart);
+ this.drainChunkTasks(deadline);
+ }, this.level, this.level.getChunkSource());
+ } catch (Throwable t) {
+ ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 异常", this.level.dimension().location(), t);
+ FaultGuard.onDimTickFailure(t);
+ } finally {
+ this.ticking = false;
+ long remaining = Math.max(0L, deadline - System.nanoTime());
+ if (this.running) {
+ this.currentTickTask = this.masterPool.schedule(this, remaining, TimeUnit.NANOSECONDS);
+ } else {
+ ModZuozhi.LOGGER.info("[DimThread] 维度 {} 独立循环已停止", this.level.dimension().location());
+ }
+ }
+ }
+
+ /** 执行由 {@link #execute} 投递回来的主线程任务。 */
+ private void processPendingTasks() {
+ Runnable task;
+ while ((task = this.taskScope.poll()) != null) {
+ try {
+ task.run();
+ } catch (Throwable t) {
+ ModZuozhi.LOGGER.error("[DimThread] 维度 {} 主线程任务异常", this.level.dimension().location(), t);
+ }
+ }
+ }
+
+ /**
+ * 排空本维度区块源任务(距离更新 + 状态提升 + 生成调度 + 光照调度 + 队列)。
+ *
+ * 原版主线程在 {@code waitUntilNextTick → pollTaskInternal → ServerChunkCache.pollTask} 期间会
+ * 连续排空这些任务,是区块加载吞吐量的主要来源。切到维度 loop 后主线程那份被
+ * {@code ServerChunkCachePollMixin} 跳过,改由本 worker 独占;这里在本 tick 的剩余预算内等价地
+ * 连续排空,直到无更多任务或预算耗尽,避免地形加载掉速、卡在未加载区块。
+ */
+ private void drainChunkTasks(long deadline) {
+ ServerChunkCache chunkSource = this.level.getChunkSource();
+ long now = System.nanoTime();
+ // 低 TPS 时 internalTick 可能已超预算(deadline 已过),若直接退出会与区块加载形成「死亡螺旋」:
+ // 区块不加载 → 实体/光照等待区块 → tick 更慢 → 更没有预算排空区块。因此给一个最低保证窗口。
+ long effectiveDeadline = Math.max(deadline, now + 2_000_000L); // 最低 2ms
+ boolean overBudget = now >= deadline;
+ while (this.running && System.nanoTime() < effectiveDeadline) {
+ if (chunkSource.pollTask()) {
+ continue; // 有就绪任务,继续排空
+ }
+ // 区块任务空档期补排空一次玩家包(移动/交互等非命令包),减轻其在高负载下的延迟。
+ ((IPlayerPacketQueue) this.level).modzuozhi$playerPacketQueue().drain();
+ if (overBudget) {
+ break; // 超预算窗口内不可忙等,避免继续占用 worker
+ }
+ // 正常预算内:区块生成是异步的,几毫秒内可能有后台任务完成回调;极短退避持续轮询,
+ // 避免错过异步结果导致管线饥饿(等价原版主线程 managedBlock)。
+ LockSupport.parkNanos(100_000L); // 0.1ms
+ }
+ }
+
+ /**
+ * 一"拍"的维度内部 tick:排空玩家包(命令)→ 挂起刷新 → 时间同步 → level.tick →
+ * 连接 tick(玩家 doTick)→ 区块发送 + 恢复刷新。注意:本阶段暂不把自动保存下沉到 loop,
+ * 仍由主线程全局保存,避免与 {@code MinecraftServerSaveMixin} 双保存。
+ */
+ private void internalTick() {
+ // 先排空玩家包队列(命令等),再进沉重的 level.tick:实体/AI 卡顿时命令仍能及时执行,
+ // 不会像原来那样被饿死到 level.tick 结束(RETURN)才处理。
+ ((IPlayerPacketQueue) this.level).modzuozhi$playerPacketQueue().drain();
+
+ MinecraftServer server = this.level.getServer();
+ this.tickCount++;
+
+ for (Connection connection : this.connections) {
+ ((ServerGamePacketListenerImpl) connection.getPacketListener()).suspendFlushing();
+ }
+
+ if (this.tickCount % 20 == 0) {
+ ((dev.modzuozhi.mixin.MinecraftServerTimeSyncAccessor) server).modzuozhi$invokeSynchronizeTime(this.level);
+ }
+
+ EventHooks.fireLevelTickPre(this.level, () -> true);
+ try {
+ this.level.tick(() -> true);
+ } catch (Throwable throwable) {
+ ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 抛异常", this.level.dimension().location(), throwable);
+ }
+ EventHooks.fireLevelTickPost(this.level, () -> true);
+
+ this.tickConnections();
+
+ for (Connection connection : this.connections) {
+ ServerGamePacketListenerImpl handler = (ServerGamePacketListenerImpl) connection.getPacketListener();
+ handler.chunkSender.sendNextChunks(handler.player);
+ handler.resumeFlushing();
+ }
+ }
+
+ /** tick 本维度玩家的连接(正是这里触发原版 {@code ServerPlayer.doTick()})。 */
+ private void tickConnections() {
+ Iterator iterator = this.connections.iterator();
+ while (iterator.hasNext()) {
+ Connection connection = iterator.next();
+ if (connection.isConnected()) {
+ try {
+ connection.tick();
+ } catch (Exception e) {
+ // 区块未就绪类瞬时异常(玩家 tick 请求尚未加载的位置区块,常见于首次传送进
+ // 全新维度、玩家位置 ticket 尚未登记):跳过本拍,下一 tick 区块就绪后自愈,
+ // 不执行断线(原行为会把玩家踢成 "Internal server error")。
+ if (isChunkNotReady(e)) {
+ ModZuozhi.LOGGER.warn("维度 {} 玩家 tick 跳过(区块未就绪):{}",
+ this.level.dimension().location(), e.getMessage());
+ continue;
+ }
+ ModZuozhi.LOGGER.warn("维度 {} 玩家连接 tick 失败 {}",
+ this.level.dimension().location(), connection.getLoggableAddress(serverTelemetry()), e);
+ Component message = Component.literal("Internal server error");
+ connection.send(new ClientboundDisconnectPacket(message),
+ PacketSendListener.thenRun(() -> connection.disconnect(message)));
+ connection.setReadOnly();
+ }
+ } else {
+ iterator.remove();
+ connection.handleDisconnection();
+ }
+ }
+ }
+
+ /** 是否为"区块未就绪"(Chunk not there when requested)瞬时异常(遍历 cause 链)。 */
+ private static boolean isChunkNotReady(Throwable t) {
+ while (t != null) {
+ if (t instanceof IllegalStateException
+ && t.getMessage() != null
+ && t.getMessage().contains("Chunk not there when requested")) {
+ return true;
+ }
+ t = t.getCause();
+ }
+ return false;
+ }
+
+ private boolean serverTelemetry() {
+ return this.level.getServer().logIPs();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java
new file mode 100644
index 0000000..58b6149
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java
@@ -0,0 +1,58 @@
+package dev.modzuozhi.core.dimthread;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * 第2步:网络包按玩家分派到维度线程的每维度任务队列。
+ *
+ * 借鉴 Chlorophyll(Unlicense)的 EntityTaskScheduler 思路:把玩家发来的游戏包处理任务
+ * 投递到玩家所在维度的队列,由该维度 worker 在维度 tick 收口处统一执行(见
+ * {@code ServerLevelPlayerPacketMixin})。主线程不再逐一执行所有玩家的包 handle,减轻主线程
+ * 瓶颈;多维度下各维度的包处理与维度 tick 并行。
+ *
+ * 线程安全:{@link ConcurrentLinkedQueue} 支持 netty 读线程 / 服务器主线程并发投递;
+ * {@link #drain()} 仅由本维度 tick 线程(维度 worker 或单机主线程串行路径)调用一次。
+ */
+public final class PlayerPacketQueue {
+ /** 标记当前线程正在执行玩家包任务(handle 展开中)。 */
+ private static final ThreadLocal EXECUTING = new ThreadLocal<>();
+
+ private final ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue<>();
+
+ /** 投递一个待处理的玩家包任务(任意线程安全)。 */
+ public void offer(Runnable task) {
+ tasks.add(task);
+ }
+
+ /**
+ * 串行执行并清空队列(仅本维度 tick 线程调用)。
+ *
+ * 执行期间设置 {@link #EXECUTING} 标记:玩家包 handle(如
+ * {@code ServerGamePacketListenerImpl.handleMovePlayer})开头会再次调用
+ * {@code PacketUtils.ensureRunningOnSameThread},此时应直接通过(等价原版
+ * "已在正确线程"),否则会把包再次投回队列造成无限循环。
+ */
+ public void drain() {
+ Runnable task;
+ while ((task = tasks.poll()) != null) {
+ EXECUTING.set(Boolean.TRUE);
+ try {
+ task.run();
+ } catch (Throwable t) {
+ // 包任务异常不应中断维度 tick 收口;记录后继续处理剩余包
+ DimThreadCore.LOGGER.warn("[PlayerPacketQueue] 玩家包处理异常", t);
+ } finally {
+ EXECUTING.remove();
+ }
+ }
+ }
+
+ /** 当前线程是否正在执行玩家包任务(handle 展开中)。 */
+ public static boolean isExecuting() {
+ return Boolean.TRUE.equals(EXECUTING.get());
+ }
+
+ public boolean isEmpty() {
+ return tasks.isEmpty();
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java
new file mode 100644
index 0000000..8f6f83f
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java
@@ -0,0 +1,40 @@
+package dev.modzuozhi.core.dimthread;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * 跨线程延迟执行队列(借鉴 MCMT 的 PostExecutePool 思路,写我们自己的实现)。
+ *
+ * 细粒度并行下,子任务(如方块实体 tick)运行在其它 worker 线程上,可能触发对
+ * {@code Level} 内部非线程安全结构(如 {@code pendingBlockEntityTickers}、
+ * {@code pendingFreshBlockEntities})的写入。为保证安全,这些写入不直接发生,
+ * 而是投递到本队列,由维度 worker 线程在本维度 tick 的收口处串行执行
+ * (此时所有并行子任务已 {@code await} 完成、无并发)。
+ *
+ * 线程安全:{@link ConcurrentLinkedQueue} 支持多线程并发投递;{@link #drain()}
+ * 只由维度 worker 调用一次。
+ */
+public final class PostExecuteQueue {
+ private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>();
+
+ /** 投递一个跨线程延迟执行的回调(任意线程安全)。 */
+ public void post(Runnable action) {
+ queue.add(action);
+ }
+
+ /** 串行执行并清空队列(仅维度 worker 在收口处调用)。 */
+ public void drain() {
+ Runnable action;
+ while ((action = queue.poll()) != null) {
+ try {
+ action.run();
+ } catch (Throwable t) {
+ // 延迟回调异常不应中断维度 tick 收口
+ }
+ }
+ }
+
+ public boolean isEmpty() {
+ return queue.isEmpty();
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java b/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java
new file mode 100644
index 0000000..c20fa9e
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java
@@ -0,0 +1,222 @@
+package dev.modzuozhi.core.dimthread;
+
+import dev.modzuozhi.ModZuozhi;
+import net.minecraft.network.Connection;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 每服务器激活状态与线程池管理(dimthreads 模型)。
+ *
+ * 线程池按服务器懒创建(每个 {@code MinecraftServer} 实例一个),并在服务器停止时关闭清理。
+ *
+ * 是否启用维度并行,由两个条件共同决定:全局开关 {@link ModZuozhi#isEnabled()}(可通过
+ * {@code /dimensionalripper on|off} 运行时切换)+ 服务器级激活标记。任一为 false 即退回原版串行。
+ */
+public class ServerManager {
+ /** CPU 核数是常量,仅在首个服务器创建线程池前计算一次。 */
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+
+ /**
+ * 使用 {@link ConcurrentHashMap}:其 {@code computeIfAbsent} 是原子的,保证同一
+ * {@code MinecraftServer} 只会有一个线程池实例。若用 {@code synchronizedMap} 包装,
+ * {@code computeIfAbsent} 默认实现非原子,维度 worker 并发调用 {@link #getThreadPool} 时会
+ * 竞态创建出多个池实例,导致"子任务提交进池 A、主线程 awaitCompletion 却等在池 B"的错配死锁。
+ */
+ private final Map actives = new ConcurrentHashMap<>();
+ public final Map threadPools = new ConcurrentHashMap<>();
+ /** 缓存上次同步的维度数,维度数未变化时跳过遍历与线程调整,把每 tick 开销降到最低。 */
+ private volatile int lastDimCount = -1;
+
+ /** 每维度独立 tick 循环(维度隔离模型,见 {@link LevelTickLoop})。key 为维度。 */
+ private final Map loops = new ConcurrentHashMap<>();
+ /** 承载所有维度 loop 自排程的调度池({@code schedule(task, nspt)} 需要 Scheduled 能力)。 */
+ private volatile ScheduledThreadPoolExecutor masterPool;
+ /** 玩家连接当前所属维度(key 为 Connection,value 为维度),供跨维度/登录时幂等迁移连接。 */
+ private final Map connectionRoutes = new ConcurrentHashMap<>();
+
+ public boolean isActive(MinecraftServer server) {
+ // 每次调用都叠加全局开关,保证 /dimensionalripper off 立即退回原版
+ return ModZuozhi.isEnabled() && this.actives.computeIfAbsent(server, s -> true);
+ }
+
+ public void setActive(MinecraftServer server, boolean value) {
+ this.actives.put(server, value);
+ }
+
+ public ThreadPool getThreadPool(MinecraftServer server) {
+ // 初始尺寸取 CPU 核数兜底,实际线程数由 syncThreadCount 每 tick 贴合维度数
+ return this.threadPools.computeIfAbsent(server, s -> {
+ ModZuozhi.LOGGER.info("[DimThread] 新建维度线程池 server@{}", System.identityHashCode(s));
+ return new ThreadPool(CPU_COUNT);
+ });
+ }
+
+ /**
+ * 回收不再属于当前活跃服务器的僵尸线程池。
+ *
+ * 启动 / 世界加载阶段可能创建多个 {@code MinecraftServer} 实例,每个都会建一套维度线程池;
+ * 旧的池不再被 {@code tickWorlds} 使用却永远占用线程(每维度一个 worker),造成线程数量失控。
+ * 主线程 {@code tickWorlds} 每 tick 调用本方法,把不属于当前 {@code current} 的旧池 shutdown 并移除,
+ * 使维度线程收敛回「每维度 1 个 worker」。
+ */
+ public void reapStalePools(MinecraftServer current) {
+ this.threadPools.forEach((server, pool) -> {
+ if (server != current) {
+ pool.shutdown();
+ this.threadPools.remove(server, pool);
+ }
+ });
+ }
+
+ /**
+ * 让该服务器的线程池线程数贴合当前活跃维度数量(每维度 1 个 worker)。
+ *
+ * 注意:细粒度子任务运行在 {@code FineGrainScheduler.SUB_POOL}(独立线程池),维度池
+ * 只需承载「每维度 1 个 worker」的维度 tick,无需随 CPU 扩容。早期版本曾在此按
+ * {@code fine} 开关把维度池扩容到 CPU 核数,但独立子任务池上线后该逻辑已冗余,会造成
+ * 大量闲置的维度线程。统一收缩到维度数即可。
+ *
+ * 目标值未变化时(绝大多数 tick)直接返回,不做遍历、不调整线程池,几乎零开销。
+ */
+ public void syncThreadCount(MinecraftServer server) {
+ int dims = 0;
+ for (var ignored : server.getAllLevels()) {
+ dims++;
+ }
+ int target = Math.max(1, Math.min(dims, CPU_COUNT));
+ if (target == this.lastDimCount) {
+ return;
+ }
+ this.lastDimCount = target;
+ ThreadPool pool = getThreadPool(server);
+ pool.setThreadCount(target);
+ }
+
+ /** 使线程数缓存失效,下次 tick 强制重算(开关切换时调用)。 */
+ public void invalidateThreadCount() {
+ this.lastDimCount = -1;
+ }
+
+ /**
+ * 全局调度池(承载所有维度 loop 的自排程)。懒创建,线程为 daemon,避免阻止服务器停机。
+ * 线程数取 CPU 核数,保证每个活跃维度 loop 都有独立线程跑、不互相拖累。
+ */
+ public ScheduledThreadPoolExecutor getMasterPool() {
+ ScheduledThreadPoolExecutor pool = this.masterPool;
+ if (pool == null || pool.isShutdown()) {
+ synchronized (this) {
+ pool = this.masterPool;
+ if (pool == null || pool.isShutdown()) {
+ AtomicInteger seq = new AtomicInteger();
+ pool = new ScheduledThreadPoolExecutor(CPU_COUNT, r -> {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ t.setName(DimThreadCore.MOD_ID + "_server_dimloop_" + seq.getAndIncrement());
+ return t;
+ });
+ pool.setRemoveOnCancelPolicy(true);
+ this.masterPool = pool;
+ }
+ }
+ }
+ return pool;
+ }
+
+ /** 获取(懒创建)某维度的独立 tick 循环。 */
+ public LevelTickLoop getLoop(ServerLevel level) {
+ return this.loops.computeIfAbsent(level, l -> new LevelTickLoop(l, getMasterPool()));
+ }
+
+ /**
+ * 该维度 loop 是否已启动接管({@code tickChildren → scheduleIfNeeded} 之后才为 true)。
+ *
+ * 启动阶段 {@code prepareLevels} 在 {@code tickChildren} 之前运行,此时 loop 尚未启动,
+ * 服务器主线程必须仍走原版 {@code ServerChunkCache.pollTask → runDistanceManagerUpdates}
+ * 来驱动出生点区块生成;直到 loop 接管后才由 {@code ServerChunkCachePollMixin} 跳过主线程 poll。
+ */
+ public boolean isLoopScheduled(ServerLevel level) {
+ LevelTickLoop loop = this.loops.get(level);
+ return loop != null && loop.isScheduled();
+ }
+
+ /**
+ * 把玩家的网络连接路由到其所属维度的 loop(幂等)。
+ *
+ * 用 {@link #connectionRoutes} 记录连接当前所属维度:目标维度与记录一致时是 no-op;不一致
+ * (登录首次 / 跨维度传送 / 重生换维度)时先从旧 loop 移除、再加入新 loop。线程安全,可由
+ * 主线程(reconcile)与 loop 线程并发调用。
+ */
+ public void routeConnection(Connection connection, ServerLevel level) {
+ ServerLevel old = this.connectionRoutes.put(connection, level);
+ if (old != level) {
+ if (old != null) {
+ LevelTickLoop oldLoop = this.loops.get(old);
+ if (oldLoop != null) {
+ oldLoop.removeConnection(connection);
+ }
+ }
+ this.getLoop(level).addConnection(connection);
+ }
+ }
+
+ /** 玩家下线时彻底移除连接路由(幂等;loop 也会在 {@code tickConnections} 自清理断开连接)。 */
+ public void unrouteConnection(Connection connection) {
+ ServerLevel old = this.connectionRoutes.remove(connection);
+ if (old != null) {
+ LevelTickLoop loop = this.loops.get(old);
+ if (loop != null) {
+ loop.removeConnection(connection);
+ }
+ }
+ }
+
+ /**
+ * 主线程每 tick 调用:把每个在线玩家的连接路由到其当前维度 loop。
+ *
+ * 登录、跨维度传送、重生换维度三大路径的时序复杂,这里用一个幂等的「回收」统一收口:遍历
+ * {@code playerList.getPlayers()},按玩家当前 {@code serverLevel()} 调用 {@link #routeConnection}。
+ * 已在正确 loop 中的连接是 no-op,仅发生实际迁移时才增删。单机/少量玩家时开销可忽略。
+ */
+ public void reconcileConnections(MinecraftServer server) {
+ for (ServerPlayer player : server.getPlayerList().getPlayers()) {
+ if (player.connection != null) {
+ routeConnection(player.connection.getConnection(), player.serverLevel());
+ }
+ }
+ }
+
+ /** 移除并停止某维度的 loop(维度卸载时调用)。 */
+ public void removeLoop(ServerLevel level) {
+ LevelTickLoop loop = this.loops.remove(level);
+ if (loop != null) {
+ loop.kill();
+ }
+ }
+
+ /** 停止所有维度 loop 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */
+ public void killAllLoops() {
+ this.loops.values().forEach(LevelTickLoop::kill);
+ this.loops.clear();
+ // 清空连接路由:重新启用时 routeConnection 会把连接当作「首次」重新挂到新 loop。
+ // 否则 old == level 会误判为 no-op,导致重启后玩家连接丢失、不再被任何 loop tick。
+ this.connectionRoutes.clear();
+ ScheduledThreadPoolExecutor pool = this.masterPool;
+ if (pool != null) {
+ pool.shutdownNow();
+ this.masterPool = null;
+ }
+ }
+
+ public void clear() {
+ actives.clear();
+ threadPools.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java b/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java
new file mode 100644
index 0000000..a429012
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java
@@ -0,0 +1,206 @@
+package dev.modzuozhi.core.dimthread;
+
+import java.util.Iterator;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.*;
+
+import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID;
+
+/**
+ * 维度共享线程池(dimthreads 模型)。
+ *
+ * 所有维度共用一个线程池,通过 {@code activeCount} 计数 + {@link #awaitCompletion()}
+ * 让主线程在每 tick 分发完所有维度后等待全部完成。线程均为 daemon,避免阻止服务器停止。
+ *
+ * 线程数可动态调整:{@link #setThreadCount(int)} 会随当前活跃维度数上下浮动,
+ * 保证每个维度都有线程并行跑,也能适配运行期由其它 mod 新建的维度。
+ */
+public class ThreadPool {
+ private ThreadPoolExecutor executor;
+ private volatile int threadCount;
+ private final IntLatch activeCount = new IntLatch();
+
+ public ThreadPool() {
+ this(Runtime.getRuntime().availableProcessors());
+ }
+
+ public ThreadPool(int threadCount) {
+ this.threadCount = Math.max(1, threadCount);
+ this.restart();
+ }
+
+ public int getThreadCount() {
+ return this.threadCount;
+ }
+
+ /**
+ * 动态调整线程数(至少 1)。使用无界队列,多余任务排队等待,绝不因任务数超过线程数而拒绝。
+ *
+ * 顺序很重要:{@code setCorePoolSize(c)} 要求 {@code c <= maximumPoolSize},否则抛
+ * {@code IllegalArgumentException}。因此增大线程数时先扩 {@code max} 再扩
+ * {@code core};缩小时先缩 {@code core} 再缩 {@code max}。
+ */
+ public void setThreadCount(int count) {
+ int c = Math.max(1, count);
+ if (c == this.threadCount) {
+ return;
+ }
+ this.threadCount = c;
+ if (this.executor != null && !this.executor.isShutdown()) {
+ if (c > this.executor.getMaximumPoolSize()) {
+ this.executor.setMaximumPoolSize(c);
+ this.executor.setCorePoolSize(c);
+ } else {
+ this.executor.setCorePoolSize(c);
+ this.executor.setMaximumPoolSize(c);
+ }
+ }
+ }
+
+ public int getActiveCount() {
+ return this.activeCount.getCount();
+ }
+
+ public ThreadPoolExecutor getExecutor() {
+ return this.executor;
+ }
+
+ public void execute(Runnable action) {
+ this.activeCount.increment();
+ try {
+ this.executor.execute(() -> {
+ try {
+ action.run();
+ } finally {
+ this.activeCount.decrement();
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ // 池已 shutdown(服务器停机/回收旧池)时会拒绝新任务;若不回滚计数,
+ // awaitCompletion 会永久等待一个永远不会被 decrement 的计数 → 保存流程死锁。
+ this.activeCount.decrement();
+ throw e;
+ }
+ }
+
+ public void execute(Iterator iterator, Consumer action) {
+ iterator.forEachRemaining(t -> this.execute(() -> action.accept(t)));
+ }
+
+ public void execute(Iterable iterable, Consumer action) {
+ iterable.forEach(t -> this.execute(() -> action.accept(t)));
+ }
+
+ public void execute(T[] array, Consumer action) {
+ for (T t : array) this.execute(() -> action.accept(t));
+ }
+
+ public void awaitFreeThread() {
+ this.waitFor(value -> value < this.getThreadCount());
+ }
+
+ public void awaitCompletion() {
+ this.waitFor(value -> value == 0);
+ }
+
+ /**
+ * 带超时的完成等待(看门狗用)。
+ *
+ * @param timeoutMillis 最长等待毫秒数
+ * @return true 表示在超时前全部完成;false 表示超时(仍有任务未完成)
+ */
+ public boolean awaitCompletion(long timeoutMillis) {
+ return this.waitFor(value -> value == 0, timeoutMillis);
+ }
+
+ public void waitFor(IntPredicate condition) {
+ try {
+ this.activeCount.waitUntil(condition);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public boolean waitFor(IntPredicate condition, long timeoutMillis) {
+ try {
+ return this.activeCount.waitUntil(condition, timeoutMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ }
+
+ public void restart() {
+ if (this.executor == null || this.executor.isShutdown()) {
+ this.executor = new ThreadPoolExecutor(this.threadCount, this.threadCount,
+ 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ t.setName(MOD_ID + "_server_" + "unassigned");
+ return t;
+ });
+ }
+ }
+
+ public void shutdown() {
+ // 仅 shutdown() 只会拒绝新任务、不回收 core 线程,旧池线程会一直挂在 take() 上占用资源。
+ // 因此先归零 core、再用 shutdownNow 中断闲置线程,使其真正退出(旧池此时必然闲置,安全)。
+ if (this.executor != null) {
+ this.executor.setCorePoolSize(0);
+ this.executor.shutdownNow();
+ }
+ }
+
+ public boolean isShutdown() {
+ return this.executor.isShutdown();
+ }
+
+ private static class IntLatch {
+ private CountDownLatch latch;
+
+ private IntLatch() {
+ this(0);
+ }
+
+ private IntLatch(int count) {
+ this.latch = new CountDownLatch(count);
+ }
+
+ private synchronized int getCount() {
+ return (int) this.latch.getCount();
+ }
+
+ private synchronized void decrement() {
+ this.latch.countDown();
+ this.notifyAll();
+ }
+
+ private synchronized void increment() {
+ this.latch = new CountDownLatch((int) this.latch.getCount() + 1);
+ this.notifyAll();
+ }
+
+ private synchronized void waitUntil(IntPredicate predicate) throws InterruptedException {
+ while (!predicate.test(this.getCount())) {
+ this.wait();
+ }
+ }
+
+ private synchronized boolean waitUntil(IntPredicate predicate, long timeoutMillis)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + timeoutMillis;
+ while (!predicate.test(this.getCount())) {
+ long remaining = deadline - System.currentTimeMillis();
+ if (remaining <= 0) {
+ return false;
+ }
+ this.wait(remaining);
+ }
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java b/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java
new file mode 100644
index 0000000..48cad28
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java
@@ -0,0 +1,125 @@
+package dev.modzuozhi.core.dimthread;
+
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.Level;
+
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 服务器 TPS 估算(供 {@code /dimensionalripper status} 展示,验证优化效果)。
+ *
+ * 维度独立 tick 循环改造后,主线程退化为「协调者」,恒按 20 拍运行;真实负载体现在每个维度 loop
+ * 的单次 tick 实际耗时(MSPT)上。因此这里按维度记录每次 {@code internalTick} 的真实耗时,
+ * {@code TPS = min(20, 1e9 / 平均MSPT)}:
+ *
+ * - 空闲时 MSPT 远小于 50ms → TPS = 20;
+ * - 某维度单 tick 超过 50ms 预算 → TPS < 20(真实掉 TPS)。
+ *
+ * 这比「记录相邻 tick 的墙钟间隔」更准确——后者会把自排程定时器的唤醒延迟(每拍约 0.5~0.8ms)
+ * 一并计入,导致完全空闲时仍显示 19.7。
+ *
+ * 主线程样本 {@link #tick()} / {@link #tps()} 仅在维度并行关闭(退回原版串行)时有对比意义,
+ * 此时主线程间隔即真实 tick 周期。
+ */
+public final class TpsTracker {
+ /** 采样窗口(tick 数)。 */
+ private static final int WINDOW = 100;
+
+ /** 主线程最近 N 个 tick 间隔(纳秒)。 */
+ private static final long[] INTERVALS = new long[WINDOW];
+ private static int index;
+ private static boolean primed;
+ private static long lastTick = -1L;
+
+ /** 各维度的独立 MSPT 采样(key 为维度,如 {@code minecraft:overworld})。 */
+ private static final Map, Sample> DIMENSIONS = new ConcurrentHashMap<>();
+
+ private TpsTracker() {
+ }
+
+ /** 服务器主线程每 tick 调用一次。 */
+ public static void tick() {
+ long now = System.nanoTime();
+ if (lastTick >= 0L) {
+ long interval = now - lastTick;
+ synchronized (TpsTracker.class) {
+ INTERVALS[index] = interval;
+ index = (index + 1) % WINDOW;
+ primed = true;
+ }
+ }
+ lastTick = now;
+ }
+
+ /** 某维度 loop 每次完成 {@code internalTick} 后调用,上报本次真实 tick 耗时(纳秒)。 */
+ public static void recordDimension(ResourceKey dimension, long tickNanos) {
+ Sample sample = DIMENSIONS.computeIfAbsent(dimension, k -> new Sample());
+ sample.record(tickNanos);
+ }
+
+ /** 主线程 (协调者) TPS。数据不足时返回 20。 */
+ public static double tps() {
+ synchronized (TpsTracker.class) {
+ if (!primed) {
+ return 20.0;
+ }
+ long sum = 0L;
+ for (long interval : INTERVALS) {
+ if (interval > 0L) {
+ sum += interval;
+ }
+ }
+ return sum <= 0L ? 20.0 : tpsFromAvg(sum / (double) WINDOW);
+ }
+ }
+
+ /** 指定维度的真实 TPS。无采样时返回 20。 */
+ public static double tps(ResourceKey dimension) {
+ Sample sample = DIMENSIONS.get(dimension);
+ return sample == null ? 20.0 : sample.tps();
+ }
+
+ /** 按维度名排序返回每维度的 TPS,供 {@code /dimensionalripper status} 展示。 */
+ public static Map allDimensionTps() {
+ Map result = new TreeMap<>();
+ for (Map.Entry, Sample> entry : DIMENSIONS.entrySet()) {
+ result.put(entry.getKey().location().toString(), entry.getValue().tps());
+ }
+ return result;
+ }
+
+ private static double tpsFromAvg(double avgNanos) {
+ if (avgNanos <= 0.0) {
+ return 20.0;
+ }
+ return Math.min(20.0, 1_000_000_000.0 / avgNanos);
+ }
+
+ /** 单个维度的独立 MSPT 采样窗口。只在所属维度 loop 线程写入,读走 {@code synchronized}。 */
+ private static final class Sample {
+ private final long[] work = new long[WINDOW];
+ private int index;
+ private int count;
+
+ synchronized void record(long nanos) {
+ work[index] = nanos;
+ index = (index + 1) % WINDOW;
+ if (count < WINDOW) {
+ count++;
+ }
+ }
+
+ synchronized double tps() {
+ if (count == 0) {
+ return 20.0;
+ }
+ long sum = 0L;
+ for (int i = 0; i < count; i++) {
+ sum += work[i];
+ }
+ return tpsFromAvg(sum / (double) count);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java b/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java
new file mode 100644
index 0000000..1683b88
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java
@@ -0,0 +1,120 @@
+package dev.modzuozhi.core.filter;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.player.Player;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 实体 tick 并行的白名单过滤器(借鉴 MCMT 思路,写我们自己的实现)。
+ *
+ * 实体 tick 远比方块实体复杂:实体互相交互、移动会更新 {@code EntitySectionStorage}、
+ * 可能繁殖/转换产生新实体。因此第4步采用保守白名单——先以最复杂的村民(Brain AI)
+ * 验证通过,再扩展为所有原版生物。
+ *
+ * 判定规则(按优先级):
+ *
+ * - 命中黑名单 {@link #BLACKLIST} → 串行(已知有问题的类型,可运行时扩展排除);
+ * - 命中白名单 {@link #WHITELIST} → 并行(显式放行,运行时可用 {@link #add} 增补);
+ * - 默认:{@code LivingEntity} 且非玩家且原版生物 → 并行;其余(modded 生物、
+ * 掉落物、经验球、箭、TNT、盔甲架等非生物实体)→ 串行。
+ *
+ * modded 生物默认串行是借鉴 Async 的思路(其用 {@code @AsyncCompatible} 注解显式放行):
+ * 跨模组生物行为不可控,保守串行以降低并行下的兼容风险;确需并行时可用 {@link #add} 显式放行。
+ * 玩家始终由 {@link PlayerList} 单独处理,不参与本并行路径。
+ */
+public final class EntityTickFilter {
+ /** 显式放行名单:命中即并行(村民已验证,作为兜底显式保留)。 */
+ private static final Set WHITELIST = ConcurrentHashMap.newKeySet();
+
+ /** 显式排除名单:命中即强制串行(覆盖默认规则,可用于运行时隔离问题生物)。 */
+ private static final Set BLACKLIST = ConcurrentHashMap.newKeySet();
+
+ /**
+ * 并行判定缓存:{@code EntityType → 是否并行}。
+ *
+ * 避免每个实体每 tick 都 {@code EntityType.getKey(...).toString()} 构建字符串
+ * (高实体量下是显著开销)。实体类型数量有限,首次遇到时计算一次并缓存;
+ * 黑/白名单运行时变化时 {@link #PARALLEL_CACHE} 清空。
+ */
+ private static final ConcurrentHashMap, Boolean> PARALLEL_CACHE = new ConcurrentHashMap<>();
+
+ static {
+ // 第4步首期验证通过的最难实体(Brain AI),显式保留在放行名单。
+ WHITELIST.add("minecraft:villager");
+
+ // 第5步(6b/6c):爆炸/破坏方块类实体强制串行。
+ // 它们在 tick 中会调用 {@code Level.setBlock} 破坏方块(苦力怕自爆、凋灵、末影龙、
+ // 末影水晶),这会写 LevelChunk 的 PalettedContainer(非线程安全)。若在并行子任务线程
+ // 执行,会与主线程的玩家操作(放置方块)并发写同一区块,导致数据竞争:
+ // 放置的方块立即消失、被破坏的方块在存档中"复活"(重进后 TNT 再次出现并爆炸)。
+ // 因此这些实体必须走原版串行路径,与玩家操作在主线程时序上互斥。
+ BLACKLIST.add("minecraft:creeper");
+ BLACKLIST.add("minecraft:end_crystal");
+ BLACKLIST.add("minecraft:ender_dragon");
+ BLACKLIST.add("minecraft:wither");
+ BLACKLIST.add("minecraft:tnt_minecart");
+ }
+
+ private EntityTickFilter() {
+ }
+
+ /** 判断实体是否可并行 tick。 */
+ public static boolean isParallel(Entity entity) {
+ // 快路径:非生物或玩家一定不并行(黑名单中的末影水晶/TNT矿车非生物,白名单只有村民)。
+ if (!(entity instanceof LivingEntity) || entity instanceof Player) {
+ return false;
+ }
+ EntityType> type = entity.getType();
+ Boolean cached = PARALLEL_CACHE.get(type);
+ if (cached != null) {
+ return cached;
+ }
+ boolean result = computeParallel(type);
+ PARALLEL_CACHE.put(type, result);
+ return result;
+ }
+
+ /** 首次遇到某实体类型时计算一次判定(含黑/白名单与命名空间判断)。 */
+ private static boolean computeParallel(EntityType> type) {
+ String key = EntityType.getKey(type).toString();
+ if (BLACKLIST.contains(key)) {
+ return false;
+ }
+ if (WHITELIST.contains(key)) {
+ return true;
+ }
+ // 默认:原版(minecraft 命名空间)生物并行;modded 生物串行(借鉴 Async 思路)。
+ return EntityType.getKey(type).getNamespace().equals("minecraft");
+ }
+
+ /** 显式放行某实体类型(运行时扩展)。 */
+ public static void add(String entityType) {
+ WHITELIST.add(entityType);
+ BLACKLIST.remove(entityType);
+ PARALLEL_CACHE.clear();
+ }
+
+ /** 显式排除某实体类型(运行时隔离问题生物)。 */
+ public static void exclude(String entityType) {
+ BLACKLIST.add(entityType);
+ WHITELIST.remove(entityType);
+ PARALLEL_CACHE.clear();
+ }
+
+ public static boolean remove(String entityType) {
+ PARALLEL_CACHE.clear();
+ return WHITELIST.remove(entityType);
+ }
+
+ public static boolean contains(String entityType) {
+ return WHITELIST.contains(entityType);
+ }
+
+ public static boolean isExcluded(String entityType) {
+ return BLACKLIST.contains(entityType);
+ }
+}
diff --git a/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java b/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java
new file mode 100644
index 0000000..bb9a4b4
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java
@@ -0,0 +1,21 @@
+package dev.modzuozhi.core.filter;
+
+import net.minecraft.world.level.block.entity.TickingBlockEntity;
+
+/**
+ * SerDes 过滤器接口(借鉴 MCMT 的 ISerDesFilter)。
+ *
+ * 决定某个方块实体 tick 是否可以在并行 worker 线程中执行。
+ * 返回 {@code true} 表示可并行;返回 {@code false} 表示该方块实体线程不安全,
+ * 需回退到主线程串行 tick。
+ */
+@FunctionalInterface
+public interface ISerDesFilter {
+ /**
+ * 该方块实体 tick 是否可并行执行。
+ *
+ * @param ticker 待 tick 的方块实体({@link TickingBlockEntity})
+ * @return true = 可并行;false = 回退主线程串行
+ */
+ boolean isParallel(TickingBlockEntity ticker);
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java b/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java
new file mode 100644
index 0000000..0cfaa95
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java
@@ -0,0 +1,62 @@
+package dev.modzuozhi.core.filter;
+
+import net.minecraft.world.level.block.entity.TickingBlockEntity;
+
+import java.util.Set;
+
+/**
+ * 默认 SerDes 过滤器。
+ *
+ * 基于方块实体注册名({@code TickingBlockEntity.getType()},形如 {@code minecraft:furnace})判断:
+ *
+ * - 黑名单(强制串行):已知线程不安全、会与主线程交互的方块实体
+ * (如移动的活塞/活塞头,涉及跨区块移除与实体交互)。
+ * - modded 方块实体(非 {@code minecraft:} 前缀)默认串行,保守兜底;
+ * 可通过 {@link #setLockModded(boolean)} 关闭该保守行为。
+ * - 其余 vanilla 方块实体默认可并行。
+ *
+ */
+public final class SerDesFilter implements ISerDesFilter {
+ /** 全局单例({@link #isParallel} 为实例方法)。 */
+ public static final SerDesFilter INSTANCE = new SerDesFilter();
+
+ /** 强制串行的方块实体注册名(黑名单)。 */
+ private static final Set BLACKLIST = Set.of(
+ "minecraft:piston_head",
+ "minecraft:moving_piston",
+ // 会跨 5×5 范围访问其他容器/方块并触发跨区块交互,worker 线程并行时
+ // 目标区块可能被并发修改(如 InvWrapper.getInv() 返回 null),强制串行
+ "minecraft:hopper",
+ "minecraft:dropper",
+ "minecraft:dispenser",
+ "minecraft:chiseled_bookshelf",
+ "minecraft:crafter",
+ // serverTick 会访问 Level 级共享随机源(LegacyRandomSource),多个蜂巢分布在不同
+ // 区块被并行 tick 时会跨线程访问同一随机源,触发 ThreadingDetector 异常,强制串行
+ "minecraft:beehive",
+ "minecraft:bee_nest"
+ );
+
+ /** 是否对非 vanilla 的 modded 方块实体强制串行(保守兜底,默认开启)。 */
+ private static volatile boolean lockModded = true;
+
+ public static void setLockModded(boolean lockModded) {
+ SerDesFilter.lockModded = lockModded;
+ }
+
+ public static boolean isLockModded() {
+ return lockModded;
+ }
+
+ @Override
+ public boolean isParallel(TickingBlockEntity ticker) {
+ String type = ticker.getType();
+ if (BLACKLIST.contains(type)) {
+ return false;
+ }
+ if (lockModded && !type.startsWith("minecraft:")) {
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java b/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java
new file mode 100644
index 0000000..a0755cb
--- /dev/null
+++ b/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java
@@ -0,0 +1,39 @@
+package dev.modzuozhi.mixin;
+
+import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.ai.attributes.AttributeInstance;
+import net.minecraft.world.entity.ai.attributes.AttributeModifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 属性修饰符并发化(借鉴 Async 思路,独立实现)。
+ *
+ * {@code AttributeInstance} 的修饰符表在并行 tick 下被跨线程读写(增益/减益结算、伤害计算、
+ * 传感器查询)。原版用普通 Map,替换为并发容器;{@code getModifiers} 返回的按操作分类子表
+ * 也并发化,保证 computeIfAbsent 原子且子表写入安全。
+ */
+@Mixin(AttributeInstance.class)
+public abstract class AttributeInstanceMixin {
+
+ @Shadow
+ private final Map> modifiersByOperation =
+ new ConcurrentHashMap<>();
+
+ @Shadow
+ private final Map modifierById = new ConcurrentHashMap<>();
+
+ @Shadow
+ private final Map permanentModifiers = new ConcurrentHashMap<>();
+
+ @WrapMethod(method = "getModifiers(Lnet/minecraft/world/entity/ai/attributes/AttributeModifier$Operation;)Ljava/util/Map;")
+ private Map modzuozhi_getModifiers(
+ AttributeModifier.Operation operation, Operation