From 453e715f3bca34a030d1fb9a0dc4341a24aa326e Mon Sep 17 00:00:00 2001 From: xxdd001 <2370346990@qq.com> Date: Wed, 26 Aug 2026 20:59:21 +0800 Subject: [PATCH] Initial commit: Dimensional Ripper mod source (NeoForge 1.21.1) --- .gitignore | 18 + build.gradle | 99 +++++ gradle.properties | 55 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 38 ++ gradlew.bat | 90 +++++ settings.gradle | 13 + src/main/java/dev/modzuozhi/ModZuozhi.java | 45 +++ .../dev/modzuozhi/command/MasterCommand.java | 111 ++++++ .../collection/ConcurrentLong2ObjectMap.java | 193 ++++++++++ .../collection/ConcurrentLongSortedSet.java | 156 ++++++++ .../Int2ObjectConcurrentHashMap.java | 111 ++++++ .../modzuozhi/core/dimthread/ChunkLock.java | 95 +++++ .../core/dimthread/ChunkPrefetcher.java | 174 +++++++++ .../core/dimthread/DimThreadCore.java | 63 ++++ .../core/dimthread/DimensionChangeQueue.java | 57 +++ .../core/dimthread/EntityTickParallel.java | 189 ++++++++++ .../modzuozhi/core/dimthread/FaultGuard.java | 148 ++++++++ .../core/dimthread/FineGrainScheduler.java | 304 +++++++++++++++ .../core/dimthread/IChunkEnvTick.java | 17 + .../core/dimthread/IMutableMainThread.java | 14 + .../core/dimthread/IPlayerPacketQueue.java | 12 + .../core/dimthread/ISectionKeyAccessor.java | 18 + .../core/dimthread/LevelTickLoop.java | 288 +++++++++++++++ .../core/dimthread/PlayerPacketQueue.java | 58 +++ .../core/dimthread/PostExecuteQueue.java | 40 ++ .../core/dimthread/ServerManager.java | 222 +++++++++++ .../modzuozhi/core/dimthread/ThreadPool.java | 206 +++++++++++ .../modzuozhi/core/dimthread/TpsTracker.java | 125 +++++++ .../core/filter/EntityTickFilter.java | 120 ++++++ .../modzuozhi/core/filter/ISerDesFilter.java | 21 ++ .../modzuozhi/core/filter/SerDesFilter.java | 62 ++++ .../mixin/AttributeInstanceMixin.java | 39 ++ .../modzuozhi/mixin/AttributeMapMixin.java | 33 ++ .../mixin/BabyFollowAdultGuardMixin.java | 74 ++++ .../dev/modzuozhi/mixin/BinaryHeapMixin.java | 59 +++ .../mixin/BlockDropCaptureMixin.java | 57 +++ .../mixin/BlockableEventLoopPollMixin.java | 61 +++ .../java/dev/modzuozhi/mixin/BrainMixin.java | 70 ++++ .../mixin/ChunkMapLoadOffloadMixin.java | 58 +++ .../mixin/ClassInstanceMultiMapMixin.java | 44 +++ .../mixin/CollectingNeighborUpdaterMixin.java | 78 ++++ .../modzuozhi/mixin/DistanceManagerMixin.java | 118 ++++++ .../mixin/EntityChangeDimensionMixin.java | 45 +++ .../mixin/EntityGetEntityCollisionsMixin.java | 55 +++ .../modzuozhi/mixin/EntityLookupMixin.java | 38 ++ .../mixin/EntitySectionManagerMixin.java | 40 ++ .../modzuozhi/mixin/EntitySectionMixin.java | 115 ++++++ .../mixin/EntitySectionStorageMixin.java | 61 +++ .../modzuozhi/mixin/EntityTickListMixin.java | 44 +++ .../mixin/EntityTickParallelMixin.java | 78 ++++ .../mixin/GameEventDispatcherMixin.java | 35 ++ .../mixin/GoToPotentialJobSiteGuardMixin.java | 37 ++ .../modzuozhi/mixin/GossipContainerMixin.java | 23 ++ .../java/dev/modzuozhi/mixin/ILevelMixin.java | 19 + .../dev/modzuozhi/mixin/LevelChunkMixin.java | 32 ++ .../modzuozhi/mixin/LevelExplodeMixin.java | 49 +++ .../modzuozhi/mixin/LevelGetChunkMixin.java | 40 ++ .../java/dev/modzuozhi/mixin/LevelMixin.java | 56 +++ .../dev/modzuozhi/mixin/LevelTicksMixin.java | 47 +++ .../LivingEntityGetInBlockStateMixin.java | 29 ++ .../mixin/MinecraftServerDimMixin.java | 88 +++++ .../mixin/MinecraftServerSaveMixin.java | 110 ++++++ .../MinecraftServerTimeSyncAccessor.java | 16 + .../modzuozhi/mixin/NavigatingMobsMixin.java | 34 ++ .../mixin/NearestLivingEntitySensorMixin.java | 72 ++++ .../modzuozhi/mixin/PacketUtilsDimMixin.java | 111 ++++++ .../modzuozhi/mixin/PlayerListSaveMixin.java | 95 +++++ .../mixin/PoiCompetitorScanGuardMixin.java | 69 ++++ .../mixin/PoiManagerDistanceTrackerMixin.java | 32 ++ .../dev/modzuozhi/mixin/PoiSectionMixin.java | 36 ++ .../modzuozhi/mixin/PortalProcessorMixin.java | 247 +++++++++++++ .../modzuozhi/mixin/SectionStorageMixin.java | 31 ++ .../mixin/ServerChunkCacheAccessor.java | 24 ++ ...ServerChunkCacheGetChunkFallbackMixin.java | 126 +++++++ .../mixin/ServerChunkCacheMixin.java | 347 ++++++++++++++++++ .../mixin/ServerChunkCachePollMixin.java | 37 ++ .../ServerChunkCacheRunDistanceMixin.java | 34 ++ .../ServerCommonPacketListenerDimMixin.java | 37 ++ .../mixin/ServerConnectionListenerMixin.java | 79 ++++ .../mixin/ServerLevelBlockEventMixin.java | 47 +++ .../mixin/ServerLevelChunkTickMixin.java | 116 ++++++ .../ServerLevelEntityCallbacksMixin.java | 72 ++++ .../mixin/ServerLevelEntityTickMixin.java | 73 ++++ .../mixin/ServerLevelPlayerPacketMixin.java | 48 +++ .../ServerPlayerChangeDimensionMixin.java | 41 +++ .../mixin/ServerPlayerPrefetchMixin.java | 24 ++ .../TickingBlockEntityParallelMixin.java | 113 ++++++ .../mixin/YieldJobSiteGuardMixin.java | 89 +++++ .../resources/META-INF/accesstransformer.cfg | 6 + .../resources/META-INF/neoforge.mods.toml | 30 ++ .../resources/dimensionalripper.mixins.json | 72 ++++ src/main/resources/logo.png | Bin 0 -> 53208 bytes 94 files changed, 7059 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/dev/modzuozhi/ModZuozhi.java create mode 100644 src/main/java/dev/modzuozhi/command/MasterCommand.java create mode 100644 src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java create mode 100644 src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java create mode 100644 src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java create mode 100644 src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java create mode 100644 src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java create mode 100644 src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java create mode 100644 src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java create mode 100644 src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/BrainMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ILevelMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LevelMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java create mode 100644 src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java create mode 100644 src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java create mode 100644 src/main/resources/META-INF/accesstransformer.cfg create mode 100644 src/main/resources/META-INF/neoforge.mods.toml create mode 100644 src/main/resources/dimensionalripper.mixins.json create mode 100644 src/main/resources/logo.png 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 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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/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 mappingFunction) { + V v = backing.computeIfAbsent(key, l -> mappingFunction.get(l)); + return v == null ? defaultReturnValue() : v; + } + + @Override + public V computeIfAbsent(long key, LongFunction 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} 中运行或排队的子任务总数(含残留排空期)。 + *

+ * 用于两点: + *

    + *
  1. {@link #awaitIdle} 判断是否排空(比 {@code getActiveCount()+queue} 更准,覆盖排队任务);
  2. + *
  3. {@link EntityTickParallel#isLockDisabled} 在「细粒度已关但仍有残留子任务」期间保持 + * 分片锁启用,保证残留子任务与退化为串行的维度 tick 并发改 {@code EntitySection} 时仍互斥, + * 否则 `fine off`/`off` 瞬间锁被禁用 + 残留子任务无锁运行 → CME(已实测)。
  4. + *
+ */ + 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} 里逐维度的部分): + *

    + *
  1. 本维度 {@code level.tick()};
  2. + *
  3. 本维度玩家的连接 tick(原版玩家 {@code doTick()} 走的就是网络连接路径);
  4. + *
  5. 本维度玩家的区块发送 + 刷新恢复。
  6. + *
+ * 全局部分(命令函数、全局连接监听、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) + * 验证通过,再扩展为所有原版生物。 + *

