Initial commit: Dimensional Ripper mod source (NeoForge 1.21.1)
This commit is contained in:
1 parent
df32c72cc9
commit
453e715f3b
94 files changed
+7059
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 管理命令。
|
||||
* <p>
|
||||
* 全局多线程优化<b>默认启用</b>,不提供全局开关(运行时切换全局并行风险高,容易引发
|
||||
* 状态不一致问题)。若触发自动降级(见 {@link FaultGuard}),默认退回原版串行,重启游戏
|
||||
* 即恢复启用,也可执行 {@code /dimensionalripper fine on} 手动复位。
|
||||
* <p>
|
||||
* /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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> 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<Level> 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<Level> dimension) {
|
||||
return switch (dimension.location().getPath()) {
|
||||
case "overworld" -> "主世界";
|
||||
case "the_nether" -> "下界";
|
||||
case "the_end" -> "末地";
|
||||
default -> dimension.location().toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立重写)。
|
||||
* <p>
|
||||
* {@code EntitySectionStorage.sections} 字段类型是 fastutil {@code Long2ObjectMap},mixin 无法改字段类型,
|
||||
* 只能替换<b>实例</b>。fastutil 没有现成的并发 {@code Long2ObjectMap},因此继承
|
||||
* {@link Long2ObjectOpenHashMap}(保持字段类型兼容),内部改用 {@link ConcurrentHashMap} 作为实际存储,
|
||||
* 仅覆盖被 {@code EntitySectionStorage} 实际调用的方法(get/remove/computeIfAbsent/keySet/size/...)。
|
||||
* <p>
|
||||
* 弱一致性说明:{@link #keySet()} 返回的 {@code LongSet} 是 {@link ConcurrentHashMap#keySet()} 的
|
||||
* 弱一致视图,并发增删期间迭代不会抛 {@code ConcurrentModificationException},可安全用于
|
||||
* {@code getAllChunksWithExistingSections} 的结构遍历。
|
||||
* <p>
|
||||
* 未覆盖的方法(entrySet/long2ObjectEntrySet/values/putAll 等)会落到父类的内部 fastutil 数组(恒空),
|
||||
* 因 {@code EntitySectionStorage} 从不对 {@code sections} 调用它们,故无影响(若未来使用需补充覆盖)。
|
||||
*/
|
||||
public final class ConcurrentLong2ObjectMap<V> extends Long2ObjectOpenHashMap<V> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 实际存储:并发哈希表。 */
|
||||
private final Map<Long, V> backing = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public V get(long key) {
|
||||
V v = backing.get(key);
|
||||
return (v == null && !backing.containsKey(key)) ? defaultReturnValue() : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
return (key instanceof Long) ? get((long) (Long) key) : defaultReturnValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(long key) {
|
||||
return backing.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return key instanceof Long && backing.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(long key, V value) {
|
||||
return backing.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(Long key, V value) {
|
||||
return backing.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(long key) {
|
||||
return backing.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
return backing.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(long key, Object value) {
|
||||
return backing.remove(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object key, Object value) {
|
||||
return key instanceof Long && backing.remove(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(long key, Long2ObjectFunction<? extends V> mappingFunction) {
|
||||
V v = backing.computeIfAbsent(key, l -> mappingFunction.get(l));
|
||||
return v == null ? defaultReturnValue() : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(long key, LongFunction<? extends V> mappingFunction) {
|
||||
V v = backing.computeIfAbsent(key, l -> mappingFunction.apply(l));
|
||||
return v == null ? defaultReturnValue() : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return backing.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return backing.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
backing.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongSet keySet() {
|
||||
return new BackedKeySet(backing.keySet());
|
||||
}
|
||||
|
||||
/** {@link ConcurrentHashMap#keySet()} 的 fastutil 弱一致 {@code LongSet} 视图。 */
|
||||
private static final class BackedKeySet extends AbstractLongSet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Set<Long> backing;
|
||||
|
||||
BackedKeySet(Set<Long> 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<Long>} 包装成 fastutil 原语迭代器。 */
|
||||
private static final class IteratorView implements LongIterator {
|
||||
private final java.util.Iterator<Long> delegate;
|
||||
|
||||
IteratorView(java.util.Iterator<Long> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立重写)。
|
||||
* <p>
|
||||
* {@code EntitySectionStorage.sectionIds} 字段类型是 fastutil {@code LongSortedSet},mixin 无法改字段类型,
|
||||
* 只能替换<b>实例</b>。fastutil 没有现成的并发 {@code LongSortedSet},因此实现
|
||||
* {@link LongSortedSet},内部用 {@link ConcurrentSkipListSet}(有序、弱一致迭代、无 CME)。
|
||||
* <p>
|
||||
* 与 MCMT 参考实现的关键差异:{@link #subSet}/{@link #headSet}/{@link #tailSet} 返回的是
|
||||
* 同一 {@link ConcurrentSkipListSet} 子集视图的弱一致包装(不拷贝快照),避免每查询一次
|
||||
* {@code forEachAccessibleNonEmptySection} 就分配一棵新 AVL 树的 GC 开销;迭代器弱一致,
|
||||
* 并发增删期间迭代不会抛 {@code ConcurrentModificationException}。
|
||||
* <p>
|
||||
* {@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<Long> back;
|
||||
|
||||
public ConcurrentLongSortedSet() {
|
||||
this.back = new ConcurrentSkipListSet<>();
|
||||
}
|
||||
|
||||
private ConcurrentLongSortedSet(NavigableSet<Long> 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<Long>} 包装成 fastutil 双向原语迭代器(previous 不用于本集合场景)。 */
|
||||
private static final class SortedIteratorView extends AbstractLongBidirectionalIterator {
|
||||
private final java.util.Iterator<Long> delegate;
|
||||
|
||||
SortedIteratorView(java.util.Iterator<Long> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立重写)。
|
||||
* <p>
|
||||
* {@code EntityLookup.byId} 字段类型是 fastutil {@code Int2ObjectMap},mixin 无法改字段类型,
|
||||
* 只能替换<b>实例</b>。fastutil 无现成并发版,因此继承 {@link Int2ObjectOpenHashMap}(保持字段类型兼容),
|
||||
* 内部用 {@link ConcurrentHashMap} 作为实际存储,仅覆盖 {@code EntityLookup} 用到的
|
||||
* get/put/remove/values/size 等方法。
|
||||
* <p>
|
||||
* {@link #values()} 返回 {@link ConcurrentHashMap#values()} 的弱一致视图(迭代并发增删不抛 CME),
|
||||
* 供 {@code EntityLookup.getEntities/getAllEntities} 安全遍历。
|
||||
*/
|
||||
public final class Int2ObjectConcurrentHashMap<V> extends Int2ObjectOpenHashMap<V> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 实际存储:并发哈希表。 */
|
||||
private final Map<Integer, V> 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<V> values() {
|
||||
return new ValuesView(backing.values());
|
||||
}
|
||||
|
||||
/** {@link ConcurrentHashMap#values()} 的弱一致视图(迭代安全)。 */
|
||||
private static final class ValuesView<V> extends AbstractObjectCollection<V> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Collection<V> backing;
|
||||
|
||||
ValuesView(Collection<V> backing) {
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectIterator<V> iterator() {
|
||||
return ObjectIterators.asObjectIterator(backing.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return backing.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 细粒度并行(subtask)下,多个子任务可能并发访问<b>同一区块</b>内的方块实体 / 方块 /
|
||||
* getChunk / 光照等结构。用「区块坐标 → {@link ReentrantLock}」做互斥:同一区块内的子任务
|
||||
* 串行,不同区块并行,既保证安全又保留并行度。
|
||||
* <p>
|
||||
* <b>第5步(6b):radius 多区块锁。</b>某些子任务会<b>跨区块</b>访问(如破坏方块、爆炸、
|
||||
* 大型实体),单区块锁不足。提供 {@link #lock(BlockPos, int)} / {@link #lock(int, int, int)},
|
||||
* 锁定以目标区块为中心 {@code (1+2r)²} 个区块的正方形范围:先对目标区块坐标<b>排序</b>
|
||||
* 再逐个加锁(排序保证多线程以相同顺序取锁,杜绝循环等待死锁),解锁时<b>逆序</b>释放。
|
||||
* 仍保留单区块 {@link #lock(BlockPos)} 供普通子任务使用(等价 radius=0)。
|
||||
* <p>
|
||||
* 线程安全:锁表用 {@link ConcurrentHashMap},{@code computeIfAbsent} 原子取锁,避免并发创建。
|
||||
* 锁按需惰性创建;为控制内存,锁数量与活跃区块数同量级,可接受(如需要可在后续版本加入过期回收)。
|
||||
*/
|
||||
public final class ChunkLock {
|
||||
private static final ConcurrentHashMap<Long, ReentrantLock> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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):自适应区块预取。
|
||||
* <p>
|
||||
* 原理:在玩家 tick(主线程 {@code playerList.tick()} → {@code ServerPlayer.doTick()})时,
|
||||
* 根据玩家移动方向,对<b>前方</b>尚未加载的区块投递一个<b>临时的 FULL 级 ticket</b>,让原版
|
||||
* 区块生成管线提前把该区块生成好;玩家到达时直接命中已生成缓存,无需现场等待。
|
||||
* <p>
|
||||
* 安全 / 无泄漏:
|
||||
* <ul>
|
||||
* <li>使用<b>自定义 {@link TicketType}</b>({@code timeout}=10s),到期由
|
||||
* {@code DistanceManager.purgeStaleTickets()} 自动回收,无需手动 remove,不存在 ticket 泄漏;</li>
|
||||
* <li>只在玩家<b>实际移动</b>时、且方向可确定时才预取,静止/回退不产生任何 ticket;</li>
|
||||
* <li>每玩家每 {@link #PREFETCH_INTERVAL_TICKS} 才重算一次,避免逐 tick 开销;</li>
|
||||
* <li>已 FULL 的区块直接跳过({@code hasChunk}),不重复下 ticket;</li>
|
||||
* <li>全流程 try/catch,预取任何异常都不影响玩家 tick(纯增益,可安全回退)。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 不碰生成管线、不碰地形生成 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<Unit> PREFETCH_TICKET =
|
||||
TicketType.create("dimensionalripper_prefetch", (a, b) -> 0, PREFETCH_TIMEOUT_TICKS);
|
||||
|
||||
/** 每玩家轻量状态(上次位置 / 上次所在 chunk / 上次重算游戏刻)。 */
|
||||
private static final ConcurrentHashMap<UUID, State> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 识别 + 线程池托管。
|
||||
* <p>
|
||||
* 核心思想:不固定"每维度专属线程",而是所有维度共用一个线程池,主线程每 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_");
|
||||
}
|
||||
}
|
||||
@@ -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 内完成。
|
||||
* <p>
|
||||
* 线程安全:维度 worker 写({@link #schedule}),主线程读({@link #flush}),
|
||||
* 且主线程 flush 时所有维度 worker 已 awaitCompletion 完成,无竞争。
|
||||
*/
|
||||
public final class DimensionChangeQueue {
|
||||
private static final Deque<PendingChange> 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} 收口处调用)。
|
||||
* <p>
|
||||
* 直接调用 {@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) {
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 实体 tick 并行与方块实体/区块环境并行的关键区别:{@code ServerLevel.tick} 里对每个实体
|
||||
* 的 {@code guardEntityTick(...)} 调用位于 {@code entityTickList.forEach(...)} 的 lambda 内部,
|
||||
* 无法像 TE/区块那样在循环体内 {@code @Redirect}。因此这里用 <b>ThreadLocal 屏障</b>在
|
||||
* {@code ServerLevelEntityTickMixin} 的 {@code forEach} 前后建立/收口,再由
|
||||
* {@code EntityTickParallelMixin} 在 {@code guardEntityTick} 处读取当前屏障并提交并行。
|
||||
* <p>
|
||||
* <b>第14步(对齐 Async 零锁路线)</b>:实体移动、生成、移除的共享状态({@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<FineGrainScheduler.Barrier> CURRENT = new ThreadLocal<>();
|
||||
|
||||
/** 本 tick 收集的待并行实体批(仅并行窗口 worker 线程非 null)。 */
|
||||
private static final ThreadLocal<Batch> 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 调用时<b>是否真正加了锁</b>,unlock 时弹出对应决定。
|
||||
* <p>
|
||||
* 背景:{@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<Deque<Boolean>> 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 分片加<b>写</b>锁(内容增删,独占);记录配对决定。 */
|
||||
public static void lockSection(long sectionKey) {
|
||||
boolean locked = !isLockDisabled();
|
||||
LOCK_STACK.get().push(locked);
|
||||
if (locked) {
|
||||
SECTION_LOCKS[stripe(sectionKey)].writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 对指定 section 分片解<b>写</b>锁;严格按 lock 时的配对决定解锁。 */
|
||||
public static void unlockSection(long sectionKey) {
|
||||
if (popLocked()) {
|
||||
SECTION_LOCKS[stripe(sectionKey)].writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 对指定 section 分片加<b>读</b>锁(内容遍历/查询,共享);记录配对决定。 */
|
||||
public static void lockSectionRead(long sectionKey) {
|
||||
boolean locked = !isLockDisabled();
|
||||
LOCK_STACK.get().push(locked);
|
||||
if (locked) {
|
||||
SECTION_LOCKS[stripe(sectionKey)].readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 对指定 section 分片解<b>读</b>锁;严格按 lock 时的配对决定解锁。 */
|
||||
public static void unlockSectionRead(long sectionKey) {
|
||||
if (popLocked()) {
|
||||
SECTION_LOCKS[stripe(sectionKey)].readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 弹出本线程最近一次 lock 的配对决定(LIFO);栈空视为未加锁(防御)。 */
|
||||
private static boolean popLocked() {
|
||||
Deque<Boolean> stack = LOCK_STACK.get();
|
||||
Boolean locked = stack.pollFirst();
|
||||
if (stack.isEmpty()) {
|
||||
LOCK_STACK.remove();
|
||||
}
|
||||
return Boolean.TRUE.equals(locked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否应跳过锁(零开销纯串行)。
|
||||
* <p>
|
||||
* 锁的唯一目的是保护<b>并行子任务</b>对 {@code EntitySection} 的并发增删。而子任务是否
|
||||
* 并行由 {@link FineGrainScheduler#isEnabled()} 决定({@code submitBatch} 关闭时同步串行)。
|
||||
* 因此锁的启用必须与细粒度并行开关<b>一致</b>:若这里跟随全局 {@code ModZuozhi.ENABLED},
|
||||
* 会出现"全局 off → 锁被跳过,但细粒度仍开 → 子任务并行增删而主线程无锁遍历"的
|
||||
* {@code ConcurrentModificationException} 竞态(已实测崩溃)。
|
||||
* <p>
|
||||
* <b>残留保护</b>:关闭细粒度({@code /dimensionalripper fine off}、{@code /dimensionalripper off}、
|
||||
* FaultGuard 降级)瞬间,{@code SUB_POOL} 里可能仍有<b>已提交未跑完</b>的实体子任务;
|
||||
* 它们与退化为串行的维度 tick / 主线程并发改 {@code EntitySection}。因此只要还存在残留
|
||||
* 子任务({@link FineGrainScheduler#activeSubTasks()} > 0),锁就必须保持启用,直到
|
||||
* {@code awaitIdle} 把它们全部排空后才回到零锁纯串行。
|
||||
* <p>
|
||||
* 注意:本判定可能随时间变化({@code fine on|off} 切换)。lock/unlock 的<b>配对一致性</b>
|
||||
* 由 {@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<Entity> consumer;
|
||||
public final List<Entity> entities = new ArrayList<>();
|
||||
|
||||
public Batch(MinecraftServer server, Consumer<Entity> consumer) {
|
||||
this.server = server;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
}
|
||||
|
||||
/** 收集一个待并行 tick 的实体(在 {@code guardEntityTick} 中调用,替代逐实体提交)。 */
|
||||
public static void collect(MinecraftServer server, Consumer<Entity> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
|
||||
/**
|
||||
* 第9步后半:故障监控与自动回退(fail-safe / graceful degradation)。
|
||||
* <p>
|
||||
* 目标:运行期检测到并行故障时<b>安全降级而非崩溃</b>。采用两级回退,先关细粒度、再退回原版串行:
|
||||
* <ul>
|
||||
* <li><b>细粒度降级</b>:子任务连续异常达到 {@link #FINE_FAULT_THRESHOLD} 次 → 自动关闭
|
||||
* {@link FineGrainScheduler}(退回纯维度并行)。子任务异常发生在独立子任务池内,不影响
|
||||
* 维度 tick 收口,因此关闭是安全的。</li>
|
||||
* <li><b>维度并行回退</b>:维度 tick 连续异常达到 {@link #DIM_FAULT_THRESHOLD} 次,或维度 tick
|
||||
* 连续超时(看门狗,{@link #AWAIT_TIMEOUT_MS})达到 {@link #AWAIT_TIMEOUT_THRESHOLD} 次 →
|
||||
* 关闭全局 {@link ModZuozhi#ENABLED},退回原版串行。</li>
|
||||
* </ul>
|
||||
* 计数均为<b>连续</b>语义(成功/按时完成会清零),容忍偶发抖动,只有持续故障才触发回退。
|
||||
* 手动 {@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] 降级状态已重置");
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 在现有 dimthreads 式「维度级并行」基础上,提供<b>维度内更细粒度并行</b>的能力:
|
||||
* 维度 worker 在本维度的 tick 内部,把可并行的子单元(方块实体、区块环境、白名单实体)
|
||||
* 提交到<b>独立的细粒度子任务池</b>并行执行,再用本维度自己的屏障 {@link Barrier} 收口,
|
||||
* 保持该维度 tick 内部的顺序边界,同时兼容外层主线程的 {@code awaitCompletion()} 收口。
|
||||
* <p>
|
||||
* 设计要点:
|
||||
* <ul>
|
||||
* <li><b>独立线程池</b>:子任务使用 {@link #SUB_POOL} 专用池,与维度 worker 池解耦。
|
||||
* 否则"维度 worker 阻塞 await 等待子任务"会占满维度池线程,导致子任务永远无人执行
|
||||
* 而死锁(这也是之前必现死锁的根因)。独立池固定 CPU 线程,从结构上不可能死锁。</li>
|
||||
* <li><b>独立开关</b>:{@link #isEnabled()} 默认关闭(保守)。关闭时 {@link Barrier#submit}
|
||||
* 直接同步执行,行为完全退化为串行,绝不引入并发风险;由 {@code /dimensionalripper fine on|off}
|
||||
* 运行时切换。</li>
|
||||
* <li><b>异常隔离</b>:子任务异常只记录日志,不中断本维度 tick,也不污染外层维度收口。</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<PostExecuteQueue> CURRENT_QUEUE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 正在 {@link #SUB_POOL} 中运行或排队的子任务总数(含残留排空期)。
|
||||
* <p>
|
||||
* 用于两点:
|
||||
* <ol>
|
||||
* <li>{@link #awaitIdle} 判断是否排空(比 {@code getActiveCount()+queue} 更准,覆盖排队任务);</li>
|
||||
* <li>{@link EntityTickParallel#isLockDisabled} 在「细粒度已关但仍有残留子任务」期间保持
|
||||
* 分片锁启用,保证残留子任务与退化为串行的维度 tick 并发改 {@code EntitySection} 时仍互斥,
|
||||
* 否则 `fine off`/`off` 瞬间锁被禁用 + 残留子任务无锁运行 → CME(已实测)。</li>
|
||||
* </ol>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待已提交的细粒度子任务全部完成(全局关闭时调用,带超时兜底)。
|
||||
* <p>
|
||||
* 目的:{@code /dimensionalripper off} 或自动降级后,{@link #SUB_POOL} 里可能残留上一拍提交、
|
||||
* 尚未跑完的实体子任务;此时主线程已退回原版串行 tick 同一批实体,会与残留子任务
|
||||
* <b>并发 tick 同一实体</b>导致异常(表现为"关闭时反复出现子任务异常")。先排空
|
||||
* 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()} 等待全部完成。
|
||||
* <p>
|
||||
* 仅在 {@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 批量化思路,独立实现)。
|
||||
* <p>
|
||||
* 按线程池大小把实体列表切成块,每块作为一个子任务提交并行执行,把调度/屏障计数
|
||||
* 开销从 O(实体数) 降到 O(线程数)。块内逐实体 tick 并保持异常隔离。
|
||||
*
|
||||
* @param server 当前服务器
|
||||
* @param entities 待并行实体列表
|
||||
* @param consumer 实体 tick 函数(如 {@code tickNonPassenger})
|
||||
*/
|
||||
public void submitBatch(MinecraftServer server, List<Entity> entities, Consumer<Entity> 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<Entity> chunk = entities.subList(i, end);
|
||||
SUB_POOL.execute(() -> modzuozhi_runChunk(chunk, consumer));
|
||||
}
|
||||
}
|
||||
|
||||
/** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */
|
||||
private void modzuozhi_runChunk(List<Entity> chunk, Consumer<Entity> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} 注入。
|
||||
* <p>
|
||||
* 并行子任务需要在线程本地随机源上执行随机 tick,但原版 {@code ServerLevel.tickChunk} 直接访问
|
||||
* {@code Level.random}({@code public final} 共享字段)。由于在 NeoForge 开发环境无法对
|
||||
* Minecraft 混淆字段做 {@code @Redirect}(无 refmap),这里改为在 {@code ServerLevel} 上提供
|
||||
* 一个<b>等价的自定义实现</b> {@link #modzuozhi_tickChunk},它接收线程本地随机源作为参数,
|
||||
* 由 {@code ServerChunkCacheMixin} 在并行子任务中调用,从而完全绕开共享随机源。
|
||||
*/
|
||||
public interface IChunkEnvTick {
|
||||
void modzuozhi_tickChunk(LevelChunk chunk, int randomTickSpeed, RandomSource random);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
/**
|
||||
* 可迁移主线程引用的对象(Level.thread / ServerChunkCache.mainThread)。
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
/**
|
||||
* 第2步:网络包按玩家分派到维度线程——{@code ServerLevel} 的每维度玩家包队列访问接口。
|
||||
* <p>
|
||||
* 由 {@code ServerLevelPlayerPacketMixin} 注入实现,供 {@code PacketUtilsDimMixin}
|
||||
* (包处理重定向)与 {@code ServerLevelPlayerPacketMixin}(tick 收口 drain)使用。
|
||||
*/
|
||||
public interface IPlayerPacketQueue {
|
||||
/** 获取本维度的玩家包处理队列。 */
|
||||
PlayerPacketQueue modzuozhi$playerPacketQueue();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
/**
|
||||
* 供 {@code EntitySectionMixin} 注入的 section key 访问器。
|
||||
* <p>
|
||||
* {@code EntitySection} 自身不持有其在 {@code EntitySectionStorage} 中的 section key,
|
||||
* 细粒度分片锁需要该 key 定位锁片。本接口由 {@code EntitySectionMixin} 实现,注入
|
||||
* {@code @Unique} 字段保存 key,并由 {@code EntitySectionStorageMixin} 在
|
||||
* {@code createSection} 时写入。
|
||||
* <p>
|
||||
* 注意:该接口<b>不能</b>放在 mixin 包内——Mixin 禁止从被转换类直接引用 mixin 包中的
|
||||
* 类型({@code IllegalClassLoadError})。因此放在核心包 {@code core.dimthread}。
|
||||
*/
|
||||
public interface ISectionKeyAccessor {
|
||||
void modzuozhi_setSectionKey(long key);
|
||||
|
||||
long modzuozhi_getSectionKey();
|
||||
}
|
||||
@@ -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} 机制独立重写)。
|
||||
* <p>
|
||||
* <b>目标</b>:每个 {@link ServerLevel} 一个常驻循环,各自按 tick 预算独立排下一次 tick。
|
||||
* 某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再像旧「有界屏障」模型那样由
|
||||
* 主线程 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。
|
||||
* <p>
|
||||
* 每个循环在一"拍"里打包三件事(对应原版 {@code tickChildren} 里逐维度的部分):
|
||||
* <ol>
|
||||
* <li>本维度 {@code level.tick()};</li>
|
||||
* <li>本维度玩家的连接 tick(原版玩家 {@code doTick()} 走的就是网络连接路径);</li>
|
||||
* <li>本维度玩家的区块发送 + 刷新恢复。</li>
|
||||
* </ol>
|
||||
* 全局部分(命令函数、全局连接监听、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<Connection> connections = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** 从主线程/跨线程投递回本 loop 执行的任务队列。 */
|
||||
private final Queue<Runnable> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 排空本维度区块源任务(距离更新 + 状态提升 + 生成调度 + 光照调度 + 队列)。
|
||||
* <p>
|
||||
* 原版主线程在 {@code waitUntilNextTick → pollTaskInternal → ServerChunkCache.pollTask} 期间会
|
||||
* <b>连续</b>排空这些任务,是区块加载吞吐量的主要来源。切到维度 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<Connection> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* 第2步:网络包按玩家分派到维度线程的每维度任务队列。
|
||||
* <p>
|
||||
* 借鉴 Chlorophyll(Unlicense)的 EntityTaskScheduler 思路:把玩家发来的游戏包处理任务
|
||||
* 投递到<b>玩家所在维度</b>的队列,由该维度 worker 在维度 tick 收口处统一执行(见
|
||||
* {@code ServerLevelPlayerPacketMixin})。主线程不再逐一执行所有玩家的包 handle,减轻主线程
|
||||
* 瓶颈;多维度下各维度的包处理与维度 tick 并行。
|
||||
* <p>
|
||||
* 线程安全:{@link ConcurrentLinkedQueue} 支持 netty 读线程 / 服务器主线程并发投递;
|
||||
* {@link #drain()} 仅由本维度 tick 线程(维度 worker 或单机主线程串行路径)调用一次。
|
||||
*/
|
||||
public final class PlayerPacketQueue {
|
||||
/** 标记当前线程正在执行玩家包任务(handle 展开中)。 */
|
||||
private static final ThreadLocal<Boolean> EXECUTING = new ThreadLocal<>();
|
||||
|
||||
private final ConcurrentLinkedQueue<Runnable> tasks = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** 投递一个待处理的玩家包任务(任意线程安全)。 */
|
||||
public void offer(Runnable task) {
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行执行并清空队列(仅本维度 tick 线程调用)。
|
||||
* <p>
|
||||
* 执行期间设置 {@link #EXECUTING} 标记:玩家包 handle(如
|
||||
* {@code ServerGamePacketListenerImpl.handleMovePlayer})开头会再次调用
|
||||
* {@code PacketUtils.ensureRunningOnSameThread},此时应<b>直接通过</b>(等价原版
|
||||
* "已在正确线程"),否则会把包再次投回队列造成无限循环。
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package dev.modzuozhi.core.dimthread;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* 跨线程延迟执行队列(借鉴 MCMT 的 PostExecutePool 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 细粒度并行下,子任务(如方块实体 tick)运行在其它 worker 线程上,可能触发对
|
||||
* {@code Level} 内部非线程安全结构(如 {@code pendingBlockEntityTickers}、
|
||||
* {@code pendingFreshBlockEntities})的写入。为保证安全,这些写入不直接发生,
|
||||
* 而是投递到本队列,由<b>维度 worker 线程</b>在本维度 tick 的收口处串行执行
|
||||
* (此时所有并行子任务已 {@code await} 完成、无并发)。
|
||||
* <p>
|
||||
* 线程安全:{@link ConcurrentLinkedQueue} 支持多线程并发投递;{@link #drain()}
|
||||
* 只由维度 worker 调用一次。
|
||||
*/
|
||||
public final class PostExecuteQueue {
|
||||
private final ConcurrentLinkedQueue<Runnable> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 模型)。
|
||||
* <p>
|
||||
* 线程池按服务器懒创建(每个 {@code MinecraftServer} 实例一个),并在服务器停止时关闭清理。
|
||||
* <p>
|
||||
* 是否启用维度并行,由两个条件共同决定:全局开关 {@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<MinecraftServer, Boolean> actives = new ConcurrentHashMap<>();
|
||||
public final Map<MinecraftServer, ThreadPool> threadPools = new ConcurrentHashMap<>();
|
||||
/** 缓存上次同步的维度数,维度数未变化时跳过遍历与线程调整,把每 tick 开销降到最低。 */
|
||||
private volatile int lastDimCount = -1;
|
||||
|
||||
/** 每维度独立 tick 循环(维度隔离模型,见 {@link LevelTickLoop})。key 为维度。 */
|
||||
private final Map<ServerLevel, LevelTickLoop> loops = new ConcurrentHashMap<>();
|
||||
/** 承载所有维度 loop 自排程的调度池({@code schedule(task, nspt)} 需要 Scheduled 能力)。 */
|
||||
private volatile ScheduledThreadPoolExecutor masterPool;
|
||||
/** 玩家连接当前所属维度(key 为 Connection,value 为维度),供跨维度/登录时幂等迁移连接。 */
|
||||
private final Map<Connection, ServerLevel> 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收不再属于当前活跃服务器的僵尸线程池。
|
||||
* <p>
|
||||
* 启动 / 世界加载阶段可能创建多个 {@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)。
|
||||
* <p>
|
||||
* 注意:细粒度子任务运行在 {@code FineGrainScheduler.SUB_POOL}(独立线程池),维度池
|
||||
* 只需承载「每维度 1 个 worker」的维度 tick,<b>无需</b>随 CPU 扩容。早期版本曾在此按
|
||||
* {@code fine} 开关把维度池扩容到 CPU 核数,但独立子任务池上线后该逻辑已冗余,会造成
|
||||
* 大量闲置的维度线程。统一收缩到维度数即可。
|
||||
* <p>
|
||||
* 目标值未变化时(绝大多数 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)。
|
||||
* <p>
|
||||
* 启动阶段 {@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(幂等)。
|
||||
* <p>
|
||||
* 用 {@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 调用:把每个在线玩家的连接路由到其<b>当前</b>维度 loop。
|
||||
* <p>
|
||||
* 登录、跨维度传送、重生换维度三大路径的时序复杂,这里用一个幂等的「回收」统一收口:遍历
|
||||
* {@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();
|
||||
}
|
||||
}
|
||||
@@ -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 模型)。
|
||||
* <p>
|
||||
* 所有维度共用一个线程池,通过 {@code activeCount} 计数 + {@link #awaitCompletion()}
|
||||
* 让主线程在每 tick 分发完所有维度后等待全部完成。线程均为 daemon,避免阻止服务器停止。
|
||||
* <p>
|
||||
* 线程数<strong>可动态调整</strong>:{@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)。使用无界队列,多余任务排队等待,绝不因任务数超过线程数而拒绝。
|
||||
* <p>
|
||||
* 顺序很重要:{@code setCorePoolSize(c)} 要求 {@code c <= maximumPoolSize},否则抛
|
||||
* {@code IllegalArgumentException}。因此<b>增大</b>线程数时先扩 {@code max} 再扩
|
||||
* {@code core};<b>缩小</b>时先缩 {@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 <T> void execute(Iterator<T> iterator, Consumer<T> action) {
|
||||
iterator.forEachRemaining(t -> this.execute(() -> action.accept(t)));
|
||||
}
|
||||
|
||||
public <T> void execute(Iterable<T> iterable, Consumer<T> action) {
|
||||
iterable.forEach(t -> this.execute(() -> action.accept(t)));
|
||||
}
|
||||
|
||||
public <T> void execute(T[] array, Consumer<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} 展示,验证优化效果)。
|
||||
* <p>
|
||||
* 维度独立 tick 循环改造后,主线程退化为「协调者」,恒按 20 拍运行;真实负载体现在每个维度 loop
|
||||
* 的<b>单次 tick 实际耗时(MSPT)</b>上。因此这里按维度记录每次 {@code internalTick} 的真实耗时,
|
||||
* {@code TPS = min(20, 1e9 / 平均MSPT)}:
|
||||
* <ul>
|
||||
* <li>空闲时 MSPT 远小于 50ms → TPS = 20;</li>
|
||||
* <li>某维度单 tick 超过 50ms 预算 → TPS < 20(真实掉 TPS)。</li>
|
||||
* </ul>
|
||||
* 这比「记录相邻 tick 的墙钟间隔」更准确——后者会把自排程定时器的唤醒延迟(每拍约 0.5~0.8ms)
|
||||
* 一并计入,导致完全空闲时仍显示 19.7。
|
||||
* <p>
|
||||
* 主线程样本 {@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<ResourceKey<Level>, 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<Level> 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<Level> dimension) {
|
||||
Sample sample = DIMENSIONS.get(dimension);
|
||||
return sample == null ? 20.0 : sample.tps();
|
||||
}
|
||||
|
||||
/** 按维度名排序返回每维度的 TPS,供 {@code /dimensionalripper status} 展示。 */
|
||||
public static Map<String, Double> allDimensionTps() {
|
||||
Map<String, Double> result = new TreeMap<>();
|
||||
for (Map.Entry<ResourceKey<Level>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 实体 tick 远比方块实体复杂:实体互相交互、移动会更新 {@code EntitySectionStorage}、
|
||||
* 可能繁殖/转换产生新实体。因此第4步采用<b>保守白名单</b>——先以最复杂的村民(Brain AI)
|
||||
* 验证通过,再扩展为所有原版生物。
|
||||
* <p>
|
||||
* 判定规则(按优先级):
|
||||
* <ol>
|
||||
* <li>命中黑名单 {@link #BLACKLIST} → 串行(已知有问题的类型,可运行时扩展排除);</li>
|
||||
* <li>命中白名单 {@link #WHITELIST} → 并行(显式放行,运行时可用 {@link #add} 增补);</li>
|
||||
* <li>默认:{@code LivingEntity} 且非玩家且<b>原版生物</b> → 并行;其余(modded 生物、
|
||||
* 掉落物、经验球、箭、TNT、盔甲架等非生物实体)→ 串行。</li>
|
||||
* </ol>
|
||||
* modded 生物默认串行是借鉴 Async 的思路(其用 {@code @AsyncCompatible} 注解显式放行):
|
||||
* 跨模组生物行为不可控,保守串行以降低并行下的兼容风险;确需并行时可用 {@link #add} 显式放行。
|
||||
* 玩家始终由 {@link PlayerList} 单独处理,不参与本并行路径。
|
||||
*/
|
||||
public final class EntityTickFilter {
|
||||
/** 显式放行名单:命中即并行(村民已验证,作为兜底显式保留)。 */
|
||||
private static final Set<String> WHITELIST = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** 显式排除名单:命中即强制串行(覆盖默认规则,可用于运行时隔离问题生物)。 */
|
||||
private static final Set<String> BLACKLIST = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* 并行判定缓存:{@code EntityType → 是否并行}。
|
||||
* <p>
|
||||
* 避免每个实体每 tick 都 {@code EntityType.getKey(...).toString()} 构建字符串
|
||||
* (高实体量下是显著开销)。实体类型数量有限,首次遇到时计算一次并缓存;
|
||||
* 黑/白名单运行时变化时 {@link #PARALLEL_CACHE} 清空。
|
||||
*/
|
||||
private static final ConcurrentHashMap<EntityType<?>, Boolean> PARALLEL_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
// 第4步首期验证通过的最难实体(Brain AI),显式保留在放行名单。
|
||||
WHITELIST.add("minecraft:villager");
|
||||
|
||||
// 第5步(6b/6c):爆炸/破坏方块类实体<b>强制串行</b>。
|
||||
// 它们在 tick 中会调用 {@code Level.setBlock} 破坏方块(苦力怕自爆、凋灵、末影龙、
|
||||
// 末影水晶),这会写 LevelChunk 的 PalettedContainer(非线程安全)。若在并行子任务线程
|
||||
// 执行,会与主线程的玩家操作(放置方块)<b>并发写同一区块</b>,导致数据竞争:
|
||||
// 放置的方块立即消失、被破坏的方块在存档中"复活"(重进后 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.modzuozhi.core.filter;
|
||||
|
||||
import net.minecraft.world.level.block.entity.TickingBlockEntity;
|
||||
|
||||
/**
|
||||
* SerDes 过滤器接口(借鉴 MCMT 的 ISerDesFilter)。
|
||||
* <p>
|
||||
* 决定某个方块实体 tick 是否可以在并行 worker 线程中执行。
|
||||
* 返回 {@code true} 表示可并行;返回 {@code false} 表示该方块实体线程不安全,
|
||||
* 需回退到主线程串行 tick。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ISerDesFilter {
|
||||
/**
|
||||
* 该方块实体 tick 是否可并行执行。
|
||||
*
|
||||
* @param ticker 待 tick 的方块实体({@link TickingBlockEntity})
|
||||
* @return true = 可并行;false = 回退主线程串行
|
||||
*/
|
||||
boolean isParallel(TickingBlockEntity ticker);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.modzuozhi.core.filter;
|
||||
|
||||
import net.minecraft.world.level.block.entity.TickingBlockEntity;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 默认 SerDes 过滤器。
|
||||
* <p>
|
||||
* 基于方块实体注册名({@code TickingBlockEntity.getType()},形如 {@code minecraft:furnace})判断:
|
||||
* <ul>
|
||||
* <li>黑名单(强制串行):已知线程不安全、会与主线程交互的方块实体
|
||||
* (如移动的活塞/活塞头,涉及跨区块移除与实体交互)。</li>
|
||||
* <li>modded 方块实体(非 {@code minecraft:} 前缀)默认串行,保守兜底;
|
||||
* 可通过 {@link #setLockModded(boolean)} 关闭该保守行为。</li>
|
||||
* <li>其余 vanilla 方块实体默认可并行。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class SerDesFilter implements ISerDesFilter {
|
||||
/** 全局单例({@link #isParallel} 为实例方法)。 */
|
||||
public static final SerDesFilter INSTANCE = new SerDesFilter();
|
||||
|
||||
/** 强制串行的方块实体注册名(黑名单)。 */
|
||||
private static final Set<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立实现)。
|
||||
* <p>
|
||||
* {@code AttributeInstance} 的修饰符表在并行 tick 下被跨线程读写(增益/减益结算、伤害计算、
|
||||
* 传感器查询)。原版用普通 Map,替换为并发容器;{@code getModifiers} 返回的按操作分类子表
|
||||
* 也并发化,保证 computeIfAbsent 原子且子表写入安全。
|
||||
*/
|
||||
@Mixin(AttributeInstance.class)
|
||||
public abstract class AttributeInstanceMixin {
|
||||
|
||||
@Shadow
|
||||
private final Map<AttributeModifier.Operation, Map<ResourceLocation, AttributeModifier>> modifiersByOperation =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@Shadow
|
||||
private final Map<ResourceLocation, AttributeModifier> modifierById = new ConcurrentHashMap<>();
|
||||
|
||||
@Shadow
|
||||
private final Map<ResourceLocation, AttributeModifier> permanentModifiers = new ConcurrentHashMap<>();
|
||||
|
||||
@WrapMethod(method = "getModifiers(Lnet/minecraft/world/entity/ai/attributes/AttributeModifier$Operation;)Ljava/util/Map;")
|
||||
private Map<ResourceLocation, AttributeModifier> modzuozhi_getModifiers(
|
||||
AttributeModifier.Operation operation, Operation<Map<ResourceLocation, AttributeModifier>> original) {
|
||||
return this.modifiersByOperation.computeIfAbsent(operation, op -> new ConcurrentHashMap<>());
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立实现)。
|
||||
* <p>
|
||||
* 并行 tick 下,传感器/寻路/目标选择会跨线程读取<b>其它实体</b>的属性
|
||||
* ({@code getAttribute → getInstance}、{@code getValue}),属性被修改时还会写入
|
||||
* {@code attributesToUpdate/attributesToSync}。原版 {@code AttributeMap} 用普通 Map/Set,
|
||||
* 跨线程读写存在数据竞争。全部替换为并发容器,读写安全。
|
||||
*/
|
||||
@Mixin(AttributeMap.class)
|
||||
public abstract class AttributeMapMixin {
|
||||
|
||||
@Shadow
|
||||
private final Map<Holder<Attribute>, AttributeInstance> attributes = new ConcurrentHashMap<>();
|
||||
|
||||
@Shadow
|
||||
private final Set<AttributeInstance> attributesToSync = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Shadow
|
||||
private final Set<AttributeInstance> attributesToUpdate = ConcurrentHashMap.newKeySet();
|
||||
}
|
||||
@@ -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} 添加维度检查。
|
||||
* <p>
|
||||
* 原版 1.21.1 的 {@code create()} 不做维度检查,当幼年生物追踪的成年目标在另一个维度时,
|
||||
* 幼年生物会尝试跨维度寻路。并行环境下需确保幼年生物只跟随同一维度的目标。
|
||||
* <p>
|
||||
* 直接覆盖 1.21.1 实际存在的 {@code create(UniformInt, Function)} 重载(此为底层实现,
|
||||
* {@code create(UniformInt, float)} 委托给此方法),增加维度检查。
|
||||
*/
|
||||
@Mixin(BabyFollowAdult.class)
|
||||
public class BabyFollowAdultGuardMixin {
|
||||
|
||||
/**
|
||||
* @reason 添加维度守卫,防止幼年生物跨维度跟随成年目标
|
||||
*/
|
||||
@Overwrite
|
||||
public static @NotNull OneShot<AgeableMob> create(
|
||||
UniformInt uniformInt,
|
||||
Function<LivingEntity, Float> 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;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* {@code BinaryHeap} 是 A* 寻路的开放集(open set),{@code Node.heapIdx} 记录节点在堆中的
|
||||
* 下标(-1 表示不在堆中)。每个 {@code PathFinder} 拥有<b>独立</b>的 {@code BinaryHeap},
|
||||
* 并行实体各自寻路时堆对象不跨实体共享,因此不会并发读写同一堆。
|
||||
* <p>
|
||||
* 但并行寻路下仍有<b>防御性</b>需要:若节点重复 {@code insert}(其 {@code heapIdx} 已在堆中),
|
||||
* 原版直接抛 {@code IllegalStateException("OW KNOWS!")};若 {@code upHeap/downHeap} 收到
|
||||
* {@code heapIdx == -1} 的节点会数组越界。这些异常在正常串行下不会发生,但在并行子任务的
|
||||
* 寻路中,若某处对同一节点的堆状态判定出现竞态,可能触发。为让并行寻路<b>失败不崩溃</b>,
|
||||
* 这里加守卫:已入堆的节点直接返回(不重复插入);堆下标为 -1 时跳过堆调整。
|
||||
* <p>
|
||||
* 语义影响:仅忽略"重复入堆/对已出堆节点的堆调整"这类非法调用,正常寻路路径完全不受影响。
|
||||
*/
|
||||
@Mixin(BinaryHeap.class)
|
||||
public abstract class BinaryHeapMixin {
|
||||
|
||||
/**
|
||||
* {@code insert}:若节点已在堆中({@code heapIdx >= 0}),跳过插入,避免重复入堆破坏堆结构。
|
||||
*/
|
||||
@WrapMethod(method = "insert")
|
||||
private Node modzuozhi_guardInsert(Node node, Operation<Node> original) {
|
||||
if (node.heapIdx >= 0) {
|
||||
return node;
|
||||
}
|
||||
return original.call(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code upHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。
|
||||
*/
|
||||
@WrapMethod(method = "upHeap")
|
||||
private void modzuozhi_guardUpHeap(int index, Operation<Void> original) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
original.call(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code downHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。
|
||||
*/
|
||||
@WrapMethod(method = "downHeap")
|
||||
private void modzuozhi_guardDownHeap(int index, Operation<Void> original) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
original.call(index);
|
||||
}
|
||||
}
|
||||
@@ -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 的方块掉落捕获对多线程安全。
|
||||
* <p>
|
||||
* NeoForge 在 {@code Block.dropResources} 中通过<b>全局静态字段</b> {@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}。
|
||||
* <p>
|
||||
* 这里把掉落捕获改为<b>线程本地</b>:每个线程维护独立的捕获列表,从根上消除跨线程污染。
|
||||
* 语义与原版一致(仅在捕获开始到结束之间拦截掉落),但并行安全。
|
||||
*/
|
||||
@Mixin(Block.class)
|
||||
public abstract class BlockDropCaptureMixin {
|
||||
/** 线程本地掉落捕获列表,替代原版全局静态 {@code Block.capturedDrops}。 */
|
||||
@Unique
|
||||
private static final ThreadLocal<List<ItemEntity>> 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<ItemEntity> modzuozhi_stopCapture() {
|
||||
List<ItemEntity> 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<ItemEntity> modzuozhi_capturedGet() {
|
||||
return modzuozhi_capturedDrops.get();
|
||||
}
|
||||
}
|
||||
@@ -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 并发竞态。
|
||||
* <p>
|
||||
* 原版 {@code pollTask()} 先 {@code peek()}(非空判断)再 {@code remove()},两步<b>非原子</b>。
|
||||
* 维度 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 次)。
|
||||
* <p>
|
||||
* 仅在<b>本模组维度 worker</b>线程上用 {@code poll()}(空返回 null,不抛异常)替代
|
||||
* {@code remove()}:并发被抢走时返回 false(跳过本拍,下一拍自愈)。其它线程保持原版行为不变。
|
||||
*/
|
||||
@Mixin(BlockableEventLoop.class)
|
||||
public abstract class BlockableEventLoopPollMixin<R extends Runnable> {
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
private Queue<R> 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<Boolean> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立实现)。
|
||||
* <p>
|
||||
* 细粒度并行时,多个实体的 tick 在子任务线程上并发执行。某个生物的大脑内存
|
||||
* ({@code Brain.memories})不仅被它自己的 tick 读写,还可能被<b>其它实体</b>的
|
||||
* 传感器/寻路并发访问(如 {@code NearestLivingEntitySensor} 读取它的记忆)。
|
||||
* 原版 {@code Brain} 用普通 HashMap,跨线程读写存在数据竞争(潜在错误行为而非崩溃)。
|
||||
* <p>
|
||||
* 这里对记忆读写入口加 {@code synchronized(this)}:单对象锁、逐方法加解锁、无嵌套,
|
||||
* 同一大脑同一时刻只允许一个线程访问,结构上无死锁;未竞争时偏向锁开销可忽略。
|
||||
* 相比给整段 villager tick 加锁,仅同步记忆访问点,保留并行收益。
|
||||
*/
|
||||
@Mixin(Brain.class)
|
||||
public abstract class BrainMixin {
|
||||
|
||||
@WrapMethod(method = "getMemory")
|
||||
private <U> Optional<U> modzuozhi_getMemory(MemoryModuleType<U> type, Operation<Optional<U>> original) {
|
||||
synchronized (this) {
|
||||
return original.call(type);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "getMemoryInternal")
|
||||
private <U> Optional<U> modzuozhi_getMemoryInternal(MemoryModuleType<U> type, Operation<Optional<U>> original) {
|
||||
synchronized (this) {
|
||||
return original.call(type);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "hasMemoryValue")
|
||||
private boolean modzuozhi_hasMemoryValue(MemoryModuleType<?> type, Operation<Boolean> original) {
|
||||
synchronized (this) {
|
||||
return original.call(type);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "checkMemory")
|
||||
private boolean modzuozhi_checkMemory(MemoryModuleType<?> type, MemoryStatus status, Operation<Boolean> original) {
|
||||
synchronized (this) {
|
||||
return original.call(type, status);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "setMemoryInternal")
|
||||
private <U> void modzuozhi_setMemoryInternal(
|
||||
MemoryModuleType<U> type, Optional<? extends ExpirableValue<?>> value, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(type, value);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "clearMemories")
|
||||
private void modzuozhi_clearMemories(Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(下一步):把「区块反序列化」从主线程下放到后台线程。
|
||||
* <p>
|
||||
* 原版 {@code ChunkMap.scheduleChunkLoad} 里,磁盘读 + datafix 已经异步(见
|
||||
* {@code readChunk}),但 {@code ChunkSerializer.read}(NBT → ProtoChunk,含方块状态/生物群系
|
||||
* 调色板解析,CPU 密集)仍跑在 {@code this.mainThreadExecutor}。这正是进图 / 快速跑图 / 传送时
|
||||
* 主线程卡顿的元凶之一——本 mixin 把这一步的执行器切换到 {@link Util#backgroundExecutor()}。
|
||||
* <p>
|
||||
* 配套加固:下放后 {@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 = "<init>", 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;
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* {@code EntitySection.getEntities(EntityTypeTest,...)} 走 {@code ClassInstanceMultiMap.find},
|
||||
* 其内部执行 {@code byClass.computeIfAbsent(...)} 懒加载类型列表——这是<b>写</b>操作。
|
||||
* 第10步把 section 内容查询改成<b>共享读锁</b>后,同一 section 的两个并发查询会同时
|
||||
* {@code computeIfAbsent} 同一个普通 {@code HashMap} → {@code ConcurrentModificationException}
|
||||
* (日志已证实:村民大脑传感器在 1 分钟内触发 5 次子任务异常导致自动降级)。
|
||||
* <p>
|
||||
* 修复:把 {@code byClass} 换成 {@link ConcurrentHashMap}——其 {@code computeIfAbsent} 原子且
|
||||
* 线程安全(同一 key 的映射函数只执行一次),并发查询互不干扰。既保留共享读锁的并行度,又根除 CME。
|
||||
* {@code allInstances} 的读写仍由 section 分片锁(写锁 vs 读锁)互斥,无需改动。
|
||||
* <p>
|
||||
* 注:字段 final 已由 access transformer({@code accesstransformer.cfg})移除,本类在
|
||||
* 构造器 RETURN 处替换为并发映射并恢复 {@code baseClass → allInstances} 注册。
|
||||
*/
|
||||
@Mixin(ClassInstanceMultiMap.class)
|
||||
public abstract class ClassInstanceMultiMapMixin<T> {
|
||||
@Shadow
|
||||
private Map<Class<?>, List<T>> byClass;
|
||||
@Shadow
|
||||
private Class<T> baseClass;
|
||||
@Shadow
|
||||
private List<T> allInstances;
|
||||
|
||||
@Inject(method = "<init>", at = @At("RETURN"))
|
||||
private void modzuozhi_concurrentByClass(CallbackInfo ci) {
|
||||
this.byClass = new ConcurrentHashMap<>();
|
||||
this.byClass.put(this.baseClass, this.allInstances);
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 根因:第3步区块环境 tick 并行中,藤蔓等 {@code randomTick} 会在子任务线程调用
|
||||
* {@code Level.setBlockAndUpdate},进而触发 {@code CollectingNeighborUpdater}。它内部用
|
||||
* 非线程安全的 {@code ArrayDeque} 作为邻居更新栈({@code stack}/{@code addedThisLayer}),
|
||||
* 多个子任务线程并发访问同一实例时会抛 {@code NoSuchElementException}(日志已证实)。
|
||||
* <p>
|
||||
* 修复:对 {@code CollectingNeighborUpdater} 的每个公开入口方法加<b>实例级重入锁</b>。
|
||||
* 同一维度的邻居更新串行执行;不同维度(不同实例)互不干扰。锁为可重入锁,内部递归
|
||||
* 更新({@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<Void> 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<Void> 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<Void> 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<Void> original) {
|
||||
this.modzuozhi_neighborLock.lock();
|
||||
try {
|
||||
original.call(pos, block, skipDir);
|
||||
} finally {
|
||||
this.modzuozhi_neighborLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 调度)并发保护。
|
||||
* <p>
|
||||
* 维度并行下,维度 worker / 细粒度 subtask 的 {@code getChunk} 会在同步路径里调用
|
||||
* {@code addTicket/removeTicket} 修改 ticket,而主线程 executor 里的
|
||||
* {@code PlayerTicketTracker.onLevelChange}(玩家移动/跨维度传送触发)也在修改同一
|
||||
* {@code DistanceManager},两者并发导致内部 {@code SortedArraySet.first()} 读到空集等
|
||||
* NPE 竞态。
|
||||
* <p>
|
||||
* 这里的锁覆盖所有 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<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call(chunkPos, ticket);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "removeTicket")
|
||||
private void modzuozhi_lockRemoveTicket(long chunkPos, Ticket<?> ticket, Operation<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call(chunkPos, ticket);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "updateChunkForced")
|
||||
private void modzuozhi_lockUpdateChunkForced(ChunkPos pos, boolean add, Operation<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call(pos, add);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "removeTicketsOnClosing")
|
||||
private void modzuozhi_lockRemoveTicketsOnClosing(Operation<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call();
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 维度循环模型下,主线程 {@code ServerChunkCache.pollTask} 与维度 worker 的
|
||||
* {@code level.tick → ServerChunkCache.tick} 会<b>并发</b>调用 {@code runAllUpdates},
|
||||
* 遍历 {@code chunksToUpdateFutures}(普通 HashSet)时抛 {@code ConcurrentModificationException}。
|
||||
* 用同一把锁串行化,彻底消除该竞态。
|
||||
*/
|
||||
@WrapMethod(method = "runAllUpdates")
|
||||
private boolean modzuozhi_lockRunAllUpdates(ChunkMap chunkMap, Operation<Boolean> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
return original.call(chunkMap);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code purgeStaleTickets} 无锁遍历 + 修改 {@code tickets},与主线程 ticket 写并发时需要串行化。 */
|
||||
@WrapMethod(method = "purgeStaleTickets")
|
||||
private void modzuozhi_lockPurgeStaleTickets(Operation<Void> 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<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call(pos, player);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "removePlayer")
|
||||
private void modzuozhi_lockRemovePlayer(SectionPos pos, ServerPlayer player, Operation<Void> original) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
original.call(pos, player);
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 式跨维度传送调度。
|
||||
* <p>
|
||||
* 各维度 tick 并行运行在不同 worker 上,实体容器 {@code Level.entityStorage} 非线程安全。
|
||||
* 若某维度 worker 直接调用 {@code changeDimension} 迁移实体到另一维度,会与其他 worker
|
||||
* 并发修改实体容器/区块生命周期而崩溃或死锁。因此当调用线程是维度 worker 时,把真正的
|
||||
* 迁移登记到 {@link DimensionChangeQueue},由主线程在维度 tick 收口后、
|
||||
* {@code playerList.tick()} 之前统一执行(见 {@code MinecraftServerDimMixin})。
|
||||
* <p>
|
||||
* 与 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<Entity> cir) {
|
||||
if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) {
|
||||
return;
|
||||
}
|
||||
if (DimThreadCore.owns(Thread.currentThread())) {
|
||||
DimensionChangeQueue.schedule((Entity) (Object) this, transition);
|
||||
cir.setReturnValue(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 模式,独立实现)。
|
||||
* <p>
|
||||
* 两个注入点(对齐 mob-collision-off 模组的 EntityCollideMixin + EntityMixin):
|
||||
* <ul>
|
||||
* <li>{@code @Redirect} {@code Entity.collide(Vec3)} 里的 {@code getEntityCollisions(...)}:
|
||||
* 实体<b>移动</b>时不查询/计算附近实体的碰撞形状(消除 O(n²) 热点)。</li>
|
||||
* <li>{@code @Inject} {@code Entity.push(Entity)} HEAD 取消:实体<b>重叠时互相推开</b>的
|
||||
* 独立推动机制也一并关闭(否则即使移动碰撞为空,重叠实体仍会被 push 挤开,表现为
|
||||
* "碰撞箱还在")。攻击击退走 {@code push(double,double,double)} 重载,不受影响。</li>
|
||||
* </ul>
|
||||
* 方块碰撞不受影响(走另一条路径 {@code BlockCollisions})。开关:{@code /dimensionalripper nocollide on|off},
|
||||
* 遵守全局开关:{@code /dimensionalripper off} 一并退化为原版。
|
||||
* <p>
|
||||
* <b>第14步(对齐 Async)</b>:第13步的"碰撞降频"(每 2 tick 评估一次碰撞)已回退——移除全局写锁、
|
||||
* 并发化实体存储后,碰撞开启也能并行跑,不再需要降频拐杖。本特性保留为<b>可选开关</b>(默认关闭
|
||||
* 碰撞以获取最大 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<VoxelShape> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}(可见实体表)并发化。
|
||||
* <p>
|
||||
* {@code EntityLookup} 是 {@code PersistentEntitySectionManager.visibleEntityStorage}:
|
||||
* <ul>
|
||||
* <li>{@code byId}({@code Int2ObjectLinkedOpenHashMap})→ {@link Int2ObjectConcurrentHashMap}
|
||||
* (内部 {@code ConcurrentHashMap},弱一致迭代);</li>
|
||||
* <li>{@code byUuid}(普通 {@code HashMap})→ {@link ConcurrentHashMap}。</li>
|
||||
* </ul>
|
||||
* 实体并行 tick 下,{@code startTracking/stopTracking}(实体可见性变更,经 onMove/updateStatus/
|
||||
* addEntityWithoutEvent 触发)会从<b>多个并行子任务线程</b>并发读写该表。此前由全局写锁
|
||||
* {@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<T extends EntityAccess> {
|
||||
|
||||
@Shadow
|
||||
private final Int2ObjectMap<T> byId = new Int2ObjectConcurrentHashMap<>();
|
||||
|
||||
@Shadow
|
||||
private final Map<UUID, T> byUuid = new ConcurrentHashMap<>();
|
||||
}
|
||||
@@ -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} 共享状态并发化。
|
||||
* <p>
|
||||
* 第4步起,实体生命周期写入({@code addEntityWithoutEvent}/{@code onMove}/{@code onRemove})由全局写锁
|
||||
* {@code SECTION_LOCK} 串行化;第14步移除该全局写锁,改由并发集合承载,使实体移动/生成可真正并行:
|
||||
* <ul>
|
||||
* <li>{@code knownUuids}(普通 {@code HashSet})→ 并发 {@code Set}({@code ConcurrentHashMap} 视图),
|
||||
* 保护 {@code addEntityUuid}/{@code isLoaded}/{@code onRemove} 的并发读写;</li>
|
||||
* <li>{@code chunkVisibility}({@code Long2ObjectOpenHashMap})→ {@link ConcurrentLong2ObjectMap},
|
||||
* 保护 {@code createSection}/{@code updateStatus}/{@code canPositionTick} 对区块可见性的并发读
|
||||
* (写主要在服务器线程 {@code updateChunkStatus})。</li>
|
||||
* </ul>
|
||||
* 用 {@code @Shadow} 初始值替换模式(与 {@code PoiSectionMixin}/{@code SectionStorageMixin} 相同,无需 AT)。
|
||||
*/
|
||||
@Mixin(PersistentEntitySectionManager.class)
|
||||
public abstract class EntitySectionManagerMixin<T extends EntityAccess> {
|
||||
|
||||
/** 已注册实体 UUID 集合:并发的 {@code Set}(替换原 {@code HashSet},读写线程安全)。 */
|
||||
@Shadow
|
||||
final Set<UUID> knownUuids = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/** 区块可见性表:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读线程安全)。 */
|
||||
@Shadow
|
||||
private final Long2ObjectMap<Visibility> chunkVisibility = new ConcurrentLong2ObjectMap<>();
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* {@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 导致实体卡进方块/遁地/窒息。
|
||||
* <p>
|
||||
* <b>第5步细粒度化:</b>将第4步的全局锁 {@link EntityTickParallel#SECTION_LOCK} 降级为<b>分片锁</b>。
|
||||
* 本类注入 {@link #modzuozhi_sectionKey}(由 {@code EntitySectionStorageMixin} 在 createSection 时写入),
|
||||
* 对 add/remove/getEntities 用该 key 定位分片锁——不同区域的实体 section 互不阻塞,真正并行。
|
||||
* 结构级操作(storage 遍历/创建/删除、manager、callback)仍持全局锁,锁序固定为
|
||||
* <b>全局锁 → 分片锁</b>,无反向路径,不会死锁。
|
||||
* 用 {@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<Void> 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<Boolean> 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<EntityAccess> consumer,
|
||||
Operation<AbortableIterationConsumer.Continuation> 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<EntityAccess, ?> typeTest, AABB aabb,
|
||||
AbortableIterationConsumer<?> consumer,
|
||||
Operation<AbortableIterationConsumer.Continuation> original
|
||||
) {
|
||||
EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey);
|
||||
try {
|
||||
return original.call(typeTest, aabb, consumer);
|
||||
} finally {
|
||||
EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code getEntities()} 返回的 {@code storage.stream()} 是<b>惰性</b>流,锁无法覆盖其消费阶段。
|
||||
* 消费方({@code PersistentEntitySectionManager.updateChunkStatus/storeChunkSections})可能在子任务
|
||||
* 并发增删时迭代底层 {@code ArrayList} 而抛 CME。这里在锁内<b>收集成快照</b>再返回,消费时必然安全。
|
||||
*/
|
||||
@WrapMethod(method = "getEntities()Ljava/util/stream/Stream;")
|
||||
private Stream<EntityAccess> modzuozhi_lockedGetEntitiesStream(Operation<Stream<EntityAccess>> original) {
|
||||
EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey);
|
||||
try {
|
||||
return original.call().collect(Collectors.toList()).stream();
|
||||
} finally {
|
||||
EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* <b>第11步(SECTION_LOCK → 并发集合)</b>:把 {@code EntitySectionStorage.sections}
|
||||
* ({@code Long2ObjectOpenHashMap})与 {@code sectionIds}({@code LongAVLTreeSet})替换为
|
||||
* {@link ConcurrentLong2ObjectMap}/{@link ConcurrentLongSortedSet}(弱一致、无 CME)。
|
||||
* 由此<b>删除结构级读路径的全部全局读锁</b>:
|
||||
* <ul>
|
||||
* <li>读:{@code forEachAccessibleNonEmptySection} 的 {@code sectionIds.subSet().iterator()} +
|
||||
* {@code sections.get} 现在无锁安全(原版源码对 {@code sections.get} 已有 null 检查,
|
||||
* 并发创建 section 时读到 null 天然跳过);</li>
|
||||
* <li>写:{@code getOrCreateSection → computeIfAbsent} 原子、{@code createSection} 内部
|
||||
* {@code sectionIds.add} 与 {@code sections.put} 由 {@code ConcurrentHashMap.computeIfAbsent}
|
||||
* 串行化,不会重复建 section。</li>
|
||||
* </ul>
|
||||
* 结构<b>写</b>(实体生命周期 {@code onMove/onRemove/addEntityWithoutEvent})仍由
|
||||
* {@link dev.modzuozhi.core.dimthread.EntityTickParallel#SECTION_LOCK} 全局写锁保护(本步不触碰),
|
||||
* 因此实体移动与 section 内容(分片锁)的既有锁序不变。
|
||||
* <p>
|
||||
* 用 {@link WrapMethod} 整方法包装,避免 {@code @WrapOperation at HEAD} 对含分支的方法注入失败。
|
||||
* <p>
|
||||
* <b>第5步细粒度化:</b>在 {@code createSection} 中把 section key 写入新创建的
|
||||
* {@code EntitySection}(见 {@link ISectionKeyAccessor}),供 {@code EntitySection} 的分片锁定位锁片。
|
||||
*/
|
||||
@Mixin(EntitySectionStorage.class)
|
||||
public abstract class EntitySectionStorageMixin<T extends EntityAccess> {
|
||||
|
||||
/** 结构存储:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读路径无锁安全)。 */
|
||||
@Shadow
|
||||
private final Long2ObjectMap<EntitySection<T>> 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<EntitySection> original) {
|
||||
EntitySection section = original.call(sectionKey);
|
||||
((ISectionKeyAccessor) (Object) section).modzuozhi_setSectionKey(sectionKey);
|
||||
return section;
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* {@code EntityTickList} 的 {@code active} 字段在 {@code forEach} 期间被迭代。
|
||||
* 并行子任务中实体可能被移除/生成,触发 {@code EntityCallbacks.onTickingEnd/onTickingStart}
|
||||
* 进而调用 {@code entityTickList.add/remove}。这些写入发生在子任务线程(非迭代线程),
|
||||
* 直接修改 {@code active} 会破坏并发迭代。因此拦截 add/remove,在子任务上下文中投递到
|
||||
* {@link PostExecuteQueue},由维度 worker 在所有实体子任务完成后统一执行。
|
||||
* <p>
|
||||
* 非子任务上下文(串行/维度 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 玩家始终走<b>串行</b>:{@code EntityTickFilter.isParallel} 对玩家恒为 false,因此玩家
|
||||
* 在维度 worker 的实体循环里按原版顺序 tick,绝不进入并行批。
|
||||
* <p>
|
||||
* <b>注意</b>:不能再在 worker 上跳过玩家的实体 tick。1.21.1 的
|
||||
* {@code PlayerList.tick()} 并不调用 {@code ServerPlayer.tick()}(其只做延迟统计广播),
|
||||
* 玩家实体 tick 唯一路径就是 {@code ServerLevel.tick} 的实体循环(维度 worker 执行)。
|
||||
* 若在这里跳过玩家,{@code ServerPlayer.tick()}(含 {@code AbstractContainerMenu
|
||||
* .broadcastChanges()}、传送门、状态推进)将<b>从不执行</b> → 熔炉/漏斗等容器 UI 进度条与
|
||||
* 物品数量永不刷新(实测),玩家相关游戏逻辑全部停滞。
|
||||
*/
|
||||
@Mixin(Level.class)
|
||||
public abstract class EntityTickParallelMixin {
|
||||
|
||||
@Inject(method = "guardEntityTick", at = @At("HEAD"), cancellable = true)
|
||||
private void modzuozhi_guardEntityTick(Consumer<Entity> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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。
|
||||
* <p>
|
||||
* 背景:细粒度并行子任务线程(或维度 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")。
|
||||
* <p>
|
||||
* 修复:包装该调用,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<GameEventListenerRegistry> original) {
|
||||
GameEventListenerRegistry registry = original.call(chunk, sectionY);
|
||||
return registry != null ? registry : GameEventListenerRegistry.NOOP;
|
||||
}
|
||||
}
|
||||
@@ -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} 添加维度检查。
|
||||
* <p>
|
||||
* 原版 {@code checkExtraStartConditions}(1.21.1)只检查 {@code Activity.WORK},不做维度检查。
|
||||
* 当村民的 {@code POTENTIAL_JOB_SITE} 存了另一个维度的 {@code GlobalPos} 时,村民会启动行为
|
||||
* 并尝试在当前维度坐标下寻路到该位置(坐标语义错乱)。并行环境下(维度并行/细粒度并行)也需
|
||||
* 确保村民只寻路当前维度的 POI。
|
||||
* <p>
|
||||
* 在 {@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<Boolean> cir
|
||||
) {
|
||||
if (villager.getBrain().getMemory(MemoryModuleType.POTENTIAL_JOB_SITE)
|
||||
.map(gp -> gp.dimension() != villager.level().dimension())
|
||||
.orElse(false)) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立实现)。
|
||||
* <p>
|
||||
* 并行 tick 下,村民之间交换流言({@code transferFrom})或读取声望时会跨线程读写彼此的
|
||||
* {@code GossipContainer.gossips}(如判断是否信任某个玩家、村民间声望值)。原版用普通
|
||||
* HashMap,跨线程读写存在数据竞争。替换为 {@link ConcurrentHashMap},读写均线程安全。
|
||||
*/
|
||||
@Mixin(GossipContainer.class)
|
||||
public abstract class GossipContainerMixin {
|
||||
|
||||
@Shadow
|
||||
private final Map<UUID, ?> gossips = new ConcurrentHashMap<>();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 区块游戏事件监听器注册表并发访问加固。
|
||||
* <p>
|
||||
* 细粒度并行时,实体 tick 触发 {@code gameEvent → GameEventDispatcher.post →
|
||||
* getListenerRegistry},子任务线程与维度 worker/服务器线程会<b>并发</b>访问
|
||||
* {@code LevelChunk.gameEventListenerRegistrySections}(fastutil
|
||||
* {@code Int2ObjectOpenHashMap},非线程安全)。并发 {@code computeIfAbsent} 会在 rehash 时
|
||||
* 损坏哈希表(实测抛 {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),
|
||||
* 导致服务器崩溃。
|
||||
* <p>
|
||||
* 修复:把 {@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<GameEventListenerRegistry> original) {
|
||||
synchronized (this) {
|
||||
return original.call(sectionY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,独立实现)。
|
||||
* <p>
|
||||
* 爆炸会<b>批量写入区块方块状态</b>({@code Explosion.explode} → {@code Level.setBlock} 写
|
||||
* 非线程安全的 {@code PalettedContainer})。并行环境下,若两个爆炸并发执行(或多个爆炸实体/
|
||||
* TNT 方块/指令/下界床等不同触发源同时引爆),会并发写同一/相邻区块 → 方块状态撕裂损坏
|
||||
* (放置方块消失、被破坏方块"复活")。爆炸实体已由 {@code EntityTickFilter} 黑名单强制串行,
|
||||
* 但爆炸还有 TNT 方块、下界床/重生锚、指令等其它触发源,需在<b>方法入口</b>兜底。
|
||||
* <p>
|
||||
* 原理:{@code Level} 上所有 {@code explode(...)} 重载与 {@code ServerLevel} 的覆写最终都
|
||||
* <b>委托到 13 参重载</b>(唯一真正创建 {@code Explosion} 并执行 {@code explode()}+
|
||||
* {@code finalizeExplosion()} 的方法)。因此只需给该终端重载加一把<b>静态全局锁</b>,所有
|
||||
* 触发源的爆炸便全部互斥串行。爆炸低频、锁竞争可忽略;该方法不递归调用
|
||||
* {@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<SoundEvent> explosionSound, Operation<Explosion> original) {
|
||||
synchronized (MODZUOZHI_EXPLODE_LOCK) {
|
||||
return original.call(source, damageSource, damageCalculator, x, y, z, radius, fire, interaction,
|
||||
spawnParticles, smallExplosionParticles, largeExplosionParticles, explosionSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)} 对「区块未就绪」场景健壮。
|
||||
* <p>
|
||||
* 原版 {@code Level.getChunk(int, int)} 直接 {@code (LevelChunk) this.getChunk(x, z, FULL)}。
|
||||
* 服务端主线程下,若目标区块尚未 FULL,会先同步完成生成再返回真实 {@code LevelChunk},强转不会失败。
|
||||
* <p>
|
||||
* 但细粒度并行 / 维度并行下,子任务线程(伪装成主线程走 {@code ServerChunkCache} 主线程路径)请求
|
||||
* 一个<b>正被主线程生成</b>的相邻区块时,返回的是 {@code ImposterProtoChunk}(世界生成占位块,
|
||||
* {@code extends ProtoChunk},<b>并非</b> {@code LevelChunk} 子类),强转失败抛
|
||||
* {@code ClassCastException}(例如草/泥土蔓延 {@code SpreadingSnowyDirtBlock.randomTick} 通过
|
||||
* {@code getBlockState} 访问相邻未就绪区块时)。
|
||||
* <p>
|
||||
* 这里在强转前,若结果是 {@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;
|
||||
}
|
||||
}
|
||||
@@ -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} 线程守卫伪装。
|
||||
* <p>
|
||||
* 原版 {@code Level.getBlockEntity} 有线程守卫:服务端且当前线程 != 世界构造时捕获的
|
||||
* {@code thread} 时返回 null。维度并行下,整个 {@code ServerLevel.tick} 运行在池中 worker 上,
|
||||
* 该守卫会把 worker 的一切 {@code getBlockEntity} 返回 null,导致漏斗/箱子合并/比较器等
|
||||
* 依赖 {@code getBlockEntity} 的逻辑失效(漏斗 NPE 等)。
|
||||
* <p>
|
||||
* 在 {@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})初始化为线程安全随机源。
|
||||
* <p>
|
||||
* 原版 {@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 = "<init>",
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 时序背景:{@code ServerLevel.tick} 中 {@code blockTicks.tick / fluidTicks.tick}(收集并执行
|
||||
* 计划内 tick)与 {@code runBlockEvents} 都运行在<b>维度 worker 线程</b>(串行阶段),
|
||||
* 不在细粒度并行窗口内,天然线程安全。
|
||||
* <p>
|
||||
* 风险点:<b>并行子任务</b>(方块实体 tick、区块随机 tick)里某些方块/方块实体会调用
|
||||
* {@code level.scheduleTick(...)} → {@code LevelTicks.schedule},多个子任务线程<b>并发写</b>
|
||||
* {@code LevelTicks} 内部的 {@code Long2ObjectOpenHashMap}/{@code PriorityQueue}(非线程安全),
|
||||
* 会损坏调度结构,导致计划内 tick 丢失、方块行为异常。
|
||||
* <p>
|
||||
* 修复:在并行子任务上下文({@link FineGrainScheduler#inSubTask()})中,把 {@code schedule}
|
||||
* 投递到 {@link PostExecuteQueue},由维度 worker 在本维度 tick 收口处(所有并行子任务已
|
||||
* {@code await} 完成、无并发)串行执行。由于收口发生在 {@code blockTicks.tick} 之后,新增
|
||||
* 调度会排到<b>下个 tick</b>执行,与原版"当前 tick 之后"语义一致。关闭并行时零开销(串行)。
|
||||
* <p>
|
||||
* 说明:读方法({@code hasScheduledTick}/{@code willTickThisTick})在子任务中几乎不被调用,
|
||||
* 且读多写少、正确性可容忍偶发旧值,不做延迟(保持原样)。
|
||||
*/
|
||||
@Mixin(LevelTicks.class)
|
||||
public abstract class LevelTicksMixin<T> {
|
||||
|
||||
@WrapMethod(method = "schedule")
|
||||
private void modzuozhi_deferSchedule(ScheduledTick<T> tick, Operation<Void> original) {
|
||||
PostExecuteQueue queue = FineGrainScheduler.currentQueue();
|
||||
if (queue == null) {
|
||||
// 非并行子任务上下文:原版直接调度
|
||||
original.call(tick);
|
||||
return;
|
||||
}
|
||||
// 并行子任务:延迟到维度 worker 收口串行调度,避免并发写 LevelTicks 内部结构
|
||||
queue.post(() -> original.call(tick));
|
||||
}
|
||||
}
|
||||
@@ -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()} 空值兜底。
|
||||
* <p>
|
||||
* 罕见 NPE 根因:并行子任务线程上,{@code getInBlockState()} 读取实体所在位置方块状态时
|
||||
* 偶发返回 {@code null}(区块读路径的边缘竞态),随后该值被传给
|
||||
* {@code CommonHooks.isLivingOnLadder(state, ...)} 并直接 {@code state.isLadder(...)} → NPE
|
||||
* (日志:[FineGrain] 子任务异常 ... because "state" is null,50 分钟约 1 次,被屏障捕获不降级)。
|
||||
* <p>
|
||||
* 注意:{@code getInBlockState()} 定义在 {@link Entity}(父类)而非 {@code LivingEntity},
|
||||
* 因此 {@code @WrapMethod} 必须目标 {@code Entity.class}(mixin 只包装目标类<b>声明</b>的方法)。
|
||||
* 这里对返回值做空值兜底(null → 空气方块),从根源上杜绝该路径的 NPE;正常情况零改动。
|
||||
*/
|
||||
@Mixin(Entity.class)
|
||||
public abstract class LivingEntityGetInBlockStateMixin {
|
||||
@WrapMethod(method = "getInBlockState")
|
||||
private BlockState modzuozhi_nullSafeInBlockState(Operation<BlockState> original) {
|
||||
BlockState state = original.call();
|
||||
return state == null ? Blocks.AIR.defaultBlockState() : state;
|
||||
}
|
||||
}
|
||||
@@ -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 思路)。
|
||||
* <p>
|
||||
* <b>目标</b>:每个 {@link ServerLevel} 一个常驻 {@code LevelTickLoop},各自按 tick 预算
|
||||
* 独立排下一次 tick。某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再由主线程
|
||||
* 统一 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。
|
||||
* <p>
|
||||
* 主线程 {@code tickChildren} 退化为「协调者」:命令函数、基础连接(登录/状态/配置)、
|
||||
* {@code playerList.tick()}、tickables 仍留在主线程;每个维度的「level.tick + 玩家连接 tick
|
||||
* + 区块发送/刷新」由该维度 loop 在 worker 线程独立完成(见 {@code ServerConnectionListenerMixin}
|
||||
* 跳过游戏连接、{@code LevelTickLoop.tickConnections} 承接游戏连接 tick,杜绝玩家双 tick)。
|
||||
* <p>
|
||||
* 全局关闭或未激活时,原方法体照常执行(退回原版串行,零副作用)。
|
||||
*/
|
||||
@Mixin(value = MinecraftServer.class, priority = 1010)
|
||||
public abstract class MinecraftServerDimMixin {
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
private List<Runnable> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 式)。
|
||||
* <p>
|
||||
* 原版 {@code MinecraftServer.saveAllChunks} 在 for 循环里<strong>串行</strong>遍历所有维度,
|
||||
* 逐个调用 {@code serverlevel.save(...)} 写区块/实体。当维度多且区块实体量大时,保存耗时线性叠加,
|
||||
* 主线程被同步写盘阻塞数秒,表现为 "Can't keep up"。
|
||||
* <p>
|
||||
* 本 mixin 用 {@code @Inject HEAD + cancellable} 完整接管 {@code saveAllChunks}:
|
||||
* <ol>
|
||||
* <li>把每个维度的 {@code serverlevel.save(...)} 分发到线程池<strong>并行执行</strong>(各维度目录独立,写盘互不干扰,安全);</li>
|
||||
* <li>{@code awaitCompletion()} 等待全部维度写完;</li>
|
||||
* <li>主线程串行补齐原方法下半段的<strong>全局</strong>保存(worldData / boss events / level.dat 标签),
|
||||
* 这些共享状态不能并行。</li>
|
||||
* </ol>
|
||||
* 维度越多、区块实体越多,加速越明显。
|
||||
*/
|
||||
@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<Boolean> cir) {
|
||||
MinecraftServer self = (MinecraftServer) (Object) this;
|
||||
if (!DimThreadCore.MANAGER.isActive(self)) {
|
||||
return; // 未开启:走原版串行路径
|
||||
}
|
||||
|
||||
// 保存前先排空细粒度子任务,避免残留子任务与实体/区块保存并发访问(残留任务此刻
|
||||
// 若卡死,Barrier 超时降级已在维度 loop 侧触发,这里再给一个短等待兜底)。
|
||||
FineGrainScheduler.awaitIdle(2000L);
|
||||
|
||||
// 1. 并行保存每个维度的区块/实体(独立写盘)
|
||||
List<ServerLevel> levels = new ArrayList<>();
|
||||
self.getAllLevels().forEach(levels::add);
|
||||
|
||||
ThreadPool pool = DimThreadCore.getThreadPool(self);
|
||||
AtomicReference<Throwable> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* {@code navigatingMobs} 记录当前正在寻路的 {@link Mob},原为 {@code ObjectOpenHashSet}(非线程安全):
|
||||
* <ul>
|
||||
* <li>写入:{@code onTrackingStart/onTrackingEnd}(实体追踪变化时 {@code add}/{@code remove}),
|
||||
* 可能来自<b>并行实体子任务线程</b>(实体 tick 触发追踪变化)、维度 worker、区块加载线程;</li>
|
||||
* <li>遍历:{@code sendBlockUpdated}(方块变化时)遍历所有 mob 的 navigation 判断是否重算路径,
|
||||
* 同样可能被任意线程触发。</li>
|
||||
* </ul>
|
||||
* 两者并发会抛 {@code ConcurrentModificationException} 或破坏集合,导致寻路异常/崩溃。
|
||||
* <p>
|
||||
* 修复:替换为 {@link ConcurrentHashMap#newKeySet()} 背书的并发集合(线程安全、迭代弱一致、
|
||||
* 不抛 CME)。原版串行行为不变,零语义影响。
|
||||
*/
|
||||
@Mixin(ServerLevel.class)
|
||||
public abstract class NavigatingMobsMixin {
|
||||
@Shadow
|
||||
@Final
|
||||
@Mutable
|
||||
private Set<Mob> navigatingMobs = ConcurrentHashMap.newKeySet();
|
||||
}
|
||||
@@ -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 异常。
|
||||
* <p>
|
||||
* 原版 {@code doTick} 里 {@code list.sort(Comparator.comparingDouble(entity::distanceToSqr))}
|
||||
* 的比较器会<b>实时</b>读取被排序实体的可变坐标。并行 tick 下,其它子任务线程正同时移动这些
|
||||
* 实体,导致比较器前后不一致(破坏传递性)→ {@code TimSort} 抛
|
||||
* {@code IllegalArgumentException: Comparison method violates its general contract}(日志已证实)。
|
||||
* <p>
|
||||
* 修复:排序前先对每个实体快照一次到锚点(被 tick 实体)的距离,按<b>快照距离</b>稳定排序并
|
||||
* 回写——比较器全程一致,既保留"按距离排序"语义,又不触发 TimSort 异常。
|
||||
* <p>
|
||||
* 线程安全:每个 {@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<LivingEntity> list, Comparator<LivingEntity> 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<LivingEntity> sorted = new ArrayList<>(n);
|
||||
for (int index : order) {
|
||||
sorted.add(list.get(index));
|
||||
}
|
||||
list.clear();
|
||||
list.addAll(sorted);
|
||||
this.modzuozhi_anchor = null;
|
||||
}
|
||||
}
|
||||
@@ -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 适配版)。
|
||||
* <p>
|
||||
* 1.21.1 的 {@code PacketUtils.ensureRunningOnSameThread} 有 3 参数重载:
|
||||
* {@code (Packet, T, ServerLevel)} 和 {@code (Packet, T, BlockableEventLoop)}。
|
||||
* <b>真实包链路</b>(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 逻辑。
|
||||
* <p>
|
||||
* 借鉴 Chlorophyll(Unlicense 1.21.10 Fabric)的 {@code PacketUtilsMixin} 思路,但适配
|
||||
* 1.21.1 NeoForge 签名:全局开启且是游戏包时,把 handle 任务投递到<b>该玩家所在维度</b>的
|
||||
* {@code PlayerPacketQueue}(替代投主线程队列),由维度 worker 在 tick 收口处执行。
|
||||
* 登录/配置/状态包仍走原版 {@code BlockableEventLoop} 重载。
|
||||
* <p>
|
||||
* <b>防无限循环(必须)</b>:包投到维度队列后,worker drain 执行 {@code packet.handle},
|
||||
* handle 开头会再次调本方法。因此用 {@link PlayerPacketQueue#isExecuting()} 标记(drain 中
|
||||
* 设置)区分两种状态:
|
||||
* <ul>
|
||||
* <li>Netty 线程首次调度(未在展开中、非主线程)→ 投玩家维度队列 + 抛异常;</li>
|
||||
* <li>维度 worker / 主线程 drain 中执行 handle({@code isExecuting()} 或主线程)→
|
||||
* <b>直接返回</b>,让 handle 继续执行,避免包被无限投回队列。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 死锁评估:{@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 <T extends PacketListener> ReportedException makeReportedException(Exception exception, Packet<T> packet, T packetListener) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @reason 把游戏包处理按玩家分派到其所在维度线程(第2步)
|
||||
*/
|
||||
@Overwrite
|
||||
public static <T extends PacketListener> void ensureRunningOnSameThread(
|
||||
Packet<T> 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());
|
||||
}
|
||||
}
|
||||
@@ -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步:自动保存分布化——玩家保存按维度分发到各维度线程并行执行。
|
||||
* <p>
|
||||
* 原版 {@code PlayerList.saveAll()} 在<strong>主线程串行</strong>遍历所有玩家,逐个
|
||||
* {@code playerIo.save(player)} 写玩家数据(+ 统计 + 进度)。玩家多时主线程被同步写盘
|
||||
* 阻塞数秒。玩家数据按 UUID 写独立文件,维度间、玩家间均无共享写盘目标,可安全并行。
|
||||
* <p>
|
||||
* 本 mixin 用 {@code @Inject HEAD + cancellable} 接管 {@code saveAll}(维度并行激活时):
|
||||
* <ol>
|
||||
* <li>把在线玩家按<b>所在维度</b>分组;</li>
|
||||
* <li>每个维度在对应维度 worker 上<b>串行保存</b>该维度玩家(维度内保序、维度间并行);</li>
|
||||
* <li>{@code awaitCompletion()} 等待全部维度写完。</li>
|
||||
* </ol>
|
||||
* 与 {@code MinecraftServerSaveMixin}(维度区块/实体保存已并行化)配合,自动保存的
|
||||
* savePlayers + saveLevel 都落在各维度线程上,主线程不再亲自执行保存逻辑。
|
||||
* <p>
|
||||
* 关服时 {@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<ServerLevel, List<ServerPlayer>> 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<Throwable> 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();
|
||||
}
|
||||
}
|
||||
@@ -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} 添加维度检查。
|
||||
* <p>
|
||||
* 原版 {@code PoiCompetitorScan.create()} 不做维度检查,当村民的 {@code JOB_SITE} 存了
|
||||
* 另一个维度的 {@code GlobalPos} 时,会跨维度访问 POI 管理器。并行环境下(维度线程/子任务线程)
|
||||
* 跨维度访问虽已由 POI 数据结构加固保障安全,但 AI 逻辑本身不应跨维度竞争工作站。
|
||||
* <p>
|
||||
* 借鉴 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<PoiType> holder, Villager villager) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Shadow
|
||||
private static Villager selectWinner(Villager villager, Villager villager2) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @reason 添加维度守卫,防止跨维度竞争工作站
|
||||
*/
|
||||
@Overwrite
|
||||
public static BehaviorControl<Villager> 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;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 并发访问加固)。
|
||||
* <p>
|
||||
* {@code PoiManager.DistanceTracker}(继承 {@code SectionTracker → DynamicGraphMinFixedPoint →
|
||||
* LeveledPriorityQueue})内部用普通 {@code LongLinkedOpenHashSet} 队列。细粒度并行下:
|
||||
* <ul>
|
||||
* <li>服务器线程每 tick 调用 {@code PoiManager.tick → distanceTracker.runAllUpdates()};</li>
|
||||
* <li>子任务线程的村民传感器调用 {@code PoiManager.sectionsToVillage → distanceTracker.runAllUpdates()}。</li>
|
||||
* </ul>
|
||||
* 两路并发改写同一队列,fastutil 哈希表 rehash 时损坏(实测抛
|
||||
* {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),导致崩溃。
|
||||
* <p>
|
||||
* 修复:把 {@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<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 并发访问加固)。
|
||||
* <p>
|
||||
* 并行 tick 下,村民传感器(找床/找工作点)在子任务线程遍历 {@code PoiSection} 的
|
||||
* {@code records}/{@code byType},而方块变化会在 worker 线程触发 {@code add/remove} 修改同一
|
||||
* 结构。普通 HashMap/fastutil 哈希表并发读写存在数据竞争(迭代时被修改 → CME 或撕裂读)。
|
||||
* <p>
|
||||
* 修复:{@code records} 换成 {@link Short2ObjectMaps#synchronize 同步包装},
|
||||
* {@code byType} 换成 {@link ConcurrentHashMap}(弱一致迭代安全)。
|
||||
*/
|
||||
@Mixin(PoiSection.class)
|
||||
public abstract class PoiSectionMixin {
|
||||
|
||||
@Shadow
|
||||
private final Short2ObjectMap<PoiRecord> records =
|
||||
Short2ObjectMaps.synchronize(new Short2ObjectOpenHashMap<>());
|
||||
|
||||
@Shadow
|
||||
private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newConcurrentMap();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 通用传送门延后修复(覆盖原版下界门与模组传送门,如暮色森林)。
|
||||
* <p>
|
||||
* 根因:{@code Entity.handlePortal → PortalProcessor.getPortalDestination →
|
||||
* portal.getPortalDestination} 在<strong>维度 worker 线程</strong>(实体 tick 上下文)
|
||||
* 执行,会对<strong>目标维度</strong>区块做大量读写。目标区块尚未 FULL 加载时:
|
||||
* <ul>
|
||||
* <li>下界门({@link NetherPortalBlock} → {@code PortalForcer}):getBlockState 等
|
||||
* required=false 读不到真实地形 → createPortal 失败 → transition 为 null → 传送失效;</li>
|
||||
* <li>模组门(如 {@code twilightforest.block.TFPortalBlock} → {@code TFTeleporter}):
|
||||
* 直接 {@code ServerLevel.getChunk(x,z)}(required=true)→ worker 在同步路径
|
||||
* {@code mainThreadProcessor.managedBlock} 上忙等目标区块生成,暮色区块生成慢,
|
||||
* 表现为卡约一分钟;且首次生成中数据不完整,传送门位置算错 → 被传到虚空。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 修复:在统一入口 {@code PortalProcessor.getPortalDestination} 上,若当前是维度 worker /
|
||||
* 细粒度子任务线程,且目标维度对应区块尚未 FULL 加载,则先投递 PORTAL ticket 触发目标
|
||||
* 维度异步生成,清传送冷却(否则 handlePortal 已设置的 300tick 冷却会阻断重试),并返回
|
||||
* null 把传送<strong>延后一 tick</strong>。玩家持续站在传送门中,portalTime 保持满值,
|
||||
* 下一 tick 自动重试——目标区块 FULL 后走原版逻辑:getChunk 命中完整缓存(不阻塞、位置
|
||||
* 正确),正常创建传送门并传送。全程不阻塞、不忙等、不刷日志,杜绝死锁与虚空。
|
||||
* <p>
|
||||
* 目标维度通过 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<Level> 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<Boolean> 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)。
|
||||
* <p>
|
||||
* 放行(目标区块已 FULL)后,暮色 TFTeleporter 在玩家路径(默认进度锁定
|
||||
* {@code ENFORCED_PROGRESSION_RULE=true})会走 {@code moveToSafeCoords →
|
||||
* scanIntoSafeBiomes} 扫描 128 格,读取预加载范围<strong>之外</strong>的未生成区块 →
|
||||
* {@code getChunk(required=true)} 抛 {@code IllegalStateException: Chunk not there} →
|
||||
* 传播到玩家 tick 被 {@code LevelTickLoop.tickConnections} 捕获跳过 → 每 tick 循环
|
||||
* "放行→抛异常→跳过" → 玩家永久卡在传送门(生物不查进度走安全路径故能传,丢生物后
|
||||
* 目标区域已生成故玩家第二次成功)。
|
||||
* <p>
|
||||
* 这里捕获该异常:投递更大范围 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<DimensionTransition> 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<DimensionTransition> 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<String> modzuozhi_logged = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** 依据 portal 实例推导目标维度;未知传送门返回 null(不干预)。 */
|
||||
@Unique
|
||||
private ServerLevel modzuozhi_guessTargetLevel(ServerLevel level) {
|
||||
MinecraftServer server = level.getServer();
|
||||
ResourceKey<Level> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 并发访问加固)。
|
||||
* <p>
|
||||
* {@code SectionStorage} 是 {@code PoiManager} 的基类,其内部 {@code storage}(
|
||||
* {@code Long2ObjectOpenHashMap})在细粒度并行下会被<b>子任务线程</b>(村民传感器查询 POI →
|
||||
* {@code getOrLoad})与<b>区块加载线程</b>({@code readColumn → storage.put})并发读写。
|
||||
* fastutil 的普通哈希表并发 put 会在 rehash 时损坏(实测抛
|
||||
* {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),
|
||||
* 进而引发 chunk 加载失败、连锁异常。
|
||||
* <p>
|
||||
* 修复:把 {@code storage} 替换为 {@link Long2ObjectMaps#synchronize 同步包装},所有
|
||||
* get/put/remove/values 访问线程安全,从根上消除该数据竞争。
|
||||
*/
|
||||
@Mixin(SectionStorage.class)
|
||||
public abstract class SectionStorageMixin<R> {
|
||||
|
||||
@Shadow
|
||||
private final Long2ObjectMap<Optional<R>> storage =
|
||||
Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
|
||||
}
|
||||
@@ -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<ChunkResult<ChunkAccess>> modzuozhi_getChunkFutureMainThread(
|
||||
int x, int z, ChunkStatus status, boolean required);
|
||||
}
|
||||
@@ -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}" 崩溃。
|
||||
* <p>
|
||||
* 维度线程并行驱动区块生命周期后,玩家移动进入新生成的区块时,会出现一种竞态:
|
||||
* 该区块已生成完毕、{@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}。
|
||||
* <p>
|
||||
* 本 mixin 在 {@code getChunk} 中:当 required 的 future 结果为 fail 时,回退到
|
||||
* {@code getChunkNow}——若该区块的完整 {@link LevelChunk} 仍在内存中,直接返回它(数据
|
||||
* 正确,返回它比抛异常合理得多),彻底消除这一崩溃类;若确实不在内存(真正未加载),
|
||||
* 维持原版抛异常语义。
|
||||
*/
|
||||
@Mixin(ServerChunkCache.class)
|
||||
public abstract class ServerChunkCacheGetChunkFallbackMixin {
|
||||
|
||||
/**
|
||||
* 包并 {@code getChunk} 里的 {@code getChunkFutureMainThread(...)} 调用点,
|
||||
* 把返回的 future 换成"fail 时回退 getChunkNow"的语义。
|
||||
* <p>
|
||||
* {@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<ChunkResult<ChunkAccess>> modzuozhi_getChunkFallback(
|
||||
ServerChunkCache instance, int x, int z, ChunkStatus status, boolean required) {
|
||||
CompletableFuture<ChunkResult<ChunkAccess>> 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<ChunkAccess> 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" → 传送被
|
||||
// 中断。这里只给<strong>被请求的这一块</strong>投 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,绝不抛异常。
|
||||
* <p>
|
||||
* 不能只依赖 {@code cache.getChunkNow}:它要求 {@code Thread.currentThread() == mainThread},
|
||||
* 而本 fallback 的 {@code handle} 回调可能运行在"补全该 future 的 worker 线程"上(区块仍在
|
||||
* 生成/打光,future 未完成),此时 getChunkNow 直接返回 null,即使完整区块已在内存中也会
|
||||
* 误判为未加载 → 仍抛 "Chunk not there"。故这里<strong>线程无关</strong>地直接读
|
||||
* {@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;
|
||||
}
|
||||
}
|
||||
@@ -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} 线程伪装。
|
||||
* <p>
|
||||
* 在 {@code swapThreadsAndRun} 期间,本维度的 {@code mainThread} 已被临时设为当前 worker,
|
||||
* 因此 worker 上 {@code Thread.currentThread() == mainThread} 自然成立,{@code getChunk}
|
||||
* 走同步路径({@code managedBlock} 驱动本维度 chunkMap),不会投递回服务器主线程并
|
||||
* {@code join} 死锁。
|
||||
* <p>
|
||||
* 当某个维度 worker 需要访问<strong>其它维度</strong>的区块(如下界门查找目标传送门)时,
|
||||
* 当前线程(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<Thread> 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<Thread> original) {
|
||||
return modzuozhi_threadDisguise(original);
|
||||
}
|
||||
|
||||
/**
|
||||
* 维度 worker / 细粒度子任务线程伪装成该 cache 的主线程,走同步路径:
|
||||
* <ul>
|
||||
* <li>{@code getChunk}:避免 supplyAsync/join 死锁;</li>
|
||||
* <li>{@code getChunkNow}:避免碰撞检测被线程守卫拦截返回 null(穿墙/遁地根因)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Unique
|
||||
private Thread modzuozhi_threadDisguise(Operation<Thread> original) {
|
||||
Thread thread = original.call();
|
||||
if (DimThreadCore.MANAGER.isActive(this.level.getServer())
|
||||
&& (DimThreadCore.owns(thread) || FineGrainScheduler.inSubTask())) {
|
||||
return this.mainThread;
|
||||
}
|
||||
return thread;
|
||||
}
|
||||
|
||||
// ===== 并行子任务:绕过撕裂的共享 lastChunk 缓存 =====
|
||||
//
|
||||
// 根因:细粒度并行时,多个子任务线程 + 维度 worker 会<strong>并发</strong>读写
|
||||
// ServerChunkCache 的 4 槽 lastChunkPos/lastChunkStatus/lastChunk 缓存(普通数组,非线程安全)。
|
||||
// storeInCache 的移位写入与 getChunk 的读取交错时会产生"撕裂读":getChunk(x,z) 命中了
|
||||
// pos/status 槽但拿到的是<strong>另一个坐标</strong>的区块对象。碰撞检测(BlockCollisions →
|
||||
// ChunkSource.getBlockState)拿着错块去读方块状态,会把脚下的实心地面读成空气 → 村民穿地/遁地/
|
||||
// 卡进方块窒息。诊断(getChunk(FULL,false) 返回"wrong 错块"计数持续增长)已证实。
|
||||
//
|
||||
// 修复:子任务线程上完全绕过共享缓存——getChunk / getChunkNow 直接按坐标从 ChunkHolder
|
||||
// (visibleChunkMap 是 volatile 快照式引用,get 线程安全)读正确区块;storeInCache 对子任务
|
||||
// 线程为 no-op,避免污染共享缓存。主线程 / 维度 worker / 串行场景行为不变(零开销)。
|
||||
|
||||
/**
|
||||
* 子任务线程的 {@code getChunk}:绕过 lastChunk 缓存,直接读正确坐标的 FULL 区块。
|
||||
* <p>
|
||||
* <b>第10步(修复撕裂回退 + 修复死锁)</b>:
|
||||
* <ul>
|
||||
* <li>FULL 命中:走 {@link #modzuozhi_subtaskCacheLookup},绝不用共享 4 槽 lastChunk 缓存
|
||||
* (多线程并发写会撕裂,拿到其它坐标区块 → 碰撞把地面读成空气 → 村民遁地)。</li>
|
||||
* <li>FULL 未命中(区块尚未 FULL,多为加载/生成中):<b>非阻塞</b>占位返回,<b>绝不能等待</b>——
|
||||
* 区块生成管线由维度 worker 的 {@code chunkSource.tick} / 服务器主线程驱动,而这两者此刻
|
||||
* 分别卡在「实体屏障 await」和「维度 awaitCompletion」上;等待生成 future 会形成
|
||||
* 循环等待 → 整服死锁(日志:[FaultGuard] 维度 tick 超时 >30000ms,玩家侧表现为游戏冻结)。
|
||||
* 返回占位块后,区块生成完的下一 tick 命中缓存、碰撞恢复正常,短暂下坠自愈,绝不冻结。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@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<ChunkAccess> 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<Biome> 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> 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<LevelChunk> 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<Void> 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<Long, LevelChunk> modzuozhi_subtaskChunkCache = new ConcurrentHashMap<>();
|
||||
|
||||
/** 缓存条目上限,超过即整表清空。 */
|
||||
@Unique
|
||||
private static final int MODZUOZHI_CACHE_MAX = 8192;
|
||||
|
||||
/**
|
||||
* 子任务线程读取 FULL 区块:优先命中缓存,未命中则从 ChunkHolder 读取并回填缓存。
|
||||
* <p>
|
||||
* <b>正确性关键</b>:本方法不仅被「只读」路径(碰撞检测、AI 传感器)调用,也会被
|
||||
* <b>写入</b>路径(爆炸破坏方块 → {@code Level.setBlock → getChunkAt → getChunk(FULL)})
|
||||
* 调用。因此返回的 {@code LevelChunk} <b>必须是 ChunkHolder 当前活动对象</b>,否则
|
||||
* {@code setBlockState} 会写到已过期(重载前)的对象上,导致方块状态丢失/复活(例如
|
||||
* 爆炸破坏的 TNT 在存档中复活并再次爆炸)。
|
||||
* <p>
|
||||
* 校验策略:命中缓存后,用 {@code holder.getFullChunkFuture().getNow(...)} 解析当前活动
|
||||
* 对象,与缓存做<b>对象身份(==)</b>比较——一致才返回缓存(对象未变,安全);
|
||||
* 不一致说明区块已重载,返回并回填新对象。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<Object[]> 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<Object> consumer, Operation<Void> 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;
|
||||
}
|
||||
}
|
||||
@@ -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。
|
||||
* <p>
|
||||
* 原版里 {@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)。
|
||||
* <p>
|
||||
* 因此:当监控开启且当前线程<b>不是</b>维度 worker(即服务器主线程/其它线程)时跳过 poll,
|
||||
* 本维度的区块源任务统一由 {@code LevelTickLoop.processMainThreadTasks()} 在 worker 上排空。
|
||||
*/
|
||||
@Mixin(ServerChunkCache.class)
|
||||
public abstract class ServerChunkCachePollMixin {
|
||||
|
||||
@WrapMethod(method = "pollTask")
|
||||
private boolean modzuozhi_skipServerThreadChunkPoll(Operation<Boolean> 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* 计数异常)。
|
||||
* <p>
|
||||
* {@code runDistanceManagerUpdates}(内部:{@code DistanceManager.runAllUpdates} →
|
||||
* {@code scheduleGenerationTask} 向 {@code ChunkMap.pendingGenerationTasks}(普通 ArrayList)
|
||||
* add;以及 {@code ChunkMap.runGenerationTasks} 遍历 + clear,并对其中的生成任务 claim/release)
|
||||
* 必须<b>单线程</b>执行。维度 loop 模型下,该维度 worker({@code level.tick → ServerChunkCache
|
||||
* .tick})与保存线程({@code save → runDistanceManagerUpdates})等路径<b>并发</b>调用时:
|
||||
* 同时遍历/修改 {@code pendingGenerationTasks} 抛 {@code ConcurrentModificationException},
|
||||
* 且对同一 {@code GenerationChunkHolder} 重复 claim/release 抛
|
||||
* {@code IllegalStateException: More releases than claims}(实测:下界维度 tick 异常 +
|
||||
* Worker 线程异常)。
|
||||
* <p>
|
||||
* 用 {@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<Boolean> original) {
|
||||
synchronized (this) {
|
||||
return original.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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步:网络包按玩家分派到维度线程——断开流程死锁防护。
|
||||
* <p>
|
||||
* 原版 {@code ServerCommonPacketListenerImpl.disconnect} 用
|
||||
* {@code MinecraftServer.executeBlocking} 把清理任务提交到主线程并<b>阻塞等待</b>。当包处理
|
||||
* 已移到维度线程(worker 在 {@code ServerLevel.tick} 收口执行包)时,主线程正阻塞在
|
||||
* {@code awaitCompletion} 等待维度 worker 完成——worker 再 executeBlocking 等主线程会
|
||||
* <b>互相等待死锁</b>。因此全局开启且为游戏连接时,把断开清理改为<b>异步</b>投递主线程
|
||||
* ({@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<Void> original) {
|
||||
if (ModZuozhi.isEnabled() && ((Object) this) instanceof ServerGamePacketListenerImpl) {
|
||||
// 异步投主线程,worker 不阻塞 → 防与主线程 awaitCompletion 互相等待死锁
|
||||
server.execute(task);
|
||||
} else {
|
||||
original.call(server, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 原版 {@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<Connection> 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<Connection> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(方块事件)队列并发访问加固。
|
||||
* <p>
|
||||
* 方块事件(音符盒/活塞/发射器/铃等延迟动作)经 {@code ServerLevel.blockEvent} 入队、
|
||||
* 每 tick {@code runBlockEvents} 出队执行并广播。原版 {@code blockEvents} 是普通非线程安全
|
||||
* 的 {@code ObjectLinkedOpenHashSet}。细粒度并行下,并行实体子任务一旦触发 {@code blockEvent}
|
||||
* (入队写),会与维度 worker/服务器线程正在 {@code runBlockEvents}(出队删)<b>并发读写</b>
|
||||
* 同一哈希集 → CME 或哈希表损坏(fastutil 并发损坏崩溃)。
|
||||
* <p>
|
||||
* 修复:对访问队列的 3 个方法({@code blockEvent}/{@code runBlockEvents}/{@code clearBlockEvents})
|
||||
* 加 {@code synchronized(this)}:单对象锁、逐调用、无嵌套,无死锁(方块事件低频,开销可忽略);
|
||||
* 同时保留原版 Set 的<b>去重 + 插入序</b>语义,行为零变化。
|
||||
*/
|
||||
@Mixin(ServerLevel.class)
|
||||
public abstract class ServerLevelBlockEventMixin {
|
||||
|
||||
@WrapMethod(method = "blockEvent")
|
||||
private void modzuozhi_blockEvent(BlockPos pos, Block block, int type, int data, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(pos, block, type, data);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "runBlockEvents")
|
||||
private void modzuozhi_runBlockEvents(Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "clearBlockEvents")
|
||||
private void modzuozhi_clearBlockEvents(BoundingBox box, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(box);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})。
|
||||
* <p>
|
||||
* 细粒度并行(fine)下多个区块的随机 tick 由不同子任务线程执行。原版
|
||||
* {@code ServerLevel.tickChunk} 直接读取 {@code Level.random}({@code public final} 共享字段),
|
||||
* 并行时会跨线程冲突;且 NeoForge 开发环境无法对混淆字段做 {@code @Redirect}(无 refmap)。
|
||||
* <p>
|
||||
* 因此本 mixin 在 {@code ServerLevel} 上注入一个<b>等价的自定义实现</b>
|
||||
* {@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));
|
||||
}
|
||||
}
|
||||
@@ -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} 对这些共享状态的串行保护。
|
||||
* <p>
|
||||
* 移除全局写锁后,实体 tick 并行子任务会<b>并发</b>触发这些回调(经
|
||||
* {@code startTracking/stopTracking/onDestroyed/onSectionChange}):
|
||||
* <ul>
|
||||
* <li>{@code onTrackingStart/onTrackingEnd}:写 {@code ServerChunkCache} 实体追踪表、玩家表、
|
||||
* {@code dragonParts}、游戏事件监听器、NeoForge 事件——非线程安全,需串行;</li>
|
||||
* <li>{@code onDestroyed}:写记分板;</li>
|
||||
* <li>{@code onSectionChange}:更新游戏事件监听器位置。</li>
|
||||
* </ul>
|
||||
* 用 {@code synchronized(this)}(同一 {@code EntityCallbacks} 实例,即每 ServerLevel 一把轻量锁)
|
||||
* 把这些<b>快速</b>回调串行化,锁持有时长远小于整段 onMove,实体移动/生成本体仍无锁并行。
|
||||
* {@code onTickingStart/onTickingEnd}(EntityTickList 增删)已在 {@link EntityTickListMixin}
|
||||
* 中按子任务上下文延迟执行,此处一并加同步兜底。
|
||||
* <p>
|
||||
* 服务器线程与维度 worker 的其它区块追踪访问不经过本回调,与移除前一致(本回调原本也未与之互斥),
|
||||
* 不引入新的竞争面。
|
||||
*/
|
||||
@Mixin(targets = "net.minecraft.server.level.ServerLevel$EntityCallbacks")
|
||||
public abstract class ServerLevelEntityCallbacksMixin {
|
||||
|
||||
@WrapMethod(method = "onTrackingStart")
|
||||
private void modzuozhi_syncTrackingStart(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onTrackingEnd")
|
||||
private void modzuozhi_syncTrackingEnd(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onDestroyed")
|
||||
private void modzuozhi_syncDestroyed(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onSectionChange")
|
||||
private void modzuozhi_syncSectionChange(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onTickingStart")
|
||||
private void modzuozhi_syncTickingStart(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onTickingEnd")
|
||||
private void modzuozhi_syncTickingEnd(Entity entity, Operation<Void> original) {
|
||||
synchronized (this) {
|
||||
original.call(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 原版实体循环({@code entityTickList.forEach(lambda)}) 的每实体处理:isRemoved 检查 →
|
||||
* checkDespawn → 载具处理 → {@code guardEntityTick(tickNonPassenger, entity)}。其中除
|
||||
* {@code guardEntityTick} 外的逻辑为<b>串行预检</b>(改动小、需按序),只有最终实体的
|
||||
* {@code tick} 是真正的重负载,适合并行。
|
||||
* <p>
|
||||
* 由于 {@code guardEntityTick} 调用位于 lambda 内部(合成方法,无法稳定引用),本 mixin 用
|
||||
* {@code @WrapOperation} 包住整个 {@code forEach} 调用:进入前建立 ThreadLocal 屏障
|
||||
* ({@link EntityTickParallel#begin()}),离开时(finally)收口等待全部实体子任务完成
|
||||
* ({@link EntityTickParallel#end()})。真正的实体 tick 由 {@link EntityTickParallelMixin}
|
||||
* 在 {@code guardEntityTick} 处读取该屏障并提交并行。
|
||||
* <p>
|
||||
* 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效;
|
||||
* 关闭时完全退化为原版串行(零开销)。
|
||||
* <p>
|
||||
* <b>第14步(对齐 Async)</b>:第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<Entity> consumer, Operation<Void> original) {
|
||||
boolean parallel = modzuozhi_entityShouldParallel();
|
||||
if (parallel) {
|
||||
EntityTickParallel.begin();
|
||||
}
|
||||
try {
|
||||
original.call(list, consumer);
|
||||
} finally {
|
||||
if (parallel) {
|
||||
EntityTickParallel.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 收口。
|
||||
* <p>
|
||||
* 在 {@code ServerLevel} 上挂一个 {@link PlayerPacketQueue}(玩家发来的包处理任务队列),
|
||||
* 并在 {@code ServerLevel.tick} 开头(HEAD)统一执行。两种 tick 路径都覆盖:
|
||||
* <ul>
|
||||
* <li><b>维度并行</b>:维度 worker 在 {@code swapThreadsAndRun} 内 tick,包任务在 worker
|
||||
* 身份下执行(getBlockEntity 等线程守卫自然通过),与其它维度并行。</li>
|
||||
* <li><b>主线程串行</b>(dimCount <= 1 / 维度并行关闭):主线程直接 tick,包任务在主线程
|
||||
* 执行,行为等价原版。</li>
|
||||
* </ul>
|
||||
* 全局关闭时 {@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 维度循环模型:玩家跨维度传送收口到主线程。
|
||||
* <p>
|
||||
* 旧的「有界屏障」模型里,玩家实体 tick 只在主线程({@code EntityTickParallelMixin} 跳过维度
|
||||
* worker 上的玩家),因此下界门触发的 {@code ServerPlayer.changeDimension} 也在主线程执行,天然安全。
|
||||
* 切到维度循环后,玩家 {@code doTick()} 由本维度 loop 的 {@code tickConnections()} 在 worker 上触发,
|
||||
* 于是 {@code changeDimension} 会在 worker 上直接 {@code removePlayerImmediately} 旧维度 +
|
||||
* {@code addDuringTeleport} 新维度,与新维度 worker 并发改写实体容器 → 竞态崩溃。
|
||||
* <p>
|
||||
* 因此这里与 {@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<Entity> cir) {
|
||||
if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) {
|
||||
return;
|
||||
}
|
||||
if (DimThreadCore.owns(Thread.currentThread())) {
|
||||
DimensionChangeQueue.schedule((Entity) (Object) this, transition);
|
||||
cir.setReturnValue(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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。
|
||||
* <p>
|
||||
* 玩家实体 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);
|
||||
}
|
||||
}
|
||||
@@ -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 思路,写我们自己的实现)。
|
||||
* <p>
|
||||
* 在 {@code Level.tickBlockEntities()} 的 while 循环中,把线程安全(经
|
||||
* {@link SerDesFilter} 判定可并行)的 TE tick 提交到 {@link FineGrainScheduler.Barrier}
|
||||
* 并行执行,其余(黑名单/非 vanilla)串行;循环结束后 {@code await} 收口,再
|
||||
* {@code drain} 子任务投递的延迟回调。
|
||||
* <p>
|
||||
* 线程安全关键:并行子任务运行在其它 worker 线程,可能并发写 {@code pendingBlockEntityTickers}
|
||||
* 等普通集合。这里拦截 {@code addBlockEntityTicker} / {@code addFreshBlockEntities},
|
||||
* 在并行子任务上下文中把写入投递到 {@link PostExecuteQueue},由维度 worker 串行执行。
|
||||
* <p>
|
||||
* 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效;
|
||||
* 关闭时完全退化为原版串行。
|
||||
*/
|
||||
@Mixin(Level.class)
|
||||
public abstract class TickingBlockEntityParallelMixin {
|
||||
@Shadow
|
||||
private void addBlockEntityTicker(TickingBlockEntity ticker) {
|
||||
}
|
||||
|
||||
@Shadow
|
||||
private void addFreshBlockEntities(Collection<BlockEntity> 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<BlockEntity> entities, CallbackInfo ci) {
|
||||
PostExecuteQueue queue = FineGrainScheduler.currentQueue();
|
||||
if (queue != null) {
|
||||
queue.post(() -> this.addFreshBlockEntities(entities));
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} 添加维度检查。
|
||||
* <p>
|
||||
* 原版 {@code YieldJobSite.create()} 不做维度检查,当 {@code POTENTIAL_JOB_SITE} 存了
|
||||
* 另一个维度的 {@code GlobalPos} 时,村民会跨维度让工作站。并行环境下需确保 AI 只在本维度
|
||||
* 操作,避免跨维度引用导致的不一致。
|
||||
* <p>
|
||||
* 借鉴 Chlorophyll(Unlicense)的 {@code YieldJobSiteMixin} 思路,增加
|
||||
* {@code globalPos.dimension() != villager.level().dimension()} 检查,维度不匹配时直接返回。
|
||||
*/
|
||||
@Mixin(YieldJobSite.class)
|
||||
public abstract class YieldJobSiteGuardMixin {
|
||||
|
||||
@Shadow
|
||||
private static boolean nearbyWantsJobsite(Holder<PoiType> holder, Villager villager, BlockPos pos) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @reason 添加维度守卫,防止跨维度让工作站
|
||||
*/
|
||||
@Overwrite
|
||||
public static BehaviorControl<Villager> 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<Holder<PoiType>> 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;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Reference in new issue
Block a user