+ * 判定规则(按优先级): + *

    + *
  1. 命中黑名单 {@link #BLACKLIST} → 串行(已知有问题的类型,可运行时扩展排除);
  2. + *
  3. 命中白名单 {@link #WHITELIST} → 并行(显式放行,运行时可用 {@link #add} 增补);
  4. + *
  5. 默认:{@code LivingEntity} 且非玩家且原版生物 → 并行;其余(modded 生物、 + * 掉落物、经验球、箭、TNT、盔甲架等非生物实体)→ 串行。
  6. + *
+ * 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> original) { + return this.modifiersByOperation.computeIfAbsent(operation, op -> new ConcurrentHashMap<>()); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java b/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java new file mode 100644 index 0000000..886c999 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java @@ -0,0 +1,33 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 属性表并发化(借鉴 Async 思路,独立实现)。 + *

+ * 并行 tick 下,传感器/寻路/目标选择会跨线程读取其它实体的属性 + * ({@code getAttribute → getInstance}、{@code getValue}),属性被修改时还会写入 + * {@code attributesToUpdate/attributesToSync}。原版 {@code AttributeMap} 用普通 Map/Set, + * 跨线程读写存在数据竞争。全部替换为并发容器,读写安全。 + */ +@Mixin(AttributeMap.class) +public abstract class AttributeMapMixin { + + @Shadow + private final Map, AttributeInstance> attributes = new ConcurrentHashMap<>(); + + @Shadow + private final Set attributesToSync = ConcurrentHashMap.newKeySet(); + + @Shadow + private final Set attributesToUpdate = ConcurrentHashMap.newKeySet(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java new file mode 100644 index 0000000..669db76 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java @@ -0,0 +1,74 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.util.valueproviders.UniformInt; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BabyFollowAdult; +import net.minecraft.world.entity.ai.behavior.EntityTracker; +import net.minecraft.world.entity.ai.behavior.OneShot; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.memory.WalkTarget; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import java.util.function.Function; + +/** + * 跨维度安全:{@link BabyFollowAdult} 添加维度检查。 + *

+ * 原版 1.21.1 的 {@code create()} 不做维度检查,当幼年生物追踪的成年目标在另一个维度时, + * 幼年生物会尝试跨维度寻路。并行环境下需确保幼年生物只跟随同一维度的目标。 + *

+ * 直接覆盖 1.21.1 实际存在的 {@code create(UniformInt, Function)} 重载(此为底层实现, + * {@code create(UniformInt, float)} 委托给此方法),增加维度检查。 + */ +@Mixin(BabyFollowAdult.class) +public class BabyFollowAdultGuardMixin { + + /** + * @reason 添加维度守卫,防止幼年生物跨维度跟随成年目标 + */ + @Overwrite + public static @NotNull OneShot create( + UniformInt uniformInt, + Function function + ) { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.NEAREST_VISIBLE_ADULT), + instance.registered(MemoryModuleType.LOOK_TARGET), + instance.absent(MemoryModuleType.WALK_TARGET) + ).apply(instance, (memoryAccessor, memoryAccessor2, memoryAccessor3) -> + (serverLevel, livingEntity, l) -> { + if (!livingEntity.isBaby()) { + return false; + } + LivingEntity livingEntity2 = instance.get(memoryAccessor); + + // 维度守卫:成年目标不在同一维度则清除记忆并跳过 + if (livingEntity2.level() != livingEntity.level()) { + memoryAccessor.erase(); + return true; + } + + if (livingEntity.closerThan(livingEntity2, + (double) (uniformInt.getMaxValue() + 1)) + && !livingEntity.closerThan(livingEntity2, + (double) uniformInt.getMinValue())) { + WalkTarget walkTarget = new WalkTarget( + new EntityTracker(livingEntity2, false), + function.apply(livingEntity), + uniformInt.getMinValue() - 1 + ); + memoryAccessor2.set(new EntityTracker(livingEntity2, true)); + memoryAccessor3.set(walkTarget); + return true; + } + return false; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java b/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java new file mode 100644 index 0000000..ffe8898 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java @@ -0,0 +1,59 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.level.pathfinder.BinaryHeap; +import net.minecraft.world.level.pathfinder.Node; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第7步:寻路/AI 并发安全——{@code BinaryHeap} 守卫(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code BinaryHeap} 是 A* 寻路的开放集(open set),{@code Node.heapIdx} 记录节点在堆中的 + * 下标(-1 表示不在堆中)。每个 {@code PathFinder} 拥有独立的 {@code BinaryHeap}, + * 并行实体各自寻路时堆对象不跨实体共享,因此不会并发读写同一堆。 + *

+ * 但并行寻路下仍有防御性需要:若节点重复 {@code insert}(其 {@code heapIdx} 已在堆中), + * 原版直接抛 {@code IllegalStateException("OW KNOWS!")};若 {@code upHeap/downHeap} 收到 + * {@code heapIdx == -1} 的节点会数组越界。这些异常在正常串行下不会发生,但在并行子任务的 + * 寻路中,若某处对同一节点的堆状态判定出现竞态,可能触发。为让并行寻路失败不崩溃, + * 这里加守卫:已入堆的节点直接返回(不重复插入);堆下标为 -1 时跳过堆调整。 + *

+ * 语义影响:仅忽略"重复入堆/对已出堆节点的堆调整"这类非法调用,正常寻路路径完全不受影响。 + */ +@Mixin(BinaryHeap.class) +public abstract class BinaryHeapMixin { + + /** + * {@code insert}:若节点已在堆中({@code heapIdx >= 0}),跳过插入,避免重复入堆破坏堆结构。 + */ + @WrapMethod(method = "insert") + private Node modzuozhi_guardInsert(Node node, Operation original) { + if (node.heapIdx >= 0) { + return node; + } + return original.call(node); + } + + /** + * {@code upHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。 + */ + @WrapMethod(method = "upHeap") + private void modzuozhi_guardUpHeap(int index, Operation original) { + if (index < 0) { + return; + } + original.call(index); + } + + /** + * {@code downHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。 + */ + @WrapMethod(method = "downHeap") + private void modzuozhi_guardDownHeap(int index, Operation original) { + if (index < 0) { + return; + } + original.call(index); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java b/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java new file mode 100644 index 0000000..b0ca95e --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java @@ -0,0 +1,57 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.block.Block; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.ArrayList; +import java.util.List; + +/** + * 让 NeoForge 的方块掉落捕获对多线程安全。 + *

+ * NeoForge 在 {@code Block.dropResources} 中通过全局静态字段 {@code Block.capturedDrops} + * 临时捕获本方块掉落的 {@code ItemEntity}(详见 {@code beginCapturingDrops}/{@code stopCapturingDrops}, + * 注释明确要求「只在服务端线程调用」)。维度并行 / 细粒度并行下,多个线程并发执行随机 tick + * (树叶凋零、方块燃烧等)都会进入 {@code dropResources},并发读写该静态字段会互相覆盖: + * 某个线程的 {@code stopCapturingDrops()} 会读到已被另一线程清空的 {@code null},导致 + * {@code BlockDropsEvent.getDrops()} 为 null 而 NPE(日志中 {@code CommonHooks.handleBlockDrops} + * 的崩溃即源于此),并连带产生 {@code ClassCastException}。 + *

+ * 这里把掉落捕获改为线程本地:每个线程维护独立的捕获列表,从根上消除跨线程污染。 + * 语义与原版一致(仅在捕获开始到结束之间拦截掉落),但并行安全。 + */ +@Mixin(Block.class) +public abstract class BlockDropCaptureMixin { + /** 线程本地掉落捕获列表,替代原版全局静态 {@code Block.capturedDrops}。 */ + @Unique + private static final ThreadLocal> modzuozhi_capturedDrops = new ThreadLocal<>(); + + @Redirect(method = "dropResources", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;beginCapturingDrops()V")) + private static void modzuozhi_beginCapture() { + modzuozhi_capturedDrops.set(new ArrayList<>()); + } + + @Redirect(method = "dropResources", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;stopCapturingDrops()Ljava/util/List;")) + private static List modzuozhi_stopCapture() { + List drops = modzuozhi_capturedDrops.get(); + modzuozhi_capturedDrops.remove(); + return drops; + } + + @Redirect(method = "popResource(Lnet/minecraft/world/level/Level;Ljava/util/function/Supplier;Lnet/minecraft/world/item/ItemStack;)V", + at = @At(value = "FIELD", + target = "Lnet/minecraft/world/level/block/Block;capturedDrops:Ljava/util/List;", + opcode = Opcodes.GETSTATIC)) + private static List modzuozhi_capturedGet() { + return modzuozhi_capturedDrops.get(); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java b/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java new file mode 100644 index 0000000..212f4cc --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java @@ -0,0 +1,61 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.util.thread.BlockableEventLoop; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Queue; + +/** + * 修复 {@link BlockableEventLoop#pollTask()} 的 peek→remove 并发竞态。 + *

+ * 原版 {@code pollTask()} 先 {@code peek()}(非空判断)再 {@code remove()},两步非原子。 + * 维度 loop 模型下,本模组维度 worker({@code LevelTickLoop.drainChunkTasks} 每拍、以及 + * {@code managedBlock})与服务器主线程 {@code pollTaskInternal}(瞬态窗口内)可能并发 poll + * 同一维度的 {@code pendingRunnables}:线程 A {@code peek()} 到元素后、{@code remove()} 前, + * 线程 B 已 {@code remove()},A 的 {@code remove()} 空队列抛 {@code NoSuchElementException} + * (实测:下界维度 tick 异常,{@code FaultGuard} 计数 1 次)。 + *

+ * 仅在本模组维度 worker线程上用 {@code poll()}(空返回 null,不抛异常)替代 + * {@code remove()}:并发被抢走时返回 false(跳过本拍,下一拍自愈)。其它线程保持原版行为不变。 + */ +@Mixin(BlockableEventLoop.class) +public abstract class BlockableEventLoopPollMixin { + + @Shadow + @Final + private Queue pendingRunnables; + + @Shadow + private int blockingCount; + + @Shadow + protected abstract boolean shouldRun(R runnable); + + @Shadow + protected abstract void doRunTask(R task); + + @WrapMethod(method = "pollTask") + private boolean modzuozhi_safePollTask(Operation original) { + if (!DimThreadCore.owns(Thread.currentThread())) { + return original.call(); + } + R r = this.pendingRunnables.peek(); + if (r == null) { + return false; + } else if (this.blockingCount == 0 && !this.shouldRun(r)) { + return false; + } else { + R removed = this.pendingRunnables.poll(); + if (removed == null) { + return false; // 并发被抢走,跳过本拍(自愈) + } + this.doRunTask(removed); + return true; + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BrainMixin.java b/src/main/java/dev/modzuozhi/mixin/BrainMixin.java new file mode 100644 index 0000000..4cd88f0 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BrainMixin.java @@ -0,0 +1,70 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.ai.Brain; +import net.minecraft.world.entity.ai.memory.ExpirableValue; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.memory.MemoryStatus; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.Optional; + +/** + * 跨线程访问生物大脑内存的同步加固(借鉴 Async/MCMT 思路,独立实现)。 + *

+ * 细粒度并行时,多个实体的 tick 在子任务线程上并发执行。某个生物的大脑内存 + * ({@code Brain.memories})不仅被它自己的 tick 读写,还可能被其它实体的 + * 传感器/寻路并发访问(如 {@code NearestLivingEntitySensor} 读取它的记忆)。 + * 原版 {@code Brain} 用普通 HashMap,跨线程读写存在数据竞争(潜在错误行为而非崩溃)。 + *

+ * 这里对记忆读写入口加 {@code synchronized(this)}:单对象锁、逐方法加解锁、无嵌套, + * 同一大脑同一时刻只允许一个线程访问,结构上无死锁;未竞争时偏向锁开销可忽略。 + * 相比给整段 villager tick 加锁,仅同步记忆访问点,保留并行收益。 + */ +@Mixin(Brain.class) +public abstract class BrainMixin { + + @WrapMethod(method = "getMemory") + private Optional modzuozhi_getMemory(MemoryModuleType type, Operation> original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "getMemoryInternal") + private Optional modzuozhi_getMemoryInternal(MemoryModuleType type, Operation> original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "hasMemoryValue") + private boolean modzuozhi_hasMemoryValue(MemoryModuleType type, Operation original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "checkMemory") + private boolean modzuozhi_checkMemory(MemoryModuleType type, MemoryStatus status, Operation original) { + synchronized (this) { + return original.call(type, status); + } + } + + @WrapMethod(method = "setMemoryInternal") + private void modzuozhi_setMemoryInternal( + MemoryModuleType type, Optional> value, Operation original) { + synchronized (this) { + original.call(type, value); + } + } + + @WrapMethod(method = "clearMemories") + private void modzuozhi_clearMemories(Operation original) { + synchronized (this) { + original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java b/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java new file mode 100644 index 0000000..2e055e2 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java @@ -0,0 +1,58 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2ByteMaps; +import net.minecraft.Util; +import net.minecraft.server.level.ChunkMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.Executor; + +/** + * 阶段 A(下一步):把「区块反序列化」从主线程下放到后台线程。 + *

+ * 原版 {@code ChunkMap.scheduleChunkLoad} 里,磁盘读 + datafix 已经异步(见 + * {@code readChunk}),但 {@code ChunkSerializer.read}(NBT → ProtoChunk,含方块状态/生物群系 + * 调色板解析,CPU 密集)仍跑在 {@code this.mainThreadExecutor}。这正是进图 / 快速跑图 / 传送时 + * 主线程卡顿的元凶之一——本 mixin 把这一步的执行器切换到 {@link Util#backgroundExecutor()}。 + *

+ * 配套加固:下放后 {@code markPosition}/{@code createEmptyChunk} 会从后台线程写 + * {@code chunkTypeCache}(仅用于调试的 chunk-type 缓存,非世界数据),而主线程仍在读/删它, + * 因此把该缓存包装为同步 Map,消除并发写冲突。 + */ +@Mixin(ChunkMap.class) +public abstract class ChunkMapLoadOffloadMixin { + + /** 反序列化下放开关(与 {@link ModZuozhi#isEnabled()} 叠加,可一键回退)。Mixin 禁止非私有静态字段。 */ + private static volatile boolean ENABLED = true; + + @Shadow + @Final + @Mutable + private Long2ByteMap chunkTypeCache; + + @Inject(method = "", at = @At("RETURN")) + private void modzuozhi$makeChunkTypeCacheConcurrent(CallbackInfo ci) { + this.chunkTypeCache = Long2ByteMaps.synchronize(this.chunkTypeCache); + } + + @ModifyArg( + method = "scheduleChunkLoad", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ), + index = 1 + ) + private Executor modzuozhi$deserializeOffMainThread(Executor executor) { + return (ModZuozhi.isEnabled() && ENABLED) ? Util.backgroundExecutor() : executor; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java b/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java new file mode 100644 index 0000000..425e758 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java @@ -0,0 +1,44 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.util.ClassInstanceMultiMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第10步:{@code ClassInstanceMultiMap} 内部结构线程安全化(修共享读锁下的 CME)。 + *

+ * {@code EntitySection.getEntities(EntityTypeTest,...)} 走 {@code ClassInstanceMultiMap.find}, + * 其内部执行 {@code byClass.computeIfAbsent(...)} 懒加载类型列表——这是写操作。 + * 第10步把 section 内容查询改成共享读锁后,同一 section 的两个并发查询会同时 + * {@code computeIfAbsent} 同一个普通 {@code HashMap} → {@code ConcurrentModificationException} + * (日志已证实:村民大脑传感器在 1 分钟内触发 5 次子任务异常导致自动降级)。 + *

+ * 修复:把 {@code byClass} 换成 {@link ConcurrentHashMap}——其 {@code computeIfAbsent} 原子且 + * 线程安全(同一 key 的映射函数只执行一次),并发查询互不干扰。既保留共享读锁的并行度,又根除 CME。 + * {@code allInstances} 的读写仍由 section 分片锁(写锁 vs 读锁)互斥,无需改动。 + *

+ * 注:字段 final 已由 access transformer({@code accesstransformer.cfg})移除,本类在 + * 构造器 RETURN 处替换为并发映射并恢复 {@code baseClass → allInstances} 注册。 + */ +@Mixin(ClassInstanceMultiMap.class) +public abstract class ClassInstanceMultiMapMixin { + @Shadow + private Map, List> byClass; + @Shadow + private Class baseClass; + @Shadow + private List allInstances; + + @Inject(method = "", at = @At("RETURN")) + private void modzuozhi_concurrentByClass(CallbackInfo ci) { + this.byClass = new ConcurrentHashMap<>(); + this.byClass.put(this.baseClass, this.allInstances); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java b/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java new file mode 100644 index 0000000..0d0c185 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java @@ -0,0 +1,78 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.redstone.CollectingNeighborUpdater; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * 修复并行随机 tick 触发邻居更新时的并发崩溃(NoSuchElementException)。 + *

+ * 根因:第3步区块环境 tick 并行中,藤蔓等 {@code randomTick} 会在子任务线程调用 + * {@code Level.setBlockAndUpdate},进而触发 {@code CollectingNeighborUpdater}。它内部用 + * 非线程安全的 {@code ArrayDeque} 作为邻居更新栈({@code stack}/{@code addedThisLayer}), + * 多个子任务线程并发访问同一实例时会抛 {@code NoSuchElementException}(日志已证实)。 + *

+ * 修复:对 {@code CollectingNeighborUpdater} 的每个公开入口方法加实例级重入锁。 + * 同一维度的邻居更新串行执行;不同维度(不同实例)互不干扰。锁为可重入锁,内部递归 + * 更新({@code executeUpdate → setBlock → 再入 addAndRun})不会死锁。 + */ +@Mixin(CollectingNeighborUpdater.class) +public abstract class CollectingNeighborUpdaterMixin { + /** 实例级重入锁:串行化同一维度内并发子任务对邻居更新栈的访问。 */ + @Unique + private final ReentrantLock modzuozhi_neighborLock = new ReentrantLock(); + + @WrapMethod(method = "shapeUpdate") + private void modzuozhi_shapeUpdate(Direction direction, BlockState state, BlockPos pos, + BlockPos neighborPos, int updateFlags, int updateLimit, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(direction, state, pos, neighborPos, updateFlags, updateLimit); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "neighborChanged(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/core/BlockPos;)V") + private void modzuozhi_neighborChanged(BlockPos pos, Block block, BlockPos neighborPos, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(pos, block, neighborPos); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "neighborChanged(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/core/BlockPos;Z)V") + private void modzuozhi_neighborChangedState(BlockState state, BlockPos pos, Block block, + BlockPos neighborPos, boolean movedByPiston, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(state, pos, block, neighborPos, movedByPiston); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "updateNeighborsAtExceptFromFacing") + private void modzuozhi_updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, + Direction skipDir, Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(pos, block, skipDir); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java b/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java new file mode 100644 index 0000000..68c96f5 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java @@ -0,0 +1,118 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.Ticket; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * {@code DistanceManager}(区块 ticket 调度)并发保护。 + *

+ * 维度并行下,维度 worker / 细粒度 subtask 的 {@code getChunk} 会在同步路径里调用 + * {@code addTicket/removeTicket} 修改 ticket,而主线程 executor 里的 + * {@code PlayerTicketTracker.onLevelChange}(玩家移动/跨维度传送触发)也在修改同一 + * {@code DistanceManager},两者并发导致内部 {@code SortedArraySet.first()} 读到空集等 + * NPE 竞态。 + *

+ * 这里的锁覆盖所有 ticket 写操作,使「维度侧 getChunk 的 ticket 修改」与「主线程玩家的 + * ticket 变更」互斥,从根上消除 DistanceManager 并发竞态。ticket 操作不频繁,串行化代价低。 + */ +@Mixin(DistanceManager.class) +public class DistanceManagerMixin { + private static final ReentrantLock LOCK = new ReentrantLock(); + + @WrapMethod(method = "addTicket") + private void modzuozhi_lockAddTicket(long chunkPos, Ticket ticket, Operation original) { + LOCK.lock(); + try { + original.call(chunkPos, ticket); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removeTicket") + private void modzuozhi_lockRemoveTicket(long chunkPos, Ticket ticket, Operation original) { + LOCK.lock(); + try { + original.call(chunkPos, ticket); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "updateChunkForced") + private void modzuozhi_lockUpdateChunkForced(ChunkPos pos, boolean add, Operation original) { + LOCK.lock(); + try { + original.call(pos, add); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removeTicketsOnClosing") + private void modzuozhi_lockRemoveTicketsOnClosing(Operation original) { + LOCK.lock(); + try { + original.call(); + } finally { + LOCK.unlock(); + } + } + + /** + * 维度循环模型下,主线程 {@code ServerChunkCache.pollTask} 与维度 worker 的 + * {@code level.tick → ServerChunkCache.tick} 会并发调用 {@code runAllUpdates}, + * 遍历 {@code chunksToUpdateFutures}(普通 HashSet)时抛 {@code ConcurrentModificationException}。 + * 用同一把锁串行化,彻底消除该竞态。 + */ + @WrapMethod(method = "runAllUpdates") + private boolean modzuozhi_lockRunAllUpdates(ChunkMap chunkMap, Operation original) { + LOCK.lock(); + try { + return original.call(chunkMap); + } finally { + LOCK.unlock(); + } + } + + /** {@code purgeStaleTickets} 无锁遍历 + 修改 {@code tickets},与主线程 ticket 写并发时需要串行化。 */ + @WrapMethod(method = "purgeStaleTickets") + private void modzuozhi_lockPurgeStaleTickets(Operation original) { + LOCK.lock(); + try { + original.call(); + } finally { + LOCK.unlock(); + } + } + + /** 玩家登录/跨维度在 worker 或主线程改 {@code playersPerChunk},与 runAllUpdates 并发时串行化。 */ + @WrapMethod(method = "addPlayer") + private void modzuozhi_lockAddPlayer(SectionPos pos, ServerPlayer player, Operation original) { + LOCK.lock(); + try { + original.call(pos, player); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removePlayer") + private void modzuozhi_lockRemovePlayer(SectionPos pos, ServerPlayer player, Operation original) { + LOCK.lock(); + try { + original.call(pos, player); + } finally { + LOCK.unlock(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java new file mode 100644 index 0000000..e2fe82d --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java @@ -0,0 +1,45 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import javax.annotation.Nullable; + +/** + * dimthreads 式跨维度传送调度。 + *

+ * 各维度 tick 并行运行在不同 worker 上,实体容器 {@code Level.entityStorage} 非线程安全。 + * 若某维度 worker 直接调用 {@code changeDimension} 迁移实体到另一维度,会与其他 worker + * 并发修改实体容器/区块生命周期而崩溃或死锁。因此当调用线程是维度 worker 时,把真正的 + * 迁移登记到 {@link DimensionChangeQueue},由主线程在维度 tick 收口后、 + * {@code playerList.tick()} 之前统一执行(见 {@code MinecraftServerDimMixin})。 + *

+ * 与 dimthreads 原版用 {@code server.execute()} 的区别:execute 依赖 {@code runAllTasks} + * 在 tick 结束后才消费,传送会延迟到下一 tick,且如果期间玩家走开会被 stale 传送拉回 + * 传送门(反复拉扯)。本队列在同一 tick 收口处消费,传送即时完成,杜绝拉扯。 + */ +@Mixin(Entity.class) +public abstract class EntityChangeDimensionMixin { + + @Shadow + @Nullable + public abstract Entity changeDimension(DimensionTransition transition); + + @Inject(method = "changeDimension", at = @At("HEAD"), cancellable = true, remap = false) + public void modzuozhi_moveToWorld(DimensionTransition transition, CallbackInfoReturnable cir) { + if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) { + return; + } + if (DimThreadCore.owns(Thread.currentThread())) { + DimensionChangeQueue.schedule((Entity) (Object) this, transition); + cir.setReturnValue(null); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java new file mode 100644 index 0000000..ca57826 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java @@ -0,0 +1,55 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * 第11步(可选特性):禁用实体间碰撞(Mob Collision Off 模式,独立实现)。 + *

+ * 两个注入点(对齐 mob-collision-off 模组的 EntityCollideMixin + EntityMixin): + *

    + *
  • {@code @Redirect} {@code Entity.collide(Vec3)} 里的 {@code getEntityCollisions(...)}: + * 实体移动时不查询/计算附近实体的碰撞形状(消除 O(n²) 热点)。
  • + *
  • {@code @Inject} {@code Entity.push(Entity)} HEAD 取消:实体重叠时互相推开的 + * 独立推动机制也一并关闭(否则即使移动碰撞为空,重叠实体仍会被 push 挤开,表现为 + * "碰撞箱还在")。攻击击退走 {@code push(double,double,double)} 重载,不受影响。
  • + *
+ * 方块碰撞不受影响(走另一条路径 {@code BlockCollisions})。开关:{@code /dimensionalripper nocollide on|off}, + * 遵守全局开关:{@code /dimensionalripper off} 一并退化为原版。 + *

+ * 第14步(对齐 Async):第13步的"碰撞降频"(每 2 tick 评估一次碰撞)已回退——移除全局写锁、 + * 并发化实体存储后,碰撞开启也能并行跑,不再需要降频拐杖。本特性保留为可选开关(默认关闭 + * 碰撞以获取最大 TPS;需要生物交互时开碰撞)。 + */ +@Mixin(Entity.class) +public abstract class EntityGetEntityCollisionsMixin { + @Redirect(method = "collide(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;getEntityCollisions(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/AABB;)Ljava/util/List;")) + private List modzuozhi_noMobCollision(Level level, @Nullable Entity entity, AABB aabb) { + // 只受 nocollide 开关控制,不随全局开关失效:全局 /dimensionalripper off 后若恢复真实碰撞, + // 实体密集场景会回归 O(n²) 碰撞热点,主线程被拖到几乎无响应(压测巨量实体时实测卡死)。 + if (ModZuozhi.MOB_COLLISION_OFF) { + return List.of(); + } + return level.getEntityCollisions(entity, aabb); + } + + @Inject(method = "push(Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD"), cancellable = true) + private void modzuozhi_noPush(Entity other, CallbackInfo ci) { + if (ModZuozhi.MOB_COLLISION_OFF) { + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java new file mode 100644 index 0000000..19d8262 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java @@ -0,0 +1,38 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.collection.Int2ObjectConcurrentHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntityLookup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第14步(对齐 Async 零锁路线):{@code EntityLookup}(可见实体表)并发化。 + *

+ * {@code EntityLookup} 是 {@code PersistentEntitySectionManager.visibleEntityStorage}: + *

    + *
  • {@code byId}({@code Int2ObjectLinkedOpenHashMap})→ {@link Int2ObjectConcurrentHashMap} + * (内部 {@code ConcurrentHashMap},弱一致迭代);
  • + *
  • {@code byUuid}(普通 {@code HashMap})→ {@link ConcurrentHashMap}。
  • + *
+ * 实体并行 tick 下,{@code startTracking/stopTracking}(实体可见性变更,经 onMove/updateStatus/ + * addEntityWithoutEvent 触发)会从多个并行子任务线程并发读写该表。此前由全局写锁 + * {@code SECTION_LOCK} 串行化;第14步移除全局写锁后,改由并发集合承载,读写均线程安全。 + * {@code getEntities/getAllEntities} 遍历 {@code byId.values()}(弱一致),并发增删不抛 CME。 + * 用 {@code @Shadow} 初始值替换模式换成并发实例(与 {@code PoiSectionMixin}/{@code SectionStorageMixin} + * 相同的已验证模式,无需 AT)。 + */ +@Mixin(EntityLookup.class) +public abstract class EntityLookupMixin { + + @Shadow + private final Int2ObjectMap byId = new Int2ObjectConcurrentHashMap<>(); + + @Shadow + private final Map byUuid = new ConcurrentHashMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java new file mode 100644 index 0000000..ddac043 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java @@ -0,0 +1,40 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import net.minecraft.world.level.entity.Visibility; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第14步(对齐 Async 零锁路线):{@code PersistentEntitySectionManager} 共享状态并发化。 + *

+ * 第4步起,实体生命周期写入({@code addEntityWithoutEvent}/{@code onMove}/{@code onRemove})由全局写锁 + * {@code SECTION_LOCK} 串行化;第14步移除该全局写锁,改由并发集合承载,使实体移动/生成可真正并行: + *

    + *
  • {@code knownUuids}(普通 {@code HashSet})→ 并发 {@code Set}({@code ConcurrentHashMap} 视图), + * 保护 {@code addEntityUuid}/{@code isLoaded}/{@code onRemove} 的并发读写;
  • + *
  • {@code chunkVisibility}({@code Long2ObjectOpenHashMap})→ {@link ConcurrentLong2ObjectMap}, + * 保护 {@code createSection}/{@code updateStatus}/{@code canPositionTick} 对区块可见性的并发读 + * (写主要在服务器线程 {@code updateChunkStatus})。
  • + *
+ * 用 {@code @Shadow} 初始值替换模式(与 {@code PoiSectionMixin}/{@code SectionStorageMixin} 相同,无需 AT)。 + */ +@Mixin(PersistentEntitySectionManager.class) +public abstract class EntitySectionManagerMixin { + + /** 已注册实体 UUID 集合:并发的 {@code Set}(替换原 {@code HashSet},读写线程安全)。 */ + @Shadow + final Set knownUuids = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** 区块可见性表:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读线程安全)。 */ + @Shadow + private final Long2ObjectMap chunkVisibility = new ConcurrentLong2ObjectMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java new file mode 100644 index 0000000..ebed5d4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java @@ -0,0 +1,115 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.dimthread.ISectionKeyAccessor; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.minecraft.util.AbortableIterationConsumer; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** + * 第4/5步:实体 tick 并行——{@code EntitySection} 内部存储的并发读写互斥(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code EntitySection.storage} 是 {@code ClassInstanceMultiMap}(内部 {@code HashMap}/{@code ArrayList}, + * 非线程安全)。并行子任务中:村民大脑传感器({@code NearestLivingEntitySensor} → {@code getEntitiesOfClass}) + * 和移动碰撞检测({@code getEntityCollisions})会遍历 {@code storage}({@code getEntities}), + * 而实体移动({@code onMove})与区块加载({@code addEntityWithoutEvent})会在任意线程(子任务、维度 worker、 + * 区块加载线程、服务器主线程)增删该 {@code storage},二者并发即抛 {@code ConcurrentModificationException}, + * 中断实体 tick 导致实体卡进方块/遁地/窒息。 + *

+ * 第5步细粒度化:将第4步的全局锁 {@link EntityTickParallel#SECTION_LOCK} 降级为分片锁。 + * 本类注入 {@link #modzuozhi_sectionKey}(由 {@code EntitySectionStorageMixin} 在 createSection 时写入), + * 对 add/remove/getEntities 用该 key 定位分片锁——不同区域的实体 section 互不阻塞,真正并行。 + * 结构级操作(storage 遍历/创建/删除、manager、callback)仍持全局锁,锁序固定为 + * 全局锁 → 分片锁,无反向路径,不会死锁。 + * 用 {@link WrapMethod} 整方法包装,避免 {@code @WrapOperation at HEAD} 对含分支的方法注入失败。 + */ +@Mixin(EntitySection.class) +public abstract class EntitySectionMixin implements ISectionKeyAccessor { + /** 本 section 在 {@code EntitySectionStorage} 中的 key(SectionPos.asLong),由 storage 注入。 */ + @Unique + private long modzuozhi_sectionKey; + + @Override + @Unique + public void modzuozhi_setSectionKey(long key) { + this.modzuozhi_sectionKey = key; + } + + @Override + @Unique + public long modzuozhi_getSectionKey() { + return this.modzuozhi_sectionKey; + } + + @WrapMethod(method = "add") + private void modzuozhi_lockedAdd(EntityAccess entity, Operation original) { + // 内容写入:分片写锁(独占) + EntityTickParallel.lockSection(this.modzuozhi_sectionKey); + try { + original.call(entity); + } finally { + EntityTickParallel.unlockSection(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "remove") + private boolean modzuozhi_lockedRemove(EntityAccess entity, Operation original) { + EntityTickParallel.lockSection(this.modzuozhi_sectionKey); + try { + return original.call(entity); + } finally { + EntityTickParallel.unlockSection(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "getEntities(Lnet/minecraft/world/phys/AABB;Lnet/minecraft/util/AbortableIterationConsumer;)Lnet/minecraft/util/AbortableIterationConsumer$Continuation;") + private AbortableIterationConsumer.Continuation modzuozhi_lockedGetEntities( + AABB aabb, AbortableIterationConsumer consumer, + Operation original + ) { + // 内容遍历/查询:分片读锁(共享,传感器/碰撞查询并发执行) + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call(aabb, consumer); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "getEntities(Lnet/minecraft/world/level/entity/EntityTypeTest;Lnet/minecraft/world/phys/AABB;Lnet/minecraft/util/AbortableIterationConsumer;)Lnet/minecraft/util/AbortableIterationConsumer$Continuation;") + private AbortableIterationConsumer.Continuation modzuozhi_lockedGetEntitiesTyped( + EntityTypeTest typeTest, AABB aabb, + AbortableIterationConsumer consumer, + Operation original + ) { + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call(typeTest, aabb, consumer); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } + + /** + * {@code getEntities()} 返回的 {@code storage.stream()} 是惰性流,锁无法覆盖其消费阶段。 + * 消费方({@code PersistentEntitySectionManager.updateChunkStatus/storeChunkSections})可能在子任务 + * 并发增删时迭代底层 {@code ArrayList} 而抛 CME。这里在锁内收集成快照再返回,消费时必然安全。 + */ + @WrapMethod(method = "getEntities()Ljava/util/stream/Stream;") + private Stream modzuozhi_lockedGetEntitiesStream(Operation> original) { + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call().collect(Collectors.toList()).stream(); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java new file mode 100644 index 0000000..87660f4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java @@ -0,0 +1,61 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap; +import dev.modzuozhi.core.collection.ConcurrentLongSortedSet; +import dev.modzuozhi.core.dimthread.ISectionKeyAccessor; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.LongSortedSet; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 第4/11步:实体 tick 并行——{@code EntitySectionStorage} 结构存储并发化(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 第11步(SECTION_LOCK → 并发集合):把 {@code EntitySectionStorage.sections} + * ({@code Long2ObjectOpenHashMap})与 {@code sectionIds}({@code LongAVLTreeSet})替换为 + * {@link ConcurrentLong2ObjectMap}/{@link ConcurrentLongSortedSet}(弱一致、无 CME)。 + * 由此删除结构级读路径的全部全局读锁: + *

    + *
  • 读:{@code forEachAccessibleNonEmptySection} 的 {@code sectionIds.subSet().iterator()} + + * {@code sections.get} 现在无锁安全(原版源码对 {@code sections.get} 已有 null 检查, + * 并发创建 section 时读到 null 天然跳过);
  • + *
  • 写:{@code getOrCreateSection → computeIfAbsent} 原子、{@code createSection} 内部 + * {@code sectionIds.add} 与 {@code sections.put} 由 {@code ConcurrentHashMap.computeIfAbsent} + * 串行化,不会重复建 section。
  • + *
+ * 结构写(实体生命周期 {@code onMove/onRemove/addEntityWithoutEvent})仍由 + * {@link dev.modzuozhi.core.dimthread.EntityTickParallel#SECTION_LOCK} 全局写锁保护(本步不触碰), + * 因此实体移动与 section 内容(分片锁)的既有锁序不变。 + *

+ * 用 {@link WrapMethod} 整方法包装,避免 {@code @WrapOperation at HEAD} 对含分支的方法注入失败。 + *

+ * 第5步细粒度化:在 {@code createSection} 中把 section key 写入新创建的 + * {@code EntitySection}(见 {@link ISectionKeyAccessor}),供 {@code EntitySection} 的分片锁定位锁片。 + */ +@Mixin(EntitySectionStorage.class) +public abstract class EntitySectionStorageMixin { + + /** 结构存储:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读路径无锁安全)。 */ + @Shadow + private final Long2ObjectMap> sections = new ConcurrentLong2ObjectMap<>(); + + /** 有序 section key 集:并发的 {@code LongSortedSet}(替换原 {@code LongAVLTreeSet},subSet 弱一致)。 */ + @Shadow + private final LongSortedSet sectionIds = new ConcurrentLongSortedSet(); + + /** + * {@code createSection(long)} 是私有方法,创建 {@code EntitySection} 时把 section key 注入 + * 给新 section,使 {@code EntitySection} 的 add/remove/getEntities 能用分片锁定位锁片。 + */ + @WrapMethod(method = "createSection") + private EntitySection modzuozhi_createSection(long sectionKey, Operation original) { + EntitySection section = original.call(sectionKey); + ((ISectionKeyAccessor) (Object) section).modzuozhi_setSectionKey(sectionKey); + return section; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java new file mode 100644 index 0000000..94822c2 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java @@ -0,0 +1,44 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 第4步:实体 tick 并行——子任务线程对 {@code EntityTickList} 的写入延迟执行 + * (借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code EntityTickList} 的 {@code active} 字段在 {@code forEach} 期间被迭代。 + * 并行子任务中实体可能被移除/生成,触发 {@code EntityCallbacks.onTickingEnd/onTickingStart} + * 进而调用 {@code entityTickList.add/remove}。这些写入发生在子任务线程(非迭代线程), + * 直接修改 {@code active} 会破坏并发迭代。因此拦截 add/remove,在子任务上下文中投递到 + * {@link PostExecuteQueue},由维度 worker 在所有实体子任务完成后统一执行。 + *

+ * 非子任务上下文(串行/维度 worker 线程)行为不变,零开销。 + */ +@Mixin(EntityTickList.class) +public abstract class EntityTickListMixin { + + @Inject(method = "add", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferAdd(Entity entity, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> ((EntityTickList) (Object) this).add(entity)); + ci.cancel(); + } + } + + @Inject(method = "remove", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferRemove(Entity entity, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> ((EntityTickList) (Object) this).remove(entity)); + ci.cancel(); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java new file mode 100644 index 0000000..b4f2356 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java @@ -0,0 +1,78 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.filter.EntityTickFilter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Consumer; + +/** + * 第4步:实体 tick 并行——拦截 {@code Level.guardEntityTick},把白名单实体的 tick 提交到 + * 实体子任务屏障并行执行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 玩家始终走串行:{@code EntityTickFilter.isParallel} 对玩家恒为 false,因此玩家 + * 在维度 worker 的实体循环里按原版顺序 tick,绝不进入并行批。 + *

+ * 注意:不能再在 worker 上跳过玩家的实体 tick。1.21.1 的 + * {@code PlayerList.tick()} 并不调用 {@code ServerPlayer.tick()}(其只做延迟统计广播), + * 玩家实体 tick 唯一路径就是 {@code ServerLevel.tick} 的实体循环(维度 worker 执行)。 + * 若在这里跳过玩家,{@code ServerPlayer.tick()}(含 {@code AbstractContainerMenu + * .broadcastChanges()}、传送门、状态推进)将从不执行 → 熔炉/漏斗等容器 UI 进度条与 + * 物品数量永不刷新(实测),玩家相关游戏逻辑全部停滞。 + */ +@Mixin(Level.class) +public abstract class EntityTickParallelMixin { + + @Inject(method = "guardEntityTick", at = @At("HEAD"), cancellable = true) + private void modzuozhi_guardEntityTick(Consumer consumer, Entity entity, CallbackInfo ci) { + if (EntityTickParallel.current() == null || !EntityTickFilter.isParallel(entity)) { + // 非并行窗口或非白名单实体(含玩家):走原版串行(guardEntityTick 原文) + return; + } + // 首次启动 / 区块生成高峰:实体所在区块尚未 FULL 时跳过本 tick。 + // 原因:并行子任务线程对未生成区块做碰撞检测会拿到空气占位(见 ServerChunkCacheMixin + // 的非阻塞占位设计,为避免 join 生成死锁),村民会被持续下坠拉穿地面遁地;区块 FULL + // 后下一 tick 恢复正常。此处既不收集并行、也不走串行(串行在 worker 上 join 等生成 + // 会死锁),直接跳过是唯一安全选择。 + if (!modzuozhi_entityChunkReady((ServerLevel) (Object) this, entity)) { + ci.cancel(); + return; + } + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + // 收集到本 tick 的实体批,循环结束统一按块提交并行(减少逐实体提交的调度/屏障开销) + EntityTickParallel.collect(server, consumer, entity); + ci.cancel(); + } + + /** + * 实体所在区块是否已 FULL(线程无关,直接查 ChunkHolder 的 FULL future)。 + * 与 {@link ServerChunkCacheMixin} 的 subtask 缓存同源,不依赖共享 lastChunk 缓存(防撕裂误判)。 + */ + @Unique + private static boolean modzuozhi_entityChunkReady(ServerLevel level, Entity entity) { + long key = ChunkPos.asLong(entity.getBlockX() >> 4, entity.getBlockZ() >> 4); + try { + ServerChunkCache cache = level.getChunkSource(); + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) cache) + .modzuozhi_getVisibleChunkIfPresent(key); + if (holder == null) { + return false; + } + return holder.getFullChunkFuture() + .getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null) != null; + } catch (Throwable t) { + return false; // 保守:拿不到就跳过本 tick + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java b/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java new file mode 100644 index 0000000..cf1d308 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java @@ -0,0 +1,35 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.gameevent.GameEventListenerRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * 兜底修复 {@code GameEventDispatcher.post} 中 + * {@code chunkaccess.getListenerRegistry(j2)} 返回 null 导致的 NPE。 + *

+ * 背景:细粒度并行子任务线程(或维度 worker)执行实体 tick 时,GlowSquid 等实体的移动会触发 + * {@code GameEventDispatcher.post}。此时读取到的区块对象可能处于"正在生成/加载中"的不一致状态, + * 其 {@code getListenerRegistry(int)} 可能返回 null,随后 {@code visitInRangeListeners(...)} 直接 + * NPE,进而被 {@code FaultGuard} 计为子任务异常并自动降级关闭细粒度并行(生产环境多次复现: + * "…because the return value of …ChunkAccess.getListenerRegistry(int) is null")。 + *

+ * 修复:包装该调用,null 时回退到 {@link GameEventListenerRegistry#NOOP}(空实现,行为等同无监听器), + * 保证任何路径下都不会 NPE,细粒度并行不再因该问题自动降级。 + */ +@Mixin(net.minecraft.world.level.gameevent.GameEventDispatcher.class) +public abstract class GameEventDispatcherMixin { + + @WrapOperation( + method = "post", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/ChunkAccess;getListenerRegistry(I)Lnet/minecraft/world/level/gameevent/GameEventListenerRegistry;")) + private GameEventListenerRegistry modzuozhi_noopIfNull( + ChunkAccess chunk, int sectionY, Operation original) { + GameEventListenerRegistry registry = original.call(chunk, sectionY); + return registry != null ? registry : GameEventListenerRegistry.NOOP; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java new file mode 100644 index 0000000..ea141aa --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.behavior.GoToPotentialJobSite; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.npc.Villager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 跨维度安全:{@link GoToPotentialJobSite} 添加维度检查。 + *

+ * 原版 {@code checkExtraStartConditions}(1.21.1)只检查 {@code Activity.WORK},不做维度检查。 + * 当村民的 {@code POTENTIAL_JOB_SITE} 存了另一个维度的 {@code GlobalPos} 时,村民会启动行为 + * 并尝试在当前维度坐标下寻路到该位置(坐标语义错乱)。并行环境下(维度并行/细粒度并行)也需 + * 确保村民只寻路当前维度的 POI。 + *

+ * 在 {@code checkExtraStartConditions} 头部注入守卫:POTENTIAL_JOB_SITE 维度不匹配时直接返回 + * false,阻止行为启动(不覆盖原版逻辑,仅在维度不匹配时短路)。 + */ +@Mixin(GoToPotentialJobSite.class) +public class GoToPotentialJobSiteGuardMixin { + + @Inject(method = "checkExtraStartConditions(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;)Z", + at = @At("HEAD"), cancellable = true) + private void modzuozhi_guardDimension( + ServerLevel serverLevel, Villager villager, CallbackInfoReturnable cir + ) { + if (villager.getBrain().getMemory(MemoryModuleType.POTENTIAL_JOB_SITE) + .map(gp -> gp.dimension() != villager.level().dimension()) + .orElse(false)) { + cir.setReturnValue(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java b/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java new file mode 100644 index 0000000..c3d4e45 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java @@ -0,0 +1,23 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.entity.ai.gossip.GossipContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 流言表并发化(借鉴 Async 思路,独立实现)。 + *

+ * 并行 tick 下,村民之间交换流言({@code transferFrom})或读取声望时会跨线程读写彼此的 + * {@code GossipContainer.gossips}(如判断是否信任某个玩家、村民间声望值)。原版用普通 + * HashMap,跨线程读写存在数据竞争。替换为 {@link ConcurrentHashMap},读写均线程安全。 + */ +@Mixin(GossipContainer.class) +public abstract class GossipContainerMixin { + + @Shadow + private final Map gossips = new ConcurrentHashMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java b/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java new file mode 100644 index 0000000..57bffb9 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java @@ -0,0 +1,19 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.IMutableMainThread; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * 让 {@link Level} 实现 {@link IMutableMainThread},暴露其 {@code thread} 字段, + * 供 {@code DimThreadCore.swapThreadsAndRun} 临时迁移主线程引用。 + */ +@Mixin(Level.class) +public interface ILevelMixin extends IMutableMainThread { + @Accessor("thread") + void dimThreads$setMainThread(Thread t); + + @Accessor("thread") + Thread dimThreads$getMainThread(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java new file mode 100644 index 0000000..0af2d08 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java @@ -0,0 +1,32 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.gameevent.GameEventListenerRegistry; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 区块游戏事件监听器注册表并发访问加固。 + *

+ * 细粒度并行时,实体 tick 触发 {@code gameEvent → GameEventDispatcher.post → + * getListenerRegistry},子任务线程与维度 worker/服务器线程会并发访问 + * {@code LevelChunk.gameEventListenerRegistrySections}(fastutil + * {@code Int2ObjectOpenHashMap},非线程安全)。并发 {@code computeIfAbsent} 会在 rehash 时 + * 损坏哈希表(实测抛 {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}), + * 导致服务器崩溃。 + *

+ * 修复:把 {@code getListenerRegistry} 整体加 {@code synchronized(this)}(单块锁、逐调用、 + * 无嵌套,无死锁;gameEvent 发布频率低,开销可忽略),串行化该 map 的所有读写入口 + * ({@code GameEventDispatcher} 与 {@code LevelChunk.add/removeGameEventListener} 都走此方法)。 + */ +@Mixin(LevelChunk.class) +public abstract class LevelChunkMixin { + + @WrapMethod(method = "getListenerRegistry") + private GameEventListenerRegistry modzuozhi_getListenerRegistry(int sectionY, Operation original) { + synchronized (this) { + return original.call(sectionY); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java new file mode 100644 index 0000000..e6351ab --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java @@ -0,0 +1,49 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** + * 爆炸全链路串行化(借鉴 Async 思路,独立实现)。 + *

+ * 爆炸会批量写入区块方块状态({@code Explosion.explode} → {@code Level.setBlock} 写 + * 非线程安全的 {@code PalettedContainer})。并行环境下,若两个爆炸并发执行(或多个爆炸实体/ + * TNT 方块/指令/下界床等不同触发源同时引爆),会并发写同一/相邻区块 → 方块状态撕裂损坏 + * (放置方块消失、被破坏方块"复活")。爆炸实体已由 {@code EntityTickFilter} 黑名单强制串行, + * 但爆炸还有 TNT 方块、下界床/重生锚、指令等其它触发源,需在方法入口兜底。 + *

+ * 原理:{@code Level} 上所有 {@code explode(...)} 重载与 {@code ServerLevel} 的覆写最终都 + * 委托到 13 参重载(唯一真正创建 {@code Explosion} 并执行 {@code explode()}+ + * {@code finalizeExplosion()} 的方法)。因此只需给该终端重载加一把静态全局锁,所有 + * 触发源的爆炸便全部互斥串行。爆炸低频、锁竞争可忽略;该方法不递归调用 + * {@code Level.explode},静态单锁无锁序环,无死锁风险。 + */ +@Mixin(Level.class) +public abstract class LevelExplodeMixin { + + /** 全局爆炸互斥锁(跨维度/跨触发源统一串行)。 */ + @Unique + private static final Object MODZUOZHI_EXPLODE_LOCK = new Object(); + + @WrapMethod(method = "explode(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lnet/minecraft/world/level/ExplosionDamageCalculator;DDDFZLnet/minecraft/world/level/Level$ExplosionInteraction;ZLnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/Holder;)Lnet/minecraft/world/level/Explosion;") + private Explosion modzuozhi_explode( + Entity source, DamageSource damageSource, ExplosionDamageCalculator damageCalculator, + double x, double y, double z, float radius, boolean fire, Level.ExplosionInteraction interaction, + boolean spawnParticles, ParticleOptions smallExplosionParticles, ParticleOptions largeExplosionParticles, + Holder explosionSound, Operation original) { + synchronized (MODZUOZHI_EXPLODE_LOCK) { + return original.call(source, damageSource, damageCalculator, x, y, z, radius, fire, interaction, + spawnParticles, smallExplosionParticles, largeExplosionParticles, explosionSound); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java new file mode 100644 index 0000000..03e7add --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java @@ -0,0 +1,40 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 让 {@code Level.getChunk(int, int)} 对「区块未就绪」场景健壮。 + *

+ * 原版 {@code Level.getChunk(int, int)} 直接 {@code (LevelChunk) this.getChunk(x, z, FULL)}。 + * 服务端主线程下,若目标区块尚未 FULL,会先同步完成生成再返回真实 {@code LevelChunk},强转不会失败。 + *

+ * 但细粒度并行 / 维度并行下,子任务线程(伪装成主线程走 {@code ServerChunkCache} 主线程路径)请求 + * 一个正被主线程生成的相邻区块时,返回的是 {@code ImposterProtoChunk}(世界生成占位块, + * {@code extends ProtoChunk},并非 {@code LevelChunk} 子类),强转失败抛 + * {@code ClassCastException}(例如草/泥土蔓延 {@code SpreadingSnowyDirtBlock.randomTick} 通过 + * {@code getBlockState} 访问相邻未就绪区块时)。 + *

+ * 这里在强转前,若结果是 {@code ImposterProtoChunk},则取其 {@code getWrapped()}(其内部委托的真实 + * {@code LevelChunk},其 {@code getBlockState}/{@code getSections} 均转发到 wrapped),既避免崩溃, + * 语义也与原版一致。单线程场景该分支从不触发,是空操作。 + */ +@Mixin(Level.class) +public abstract class LevelGetChunkMixin { + @Redirect(method = "getChunk(II)Lnet/minecraft/world/level/chunk/LevelChunk;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;)Lnet/minecraft/world/level/chunk/ChunkAccess;")) + private ChunkAccess modzuozhi_getRealChunk(Level level, int x, int z, ChunkStatus status) { + ChunkAccess chunk = level.getChunk(x, z, status); + if (chunk instanceof ImposterProtoChunk imposter) { + return imposter.getWrapped(); + } + return chunk; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelMixin.java new file mode 100644 index 0000000..9f1ce60 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelMixin.java @@ -0,0 +1,56 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * dimthreads 式 {@code Level.getBlockEntity} 线程守卫伪装。 + *

+ * 原版 {@code Level.getBlockEntity} 有线程守卫:服务端且当前线程 != 世界构造时捕获的 + * {@code thread} 时返回 null。维度并行下,整个 {@code ServerLevel.tick} 运行在池中 worker 上, + * 该守卫会把 worker 的一切 {@code getBlockEntity} 返回 null,导致漏斗/箱子合并/比较器等 + * 依赖 {@code getBlockEntity} 的逻辑失效(漏斗 NPE 等)。 + *

+ * 在 {@code swapThreadsAndRun} 期间本维度的 {@code thread} 已被设为当前 worker,守卫本应通过; + * 这里额外处理「另一维度 worker 访问本维度方块实体」的情况:当调用者(t)与属主(this.thread) + * 都是维度 worker 时,把当前线程伪装成属主线程,使守卫通过(属主 worker 此刻 tick 本维度, + * 是唯一写入者,读方直接同步读取安全)。 + */ +@Mixin(Level.class) +public abstract class LevelMixin { + @Shadow + public Thread thread; + + /** + * 将 {@code Level.random}(及 {@code randValue})初始化为线程安全随机源。 + *

+ * 原版 {@code Level.random = RandomSource.create()} 得到 {@code LegacyRandomSource}, + * 其内部通过 {@code ThreadingDetector} 检测跨线程访问并抛异常。维度并行/细粒度并行下, + * 实体 AI(村民、群游鱼等)与服务器主线程会并发访问 {@code getRandom()},导致 + * "Accessing LegacyRandomSource from multiple threads"。这里重定向构造期间的 + * {@code RandomSource.create()} 为 {@code createThreadSafe()}({@code ThreadSafeLegacyRandomSource}, + * 基于 AtomicLong CAS,无线程检测),使并发访问安全。 + */ + @Redirect(method = "", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;create()Lnet/minecraft/util/RandomSource;")) + private static RandomSource modzuozhi_threadSafeRandom() { + return RandomSource.createThreadSafe(); + } + + @Redirect(method = "getBlockEntity", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + private Thread modzuozhi_recallForDimthreads() { + Thread t = Thread.currentThread(); + // 维度 worker 访问本维度方块实体时,伪装成属主线程,使守卫通过; + // 细粒度子任务线程(subtask)同样需要伪装,否则 getBlockEntity 返回 null + boolean callerIsActive = DimThreadCore.owns(t) || FineGrainScheduler.inSubTask(); + boolean ownerIsActive = DimThreadCore.owns(this.thread); + return callerIsActive && ownerIsActive ? this.thread : t; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java new file mode 100644 index 0000000..662e3e1 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java @@ -0,0 +1,47 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import net.minecraft.core.BlockPos; +import net.minecraft.world.ticks.LevelTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第6步:区块调度({@code LevelTicks})并发安全——并行子任务中的 tick 调度写入延迟到 + * 维度 worker 收口串行执行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 时序背景:{@code ServerLevel.tick} 中 {@code blockTicks.tick / fluidTicks.tick}(收集并执行 + * 计划内 tick)与 {@code runBlockEvents} 都运行在维度 worker 线程(串行阶段), + * 不在细粒度并行窗口内,天然线程安全。 + *

+ * 风险点:并行子任务(方块实体 tick、区块随机 tick)里某些方块/方块实体会调用 + * {@code level.scheduleTick(...)} → {@code LevelTicks.schedule},多个子任务线程并发写 + * {@code LevelTicks} 内部的 {@code Long2ObjectOpenHashMap}/{@code PriorityQueue}(非线程安全), + * 会损坏调度结构,导致计划内 tick 丢失、方块行为异常。 + *

+ * 修复:在并行子任务上下文({@link FineGrainScheduler#inSubTask()})中,把 {@code schedule} + * 投递到 {@link PostExecuteQueue},由维度 worker 在本维度 tick 收口处(所有并行子任务已 + * {@code await} 完成、无并发)串行执行。由于收口发生在 {@code blockTicks.tick} 之后,新增 + * 调度会排到下个 tick执行,与原版"当前 tick 之后"语义一致。关闭并行时零开销(串行)。 + *

+ * 说明:读方法({@code hasScheduledTick}/{@code willTickThisTick})在子任务中几乎不被调用, + * 且读多写少、正确性可容忍偶发旧值,不做延迟(保持原样)。 + */ +@Mixin(LevelTicks.class) +public abstract class LevelTicksMixin { + + @WrapMethod(method = "schedule") + private void modzuozhi_deferSchedule(ScheduledTick tick, Operation original) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue == null) { + // 非并行子任务上下文:原版直接调度 + original.call(tick); + return; + } + // 并行子任务:延迟到维度 worker 收口串行调度,避免并发写 LevelTicks 内部结构 + queue.post(() -> original.call(tick)); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java b/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java new file mode 100644 index 0000000..83a6db8 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java @@ -0,0 +1,29 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 防御:{@code Entity.getInBlockState()} 空值兜底。 + *

+ * 罕见 NPE 根因:并行子任务线程上,{@code getInBlockState()} 读取实体所在位置方块状态时 + * 偶发返回 {@code null}(区块读路径的边缘竞态),随后该值被传给 + * {@code CommonHooks.isLivingOnLadder(state, ...)} 并直接 {@code state.isLadder(...)} → NPE + * (日志:[FineGrain] 子任务异常 ... because "state" is null,50 分钟约 1 次,被屏障捕获不降级)。 + *

+ * 注意:{@code getInBlockState()} 定义在 {@link Entity}(父类)而非 {@code LivingEntity}, + * 因此 {@code @WrapMethod} 必须目标 {@code Entity.class}(mixin 只包装目标类声明的方法)。 + * 这里对返回值做空值兜底(null → 空气方块),从根源上杜绝该路径的 NPE;正常情况零改动。 + */ +@Mixin(Entity.class) +public abstract class LivingEntityGetInBlockStateMixin { + @WrapMethod(method = "getInBlockState") + private BlockState modzuozhi_nullSafeInBlockState(Operation original) { + BlockState state = original.call(); + return state == null ? Blocks.AIR.defaultBlockState() : state; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java new file mode 100644 index 0000000..2b71381 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java @@ -0,0 +1,88 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.TpsTracker; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.function.BooleanSupplier; + +/** + * 维度独立 tick 循环分发(替代原「有界屏障 awaitCompletion」模型,借鉴 Chlorophyll 思路)。 + *

+ * 目标:每个 {@link ServerLevel} 一个常驻 {@code LevelTickLoop},各自按 tick 预算 + * 独立排下一次 tick。某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再由主线程 + * 统一 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。 + *

+ * 主线程 {@code tickChildren} 退化为「协调者」:命令函数、基础连接(登录/状态/配置)、 + * {@code playerList.tick()}、tickables 仍留在主线程;每个维度的「level.tick + 玩家连接 tick + * + 区块发送/刷新」由该维度 loop 在 worker 线程独立完成(见 {@code ServerConnectionListenerMixin} + * 跳过游戏连接、{@code LevelTickLoop.tickConnections} 承接游戏连接 tick,杜绝玩家双 tick)。 + *

+ * 全局关闭或未激活时,原方法体照常执行(退回原版串行,零副作用)。 + */ +@Mixin(value = MinecraftServer.class, priority = 1010) +public abstract class MinecraftServerDimMixin { + + @Shadow + @Final + private List tickables; + + /** + * 主线程每 tick 的协调入口:只做全局部分 + 启动各维度 loop。 + * 命中了维度循环模型的场景才 {@code cancel()} 原方法体。 + */ + @Inject(method = "tickChildren", at = @At("HEAD"), cancellable = true) + private void modzuozhi_worldizedTickChildren(BooleanSupplier hasTimeLeft, CallbackInfo ci) { + // 无论开关状态,每 tick 记录 TPS(供 /dimensionalripper status 对比优化效果) + TpsTracker.tick(); + + MinecraftServer self = (MinecraftServer) (Object) this; + if (!DimThreadCore.MANAGER.isActive(self)) { + return; // 未开启:走原版串行 tickChildren + } + + // 1. 命令函数(主线程全局) + self.getFunctions().tick(); + + // 2. 启动每个维度的独立 loop(只启动一次,loop 此后自排程) + for (ServerLevel level : self.getAllLevels()) { + DimThreadCore.MANAGER.getLoop(level).scheduleIfNeeded(); + } + + // 3. 跨维度传送收口(loop 线程登记的传送,由主线程执行,见 EntityChangeDimensionMixin) + DimensionChangeQueue.flush(); + + // 4. 基础连接(登录/状态/配置)——游戏连接已由维度 loop tick(ServerConnectionListenerMixin) + self.getConnection().tick(); + + // 4.5 连接回收:登录/跨维度/重生后,把每个在线玩家的连接路由到其当前维度 loop(幂等) + DimThreadCore.MANAGER.reconcileConnections(self); + + // 5. playerList.tick(统计/延迟,主线程) + self.getPlayerList().tick(); + + // 6. tickables(服务器 GUI 刷新等) + for (Runnable tickable : this.tickables) { + tickable.run(); + } + + ci.cancel(); + } + + /** 服务器停止时:先停维度 loop,再关闭线程池,防止挂起。 */ + @Inject(method = "stopServer", at = @At("HEAD")) + public void modzuozhi_shutdownThreadpool(CallbackInfo ci) { + DimThreadCore.MANAGER.killAllLoops(); + DimThreadCore.MANAGER.threadPools.forEach((server, pool) -> pool.shutdown()); + DimThreadCore.MANAGER.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java new file mode 100644 index 0000000..22adaf4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java @@ -0,0 +1,110 @@ +package dev.modzuozhi.mixin; + +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.ThreadPool; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.WorldData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 多维度并行保存(dimthreads 式)。 + *

+ * 原版 {@code MinecraftServer.saveAllChunks} 在 for 循环里串行遍历所有维度, + * 逐个调用 {@code serverlevel.save(...)} 写区块/实体。当维度多且区块实体量大时,保存耗时线性叠加, + * 主线程被同步写盘阻塞数秒,表现为 "Can't keep up"。 + *

+ * 本 mixin 用 {@code @Inject HEAD + cancellable} 完整接管 {@code saveAllChunks}: + *

    + *
  1. 把每个维度的 {@code serverlevel.save(...)} 分发到线程池并行执行(各维度目录独立,写盘互不干扰,安全);
  2. + *
  3. {@code awaitCompletion()} 等待全部维度写完;
  4. + *
  5. 主线程串行补齐原方法下半段的全局保存(worldData / boss events / level.dat 标签), + * 这些共享状态不能并行。
  6. + *
+ * 维度越多、区块实体越多,加速越明显。 + */ +@Mixin(value = MinecraftServer.class, priority = 1010) +public abstract class MinecraftServerSaveMixin { + @Shadow + @Final + protected WorldData worldData; + @Shadow + @Final + protected LevelStorageSource.LevelStorageAccess storageSource; + + @Inject(method = "saveAllChunks", at = @At("HEAD"), cancellable = true) + public void modzuozhi_parallelSaveAllChunks(boolean suppressLog, boolean flush, boolean forced, + CallbackInfoReturnable cir) { + MinecraftServer self = (MinecraftServer) (Object) this; + if (!DimThreadCore.MANAGER.isActive(self)) { + return; // 未开启:走原版串行路径 + } + + // 保存前先排空细粒度子任务,避免残留子任务与实体/区块保存并发访问(残留任务此刻 + // 若卡死,Barrier 超时降级已在维度 loop 侧触发,这里再给一个短等待兜底)。 + FineGrainScheduler.awaitIdle(2000L); + + // 1. 并行保存每个维度的区块/实体(独立写盘) + List levels = new ArrayList<>(); + self.getAllLevels().forEach(levels::add); + + ThreadPool pool = DimThreadCore.getThreadPool(self); + AtomicReference crash = new AtomicReference<>(); + + for (ServerLevel level : levels) { + pool.execute(() -> { + DimThreadCore.attach(Thread.currentThread(), level); + DimThreadCore.swapThreadsAndRun(() -> { + try { + // 等价于原版 serverlevel.save(null, flush, noSave) + level.save(null, flush, level.noSave && !forced); + } catch (Throwable t) { + crash.set(t); + } + }, level, level.getChunkSource()); + }); + } + // 带超时等待:某维度保存任务若被卡死的线程持锁阻塞,绝不永久挂起主线程(否则 + // 表现为「正在保存中」死锁无法退出)。 + if (!pool.awaitCompletion(FaultGuard.AWAIT_TIMEOUT_MS)) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存超时(>{}ms),跳过等待继续主线程全局保存", FaultGuard.AWAIT_TIMEOUT_MS); + } + + Throwable t = crash.get(); + if (t != null) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存维度异常", t); + } + + // 2. 主线程串行补齐全局保存(与原版 saveAllChunks 下半段完全一致) + ServerLevel overworld = self.overworld(); + ServerLevelData serverleveldata = this.worldData.overworldData(); + serverleveldata.setWorldBorder(overworld.getWorldBorder().createSettings()); + this.worldData.setCustomBossEvents(self.getCustomBossEvents().save(self.registryAccess())); + this.storageSource.saveDataTag(self.registryAccess(), this.worldData, + self.getPlayerList().getSingleplayerData()); + + if (flush) { + for (ServerLevel serverlevel : levels) { + ModZuozhi.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", + serverlevel.getChunkSource().chunkMap.getStorageName()); + } + ModZuozhi.LOGGER.info("ThreadedAnvilChunkStorage: All dimensions are saved"); + } + + cir.setReturnValue(true); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java new file mode 100644 index 0000000..1135696 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java @@ -0,0 +1,16 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * 暴露原版私有的 {@code MinecraftServer.synchronizeTime(ServerLevel)},供维度 loop 在每次 + * 时间同步(每 20 tick)时用原版逻辑(含 NeoForge 自定义时间载荷)广播时间包。 + */ +@Mixin(MinecraftServer.class) +public interface MinecraftServerTimeSyncAccessor { + @Invoker("synchronizeTime") + void modzuozhi$invokeSynchronizeTime(ServerLevel level); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java b/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java new file mode 100644 index 0000000..5c0a2ba --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java @@ -0,0 +1,34 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Mob; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第7步:寻路/AI 并发安全——{@code ServerLevel.navigatingMobs} 并发化(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code navigatingMobs} 记录当前正在寻路的 {@link Mob},原为 {@code ObjectOpenHashSet}(非线程安全): + *

    + *
  • 写入:{@code onTrackingStart/onTrackingEnd}(实体追踪变化时 {@code add}/{@code remove}), + * 可能来自并行实体子任务线程(实体 tick 触发追踪变化)、维度 worker、区块加载线程;
  • + *
  • 遍历:{@code sendBlockUpdated}(方块变化时)遍历所有 mob 的 navigation 判断是否重算路径, + * 同样可能被任意线程触发。
  • + *
+ * 两者并发会抛 {@code ConcurrentModificationException} 或破坏集合,导致寻路异常/崩溃。 + *

+ * 修复:替换为 {@link ConcurrentHashMap#newKeySet()} 背书的并发集合(线程安全、迭代弱一致、 + * 不抛 CME)。原版串行行为不变,零语义影响。 + */ +@Mixin(ServerLevel.class) +public abstract class NavigatingMobsMixin { + @Shadow + @Final + @Mutable + private Set navigatingMobs = ConcurrentHashMap.newKeySet(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java b/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java new file mode 100644 index 0000000..1c4a0af --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java @@ -0,0 +1,72 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.sensing.NearestLivingEntitySensor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * 第10步:{@code NearestLivingEntitySensor} 排序快照化,消除并发 TimSort 异常。 + *

+ * 原版 {@code doTick} 里 {@code list.sort(Comparator.comparingDouble(entity::distanceToSqr))} + * 的比较器会实时读取被排序实体的可变坐标。并行 tick 下,其它子任务线程正同时移动这些 + * 实体,导致比较器前后不一致(破坏传递性)→ {@code TimSort} 抛 + * {@code IllegalArgumentException: Comparison method violates its general contract}(日志已证实)。 + *

+ * 修复:排序前先对每个实体快照一次到锚点(被 tick 实体)的距离,按快照距离稳定排序并 + * 回写——比较器全程一致,既保留"按距离排序"语义,又不触发 TimSort 异常。 + *

+ * 线程安全:每个 {@code NearestLivingEntitySensor} 实例属于单个实体的大脑,仅被该实体自己的 + * 子任务线程调用,字段无跨线程竞争。 + */ +@Mixin(NearestLivingEntitySensor.class) +public abstract class NearestLivingEntitySensorMixin { + /** 当前 tick 的锚点(被 tick 的实体)。 */ + @Unique + private LivingEntity modzuozhi_anchor; + + @Inject(method = "doTick", at = @At("HEAD")) + private void modzuozhi_captureAnchor(ServerLevel level, LivingEntity entity, CallbackInfo ci) { + this.modzuozhi_anchor = entity; + } + + @Redirect(method = "doTick", + at = @At(value = "INVOKE", target = "Ljava/util/List;sort(Ljava/util/Comparator;)V")) + private void modzuozhi_snapshotSort(List list, Comparator comparator) { + LivingEntity anchor = this.modzuozhi_anchor; + if (anchor == null) { + // 兜底:正常不会触发 + list.sort(comparator); + this.modzuozhi_anchor = null; + return; + } + int n = list.size(); + double[] dist = new double[n]; + for (int i = 0; i < n; i++) { + dist[i] = list.get(i).distanceToSqr(anchor); + } + Integer[] order = new Integer[n]; + for (int i = 0; i < n; i++) { + order[i] = i; + } + // 快照距离固定 → 比较器一致 → 不会抛 TimSort 异常(稳定排序,等距保持原顺序) + Arrays.sort(order, (a, b) -> Double.compare(dist[a], dist[b])); + List sorted = new ArrayList<>(n); + for (int index : order) { + sorted.add(list.get(index)); + } + list.clear(); + list.addAll(sorted); + this.modzuozhi_anchor = null; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java b/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java new file mode 100644 index 0000000..29da5d1 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java @@ -0,0 +1,111 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.IPlayerPacketQueue; +import dev.modzuozhi.core.dimthread.PlayerPacketQueue; +import net.minecraft.ReportedException; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketUtils; +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; +import net.minecraft.network.protocol.game.ServerboundChatCommandSignedPacket; +import net.minecraft.network.protocol.game.ServerboundChatPacket; +import net.minecraft.server.RunningOnDifferentThreadException; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.thread.BlockableEventLoop; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 第2步:网络包按玩家分派到维度线程——包处理入口重定向(1.21.1 API 适配版)。 + *

+ * 1.21.1 的 {@code PacketUtils.ensureRunningOnSameThread} 有 3 参数重载: + * {@code (Packet, T, ServerLevel)} 和 {@code (Packet, T, BlockableEventLoop)}。 + * 真实包链路(1.21.1):Netty IO 线程 {@code Connection.channelRead0 → genericsFtw → + * packet.handle(listener)} 直接调用 handle;而 {@code ServerGamePacketListenerImpl} 几乎 + * 每个 {@code handleXxx} 方法开头都会调 {@code ensureRunningOnSameThread(ServerLevel)}, + * 非主线程时投递到主线程队列并抛 {@code RunningOnDifferentThreadException}(由 + * {@code genericsFtw} 吞掉),主线程队列执行时才真正跑 handle 逻辑。 + *

+ * 借鉴 Chlorophyll(Unlicense 1.21.10 Fabric)的 {@code PacketUtilsMixin} 思路,但适配 + * 1.21.1 NeoForge 签名:全局开启且是游戏包时,把 handle 任务投递到该玩家所在维度的 + * {@code PlayerPacketQueue}(替代投主线程队列),由维度 worker 在 tick 收口处执行。 + * 登录/配置/状态包仍走原版 {@code BlockableEventLoop} 重载。 + *

+ * 防无限循环(必须):包投到维度队列后,worker drain 执行 {@code packet.handle}, + * handle 开头会再次调本方法。因此用 {@link PlayerPacketQueue#isExecuting()} 标记(drain 中 + * 设置)区分两种状态: + *

    + *
  • Netty 线程首次调度(未在展开中、非主线程)→ 投玩家维度队列 + 抛异常;
  • + *
  • 维度 worker / 主线程 drain 中执行 handle({@code isExecuting()} 或主线程)→ + * 直接返回,让 handle 继续执行,避免包被无限投回队列。
  • + *
+ *

+ * 死锁评估:{@code ServerGamePacketListenerImpl} 无 {@code executeBlocking};唯一的 + * {@code ServerCommonPacketListenerImpl.disconnect} 的 {@code executeBlocking} 已由 + * {@code ServerCommonPacketListenerDimMixin} 异步化,worker 不阻塞。 + */ +@Mixin(PacketUtils.class) +public abstract class PacketUtilsDimMixin { + + @Shadow + @Final + private static Logger LOGGER; + + @Shadow + public static ReportedException makeReportedException(Exception exception, Packet packet, T packetListener) { + throw new AssertionError(); + } + + /** + * @reason 把游戏包处理按玩家分派到其所在维度线程(第2步) + */ + @Overwrite + public static void ensureRunningOnSameThread( + Packet packet, T packetListener, ServerLevel serverLevel + ) throws RunningOnDifferentThreadException { + if (packetListener instanceof ServerGamePacketListenerImpl && ModZuozhi.isEnabled()) { + // 命令/聊天包改走原版主线程(BlockableEventLoop): + // 维度 loop 一旦被实体/区块 tick 阻塞,命令会跟着延迟 1-2 分钟才执行;而主线程只做 + // 协调(不 tick 实体),能保持约 50ms 一拍,命令可秒响应。聊天广播是全局的,卡顿维度 + // 的聊天包被拖住会连累其它维度的玩家延迟收到消息,故聊天/命令都走主线程。 + boolean routeMainThread = packet instanceof ServerboundChatCommandPacket + || packet instanceof ServerboundChatCommandSignedPacket + || packet instanceof ServerboundChatPacket; + if (!routeMainThread) { + // 两种情况不拦截,直接让 handle 继续执行(等价原版"已在正确线程"): + // 1. PlayerPacketQueue.drain() 正在执行包任务(维度 worker / 主线程串行) + // 2. 当前已在主线程(原版主线程队列执行路径) + if (PlayerPacketQueue.isExecuting() || serverLevel.getServer().isSameThread()) { + return; + } + // 首次调度(Netty IO 线程):投递到玩家所在维度的队列,由该维度线程在 tick 收口执行 + ServerLevel playerLevel = (ServerLevel) ((ServerGamePacketListenerImpl) packetListener).player.level(); + ((IPlayerPacketQueue) playerLevel).modzuozhi$playerPacketQueue().offer(() -> { + if (packetListener.shouldHandleMessage(packet)) { + try { + packet.handle(packetListener); + } catch (Exception e) { + if (e instanceof ReportedException reportedException + && reportedException.getCause() instanceof OutOfMemoryError) { + throw makeReportedException(e, packet, packetListener); + } + packetListener.onPacketError(packet, e); + } + } else { + LOGGER.debug("Ignoring packet due to disconnection: {}", packet); + } + }); + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; + } + // routeMainThread == true:落到下方走原版 BlockableEventLoop(主线程)路径 + } + + // 登录/配置/状态包,或全局关闭:走原版 BlockableEventLoop 重载 + PacketUtils.ensureRunningOnSameThread(packet, packetListener, (BlockableEventLoop) serverLevel.getServer()); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java b/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java new file mode 100644 index 0000000..32c4960 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java @@ -0,0 +1,95 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FaultGuard; +import dev.modzuozhi.core.dimthread.ThreadPool; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 第3步:自动保存分布化——玩家保存按维度分发到各维度线程并行执行。 + *

+ * 原版 {@code PlayerList.saveAll()} 在主线程串行遍历所有玩家,逐个 + * {@code playerIo.save(player)} 写玩家数据(+ 统计 + 进度)。玩家多时主线程被同步写盘 + * 阻塞数秒。玩家数据按 UUID 写独立文件,维度间、玩家间均无共享写盘目标,可安全并行。 + *

+ * 本 mixin 用 {@code @Inject HEAD + cancellable} 接管 {@code saveAll}(维度并行激活时): + *

    + *
  1. 把在线玩家按所在维度分组;
  2. + *
  3. 每个维度在对应维度 worker 上串行保存该维度玩家(维度内保序、维度间并行);
  4. + *
  5. {@code awaitCompletion()} 等待全部维度写完。
  6. + *
+ * 与 {@code MinecraftServerSaveMixin}(维度区块/实体保存已并行化)配合,自动保存的 + * savePlayers + saveLevel 都落在各维度线程上,主线程不再亲自执行保存逻辑。 + *

+ * 关服时 {@code MinecraftServerDimMixin.stopServer} 已先 shutdown 线程池并 clear,本 mixin + * 经 {@code DimThreadCore.MANAGER.isActive} 判定失效,自动退回原版串行(安全)。 + */ +@Mixin(PlayerList.class) +public abstract class PlayerListSaveMixin { + + @Shadow + protected abstract void save(ServerPlayer player); + + @Inject(method = "saveAll", at = @At("HEAD"), cancellable = true) + private void modzuozhi_parallelSaveAll(CallbackInfo ci) { + PlayerList self = (PlayerList) (Object) this; + MinecraftServer server = self.getServer(); + if (!DimThreadCore.MANAGER.isActive(server)) { + return; // 未开启 / 关服收尾:走原版串行 + } + + // 按玩家所在维度分组(等价 save() 内部的 connection==null 提前返回) + Map> byDimension = new LinkedHashMap<>(); + for (ServerPlayer player : self.getPlayers()) { + if (player.connection == null) { + continue; + } + byDimension.computeIfAbsent(player.serverLevel(), key -> new ArrayList<>()).add(player); + } + if (byDimension.isEmpty()) { + return; // 无在线玩家:直接走原版(saveAll 空循环,无副作用) + } + + ThreadPool pool = DimThreadCore.getThreadPool(server); + AtomicReference crash = new AtomicReference<>(); + + byDimension.forEach((level, players) -> pool.execute(() -> { + DimThreadCore.attach(Thread.currentThread(), level); + DimThreadCore.swapThreadsAndRun(() -> { + try { + for (ServerPlayer player : players) { + this.save(player); + } + } catch (Throwable t) { + crash.set(t); + } + }, level); + })); + + if (!pool.awaitCompletion(FaultGuard.AWAIT_TIMEOUT_MS)) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存玩家超时(>{}ms),跳过等待继续", FaultGuard.AWAIT_TIMEOUT_MS); + } + + Throwable t = crash.get(); + if (t != null) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存玩家异常", t); + } + + ci.cancel(); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java new file mode 100644 index 0000000..59ba56c --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java @@ -0,0 +1,69 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.PoiCompetitorScan; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.Villager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 跨维度安全:{@link PoiCompetitorScan} 添加维度检查。 + *

+ * 原版 {@code PoiCompetitorScan.create()} 不做维度检查,当村民的 {@code JOB_SITE} 存了 + * 另一个维度的 {@code GlobalPos} 时,会跨维度访问 POI 管理器。并行环境下(维度线程/子任务线程) + * 跨维度访问虽已由 POI 数据结构加固保障安全,但 AI 逻辑本身不应跨维度竞争工作站。 + *

+ * 借鉴 Chlorophyll(Unlicense)的 {@code PoiCompetitorScanMixin} 思路,在竞争逻辑前 + * 增加 {@code globalPos.dimension() != villager.level().dimension()} 检查,维度不匹配时 + * 直接跳过(不参与竞争)。 + */ +@Mixin(PoiCompetitorScan.class) +public abstract class PoiCompetitorScanGuardMixin { + + @Shadow + private static boolean competesForSameJobsite(GlobalPos globalPos, Holder holder, Villager villager) { + throw new AssertionError(); + } + + @Shadow + private static Villager selectWinner(Villager villager, Villager villager2) { + throw new AssertionError(); + } + + /** + * @reason 添加维度守卫,防止跨维度竞争工作站 + */ + @Overwrite + public static BehaviorControl create() { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.JOB_SITE), + instance.present(MemoryModuleType.NEAREST_LIVING_ENTITIES) + ).apply(instance, (memoryAccessor, memoryAccessor2) -> + (serverLevel, villager, l) -> { + GlobalPos globalPos = instance.get(memoryAccessor); + // 维度守卫:工作站不在当前维度则不参与竞争 + if (globalPos.dimension() != villager.level().dimension()) { + return true; + } + serverLevel.getPoiManager().getType(globalPos.pos()).ifPresent((holder) -> + instance.get(memoryAccessor2).stream() + .filter((livingEntity) -> livingEntity instanceof Villager && livingEntity != villager) + .map(entity -> (Villager) entity) + .filter(LivingEntity::isAlive) + .filter((villagerx) -> competesForSameJobsite(globalPos, holder, villagerx)) + .reduce(villager, (v1, v2) -> selectWinner(v1, v2)) + ); + return true; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java new file mode 100644 index 0000000..9c4a2af --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java @@ -0,0 +1,32 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import org.spongepowered.asm.mixin.Mixin; + +/** + * POI 距离追踪器并发访问加固(村民 POI 并发访问加固)。 + *

+ * {@code PoiManager.DistanceTracker}(继承 {@code SectionTracker → DynamicGraphMinFixedPoint → + * LeveledPriorityQueue})内部用普通 {@code LongLinkedOpenHashSet} 队列。细粒度并行下: + *

    + *
  • 服务器线程每 tick 调用 {@code PoiManager.tick → distanceTracker.runAllUpdates()};
  • + *
  • 子任务线程的村民传感器调用 {@code PoiManager.sectionsToVillage → distanceTracker.runAllUpdates()}。
  • + *
+ * 两路并发改写同一队列,fastutil 哈希表 rehash 时损坏(实测抛 + * {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),导致崩溃。 + *

+ * 修复:把 {@code runAllUpdates}(两路唯一入口,内部完成全部队列处理)加 + * {@code synchronized(this)},单对象锁、逐调用、无嵌套,无死锁;开销集中在有更新时的 + * 队列处理阶段,可忽略。 + */ +@Mixin(targets = "net.minecraft.world.entity.ai.village.poi.PoiManager$DistanceTracker") +public abstract class PoiManagerDistanceTrackerMixin { + + @WrapMethod(method = "runAllUpdates") + private void modzuozhi_runAllUpdates(Operation original) { + synchronized (this) { + original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java new file mode 100644 index 0000000..64666eb --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java @@ -0,0 +1,36 @@ +package dev.modzuozhi.mixin; + +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMaps; +import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.Set; + +/** + * POI 区块内部表线程安全化(村民 POI 并发访问加固)。 + *

+ * 并行 tick 下,村民传感器(找床/找工作点)在子任务线程遍历 {@code PoiSection} 的 + * {@code records}/{@code byType},而方块变化会在 worker 线程触发 {@code add/remove} 修改同一 + * 结构。普通 HashMap/fastutil 哈希表并发读写存在数据竞争(迭代时被修改 → CME 或撕裂读)。 + *

+ * 修复:{@code records} 换成 {@link Short2ObjectMaps#synchronize 同步包装}, + * {@code byType} 换成 {@link ConcurrentHashMap}(弱一致迭代安全)。 + */ +@Mixin(PoiSection.class) +public abstract class PoiSectionMixin { + + @Shadow + private final Short2ObjectMap records = + Short2ObjectMaps.synchronize(new Short2ObjectOpenHashMap<>()); + + @Shadow + private final Map, Set> byType = Maps.newConcurrentMap(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java b/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java new file mode 100644 index 0000000..918e69f --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java @@ -0,0 +1,247 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.PortalProcessor; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.NetherPortalBlock; +import net.minecraft.world.level.block.Portal; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 通用传送门延后修复(覆盖原版下界门与模组传送门,如暮色森林)。 + *

+ * 根因:{@code Entity.handlePortal → PortalProcessor.getPortalDestination → + * portal.getPortalDestination} 在维度 worker 线程(实体 tick 上下文) + * 执行,会对目标维度区块做大量读写。目标区块尚未 FULL 加载时: + *

    + *
  • 下界门({@link NetherPortalBlock} → {@code PortalForcer}):getBlockState 等 + * required=false 读不到真实地形 → createPortal 失败 → transition 为 null → 传送失效;
  • + *
  • 模组门(如 {@code twilightforest.block.TFPortalBlock} → {@code TFTeleporter}): + * 直接 {@code ServerLevel.getChunk(x,z)}(required=true)→ worker 在同步路径 + * {@code mainThreadProcessor.managedBlock} 上忙等目标区块生成,暮色区块生成慢, + * 表现为卡约一分钟;且首次生成中数据不完整,传送门位置算错 → 被传到虚空。
  • + *
+ *

+ * 修复:在统一入口 {@code PortalProcessor.getPortalDestination} 上,若当前是维度 worker / + * 细粒度子任务线程,且目标维度对应区块尚未 FULL 加载,则先投递 PORTAL ticket 触发目标 + * 维度异步生成,清传送冷却(否则 handlePortal 已设置的 300tick 冷却会阻断重试),并返回 + * null 把传送延后一 tick。玩家持续站在传送门中,portalTime 保持满值, + * 下一 tick 自动重试——目标区块 FULL 后走原版逻辑:getChunk 命中完整缓存(不阻塞、位置 + * 正确),正常创建传送门并传送。全程不阻塞、不忙等、不刷日志,杜绝死锁与虚空。 + *

+ * 目标维度通过 portal 实例推导:下界门 ↔ 主世界/下界;暮色门 ↔ 主世界/暮色(用类名反射, + * 避免编译期依赖模组类,未安装暮色时自然跳过)。 + */ +@Mixin(PortalProcessor.class) +public abstract class PortalProcessorMixin { + + /** 暮色传送门类名(反射判断,避免编译期依赖模组类)。 */ + @Unique + private static final String TF_PORTAL_CLASS = "twilightforest.block.TFPortalBlock"; + /** 暮色维度 key。 */ + @Unique + private static final ResourceKey TF_DIMENSION = + ResourceKey.create(Registries.DIMENSION, ResourceLocation.fromNamespaceAndPath("twilightforest", "twilight_forest")); + + @Shadow + private Portal portal; + + @Shadow + private int portalTime; + + @Shadow + private BlockPos entryPosition; + + /** + * 预加载目标维度区块:玩家站在传送门中、portalTime 累积期间(暮色默认 60tick)每 tick + * 投递 PORTAL ticket,让目标区块提前异步生成。这样玩家 portalTime 满、真正触发 + * {@code getPortalDestination} 时目标区块大概率已 FULL → 直接传送,无需延后等待。 + * ticket 幂等(DistanceManager 按 type+pos+radius 合并),重复投递无害。 + */ + @Inject(method = "processPortalTeleportation", at = @At("HEAD")) + private void modzuozhi_preloadPortalTarget( + ServerLevel level, Entity entity, boolean canUse, CallbackInfoReturnable cir) { + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return; + } + ServerLevel target = modzuozhi_guessTargetLevel(level); + if (target == null) { + return; + } + double scale = DimensionType.getTeleportationScale(level.dimensionType(), target.dimensionType()); + BlockPos targetPos = target.getWorldBorder() + .clampToBounds(entity.getX() * scale, entity.getY(), entity.getZ() * scale); + ServerChunkCache cache = target.getChunkSource(); + int cx = targetPos.getX() >> 4; + int cz = targetPos.getZ() >> 4; + if (modzuozhi_needTargetChunks(cache, cx, cz)) { + cache.addRegionTicket(TicketType.PORTAL, new ChunkPos(cx, cz), modzuozhi_portalRadius(), targetPos); + } + } + + /** + * 包裹 {@code portal.getPortalDestination(...)}(实际执行 PortalForcer / TFTeleporter)。 + *

+ * 放行(目标区块已 FULL)后,暮色 TFTeleporter 在玩家路径(默认进度锁定 + * {@code ENFORCED_PROGRESSION_RULE=true})会走 {@code moveToSafeCoords → + * scanIntoSafeBiomes} 扫描 128 格,读取预加载范围之外的未生成区块 → + * {@code getChunk(required=true)} 抛 {@code IllegalStateException: Chunk not there} → + * 传播到玩家 tick 被 {@code LevelTickLoop.tickConnections} 捕获跳过 → 每 tick 循环 + * "放行→抛异常→跳过" → 玩家永久卡在传送门(生物不查进度走安全路径故能传,丢生物后 + * 目标区域已生成故玩家第二次成功)。 + *

+ * 这里捕获该异常:投递更大范围 PORTAL ticket 预加载 + 清冷却 + portalTime 防衰减 + + * 返回 null 延后一 tick。下一 tick 重试时目标区域渐进生成,最终 TFTeleporter 找到安全 + * 位置完成传送。不掩盖其它真实异常。 + */ + @WrapOperation(method = "getPortalDestination", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Portal;getPortalDestination(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/portal/DimensionTransition;")) + private DimensionTransition modzuozhi_wrapPortalDestination( + Portal portal, ServerLevel level, Entity entity, BlockPos pos, Operation original) { + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return original.call(portal, level, entity, pos); + } + try { + return original.call(portal, level, entity, pos); + } catch (Throwable t) { + if (!modzuozhi_isChunkNotReady(t)) { + throw t; // 非区块未就绪异常:照常抛出,不掩盖真实 bug + } + // 目标区块未充分生成(暮色 TFTeleporter 玩家路径 scanIntafeBiomes 读远处区块)。 + // 不做大范围预加载(radius 6 会造成暮色 worker 疯狂生成 → 游戏冻结), + // 由 ServerChunkCacheGetChunkFallbackMixin 对"被请求的单块"投 PORTAL ticket 渐进生成; + // 这里仅清冷却 + portalTime 防衰减 + 延后一 tick,让下一 tick 重试时渐进推进。 + if (modzuozhi_logged.add("chunkMiss@" + entity.getId() + "@" + level.dimension().location())) { + DimThreadCore.LOGGER.info("[PortalProcessor] 目标区块未充分生成(Chunk not there),延后渐进生成: 原因={}", t.getMessage()); + } + entity.setPortalCooldown(0); + this.portalTime = 1_000_000_000; + return null; // 延后,下一 tick 重试(单块渐进生成后最终成功) + } + } + + @Inject(method = "getPortalDestination", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferPortalUntilTargetChunkLoaded( + ServerLevel level, Entity entity, CallbackInfoReturnable cir) { + // 仅维度 worker / 细粒度子任务线程需要延后(主线程/串行可同步生成目标区块,保持原版语义) + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return; + } + ServerLevel target = modzuozhi_guessTargetLevel(level); + if (target == null) { + if (modzuozhi_logged.add("noTarget@" + level.dimension().location() + "@" + this.portal.getClass().getName())) { + DimThreadCore.LOGGER.info("[PortalProcessor] 无法确定目标维度,不干预: portal={}, 当前维度={}", + this.portal.getClass().getName(), level.dimension().location()); + } + return; // 无法确定目标维度(未知传送门),不干预 + } + double scale = DimensionType.getTeleportationScale(level.dimensionType(), target.dimensionType()); + BlockPos targetPos = target.getWorldBorder() + .clampToBounds(entity.getX() * scale, entity.getY(), entity.getZ() * scale); + ServerChunkCache cache = target.getChunkSource(); + int cx = targetPos.getX() >> 4; + int cz = targetPos.getZ() >> 4; + // 目标区块(及 5x5 周围区域,暮色 TFTeleporter 会同步 getChunk 5x5)尚未 FULL 加载: + // 投递 PORTAL ticket 触发异步生成,本 tick 延后传送 + if (modzuozhi_needTargetChunks(cache, cx, cz)) { + cache.addRegionTicket(TicketType.PORTAL, new ChunkPos(cx, cz), modzuozhi_portalRadius(), targetPos); + // 关键1:handlePortal 在 getPortalDestination 之前已调用 setPortalCooldown()(300tick 冷却)。 + // 若不清冷却,玩家持续站传送门时 setAsInsidePortal 走冷却分支、不再置 + // insidePortalThisTick=true → processPortalTeleportation 返回 false → getPortalDestination + // 不再重试 → 只能等冷却结束、脱离再进传送门才成功。清冷却后下 tick 保留 portalProcess + // 并重试 getPortalDestination,目标区块 FULL 后立即传送。 + entity.setPortalCooldown(0); + // 关键2:延后期间 handlePortal 与 entityInside 的执行顺序导致 insidePortalThisTick + // 隔 tick 才 true → processPortalTeleportation 每 2 tick 触发、每 2 tick decayTick -4 + // 而 portalTime++ 只 +1 → portalTime 净衰减 → 衰减到 < transitionTime(玩家 60) → + // processPortalTeleportation false → hasExpired → portalProcess 被清除 → 重试彻底中断 + // (玩家第一次进传送门失效的真因;村民 transitionTime=0 恒 >=0 不衰减故能持续重试)。 + // 把 portalTime 拉到安全大值,确保延后期间永不衰减失效,区块 FULL 后必然传送成功。 + this.portalTime = 1_000_000_000; + if (modzuozhi_logged.add("defer@" + entity.getId() + "@" + cx + "," + cz)) { + DimThreadCore.LOGGER.info("[PortalProcessor] 传送门延后: 目标维度={}, 区块=({},{}), 等待目标区块FULL后自动重试", + target.dimension().location(), cx, cz); + } + cir.setReturnValue(null); + } else if (modzuozhi_logged.add("ready@" + entity.getId() + "@" + cx + "," + cz)) { + // 目标区块已 FULL:放行走原逻辑(诊断用,确认延后最终放行) + DimThreadCore.LOGGER.info("[PortalProcessor] 目标区块已就绪放行: 目标维度={}, 区块=({},{}), 走原版传送逻辑", + target.dimension().location(), cx, cz); + } + } + + /** 目标区块 5x5(±2)范围内是否有未 FULL 加载的区块(暮色 TFTeleporter 需同步读 5x5)。 */ + @Unique + private static boolean modzuozhi_needTargetChunks(ServerChunkCache cache, int cx, int cz) { + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + if (cache.getChunkNow(cx + dx, cz + dz) == null) { + return true; + } + } + } + return false; + } + + /** 已打印过的诊断日志(防止刷屏)。 */ + @Unique + private static final java.util.Set modzuozhi_logged = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + /** 依据 portal 实例推导目标维度;未知传送门返回 null(不干预)。 */ + @Unique + private ServerLevel modzuozhi_guessTargetLevel(ServerLevel level) { + MinecraftServer server = level.getServer(); + ResourceKey dim = level.dimension(); + if (this.portal instanceof NetherPortalBlock) { + return server.getLevel(dim == Level.NETHER ? Level.OVERWORLD : Level.NETHER); + } + if (this.portal.getClass().getName().equals(TF_PORTAL_CLASS)) { + return server.getLevel(dim == TF_DIMENSION ? Level.OVERWORLD : TF_DIMENSION); + } + return null; + } + + /** + * 预加载半径:暮色传送门较大(TFTeleporter 玩家路径 scanIntoSafeBiomes 会扫 128 格, + * 覆盖半径约 6 区块),下界门只需中心区域(半径 2)。 + */ + @Unique + private int modzuozhi_portalRadius() { + return this.portal.getClass().getName().equals(TF_PORTAL_CLASS) ? 6 : 2; + } + + /** 是否为"区块未就绪"(Chunk not there when requested)瞬时异常(遍历 cause 链)。 */ + @Unique + private static boolean modzuozhi_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; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java new file mode 100644 index 0000000..eee6ad6 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java @@ -0,0 +1,31 @@ +package dev.modzuozhi.mixin; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +/** + * POI 区块存储表线程安全化(村民 POI 并发访问加固)。 + *

+ * {@code SectionStorage} 是 {@code PoiManager} 的基类,其内部 {@code storage}( + * {@code Long2ObjectOpenHashMap})在细粒度并行下会被子任务线程(村民传感器查询 POI → + * {@code getOrLoad})与区块加载线程({@code readColumn → storage.put})并发读写。 + * fastutil 的普通哈希表并发 put 会在 rehash 时损坏(实测抛 + * {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}), + * 进而引发 chunk 加载失败、连锁异常。 + *

+ * 修复:把 {@code storage} 替换为 {@link Long2ObjectMaps#synchronize 同步包装},所有 + * get/put/remove/values 访问线程安全,从根上消除该数据竞争。 + */ +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin { + + @Shadow + private final Long2ObjectMap> storage = + Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java new file mode 100644 index 0000000..0bf5c4c --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java @@ -0,0 +1,24 @@ +package dev.modzuozhi.mixin; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * 暴露 {@code ServerChunkCache} 的私有方法,供 worker 线程非阻塞地读取已加载区块, + * 以及供 {@link ServerChunkCacheGetChunkFallbackMixin} 构建"已生成但 ticket 尚未对齐"区块的回退。 + */ +@Mixin(ServerChunkCache.class) +public interface ServerChunkCacheAccessor { + @Invoker("getVisibleChunkIfPresent") + ChunkHolder modzuozhi_getVisibleChunkIfPresent(long chunkPos); + + @Invoker("getChunkFutureMainThread") + CompletableFuture> modzuozhi_getChunkFutureMainThread( + int x, int z, ChunkStatus status, boolean required); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java new file mode 100644 index 0000000..bebb5e7 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java @@ -0,0 +1,126 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import java.util.concurrent.CompletableFuture; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 修复 {@code ServerChunkCache.getChunk} 的 "{@code Chunk not there when requested}" 崩溃。 + *

+ * 维度线程并行驱动区块生命周期后,玩家移动进入新生成的区块时,会出现一种竞态: + * 该区块已生成完毕、{@code getChunkNow} 能拿到完整 {@link LevelChunk}(数据在),但其 + * vanilla ticket 级别尚未对齐到 FULL(玩家位置 ticket 往往在 {@code doTick} 之后的网络 + * tick 才登记)。此时 {@code getChunk(x, z, FULL, true)} 的 + * {@code getChunkFutureMainThread} 会因 {@code chunkAbsent} 返回 + * {@code UNLOADED_CHUNK_FUTURE},进而在第 163 行抛 + * {@code IllegalStateException: Chunk not there when requested: Unloaded chunk}。 + *

+ * 本 mixin 在 {@code getChunk} 中:当 required 的 future 结果为 fail 时,回退到 + * {@code getChunkNow}——若该区块的完整 {@link LevelChunk} 仍在内存中,直接返回它(数据 + * 正确,返回它比抛异常合理得多),彻底消除这一崩溃类;若确实不在内存(真正未加载), + * 维持原版抛异常语义。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheGetChunkFallbackMixin { + + /** + * 包并 {@code getChunk} 里的 {@code getChunkFutureMainThread(...)} 调用点, + * 把返回的 future 换成"fail 时回退 getChunkNow"的语义。 + *

+ * {@code required=false}(碰撞检测 {@code BlockCollisions → getChunkForCollisions → getChunk(x,z,FULL,false)}) + * 同样需要该回退:区块已生成但 ticket 尚未对齐 FULL 时,原版会返回 null → 碰撞被整体跳过 → + * 实体穿墙、遁地、卡进方块窒息。回退到内存中的完整区块后,碰撞检测即可拿到真实方块形状。 + */ + @Redirect( + method = "getChunk", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerChunkCache;getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture;") + ) + private CompletableFuture> modzuozhi_getChunkFallback( + ServerChunkCache instance, int x, int z, ChunkStatus status, boolean required) { + CompletableFuture> fut = + ((ServerChunkCacheAccessor) (Object) instance).modzuozhi_getChunkFutureMainThread(x, z, status, required); + // 无论 required 与否,future 以 fail 结束时都尝试返回仍驻留内存的完整区块: + // required=true 避免 "Chunk not there" 崩溃; + // required=false 避免碰撞检测拿到 null 而遁地/穿墙。 + return fut.handle((ChunkResult res, Throwable t) -> { + if (res != null && res.isSuccess()) { + return res; + } + if (t != null) { + return ChunkResult.error(t.getMessage() == null ? "Unloaded chunk" : t.getMessage()); + } + LevelChunk now = modzuozhi_tryGetChunkNow(instance, x, z); + if (now != null) { + return ChunkResult.of(now); + } + // 区块确实不在内存(未加载):required 时投递单块 PORTAL ticket 触发异步生成。 + // 用途:维度 worker / 子任务线程执行传送门逻辑(如暮色 TFTeleporter 玩家路径 + // scanIntafeBiomes 扫描 128 格)时,会同步 getChunk(required=true) 请求远处未生成 + // 区块 → future fail → 本 fallback 仍失败 → 原版抛 "Chunk not there" → 传送被 + // 中断。这里只给被请求的这一块投 ticket,单块渐进生成(不冻结), + // 上层 PortalProcessorMixin 捕获异常后延后重试,下一 tick 该块已生成/生成中 → + // 渐进推进直到传送门逻辑所需的全部区块就绪。 + if (required && modzuozhi_shouldProgressiveGenerate(instance)) { + instance.addRegionTicket(TicketType.PORTAL, new ChunkPos(x, z), 1, + new BlockPos(x << 4, 0, z << 4)); + } + return res != null ? res : ChunkResult.error("Unloaded chunk"); + }); + } + + /** 是否为维度并行激活下的 worker / 子任务线程(此时需要渐进生成而非同步等待)。 */ + @Unique + private static boolean modzuozhi_shouldProgressiveGenerate(ServerChunkCache cache) { + return DimThreadCore.MANAGER.isActive(cache.level.getServer()) + && (DimThreadCore.owns(Thread.currentThread()) || FineGrainScheduler.inSubTask()); + } + + /** + * required 时安全读取内存中的完整区块;未加载返回 null,绝不抛异常。 + *

+ * 不能只依赖 {@code cache.getChunkNow}:它要求 {@code Thread.currentThread() == mainThread}, + * 而本 fallback 的 {@code handle} 回调可能运行在"补全该 future 的 worker 线程"上(区块仍在 + * 生成/打光,future 未完成),此时 getChunkNow 直接返回 null,即使完整区块已在内存中也会 + * 误判为未加载 → 仍抛 "Chunk not there"。故这里线程无关地直接读 + * {@link ChunkHolder#getFullChunkFuture()},绕过 mainThread 线程校验。 + */ + @Unique + private static LevelChunk modzuozhi_tryGetChunkNow(ServerChunkCache cache, int x, int z) { + // 1. 主线程优先:getChunkNow(含 lastChunk 缓存命中) + try { + LevelChunk c = cache.getChunkNow(x, z); + if (c != null) { + return c; + } + } catch (Throwable ignored) { + // 继续 + } + // 2. 线程无关回退:直接查 ChunkHolder 的 FULL future + try { + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) cache).modzuozhi_getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (holder != null) { + LevelChunk lc = holder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null); + if (lc != null) { + return lc; + } + } + } catch (Throwable ignored) { + // 继续 + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java new file mode 100644 index 0000000..6d859c4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java @@ -0,0 +1,347 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.IChunkEnvTick; +import dev.modzuozhi.core.dimthread.IMutableMainThread; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * dimthreads 式 {@code ServerChunkCache} 线程伪装。 + *

+ * 在 {@code swapThreadsAndRun} 期间,本维度的 {@code mainThread} 已被临时设为当前 worker, + * 因此 worker 上 {@code Thread.currentThread() == mainThread} 自然成立,{@code getChunk} + * 走同步路径({@code managedBlock} 驱动本维度 chunkMap),不会投递回服务器主线程并 + * {@code join} 死锁。 + *

+ * 当某个维度 worker 需要访问其它维度的区块(如下界门查找目标传送门)时, + * 当前线程(A worker)!= 目标维度 mainThread(B worker),原版会走 + * {@code getChunkFutureMainThread + managedBlock + join} 慢路径。这里把 + * {@code Thread.currentThread()} 伪装成 {@code this.mainThread},使 A 直接同步读取 B 维度 + * 已加载区块,避免跨维度 supplyAsync/join 死锁。同时实现 {@link IMutableMainThread}, + * 供 {@code swapThreadsAndRun} 临时迁移主线程引用。 + */ +@Mixin(value = ServerChunkCache.class, priority = 1001) +public abstract class ServerChunkCacheMixin implements IMutableMainThread { + @Shadow + public Thread mainThread; + @Shadow + @Final + public ServerLevel level; + + @Override + @Unique + public Thread dimThreads$getMainThread() { + return this.mainThread; + } + + @Override + @Unique + public void dimThreads$setMainThread(Thread thread) { + this.mainThread = thread; + } + + @WrapOperation(method = "getChunk", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + public Thread modzuozhi_currentThread(Operation original) { + return modzuozhi_threadDisguise(original); + } + + /** + * {@code getChunkNow} 同样有 {@code Thread.currentThread() != mainThread → return null} 的线程守卫。 + * 碰撞检测({@code BlockCollisions})通过 {@code Level.getChunkForCollisions} 走 {@code getChunkNow}, + * 子任务线程若被守卫拦截返回 null,实体的方块碰撞被整体跳过 → 穿墙、遁地、卡进方块窒息。 + * 这里与 {@code getChunk} 一样伪装当前线程,使子任务线程能读取到已加载区块的完整碰撞形状。 + */ + @WrapOperation(method = "getChunkNow", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + public Thread modzuozhi_currentThreadNow(Operation original) { + return modzuozhi_threadDisguise(original); + } + + /** + * 维度 worker / 细粒度子任务线程伪装成该 cache 的主线程,走同步路径: + *

    + *
  • {@code getChunk}:避免 supplyAsync/join 死锁;
  • + *
  • {@code getChunkNow}:避免碰撞检测被线程守卫拦截返回 null(穿墙/遁地根因)。
  • + *
+ */ + @Unique + private Thread modzuozhi_threadDisguise(Operation original) { + Thread thread = original.call(); + if (DimThreadCore.MANAGER.isActive(this.level.getServer()) + && (DimThreadCore.owns(thread) || FineGrainScheduler.inSubTask())) { + return this.mainThread; + } + return thread; + } + + // ===== 并行子任务:绕过撕裂的共享 lastChunk 缓存 ===== + // + // 根因:细粒度并行时,多个子任务线程 + 维度 worker 会并发读写 + // ServerChunkCache 的 4 槽 lastChunkPos/lastChunkStatus/lastChunk 缓存(普通数组,非线程安全)。 + // storeInCache 的移位写入与 getChunk 的读取交错时会产生"撕裂读":getChunk(x,z) 命中了 + // pos/status 槽但拿到的是另一个坐标的区块对象。碰撞检测(BlockCollisions → + // ChunkSource.getBlockState)拿着错块去读方块状态,会把脚下的实心地面读成空气 → 村民穿地/遁地/ + // 卡进方块窒息。诊断(getChunk(FULL,false) 返回"wrong 错块"计数持续增长)已证实。 + // + // 修复:子任务线程上完全绕过共享缓存——getChunk / getChunkNow 直接按坐标从 ChunkHolder + // (visibleChunkMap 是 volatile 快照式引用,get 线程安全)读正确区块;storeInCache 对子任务 + // 线程为 no-op,避免污染共享缓存。主线程 / 维度 worker / 串行场景行为不变(零开销)。 + + /** + * 子任务线程的 {@code getChunk}:绕过 lastChunk 缓存,直接读正确坐标的 FULL 区块。 + *

+ * 第10步(修复撕裂回退 + 修复死锁): + *

    + *
  • FULL 命中:走 {@link #modzuozhi_subtaskCacheLookup},绝不用共享 4 槽 lastChunk 缓存 + * (多线程并发写会撕裂,拿到其它坐标区块 → 碰撞把地面读成空气 → 村民遁地)。
  • + *
  • FULL 未命中(区块尚未 FULL,多为加载/生成中):非阻塞占位返回,绝不能等待—— + * 区块生成管线由维度 worker 的 {@code chunkSource.tick} / 服务器主线程驱动,而这两者此刻 + * 分别卡在「实体屏障 await」和「维度 awaitCompletion」上;等待生成 future 会形成 + * 循环等待 → 整服死锁(日志:[FaultGuard] 维度 tick 超时 >30000ms,玩家侧表现为游戏冻结)。 + * 返回占位块后,区块生成完的下一 tick 命中缓存、碰撞恢复正常,短暂下坠自愈,绝不冻结。
  • + *
+ */ + @WrapMethod(method = "getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Lnet/minecraft/world/level/chunk/ChunkAccess;") + private ChunkAccess modzuozhi_subtaskSafeGetChunk( + int x, int z, ChunkStatus status, boolean required, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + return original.call(x, z, status, required); + } + if (status != ChunkStatus.FULL) { + return original.call(x, z, status, required); + } + LevelChunk full = modzuozhi_subtaskCacheLookup(x, z); + if (full != null) { + return full; + } + // 未 FULL:统一返回实心占位(石头),不再区分 required。 + // 根因:并行子任务线程对未生成区块的碰撞检测(required=false → 原返回 null → 碰撞整体 + // 跳过)与寻路/方块读取(required=true → 原返回空气占位)都把未生成区块当作"空气/无地面", + // 村民会持续下坠穿入未生成区块 → 加载高峰期遁地(已实测:fine off 串行时 getChunk 会 + // join 同步生成拿真实方块故不遁地,仅并行路径有此问题)。 + // 实心占位让未生成区块表现为"实心墙":村民被挡在生成边界,区块 FULL 后恢复正常。 + // 仍保持非阻塞(不等待生成 future,避免维度 worker 被 barrier 卡住的循环等待死锁)。 + return modzuozhi_solidChunk(x, z); + } + + /** 构造一个所有方块读作石头的实心占位区块(非阻塞、线程安全,避免生成等待死锁)。 */ + @Unique + private LevelChunk modzuozhi_solidChunk(int x, int z) { + Holder plains = this.level.registryAccess() + .registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS); + return new ModZuozhiSolidChunk(this.level, new ChunkPos(x, z), plains); + } + + /** + * 未 FULL 区块的实心占位:重写 {@code getBlockState} 恒返回石头。 + * 并行子任务线程把未生成区块表现为实心墙,防止实体下坠穿入(遁地)。 + */ + @Unique + private static final class ModZuozhiSolidChunk extends EmptyLevelChunk { + private ModZuozhiSolidChunk(ServerLevel level, ChunkPos pos, Holder biome) { + super(level, pos, biome); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return Blocks.STONE.defaultBlockState(); + } + } + + /** + * 子任务线程的 {@code getChunkNow}:同样绕过 lastChunk 缓存(fallback 的 + * {@code modzuozhi_tryGetChunkNow} 也依赖它),避免撕裂读返回错块。 + */ + @WrapMethod(method = "getChunkNow(II)Lnet/minecraft/world/level/chunk/LevelChunk;") + private LevelChunk modzuozhi_subtaskSafeGetChunkNow(int x, int z, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + return original.call(x, z); + } + return modzuozhi_subtaskCacheLookup(x, z); + } + + /** + * 子任务线程的 {@code storeInCache}:no-op,避免污染(撕裂)主线程/worker 共享的 lastChunk 缓存。 + */ + @WrapMethod(method = "storeInCache(JLnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/chunk/status/ChunkStatus;)V") + private void modzuozhi_subtaskSafeStoreInCache( + long pos, ChunkAccess chunk, ChunkStatus status, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + original.call(pos, chunk, status); + } + // 子任务线程:不写共享缓存 + } + + // ===== 第5步(6c):子任务线程专用并发区块缓存 ===== + // + // 目标:减少子任务线程高频 getChunkNow/getChunk(FULL)(碰撞检测、AI 传感器)的 + // ChunkHolder future 解析开销与共享缓存撕裂风险。设计要点: + // 1. 只缓存 FULL 状态的 LevelChunk(碰撞/AI 只读完整区块); + // 2. 用 ConcurrentHashMap(线程安全)按 chunk pos 缓存,天然与维度隔离(per-cache 字段); + // 3. 命中前先校验 ChunkHolder 仍可见(区块未卸载),杜绝返回已卸载/失效区块; + // 4. 位置校验(x/z 匹配)双保险,防止任何错块路径(吸取 lastChunk 撕裂教训); + // 5. 超过阈值时清空,避免长期运行内存膨胀(区块生命周期短,全清可接受)。 + + /** 子任务线程专用 FULL 区块缓存:chunkPos(long) → LevelChunk。 */ + @Unique + private final ConcurrentHashMap modzuozhi_subtaskChunkCache = new ConcurrentHashMap<>(); + + /** 缓存条目上限,超过即整表清空。 */ + @Unique + private static final int MODZUOZHI_CACHE_MAX = 8192; + + /** + * 子任务线程读取 FULL 区块:优先命中缓存,未命中则从 ChunkHolder 读取并回填缓存。 + *

+ * 正确性关键:本方法不仅被「只读」路径(碰撞检测、AI 传感器)调用,也会被 + * 写入路径(爆炸破坏方块 → {@code Level.setBlock → getChunkAt → getChunk(FULL)}) + * 调用。因此返回的 {@code LevelChunk} 必须是 ChunkHolder 当前活动对象,否则 + * {@code setBlockState} 会写到已过期(重载前)的对象上,导致方块状态丢失/复活(例如 + * 爆炸破坏的 TNT 在存档中复活并再次爆炸)。 + *

+ * 校验策略:命中缓存后,用 {@code holder.getFullChunkFuture().getNow(...)} 解析当前活动 + * 对象,与缓存做对象身份(==)比较——一致才返回缓存(对象未变,安全); + * 不一致说明区块已重载,返回并回填新对象。completed future 的 {@code getNow} 开销可忽略, + * 但彻底杜绝"写到过期对象"这一致数据损坏路径。 + */ + @Unique + private LevelChunk modzuozhi_subtaskCacheLookup(int x, int z) { + long key = ChunkPos.asLong(x, z); + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) this) + .modzuozhi_getVisibleChunkIfPresent(key); + if (holder == null) { + // 区块不在可见列表:不缓存(避免缓存已卸载区块),直接返回 null + return null; + } + // 解析当前活动 FULL 区块(completed future,开销极低) + LevelChunk full = holder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null); + if (full == null) { + return null; + } + // 身份校验:缓存对象与当前活动对象一致才可复用(防止写已过期对象) + LevelChunk cached = this.modzuozhi_subtaskChunkCache.get(key); + if (cached != full) { + this.modzuozhi_subtaskChunkCache.put(key, full); + if (this.modzuozhi_subtaskChunkCache.size() > MODZUOZHI_CACHE_MAX) { + this.modzuozhi_subtaskChunkCache.clear(); + } + } + return full; + } + + // ===== 第3步:区块环境 tick(随机 tick / 流体)跨 chunk 并行 ===== + + /** 并行开启且处于维度 worker 线程时才收集待并行的区块。 */ + @Unique + private boolean modzuozhi_chunkParallel; + /** 收集的待并行区块:(ServerLevel, LevelChunk, randomTickSpeed)。 */ + @Unique + private final List modzuozhi_pendingChunks = new ArrayList<>(); + /** 本次 chunk tick 的子任务屏障。 */ + @Unique + private FineGrainScheduler.Barrier modzuozhi_chunkBarrier; + + @Unique + private boolean modzuozhi_chunkShouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + MinecraftServer server = this.level.getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + @Inject(method = "tickChunks", at = @At("HEAD")) + private void modzuozhi_chunkBegin(CallbackInfo ci) { + this.modzuozhi_chunkParallel = modzuozhi_chunkShouldParallel(); + this.modzuozhi_pendingChunks.clear(); + } + + /** + * 拦截 {@code ServerChunkCache.tickChunks} 里对每个区块的 {@code level.tickChunk(...)}: + * 并行开启时先收集到 {@link #modzuozhi_pendingChunks},由 {@link #modzuozhi_chunkFlush} + * 在广播前统一提交并行执行;关闭时直接串行执行,行为与原版完全一致。 + */ + @Redirect(method = "tickChunks", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;tickChunk(Lnet/minecraft/world/level/chunk/LevelChunk;I)V")) + private void modzuozhi_chunkTick(ServerLevel level, LevelChunk chunk, int randomTickSpeed) { + if (this.modzuozhi_chunkParallel) { + this.modzuozhi_pendingChunks.add(new Object[]{level, chunk, randomTickSpeed}); + } else { + level.tickChunk(chunk, randomTickSpeed); + } + } + + /** + * 在 {@code tickChunks} 的 {@code list.forEach(... broadcastChanges ...)} 之前收口: + * 把收集的区块并行执行随机 tick / 流体,并 {@code await} 全部完成,确保方块状态变化 + * 在广播前已应用。 + */ + @WrapOperation(method = "tickChunks", + at = @At(value = "INVOKE", + target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V")) + private void modzuozhi_chunkFlush(List list, Consumer consumer, Operation original) { + this.modzuozhi_flushChunkTicks(); + original.call(list, consumer); + } + + @Unique + private void modzuozhi_flushChunkTicks() { + if (!this.modzuozhi_chunkParallel || this.modzuozhi_pendingChunks.isEmpty()) { + this.modzuozhi_chunkParallel = false; + return; + } + MinecraftServer server = this.level.getServer(); + this.modzuozhi_chunkBarrier = FineGrainScheduler.beginBarrier(); + for (Object[] entry : this.modzuozhi_pendingChunks) { + ServerLevel level = (ServerLevel) entry[0]; + LevelChunk chunk = (LevelChunk) entry[1]; + int randomTickSpeed = (Integer) entry[2]; + this.modzuozhi_chunkBarrier.submit(server, () -> { + // 线程本地随机源,避免并行子任务跨线程访问共享 Level.random + ((IChunkEnvTick) level).modzuozhi_tickChunk(chunk, randomTickSpeed, RandomSource.create()); + }, chunk.getPos().getWorldPosition()); + } + this.modzuozhi_pendingChunks.clear(); + this.modzuozhi_chunkBarrier.await(); + this.modzuozhi_chunkBarrier.drain(); + this.modzuozhi_chunkBarrier = null; + this.modzuozhi_chunkParallel = false; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java new file mode 100644 index 0000000..3fc6750 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.server.level.ServerChunkCache; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 维度循环模型:把区块源「主线程」任务收口到维度 loop,禁止服务器主线程二次 poll。 + *

+ * 原版里 {@code ServerChunkCache.pollTask()} 由 {@code MinecraftServer.pollTaskInternal}(主线程 + * waitUntilNextTick 期间)调用,负责距离更新({@code runDistanceManagerUpdates})+ 光照调度 + + * 主线程队列排空。切到维度 loop 后,本维度 {@code level.tick()} 已在 worker 上跑 + * {@code ServerChunkCache.tick → runDistanceManagerUpdates},若服务器主线程仍为该维度 + * {@code pollTask},两个线程会并发改写同一 {@code DistanceManager}/{@code ChunkMap}(本次启动即 + * 因此在 {@code DistanceManager.runAllUpdates} 抛了 ConcurrentModificationException)。 + *

+ * 因此:当监控开启且当前线程不是维度 worker(即服务器主线程/其它线程)时跳过 poll, + * 本维度的区块源任务统一由 {@code LevelTickLoop.processMainThreadTasks()} 在 worker 上排空。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCachePollMixin { + + @WrapMethod(method = "pollTask") + private boolean modzuozhi_skipServerThreadChunkPoll(Operation original) { + ServerChunkCache self = (ServerChunkCache) (Object) this; + // 仅当维度 loop 已启动接管(tickChildren 之后)才跳过主线程 poll;启动阶段 prepareLevels + // 仍依赖主线程 poll 驱动出生点区块生成,跳过会卡死在「Preparing start region」。 + if (DimThreadCore.MANAGER.isActive(self.level.getServer()) + && DimThreadCore.MANAGER.isLoopScheduled(self.level) + && !DimThreadCore.owns(Thread.currentThread())) { + return false; + } + return original.call(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java new file mode 100644 index 0000000..590729e --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java @@ -0,0 +1,34 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.server.level.ServerChunkCache; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 串行化同一维度的区块距离更新(修复并发 {@code runGenerationTasks} 的 CME 与 claim/release + * 计数异常)。 + *

+ * {@code runDistanceManagerUpdates}(内部:{@code DistanceManager.runAllUpdates} → + * {@code scheduleGenerationTask} 向 {@code ChunkMap.pendingGenerationTasks}(普通 ArrayList) + * add;以及 {@code ChunkMap.runGenerationTasks} 遍历 + clear,并对其中的生成任务 claim/release) + * 必须单线程执行。维度 loop 模型下,该维度 worker({@code level.tick → ServerChunkCache + * .tick})与保存线程({@code save → runDistanceManagerUpdates})等路径并发调用时: + * 同时遍历/修改 {@code pendingGenerationTasks} 抛 {@code ConcurrentModificationException}, + * 且对同一 {@code GenerationChunkHolder} 重复 claim/release 抛 + * {@code IllegalStateException: More releases than claims}(实测:下界维度 tick 异常 + + * Worker 线程异常)。 + *

+ * 用 {@code synchronized(this)}(每 {@code ServerChunkCache} 一把锁)把所有调用者串行化: + * 生成任务 add/遍历/claim 互斥,release(ChunkMap 生成线程异步)与 claim 自然配对,计数平衡。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheRunDistanceMixin { + + @WrapMethod(method = "runDistanceManagerUpdates") + private boolean modzuozhi_syncRunDistanceUpdates(Operation original) { + synchronized (this) { + return original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java new file mode 100644 index 0000000..072efde --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.ModZuozhi; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * 第2步:网络包按玩家分派到维度线程——断开流程死锁防护。 + *

+ * 原版 {@code ServerCommonPacketListenerImpl.disconnect} 用 + * {@code MinecraftServer.executeBlocking} 把清理任务提交到主线程并阻塞等待。当包处理 + * 已移到维度线程(worker 在 {@code ServerLevel.tick} 收口执行包)时,主线程正阻塞在 + * {@code awaitCompletion} 等待维度 worker 完成——worker 再 executeBlocking 等主线程会 + * 互相等待死锁。因此全局开启且为游戏连接时,把断开清理改为异步投递主线程 + * ({@code execute}),不阻塞 worker,杜绝死锁。 + */ +@Mixin(ServerCommonPacketListenerImpl.class) +public abstract class ServerCommonPacketListenerDimMixin { + + @WrapOperation(method = "disconnect(Lnet/minecraft/network/DisconnectionDetails;)V", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;executeBlocking(Ljava/lang/Runnable;)V")) + private void modzuozhi_asyncDisconnect(MinecraftServer server, Runnable task, Operation original) { + if (ModZuozhi.isEnabled() && ((Object) this) instanceof ServerGamePacketListenerImpl) { + // 异步投主线程,worker 不阻塞 → 防与主线程 awaitCompletion 互相等待死锁 + server.execute(task); + } else { + original.call(server, task); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java new file mode 100644 index 0000000..4769ee3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java @@ -0,0 +1,79 @@ +package dev.modzuozhi.mixin; + +import com.mojang.logging.LogUtils; +import dev.modzuozhi.core.dimthread.DimThreadCore; +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.network.ServerConnectionListener; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Iterator; +import java.util.List; + +/** + * 维度循环模型:主线程的连接监听不再 tick 游戏连接(防止玩家被双 tick)。 + *

+ * 原版 {@code ServerConnectionListener.tick()} 会 tick 所有连接(游戏连接触发 + * {@code ServerPlayer.doTick()})。维度 loop 的 {@code tickConnections()} 已经 tick 了它 + * 名下的游戏连接,因此这里对 {@link ServerGamePacketListenerImpl}(游戏连接)只做「断开时 + * 从监听列表移除」,不 tick、不重复清理(断开清理由 loop 完成)。登录/状态/配置连接仍在本处 tick。 + */ +@Mixin(ServerConnectionListener.class) +public abstract class ServerConnectionListenerMixin { + + @Shadow + @Final + private List connections; + + @Shadow + @Final + private MinecraftServer server; + + /** + * @reason 游戏连接交维度 loop tick;主线程只兜底登录/状态/配置连接与列表清理。 + * 全局开关关闭(isActive=false)时恢复原版:所有连接(含游戏连接)都回主线程 tick。 + */ + @Overwrite + public void tick() { + boolean active = DimThreadCore.MANAGER.isActive(this.server); + synchronized (this.connections) { + Iterator iterator = this.connections.iterator(); + while (iterator.hasNext()) { + Connection connection = iterator.next(); + if (connection.isConnecting()) { + continue; + } + boolean game = connection.getPacketListener() instanceof ServerGamePacketListenerImpl; + if (connection.isConnected()) { + if (game && active) { + continue; // 由所属维度 loop tick + } + try { + connection.tick(); + } catch (Exception e) { + Logger logger = LogUtils.getLogger(); + logger.warn("Failed to handle packet for {}", + connection.getLoggableAddress(this.server.logIPs()), e); + Component component = Component.literal("Internal server error"); + connection.send(new ClientboundDisconnectPacket(component), + PacketSendListener.thenRun(() -> connection.disconnect(component))); + connection.setReadOnly(); + } + } else { + iterator.remove(); + if (!game || !active) { + connection.handleDisconnection(); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java new file mode 100644 index 0000000..0dc9da3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java @@ -0,0 +1,47 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import org.spongepowered.asm.mixin.Mixin; + +/** + * Block Event(方块事件)队列并发访问加固。 + *

+ * 方块事件(音符盒/活塞/发射器/铃等延迟动作)经 {@code ServerLevel.blockEvent} 入队、 + * 每 tick {@code runBlockEvents} 出队执行并广播。原版 {@code blockEvents} 是普通非线程安全 + * 的 {@code ObjectLinkedOpenHashSet}。细粒度并行下,并行实体子任务一旦触发 {@code blockEvent} + * (入队写),会与维度 worker/服务器线程正在 {@code runBlockEvents}(出队删)并发读写 + * 同一哈希集 → CME 或哈希表损坏(fastutil 并发损坏崩溃)。 + *

+ * 修复:对访问队列的 3 个方法({@code blockEvent}/{@code runBlockEvents}/{@code clearBlockEvents}) + * 加 {@code synchronized(this)}:单对象锁、逐调用、无嵌套,无死锁(方块事件低频,开销可忽略); + * 同时保留原版 Set 的去重 + 插入序语义,行为零变化。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelBlockEventMixin { + + @WrapMethod(method = "blockEvent") + private void modzuozhi_blockEvent(BlockPos pos, Block block, int type, int data, Operation original) { + synchronized (this) { + original.call(pos, block, type, data); + } + } + + @WrapMethod(method = "runBlockEvents") + private void modzuozhi_runBlockEvents(Operation original) { + synchronized (this) { + original.call(); + } + } + + @WrapMethod(method = "clearBlockEvents") + private void modzuozhi_clearBlockEvents(BoundingBox box, Operation original) { + synchronized (this) { + original.call(box); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java new file mode 100644 index 0000000..fda3e8d --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java @@ -0,0 +1,116 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.IChunkEnvTick; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.animal.horse.SkeletonHorse; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.LightningRodBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +/** + * 区块环境随机 tick / 流体 tick 的并行版本(自定义实现,绕开共享 {@code Level.random})。 + *

+ * 细粒度并行(fine)下多个区块的随机 tick 由不同子任务线程执行。原版 + * {@code ServerLevel.tickChunk} 直接读取 {@code Level.random}({@code public final} 共享字段), + * 并行时会跨线程冲突;且 NeoForge 开发环境无法对混淆字段做 {@code @Redirect}(无 refmap)。 + *

+ * 因此本 mixin 在 {@code ServerLevel} 上注入一个等价的自定义实现 + * {@link #modzuozhi_tickChunk}(实现 {@link IChunkEnvTick}),接收线程本地随机源为参数, + * 复刻原版 {@code tickChunk} 的 thunder / ice-and-snow / 随机 tick + 流体逻辑。随机源全部来自 + * 参数 {@code random},由 {@code ServerChunkCacheMixin} 在并行子任务中调用,完全绕开共享字段。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelChunkTickMixin implements IChunkEnvTick { + /** {@code findLightningTargetAround} 为 protected,需 shadow 后在本实现内调用。 */ + @Shadow + protected abstract BlockPos findLightningTargetAround(BlockPos pos); + + /** + * 等价于原版 {@code ServerLevel.tickChunk},但使用传入的线程本地随机源 {@code random}。 + */ + @Override + @Unique + public void modzuozhi_tickChunk(LevelChunk chunk, int randomTickSpeed, RandomSource random) { + ServerLevel level = (ServerLevel) (Object) this; + ChunkPos chunkpos = chunk.getPos(); + boolean raining = level.isRaining(); + int minX = chunkpos.getMinBlockX(); + int minZ = chunkpos.getMinBlockZ(); + + // thunder:用线程本地随机源判断是否产生闪电 + if (raining && level.isThundering() && random.nextInt(100000) == 0) { + BlockPos target = this.findLightningTargetAround(randomPos(minX, 0, minZ, 15, random)); + if (level.isRainingAt(target)) { + DifficultyInstance difficulty = level.getCurrentDifficultyAt(target); + boolean trapHorse = level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) + && random.nextDouble() < (double) difficulty.getEffectiveDifficulty() * 0.01 + && !(level.getBlockState(target.below()).getBlock() instanceof LightningRodBlock); + if (trapHorse) { + SkeletonHorse horse = EntityType.SKELETON_HORSE.create(level); + if (horse != null) { + horse.setTrap(true); + horse.setAge(0); + horse.setPos((double) target.getX(), (double) target.getY(), (double) target.getZ()); + level.addFreshEntity(horse); + } + } + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt != null) { + bolt.moveTo(Vec3.atBottomCenterOf(target)); + bolt.setVisualOnly(trapHorse); + level.addFreshEntity(bolt); + } + } + } + + // ice and snow(降水 / 结冰) + for (int i = 0; i < randomTickSpeed; i++) { + if (random.nextInt(48) == 0) { + level.tickPrecipitation(randomPos(minX, 0, minZ, 15, random)); + } + } + + // 随机 tick + 流体 tick + if (randomTickSpeed > 0) { + LevelChunkSection[] sections = chunk.getSections(); + for (int idx = 0; idx < sections.length; idx++) { + LevelChunkSection section = sections[idx]; + if (section.isRandomlyTicking()) { + int sectionY = chunk.getSectionYFromSectionIndex(idx); + int baseY = SectionPos.sectionToBlockCoord(sectionY); + for (int l = 0; l < randomTickSpeed; l++) { + BlockPos pos = randomPos(minX, baseY, minZ, 15, random); + BlockState state = section.getBlockState(pos.getX() - minX, pos.getY() - baseY, pos.getZ() - minZ); + if (state.isRandomlyTicking()) { + state.randomTick(level, pos, random); + } + FluidState fluid = state.getFluidState(); + if (fluid.isRandomlyTicking()) { + fluid.randomTick(level, pos, random); + } + } + } + } + } + } + + /** 等价于原版 {@code ServerLevel.getBlockRandomPos},但使用传入的随机源。 */ + @Unique + private static BlockPos randomPos(int x, int y, int z, int mask, RandomSource random) { + return new BlockPos(x + random.nextInt(mask + 1), y, z + random.nextInt(mask + 1)); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java new file mode 100644 index 0000000..6ac9127 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java @@ -0,0 +1,72 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第14步(对齐 Async 零锁路线):{@code ServerLevel$EntityCallbacks} 的可见性/生命周期回调加 + * {@code synchronized(this)},替代原全局写锁 {@code SECTION_LOCK} 对这些共享状态的串行保护。 + *

+ * 移除全局写锁后,实体 tick 并行子任务会并发触发这些回调(经 + * {@code startTracking/stopTracking/onDestroyed/onSectionChange}): + *

    + *
  • {@code onTrackingStart/onTrackingEnd}:写 {@code ServerChunkCache} 实体追踪表、玩家表、 + * {@code dragonParts}、游戏事件监听器、NeoForge 事件——非线程安全,需串行;
  • + *
  • {@code onDestroyed}:写记分板;
  • + *
  • {@code onSectionChange}:更新游戏事件监听器位置。
  • + *
+ * 用 {@code synchronized(this)}(同一 {@code EntityCallbacks} 实例,即每 ServerLevel 一把轻量锁) + * 把这些快速回调串行化,锁持有时长远小于整段 onMove,实体移动/生成本体仍无锁并行。 + * {@code onTickingStart/onTickingEnd}(EntityTickList 增删)已在 {@link EntityTickListMixin} + * 中按子任务上下文延迟执行,此处一并加同步兜底。 + *

+ * 服务器线程与维度 worker 的其它区块追踪访问不经过本回调,与移除前一致(本回调原本也未与之互斥), + * 不引入新的竞争面。 + */ +@Mixin(targets = "net.minecraft.server.level.ServerLevel$EntityCallbacks") +public abstract class ServerLevelEntityCallbacksMixin { + + @WrapMethod(method = "onTrackingStart") + private void modzuozhi_syncTrackingStart(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTrackingEnd") + private void modzuozhi_syncTrackingEnd(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onDestroyed") + private void modzuozhi_syncDestroyed(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onSectionChange") + private void modzuozhi_syncSectionChange(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTickingStart") + private void modzuozhi_syncTickingStart(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTickingEnd") + private void modzuozhi_syncTickingEnd(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java new file mode 100644 index 0000000..d5502d7 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java @@ -0,0 +1,73 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Consumer; + +/** + * 第4步:实体 tick 并行——在 {@code ServerLevel.tick} 的 {@code entityTickList.forEach} 前后 + * 建立/收口实体子任务屏障(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 原版实体循环({@code entityTickList.forEach(lambda)}) 的每实体处理:isRemoved 检查 → + * checkDespawn → 载具处理 → {@code guardEntityTick(tickNonPassenger, entity)}。其中除 + * {@code guardEntityTick} 外的逻辑为串行预检(改动小、需按序),只有最终实体的 + * {@code tick} 是真正的重负载,适合并行。 + *

+ * 由于 {@code guardEntityTick} 调用位于 lambda 内部(合成方法,无法稳定引用),本 mixin 用 + * {@code @WrapOperation} 包住整个 {@code forEach} 调用:进入前建立 ThreadLocal 屏障 + * ({@link EntityTickParallel#begin()}),离开时(finally)收口等待全部实体子任务完成 + * ({@link EntityTickParallel#end()})。真正的实体 tick 由 {@link EntityTickParallelMixin} + * 在 {@code guardEntityTick} 处读取该屏障并提交并行。 + *

+ * 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效; + * 关闭时完全退化为原版串行(零开销)。 + *

+ * 第14步(对齐 Async):第12步曾因"并行实体 tick + 碰撞开启"的锁竞争而改为碰撞开启时 + * 强制串行实体 tick;第14步移除全局写锁、并发化 {@code EntityLookup/knownUuids/chunkVisibility}、 + * 并同步实体回调后,实体 tick 无条件走并行(碰撞开启也并行),不再需要该串行回退。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelEntityTickMixin { + + @Unique + private boolean modzuozhi_entityShouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + /** + * 包住整个实体 tick 循环:进入前建立实体子任务屏障,离开时(含异常)收口等待完成。 + */ + @WrapOperation(method = "tick(Ljava/util/function/BooleanSupplier;)V", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntityTickList;forEach(Ljava/util/function/Consumer;)V")) + private void modzuozhi_entityLoop(EntityTickList list, Consumer consumer, Operation original) { + boolean parallel = modzuozhi_entityShouldParallel(); + if (parallel) { + EntityTickParallel.begin(); + } + try { + original.call(list, consumer); + } finally { + if (parallel) { + EntityTickParallel.end(); + } + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java new file mode 100644 index 0000000..4b2e9a3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java @@ -0,0 +1,48 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.IPlayerPacketQueue; +import dev.modzuozhi.core.dimthread.PlayerPacketQueue; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.BooleanSupplier; + +/** + * 第2步:网络包按玩家分派到维度线程——每维度包队列的挂载与 tick 收口。 + *

+ * 在 {@code ServerLevel} 上挂一个 {@link PlayerPacketQueue}(玩家发来的包处理任务队列), + * 并在 {@code ServerLevel.tick} 开头(HEAD)统一执行。两种 tick 路径都覆盖: + *

    + *
  • 维度并行:维度 worker 在 {@code swapThreadsAndRun} 内 tick,包任务在 worker + * 身份下执行(getBlockEntity 等线程守卫自然通过),与其它维度并行。
  • + *
  • 主线程串行(dimCount <= 1 / 维度并行关闭):主线程直接 tick,包任务在主线程 + * 执行,行为等价原版。
  • + *
+ * 全局关闭时 {@code drain} 不执行(空转,零副作用)。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelPlayerPacketMixin implements IPlayerPacketQueue { + + @Unique + private final PlayerPacketQueue modzuozhi_playerPacketQueue = new PlayerPacketQueue(); + + @Override + @Unique + public PlayerPacketQueue modzuozhi$playerPacketQueue() { + return this.modzuozhi_playerPacketQueue; + } + + /** 维度 tick 开头:先排空本维度玩家排队的包处理任务(命令等),再进沉重的实体/AI tick, + * 避免指令在高负载(实体多/地形慢)时被饿死到 level.tick 结束才执行(仅全局开启时)。 */ + @Inject(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At("HEAD")) + private void modzuozhi_drainPlayerPackets(BooleanSupplier hasTimeLeft, CallbackInfo ci) { + if (ModZuozhi.isEnabled()) { + this.modzuozhi_playerPacketQueue.drain(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java new file mode 100644 index 0000000..e1e20cf --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java @@ -0,0 +1,41 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 维度循环模型:玩家跨维度传送收口到主线程。 + *

+ * 旧的「有界屏障」模型里,玩家实体 tick 只在主线程({@code EntityTickParallelMixin} 跳过维度 + * worker 上的玩家),因此下界门触发的 {@code ServerPlayer.changeDimension} 也在主线程执行,天然安全。 + * 切到维度循环后,玩家 {@code doTick()} 由本维度 loop 的 {@code tickConnections()} 在 worker 上触发, + * 于是 {@code changeDimension} 会在 worker 上直接 {@code removePlayerImmediately} 旧维度 + + * {@code addDuringTeleport} 新维度,与新维度 worker 并发改写实体容器 → 竞态崩溃。 + *

+ * 因此这里与 {@code EntityChangeDimensionMixin}(针对 {@code Entity.changeDimension},非玩家实体) + * 平行:当调用线程是维度 worker 时,把真正的变更登记到 {@link DimensionChangeQueue},由主线程在维度 + * tick 收口后({@code MinecraftServerDimMixin} 的 {@code DimensionChangeQueue.flush()})统一执行。 + * 主线程上直接调用(如命令 {@code /execute in})不经本拦截,行为与原版一致。 + */ +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerChangeDimensionMixin { + + /** 在 worker 上触发跨维度时退避到主线程队列,避免并发改写两个维度的实体容器。 */ + @Inject(method = "changeDimension", at = @At("HEAD"), cancellable = true, remap = false) + private void modzuozhi_deferPlayerDimChange(DimensionTransition transition, CallbackInfoReturnable cir) { + if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) { + return; + } + if (DimThreadCore.owns(Thread.currentThread())) { + DimensionChangeQueue.schedule((Entity) (Object) this, transition); + cir.setReturnValue(null); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java new file mode 100644 index 0000000..ecb5c99 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java @@ -0,0 +1,24 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.ChunkPrefetcher; +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 阶段 A(步骤 0):挂载区块预取到玩家 tick。 + *

+ * 玩家实体 tick 只由主线程 {@code playerList.tick()} → {@code ServerPlayer.doTick()} 驱动 + * (维度 worker 已跳过玩家 tick,见 {@link EntityTickParallelMixin}),因此本注入天然运行在 + * 主线程,与维度并行无冲突。预取逻辑本身是纯增益、全 try/catch,任何异常都不影响玩家。 + */ +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerPrefetchMixin { + + @Inject(method = "doTick", at = @At("HEAD")) + private void modzuozhi_prefetchChunks(CallbackInfo ci) { + ChunkPrefetcher.onPlayerTick((ServerPlayer) (Object) this); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java b/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java new file mode 100644 index 0000000..72b0e0b --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java @@ -0,0 +1,113 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import dev.modzuozhi.core.filter.SerDesFilter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collection; + +/** + * 方块实体(TE)细粒度并行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 在 {@code Level.tickBlockEntities()} 的 while 循环中,把线程安全(经 + * {@link SerDesFilter} 判定可并行)的 TE tick 提交到 {@link FineGrainScheduler.Barrier} + * 并行执行,其余(黑名单/非 vanilla)串行;循环结束后 {@code await} 收口,再 + * {@code drain} 子任务投递的延迟回调。 + *

+ * 线程安全关键:并行子任务运行在其它 worker 线程,可能并发写 {@code pendingBlockEntityTickers} + * 等普通集合。这里拦截 {@code addBlockEntityTicker} / {@code addFreshBlockEntities}, + * 在并行子任务上下文中把写入投递到 {@link PostExecuteQueue},由维度 worker 串行执行。 + *

+ * 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效; + * 关闭时完全退化为原版串行。 + */ +@Mixin(Level.class) +public abstract class TickingBlockEntityParallelMixin { + @Shadow + private void addBlockEntityTicker(TickingBlockEntity ticker) { + } + + @Shadow + private void addFreshBlockEntities(Collection entities) { + } + + @Unique + private FineGrainScheduler.Barrier modzuozhi_teBarrier; + @Unique + private boolean modzuozhi_teParallel; + + @Unique + private boolean modzuozhi_shouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + if (!((Object) this instanceof ServerLevel serverLevel)) { + return false; + } + MinecraftServer server = serverLevel.getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + @Inject(method = "tickBlockEntities", at = @At("HEAD")) + private void modzuozhi_teBegin(CallbackInfo ci) { + this.modzuozhi_teParallel = modzuozhi_shouldParallel(); + if (this.modzuozhi_teParallel) { + this.modzuozhi_teBarrier = FineGrainScheduler.beginBarrier(); + } + } + + @Redirect(method = "tickBlockEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/TickingBlockEntity;tick()V")) + private void modzuozhi_teTick(TickingBlockEntity ticker) { + if (this.modzuozhi_teParallel && SerDesFilter.INSTANCE.isParallel(ticker)) { + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + this.modzuozhi_teBarrier.submit(server, ticker::tick, ticker.getPos()); + } else { + ticker.tick(); + } + } + + @Inject(method = "tickBlockEntities", at = @At("RETURN")) + private void modzuozhi_teEnd(CallbackInfo ci) { + if (this.modzuozhi_teParallel && this.modzuozhi_teBarrier != null) { + this.modzuozhi_teBarrier.await(); + this.modzuozhi_teBarrier.drain(); + this.modzuozhi_teBarrier = null; + this.modzuozhi_teParallel = false; + } + } + + @Inject(method = "addBlockEntityTicker", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferTicker(TickingBlockEntity ticker, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> this.addBlockEntityTicker(ticker)); + ci.cancel(); + } + } + + @Inject(method = "addFreshBlockEntities", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferFresh(Collection entities, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> this.addFreshBlockEntities(entities)); + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java new file mode 100644 index 0000000..4d00a54 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java @@ -0,0 +1,89 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.BehaviorUtils; +import net.minecraft.world.entity.ai.behavior.YieldJobSite; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +/** + * 跨维度安全:{@link YieldJobSite} 添加维度检查。 + *

+ * 原版 {@code YieldJobSite.create()} 不做维度检查,当 {@code POTENTIAL_JOB_SITE} 存了 + * 另一个维度的 {@code GlobalPos} 时,村民会跨维度让工作站。并行环境下需确保 AI 只在本维度 + * 操作,避免跨维度引用导致的不一致。 + *

+ * 借鉴 Chlorophyll(Unlicense)的 {@code YieldJobSiteMixin} 思路,增加 + * {@code globalPos.dimension() != villager.level().dimension()} 检查,维度不匹配时直接返回。 + */ +@Mixin(YieldJobSite.class) +public abstract class YieldJobSiteGuardMixin { + + @Shadow + private static boolean nearbyWantsJobsite(Holder holder, Villager villager, BlockPos pos) { + throw new AssertionError(); + } + + /** + * @reason 添加维度守卫,防止跨维度让工作站 + */ + @Overwrite + public static BehaviorControl create(float f) { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.POTENTIAL_JOB_SITE), + instance.absent(MemoryModuleType.JOB_SITE), + instance.present(MemoryModuleType.NEAREST_LIVING_ENTITIES), + instance.registered(MemoryModuleType.WALK_TARGET), + instance.registered(MemoryModuleType.LOOK_TARGET) + ).apply(instance, (memoryAccessor, memoryAccessor2, memoryAccessor3, memoryAccessor4, memoryAccessor5) -> + (serverLevel, villager, l) -> { + if (villager.isBaby()) { + return false; + } + if (villager.getVillagerData().getProfession() != VillagerProfession.NONE) { + return false; + } + GlobalPos globalPos = instance.get(memoryAccessor); + // 维度守卫:潜在工作站不在当前维度则跳过 + if (globalPos.dimension() != villager.level().dimension()) { + return true; + } + BlockPos blockPos = globalPos.pos(); + Optional> optional = serverLevel.getPoiManager().getType(blockPos); + + optional.flatMap(poiTypeHolder -> + instance.get(memoryAccessor3).stream() + .filter((livingEntity) -> livingEntity instanceof Villager && livingEntity != villager) + .map((livingEntity) -> (Villager) livingEntity) + .filter(LivingEntity::isAlive) + .filter((villagerx) -> nearbyWantsJobsite(poiTypeHolder, villagerx, blockPos)) + .findFirst() + ).ifPresent((villagerx) -> { + memoryAccessor4.erase(); + memoryAccessor5.erase(); + memoryAccessor.erase(); + if (villagerx.getBrain().getMemory(MemoryModuleType.JOB_SITE).isEmpty()) { + BehaviorUtils.setWalkAndLookTargetMemories(villagerx, blockPos, f, 1); + villagerx.getBrain().setMemory(MemoryModuleType.POTENTIAL_JOB_SITE, GlobalPos.of(serverLevel.dimension(), blockPos)); + } + }); + + return true; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 0000000..c860f6c --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,6 @@ +# 允许 dimthreads 式 swapThreadsAndRun 在非构造方法中写这些字段 +# -f 表示移除 final 修饰符,使 Java 21 允许跨方法写入 +public-f net.minecraft.world.level.Level thread # thread +public-f net.minecraft.server.level.ServerChunkCache mainThread # mainThread +# 第10步:允许在构造器中把 ClassInstanceMultiMap.byClass 换成 ConcurrentHashMap(修 find 的 computeIfAbsent 并发 CME) +public-f net.minecraft.util.ClassInstanceMultiMap byClass # byClass diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..73089b0 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,30 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +issueTrackerURL="https://example.com/issues" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' +logoFile="logo.png" +accesstransformer="META-INF/accesstransformer.cfg" + +[[dependencies.${mod_id}]] + modId="neoforge" + mandatory=true + versionRange="[21.1.0,)" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +[[mixins]] +config="dimensionalripper.mixins.json" \ No newline at end of file diff --git a/src/main/resources/dimensionalripper.mixins.json b/src/main/resources/dimensionalripper.mixins.json new file mode 100644 index 0000000..9995018 --- /dev/null +++ b/src/main/resources/dimensionalripper.mixins.json @@ -0,0 +1,72 @@ +{ + "required": true, + "minVersion": "0.8.5", + "package": "dev.modzuozhi.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "AttributeInstanceMixin", + "AttributeMapMixin", + "BabyFollowAdultGuardMixin", + "BinaryHeapMixin", + "BlockableEventLoopPollMixin", + "BlockDropCaptureMixin", + "BrainMixin", + "ChunkMapLoadOffloadMixin", + "ClassInstanceMultiMapMixin", + "CollectingNeighborUpdaterMixin", + "DistanceManagerMixin", + "EntityChangeDimensionMixin", + "EntityGetEntityCollisionsMixin", + "EntityLookupMixin", + "EntitySectionManagerMixin", + "EntitySectionMixin", + "EntitySectionStorageMixin", + "EntityTickListMixin", + "EntityTickParallelMixin", + "GameEventDispatcherMixin", + "GossipContainerMixin", + "GoToPotentialJobSiteGuardMixin", + "ILevelMixin", + "LevelChunkMixin", + "LevelExplodeMixin", + "LevelGetChunkMixin", + "LevelMixin", + "LevelTicksMixin", + "LivingEntityGetInBlockStateMixin", + "MinecraftServerDimMixin", + "MinecraftServerSaveMixin", + "MinecraftServerTimeSyncAccessor", + "NavigatingMobsMixin", + "NearestLivingEntitySensorMixin", + "PacketUtilsDimMixin", + "PoiCompetitorScanGuardMixin", + "PoiManagerDistanceTrackerMixin", + "PoiSectionMixin", + "PlayerListSaveMixin", + "PortalProcessorMixin", + "SectionStorageMixin", + "ServerChunkCacheAccessor", + "ServerChunkCacheGetChunkFallbackMixin", + "ServerChunkCacheMixin", + "ServerChunkCachePollMixin", + "ServerChunkCacheRunDistanceMixin", + "ServerCommonPacketListenerDimMixin", + "ServerConnectionListenerMixin", + "ServerLevelBlockEventMixin", + "ServerLevelChunkTickMixin", + "ServerLevelEntityCallbacksMixin", + "ServerLevelEntityTickMixin", + "ServerLevelPlayerPacketMixin", + "ServerPlayerChangeDimensionMixin", + "ServerPlayerPrefetchMixin", + "TickingBlockEntityParallelMixin", + "YieldJobSiteGuardMixin" + ], + "client": [ + ], + "server": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ded6654e186c453b23ed7632811fb9f60f4f7e8d GIT binary patch literal 53208 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 literal 0 HcmV?d00001