Compare commits
14
Commits
849ce7171c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cef4e20ad | ||
|
|
763c015103 | ||
|
|
691e949691 | ||
|
|
2619f0936d | ||
|
|
32b11aee87 | ||
|
|
74cae0b581 | ||
|
|
629f9e50d8 | ||
|
|
00bbb8005e | ||
|
|
98e39a0265 | ||
|
|
563787c5d4 | ||
|
|
7990d70255 | ||
|
|
350920eaed | ||
|
|
44e6fa2537 | ||
|
|
75ed486fec |
No files matched your search
@@ -2,13 +2,62 @@
|
||||
|
||||
Dimensional Ripper 是一个基于 **NeoForge 1.21.1** 的多线程维度并行优化模组。它通过独立维度 tick 循环与细粒度并行调度,充分利用多核 CPU 提升服务端性能,同时保持稳定的 20 TPS。
|
||||
|
||||
## 功能特性
|
||||
## 1.1.17:公共收包线程归属候选修复
|
||||
|
||||
服务端游戏阶段经 NeoForge `ServerPayloadContext.enqueueWork` 明确提交到主线程的工作,现在统一进入有界队列,并在维度更新归还执行权后,由真正的服务器主线程调用原处理器。Create/航空学旧组装入口复用同一安全阶段;采用这条公共入口的模组不需要主动适配。登录、配置阶段、客户端及纯网络计算不改派,也不代表任意模组的任意线程访问已自动安全。
|
||||
|
||||
队列上限为服务器 4096 项、单连接 256 项,每批最多 64 项或 2 ms(不强行中断已经开始的处理器)。保留观察到的入队顺序;执行前取消的工作不再修改世界,换世界/换玩家/停服后的旧工作明确失效。超载通过原断线流程处理;业务处理器自己抛出的拒绝异常不会被误判为超载踢人。新循环继承菜单暂停,仍在退出的循环也必须归还执行权。等待期间继续服务 Sable 主线程交接,拒绝会产生锁反转的同步重入。
|
||||
|
||||
真实本地模组回归:Sable 物理子世界内创建了真实把手约束,复现原路径松手后约束仍有效;经新入口处理同一松手包后约束失效。Create 调速包及 Jade 查询通过,原距离拒绝仍有效。另有队列、取消、暂停、停止和真实 CC 回归。详细环境、日志标记及未覆盖项见 [1.1.17 验证报告](docs/payload-ownership-1.1.17-validation.md)。这些是隔离服务器测试,不等于用户完整整合包、客户端飞行或多人压力验收。
|
||||
|
||||
阶段之外维度并行仍保留;有交互时会等待最慢维度收口,不承诺零开销。没有待处理交互时不建立收包屏障。`/dimensionalripper status` 与归属崩溃快照包含队列积压、最老等待、最近批次等待/执行时间;仅凭 Carpet 的低 MSPT 不能判断所有维度正常。记录最多 128 个具名包 ID 加 other、最近 32 条结果,失败警告全局 30 秒限频,不记录包正文。
|
||||
|
||||
**仍为测试候选,不要直接替换正式服务器:ServerWarashi(坐服童子)启动分析崩溃、C2ME 冲突、任意后台世界访问及既有停服超时不在这次修复范围内。** 工作线程提交后同步等待公共队列 future 仍不受支持;不能把它宣传为所有模组免适配。
|
||||
|
||||
## 1.1.16:CC 调度线程归属候选修复
|
||||
|
||||
针对 CC:Tweaked 1.118.0 `TickScheduler` 主线程读取正在被维度线程更新的世界、导致显示器身份检查得到 `null` 的问题:有待处理 token 时,先暂停维度续排并等待在途更新归还所有权,在服务器主线程执行原调度检查,最后恢复原先运行的循环。等待期间继续处理主线程回调,允许 Sable 交接完成;不绕过身份检查,不吞掉真实异常,不强加载未加载区块。CC 的全局延迟队列表对应的区块回调也做串行保护。未安装 CC 时不启用此补丁。
|
||||
|
||||
这不是零开销修复:CC 有任务时,安全阶段会等待当前最慢的维度收口,可能暂时影响主线程 TPS;没有任务时不建立屏障,阶段结束后维度与细粒度并行继续运行。不能保证所有维度在这一兼容阶段完全互不影响。`/dimensionalripper status` 增加最近一次交接等待/执行耗时。
|
||||
|
||||
线程诊断记录维度、Level/ChunkSource 所属线程、调用线程、位置、阶段和 Sable 交接状态。每维度最多保留 16 条最近事件;入口归属不匹配警告每维度最多 30 秒一次,抑制期间累计计数,慢安全阶段全局最多 30 秒一次。入口快照不等于原子世界快照,也不据此单独判定死锁。崩溃报告新增 `DimensionalRipper Thread Ownership`。
|
||||
|
||||
隔离测试:`gradlew runServer -PownershipRegression -PccTestJar=<本机CC的jar绝对路径>`。使用 `runs/server` 下的专用测试世界(运行前确认 `level-name=interaction-regression`、`server-ip=127.0.0.1`、`server-port=25579`),不要指向正式存档。测试源码和 CC jar 不加入常规发布包。已在修复前用真实 CC 显示器复现 `Expected MonitorBlockEntity ... got null`;修复后运行任务会检查日志中的 `OWNERSHIP REGRESSION PASS`、`OWNERSHIP EDGE PASS`、`OWNERSHIP CALLBACK PASS` 和 `OWNERSHIP RESUME PASS`。Minecraft 崩溃有时仍以零退出码结束,不能只检查 Java 退出码。
|
||||
|
||||
覆盖:真实显示器身份检查、实际主线程交接桥的回调执行、嵌套/异常后的暂停恢复、拆除与真实身份不匹配、未加载 token、80 个延迟 token 的双维度并发 ticket/unload 回调,以及崩溃诊断。1001 次入口归属不匹配在测试日志中仅输出 1 条警告。另用 1 TPS 的空闲预算复现暂停后仍等待整秒的问题,修正为世界工作完成后立即退出已请求暂停的空闲轮询;保留在途工作的收尾。一次本机测试的 CC 等待/执行为约 7.4/0.12 ms,仅是隔离样本,不是整合包性能承诺。测试调用实际交接桥,但没有加载 Sable 原生物理引擎。
|
||||
|
||||
本修复限定于 CC 调度路径,不是对所有模组跨线程访问的通用放行。仍需在完整整合包的存档副本验证,尤其 CC 显示器随 Sable 结构移动的情况;不要据此认为所有航空学、区块发送、停服问题均已解决。
|
||||
|
||||
## 优化范围(均为服务端运算,不影响客户端画面/渲染)
|
||||
|
||||
### 🗺️ 维度
|
||||
- **维度并行 tick**:主世界/下界/末地/自定义维度各一个独立循环线程同时跑,一个维度卡顿不拖累其它维度。
|
||||
- **保存独立接管**:存档保存流程单独处理,防止存盘时整服冻结。
|
||||
|
||||
### 👾 实体(优化重点)
|
||||
- **实体 tick 并行**:村民、动物、怪物、掉落物等原版生物按 ID 分几十片多线程同时 tick(ShardGate 分片掉拍:单片慢只拖自己,不阻塞全局屏障)。
|
||||
- **存储结构并发化**:实体查找表、区块内实体表、section 容器全部换成无锁并发集合,消除读锁排队。
|
||||
- **村民 AI 特化**:大脑(Brain)、最近实体传感器、寻路、找床/找工作点、抢职位竞争、八卦消息全部做了多线程安全加固。
|
||||
- **碰撞优化**:实体间碰撞默认关闭(`nocollide`,省去 O(n²) 开销),碰撞查询本身并行执行。
|
||||
- **跨维度传送**:换维度处理重定向到正确线程,避免跨线程状态错乱。
|
||||
- 压测数据:约 1.9 万村民场景下 TPS 从 9 提升到 20(16 核)。
|
||||
|
||||
### 🧱 区块 / 世界
|
||||
- **区块环境并行**:每个区块的随机 tick(植物生长、水流、岩浆、红石脉冲)按区块坐标分片并行跑。
|
||||
- **调度并发化**:区块加载/卸载、DistanceManager ticket 管理、计划刻调度(LevelTicks/BinaryHeap)全部线程安全。
|
||||
- **爆炸**:统一串行执行,防止并发写坏区块数据。
|
||||
|
||||
### ⚙️ 方块实体(机器)
|
||||
- **机器 tick 并行**:熔炉、箱子、信标等原版方块实体的 tick 按位置分片并行。
|
||||
- **POI 并发化**:村民找床/找工作点查询不再排队等锁。
|
||||
- **方块更新/掉落/游戏事件**:收集器、掉落物捕捉、游戏事件分发重定向到正确线程。
|
||||
- **保守串行清单(黑名单)**(有意为之,非未实现):漏斗/投掷器/发射器/雕刻书架/合成器/蜂巢(跨容器 5×5 访问、共享 Level 级随机源,并行会数据竞争,见 [SerDesFilter.BLACKLIST](https://github.com/xxdd001/DimensionalRipper/blob/main/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java)),黑名单内置不可命令修改;第三方模组 TE 默认串行,可用白名单指令放行(见下)。
|
||||
|
||||
### 🎒 物品
|
||||
- 物品本身无独立并行优化(合成、堆叠仍走原版逻辑);掉落物作为实体参与并行 tick,方块掉落物在正确线程处理。
|
||||
|
||||
## 其它特性
|
||||
|
||||
- **维度并行 Tick**:每个维度拥有独立的 tick 循环与 worker 线程,维度之间互不拖累——单维度低 TPS 不会影响其他维度。
|
||||
- **细粒度并行**:在维度内部对实体、方块实体、区块环境 tick 进行并行调度,充分压榨多核性能。
|
||||
- **分片并行 (ShardGate)**:实体/方块实体/区块环境三阶段统一按稳定键分片(实体按 ID、TE 按方块坐标、区块环境按区块坐标),每片由跨 tick 的 CAS 门控独立提交。单个慢分片自动掉拍延后,不再阻塞对应阶段的并行屏障——把"单点慢"从拖垮全局变成只拖自己。
|
||||
- **POI 并发锁优化**:将村民等实体高频读取的职业兴趣点(POI)存储从全互斥同步容器替换为无锁并发容器,消除并行 tick 下的全局锁瓶颈。约 1.9 万村民压测下 TPS 由 9 提升至 20(16 核)。
|
||||
- **碰撞优化**:可禁用实体间碰撞,消除 O(n²) 碰撞开销,显著释放 TPS。
|
||||
- **异步传送门传送**:支持原版下界/末地传送门与模组维度(如暮色森林、以太)传送门的异步预加载与渐进生成,避免传送到未加载区块导致的卡顿、冻结与虚空传送。
|
||||
- **区块预取**:根据玩家位置与朝向预加载区块,加快跑图时的区块加载速度。
|
||||
- **故障降级**:内置看门狗与故障熔断机制,异常时安全降级回原版串行 tick,保障服务器长期稳定运行。
|
||||
@@ -19,9 +68,59 @@ Dimensional Ripper 是一个基于 **NeoForge 1.21.1** 的多线程维度并行
|
||||
- NeoForge **21.1.x**
|
||||
- JDK **21**
|
||||
|
||||
## ⚠️ 兼容性警告
|
||||
## ⚠️ 兼容性说明
|
||||
|
||||
> **不兼容警告**:本模组**不建议与任何其他多线程优化模组或跨维度交互类模组同时使用**。经实际测试,与这类模组共同加载会发生冲突并导致游戏崩溃。若需使用本模组,请单独加载,或移除其它多线程优化模组 / 跨维度交互模组。
|
||||
**已知未修复问题(1.1.15 / 1.1.16)**:与 C2ME 的 `c2me_fixes_worldgen_threading_issues` 同时加载时,双方对 `Level` 构造器内随机源初始化的 `@Redirect` 冲突,可能导致启动失败。日志已确认 C2ME 优先级 1100 的重定向覆盖本模组优先级 1000 的 `modzuozhi_threadSafeRandom`,继而触发注入检查失败。当前源码尚未包含此兼容修复,不能视为可直接部署的稳定版。
|
||||
|
||||
1.1.15 针对 Sable 碰撞查询期间反复创建实心占位区块的问题:
|
||||
|
||||
- 删除 `ModZuozhiSolidChunk`。并行子任务的 `getChunk(..., required=false)` 在区块缺失时返回 `null`,不再伪造“已加载”结果、不分配区块段,也不触发加载/生成。已有区块仍返回当前活动对象。
|
||||
- `required=true` 的缺失 FULL 区块与缺失其它状态统一快速失败,并关闭后续细粒度并行;不会等待正处于屏障的维度线程,也不在部分执行后重跑同一个实体。
|
||||
- 实体预计移动包围盒(外扩 1 格)涉及未加载区块,或扫描跨度超过 8 个区块时,在实体批提交前交所属维度线程串行执行;已加载区域仍保留并行。此预检不是对任意模组远距离查询的保证,遗漏的必须加载请求仍由上一条保护。
|
||||
- 每个超时阶段只输出一次详细线程栈,限服务器/本模组非空闲线程;后续保留简短等待信息。超时不会强行放走仍在操作世界的任务。
|
||||
|
||||
隔离 NeoForge 21.1.244 / Java 21 运行测试:两条实际子任务共执行 60 万次缺失查询、40 万次已加载查询,确认返回值/对象身份、区块数量不增加且并行未降级;验证缺失 FULL/EMPTY 必须加载请求快速失败,加载区域实体并行、超大移动范围实体串行且不重复执行。主线程交接、水流计划刻和维度完成进度测试同时通过。测试中的一次 FaultGuard ERROR 是必须加载失败用例主动触发并在测试中复位,不代表正常运行需要复位。运行方式:`gradlew runServer -PinteractionRegression`,仅用于隔离测试世界。
|
||||
|
||||
本次未运行用户完整服务端整合包(尤其其额外的 Sable Chunk Guard 1.0.0);以上不是完整飞行器/区块发送验收,也不宣称此前 DriveByWire 并发异常已修复。请在存档副本测试,不直接用于正式服。
|
||||
|
||||
1.1.14 增加每维度进度监控:`/dimensionalripper status` 列出所有已创建维度循环的实际完成 TPS(约 10 秒滚动窗口,100ms 分桶)、工作 MSPT 均值/最近值、当前 Tick 持续时间、距最近成功完成时间及失败 Tick 总数。TPS 不再由工作耗时推算,也不钳制到 20;没有新完成时约 10 秒内降至 0,当前 Tick 或无完成时间超过 1 秒单独提示,提示不等于断定死锁。暂停/停止显示状态和 `—` TPS;恢复后的窗口包含暂停时间,约 10 秒后更新完毕。无采样不显示 20。统计随维度循环销毁,不跨存档复用。
|
||||
|
||||
工作 MSPT 含玩家包处理、主线程交接等待和世界工作,排除末尾区块队列轮询;完成记录在整个循环归还世界所有权后写入。世界 Tick 抛异常不计成功完成;细粒度内部自行捕获的错误仍应结合故障状态判断。这些指标描述执行进度,不能证明水流、组装等游戏逻辑正确,也不修改 Carpet 的 HUD。
|
||||
|
||||
1.1.13 为水流问题的**诊断版,不宣称修复停流**。`flowstatus` 改为优先检查准星所指的 8 格内方块/水源,报告实体加载状态、LevelTicks 完整执行条件、最近调度时间、该区块队首计划刻的位置/剩余时间和调度索引。此前 1.1.12 仅显示玩家区块状态及全维度计数,不能据此判断同两项流体任务一直被卡住。本版不绕过实体加载门槛、不重排计划刻、不改写用户存档。
|
||||
|
||||
1.1.12 候选修复:Simulated 的 AssemblePacket(组装/拆解)和 Create 的 BlockEntityConfigurationPacket(包含转速 ValueSettingsPacket)先进入所属维度安全点,再借用服务器主线程执行原处理器。避免维度线程接管期间,主线程 getBlockEntity 返回空值而请求静默失效;保留原处理器的距离和权限检查,排队后换维度/离线则丢弃请求。其余维度继续运行,不全局关闭并行。
|
||||
|
||||
隔离运行测试 `InteractionFlowRegression` 验证了真实 ServerLevel 中的独占主线程方块实体读取,以及放水后的计划刻扩散。测试不加载 Create/Simulated,不能代替完整整合包的实际拉杆、转速和拆解验收;报告的整合包水流停滞尚未复现、未宣称修复。可站在问题水源附近间隔数秒执行两次 `/dimensionalripper flowstatus`,获取世界时间、区块更新条件和计划刻数量;诊断只应用户请求输出,不持续刷日志。
|
||||
|
||||
1.1.11 修正:加载界面的暂停状态不再停止维度循环。暂停交接移到服务器线程,在原版暂停保存之前等待在途维度 tick 完成,并继续处理 Sable 回调;已暂停且完全空闲的维度将区块处理权交还服务器主线程。调度入口与暂停共用同步保护和代次校验,防止取消后的旧任务在恢复时重复启动。
|
||||
|
||||
验证:Lithium 0.15.3 + Sable 2.0.5 临时服务端完成 10 次暂停状态下的真实 ServerPlayer 构造、同步生成新区块、恢复循环,以及 10,000 次运行中区块请求,细粒度保持开启。纯 Java 测试覆盖加载/登录暂停条件及在途/空闲所有权。服务端测试不加载客户端暂停 Mixin,因此不能替代完整 StarLight 单机进世界、ESC 暂停及飞行器验证。临时测试停机时仍记录 Sable 等待被中断的异常,不能将本测试描述为全程无错误。
|
||||
|
||||
1.1.10 修正:1.1.9 在 `getChunk` 入口检查 `mainThread` 后才进入原版方法;维度 worker 可在两次检查之间接管,导致真正服务器线程仍落入原版阻塞 `join`,同时 Sable 排队等服务器线程。现在维度循环启动后,外部服务器区块请求一律通过 `tell` 排队,等待期间处理服务器回调;只有明确暂停了维度 worker 的同步 Sable/玩家交接阶段才允许直接读取。维度异常也不再强行清空仍有区块请求的线程队列。
|
||||
|
||||
本次在 Lithium 0.15.3 + Sable 2.0.5 的临时服务端测试了 10,000 次 server-post-tick 同步区块请求,未卡死且细粒度保持启用,保存退出完成。测试夹具保留于 `tests/integration/ChunkHandoffRegression.java`,不进入发布 jar;完整 StarLight 客户端整合包尚未验证。
|
||||
|
||||
1.1.9 修正:
|
||||
|
||||
- 子任务屏障改为按完整提交阶段计数,修复“先完成一批、随后又提交任务”时一次性 latch 提前放行的问题。超时和中断均不再允许在任务未完成时继续下一阶段。
|
||||
- 10 秒是诊断阈值,不是正常加载预算;超时后关闭后续细粒度阶段,但保留当前维度所有权并记录完整线程栈、锁持有者、阶段和池状态。真正卡死的任务不会被此机制强行解锁,仍需根据诊断定位。
|
||||
- 实体查询先在锁内收集结果,再在锁外执行回调,避免回调移动/删除实体导致读锁升级死锁。
|
||||
- 子任务读取非 FULL 区块也走已就绪路径;必须同步生成时明确报错并关闭后续细粒度并行,避免等待正在等待自己的维度线程。
|
||||
- **Lithium 环境下村民改在所属维度线程执行**:实测 65 村民并行查询 POI 可写坏 Lithium 的 `ListeningLong2ObjectOpenHashMap`。这是定向限制,Sable 物理主线程桥接、其它生物、方块实体和区块环境的并行开关不因此关闭。
|
||||
- Lithium 的实体分类缓存添加同一实例上的读写同步,并返回快照,修复碰撞查询绕过 section 锁后与实体移动并发修改分类集合的问题。
|
||||
|
||||
等待逻辑回归测试:`tests/TaskBarrierRegression.java`(提交间隙、超时、中断、可见性、2 万次并发完成)和 `tests/ManagedFutureAwaitRegression.java`。这些测试与小规模 Lithium + Sable 临时世界测试不替代完整整合包及真实飞行器验证;旧日志缺少子任务栈,无法唯一确定旧 10 秒超时的具体阻塞点。
|
||||
|
||||
1.1.8 修正:真实服务器主线程同步等待区块 Future 时,使用服务器事件循环的 managedBlock 继续处理回调,避免主线程等待区块、维度线程等待物理/玩家主线程回调的循环等待。区块任务仍由原区块执行器处理。独立等待关系回归测试位于 `tests/ManagedFutureAwaitRegression.java`;它验证回调归属、完成、异常和取消,不替代整合包内的进服及飞行器测试。
|
||||
|
||||
1.1.7 修正:加载 StarLight Extra 时,玩家连接 Tick(包括玩家 Tick 事件和断开清理)同步交回服务器主线程,对应维度等待完成后继续。区块反序列化恢复使用原有区块执行器,避免后台反序列化同时写入 Lithium 的 POI 存储。其它维度及细粒度并行开关不因此关闭。这是针对已观察到异常的修补,编译或空服启动成功不代表完整整合包、玩家交互和飞行器压力测试已通过。
|
||||
|
||||
> **Create: Aeronautics / Sable**:1.1.5 起内置主线程桥接。Sable 的物理子世界 Tick 同步交回真正的服务器主线程,完成后维度 worker 继续运行。已开始的物理阶段必须结束后才能归还所有权,不能通过超时强行中断。可通过 `/dimensionalripper status` 确认桥接及 Lithium 村民限制。
|
||||
>
|
||||
> **Lithium**:1.1.6 起会优先保留 Lithium 的实体移动碰撞注入,避免双方争抢同一调用点导致启动崩溃。本模组的实体互推关闭仍会生效;移动碰撞查询交由 Lithium 优化。
|
||||
>
|
||||
> 其它多线程优化模组或跨维度交互类模组仍不建议与本模组同时使用;尚未建立专门兼容路径的组合可能发生冲突。
|
||||
|
||||
## 指令
|
||||
|
||||
@@ -31,6 +130,10 @@ Dimensional Ripper 是一个基于 **NeoForge 1.21.1** 的多线程维度并行
|
||||
| `/dimensionalripper fine on\|off` | 开关维度内细粒度并行(实体/方块实体/区块环境) |
|
||||
| `/dimensionalripper fine sharded on\|off` | 开关分片并行(实体/TE/区块环境,ShardGate,默认开启) |
|
||||
| `/dimensionalripper nocollide on\|off` | 开关实体碰撞优化(默认开启) |
|
||||
| `/dimensionalripper tewhitelist <注册名>` | **白名单**:将指定第三方模组方块实体(TE)加入并行白名单放行(如 `stresskit:heavy_furnace`) |
|
||||
| `/dimensionalripper teunwhitelist <注册名>` | 从并行白名单移除该 TE(恢复默认强制串行;重启服务器也会自动清空白名单) |
|
||||
|
||||
> **白名单说明**:第三方模组 TE 默认串行处理(保守策略),加入白名单后才会进入并行分片管线。黑名单(漏斗/发射器等原版保守串行 TE)为内置硬编码,**不支持命令添加/移除**。
|
||||
|
||||
模组**默认启用**多线程与分片并行,开箱即用,无需额外配置。
|
||||
|
||||
|
||||
@@ -76,6 +76,71 @@ dependencies {
|
||||
implementation "net.neoforged:neoforge:${neo_version}"
|
||||
}
|
||||
|
||||
// Opt-in isolated runtime fixture; excluded from normal builds.
|
||||
if (providers.gradleProperty('payloadRegression').isPresent()) {
|
||||
if (providers.gradleProperty('reviewRegression').isPresent()) {
|
||||
runs.configureEach { systemProperty 'dimensionalripper.reviewRegression', 'true' }
|
||||
}
|
||||
if (providers.gradleProperty('interactionRegression').isPresent() || providers.gradleProperty('ownershipRegression').isPresent()) {
|
||||
throw new GradleException('Run payloadRegression separately.')
|
||||
}
|
||||
sourceSets.main.java.srcDir('tests/payload')
|
||||
if (providers.gradleProperty('payloadModsDir').isPresent()) {
|
||||
dependencies {
|
||||
runtimeOnly fileTree(dir: providers.gradleProperty('payloadModsDir').get(), includes: [
|
||||
'create-1.21.1-6.0.10.starlight.jar', 'create-aeronautics-bundled-1.21.1-1.3.0.starlight.jar',
|
||||
'sable-neoforge-1.21.1-2.0.5.starlight.jar', 'veil-neoforge-1.21.1-4.2.0.starlight.jar',
|
||||
'Jade-1.21.1-NeoForge-15.10.5.starlight.jar'
|
||||
])
|
||||
}
|
||||
}
|
||||
tasks.matching { it.name == 'runServer' }.configureEach {
|
||||
doLast {
|
||||
def output = file('runs/server/logs/latest.log').getText('UTF-8')
|
||||
if (providers.gradleProperty('reviewRegression').isPresent()) {
|
||||
if (!output.contains('PAYLOAD REVIEW PASS') || output.contains('Encountered an unexpected exception'))
|
||||
throw new GradleException('Payload review regression did not pass.')
|
||||
return
|
||||
}
|
||||
if (!['PAYLOAD PHASE PASS', 'PAYLOAD ENTRY PASS', 'PAYLOAD ASYNC PASS', 'PAYLOAD LIFECYCLE PASS', 'PAYLOAD DIAGNOSTICS PASS'].every { output.contains(it) }
|
||||
|| output.contains('Encountered an unexpected exception'))
|
||||
throw new GradleException('Payload phase regression did not pass.')
|
||||
if (providers.gradleProperty('payloadModsDir').isPresent()
|
||||
&& !['SIMULATED HANDLE PACKET PASS', 'CREATE CONFIG PASS', 'JADE REQUEST PASS'].every { output.contains(it) })
|
||||
throw new GradleException('Real Simulated/Create/Jade packet regression did not pass.')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providers.gradleProperty('interactionRegression').isPresent()) {
|
||||
sourceSets.main.java.srcDir('tests/integration')
|
||||
sourceSets.main.java.exclude('**/ChunkHandoffRegression.java')
|
||||
tasks.matching { it.name == 'runServer' }.configureEach {
|
||||
doLast {
|
||||
def output = file('runs/server/logs/latest.log').getText('UTF-8')
|
||||
if (!['ENTITY ROUTING REGRESSION PASS', 'MISSING CHUNK REGRESSION PASS',
|
||||
'REQUIRED CHUNK REGRESSION PASS', 'INTERACTION REGRESSION PASS'].every { output.contains(it) }
|
||||
|| output.contains('Encountered an unexpected exception'))
|
||||
throw new GradleException('Interaction/water regression did not pass.')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providers.gradleProperty('ownershipRegression').isPresent()) {
|
||||
if (providers.gradleProperty('interactionRegression').isPresent()) {
|
||||
throw new GradleException('Run ownershipRegression and interactionRegression separately.')
|
||||
}
|
||||
sourceSets.main.java.srcDir('tests/ownership')
|
||||
dependencies { runtimeOnly files(providers.gradleProperty('ccTestJar').get()) }
|
||||
tasks.matching { it.name == 'runServer' }.configureEach {
|
||||
doLast {
|
||||
def output = file('runs/server/logs/latest.log').getText('UTF-8')
|
||||
['OWNERSHIP REGRESSION PASS', 'OWNERSHIP EDGE PASS', 'OWNERSHIP CALLBACK PASS', 'OWNERSHIP RESUME PASS'].each { marker ->
|
||||
if (!output.contains(marker)) throw new GradleException("Missing runtime test result: ${marker}")
|
||||
}
|
||||
if (output.contains('Encountered an unexpected exception')) throw new GradleException('Runtime regression server crashed.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(ProcessResources).configureEach {
|
||||
var replaceProperties = [
|
||||
minecraft_version : minecraft_version,
|
||||
@@ -107,3 +172,27 @@ idea {
|
||||
downloadJavadoc = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pure-Java scheduling regressions run in check without bootstrapping Minecraft.
|
||||
def regressionClasses = layout.buildDirectory.dir('regression')
|
||||
tasks.register('compileConcurrencyRegression', JavaCompile) {
|
||||
source = files('tests/TaskBarrierRegression.java', 'tests/ManagedFutureAwaitRegression.java',
|
||||
'src/main/java/dev/modzuozhi/core/compat/TaskBarrier.java',
|
||||
'src/main/java/dev/modzuozhi/core/compat/ManagedFutureAwait.java',
|
||||
'src/main/java/dev/modzuozhi/core/compat/PauseOwnership.java', 'tests/PauseOwnershipRegression.java',
|
||||
'src/main/java/dev/modzuozhi/core/compat/TickProgress.java', 'tests/TickProgressRegression.java',
|
||||
'src/main/java/dev/modzuozhi/core/compat/PayloadWorkQueue.java', 'tests/PayloadWorkQueueRegression.java')
|
||||
classpath = files()
|
||||
destinationDirectory = regressionClasses
|
||||
}
|
||||
['TaskBarrierRegression', 'ManagedFutureAwaitRegression', 'PauseOwnershipRegression', 'TickProgressRegression', 'PayloadWorkQueueRegression'].each { regressionMain ->
|
||||
def regressionTask = tasks.register("run${regressionMain}", JavaExec) {
|
||||
dependsOn 'compileConcurrencyRegression'
|
||||
classpath = files(regressionClasses)
|
||||
mainClass = regressionMain
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
tasks.named('check') { dependsOn regressionTask }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# 1.1.17 公共收包归属:候选验证记录
|
||||
|
||||
日期:2026-10-06。结论:公共入口与已列交互路径已实施;不是完整整合包兼容认证,也不是 ServerWarashi 修复。
|
||||
|
||||
## 环境与范围
|
||||
|
||||
Java 21 Zulu、Minecraft 1.21.1、NeoForge 21.1.244。仅使用仓库 `runs/server` 的 `interaction-regression` 世界、127.0.0.1:25579。没有修改用户正式世界、启动器 mods 或远端仓库。用户报告过的 NeoForge 21.1.233 未在本轮单独运行。
|
||||
|
||||
可选模组为用户本机实际 jar:Create 6.0.10.starlight、Create Aeronautics bundled 1.3.0.starlight(含 Simulated)、Sable 2.0.5.starlight、Veil 4.2.0.starlight、Jade 15.10.5.starlight;CC:Tweaked 1.118.0.starlight 单独运行。
|
||||
|
||||
## 证据层级
|
||||
|
||||
| 层级 | 已验证内容 | 证据 |
|
||||
|---|---|---|
|
||||
| 纯 Java | FIFO、至多一次、256/4096 容量、取消回收、64 项/2 ms 预算、关闭、重入完成、8 生产者 | `runPayloadWorkQueueRegression` 的 `PAYLOAD QUEUE PASS` |
|
||||
| 实际 Minecraft/NeoForge 上下文 | Runnable/Supplier、返回值、主线程异常身份、异步 guard、取消、换世界失效、配置阶段原路径、嵌套、Sable 同步重入拒绝 | `PAYLOAD PHASE/ENTRY/ASYNC PASS` |
|
||||
| 实际 Minecraft 生命周期 | 新循环、菜单暂停、退出中循环仍等待、停止不恢复、停止后晚到任务不能复建诊断 | `PAYLOAD LIFECYCLE PASS`、`PAYLOAD REVIEW PASS` |
|
||||
| 实际可选模组和原生物理 | 真实子世界、真实把手 BE、真实包和 Veil 上下文、Rapier 约束的创建及松手销毁 | `SIMULATED NATIVE RED` 后 `SIMULATED HANDLE PACKET PASS` |
|
||||
| 实际 Create / Jade | 调速写入、查询 BE、原始距离拒绝 | `CREATE CONFIG PASS`、`JADE REQUEST PASS` |
|
||||
| 实际 CC | 显示器同一性、回调等待、80 个延迟 token、双维度 ticket/unload、恢复后继续 tick | 四个 `OWNERSHIP … PASS` |
|
||||
| 已有流水/区块/实体路径 | 600000 次缺失读取、400000 次已加载读取、必须生成请求安全失败、实体路由至多一次、真实水流扩散 | 四个 `… REGRESSION PASS`,其中包括 `INTERACTION REGRESSION PASS` |
|
||||
| 空载/无可选模组 | 不安装上述可选模组仍启动;100 次空 drain 没有新增批次 | `PAYLOAD IDLE PASS` 及通用 payload 标记 |
|
||||
|
||||
真实把手的失败证明不是“函数调用成功”:测试让维度线程持有 plot 世界归属,直接运行原松手包后物理约束仍然 `isValid=true`;相同真实包经公共安全入口运行后 `isValid=false`。测试使用 FakePlayer(有真实测试监听器),并非真人客户端输入或真实网络连接。
|
||||
|
||||
四个审查问题均先失败后通过:业务拒绝异常误触发踢人、动态循环丢失菜单暂停、退出中的写入者漏过屏障、停服清理后晚到诊断重新持有服务器。日志 `payload-review-red-2.log` 对应失败,`payload-review-final.log` 对应修正后的通过。
|
||||
|
||||
诊断分类额外使用 `payload-counter-red.log` 复现“业务失败与容量拒绝混计”,`payload-counter-green.log` 通过;`payload-candidate-real.log` 再次通过真实模组测试并检查换世界旧任务计入 expired。failed、cancelled、expired、rejected 分开统计。
|
||||
|
||||
旧交互回归第一次运行失败,不能因 Gradle 返回零而算通过:旧夹具没有 connection,且仍要求已经替换掉的单维度桥。夹具改为 FakePlayer 与公共独占断言,保留所有流水/区块检查,并补上日志 PASS/异常检测门禁;`payload-final-interaction-green.log` 才是通过记录。
|
||||
|
||||
## 本轮取舍
|
||||
|
||||
1. Windows 无 bash,以 PowerShell/apply_patch 维护执行记录;风险是记录遗漏,测试门禁不变。
|
||||
2. 主线程独占标志与菜单暂停分离;否则嵌套 resume 会提前放走维度线程。代价是多一组生命周期状态。
|
||||
3. 阶段、入口、生命周期共用夹具及文件,相关提交合并验证,不把中间状态当发布版本。
|
||||
4. 保留 NeoForge 异步业务异常返回 null 的 guard 行为,但替换无限逐条异常打印为有界诊断。显式策略失败(旧玩家/停止/溢出)保留异常 future,执行前取消传播到底层队列;这是有意的兼容语义细化,依赖“失败也返回 null”的调用方可能观察到变化。
|
||||
5. 普通主线程同步调用为保序与同步完成可连续处理多个有界批次;2 ms 不是整 tick 硬上限,慢维度和慢处理器仍可能拉长等待。独占阶段内嵌套立即运行。
|
||||
6. 旧交互夹具适配新连接/归属契约而非放松生产守卫;不能替代真实客户端网络压力验证。
|
||||
7. 既有停服 30 秒超时、ServerWarashi、C2ME、任意后台世界/光照访问保留为独立问题;它们仍可能造成完整整合包失败。
|
||||
8. 只交付隔离环境验证的候选,不将以下未测项标成通过。审查没有列出待办 Minor。
|
||||
|
||||
## 未完成的验收,不属于已修复声明
|
||||
|
||||
- 真人客户端整套飞行、抓握输入、GUI 与完整整合包存档副本测试。
|
||||
- 两玩家同时抓握、死亡、断线、换维度、拆把手、卸载子世界及溢出拒绝松手后的完整原生约束清理矩阵。已测普通真实松手、通用旧世界拒绝和溢出断线调用,不能互相替代。
|
||||
- 跨服务器实例隔离的双实例并行压力测试;实现按服务器分区,不能仅凭代码视为此项实测。
|
||||
- 空载、普通交互、密集抓握、多人查询和慢维度的完整对比压测。空队列无批次及慢 worker 等待机制已验证,不承诺开销比例或全服 20 TPS。
|
||||
- 任意 worker 提交后同步 join 主线程队列不受支持。夹具通过 250 ms 超时检测并取消这种等待环,不表示生产代码会自动化解任意 join 死锁。
|
||||
- ServerWarashi 分析立即崩溃仍需单独从采样栈修复。
|
||||
|
||||
## 重跑
|
||||
|
||||
以下命令均使用 `--no-daemon --no-configuration-cache --console=plain`,先检查隔离世界配置,不能指向正式存档:
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat check
|
||||
.\gradlew.bat runServer -PpayloadRegression
|
||||
.\gradlew.bat runServer -PpayloadRegression -PreviewRegression
|
||||
.\gradlew.bat runServer -PpayloadRegression '-PpayloadModsDir=<本机上述模组目录>'
|
||||
.\gradlew.bat runServer -PownershipRegression '-PccTestJar=<本机CC jar>'
|
||||
.\gradlew.bat runServer -PinteractionRegression
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
每组运行必须完成后再启动下一组。查看对应 PASS 标记及服务器异常,而非只看 Java/Gradle 退出码。正常构建不可带任何回归夹具属性。
|
||||
|
||||
## 打包门禁
|
||||
|
||||
`check runServer -PpayloadRegression` 成功(五个纯 Java 回归程序及无可选模组服务器夹具);随后不带任何夹具属性的 `clean build` 成功,五个纯 Java 回归再次执行通过。真实模组、CC、交互及审查运行见上述分组,未将测试标记缺失的运行计入通过。
|
||||
|
||||
发布 jar:`build/libs/dimensionalripper-1.1.17.jar`,277211 字节。ZIP 内容检查确认元数据版本为 1.1.17,包含公共入口 mixin/执行器,不含 Regression、FakePlayer、tests 或打包的可选模组 jar/类。
|
||||
|
||||
SHA256:`CF1DE863DE119272098142CF5F0ABE78543BA55B15074369A1A66FBC922FBAD3`。
|
||||
|
||||
完整本地日志已保存在 `build/reports/payload/`(忽略提交)。关键最终文件:`payload-clean-build.log`、`payload-candidate-check.log`、`payload-candidate-real.log`、`payload-counter-green.log`、`payload-final-cc.log`、`payload-final-interaction-green.log`。失败前例也保留供对照。执行工作区仍保留未完成验收记录,不把剩余矩阵标为完成。
|
||||
@@ -0,0 +1,133 @@
|
||||
# 公共收包线程归属 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将受支持的服务端游戏包交给真正主线程独占处理,修复把手松开路径,同时保留阶段之外的维度并行。
|
||||
|
||||
**Architecture:** 每服务器有界 FIFO 接收显式主线程工作;服务器外层 tick 建立全维度安全阶段,批量调用原处理器。阶段状态阻止递归收口和新增维度写入,旧组装入口复用同一执行权。生命周期清理与通用包失效分开处理。
|
||||
|
||||
**Tech Stack:** Java 21、Minecraft 1.21.1、NeoForge、Mixin/MixinExtras、现有 Gradle 隔离服务器回归。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-06-payload-ownership-design.md`
|
||||
|
||||
**执行状态(2026-10-06):** Task 1 已提交 `691e949`;Task 2–5 的公共入口、阶段、适配与诊断代码已实施并有回归证据。Task 4 的完整原生资源异常清理矩阵、Task 5 的双服务器压力测试、Task 6 的完整性能/客户端矩阵未全部验收,因此不勾选混合这些要求的条目。具体通过项和剩余项以 `docs/payload-ownership-1.1.17-validation.md` 为准,本轮只交付候选包。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 第一版新公共入口默认全维度隔离,不凭“玩家当前维度”或包内坐标猜测整个处理器的访问范围。
|
||||
- 初始批次上限为 64 项或 2 ms 的处理时间,达到任一条件后不启动下一项;不强行中断已开始的处理器。
|
||||
- 服务器队列上限 4096 项、单连接上限 256 项。
|
||||
- 记录上限 128 个 ID,超出归入 other,避免恶意 ID 撑大统计。
|
||||
- 登录、握手、配置阶段、客户端接收和纯网络线程处理不被无条件改派。
|
||||
- 超时用于诊断,不是允许并发执行的理由。
|
||||
- 不自动上传远端或部署正式服务器。测试仅操作仓库隔离世界或显式创建的存档副本。
|
||||
- 不改变原处理器权限、距离、强加载规则;不放松 Level 的线程守卫。
|
||||
- ServerWarashi 是后续独立修复,需要单独的采样设计及失败测试;本计划不宣称解决该崩溃。
|
||||
- C2ME、后台光照查询及既有停服超时不纳入修复声明。
|
||||
|
||||
## Review Focus
|
||||
|
||||
- 主线程正在服务等待中的 Sable 回调时再次 enqueue:不得等待该回调所属 worker,任务 2、3 验证。
|
||||
- 前一个包传送/替换玩家后,同连接后续旧包不得操作新世界:任务 3、4 验证。
|
||||
- 嵌套调用导致新世界/循环出现:不得在独占期间启动另一个写入者,任务 2 验证。
|
||||
- 队列满载时恰好收到松手:拒绝要显式完成 future,断线清理不能遗留约束,任务 1、4 验证。
|
||||
- 停服与阶段 finally、开关/菜单暂停同时发生:不得恢复已停止或仍应暂停的循环,任务 2、5 验证。
|
||||
|
||||
## 执行与证据约定
|
||||
|
||||
当前工作树含尚未提交的 1.1.16 归属修复,不能用新 worktree 的裸 HEAD 替代这些依赖。执行开始按 worktree 技能检查隔离方式;保留并核对所有现有变更,禁止 reset 或覆盖。不把原有未验证内容顺手算作本次通过。
|
||||
|
||||
以下路径相对仓库根。每个任务先写断言、记录失败,再改生产代码;每任务结束显式暂存该任务文件并提交(git 使用本仓库 safe.directory)。混合文件先核对 diff,不能误纳入用户后续改动。
|
||||
|
||||
命令统一使用 Java 21、`--no-daemon --no-configuration-cache --console=plain`。新的 `-PpayloadRegression` 夹具与 `ownershipRegression`、`interactionRegression` 互斥;运行结束必须有全部指定 PASS 标记且没有 unexpected exception,不能仅依赖进程退出码。证据放 `build/reports/payload/`。
|
||||
|
||||
## 文件职责
|
||||
|
||||
- `core/compat/PayloadWorkQueue.java`:不依赖 Minecraft 的 FIFO、容量、取消及批次预算。
|
||||
- `core/compat/MainThreadPhaseCoordinator.java`:服务器阶段状态与独占租约。
|
||||
- `core/compat/PayloadMainThreadDispatch.java`:实际玩家上下文、future、生命周期与 drain。
|
||||
- `core/compat/PayloadDiagnostics.java`:有界计数及耗时快照。
|
||||
- `mixin/ServerPayloadContextMixin.java`:两个 enqueueWork 重载,保留原有分支及 guard。
|
||||
- `mixin/MinecraftServerPayloadMixin.java`:外层 tick 的 drain,不扩展到通用 pollTask。
|
||||
- 已有 ServerManager、LevelTickLoop、Sable 桥、组装兼容、停止入口:接入阶段生命周期。
|
||||
- `tests/PayloadWorkQueueRegression.java`:纯 Java 队列测试。
|
||||
- `tests/payload/PayloadRegression.java`:实际 NeoForge 上下文与多维度集成。
|
||||
- `tests/payload/SimulatedHandleRegression.java`:可选真实模组处理器及约束清理测试。
|
||||
|
||||
## Task 1: 可测试的有界队列
|
||||
|
||||
**Files:** 创建 `src/main/java/dev/modzuozhi/core/compat/PayloadWorkQueue.java`、`tests/PayloadWorkQueueRegression.java`;修改 `build.gradle`。
|
||||
|
||||
**Interfaces:** `PayloadWorkQueue<K>(LongSupplier nanoTime)`;`<T> CompletableFuture<T> submit(K connection, Supplier<T> action)`;`int drainBatch()`;`void close(Throwable reason)`;`int size()`。拒绝返回异常 future;执行前已取消的任务不产生副作用。锁只保护队列及计数,绝不包住 action 或 completion 回调。
|
||||
|
||||
- [ ] 写 `fifoAndExactlyOnce`、`capacityAndCancellation`、`batchCountAndTime`、`closeAndReentrantCompletion` 断言:同连接顺序 0..255;第 257 项拒绝;16 个连接各 256 项填满后第 4097 项拒绝;取消使容量回收;批次最多 64;假时钟到 2_000_000 ns 后不再启动下一项;慢 action 不被中断;close 后旧 future 全结束且不能再接收。
|
||||
- [ ] 添加 `runPayloadWorkQueueRegression` 到 check;先运行并保存失败证据,不能以无测试发现作为 RED。
|
||||
- [ ] 实现上述接口;future 完成回调可重新 submit/close 而无锁死;Supplier 异常只影响本项。
|
||||
- [ ] 运行 `./gradlew.bat runPayloadWorkQueueRegression`,输出 `PAYLOAD QUEUE PASS`;并行提交/取消压力测试核对计数为零。
|
||||
- [ ] 审阅并提交本任务文件。
|
||||
|
||||
## Task 2: 安全阶段与循环生命周期
|
||||
|
||||
**Files:** 创建 `src/main/java/dev/modzuozhi/core/compat/MainThreadPhaseCoordinator.java`、`tests/payload/PayloadRegression.java`;修改 `core/dimthread/ServerManager.java`、`core/dimthread/LevelTickLoop.java`、`core/compat/SableMainThreadBridge.java`(均位于 `src/main/java/dev/modzuozhi/`),修改 `build.gradle`。
|
||||
|
||||
**Interfaces:** coordinator 的 `void run(MinecraftServer server, String reason, Runnable action)`、`boolean owns(MinecraftServer server)`、`boolean blocksWorkerStart(MinecraftServer server)`、`void stopping(MinecraftServer server)`、`void clear(MinecraftServer server)`;Sable 桥新增 `boolean inHandoff(MinecraftServer server)`。ServerManager.runMainThreadPhase 委托 coordinator,保持现有 CC 调用不变。
|
||||
|
||||
- [ ] 写 `exclusivePhase`、`nestedPhase`、`handoffReentryRejected`、`newLoopDuringPhase`、`stopDuringPhase`:处理期间全部世界 owner 为真主线程、worker 写入计数不增加;等待可完成 Sable 回调;独占内嵌套只建一次屏障;handoff 中建全局屏障立即明确失败;新 loop 在阶段退出前不能开跑;预先暂停、菜单暂停、停止状态不被 finally 恢复。
|
||||
- [ ] `./gradlew.bat runServer -PpayloadRegression` 得到预期断言失败,保存日志;夹具另用超时终止测试而非让生产代码越过屏障。
|
||||
- [ ] 实现 IDLE/DRAINING/EXCLUSIVE/RESTORING/STOPPING 状态、租约与入口检查。循环注册、scheduleIfNeeded、resume 共享阶段启动门禁;新 loop 参与退出处理。等待不持锁,继续 pump 必要回调;嵌套 drain 不递归。循环已经在途时等待其子任务及线程字段全部归还。
|
||||
- [ ] 运行夹具输出 `PAYLOAD PHASE PASS`;复跑 ownershipRegression,确认 CC 预置暂停及 Sable 回调仍通过。
|
||||
- [ ] 审阅并提交本任务文件。
|
||||
|
||||
## Task 3: NeoForge 公共入口与 future 语义
|
||||
|
||||
**Files:** 创建 `src/main/java/dev/modzuozhi/core/compat/PayloadMainThreadDispatch.java`、`src/main/java/dev/modzuozhi/mixin/ServerPayloadContextMixin.java`、`src/main/java/dev/modzuozhi/mixin/MinecraftServerPayloadMixin.java`;修改 `tests/payload/PayloadRegression.java`、`src/main/resources/dimensionalripper.mixins.json`。
|
||||
|
||||
**Interfaces:** dispatch 的 `<T> CompletableFuture<T> submit(ServerPlayer player, ResourceLocation payloadId, Supplier<T> action)`、`boolean owns(MinecraftServer server)`、`void drain(MinecraftServer server)`、`void stop(MinecraftServer server)`。Runnable 适配 Supplier<Void>。任务保存实际 connection、player、接收时 ServerLevel 身份及时间;各服务器实例独立。
|
||||
|
||||
- [ ] 用真实 ServerPayloadContext 的 Runnable/Supplier 路径写 `mainPayloadSeesBlockEntity`、`futureContract`、`phaseEscapeAndStalePlayer`、`configurationPassthrough`。断言并发维度下能取得原 BE、true main、至多一次;Supplier 返回 42;异常、取消和 guard 的对外结果与未修改 NeoForge 一致,内部真实失败仍可见;权限拒绝未发生世界变更;配置阶段不调用 player();网络计算原线程不变。
|
||||
- [ ] 运行 payloadRegression 记录原入口 wrong-owner/null 的失败;记录尚未覆盖的真实 Veil 路径,不冒充实测。
|
||||
- [ ] 两个 enqueueWork 仅拦截游戏玩家的显式主线程工作。网络/worker 投递不等待;独占内嵌套立即完成;普通主线程外层进入安全阶段时先遵守同连接已排队顺序;DRAINING 中普通投递只入队;同步 handoff 重入按任务 2 契约明确失败。原 handler、上下文与 NetworkRegistry.guard 保留,不双重执行。
|
||||
- [ ] 在 MinecraftServer.tickServer HEAD 的外层主线程 drain 一批;不在 pollTask 或 worker 回调内 drain。执行前验证连接、player 身份及世界;失效 future 明确结束。无队列不建屏障。
|
||||
- [ ] 加 `workerWaitCycleDetected` 测试:worker 等待队列 future 时测试必须识别无法收口且取消夹具,不误报自动兼容。运行集成输出 `PAYLOAD ENTRY PASS`,覆盖没有并行 loop 的原版情况。
|
||||
- [ ] 审阅并提交本任务文件。
|
||||
|
||||
## Task 4: 原模组交互与清理路径
|
||||
|
||||
**Files:** 修改 `src/main/java/dev/modzuozhi/core/compat/SableAssemblyDispatch.java`、`src/main/java/dev/modzuozhi/mixin/CreateConfigurationPacketMixin.java`、`src/main/java/dev/modzuozhi/mixin/SimulatedAssemblePacketMixin.java`;创建 `tests/payload/SimulatedHandleRegression.java`;修改 `build.gradle`。若真实路径证明需要专用适配,仅新增 `src/main/java/dev/modzuozhi/mixin/JadeRequestDispatchMixin.java` 与 `src/main/java/dev/modzuozhi/mixin/SimulatedHandleLifecycleMixin.java`,并登记可选 mixin。
|
||||
|
||||
**Interfaces:** 保持 `SableAssemblyDispatch.dispatch(ServerPlayer,Runnable)`,改为复用公共阶段/投递,禁止再送进已暂停的维度队列。具体可选模组方法描述符从本地实际 jar 审计后锁定,不能猜注入目标。
|
||||
|
||||
- [ ] 先记录真实 `UpdatePlayerUsingHandlePacket.handle` 的抓取、距离、松手入口、BE 身份、HandleConstraint 移除结果,建立 RED。核对 HandleBlockEntity.checkPlayers 及断线/死亡/卸载清理,不仅测试模拟 Runnable。
|
||||
- [ ] 审计 Veil 注册和 Jade 二次 server.execute;建立逃逸测试。只有已观察的逃逸点使用定点包装,不改写全部 MinecraftServer.execute。旧 Create/组装入口在公共独占内直接调用原 handler;阶段外走相同队列。
|
||||
- [ ] 测试顺序与副作用:抓取一次、更新距离一次、松手一次,最后 constraint 不存在;两个玩家不会互相释放;转速配置、组装拆解保留原权限和距离行为,错误坐标不强加载。
|
||||
- [ ] 覆盖断线、换维度、死亡、把手拆除、sublevel 卸载和单连接 256 满载后松手被拒绝;若原清理不足,可信生命周期适配在旧世界安全阶段释放资源,不重放过期客户端包。过载断开连接调用原 disconnect/cleanup,日志限频。
|
||||
- [ ] 真实运行输出 `SIMULATED HANDLE PASS`、`CREATE CONFIG PASS`、`JADE REQUEST PASS`。原模组依赖或原生物理无法启动时,记录未通过项并暂停“把手已修复”的交付结论,不能用假夹具替代。
|
||||
- [ ] 审阅并提交已验证的适配文件;无必要不创建专用 mixin。
|
||||
|
||||
## Task 5: 停止、可观测性与限频
|
||||
|
||||
**Files:** 创建 `src/main/java/dev/modzuozhi/core/compat/PayloadDiagnostics.java`;修改 `src/main/java/dev/modzuozhi/core/compat/PayloadMainThreadDispatch.java`、`src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java`、`src/main/java/dev/modzuozhi/core/compat/ThreadOwnershipDiagnostics.java`、`src/main/java/dev/modzuozhi/command/MasterCommand.java`、`tests/payload/PayloadRegression.java`。
|
||||
|
||||
**Interfaces:** `PayloadDiagnostics.snapshot(MinecraftServer server): String`,只读且不新建循环;dispatch.stop 拒绝接收、终结待执行 future,阶段退出不恢复停止中的 loop;clear 在清理结束移除服务器引用。
|
||||
|
||||
- [ ] 写 `boundedDiagnostics`、`stopRejectsAndReleases`、`twoServersIsolated`:1000 个不同 ID 最多 128 个具名项加 other;1000 次失败不输出1000份堆栈;停止后无遗留 future/连接/世界引用;某一服务器 stop 不影响另一服务器。
|
||||
- [ ] 运行得到 RED。实现每服务器计数、队列最老等待、最近 32 事件、等待/执行耗时、批次数;异常全局 30 秒限频,快照无包正文。正常/异常停止入口在既有循环停止与保存之前关闭新队列;不要把旧 30 秒停服风险说成已解决。
|
||||
- [ ] status 和 crash report 附快照,区分等待 worker、handler 慢和 Sable handoff;测试所有失败 future 最终完成且 handler 从不重复执行。
|
||||
- [ ] 运行 payloadRegression 输出 `PAYLOAD LIFECYCLE PASS`、`PAYLOAD DIAGNOSTICS PASS`,审阅并提交。
|
||||
|
||||
## Task 6: 兼容验收与候选包
|
||||
|
||||
**Files:** 修改 `README.md`、`gradle.properties`(候选版本 1.1.17);证据 `build/reports/payload/`。
|
||||
|
||||
**Interfaces:** 交付明确列出“源码/字节码审计”“夹具通过”“真实模组通过”的覆盖表,不将它们合并为兼容保证。
|
||||
|
||||
- [ ] 分别运行 `./gradlew.bat check`、`runServer -PpayloadRegression`、`runServer -PownershipRegression -PccTestJar=<已验证本地CC路径>`、`runServer -PinteractionRegression`。逐项确认纯 Java、CC、区块、水流、实体路由和新 PASS 标记,不把预期故障夹具当成功事故。
|
||||
- [ ] 无 Create/Veil/Jade/Simulated 可选模组启动通过;真实模组组合另跑,不自动复制到用户 mods 目录。
|
||||
- [ ] 对比无交互、普通交互、密集抓握、多人查询及慢维度:主线程 TPS、维度完成 TPS、队列延迟、阶段耗时。无交互新增屏障数必须为零;报告慢维度代价,不以 Carpet MSPT 独立判定。
|
||||
- [ ] 正常构建 `./gradlew.bat clean build`,不带任何夹具属性。检查 jar 无 tests/Regression/测试模组类、元数据 1.1.17,记录 SHA256。
|
||||
- [ ] 按 requesting-code-review 技能审查完整差异,修复阻塞问题并重跑受影响测试。README 清楚标注 ServerWarashi 仍未由本计划修复以及真实航空学验收状态;提交本任务。
|
||||
- [ ] 只交付本地候选 jar 和覆盖/风险说明;如真实物理未通过,交付测试候选而非宣称全部修好。不推远端、不部署。
|
||||
|
||||
## 自审结果
|
||||
|
||||
设计第 4 节对应任务 1–3;旧适配、Veil/Jade 与生命周期对应任务 4–5;有界诊断与性能/打包对应任务 5–6。所有五个 Review Focus 已映射到断言。ServerWarashi 明确作为独立后续工作,不以当前方案替代采样栈修复。
|
||||
@@ -0,0 +1,121 @@
|
||||
# 模组网络包的线程归属与安全交接设计
|
||||
|
||||
日期:2026-10-06(Asia/Shanghai)
|
||||
|
||||
状态:用户于 2026-10-06 批准直接实施;公共入口已实现并进行候选验证。完整验收尚有未测项,见 `docs/payload-ownership-1.1.17-validation.md`,不得把本文的目标清单当作全部通过。
|
||||
|
||||
## 1. 用户目标与边界
|
||||
|
||||
修复 Simulated 普通抓握把手无法松手,并覆盖走相同网络基础设施的更多模组交互,而非继续逐个包补丁。维度更新、实体等负载继续并行,要求主线程的处理器仍在真正的服务器主线程执行。允许为了正确性增加可观测的同步等待,但不把并行优化整体关闭。
|
||||
|
||||
首轮目标是 NeoForge 1.21.1 的服务端游戏阶段自定义包及其受支持的同步后续任务。登录、握手、配置阶段、客户端接收和纯网络线程处理不被无条件改派。不能据公共入口适配便宣称全部模组兼容。
|
||||
|
||||
坐服童子 ServerWarashi 的采样崩溃仍是待修复项,但属于独立的采样状态隔离工作,不混入本设计的包调度实现。不能声称网络修复会顺带解决它。
|
||||
|
||||
## 2. 已有证据
|
||||
|
||||
- 1.1.16 在隔离测试中修复了 CC 主线程读取被维度线程接管的世界而得到假空值的路径,已有暂停、收口、恢复和主线程回调处理机制。
|
||||
- 当前仅针对 Create `BlockEntityConfigurationPacket` 和 Simulated `AssemblePacket` 增加了 `SableAssemblyDispatch`,没有覆盖抓握把手更新包。
|
||||
- 本机 Simulated 1.3.0 的 `UpdatePlayerUsingHandlePacket.handle` 先读取方块实体。松手时会先停止玩家握持状态,但只有读到 `HandleBlockEntity` 才调用 `stopGrabbingServer`、释放约束。因此假空值存在“玩家状态清了,物理约束没清”的路径,现场仍须复现验证。
|
||||
- NeoForge `MainThreadPayloadHandler` 通过 `IPayloadContext.enqueueWork` 调用原处理器;`ServerPayloadContext` 同时存在 Runnable 和 Supplier 两种返回 future 的重载。
|
||||
- 本机 Veil 4.2.0 通过 NeoForge 的注册接口注册服务端处理器,公共入口有机会覆盖 Simulated 等使用 Veil 的模组。
|
||||
- 23:43 崩溃报告的新增诊断记录了 Jade 主线程查询与 Create 后台光照查询的归属不匹配。后者不属于网络包入口,必须保留为范围外问题。
|
||||
- ServerWarashi 2.1.1.2 的直接异常是 `PerfWorkTracker.claimSlot` 下标 -1。其共享采样栈不隔离主线程、维度线程,这是另一条需要复现的兼容路径。
|
||||
|
||||
## 3. 选定方案与取舍
|
||||
|
||||
采用公共入口适配、保守的全维度安全阶段,以及可审计的单维度例外机制。
|
||||
|
||||
对比:逐包白名单同步开销较小,但容易漏掉新的模组;直接允许任意线程读取世界仅消除检查,不能消除数据竞争。两者都不作为默认方案。
|
||||
|
||||
第一版新公共入口默认全维度隔离,不凭“玩家当前维度”或包内坐标猜测整个处理器的访问范围。已有单维度补丁必须审计其调用链后决定保留或归入公共阶段,不能因已存在就视为线程安全。以后缩小隔离范围须增加对应测试,不自动学习白名单。
|
||||
|
||||
代价:有交互负载时,主线程可能等待最慢的在途维度更新;批处理只能摊薄交接开销,不能保证忙维度绝不拖累交互。无待处理包时不建立新的安全屏障。
|
||||
|
||||
## 4. 组件职责
|
||||
|
||||
### 4.1 入口适配层
|
||||
|
||||
识别服务端、游戏阶段、有效玩家连接和原始 payload ID。拦截 NeoForge 主线程处理器的调度边界,保留原处理器及其上下文,不重写权限、距离、游戏规则和业务逻辑。审计 `ServerPayloadContext.enqueueWork` 两个重载,明确区分首次投递、阶段内嵌套任务和网络处理器主动提交的主线程工作。
|
||||
|
||||
每项任务携带服务器实例、连接身份、玩家身份、接收时世界身份、payload ID、入队时间和完成结果。禁止保存或输出包正文、聊天文本及认证数据作为诊断。
|
||||
|
||||
普通网络线程处理器的计算仍在原线程;它显式提交的世界主线程工作才进入交接。注册/注销通道、登录任务完成等路径照旧,不能在配置阶段调用只能用于游戏玩家的接口。
|
||||
|
||||
### 4.2 待处理队列
|
||||
|
||||
每个服务器独立托管,连接内按公共入口观察到的投递顺序执行,单项任务至多执行一次。不得按 payload 类型重排或合并抓取、松手和配置操作。不同来源的 vanilla 与 mod 包不承诺原本不存在的全局顺序,必须测试握持流程实际依赖的顺序。
|
||||
|
||||
初始批次上限为 64 项或 2 ms 的处理时间,达到任一条件后不启动下一项;不强行中断已开始的处理器。服务器队列上限 4096 项、单连接上限 256 项。满载时拒绝新任务、结束其 future 并限频报告,不能静默丢弃,不能无限堆积。连接过载应明确断开并走原有清理逻辑;把手清理须纳入验收,不能留下永久约束。
|
||||
|
||||
### 4.3 安全阶段执行器
|
||||
|
||||
由真正的服务器线程在不位于同步 worker→main 回调内的外层调度点取批次。请求暂停维度续排,等待已开始的维度工作及其子任务完全收口;等待期间继续服务使这些工作完成所需的主线程回调。禁止持有包队列锁、模组锁或世界锁等待。
|
||||
|
||||
取得全部所需世界的独占执行权后执行本批次。退出时只恢复本次暂停的循环,保留菜单暂停、停服和外层阶段的状态。异常也必须释放阶段,完成 future,不重复执行已经产生副作用的处理器。
|
||||
|
||||
阶段状态至少区分空闲、等待收口、独占执行、恢复。等待期间收到的普通包只排队,不能递归建立另一个全局阶段。独占执行期间的同服务器嵌套同步任务复用现有阶段。
|
||||
|
||||
服务器启动的未接管世界沿用原版执行权;动态维度创建/移除及停服必须与阶段生命周期协调,不能在独占阶段中偷偷启动新的维度写入者。超时用于诊断,不是允许并发执行的理由。
|
||||
|
||||
### 4.4 Future、异常与重入契约
|
||||
|
||||
保留 Runnable/Supplier 的结果、异常、取消语义;审计并保留 NeoForge 原有 `NetworkRegistry.guard` 对外语义,同时内部保留真实失败原因用于诊断。不能把未执行任务报告为成功,也不能统一把失败值伪造为 null。
|
||||
|
||||
阶段内同步调用应立即完成,避免原处理器 join 自己的排队任务。普通主线程外层调用可进入安全阶段后同步执行;同步 Sable 回调内尚未持有全局执行权的任务不得阻塞等待自己的维度归还所有权。此类不支持的同步重入必须明确失败并记录调用路径,不能以泛化修复之名引入死锁;对应模组须使用经验证的专门适配。
|
||||
|
||||
阶段外 worker/网络线程投递立即返回 future,不等待主线程。若上游工作线程本身阻塞等待这个 future,会与全局收口形成环路:不得宣称该路径自动兼容,测试必须检出并作为未支持调用契约报告。不可通过放开读取守卫掩盖。
|
||||
|
||||
## 5. 模组覆盖与旧补丁迁移
|
||||
|
||||
优先验收以下路径,但每条分别记录“已审计入口、已模拟验证、已实际模组验证”,不混为一项结论:
|
||||
|
||||
| 路径 | 验收目标 |
|
||||
| --- | --- |
|
||||
| NeoForge MAIN 自定义包 | 真主线程、世界独占、原处理器至多一次 |
|
||||
| Veil / Simulated | 抓取、滚轮距离、松手、组装与拆解 |
|
||||
| Create 配置 | 转速设置与权限/距离检查保留 |
|
||||
| Jade 查询 | 返回正确方块实体数据,追踪再次提交任务的路径 |
|
||||
| 非 MAIN 处理器的 enqueueWork | 原网络计算不迁移,显式同步工作安全交接 |
|
||||
|
||||
公共阶段已持有执行权时,旧 Create/组装适配必须直接复用它,不能再次排到已暂停的维度队列。旧补丁的移除需有相应通过测试;不存在的可选模组不得导致启动注入失败。
|
||||
|
||||
模组通过裸 `server.execute`、自建线程或其他框架再次提交的工作,不因首次包进入公共阶段就自动受保护。首轮对以上已知目标审计这些逃逸点,必要时增加范围明确的适配。禁止全局替换所有服务器任务的执行语义。
|
||||
|
||||
## 6. 玩家与世界生命周期
|
||||
|
||||
执行前重新核对连接仍有效、玩家未被替换、世界与接收时一致。过期的普通交互不对新世界重放,其 future 明确结束,原因计入诊断。
|
||||
|
||||
抓握释放不能只靠取消过期包:需要验证原模组在断线、换维度、死亡、把手拆除及子世界卸载时的约束清理。若原清理不完整,增加针对握持资源的可信生命周期适配,在旧世界安全执行;不能将过期客户端包改写坐标后重新执行。业务清理与通用排队失效策略分别测试。
|
||||
|
||||
服务器停止时停止接收新任务,尚未执行的 future 以取消/失败结束,正在执行者完成安全收尾;清空服务器持有的连接与世界引用。不能在保存开始后重启循环或继续应用包。
|
||||
|
||||
## 7. 诊断与性能验收
|
||||
|
||||
新增按 payload ID 的有界聚合:接收数、完成数、失败/过期/拒绝数、待处理数、最老等待时间、阶段等待/执行耗时、批次数。记录上限 128 个 ID,超出归入 other,避免恶意 ID 撑大统计。异常日志全局限频,保留最近有限条事件,绝不按包持续打印堆栈。
|
||||
|
||||
状态输出与崩溃报告要能区分:网络队列积压、等待维度收口、处理器慢、Sable 回调等待。沿用 1.1.16 的归属快照,不将快照中的 queued 单独称为死锁。
|
||||
|
||||
比较空载、普通交互、密集抓取/松手、多人查询和一个慢维度时的主线程 TPS、各维度实际完成 TPS、排队延迟及阶段耗时。报告回退代价,不以低 Carpet MSPT 判断成功。
|
||||
|
||||
## 8. 必须通过的验证
|
||||
|
||||
1. 先复现普通把手的失败路径:证明松手包到达、读到的对象和约束释放结果;测试不能只证明假包调用成功。
|
||||
2. 公共入口在维度工作进行中执行 handler 时仍取得正确对象、真实主线程和独占权;修复前应失败。
|
||||
3. 真实的抓取→距离修改→松手严格按投递顺序且仅一次执行。包含高频交互、不同玩家、掉线/换维度和子世界卸载。
|
||||
4. 权限拒绝、超距、错误坐标仍按原处理器拒绝,不造成强加载或越权操作。
|
||||
5. Runnable/Supplier 结果、异常、取消、嵌套 enqueue、等待中回调和非法同步重入都有测试;任何失败不能让 future 永远悬挂。
|
||||
6. 多维度、预先暂停、异常恢复、动态维度生命周期及停服时不发生并发写入、错误恢复或残留任务。
|
||||
7. 队列容量、批次预算和限频实际生效;松手不会被合并或被静默丢弃。
|
||||
8. 未安装 Create/Veil/Jade 等可选模组可启动;现有 CC、区块读取、水流、交互和纯 Java 调度回归全部运行。
|
||||
9. 验证发布 jar 不包含夹具和测试模组。完整整合包只使用存档副本,不修改正式服务器、启动器和模组目录。
|
||||
|
||||
真实航空学依赖无法加载或无法可靠自动化时,明确保留该验收项未通过,不将公共入口夹具等同于真实物理验收。
|
||||
|
||||
## 9. 本轮交付与后续独立工作
|
||||
|
||||
本文批准后编写实现计划;计划审阅后实施入口、执行器、适配及测试。交付候选 jar、覆盖清单、验证证据与未解决项。不自动上传远端或部署正式服务器。
|
||||
|
||||
ServerWarashi 随后的独立修复必须从其真实采样调用链建立失败测试,决定线程过滤或按线程分离状态时说明报告覆盖范围;不能只夹紧数组下标,也不能把仅主线程采样伪称全维度采样。
|
||||
|
||||
C2ME 随机源注入冲突、Create 后台光照读取、通用异步世界访问及既有停服超时行为不因本设计而宣称解决。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 随机刻与服务器 tick 回调归属修复
|
||||
|
||||
日期:2026-10-13(Asia/Shanghai)
|
||||
|
||||
状态:用户已选择先修复随机刻/邻居通知链和服务器全局 tick 回调。本文为待确认的书面设计;尚未实施代码,也未生成新 jar。
|
||||
|
||||
基线:`763c015`,DimensionalRipper 1.1.17,工作分支 `codex/payload-ownership`。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
修复日志中两条已经定位的错误路径,同时保留其他现有并行能力:
|
||||
|
||||
1. 同一世界的不同区块并行随机刻,经同步邻居通知进入 DriveByWire 共享信号网络,触发 `WireNetworkManager.setSource:240` 的 `HashMap.computeIfAbsent` CME。
|
||||
2. Lootr 在主线程 `ServerTickEvent.Post` 扫描/转换容器,而维度 worker 仍持有世界归属,导致方块实体读取被守卫拒绝。
|
||||
|
||||
用户选择的是以上两项,不包括坐服童子采样修复。不修改用户正式服务器、启动器 mods、原存档,不自动推送远端。
|
||||
|
||||
验收目标是正确执行而非仅不抛异常:随机刻、信号、箱子转换没有丢失或重复;等待期间 Sable 交接能结束;安全阶段不能错误恢复暂停或停止中的循环。
|
||||
|
||||
## 2. 已核对的证据
|
||||
|
||||
- 用户 2026-10-13 日志加载 DimensionalRipper 1.1.17、NeoForge 21.1.233、DriveByWire 0.3.0、Lootr 1.21.1-1.11.37.120。
|
||||
- 三次 CME 分别在 11:02:10、11:02:17、11:02:23;事件总线和 ChunkEnv 各打印一份,不把六份堆栈计作六次独立异常。
|
||||
- 当前 `ServerChunkCacheMixin` 将 `level.tickChunk` 收集成分片,使用按区块锁后调用自定义 `IChunkEnvTick`。这些锁不保护同一世界的 DriveByWire 网络管理器。
|
||||
- 本机同版本 DriveByWire 字节码显示 `sourceValues` 是普通 HashMap,`setSource` 的计算函数只新建内部 HashMap。并发写入是强证据支持的原因;尚未捕获用户环境中另一条写线程,不能宣称已有完整双线程现场复现。
|
||||
- 本机 Lootr 的 `HandleTick.onServerTick(ServerTickEvent.Post)` 直接调用 `BlockEntityTicker.onServerTick(server)`,遍历不同维度的 ticker;不经过公共收包入口。
|
||||
- 本地 NeoForge 21.1.244 的 `MinecraftServer.tickServer` 在 `tickChildren` 前调用 `fireServerTickPre`,在方法末尾调用 `fireServerTickPost`。用户 21.1.233 必须另外核对注入及运行兼容,不能仅凭版本范围宣称已测试。
|
||||
|
||||
## 3. 选择的方案
|
||||
|
||||
不选仅替换 DriveByWire 集合:嵌套集合及“读取—更新—通知”仍是复合操作,且无法覆盖其他邻居监听器。
|
||||
|
||||
不选关闭所有并行:这是应急措施,会扩大性能回退范围。
|
||||
|
||||
采用两项边界调整,共用已有世界执行权协调器。第一项减少维度内不安全并行;第二项让明确的主线程事件边界与维度写入互斥。它不是任意模组线程安全的通用保证。
|
||||
|
||||
### 3.1 区块环境 tick 恢复所属维度内串行
|
||||
|
||||
- 当前 `tickChunks` 中每次 `level.tickChunk(chunk, randomTickSpeed)` 恢复直接调用原方法,不再投递区块环境子任务或分片。
|
||||
- 调用在该世界合法 owner 上执行:维度并行启用时是所属维度 worker,原版路径则为服务器主线程。不是把每个区块同步投递到主线程。
|
||||
- 沿用原 tickChunks 选中的区块、顺序和随机刻速度;同一已选区块只执行一次。由原方法及其模组注入处理随机源、降水、闪电、方块和流体随机刻,不继续绕过原方法手工复刻。
|
||||
- 方块更新与同步邻居通知保留在同一调用链,不独立排队、不合并、不延后一拍,不调整 `randomTickSpeed`。
|
||||
- 不允许 `fine on` 或 `fine sharded on` 重新开启旧区块环境并行。第一版不保留“默认关闭但可误开启”的不安全开关;现有实体/方块实体等其他并行开关保持原功能。
|
||||
- 既有实体/方块实体子任务必须先按原屏障归还执行权,不能与本阶段重叠;实施测试需验证,不以线程名称替代归属检查。
|
||||
- 修改 README/status 的区块环境并行说明,不能继续宣称此阶段仍分片并行。历史累计指标如保留,必须明确不是当前并行工作量。
|
||||
|
||||
范围限制:这一改动消除已知的同世界并行随机刻来源,不代表所有实体、方块实体、异步模组或跨世界共享状态已安全。若真实 DriveByWire 回归暴露其他写入来源,先报告证据并重新确认范围,不能仅压住 CME。
|
||||
|
||||
### 3.2 服务器全局 Pre/Post 回调取得执行权
|
||||
|
||||
- 只包装 `MinecraftServer.tickServer` 对 `EventHooks.fireServerTickPre` 与 `fireServerTickPost` 的两个原调用点,保留同一 server、BooleanSupplier、原位置及原事件总线顺序,原调用恰好执行一次。
|
||||
- 复用 `MainThreadPhaseCoordinator.run`:禁止维度续排,等待全部在途及退出中循环完成;期间继续处理使 Sable 交接结束所需的主线程工作;随后进入真实主线程独占阶段执行原事件回调。
|
||||
- 不包住整个 tickServer,不把两个事件之间全部主线程工作都纳入长期独占,也不移动事件的顺序。
|
||||
- 不修改全局 `EventBus.post`、通用 `MinecraftServer.execute` 或 `pollTask`;不把 `LevelTickEvent.Pre/Post` 搬到主线程。因而坐服童子的维度事件采样异常仍可能出现。
|
||||
- 不能只看总开关判断“无并行”;存在尚未归还执行权的循环时仍须等待。没有并行循环的原版路径应保留直接原调用,不创建或启动新循环。
|
||||
- 独占内 CC 或 payload 嵌套使用既有租约,不递归建立屏障。禁止从正在被维度 worker 同步等待的 Sable 回调创建全局阶段,避免主线程与该 worker 互等。
|
||||
- 等待不能持有第三方模组锁;超时只做限频诊断,不能授权与 worker 并发写世界。
|
||||
- 异常保持原对象和传播语义,不吞异常、不重试整个事件、不重放已执行监听器。finally 正确释放本次租约,但不能解除菜单暂停或复活停止状态;阶段内新建循环仍受约束。
|
||||
|
||||
全局事件每次进入都需要安全条件,不能根据猜测“这 tick Lootr 没任务”跳过保护;也不为单个模组过滤事件监听器。
|
||||
|
||||
## 4. 性能与可观测性
|
||||
|
||||
- 第一项取消区块环境细粒度并行;第二项在服务器每 tick 的 Pre/Post 两个边界收口,主线程可能等待最慢维度。因此不再承诺“一个慢维度绝不影响主线程和其他维度”。两次安全阶段之外仍允许原有维度及其他阶段并行。
|
||||
- 不承诺固定开销比例、20 TPS 或性能提升。先保证正确性,再测量收益;如果安全回退代价不可接受,应基于实测重新设计,而不是放松互斥。
|
||||
- Pre 与 Post 分开记录次数、最近等待/执行时间、累计或最大耗时;使用有界数据,不按监听器或每 tick 打堆栈。
|
||||
- 本地原版实现先记录部分 tick 耗时再执行 Post,故 Carpet/原版 MSPT 可能未完整涵盖新增 Post 等待。必须同时查看主线程实际 TPS、维度完成 TPS 和 Pre/Post 独立耗时,不能用低 MSPT 证明不卡。
|
||||
- 不把本次扩展成通用采样器或故障保护重写。
|
||||
|
||||
## 5. 必须验证的项目
|
||||
|
||||
先建立能失败的回归,再改变生产行为:
|
||||
|
||||
1. 两个以上区块的真实随机刻及邻居监听器,在旧版本记录并行重叠,新版本无同世界重叠且不运行在细粒度子任务上。核对原 `tickChunk` 注入仍被调用、选中区块至多一次、邻居通知同步完成。
|
||||
2. 实际 DriveByWire 0.3.0 对多个位置的信号更新测试,核对最终信号值及连接状态,不能仅测一个自建 HashMap 或等待偶然 CME。
|
||||
3. 注册真实 ServerTickEvent.Pre/Post 测试监听器;旧版本在 worker 持有世界时不能合法读取,新版本看到原 BE、真正主线程、全部循环不在途。核对事件顺序和执行次数。
|
||||
4. 真实 Lootr 扫描/转换的测试容器,核对预期转换结果及内容/战利品表,而非仅注册假监听器。检查未加载位置不会被额外强加载。
|
||||
5. 等待中的 Sable 回调、嵌套 CC/payload、异常、菜单暂停、动态/退出中的循环、停服状态均不引入死锁、重复回调或错误恢复。
|
||||
6. 无 DriveByWire/Lootr/Sable 时可启动;分别验证细粒度 on/off、分片 on/off 都不会重开区块环境并行。重复单人世界进入/退出,检查静态诊断引用释放。
|
||||
7. 重跑已有队列、屏障、CC、payload、实际航空学把手、Create/Jade、水流、区块与实体路由回归。
|
||||
8. 比较空载、随机刻密集、Lootr 转换负载及一个人为慢维度:报告实际 TPS、阶段等待/执行耗时及随机刻进度,不只看 Carpet MSPT。
|
||||
9. Java 21 干净构建;校验 jar 元数据、无测试夹具及可选依赖打包,记录 SHA256。运行日志必须具备 PASS 标记且没有非预期服务器异常,不能只看 Gradle 退出码。
|
||||
|
||||
真实模组无法运行或完整测试矩阵未通过时,明确保留未验收项目,只能交付对应范围的测试候选,不能宣称两项已全面解决。坐服童子测试应独立,不能让其已知异常混淆这两项因果验证。
|
||||
|
||||
## 6. 不在本次范围与交付流程
|
||||
|
||||
不修 ServerWarashi 采样栈,不宣称修复任意异步世界访问、其他实体/方块实体并行、跨维度共享集合、C2ME、既有停服超时或所有主线程世界访问。保护两个 tick 事件不等于保护所有主线程指令、函数和第三方后台任务。
|
||||
|
||||
用户确认本文后再编写具体实施计划及失败测试拆分,确认计划和执行方式后实施。交付本地候选 jar、测试证据、回退代价及未解决项;不部署或推送。
|
||||
|
||||
## 自审
|
||||
|
||||
设计与“先修 1 和 2”的范围一致。随机刻选择维度 owner 串行而非 worker 同步请求全局阶段,避免自等待;全局事件仅保护两个明确入口。性能描述明确承认两次全局等待破坏完全维度隔离;验收区分假监听器与真实模组。未将坐服童子或其他已有问题纳入修复承诺。
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
|
||||
# This is required to provide enough memory for the Minecraft decompilation process.
|
||||
org.gradle.java.home=C:\\Program Files\\Java\\jdk-21
|
||||
org.gradle.jvmargs=-Xmx3G
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
@@ -37,7 +36,7 @@ mod_name=Dimensional Ripper
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=MIT
|
||||
# The mod version. See https://semver.org/
|
||||
mod_version=1.1.2
|
||||
mod_version=1.1.17
|
||||
# The group ID for the mod. It is only important when publishing build artifacts to Maven.
|
||||
mod_group_id=dev.modzuozhi
|
||||
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,12 +3,12 @@ package dev.modzuozhi.command;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
import dev.modzuozhi.core.compat.SableMainThreadBridge;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.dimthread.EntityTickParallel;
|
||||
import dev.modzuozhi.core.dimthread.FaultGuard;
|
||||
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
||||
import dev.modzuozhi.core.dimthread.ShardGate;
|
||||
import dev.modzuozhi.core.dimthread.TpsTracker;
|
||||
import dev.modzuozhi.core.filter.SerDesFilter;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.arguments.ResourceLocationArgument;
|
||||
@@ -37,6 +37,7 @@ public final class MasterCommand {
|
||||
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(
|
||||
literal("dimensionalripper")
|
||||
.then(literal("flowstatus").executes(MasterCommand::flowStatus))
|
||||
.then(literal("status").executes(MasterCommand::status))
|
||||
.then(literal("fine")
|
||||
.then(literal("on").executes(ctx -> setFine(ctx, true)))
|
||||
@@ -50,6 +51,9 @@ public final class MasterCommand {
|
||||
.then(literal("tewhitelist")
|
||||
.then(net.minecraft.commands.Commands.argument("type", net.minecraft.commands.arguments.ResourceLocationArgument.id())
|
||||
.executes(MasterCommand::teWhitelist)))
|
||||
.then(literal("teunwhitelist")
|
||||
.then(net.minecraft.commands.Commands.argument("type", net.minecraft.commands.arguments.ResourceLocationArgument.id())
|
||||
.executes(MasterCommand::teUnwhitelist)))
|
||||
.then(literal("status").executes(MasterCommand::status)));
|
||||
}
|
||||
|
||||
@@ -64,12 +68,8 @@ public final class MasterCommand {
|
||||
ModZuozhi.LOGGER.info("[ModZuozhi] 全局随 /dimensionalripper fine on 自动恢复为开启");
|
||||
}
|
||||
FineGrainScheduler.setEnabled(enabled);
|
||||
if (!enabled) {
|
||||
// 关闭细粒度后必须先排空 SUB_POOL 残留子任务:此时维度 loop 已退化为串行 tick,
|
||||
// 残留实体子任务会与它并发改 EntitySection(靠 isLockDisabled 的残留保护仍持锁互斥,
|
||||
// 见 EntityTickParallel),等排空后锁才回落到零开销纯串行。
|
||||
FineGrainScheduler.awaitIdle(5000L);
|
||||
}
|
||||
// Barriers drain each current phase; keep the server event loop available
|
||||
// for any in-flight task that depends on a server callback.
|
||||
if (enabled) {
|
||||
FaultGuard.reset();
|
||||
}
|
||||
@@ -103,6 +103,49 @@ public final class MasterCommand {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 将指定 TE 注册名移出 modded 并行白名单(恢复该 modded TE 默认的强制串行)。 */
|
||||
private static int teUnwhitelist(CommandContext<CommandSourceStack> ctx) {
|
||||
String type = ResourceLocationArgument.getId(ctx, "type").toString();
|
||||
boolean removed = SerDesFilter.unwhitelistModded(type);
|
||||
ctx.getSource().sendSuccess(() ->
|
||||
Component.literal("TE 白名单已" + (removed ? "移除" : "移除(原本不在白名单中)") + ": " + type
|
||||
+ "(清单: " + SerDesFilter.moddedWhitelist() + ")"), false);
|
||||
ModZuozhi.LOGGER.info("[ModZuozhi] TE 白名单移除 {}", type);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int flowStatus(CommandContext<CommandSourceStack> ctx) {
|
||||
var player = ctx.getSource().getPlayer();
|
||||
if (player == null) {
|
||||
ctx.getSource().sendFailure(Component.literal("请由玩家站在不流动的水附近执行。"));
|
||||
return 0;
|
||||
}
|
||||
dev.modzuozhi.core.compat.SableAssemblyDispatch.dispatch(player, () -> {
|
||||
var level = player.serverLevel();
|
||||
var hit = player.pick(8.0, 0.0F, true);
|
||||
var pos = hit instanceof net.minecraft.world.phys.BlockHitResult blockHit
|
||||
&& hit.getType() == net.minecraft.world.phys.HitResult.Type.BLOCK
|
||||
? blockHit.getBlockPos() : player.blockPosition();
|
||||
var targetChunk = new net.minecraft.world.level.ChunkPos(pos);
|
||||
long chunk = targetChunk.toLong();
|
||||
String message = "水流诊断:维度=" + level.dimension().location()
|
||||
+ ",目标=" + pos.toShortString() + ",区块=" + targetChunk
|
||||
+ ",世界时间=" + level.getGameTime()
|
||||
+ ",正常刻=" + level.tickRateManager().runsNormally()
|
||||
+ ",调试世界=" + level.isDebug()
|
||||
+ ",区块可更新=" + level.getChunkSource().isPositionTicking(chunk)
|
||||
+ ",实体已加载=" + level.areEntitiesLoaded(chunk)
|
||||
+ ",目标流体=" + level.getFluidState(pos).getType()
|
||||
+ ",待处理流体刻=" + level.getFluidTicks().count()
|
||||
+ ",待处理方块刻=" + level.getBlockTicks().count();
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(message), false);
|
||||
String detail = ((dev.modzuozhi.core.compat.ScheduledTickDiagnostics) level.getFluidTicks())
|
||||
.dimensionalripper$describeTicks(pos, level.getGameTime());
|
||||
ctx.getSource().sendSuccess(() -> Component.literal("流体调度明细:" + detail), false);
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int status(CommandContext<CommandSourceStack> ctx) {
|
||||
String fine = FineGrainScheduler.isEnabled() ? "开启" : "关闭";
|
||||
String noCollide = ModZuozhi.MOB_COLLISION_OFF ? "开启" : "关闭";
|
||||
@@ -112,25 +155,58 @@ public final class MasterCommand {
|
||||
|
||||
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,故障原因:%s\n"
|
||||
+ "当前维度:%s,TPS:%.1f,掉拍分片累计:实体 %d / TE %d / 区块环境 %d",
|
||||
+ "兼容状态:%s\n"
|
||||
+ "当前维度:%s,掉拍分片累计:实体 %d / TE %d / 区块环境 %d",
|
||||
fine, fineMark,
|
||||
EntityTickParallel.isSharded() ? "开启" : "关闭",
|
||||
noCollide, reason,
|
||||
dimensionName(dimension), tps,
|
||||
SableMainThreadBridge.statusReason(),
|
||||
dimensionName(dimension),
|
||||
EntityTickParallel.missedShards(), ShardGate.teMissed(), ShardGate.chunkMissed());
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(msg), false);
|
||||
ctx.getSource().sendSuccess(() -> Component.literal("线程交接:"
|
||||
+ dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics.timing()), false);
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(
|
||||
dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(ctx.getSource().getServer())), false);
|
||||
var loops = DimThreadCore.MANAGER.loopsSnapshot();
|
||||
if (loops.isEmpty()) {
|
||||
ctx.getSource().sendSuccess(() -> Component.literal("维度循环:无数据(尚未启动或已停止),不按 20 TPS 显示。"), false);
|
||||
} else {
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(
|
||||
"维度实测:完成TPS按最近10秒计数(启动不足10秒用已观测时间,含暂停时间);"
|
||||
+ "工作MSPT为最近100次均值/最近一次,包含交接等待,不含末尾区块队列轮询。"), false);
|
||||
long now = System.nanoTime();
|
||||
loops.entrySet().stream().sorted(java.util.Comparator.comparing(e -> e.getKey().dimension().location().toString()))
|
||||
.forEach(entry -> {
|
||||
var loop = entry.getValue();
|
||||
var p = loop.progress(now);
|
||||
String state = !loop.isRunning() ? "已停止" : loop.isPaused()
|
||||
? (loop.isTicking() ? "暂停收口中" : "已暂停")
|
||||
: !loop.isScheduled() ? "未启动"
|
||||
: p.windowSeconds() == 0 ? "无采样"
|
||||
: p.runningMs() >= 1000 ? "当前Tick超过1秒"
|
||||
: p.sinceCompletedMs() >= 1000 ? "超过1秒无完成"
|
||||
: Double.isNaN(p.sinceCompletedMs()) ? "尚无成功完成" : "运行中";
|
||||
String line = String.format(java.util.Locale.ROOT,
|
||||
"%s [%s] 完成TPS=%s (窗口%.1fs),工作MSPT(均/末)=%s/%s ms,当前Tick=%s ms,距上次完成=%s ms,失败Tick=%d",
|
||||
entry.getKey().dimension().location(), state,
|
||||
loop.isPaused() || !loop.isRunning() ? "—" : metric(p.tps()),
|
||||
p.windowSeconds(),
|
||||
metric(p.averageWorkMs()), metric(p.lastWorkMs()),
|
||||
Double.isNaN(p.runningMs()) ? "未执行" : metric(p.runningMs()),
|
||||
Double.isNaN(p.sinceCompletedMs()) ? "无记录" : metric(p.sinceCompletedMs()), p.failures());
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(line), false);
|
||||
});
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static String metric(double value) {
|
||||
return Double.isNaN(value) ? "无数据" : String.format(java.util.Locale.ROOT, "%.1f", value);
|
||||
}
|
||||
|
||||
/** 把维度 key 映射成玩家可读的中文名。 */
|
||||
private static String dimensionName(ResourceKey<Level> dimension) {
|
||||
return switch (dimension.location().getPath()) {
|
||||
@@ -140,4 +216,4 @@ public final class MasterCommand {
|
||||
default -> dimension.location().toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.dimthread.LevelTickLoop;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Separate compatibility lease; ordinary pause/resume cannot release it. */
|
||||
public final class MainThreadPhaseCoordinator {
|
||||
private enum Phase { IDLE, DRAINING, EXCLUSIVE, RESTORING, STOPPING }
|
||||
private static final class State { volatile Phase phase = Phase.IDLE; }
|
||||
private static final ConcurrentHashMap<MinecraftServer, State> STATES = new ConcurrentHashMap<>();
|
||||
private MainThreadPhaseCoordinator() {}
|
||||
private static State state(MinecraftServer server) { return STATES.computeIfAbsent(server, s -> new State()); }
|
||||
|
||||
public static boolean owns(MinecraftServer server) {
|
||||
State s = STATES.get(server);
|
||||
return server.isSameThread() && s != null && s.phase == Phase.EXCLUSIVE;
|
||||
}
|
||||
public static boolean blocksWorkerStart(MinecraftServer server) {
|
||||
State s = STATES.get(server);
|
||||
return s != null && s.phase != Phase.IDLE;
|
||||
}
|
||||
public static String phase(MinecraftServer server) {
|
||||
State s = STATES.get(server); return s == null ? "IDLE" : s.phase.name();
|
||||
}
|
||||
/** Serializes loop registration with taking/releasing all leases; never waits for workers. */
|
||||
public static LevelTickLoop register(MinecraftServer server, Supplier<LevelTickLoop> registration) {
|
||||
State s = state(server);
|
||||
synchronized (s) {
|
||||
LevelTickLoop loop = registration.get();
|
||||
if (s.phase != Phase.IDLE || PayloadMainThreadDispatch.closed(server)) loop.holdForMainThread(true);
|
||||
return loop;
|
||||
}
|
||||
}
|
||||
/** Pause changes and loop creation use the same short lock; never hold it while draining. */
|
||||
public static void updatePause(MinecraftServer server, Runnable change) {
|
||||
synchronized(state(server)){change.run();}
|
||||
}
|
||||
public static void run(MinecraftServer server, String reason, Runnable action) {
|
||||
if (!server.isSameThread()) throw new IllegalStateException("Compatibility phase requires server thread");
|
||||
State s = state(server);
|
||||
if (owns(server)) { action.run(); return; }
|
||||
if (SableMainThreadBridge.inHandoff(server))
|
||||
throw new IllegalStateException("Global phase inside synchronous Sable handoff would deadlock: " + reason);
|
||||
synchronized (s) {
|
||||
if (s.phase != Phase.IDLE) throw new IllegalStateException("Reentrant global phase during " + s.phase);
|
||||
s.phase = Phase.DRAINING;
|
||||
loops(server).forEach(loop -> loop.holdForMainThread(true));
|
||||
}
|
||||
long began = System.nanoTime(), drained = began;
|
||||
try {
|
||||
server.managedBlock(() -> {
|
||||
boolean idle = loops(server).stream().noneMatch(LevelTickLoop::isTicking);
|
||||
if (!idle) ThreadOwnershipDiagnostics.slowPhase(server, reason, began);
|
||||
return idle;
|
||||
});
|
||||
synchronized (s) {
|
||||
if (s.phase == Phase.STOPPING) throw new IllegalStateException("Server stopping during compatibility phase");
|
||||
s.phase = Phase.EXCLUSIVE;
|
||||
}
|
||||
drained = System.nanoTime();
|
||||
action.run();
|
||||
} finally {
|
||||
ThreadOwnershipDiagnostics.phaseTiming(reason, began, drained);
|
||||
synchronized (s) {
|
||||
if (s.phase != Phase.STOPPING) {
|
||||
s.phase = Phase.RESTORING;
|
||||
loops(server).forEach(loop -> loop.holdForMainThread(false));
|
||||
s.phase = Phase.IDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static java.util.List<LevelTickLoop> loops(MinecraftServer server) {
|
||||
return DimThreadCore.MANAGER.ownershipLoopsSnapshot().stream()
|
||||
.filter(loop -> loop.getLevel().getServer() == server).toList();
|
||||
}
|
||||
public static void stopping(MinecraftServer server) {
|
||||
State s = state(server);
|
||||
synchronized (s) { s.phase = Phase.STOPPING; loops(server).forEach(loop -> loop.holdForMainThread(true)); }
|
||||
}
|
||||
public static void clear(MinecraftServer server) { STATES.remove(server); }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Lets the owning event loop service callbacks while waiting for work on another thread. */
|
||||
public final class ManagedFutureAwait {
|
||||
private ManagedFutureAwait() {}
|
||||
|
||||
/** enqueue must always queue (e.g. BlockableEventLoop.tell), never run inline. */
|
||||
public static <T> T callQueued(Supplier<T> work, Consumer<Runnable> enqueue,
|
||||
Consumer<BooleanSupplier> managedBlock) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
enqueue.accept(() -> {
|
||||
try { future.complete(work.get()); }
|
||||
catch (Throwable failure) { future.completeExceptionally(failure); }
|
||||
});
|
||||
return join(future, managedBlock);
|
||||
}
|
||||
|
||||
public static <T> T join(CompletableFuture<T> future, Consumer<BooleanSupplier> managedBlock) {
|
||||
if (!future.isDone()) {
|
||||
managedBlock.accept(future::isDone);
|
||||
}
|
||||
// Preserve exceptional completion and cancellation semantics.
|
||||
return future.join();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
/** Shared pause policy, independent of client classes so it can be regression-tested. */
|
||||
public final class PauseOwnership {
|
||||
private PauseOwnership() {}
|
||||
|
||||
public static boolean shouldPause(boolean menuPaused, boolean connected, boolean hasLevel, boolean hasPlayer) {
|
||||
return menuPaused && connected && hasLevel && hasPlayer;
|
||||
}
|
||||
|
||||
public static boolean workerOwns(boolean scheduled, boolean paused, boolean ticking) {
|
||||
// A paused in-flight tick still owns the level until its finally restores the fields.
|
||||
return scheduled && (!paused || ticking);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** Bounded metadata only; never records payload bodies or exception messages containing user data. */
|
||||
public final class PayloadDiagnostics {
|
||||
private static final ConcurrentHashMap<MinecraftServer, Stats> STATS = new ConcurrentHashMap<>();
|
||||
private static final AtomicLong NEXT_WARNING = new AtomicLong();
|
||||
public record Ticket(String label, long started) {}
|
||||
private static final class Stats {
|
||||
final Map<String,long[]> ids = new LinkedHashMap<>();
|
||||
// received, completed, handler failure, cancelled, expired, capacity rejected
|
||||
final long[] other = new long[6];
|
||||
final Map<Ticket,Boolean> pending = new IdentityHashMap<>();
|
||||
final ArrayDeque<String> recent = new ArrayDeque<>();
|
||||
long batches; double waitMs, actionMs;
|
||||
}
|
||||
private PayloadDiagnostics() {}
|
||||
public static Ticket begin(MinecraftServer server, String id) {
|
||||
synchronized(PayloadMainThreadDispatch.LIFECYCLE) {
|
||||
if(PayloadMainThreadDispatch.closed(server))return null;
|
||||
Stats s = STATS.computeIfAbsent(server, key -> new Stats());
|
||||
synchronized (s) {
|
||||
String label = s.ids.containsKey(id) || s.ids.size()<128 ? id : "other";
|
||||
long[] values = label.equals("other") ? s.other : s.ids.computeIfAbsent(label, k -> new long[6]);
|
||||
values[0]++;
|
||||
Ticket ticket = new Ticket(label,System.nanoTime()); s.pending.put(ticket,Boolean.TRUE); return ticket;
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void end(MinecraftServer server, Ticket ticket, Throwable error) {
|
||||
if(ticket==null)return;
|
||||
Stats s=STATS.get(server); if(s==null) return;
|
||||
synchronized(s) {
|
||||
if(s.pending.remove(ticket)==null) return;
|
||||
long[] values=ticket.label.equals("other") ? s.other : s.ids.get(ticket.label);
|
||||
int outcome = error == null ? 1
|
||||
: PayloadMainThreadDispatch.expired(error) ? 4
|
||||
: error instanceof PayloadWorkQueue.CapacityExceeded ? 5
|
||||
: error instanceof java.util.concurrent.CancellationException ? 3 : 2;
|
||||
values[outcome]++;
|
||||
if(error!=null) {
|
||||
if(s.recent.size()==32) s.recent.removeFirst();
|
||||
s.recent.addLast(ticket.label+":"+error.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
if(error!=null) warn(ticket.label,error);
|
||||
}
|
||||
private static void warn(String id, Throwable error) {
|
||||
long now=System.nanoTime(),next=NEXT_WARNING.get();
|
||||
if((next==0 || now-next>=0) && NEXT_WARNING.compareAndSet(next,now+TimeUnit.SECONDS.toNanos(30)))
|
||||
ModZuozhi.LOGGER.warn("[PayloadOwnership] Task {} ended with {}; further failures aggregated for 30s",id,error.getClass().getSimpleName());
|
||||
}
|
||||
public static void batch(MinecraftServer server,long start,long acquired) {
|
||||
Stats s=STATS.get(server);if(s==null)return;
|
||||
synchronized(s){s.batches++;s.waitMs=(acquired-start)/1_000_000.0;s.actionMs=(System.nanoTime()-acquired)/1_000_000.0;}
|
||||
}
|
||||
public static String snapshot(MinecraftServer server) {
|
||||
Stats s=STATS.get(server);if(s==null)return "payloads: pending=0 batches=0";
|
||||
synchronized(s) {
|
||||
long oldest=s.pending.keySet().stream().mapToLong(Ticket::started).min().orElse(System.nanoTime());
|
||||
long received=s.other[0],completed=s.other[1],failed=s.other[2],cancelled=s.other[3],expired=s.other[4],rejected=s.other[5];
|
||||
for(long[] v:s.ids.values()){received+=v[0];completed+=v[1];failed+=v[2];cancelled+=v[3];expired+=v[4];rejected+=v[5];}
|
||||
return "payloads: phase="+MainThreadPhaseCoordinator.phase(server)+" received="+received+" completed="+completed
|
||||
+" failed="+failed+" cancelled="+cancelled+" expired="+expired+" rejected="+rejected
|
||||
+" pending="+s.pending.size()+" oldestMs="+(System.nanoTime()-oldest)/1_000_000.0
|
||||
+" batches="+s.batches+" waitMs="+s.waitMs+" actionMs="+s.actionMs+" ids="+s.ids.size()+" other="+Arrays.toString(s.other)+" recent="+s.recent;
|
||||
}
|
||||
}
|
||||
public static void clear(MinecraftServer server){STATS.remove(server);}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Shared game payload handoff. No Minecraft world access is authorized merely by thread name. */
|
||||
public final class PayloadMainThreadDispatch {
|
||||
static final Object LIFECYCLE = new Object();
|
||||
private static final class StalePayload extends CancellationException {
|
||||
StalePayload(String reason){super(reason);}
|
||||
}
|
||||
private static final ConcurrentHashMap<MinecraftServer, PayloadWorkQueue<Connection>> QUEUES = new ConcurrentHashMap<>();
|
||||
private static final java.util.Map<MinecraftServer,Boolean> STOPPED = new java.util.WeakHashMap<>();
|
||||
private static final ConcurrentHashMap<MinecraftServer, ConcurrentHashMap<Connection,ServerPlayer>> OVERLOADED = new ConcurrentHashMap<>();
|
||||
private PayloadMainThreadDispatch() {}
|
||||
|
||||
public static boolean owns(MinecraftServer server) { return MainThreadPhaseCoordinator.owns(server); }
|
||||
static boolean closed(MinecraftServer server) { synchronized(LIFECYCLE){return STOPPED.containsKey(server)||server.isStopped();} }
|
||||
static boolean expired(Throwable error) { return error instanceof StalePayload; }
|
||||
|
||||
public static <T> CompletableFuture<T> submit(ServerPlayer player, ResourceLocation payloadId, Supplier<T> action) {
|
||||
var level = player.serverLevel();
|
||||
var server = level.getServer();
|
||||
var listener = player.connection;
|
||||
if (listener == null || player.hasDisconnected() || player.isRemoved() || closed(server))
|
||||
return CompletableFuture.failedFuture(new StalePayload("Stale payload player"));
|
||||
if (owns(server)) return CompletableFuture.completedFuture(action.get());
|
||||
if (server.isSameThread() && SableMainThreadBridge.inHandoff(server))
|
||||
throw new IllegalStateException("Payload " + payloadId + " requested global ownership from synchronous Sable callback");
|
||||
PayloadWorkQueue<Connection> queue;
|
||||
synchronized(LIFECYCLE) {
|
||||
if(closed(server)) return CompletableFuture.failedFuture(new StalePayload("Server stopping"));
|
||||
queue = QUEUES.computeIfAbsent(server, s -> new PayloadWorkQueue<>(System::nanoTime));
|
||||
}
|
||||
var ticket=PayloadDiagnostics.begin(server,payloadId.toString());
|
||||
CompletableFuture<T> result = queue.submit(listener.getConnection(), () -> {
|
||||
if (player.connection != listener || listener.player != player || player.hasDisconnected()
|
||||
|| player.isRemoved() || player.serverLevel() != level)
|
||||
throw new StalePayload("Payload player/world changed before execution");
|
||||
try{return action.get();}
|
||||
catch(CancellationException handlerFailure){throw new CompletionException(handlerFailure);}
|
||||
});
|
||||
result.whenComplete((value,error) -> {
|
||||
PayloadDiagnostics.end(server,ticket,error);
|
||||
if(error instanceof PayloadWorkQueue.CapacityExceeded) synchronized(LIFECYCLE) {
|
||||
if(!closed(server)) OVERLOADED.computeIfAbsent(server,s -> new ConcurrentHashMap<>()).putIfAbsent(listener.getConnection(),player);
|
||||
}
|
||||
});
|
||||
// Preserve synchronous main-thread enqueueWork semantics and observed FIFO order.
|
||||
// Each acquisition still obeys the per-batch budget. Never drain recursively while pumping callbacks.
|
||||
if (server.isSameThread() && !MainThreadPhaseCoordinator.blocksWorkerStart(server)) {
|
||||
while (!result.isDone()) drain(server);
|
||||
try { result.join(); }
|
||||
catch (CompletionException failed) {
|
||||
if (failed.getCause() instanceof RuntimeException runtime) throw runtime;
|
||||
if (failed.getCause() instanceof Error error) throw error;
|
||||
throw failed;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public static void drain(MinecraftServer server) {
|
||||
if (!server.isSameThread()) throw new IllegalStateException("Payload drain requires server thread");
|
||||
var queue = QUEUES.get(server);
|
||||
var overloaded=OVERLOADED.get(server);
|
||||
if ((queue == null || queue.size() == 0) && (overloaded==null || overloaded.isEmpty())) return;
|
||||
if (MainThreadPhaseCoordinator.blocksWorkerStart(server)
|
||||
|| SableMainThreadBridge.inHandoff(server)) return;
|
||||
long start=System.nanoTime();
|
||||
MainThreadPhaseCoordinator.run(server, "custom payloads", () -> {
|
||||
long acquired=System.nanoTime();
|
||||
try {
|
||||
// Cleanup goes through the original disconnect lifecycle with world ownership.
|
||||
// A cleanup-only batch avoids bypassing the payload batch count/time budget.
|
||||
if(overloaded!=null && !overloaded.isEmpty()) {
|
||||
int count=0;
|
||||
for(var entry:overloaded.entrySet()) {
|
||||
if(count++>=64 || System.nanoTime()-acquired>=2_000_000L) break;
|
||||
if(overloaded.remove(entry.getKey(),entry.getValue())) {
|
||||
var player=entry.getValue();
|
||||
if(player.connection!=null && player.connection.getConnection()==entry.getKey() && !player.hasDisconnected())
|
||||
player.connection.disconnect(net.minecraft.network.chat.Component.literal("Too many pending mod interactions; please reconnect."));
|
||||
}
|
||||
}
|
||||
} else if(queue!=null) queue.drainBatch();
|
||||
} finally {PayloadDiagnostics.batch(server,start,acquired);}
|
||||
});
|
||||
}
|
||||
public static void stop(MinecraftServer server) {
|
||||
PayloadWorkQueue<Connection> queue;
|
||||
synchronized(LIFECYCLE){STOPPED.put(server,Boolean.TRUE);queue=QUEUES.get(server);OVERLOADED.remove(server);}
|
||||
if(queue!=null)queue.close(new StalePayload("Server stopping"));
|
||||
MainThreadPhaseCoordinator.stopping(server);
|
||||
}
|
||||
public static void clear(MinecraftServer server) {
|
||||
synchronized(LIFECYCLE){QUEUES.remove(server); OVERLOADED.remove(server); PayloadDiagnostics.clear(server);}
|
||||
MainThreadPhaseCoordinator.clear(server);
|
||||
}
|
||||
/** NeoForge guard's null-on-handler-error contract, with bounded diagnostics already attached to raw result. */
|
||||
public static <T> CompletableFuture<T> guard(CompletableFuture<T> result) {
|
||||
CompletableFuture<T> guarded=result.handle((value,error) -> {
|
||||
// Preserve NeoForge's handler-error result, not false success for queue policy failures.
|
||||
if(error instanceof StalePayload || error instanceof PayloadWorkQueue.CapacityExceeded || result.isCancelled())
|
||||
throw new CompletionException(error);
|
||||
return error==null ? value : null;
|
||||
});
|
||||
guarded.whenComplete((value,error) -> {if(guarded.isCancelled())result.cancel(false);});
|
||||
return guarded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Bounded FIFO. Queue locks never encompass handlers or future completion callbacks. */
|
||||
public final class PayloadWorkQueue<K> {
|
||||
public static final class CapacityExceeded extends RejectedExecutionException {
|
||||
private CapacityExceeded() { super("Payload queue capacity exceeded"); }
|
||||
}
|
||||
private final LongSupplier clock;
|
||||
private final LinkedHashSet<Entry<?>> pending = new LinkedHashSet<>();
|
||||
private final Map<K, Integer> counts = new HashMap<>();
|
||||
private Throwable closed;
|
||||
private boolean draining;
|
||||
|
||||
public PayloadWorkQueue(LongSupplier nanoTime) { clock = Objects.requireNonNull(nanoTime); }
|
||||
|
||||
public <T> CompletableFuture<T> submit(K connection, Supplier<T> action) {
|
||||
Objects.requireNonNull(connection);
|
||||
Objects.requireNonNull(action);
|
||||
Entry<T> entry = new Entry<>(connection, action);
|
||||
Throwable rejected;
|
||||
synchronized (this) {
|
||||
rejected = closed;
|
||||
if (rejected == null && (pending.size() >= 4096 || counts.getOrDefault(connection, 0) >= 256))
|
||||
rejected = new CapacityExceeded();
|
||||
if (rejected == null) {
|
||||
pending.add(entry);
|
||||
counts.merge(connection, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
if (rejected != null) entry.result.completeExceptionally(rejected);
|
||||
else entry.result.whenComplete((value, error) -> {
|
||||
if (entry.result.isCancelled()) synchronized (this) { remove(entry); }
|
||||
});
|
||||
return entry.result;
|
||||
}
|
||||
|
||||
private void remove(Entry<?> entry) {
|
||||
if (pending.remove(entry)) counts.compute(entry.connection, (key, count) -> count == 1 ? null : count - 1);
|
||||
}
|
||||
|
||||
/** Single consumer; recursive/concurrent drains are no-ops. Already running work is never interrupted. */
|
||||
public int drainBatch() {
|
||||
synchronized (this) {
|
||||
if (draining || closed != null) return 0;
|
||||
draining = true;
|
||||
}
|
||||
int ran = 0;
|
||||
long start = clock.getAsLong();
|
||||
try {
|
||||
while (ran < 64 && clock.getAsLong() - start < 2_000_000L) {
|
||||
Entry<?> entry;
|
||||
synchronized (this) {
|
||||
if (closed != null || pending.isEmpty()) break;
|
||||
entry = pending.iterator().next();
|
||||
remove(entry);
|
||||
}
|
||||
entry.run();
|
||||
ran++;
|
||||
}
|
||||
return ran;
|
||||
} finally { synchronized (this) { draining = false; } }
|
||||
}
|
||||
|
||||
public void close(Throwable reason) {
|
||||
Objects.requireNonNull(reason);
|
||||
List<Entry<?>> abandoned;
|
||||
synchronized (this) {
|
||||
if (closed != null) return;
|
||||
closed = reason;
|
||||
abandoned = new ArrayList<>(pending);
|
||||
pending.clear(); counts.clear();
|
||||
}
|
||||
abandoned.forEach(entry -> entry.result.completeExceptionally(reason));
|
||||
}
|
||||
|
||||
public synchronized int size() { return pending.size(); }
|
||||
|
||||
private final class Entry<T> {
|
||||
final K connection;
|
||||
final Supplier<T> action;
|
||||
final CompletableFuture<T> result = new CompletableFuture<>();
|
||||
Entry(K connection, Supplier<T> action) { this.connection = connection; this.action = action; }
|
||||
void run() {
|
||||
if (result.isCancelled()) return;
|
||||
try { result.complete(action.get()); }
|
||||
catch (Throwable error) { result.completeExceptionally(error); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Legacy Create/assembly callers share the generic payload phase; no second dimension queue. */
|
||||
public final class SableAssemblyDispatch {
|
||||
private SableAssemblyDispatch() {}
|
||||
|
||||
public static void dispatch(ServerPlayer player, Runnable action) {
|
||||
if (player.hasDisconnected() || player.isRemoved()) return;
|
||||
PayloadMainThreadDispatch.submit(player, ResourceLocation.fromNamespaceAndPath("dimensionalripper", "legacy_interaction"),
|
||||
() -> { action.run(); return null; });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.dimthread.FaultGuard;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.neoforged.fml.ModList;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Routes Sable's sub-level/physics tick back to the real Minecraft server thread.
|
||||
*
|
||||
* <p>Dimensional Ripper normally runs {@code ServerLevel.tick} on a dimension worker and swaps the
|
||||
* two vanilla thread-owner fields for the duration. Sable retains additional main-thread-bound
|
||||
* sub-level and native physics state, so running its container tick on that worker violates Sable's
|
||||
* threading contract. The Sable compatibility mixin calls this bridge at the start of the parent
|
||||
* level tick. The dimension worker waits while only the Sable segment runs on the server thread;
|
||||
* after it completes, normal dimension and fine-grained parallelism continues unchanged.</p>
|
||||
*/
|
||||
public final class SableMainThreadBridge {
|
||||
private static final long TIMEOUT_SECONDS = 10L;
|
||||
private static final ThreadLocal<ServerLevel> HANDED_OFF_LEVEL = new ThreadLocal<>();
|
||||
|
||||
public static boolean ownsHandoff(ServerLevel level) {
|
||||
return HANDED_OFF_LEVEL.get() == level;
|
||||
}
|
||||
public static boolean inHandoff(MinecraftServer server) {
|
||||
ServerLevel level = HANDED_OFF_LEVEL.get();
|
||||
return level != null && level.getServer() == server;
|
||||
}
|
||||
private static final ConcurrentHashMap<Class<?>, Method> GET_LEVEL_METHODS = new ConcurrentHashMap<>();
|
||||
|
||||
private SableMainThreadBridge() {
|
||||
}
|
||||
|
||||
public static boolean isSableLoaded() {
|
||||
return ModList.get().isLoaded("sable");
|
||||
}
|
||||
|
||||
public static String statusReason() {
|
||||
String reason = isSableLoaded()
|
||||
? "航空学/Sable 物理 Tick 已桥接至服务器主线程"
|
||||
: "未加载航空学/Sable";
|
||||
return ModList.get().isLoaded("lithium")
|
||||
? reason + ";Lithium 村民 POI 兼容:村民留在维度线程,其余负载按并行开关运行"
|
||||
: reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one Sable container tick with vanilla server-thread ownership.
|
||||
*
|
||||
* @param container Sable's ServerSubLevelContainer (kept as Object so Sable stays optional)
|
||||
* @param tick original container tick
|
||||
*/
|
||||
public static void run(Object container, Runnable tick) {
|
||||
ServerLevel level = getLevel(container);
|
||||
runForLevel(level, tick);
|
||||
}
|
||||
|
||||
/** Synchronously hands off a phase while its owning dimension worker waits. */
|
||||
public static void runForLevel(ServerLevel level, Runnable tick) {
|
||||
MinecraftServer server = level.getServer();
|
||||
if (server.isSameThread()) {
|
||||
tick.run();
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Void> completion = new CompletableFuture<>();
|
||||
// 0 = queued, 1 = running, 2 = cancelled before start.
|
||||
AtomicInteger state = new AtomicInteger();
|
||||
ThreadOwnershipDiagnostics.handoff(level, "queued");
|
||||
server.execute(() -> {
|
||||
// A timed-out queued task must not execute later, detached from its waiting level tick.
|
||||
if (!state.compareAndSet(0, 1)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ThreadOwnershipDiagnostics.handoff(level, "running");
|
||||
// The parent dimension worker temporarily owns both vanilla thread fields while
|
||||
// it is paused here. Restore real server-thread ownership for the complete Sable
|
||||
// segment, then hand ownership back before waking the worker.
|
||||
ServerLevel previous = HANDED_OFF_LEVEL.get();
|
||||
HANDED_OFF_LEVEL.set(level);
|
||||
try {
|
||||
DimThreadCore.swapThreadsAndRun(tick, level, level.getChunkSource());
|
||||
} finally {
|
||||
if (previous == null) HANDED_OFF_LEVEL.remove(); else HANDED_OFF_LEVEL.set(previous);
|
||||
}
|
||||
ThreadOwnershipDiagnostics.handoff(level, "completed-owner-restored");
|
||||
completion.complete(null);
|
||||
} catch (Throwable throwable) {
|
||||
ThreadOwnershipDiagnostics.handoff(level, "failed-owner-restored: " + throwable.getClass().getSimpleName());
|
||||
completion.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
completion.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException interrupted) {
|
||||
if (state.compareAndSet(0, 2)) {
|
||||
ThreadOwnershipDiagnostics.handoff(level, "cancelled-interrupted-before-start");
|
||||
completion.cancel(false);
|
||||
} else {
|
||||
// A running main-thread phase must finish before world ownership is released.
|
||||
completion.handle((result, failure) -> null).join();
|
||||
}
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for Sable main-thread physics tick", interrupted);
|
||||
} catch (TimeoutException timeout) {
|
||||
if (state.compareAndSet(0, 2)) {
|
||||
ThreadOwnershipDiagnostics.handoff(level, "cancelled-timeout-before-start");
|
||||
completion.cancel(false);
|
||||
ModZuozhi.LOGGER.error(
|
||||
"[Compatibility] Sable 物理 Tick 排队超过 {} 秒且尚未开始,交由故障保护降级",
|
||||
TIMEOUT_SECONDS);
|
||||
FaultGuard.onBarrierTimeout();
|
||||
throw new IllegalStateException("Timed out queueing Sable main-thread physics tick", timeout);
|
||||
}
|
||||
// The server thread is already inside native physics. Resuming/falling back here would
|
||||
// race the same world state; wait for that in-flight step to finish instead.
|
||||
ModZuozhi.LOGGER.warn(
|
||||
"[Compatibility] Sable 主线程物理 Tick 已运行超过 {} 秒;为避免世界并发写入,继续等待其安全结束",
|
||||
TIMEOUT_SECONDS);
|
||||
ThreadOwnershipDiagnostics.handoff(level, "running-over-10s");
|
||||
awaitRunningTick(completion);
|
||||
} catch (ExecutionException failed) {
|
||||
Throwable cause = failed.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Sable main-thread physics tick failed", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitRunningTick(CompletableFuture<Void> completion) {
|
||||
try {
|
||||
completion.get();
|
||||
} catch (InterruptedException interrupted) {
|
||||
completion.handle((result, failure) -> null).join();
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted during an in-flight Sable physics tick", interrupted);
|
||||
} catch (ExecutionException failed) {
|
||||
Throwable cause = failed.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Sable main-thread physics tick failed", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerLevel getLevel(Object container) {
|
||||
try {
|
||||
Method method = GET_LEVEL_METHODS.computeIfAbsent(container.getClass(), type -> {
|
||||
try {
|
||||
return type.getMethod("getLevel");
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("Unsupported Sable container: " + type.getName(), exception);
|
||||
}
|
||||
});
|
||||
Object level = method.invoke(container);
|
||||
if (level instanceof ServerLevel serverLevel) {
|
||||
return serverLevel;
|
||||
}
|
||||
throw new IllegalStateException("Sable container did not return a ServerLevel");
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("Unable to read Sable container level", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
public interface ScheduledTickDiagnostics {
|
||||
String dimensionalripper$describeTicks(BlockPos pos, long gameTime);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Counts a whole submission phase, including gaps between successive submissions. */
|
||||
public final class TaskBarrier {
|
||||
private int pending;
|
||||
private boolean sealed;
|
||||
|
||||
public synchronized void register() {
|
||||
if (sealed) throw new IllegalStateException("Submission after barrier was sealed");
|
||||
pending++;
|
||||
}
|
||||
|
||||
public synchronized void complete() {
|
||||
if (pending == 0) throw new IllegalStateException("Unbalanced task completion");
|
||||
if (--pending == 0) notifyAll();
|
||||
}
|
||||
|
||||
public synchronized int pending() {
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** A timeout is diagnostic, never permission to resume while tasks still own world state. */
|
||||
public void await(long warningMillis, Runnable onSlow) {
|
||||
boolean interrupted = false;
|
||||
long interval = TimeUnit.MILLISECONDS.toNanos(warningMillis);
|
||||
long nextWarning = System.nanoTime() + interval;
|
||||
try {
|
||||
while (true) {
|
||||
synchronized (this) {
|
||||
sealed = true;
|
||||
if (pending == 0) return;
|
||||
long remaining = nextWarning - System.nanoTime();
|
||||
if (remaining > 0) {
|
||||
try {
|
||||
TimeUnit.NANOSECONDS.timedWait(this, remaining);
|
||||
} catch (InterruptedException ignored) {
|
||||
interrupted = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
onSlow.run();
|
||||
nextWarning = System.nanoTime() + Math.max(interval, TimeUnit.SECONDS.toNanos(30));
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.dimthread.IMutableMainThread;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** Bounded, read-only evidence: never load chunks or query live block-entity collections here. */
|
||||
public final class ThreadOwnershipDiagnostics {
|
||||
private static final ConcurrentHashMap<ServerLevel, Trace> TRACES = new ConcurrentHashMap<>();
|
||||
private static final AtomicLong NEXT_SLOW_WARNING = new AtomicLong();
|
||||
private static volatile String lastTiming = "no CC ownership phase sampled";
|
||||
private ThreadOwnershipDiagnostics() {}
|
||||
|
||||
private static final class Trace {
|
||||
String phase = "unobserved";
|
||||
String handoff = "none";
|
||||
final ArrayDeque<String> recent = new ArrayDeque<>();
|
||||
long rejected;
|
||||
long nextWarning;
|
||||
String lastRejected = "none";
|
||||
void add(String entry) {
|
||||
if (recent.size() == 16) recent.removeFirst();
|
||||
recent.addLast(System.nanoTime() + "ns " + entry);
|
||||
}
|
||||
}
|
||||
|
||||
public static void phase(ServerLevel level, String phase) {
|
||||
Trace trace = TRACES.computeIfAbsent(level, key -> new Trace());
|
||||
synchronized (trace) { trace.phase = phase; trace.add(phase + " caller=" + thread(Thread.currentThread())); }
|
||||
}
|
||||
|
||||
public static void handoff(ServerLevel level, String state) {
|
||||
Trace trace = TRACES.computeIfAbsent(level, key -> new Trace());
|
||||
synchronized (trace) { trace.handoff = state; trace.add("handoff=" + state + " caller=" + thread(Thread.currentThread())); }
|
||||
}
|
||||
|
||||
public static void rejectedLookup(ServerLevel level, BlockPos pos, Thread observedOwner) {
|
||||
Trace trace = TRACES.computeIfAbsent(level, key -> new Trace());
|
||||
long now = System.nanoTime();
|
||||
synchronized (trace) {
|
||||
trace.rejected++;
|
||||
if (trace.nextWarning != 0 && now - trace.nextWarning < 0) return;
|
||||
trace.nextWarning = now + TimeUnit.SECONDS.toNanos(30);
|
||||
String callers = StackWalker.getInstance().walk(frames -> frames
|
||||
.filter(frame -> !frame.getClassName().equals(ThreadOwnershipDiagnostics.class.getName()))
|
||||
.limit(8).map(Object::toString).collect(java.util.stream.Collectors.joining(" <- ")));
|
||||
trace.lastRejected = "pos=" + pos.toShortString() + " caller=" + thread(Thread.currentThread())
|
||||
+ " observedOwner=" + thread(observedOwner) + " at=" + callers;
|
||||
trace.add("rejected " + trace.lastRejected);
|
||||
ModZuozhi.LOGGER.warn("[ThreadOwnership] getBlockEntity owner mismatch at entry (guard may return null): {}", describe(level));
|
||||
}
|
||||
}
|
||||
|
||||
public static String describe(ServerLevel level) {
|
||||
Trace trace = TRACES.get(level);
|
||||
String owners = "dimension=" + level.dimension().location()
|
||||
+ " levelOwner=" + thread(((IMutableMainThread) level).dimThreads$getMainThread())
|
||||
+ " chunkOwner=" + thread(((IMutableMainThread) level.getChunkSource()).dimThreads$getMainThread());
|
||||
if (trace == null) return owners + " phase=unobserved";
|
||||
synchronized (trace) {
|
||||
return owners + " phase=" + trace.phase + " handoff=" + trace.handoff
|
||||
+ " rejected=" + trace.rejected + " lastRejected={" + trace.lastRejected + "} recent=" + trace.recent;
|
||||
}
|
||||
}
|
||||
|
||||
public static String report(MinecraftServer server) {
|
||||
StringBuilder out = new StringBuilder("Snapshot (not an atomic world view); observer=")
|
||||
.append(thread(Thread.currentThread())).append(';').append(lastTiming);
|
||||
DimThreadCore.MANAGER.loopsSnapshot().forEach((level, loop) -> {
|
||||
if (level.getServer() == server) out.append('\n').append(describe(level))
|
||||
.append(" ticking=").append(loop.isTicking()).append(" paused=").append(loop.isPaused())
|
||||
.append(" running=").append(loop.isRunning());
|
||||
});
|
||||
return out.append('\n').append(PayloadDiagnostics.snapshot(server)).toString();
|
||||
}
|
||||
|
||||
public static void slowPhase(MinecraftServer server, String reason, long began) {
|
||||
long now = System.nanoTime();
|
||||
if (now - began < TimeUnit.SECONDS.toNanos(1)) return;
|
||||
long next = NEXT_SLOW_WARNING.get();
|
||||
if ((next == 0 || now - next >= 0) && NEXT_SLOW_WARNING.compareAndSet(next, now + TimeUnit.SECONDS.toNanos(30)))
|
||||
ModZuozhi.LOGGER.warn("[ThreadOwnership] {} waiting {} ms; {}", reason, (now-began)/1_000_000, report(server));
|
||||
}
|
||||
|
||||
public static void phaseTiming(String reason, long began, long drained) {
|
||||
lastTiming = reason + " lastWaitMs=" + (drained-began)/1_000_000.0
|
||||
+ " lastActionMs=" + (System.nanoTime()-drained)/1_000_000.0;
|
||||
}
|
||||
public static String timing() { return lastTiming; }
|
||||
public static String thread(Thread thread) {
|
||||
return thread == null ? "null" : thread.getName() + "#" + thread.threadId() + '/' + thread.getState();
|
||||
}
|
||||
public static void clear() { TRACES.clear(); NEXT_SLOW_WARNING.set(0); lastTiming = "no CC ownership phase sampled"; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package dev.modzuozhi.core.compat;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Bounded 10-second completion-rate window, independent of Minecraft for deterministic tests. */
|
||||
public final class TickProgress {
|
||||
private static final long BUCKET_NS = 100_000_000L;
|
||||
private static final int BUCKETS = 100;
|
||||
private final long[] epochs = new long[BUCKETS], completions = new long[BUCKETS];
|
||||
private final long[] work = new long[100];
|
||||
private long observed = Long.MIN_VALUE, started = Long.MIN_VALUE, lastCompleted = Long.MIN_VALUE;
|
||||
private long failures, lastWork;
|
||||
private int workIndex, workCount;
|
||||
|
||||
public TickProgress() { Arrays.fill(epochs, Long.MIN_VALUE); }
|
||||
|
||||
public synchronized void begin(long now) {
|
||||
if (observed == Long.MIN_VALUE) observed = now;
|
||||
started = now;
|
||||
}
|
||||
|
||||
public synchronized void finish(long now, long workNanos, boolean success) {
|
||||
lastWork = Math.max(0, workNanos);
|
||||
work[workIndex] = lastWork;
|
||||
workIndex = (workIndex + 1) % work.length;
|
||||
workCount = Math.min(work.length, workCount + 1);
|
||||
started = Long.MIN_VALUE;
|
||||
if (!success) { failures++; return; }
|
||||
lastCompleted = now;
|
||||
long epoch = Math.floorDiv(now, BUCKET_NS);
|
||||
int slot = Math.floorMod(epoch, BUCKETS);
|
||||
if (epochs[slot] != epoch) { epochs[slot] = epoch; completions[slot] = 0; }
|
||||
completions[slot]++;
|
||||
}
|
||||
|
||||
public synchronized Snapshot snapshot(long now) {
|
||||
long epoch = Math.floorDiv(now, BUCKET_NS), count = 0, total = 0;
|
||||
for (int i = 0; i < BUCKETS; i++)
|
||||
if (epochs[i] <= epoch && epochs[i] > epoch - BUCKETS) count += completions[i];
|
||||
for (int i = 0; i < workCount; i++) total += work[i];
|
||||
double seconds = observed == Long.MIN_VALUE ? 0 : Math.min(10, Math.max(0, now - observed) / 1e9);
|
||||
return new Snapshot(seconds == 0 ? Double.NaN : count / seconds,
|
||||
workCount == 0 ? Double.NaN : total / (double) workCount / 1e6,
|
||||
workCount == 0 ? Double.NaN : lastWork / 1e6,
|
||||
started == Long.MIN_VALUE ? Double.NaN : Math.max(0, now - started) / 1e6,
|
||||
lastCompleted == Long.MIN_VALUE ? Double.NaN : Math.max(0, now - lastCompleted) / 1e6,
|
||||
failures, seconds);
|
||||
}
|
||||
|
||||
public record Snapshot(double tps, double averageWorkMs, double lastWorkMs,
|
||||
double runningMs, double sinceCompletedMs, long failures, double windowSeconds) {}
|
||||
}
|
||||
@@ -69,10 +69,9 @@ public final class DimThreadCore {
|
||||
/**
|
||||
* 单机内嵌服务器暂停/恢复信号(见 {@link ServerManager#setPaused})。
|
||||
* <p>
|
||||
* 由客户端 mixin {@code MixinMinecraftPauseDetector} 在每帧 {@code Minecraft.runTick} 检测
|
||||
* {@code isPaused()} 变化后调用;dedicated server 无客户端,永不会触发。
|
||||
* 由 {@code IntegratedServerPauseMixin} 在服务器线程同步;加载界面不触发暂停。
|
||||
*/
|
||||
public static void setPaused(boolean paused) {
|
||||
MANAGER.setPaused(paused);
|
||||
public static void setPaused(MinecraftServer server, boolean paused) {
|
||||
MANAGER.setPaused(server, paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,9 @@ public final class EntityTickParallel {
|
||||
* 在两次调用间翻转而对未持有的锁 unlock。
|
||||
*/
|
||||
public static boolean isLockDisabled() {
|
||||
return !FineGrainScheduler.isEnabled() && FineGrainScheduler.activeSubTasks() == 0;
|
||||
// A phase created before fine-off may still submit work after a temporary zero
|
||||
// pending count. Keep content locks active across mode transitions as well.
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 当前线程的实体屏障(worker 端);无则 null。 */
|
||||
@@ -237,7 +239,7 @@ public final class EntityTickParallel {
|
||||
|
||||
/** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */
|
||||
public static void begin() {
|
||||
CURRENT.set(FineGrainScheduler.beginBarrier());
|
||||
CURRENT.set(FineGrainScheduler.beginBarrier("entity"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,8 +55,8 @@ public final class FaultGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/** 上报维度 tick 异常;达阈值自动回退原版串行。 */
|
||||
public static void onDimTickFailure(Throwable t) {
|
||||
/** Report repeated dimension failures without abandoning outstanding chunk requests. */
|
||||
public static synchronized void onDimTickFailure(Throwable t) {
|
||||
if (dimDegraded) {
|
||||
return;
|
||||
}
|
||||
@@ -64,32 +64,34 @@ public final class FaultGuard {
|
||||
ModZuozhi.LOGGER.error("[FaultGuard] 维度 tick 异常(第 {} 次)", dimFailures, t);
|
||||
if (dimFailures >= DIM_FAULT_THRESHOLD) {
|
||||
dimDegraded = true;
|
||||
reason = "维度 tick 连续异常 " + dimFailures + " 次,已自动回退原版串行";
|
||||
fineDegraded = true;
|
||||
reason = "维度 tick 连续异常 " + dimFailures + " 次,关闭后续细粒度;保留维度所有权和队列,禁止强制切回主线程";
|
||||
ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
|
||||
// 关闭顺序与 setMaster 一致:先关细粒度并排空残留子任务(此时 ENABLED 仍 true,
|
||||
// section 锁与碰撞拦截仍生效,残留任务安全跑完),再关全局,最后熄灭维度 loop
|
||||
// (否则主线程退回原版串行 tick 时仍会经 mixin 提交并行子任务 → 双 tick/CME 竞态)。
|
||||
// Killing the loops here strands queued chunk futures and races other levels.
|
||||
// Leave dimension ownership intact; a global handoff requires a separate protocol.
|
||||
FineGrainScheduler.setEnabled(false);
|
||||
FineGrainScheduler.awaitIdle(5000L);
|
||||
ModZuozhi.ENABLED = false;
|
||||
DimThreadCore.MANAGER.killAllLoops();
|
||||
}
|
||||
}
|
||||
|
||||
/** 上报子任务屏障等待超时(子任务卡死/死锁):立即降级,避免维度 worker 永久阻塞导致整服冻结。 */
|
||||
public static void onBarrierTimeout() {
|
||||
if (dimDegraded) {
|
||||
public static synchronized void onBarrierTimeout() {
|
||||
if (fineDegraded) {
|
||||
return;
|
||||
}
|
||||
dimDegraded = true;
|
||||
reason = "子任务屏障等待超时(疑似死锁),已自动回退原版串行";
|
||||
fineDegraded = true;
|
||||
reason = "子任务等待超过 10 秒,关闭后续细粒度并行;当前任务继续收口,保留维度线程所有权";
|
||||
ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
|
||||
// Disabling a switch cannot release locks or stop a running task. The owning
|
||||
// barrier must finish before the next phase; never hand this world to the server here.
|
||||
FineGrainScheduler.setEnabled(false);
|
||||
}
|
||||
|
||||
public static synchronized void onUnsafeChunkRequest(String detail) {
|
||||
if (fineDegraded) return;
|
||||
fineDegraded = true;
|
||||
reason = "子任务请求同步生成区块,关闭后续细粒度并行(保留维度并行): " + detail;
|
||||
ModZuozhi.LOGGER.error("[FaultGuard] {}", reason);
|
||||
// 与 onDimTickFailure 一致:先关细粒度并尽力排空残留子任务,再关全局,
|
||||
// 最后熄灭维度 loop,释放可能被卡死子任务持有的锁,恢复主线程原版串行。
|
||||
FineGrainScheduler.setEnabled(false);
|
||||
FineGrainScheduler.awaitIdle(2000L);
|
||||
ModZuozhi.ENABLED = false;
|
||||
DimThreadCore.MANAGER.killAllLoops();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +114,10 @@ public final class FaultGuard {
|
||||
// 与 onDimTickFailure 一致:先关细粒度并排空残留子任务,再关全局,
|
||||
// 避免主线程串行 tick 时仍提交并行子任务(双 tick / section 无锁遍历 CME 竞态)。
|
||||
FineGrainScheduler.setEnabled(false);
|
||||
FineGrainScheduler.awaitIdle(5000L);
|
||||
if (!FineGrainScheduler.awaitIdle(5000L)) {
|
||||
reason = "维度超时且子任务尚未排空,拒绝切回主线程";
|
||||
return false;
|
||||
}
|
||||
ModZuozhi.ENABLED = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import dev.modzuozhi.core.compat.TaskBarrier;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.ThreadInfo;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -27,9 +29,9 @@ import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID;
|
||||
* <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>:{@link #isEnabled()} 默认开启。关闭时 {@link Barrier#submit}
|
||||
* 在新阶段同步执行;已开始的阶段保持原模式直到屏障收口;由 {@code /dimensionalripper fine on|off}
|
||||
* 运行时切换。</li>
|
||||
* <li><b>异常隔离</b>:子任务异常只记录日志,不中断本维度 tick,也不污染外层维度收口。</li>
|
||||
* </ul>
|
||||
@@ -37,7 +39,7 @@ import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID;
|
||||
public final class FineGrainScheduler {
|
||||
|
||||
/**
|
||||
* 细粒度并行总开关(默认开启,已长时间测试稳定;出错时 {@link FaultGuard} 会自动降级关闭)。
|
||||
* 细粒度并行总开关(默认开启;出错时 {@link FaultGuard} 会自动降级关闭)。
|
||||
* 可由 {@code /dimensionalripper fine on|off} 运行时切换。
|
||||
*/
|
||||
private static volatile boolean enabled = true;
|
||||
@@ -49,7 +51,7 @@ public final class FineGrainScheduler {
|
||||
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName(MOD_ID + "_subtask");
|
||||
t.setName(MOD_ID + "_subtask_" + t.threadId());
|
||||
return t;
|
||||
});
|
||||
|
||||
@@ -100,17 +102,18 @@ public final class FineGrainScheduler {
|
||||
* 目的:{@code /dimensionalripper off} 或自动降级后,{@link #SUB_POOL} 里可能残留上一拍提交、
|
||||
* 尚未跑完的实体子任务;此时主线程已退回原版串行 tick 同一批实体,会与残留子任务
|
||||
* <b>并发 tick 同一实体</b>导致异常(表现为"关闭时反复出现子任务异常")。先排空
|
||||
* SUB_POOL 再让主线程接管,消除该竞态。超时则强制继续,避免异常场景下永久阻塞。
|
||||
* SUB_POOL 再让主线程接管。超时返回 false,调用方必须拒绝不安全的交接。
|
||||
*/
|
||||
public static void awaitIdle(long timeoutMs) {
|
||||
public static boolean awaitIdle(long timeoutMs) {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
|
||||
while (System.nanoTime() < deadline) {
|
||||
if (ACTIVE_SUBTASKS.get() == 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
LockSupport.parkNanos(100_000L); // 0.1ms 退避
|
||||
}
|
||||
ModZuozhi.LOGGER.warn("[FineGrain] 等待子任务排空超时(>{}ms),强制继续", timeoutMs);
|
||||
ModZuozhi.LOGGER.error("[FineGrain] 等待排空超时,仍有 {} 个子任务,拒绝交接世界所有权", ACTIVE_SUBTASKS.get());
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 当前并行子任务的延迟执行队列;非子任务上下文返回 null。 */
|
||||
@@ -120,9 +123,11 @@ public final class FineGrainScheduler {
|
||||
|
||||
/** 为本维度 tick 创建一个新的子任务屏障。 */
|
||||
public static Barrier beginBarrier() {
|
||||
return new Barrier();
|
||||
return beginBarrier("unspecified");
|
||||
}
|
||||
|
||||
public static Barrier beginBarrier(String phase) { return new Barrier(phase); }
|
||||
|
||||
/**
|
||||
* 维度内子任务屏障:提交多个可并行子任务,{@link #await()} 等待全部完成。
|
||||
* <p>
|
||||
@@ -130,14 +135,20 @@ public final class FineGrainScheduler {
|
||||
* {@link #submit} 直接在当前线程同步执行,保证开关无关的正确性。
|
||||
*/
|
||||
public static final class Barrier {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private final TaskBarrier tasks = new TaskBarrier();
|
||||
// Freeze mode per phase: never mix inline ticks with still-running siblings.
|
||||
private final boolean parallel = enabled;
|
||||
private final String owner = Thread.currentThread().getName();
|
||||
private final String phase;
|
||||
private boolean dumpedThreads;
|
||||
|
||||
private Barrier(String phase) { this.phase = phase; }
|
||||
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;
|
||||
|
||||
@@ -172,21 +183,18 @@ public final class FineGrainScheduler {
|
||||
* (排序加锁防死锁),供跨区块访问的子任务使用
|
||||
*/
|
||||
public void submit(MinecraftServer server, Runnable task, BlockPos pos, int radius) {
|
||||
if (!FineGrainScheduler.enabled) {
|
||||
if (!parallel) {
|
||||
task.run();
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
pending++;
|
||||
}
|
||||
tasks.register();
|
||||
ACTIVE_SUBTASKS.incrementAndGet();
|
||||
SUB_POOL.execute(() -> {
|
||||
AutoCloseable chunkLock = (pos == null)
|
||||
? null
|
||||
: (radius > 0 ? ChunkLock.lock(pos, radius) : ChunkLock.lock(pos));
|
||||
executeOrRun(() -> {
|
||||
AutoCloseable chunkLock = null;
|
||||
PostExecuteQueue prev = CURRENT_QUEUE.get();
|
||||
CURRENT_QUEUE.set(postQueue);
|
||||
try {
|
||||
chunkLock = pos == null ? null : radius > 0 ? ChunkLock.lock(pos, radius) : ChunkLock.lock(pos);
|
||||
task.run();
|
||||
} catch (Throwable t) {
|
||||
ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t);
|
||||
@@ -199,13 +207,8 @@ public final class FineGrainScheduler {
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
synchronized (this) {
|
||||
pending--;
|
||||
if (pending == 0) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
ACTIVE_SUBTASKS.decrementAndGet();
|
||||
tasks.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -224,7 +227,7 @@ public final class FineGrainScheduler {
|
||||
if (entities.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!FineGrainScheduler.enabled) {
|
||||
if (!parallel) {
|
||||
// 细粒度关闭:当前线程逐实体串行 tick(行为等价原版)
|
||||
for (Entity entity : entities) {
|
||||
if (!entity.isRemoved()) {
|
||||
@@ -236,14 +239,12 @@ public final class FineGrainScheduler {
|
||||
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;
|
||||
}
|
||||
for (int i = 0; i < chunkCount; i++) tasks.register();
|
||||
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));
|
||||
executeOrRun(() -> modzuozhi_runChunk(chunk, consumer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,16 +263,13 @@ public final class FineGrainScheduler {
|
||||
* @param task 子任务(如某一片实体的 tick)
|
||||
*/
|
||||
public void submitShard(MinecraftServer server, ShardGate gate, int shard, Runnable task) {
|
||||
if (!FineGrainScheduler.enabled) {
|
||||
gate.release(shard);
|
||||
task.run();
|
||||
if (!parallel) {
|
||||
try { task.run(); } finally { gate.release(shard); }
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
pending++;
|
||||
}
|
||||
tasks.register();
|
||||
ACTIVE_SUBTASKS.incrementAndGet();
|
||||
SUB_POOL.execute(() -> {
|
||||
executeOrRun(() -> {
|
||||
PostExecuteQueue prev = CURRENT_QUEUE.get();
|
||||
CURRENT_QUEUE.set(postQueue);
|
||||
try {
|
||||
@@ -282,13 +280,8 @@ public final class FineGrainScheduler {
|
||||
} finally {
|
||||
CURRENT_QUEUE.set(prev);
|
||||
gate.release(shard);
|
||||
synchronized (this) {
|
||||
pending--;
|
||||
if (pending == 0) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
ACTIVE_SUBTASKS.decrementAndGet();
|
||||
tasks.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -310,36 +303,48 @@ public final class FineGrainScheduler {
|
||||
}
|
||||
} finally {
|
||||
CURRENT_QUEUE.set(prev);
|
||||
synchronized (this) {
|
||||
pending--;
|
||||
if (pending == 0) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
ACTIVE_SUBTASKS.decrementAndGet();
|
||||
tasks.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/** 阻塞等待本维度所有已提交子任务完成;超时触发 FaultGuard 降级,避免永久死锁。 */
|
||||
public void await() {
|
||||
// 关键:若从未提交任何子任务(pending==0),latch 永远不会 countDown,
|
||||
// 无条件 await 会永久死锁。因此先检查 pending,为 0 直接返回。
|
||||
synchronized (this) {
|
||||
if (pending == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
boolean done;
|
||||
private static void executeOrRun(Runnable task) {
|
||||
try {
|
||||
done = latch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
SUB_POOL.execute(task);
|
||||
} catch (java.util.concurrent.RejectedExecutionException rejected) {
|
||||
task.run(); // Preserve the same accounting and shard-release finally path.
|
||||
}
|
||||
if (!done) {
|
||||
ModZuozhi.LOGGER.error("[FineGrain] 子任务屏障等待超时(>{}ms),存在卡死的子任务,触发降级", AWAIT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Await the whole phase, retaining ownership after a timeout or interruption. */
|
||||
public void await() {
|
||||
tasks.await(AWAIT_TIMEOUT_MS, () -> {
|
||||
FaultGuard.onBarrierTimeout();
|
||||
}
|
||||
ModZuozhi.LOGGER.error("[FineGrain] 等待超过 10 秒: owner={}, phase={}, pending={}, poolActive={}, queued={}",
|
||||
owner, phase, tasks.pending(), SUB_POOL.getActiveCount(), SUB_POOL.getQueue().size());
|
||||
// One detailed snapshot per stalled phase. Later warnings remain compact:
|
||||
// repeatedly printing every mod's idle threads can overwhelm launchers.
|
||||
if (dumpedThreads) return;
|
||||
dumpedThreads = true;
|
||||
try {
|
||||
for (ThreadInfo info : ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)) {
|
||||
if (info == null) continue;
|
||||
if (!info.getThreadName().equals("Server thread")
|
||||
&& !info.getThreadName().startsWith(MOD_ID + "_")) continue;
|
||||
if (java.util.Arrays.stream(info.getStackTrace()).anyMatch(frame ->
|
||||
frame.getClassName().equals("java.util.concurrent.ThreadPoolExecutor")
|
||||
&& frame.getMethodName().equals("getTask"))) continue;
|
||||
StringBuilder dump = new StringBuilder().append(info.getThreadName())
|
||||
.append(" #").append(info.getThreadId()).append(' ').append(info.getThreadState())
|
||||
.append(" waiting=").append(info.getLockInfo()).append(" owner=").append(info.getLockOwnerName());
|
||||
for (StackTraceElement frame : info.getStackTrace()) dump.append("\n\tat ").append(frame);
|
||||
for (var lock : info.getLockedSynchronizers()) dump.append("\n\towned: ").append(lock);
|
||||
ModZuozhi.LOGGER.error("[FineGrain thread dump] {}", dump);
|
||||
}
|
||||
} catch (RuntimeException failure) {
|
||||
ModZuozhi.LOGGER.error("[FineGrain] Unable to capture thread diagnostics", failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 维度 worker 收口处:串行执行子任务投递的延迟回调。 */
|
||||
|
||||
@@ -2,6 +2,7 @@ package dev.modzuozhi.core.dimthread;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.modzuozhi.ModZuozhi;
|
||||
import dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.PacketSendListener;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -62,6 +63,13 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
private volatile boolean ticking = false;
|
||||
/** 单机内嵌服务器暂停(ESC 菜单)时置 true:取消下一拍并不再自排,杜绝暂停/保存期间并行写区块。 */
|
||||
private volatile boolean paused = false;
|
||||
private volatile boolean mainThreadHeld;
|
||||
private long scheduleGeneration;
|
||||
private final dev.modzuozhi.core.compat.TickProgress progress = new dev.modzuozhi.core.compat.TickProgress();
|
||||
|
||||
public dev.modzuozhi.core.compat.TickProgress.Snapshot progress(long now) {
|
||||
return progress.snapshot(now);
|
||||
}
|
||||
|
||||
/** 本循环独立的 tick 计数(用于每 20 tick 的时间同步)。 */
|
||||
private int tickCount;
|
||||
@@ -89,8 +97,9 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
}
|
||||
|
||||
/** 申请停止自排程(维度卸载 / 服务器停机时调用)。 */
|
||||
public void kill() {
|
||||
public synchronized void kill() {
|
||||
this.running = false;
|
||||
this.scheduleGeneration++;
|
||||
Future<?> task = this.currentTickTask;
|
||||
if (task != null && !task.isCancelled()) {
|
||||
task.cancel(true);
|
||||
@@ -98,16 +107,22 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
}
|
||||
|
||||
/** 首次启动(或外部主动重排)本循环。 */
|
||||
public void schedule() {
|
||||
public synchronized void schedule() {
|
||||
if (!this.running || this.paused || this.mainThreadHeld || this.ticking) return;
|
||||
this.scheduleAfter(0L);
|
||||
}
|
||||
|
||||
private void scheduleAfter(long delay) {
|
||||
long generation = ++this.scheduleGeneration;
|
||||
try {
|
||||
this.currentTickTask = this.masterPool.submit(this);
|
||||
this.currentTickTask = this.masterPool.schedule(() -> this.runIfCurrent(generation), delay, TimeUnit.NANOSECONDS);
|
||||
} catch (java.util.concurrent.RejectedExecutionException ignored) {
|
||||
// masterPool 已被关闭(停机/降级/重启用):放弃排程,run 内的 running 检查兜底
|
||||
}
|
||||
}
|
||||
|
||||
/** 每 tick 由主线程调用:保证本 loop 只启动一次自排程。 */
|
||||
public void scheduleIfNeeded() {
|
||||
public synchronized void scheduleIfNeeded() {
|
||||
if (!this.scheduled) {
|
||||
this.scheduled = true;
|
||||
this.schedule();
|
||||
@@ -115,7 +130,16 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return this.paused;
|
||||
return this.paused || this.mainThreadHeld;
|
||||
}
|
||||
|
||||
public synchronized void holdForMainThread(boolean held) {
|
||||
if (this.mainThreadHeld == held) return;
|
||||
this.mainThreadHeld = held;
|
||||
if (held) {
|
||||
this.scheduleGeneration++;
|
||||
if (this.currentTickTask != null) this.currentTickTask.cancel(false);
|
||||
} else if (this.scheduled) this.schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,11 +149,12 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
* 不再有任何并行写入 LevelChunk 的随机tick/TE/实体,消灭「暂停/保存期间区块数据包序列化
|
||||
* 读到中间态 → 客户端解码越界 → 网络协议错误掉线」的竞态。
|
||||
*/
|
||||
public void pause() {
|
||||
public synchronized void pause() {
|
||||
if (this.paused) {
|
||||
return;
|
||||
}
|
||||
this.paused = true;
|
||||
this.scheduleGeneration++;
|
||||
Future<?> task = this.currentTickTask;
|
||||
if (task != null && !task.isCancelled()) {
|
||||
task.cancel(false);
|
||||
@@ -137,7 +162,7 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
}
|
||||
|
||||
/** 恢复本维度循环:立即重排下一拍。 */
|
||||
public void resume() {
|
||||
public synchronized void resume() {
|
||||
if (!this.paused) {
|
||||
return;
|
||||
}
|
||||
@@ -160,15 +185,17 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
DimThreadCore.attach(Thread.currentThread(), this.level);
|
||||
// 暂停期间若仍有取消失败/竞态残留的任务被执行,直接放弃本拍(不进入维度 tick,不续排);
|
||||
// 正常路径下 pause() 会 cancel 掉当前拍,这里只是兜底。
|
||||
if (this.paused || !this.running) {
|
||||
this.currentTickTask = null;
|
||||
return;
|
||||
}
|
||||
this.ticking = true;
|
||||
long generation;
|
||||
synchronized (this) { generation = this.scheduleGeneration; }
|
||||
this.runIfCurrent(generation);
|
||||
}
|
||||
|
||||
private void runIfCurrent(long generation) {
|
||||
synchronized (this) {
|
||||
if (generation != this.scheduleGeneration || this.paused || this.mainThreadHeld || !this.running || this.ticking) return;
|
||||
this.ticking = true;
|
||||
}
|
||||
DimThreadCore.attach(Thread.currentThread(), this.level);
|
||||
MinecraftServer server = this.level.getServer();
|
||||
long nspt = server.tickRateManager().nanosecondsPerTick();
|
||||
if (server.tickRateManager().isSprinting()) {
|
||||
@@ -176,31 +203,37 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
}
|
||||
|
||||
long tickStart = System.nanoTime();
|
||||
progress.begin(tickStart);
|
||||
boolean[] completed = {false};
|
||||
long[] workNanos = {0L};
|
||||
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);
|
||||
try {
|
||||
ThreadOwnershipDiagnostics.phase(this.level, "queued-tasks");
|
||||
this.processPendingTasks();
|
||||
ThreadOwnershipDiagnostics.phase(this.level, "level-tick");
|
||||
completed[0] = this.internalTick();
|
||||
} finally {
|
||||
workNanos[0] = System.nanoTime() - tickStart;
|
||||
}
|
||||
ThreadOwnershipDiagnostics.phase(this.level, "chunk-tasks");
|
||||
this.drainChunkTasks(deadline);
|
||||
}, this.level, this.level.getChunkSource());
|
||||
} catch (Throwable t) {
|
||||
completed[0] = false;
|
||||
ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 异常", this.level.dimension().location(), t);
|
||||
FaultGuard.onDimTickFailure(t);
|
||||
} finally {
|
||||
this.ticking = false;
|
||||
if (this.running && !this.paused) {
|
||||
long remaining = Math.max(0L, deadline - System.nanoTime());
|
||||
try {
|
||||
this.currentTickTask = this.masterPool.schedule(this, remaining, TimeUnit.NANOSECONDS);
|
||||
} catch (java.util.concurrent.RejectedExecutionException ignored) {
|
||||
// masterPool 已关闭:放弃续排
|
||||
ThreadOwnershipDiagnostics.phase(this.level, "idle");
|
||||
progress.finish(System.nanoTime(), workNanos[0], completed[0]);
|
||||
synchronized (this) {
|
||||
this.ticking = false;
|
||||
if (this.running && !this.paused && !this.mainThreadHeld) {
|
||||
this.scheduleAfter(Math.max(0L, deadline - System.nanoTime()));
|
||||
} else if (!this.running) {
|
||||
ModZuozhi.LOGGER.info("[DimThread] 维度 {} 独立循环已停止", this.level.dimension().location());
|
||||
}
|
||||
} else if (!this.running) {
|
||||
ModZuozhi.LOGGER.info("[DimThread] 维度 {} 独立循环已停止", this.level.dimension().location());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +265,9 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
// 区块不加载 → 实体/光照等待区块 → tick 更慢 → 更没有预算排空区块。因此给一个最低保证窗口。
|
||||
long effectiveDeadline = Math.max(deadline, now + 2_000_000L); // 最低 2ms
|
||||
boolean overBudget = now >= deadline;
|
||||
while (this.running && System.nanoTime() < effectiveDeadline) {
|
||||
// A compatibility phase needs completed work, not the remainder of an idle poll budget.
|
||||
// The in-flight level tick has already finished; leave queued work for its next owner.
|
||||
while (this.running && !this.paused && !this.mainThreadHeld && System.nanoTime() < effectiveDeadline) {
|
||||
if (chunkSource.pollTask()) {
|
||||
continue; // 有就绪任务,继续排空
|
||||
}
|
||||
@@ -252,7 +287,8 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
* 连接 tick(玩家 doTick)→ 区块发送 + 恢复刷新。注意:本阶段暂不把自动保存下沉到 loop,
|
||||
* 仍由主线程全局保存,避免与 {@code MinecraftServerSaveMixin} 双保存。
|
||||
*/
|
||||
private void internalTick() {
|
||||
private boolean internalTick() {
|
||||
boolean successful = true;
|
||||
// 先排空玩家包队列(命令等),再进沉重的 level.tick:实体/AI 卡顿时命令仍能及时执行,
|
||||
// 不会像原来那样被饿死到 level.tick 结束(RETURN)才处理。
|
||||
((IPlayerPacketQueue) this.level).modzuozhi$playerPacketQueue().drain();
|
||||
@@ -272,17 +308,27 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
try {
|
||||
this.level.tick(() -> true);
|
||||
} catch (Throwable throwable) {
|
||||
successful = false;
|
||||
ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 抛异常", this.level.dimension().location(), throwable);
|
||||
}
|
||||
EventHooks.fireLevelTickPost(this.level, () -> true);
|
||||
|
||||
this.tickConnections();
|
||||
ThreadOwnershipDiagnostics.phase(this.level, "connections");
|
||||
|
||||
if (net.neoforged.fml.ModList.get().isLoaded("starlight_extra")) {
|
||||
// Farm inventory persistence and logout callbacks require the actual server thread.
|
||||
// Keep the whole connection phase together, including disconnection cleanup.
|
||||
dev.modzuozhi.core.compat.SableMainThreadBridge.runForLevel(this.level, this::tickConnections);
|
||||
} else {
|
||||
this.tickConnections();
|
||||
}
|
||||
|
||||
for (Connection connection : this.connections) {
|
||||
ServerGamePacketListenerImpl handler = (ServerGamePacketListenerImpl) connection.getPacketListener();
|
||||
handler.chunkSender.sendNextChunks(handler.player);
|
||||
handler.resumeFlushing();
|
||||
}
|
||||
return successful;
|
||||
}
|
||||
|
||||
/** tick 本维度玩家的连接(正是这里触发原版 {@code ServerPlayer.doTick()})。 */
|
||||
@@ -332,4 +378,4 @@ public final class LevelTickLoop implements Runnable, java.util.concurrent.Execu
|
||||
private boolean serverTelemetry() {
|
||||
return this.level.getServer().logIPs();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ public class ServerManager {
|
||||
|
||||
/** 每维度独立 tick 循环(维度隔离模型,见 {@link LevelTickLoop})。key 为维度。 */
|
||||
private final Map<ServerLevel, LevelTickLoop> loops = new ConcurrentHashMap<>();
|
||||
private final java.util.Set<LevelTickLoop> retiringLoops = ConcurrentHashMap.newKeySet();
|
||||
private final Map<MinecraftServer,Boolean> serverPauses = new ConcurrentHashMap<>();
|
||||
/** 承载所有维度 loop 自排程的调度池({@code schedule(task, nspt)} 需要 Scheduled 能力)。 */
|
||||
private volatile ScheduledThreadPoolExecutor masterPool;
|
||||
/** 玩家连接当前所属维度(key 为 Connection,value 为维度),供跨维度/登录时幂等迁移连接。 */
|
||||
@@ -143,7 +145,40 @@ public class ServerManager {
|
||||
|
||||
/** 获取(懒创建)某维度的独立 tick 循环。 */
|
||||
public LevelTickLoop getLoop(ServerLevel level) {
|
||||
return this.loops.computeIfAbsent(level, l -> new LevelTickLoop(l, getMasterPool()));
|
||||
return dev.modzuozhi.core.compat.MainThreadPhaseCoordinator.register(level.getServer(),
|
||||
() -> {
|
||||
for(var retired:retiringLoops) {
|
||||
if(retired.getLevel()==level && retired.isTicking())
|
||||
throw new IllegalStateException("Cannot replace dimension loop before retired tick drains");
|
||||
}
|
||||
return this.loops.computeIfAbsent(level, l -> {
|
||||
var loop=new LevelTickLoop(l,getMasterPool());
|
||||
if(serverPauses.getOrDefault(l.getServer(),false))loop.pause();
|
||||
return loop;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Includes removed-but-still-running writers; removing a map entry does not return ownership. */
|
||||
public java.util.List<LevelTickLoop> ownershipLoopsSnapshot() {
|
||||
retiringLoops.removeIf(loop -> !loop.isTicking());
|
||||
var snapshot=new java.util.HashSet<>(this.loops.values());
|
||||
snapshot.addAll(retiringLoops);
|
||||
return java.util.List.copyOf(snapshot);
|
||||
}
|
||||
|
||||
/** Read-only inventory; observing status must never create or start a loop. */
|
||||
public Map<ServerLevel, LevelTickLoop> loopsSnapshot() {
|
||||
return Map.copyOf(this.loops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Short main-thread compatibility phase. Never hold a mod lock while draining workers.
|
||||
* Call from the server tick, not from a synchronous worker-to-main handoff: that caller's
|
||||
* worker cannot drain until the callback returns. CC TickScheduler is a server-tick hook.
|
||||
*/
|
||||
public void runMainThreadPhase(MinecraftServer server, String reason, Runnable action) {
|
||||
dev.modzuozhi.core.compat.MainThreadPhaseCoordinator.run(server, reason, action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,8 +189,10 @@ public class ServerManager {
|
||||
* 来驱动出生点区块生成;直到 loop 接管后才由 {@code ServerChunkCachePollMixin} 跳过主线程 poll。
|
||||
*/
|
||||
public boolean isLoopScheduled(ServerLevel level) {
|
||||
if(retiringLoops.stream().anyMatch(loop -> loop.getLevel()==level && loop.isTicking()))return true;
|
||||
LevelTickLoop loop = this.loops.get(level);
|
||||
return loop != null && loop.isScheduled();
|
||||
return loop != null && dev.modzuozhi.core.compat.PauseOwnership.workerOwns(
|
||||
loop.isScheduled(), loop.isPaused(), loop.isTicking());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,7 +234,9 @@ public class ServerManager {
|
||||
* 已在正确 loop 中的连接是 no-op,仅发生实际迁移时才增删。单机/少量玩家时开销可忽略。
|
||||
*/
|
||||
public void reconcileConnections(MinecraftServer server) {
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
// 快照拷贝:playerList.getPlayers() 返回不可变包装,高负载下(大量假人批量上线/下线)
|
||||
// 原列表可能被并发修改 → 遍历报 ConcurrentModificationException。拷贝后迭代安全。
|
||||
for (ServerPlayer player : new java.util.ArrayList<>(server.getPlayerList().getPlayers())) {
|
||||
if (player.connection != null) {
|
||||
routeConnection(player.connection.getConnection(), player.serverLevel());
|
||||
}
|
||||
@@ -206,7 +245,7 @@ public class ServerManager {
|
||||
|
||||
/**
|
||||
* 单机内嵌服务器暂停/恢复(由客户端 {@code Minecraft.isPaused()} 驱动,见
|
||||
* {@code MixinMinecraftPauseDetector})。
|
||||
* {@code IntegratedServerPauseMixin},只在服务器线程交接)。
|
||||
* <p>
|
||||
* 暂停时:先取消所有维度 loop 的下一拍,再等待正在执行的那一拍完全收口(含其内部
|
||||
* 并行子任务屏障),使所有维度 worker 彻底停摆——暂停/保存期间不再有任何线程并行写入
|
||||
@@ -215,33 +254,29 @@ public class ServerManager {
|
||||
* <p>
|
||||
* 恢复时:重新排程所有维度 loop。LAN 开放时客户端 {@code isPaused()} 恒 false,不会误暂停。
|
||||
*/
|
||||
public void setPaused(boolean paused) {
|
||||
public void setPaused(MinecraftServer server, boolean paused) {
|
||||
if (!server.isSameThread()) {
|
||||
throw new IllegalStateException("Pause ownership must be changed on the server thread");
|
||||
}
|
||||
dev.modzuozhi.core.compat.MainThreadPhaseCoordinator.updatePause(server,() -> {
|
||||
serverPauses.put(server,paused);
|
||||
ownershipLoopsSnapshot().stream().filter(loop -> loop.getLevel().getServer()==server)
|
||||
.forEach(loop -> {if(paused)loop.pause();else loop.resume();});
|
||||
});
|
||||
if (paused) {
|
||||
this.loops.values().forEach(LevelTickLoop::pause);
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
boolean anyTicking = true;
|
||||
while (anyTicking && System.nanoTime() < deadline) {
|
||||
anyTicking = false;
|
||||
for (LevelTickLoop loop : this.loops.values()) {
|
||||
if (loop.isTicking()) {
|
||||
anyTicking = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anyTicking) {
|
||||
LockSupport.parkNanos(100_000L); // 0.1ms 退避
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.loops.values().forEach(LevelTickLoop::resume);
|
||||
// In-flight ticks may be waiting for Sable on this very thread. Pump callbacks
|
||||
// until they return ownership; a timeout must not authorize concurrent saving.
|
||||
server.managedBlock(() -> ownershipLoopsSnapshot().stream().filter(loop -> loop.getLevel().getServer()==server).noneMatch(LevelTickLoop::isTicking));
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除并停止某维度的 loop(维度卸载时调用)。 */
|
||||
public void removeLoop(ServerLevel level) {
|
||||
LevelTickLoop loop = this.loops.remove(level);
|
||||
LevelTickLoop loop = this.loops.get(level);
|
||||
if (loop != null) {
|
||||
loop.kill();
|
||||
retiringLoops.add(loop);
|
||||
this.loops.remove(level,loop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,9 +295,10 @@ public class ServerManager {
|
||||
* 带 30s 看门狗防止异常场景下永久阻塞。
|
||||
*/
|
||||
public void stopLoopsAndWaitIdle() {
|
||||
this.loops.values().forEach(LevelTickLoop::kill);
|
||||
var stopping = ownershipLoopsSnapshot();
|
||||
stopping.forEach(LevelTickLoop::kill);
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
|
||||
for (LevelTickLoop loop : this.loops.values()) {
|
||||
for (LevelTickLoop loop : stopping) {
|
||||
while (loop.isTicking()) {
|
||||
if (System.nanoTime() > deadline) {
|
||||
ModZuozhi.LOGGER.warn("[DimThread] 等待维度 {} loop 收口超时(>30s),强制继续",
|
||||
@@ -277,6 +313,7 @@ public class ServerManager {
|
||||
/** 停止所有维度 loop 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */
|
||||
public void killAllLoops() {
|
||||
this.loops.values().forEach(LevelTickLoop::kill);
|
||||
this.loops.values().stream().filter(LevelTickLoop::isTicking).forEach(retiringLoops::add);
|
||||
this.loops.clear();
|
||||
// 清空连接路由:重新启用时 routeConnection 会把连接当作「首次」重新挂到新 loop。
|
||||
// 否则 old == level 会误判为 no-op,导致重启后玩家连接丢失、不再被任何 loop tick。
|
||||
@@ -289,7 +326,11 @@ public class ServerManager {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics.clear();
|
||||
TpsTracker.reset();
|
||||
actives.clear();
|
||||
serverPauses.clear();
|
||||
retiringLoops.removeIf(loop -> !loop.isTicking());
|
||||
threadPools.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,32 @@
|
||||
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 周期。
|
||||
*/
|
||||
/** Main coordinator cadence only; dimension progress lives on each LevelTickLoop. */
|
||||
public final class TpsTracker {
|
||||
/** 采样窗口(tick 数)。 */
|
||||
private static final int WINDOW = 100;
|
||||
private static final long[] INTERVALS = new long[100];
|
||||
private static int index, count;
|
||||
private static long lastTick = Long.MIN_VALUE;
|
||||
private TpsTracker() {}
|
||||
|
||||
/** 主线程最近 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() {
|
||||
public static synchronized 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;
|
||||
}
|
||||
if (lastTick != Long.MIN_VALUE) {
|
||||
INTERVALS[index] = now - lastTick;
|
||||
index = (index + 1) % INTERVALS.length;
|
||||
count = Math.min(count + 1, INTERVALS.length);
|
||||
}
|
||||
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);
|
||||
public static synchronized double tps() {
|
||||
if (count == 0) return Double.NaN;
|
||||
long total = 0;
|
||||
for (int i = 0; i < count; i++) total += INTERVALS[i];
|
||||
return total <= 0 ? Double.NaN : 1e9 * count / total;
|
||||
}
|
||||
|
||||
/** 主线程 (协调者) 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);
|
||||
}
|
||||
public static synchronized void reset() {
|
||||
index = count = 0;
|
||||
lastTick = Long.MIN_VALUE;
|
||||
java.util.Arrays.fill(INTERVALS, 0L);
|
||||
}
|
||||
|
||||
/** 指定维度的真实 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ 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 net.minecraft.world.entity.npc.Villager;
|
||||
import net.neoforged.fml.ModList;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -27,6 +29,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* 玩家始终由 {@link PlayerList} 单独处理,不参与本并行路径。
|
||||
*/
|
||||
public final class EntityTickFilter {
|
||||
// Lithium replaces SectionStorage.storage with its own listening hash map after our
|
||||
// initializer. Concurrent villager POI searches can rehash/corrupt that non-thread-safe
|
||||
// map. Keep this proven unsafe workload on its dimension owner, not the subtask pool.
|
||||
private static final boolean SERIAL_LITHIUM_VILLAGERS = ModList.get().isLoaded("lithium");
|
||||
/** 显式放行名单:命中即并行(村民已验证,作为兜底显式保留)。 */
|
||||
private static final Set<String> WHITELIST = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@@ -64,6 +70,9 @@ public final class EntityTickFilter {
|
||||
|
||||
/** 判断实体是否可并行 tick。 */
|
||||
public static boolean isParallel(Entity entity) {
|
||||
if (SERIAL_LITHIUM_VILLAGERS && entity instanceof Villager) {
|
||||
return false;
|
||||
}
|
||||
// 快路径:非生物或玩家一定不并行(黑名单中的末影水晶/TNT矿车非生物,白名单只有村民)。
|
||||
if (!(entity instanceof LivingEntity) || entity instanceof Player) {
|
||||
return false;
|
||||
|
||||
@@ -50,9 +50,9 @@ public final class SerDesFilter implements ISerDesFilter {
|
||||
MODDED_WHITELIST.add(type);
|
||||
}
|
||||
|
||||
/** 移除白名单项(恢复默认的 modded 强制串行)。 */
|
||||
public static void unwhitelistModded(String type) {
|
||||
MODDED_WHITELIST.remove(type);
|
||||
/** 移除白名单项(恢复默认的 modded 强制串行);返回是否真的存在并移除了该项。 */
|
||||
public static boolean unwhitelistModded(String type) {
|
||||
return MODDED_WHITELIST.remove(type);
|
||||
}
|
||||
|
||||
/** 当前白名单内容(诊断展示用)。 */
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import net.minecraft.server.level.ChunkMap;
|
||||
import net.minecraft.util.thread.BlockableEventLoop;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(ChunkMap.class)
|
||||
public interface ChunkMapExecutorAccessor {
|
||||
@Accessor("mainThreadExecutor")
|
||||
BlockableEventLoop<Runnable> modzuozhi_chunkExecutor();
|
||||
}
|
||||
@@ -32,7 +32,10 @@ import java.util.concurrent.Executor;
|
||||
public abstract class ChunkMapLoadOffloadMixin {
|
||||
|
||||
/** 反序列化下放开关(与 {@link ModZuozhi#isEnabled()} 叠加,可一键回退)。Mixin 禁止非私有静态字段。 */
|
||||
private static volatile boolean ENABLED = true;
|
||||
// ChunkSerializer.read also mutates POI storage. A concurrent replacement of one vanilla
|
||||
// map does not protect Lithium's listening map or its column indexes. Keep deserialization
|
||||
// on the chunk executor until all these side effects have an explicit ownership protocol.
|
||||
private static volatile boolean ENABLED = false;
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
@@ -55,4 +58,4 @@ public abstract class ChunkMapLoadOffloadMixin {
|
||||
private Executor modzuozhi$deserializeOffMainThread(Executor executor) {
|
||||
return (ModZuozhi.isEnabled() && ENABLED) ? Util.backgroundExecutor() : executor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import java.util.Queue;
|
||||
|
||||
/** CC remains optional. Keep its identity check intact, but run it with exclusive ownership. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "dan200.computercraft.shared.util.TickScheduler", remap = false)
|
||||
public abstract class ComputerCraftTickSchedulerMixin {
|
||||
@Shadow @Final private static Queue<?> toTick;
|
||||
@Unique private static final Object dimensionalripper$schedulerLock = new Object();
|
||||
|
||||
@WrapMethod(method = "tick()V")
|
||||
private static void dimensionalripper$safeTick(Operation<Void> original) {
|
||||
// No tokens -> no world access, no global barrier. New tokens can wait until next tick.
|
||||
if (toTick.isEmpty()) return;
|
||||
var server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server == null) throw new IllegalStateException("CC tick without a running server");
|
||||
DimThreadCore.MANAGER.runMainThreadPhase(server, "CC TickScheduler", () -> {
|
||||
synchronized (dimensionalripper$schedulerLock) { original.call(); }
|
||||
});
|
||||
}
|
||||
|
||||
// CC's delayed map is global across dimensions. These callbacks only manipulate tokens;
|
||||
// neither loads chunks nor waits on the main thread. Never take this lock before draining.
|
||||
@WrapMethod(method = "onChunkTicketChanged")
|
||||
private static void dimensionalripper$ticket(ServerLevel level, long pos, int oldTicket, int newTicket, Operation<Void> original) {
|
||||
synchronized (dimensionalripper$schedulerLock) { original.call(level, pos, oldTicket, newTicket); }
|
||||
}
|
||||
|
||||
@WrapMethod(method = "onChunkUnload")
|
||||
private static void dimensionalripper$unload(LevelChunk chunk, Operation<Void> original) {
|
||||
synchronized (dimensionalripper$schedulerLock) { original.call(chunk); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import dev.modzuozhi.core.compat.SableAssemblyDispatch;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
|
||||
/** Includes ValueSettingsPacket (rotation speed); preserve Create's range/permission checks. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket", remap = false)
|
||||
public abstract class CreateConfigurationPacketMixin {
|
||||
@WrapMethod(method = "handle(Lnet/minecraft/server/level/ServerPlayer;)V")
|
||||
private void dimensionalripper$configWithOwnership(ServerPlayer player, Operation<Void> original) {
|
||||
if (player == null) {
|
||||
original.call(player);
|
||||
return;
|
||||
}
|
||||
SableAssemblyDispatch.dispatch(player, () -> original.call(player));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import net.neoforged.fml.loading.FMLLoader;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Selects compatibility-safe mixins before Minecraft target classes are transformed. */
|
||||
public final class DimensionalRipperMixinPlugin implements IMixinConfigPlugin {
|
||||
private boolean lithiumLoaded;
|
||||
|
||||
@Override
|
||||
public void onLoad(String mixinPackage) {
|
||||
try {
|
||||
var loadingMods = FMLLoader.getLoadingModList();
|
||||
this.lithiumLoaded = loadingMods != null && loadingMods.getModFileById("lithium") != null;
|
||||
} catch (Throwable ignored) {
|
||||
// If loader discovery is unavailable, preserve the standalone behavior.
|
||||
this.lithiumLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
|
||||
if (mixinClassName.endsWith(".LithiumEntityGroupsMixin")) {
|
||||
return this.lithiumLoaded;
|
||||
}
|
||||
// Lithium replaces Level#getChunk(int, int) wholesale. Injecting into that merged method is
|
||||
// forbidden by Mixin and aborts startup, while Lithium already owns this access path.
|
||||
if (!this.lithiumLoaded) {
|
||||
return true;
|
||||
}
|
||||
return !mixinClassName.endsWith(".LevelGetChunkMixin")
|
||||
&& !mixinClassName.endsWith(".ServerChunkCacheGetChunkFallbackMixin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRefMapperConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getMixins() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,15 @@ import java.util.List;
|
||||
* 并发化实体存储后,碰撞开启也能并行跑,不再需要降频拐杖。本特性保留为<b>可选开关</b>(默认关闭
|
||||
* 碰撞以获取最大 TPS;需要生物交互时开碰撞)。
|
||||
*/
|
||||
@Mixin(Entity.class)
|
||||
// Give optimization mods such as Lithium first choice of this hot invocation. If another mixin
|
||||
// redirects it, our optional redirect is skipped instead of making that mod's required injection
|
||||
// fail and aborting game startup. The independent push cancellation below still applies.
|
||||
@Mixin(value = Entity.class, priority = 900)
|
||||
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;"))
|
||||
target = "Lnet/minecraft/world/level/Level;getEntityCollisions(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/AABB;)Ljava/util/List;"),
|
||||
require = 0)
|
||||
private List<VoxelShape> modzuozhi_noMobCollision(Level level, @Nullable Entity entity, AABB aabb) {
|
||||
// 只受 nocollide 开关控制,不随全局开关失效:全局 /dimensionalripper off 后若恢复真实碰撞,
|
||||
// 实体密集场景会回归 O(n²) 碰撞热点,主线程被拖到几乎无响应(压测巨量实体时实测卡死)。
|
||||
|
||||
@@ -6,6 +6,8 @@ import dev.modzuozhi.core.dimthread.EntityTickParallel;
|
||||
import dev.modzuozhi.core.dimthread.ISectionKeyAccessor;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.util.AbortableIterationConsumer;
|
||||
import net.minecraft.world.level.entity.EntityAccess;
|
||||
import net.minecraft.world.level.entity.EntitySection;
|
||||
@@ -75,27 +77,46 @@ public abstract class EntitySectionMixin implements ISectionKeyAccessor {
|
||||
AABB aabb, AbortableIterationConsumer<EntityAccess> consumer,
|
||||
Operation<AbortableIterationConsumer.Continuation> original
|
||||
) {
|
||||
// 内容遍历/查询:分片读锁(共享,传感器/碰撞查询并发执行)
|
||||
// Never invoke arbitrary mod callbacks under a read lock: a callback can move/remove
|
||||
// an entity, attempting to upgrade this lock to a write lock and deadlocking itself.
|
||||
List<EntityAccess> matches = new ArrayList<>();
|
||||
AbortableIterationConsumer.Continuation result;
|
||||
EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey);
|
||||
try {
|
||||
return original.call(aabb, consumer);
|
||||
result = original.call(aabb, (AbortableIterationConsumer<EntityAccess>) entity -> {
|
||||
matches.add(entity);
|
||||
return AbortableIterationConsumer.Continuation.CONTINUE;
|
||||
});
|
||||
} finally {
|
||||
EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey);
|
||||
}
|
||||
for (EntityAccess entity : matches) {
|
||||
if (consumer.accept(entity).shouldAbort()) return AbortableIterationConsumer.Continuation.ABORT;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@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,
|
||||
private <U extends EntityAccess> AbortableIterationConsumer.Continuation modzuozhi_lockedGetEntitiesTyped(
|
||||
EntityTypeTest<EntityAccess, U> typeTest, AABB aabb,
|
||||
AbortableIterationConsumer<U> consumer,
|
||||
Operation<AbortableIterationConsumer.Continuation> original
|
||||
) {
|
||||
List<U> matches = new ArrayList<>();
|
||||
AbortableIterationConsumer.Continuation result;
|
||||
EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey);
|
||||
try {
|
||||
return original.call(typeTest, aabb, consumer);
|
||||
result = original.call(typeTest, aabb, (AbortableIterationConsumer<U>) entity -> {
|
||||
matches.add(entity);
|
||||
return AbortableIterationConsumer.Continuation.CONTINUE;
|
||||
});
|
||||
} finally {
|
||||
EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey);
|
||||
}
|
||||
for (U entity : matches) {
|
||||
if (consumer.accept(entity).shouldAbort()) return AbortableIterationConsumer.Continuation.ABORT;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,15 +40,16 @@ public abstract class EntityTickParallelMixin {
|
||||
// 非并行窗口或非白名单实体(含玩家):走原版串行(guardEntityTick 原文)
|
||||
return;
|
||||
}
|
||||
// 首次启动 / 区块生成高峰:实体所在区块尚未 FULL 时跳过本 tick。
|
||||
// 原因:并行子任务线程对未生成区块做碰撞检测会拿到空气占位(见 ServerChunkCacheMixin
|
||||
// 的非阻塞占位设计,为避免 join 生成死锁),村民会被持续下坠拉穿地面遁地;区块 FULL
|
||||
// 后下一 tick 恢复正常。此处既不收集并行、也不走串行(串行在 worker 上 join 等生成
|
||||
// 会死锁),直接跳过是唯一安全选择。
|
||||
// Retain the existing centre-chunk readiness gate. Never invent terrain for an
|
||||
// entity whose actual chunk is not ready.
|
||||
if (!modzuozhi_entityChunkReady((ServerLevel) (Object) this, entity)) {
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
// The solid-placeholder workaround is gone. Keep entities whose swept collision
|
||||
// bounds touch missing terrain on the owner, before any collected batch starts.
|
||||
// The owner can drive chunk work; a subtask cannot while its owner awaits it.
|
||||
if (!modzuozhi_collisionChunksReady((ServerLevel) (Object) this, entity)) return;
|
||||
MinecraftServer server = ((ServerLevel) (Object) this).getServer();
|
||||
// 收集到本 tick 的实体批,循环结束统一按块提交并行(减少逐实体提交的调度/屏障开销)
|
||||
EntityTickParallel.collect(server, consumer, entity);
|
||||
@@ -61,7 +62,29 @@ public abstract class EntityTickParallelMixin {
|
||||
*/
|
||||
@Unique
|
||||
private static boolean modzuozhi_entityChunkReady(ServerLevel level, Entity entity) {
|
||||
long key = ChunkPos.asLong(entity.getBlockX() >> 4, entity.getBlockZ() >> 4);
|
||||
return modzuozhi_chunkReady(level, entity.getBlockX() >> 4, entity.getBlockZ() >> 4);
|
||||
}
|
||||
|
||||
@Unique
|
||||
private static boolean modzuozhi_collisionChunksReady(ServerLevel level, Entity entity) {
|
||||
var bounds = entity.getBoundingBox().expandTowards(entity.getDeltaMovement()).inflate(1.0);
|
||||
int minX = net.minecraft.util.Mth.floor(bounds.minX) >> 4;
|
||||
int maxX = net.minecraft.util.Mth.floor(bounds.maxX) >> 4;
|
||||
int minZ = net.minecraft.util.Mth.floor(bounds.minZ) >> 4;
|
||||
int maxZ = net.minecraft.util.Mth.floor(bounds.maxZ) >> 4;
|
||||
// Avoid a huge readiness scan for unusual motion/size supplied by other mods.
|
||||
if ((long) maxX - minX > 8 || (long) maxZ - minZ > 8) return false;
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
if (!modzuozhi_chunkReady(level, x, z)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Unique
|
||||
private static boolean modzuozhi_chunkReady(ServerLevel level, int x, int z) {
|
||||
long key = ChunkPos.asLong(x, z);
|
||||
try {
|
||||
ServerChunkCache cache = level.getChunkSource();
|
||||
ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) cache)
|
||||
@@ -75,4 +98,4 @@ public abstract class EntityTickParallelMixin {
|
||||
return false; // 保守:拿不到就跳过本 tick
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import dev.modzuozhi.core.compat.PauseOwnership;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.server.IntegratedServer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Apply pause and ownership on the server, before vanilla's pause-triggered save. */
|
||||
@Mixin(IntegratedServer.class)
|
||||
public abstract class IntegratedServerPauseMixin {
|
||||
@WrapOperation(method = "tickServer", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/Minecraft;isPaused()Z"))
|
||||
private boolean modzuozhi_syncPauseState(Minecraft client, Operation<Boolean> original) {
|
||||
boolean paused = PauseOwnership.shouldPause(original.call(client), client.getConnection() != null,
|
||||
client.level != null, client.player != null);
|
||||
DimThreadCore.MANAGER.setPaused((IntegratedServer) (Object) this, paused);
|
||||
return paused;
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,14 @@ public abstract class LevelMixin {
|
||||
boolean ownerIsActive = DimThreadCore.owns(this.thread);
|
||||
return callerIsActive && ownerIsActive ? this.thread : t;
|
||||
}
|
||||
|
||||
@org.spongepowered.asm.mixin.injection.Inject(method = "getBlockEntity", at = @At("HEAD"))
|
||||
private void dimensionalripper$observeLookup(net.minecraft.core.BlockPos pos,
|
||||
org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable<net.minecraft.world.level.block.entity.BlockEntity> ci) {
|
||||
if (!((Object) this instanceof net.minecraft.server.level.ServerLevel level) || level.isOutsideBuildHeight(pos)) return;
|
||||
Thread caller = Thread.currentThread();
|
||||
Thread owner = this.thread;
|
||||
if (caller != owner && !((DimThreadCore.owns(caller) || FineGrainScheduler.inSubTask()) && DimThreadCore.owns(owner)))
|
||||
dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics.rejectedLookup(level, pos, owner);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,19 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.ticks.LevelTicks;
|
||||
import net.minecraft.world.ticks.ScheduledTick;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.modzuozhi.core.compat.ScheduledTickDiagnostics;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongMap;
|
||||
import net.minecraft.world.ticks.LevelChunkTicks;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import java.util.function.LongPredicate;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* 第6步:区块调度({@code LevelTicks})并发安全——并行子任务中的 tick 调度写入延迟到
|
||||
@@ -31,7 +44,30 @@ import org.spongepowered.asm.mixin.Mixin;
|
||||
* 且读多写少、正确性可容忍偶发旧值,不做延迟(保持原样)。
|
||||
*/
|
||||
@Mixin(LevelTicks.class)
|
||||
public abstract class LevelTicksMixin<T> {
|
||||
public abstract class LevelTicksMixin<T> implements ScheduledTickDiagnostics {
|
||||
@Shadow @Final private Long2ObjectMap<LevelChunkTicks<T>> allContainers;
|
||||
@Shadow @Final private Long2LongMap nextTickForContainer;
|
||||
@Shadow @Final private LongPredicate tickCheck;
|
||||
@Unique private long dimensionalripper$lastTick = Long.MIN_VALUE;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void dimensionalripper$recordTick(long time, int limit, BiConsumer<BlockPos, T> consumer, CallbackInfo ci) {
|
||||
dimensionalripper$lastTick = time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dimensionalripper$describeTicks(BlockPos pos, long gameTime) {
|
||||
long key = ChunkPos.asLong(pos);
|
||||
var container = allContainers.get(key);
|
||||
var first = container == null ? null : container.peek();
|
||||
return "最近调度时间=" + dimensionalripper$lastTick
|
||||
+ ",完整执行条件=" + tickCheck.test(key)
|
||||
+ ",区块容器=" + (container != null)
|
||||
+ ",区块待执行=" + (container == null ? 0 : container.count())
|
||||
+ ",索引时间=" + (nextTickForContainer.containsKey(key) ? nextTickForContainer.get(key) : "无")
|
||||
+ ",队首=" + (first == null ? "无" : first.type() + "@" + first.pos().toShortString()
|
||||
+ " 剩余刻=" + (first.triggerTick() - gameTime));
|
||||
}
|
||||
|
||||
@WrapMethod(method = "schedule")
|
||||
private void modzuozhi_deferSchedule(ScheduledTick<T> tick, Operation<Void> original) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import net.minecraft.util.ClassInstanceMultiMap;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.Coerce;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
/** Lithium collision queries bypass EntitySection locks and lazily mutate group caches. */
|
||||
@Mixin(value = ClassInstanceMultiMap.class, priority = 900)
|
||||
public abstract class LithiumEntityGroupsMixin {
|
||||
@WrapMethod(method = "add")
|
||||
private boolean modzuozhi_add(Object entity, Operation<Boolean> original) {
|
||||
synchronized (this) { return original.call(entity); }
|
||||
}
|
||||
|
||||
@WrapMethod(method = "remove")
|
||||
private boolean modzuozhi_remove(Object entity, Operation<Boolean> original) {
|
||||
synchronized (this) { return original.call(entity); }
|
||||
}
|
||||
|
||||
@WrapMethod(method = "find")
|
||||
private Collection<?> modzuozhi_find(Class<?> type, Operation<Collection<?>> original) {
|
||||
synchronized (this) { return new ArrayList<>(original.call(type)); }
|
||||
}
|
||||
|
||||
@WrapMethod(method = "lithium$getAllOfGroupType", remap = false)
|
||||
private Collection<?> modzuozhi_groupSnapshot(@Coerce Object group, Operation<Collection<?>> original) {
|
||||
// Share the exact same lock with add/remove. Never return the live linked hash set:
|
||||
// callers iterate it after returning, concurrently with entity movement.
|
||||
synchronized (this) { return new ArrayList<>(original.call(group)); }
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ public abstract class MinecraftServerDimMixin {
|
||||
/** 服务器停止时:先等所有维度 tick 收口,再停 loop、关线程池,防止保存与 worker 并发卡死。 */
|
||||
@Inject(method = "stopServer", at = @At("HEAD"))
|
||||
public void modzuozhi_shutdownThreadpool(CallbackInfo ci) {
|
||||
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.stop((MinecraftServer)(Object)this);
|
||||
// 必须先等正在执行的维度 tick 完全退出再进入保存:否则主线程 saveAllChunks 会与
|
||||
// 仍在飞的维度 worker(level.tick → processUnloads)并发修改 ChunkMap.toDrop
|
||||
// (非线程安全 fastutil 结构),导致「正在保存中」无限卡死(只能强杀进程)。
|
||||
@@ -90,4 +91,9 @@ public abstract class MinecraftServerDimMixin {
|
||||
DimThreadCore.MANAGER.threadPools.forEach((server, pool) -> pool.shutdown());
|
||||
DimThreadCore.MANAGER.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "stopServer", at = @At("RETURN"))
|
||||
private void dimensionalripper$clearPayloads(CallbackInfo ci) {
|
||||
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.clear((MinecraftServer)(Object)this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import dev.modzuozhi.core.compat.PayloadMainThreadDispatch;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
@Mixin(MinecraftServer.class)
|
||||
public abstract class MinecraftServerPayloadMixin {
|
||||
@Inject(method = "tickServer", at = @At("HEAD"))
|
||||
private void dimensionalripper$drainPayloads(BooleanSupplier budget, CallbackInfo ci) {
|
||||
PayloadMainThreadDispatch.drain((MinecraftServer) (Object) this);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,11 @@ public abstract class MinecraftServerSaveMixin {
|
||||
|
||||
// 保存前先排空细粒度子任务,避免残留子任务与实体/区块保存并发访问(残留任务此刻
|
||||
// 若卡死,Barrier 超时降级已在维度 loop 侧触发,这里再给一个短等待兜底)。
|
||||
FineGrainScheduler.awaitIdle(2000L);
|
||||
if (!FineGrainScheduler.awaitIdle(2000L)) {
|
||||
ModZuozhi.LOGGER.error("[DimThread] 子任务未结束,本次保存取消,避免与仍运行的任务并发写入");
|
||||
cir.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 并行保存每个维度的区块/实体(独立写盘)
|
||||
List<ServerLevel> levels = new ArrayList<>();
|
||||
@@ -107,4 +111,4 @@ public abstract class MinecraftServerSaveMixin {
|
||||
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 客户端侧:每帧检测单机内嵌服务器的暂停状态(ESC 菜单「暂停并保存」)。
|
||||
* <p>
|
||||
* 修复历史 bug:单机暂停只冻结主线程,Dimensional Ripper 的维度 worker 不受影响仍在并行
|
||||
* 写入 {@code LevelChunk},与主线程的存盘/区块数据包序列化并发 → 客户端收到残缺区块包解码
|
||||
* 越界("网络协议错误")掉线(ESC 暂停后 0.5s 内必现)。这里在暂停开始的帧内把维度 loop
|
||||
* 一并冻结(见 {@link DimThreadCore#setPaused(boolean)}),恢复时自动重排。
|
||||
* <ul>
|
||||
* <li>LAN 开放时 {@code Minecraft.isPaused()} 恒为 false,不会误暂停;</li>
|
||||
* <li>dedicated server 无客户端,本 mixin 仅注册于 {@code client} 段,不会被加载;</li>
|
||||
* <li>{@code setPaused} 内部同值短路,每帧一次布尔比较开销可忽略。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Mixin(Minecraft.class)
|
||||
public abstract class MixinMinecraftPauseDetector {
|
||||
|
||||
@Inject(method = "runTick", at = @At("HEAD"))
|
||||
private void modzuozhi$syncPauseState(CallbackInfo ci) {
|
||||
DimThreadCore.setPaused(((Minecraft) (Object) this).isPaused());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import dev.modzuozhi.core.compat.SableMainThreadBridge;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
|
||||
/**
|
||||
* Optional Create: Aeronautics / Sable compatibility hook.
|
||||
*
|
||||
* <p>Sable calls {@code ServerSubLevelContainer.tick()} from the HEAD of
|
||||
* {@code ServerLevel.tick()}. When the parent level is running on a Dimensional Ripper worker, only
|
||||
* this main-thread-bound physics segment is routed to the server thread. The worker blocks until it
|
||||
* completes, so Sable never races the rest of its parent level tick.</p>
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer", remap = false)
|
||||
public abstract class SableSubLevelContainerMixin {
|
||||
|
||||
@WrapMethod(method = "tick")
|
||||
private void dimensionalripper$runSableTickOnServerThread(Operation<Void> original) {
|
||||
SableMainThreadBridge.run(this, original::call);
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,13 @@ import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
||||
import dev.modzuozhi.core.dimthread.IChunkEnvTick;
|
||||
import dev.modzuozhi.core.dimthread.IMutableMainThread;
|
||||
import dev.modzuozhi.core.dimthread.ShardGate;
|
||||
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;
|
||||
@@ -60,6 +52,12 @@ import java.util.function.Consumer;
|
||||
*/
|
||||
@Mixin(value = ServerChunkCache.class, priority = 1001)
|
||||
public abstract class ServerChunkCacheMixin implements IMutableMainThread {
|
||||
/**
|
||||
* The real server thread can request chunks while a dimension worker owns this cache.
|
||||
* Vanilla queues that request to the chunk executor and joins without pumping server tasks.
|
||||
* A worker awaiting a Sable/player main-thread phase then cannot reach the queued request.
|
||||
* Pump only the server event loop here; chunk state remains owned by the dimension worker.
|
||||
*/
|
||||
@Shadow
|
||||
public Thread mainThread;
|
||||
@Shadow
|
||||
@@ -129,62 +127,48 @@ public abstract class ServerChunkCacheMixin implements IMutableMainThread {
|
||||
/**
|
||||
* 子任务线程的 {@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>
|
||||
* <li>未命中:required=false 返回 null;required=true 明确失败并关闭后续细粒度并行。
|
||||
* 禁止创建假区块,也不能等待正在屏障处等待本子任务的维度线程生成区块。</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);
|
||||
MinecraftServer server = this.level.getServer();
|
||||
// mainThread changes between ticks: checking it here and then calling vanilla is
|
||||
// a TOCTOU race. Route ALL external server requests once the level loop is scheduled.
|
||||
// Only a synchronous Sable/player handoff guarantees that this worker is paused.
|
||||
if (server.isSameThread() && DimThreadCore.MANAGER.isLoopScheduled(this.level)
|
||||
&& !dev.modzuozhi.core.compat.SableMainThreadBridge.ownsHandoff(this.level)) {
|
||||
ServerChunkCache self = (ServerChunkCache) (Object) this;
|
||||
var executor = ((ChunkMapExecutorAccessor) self.chunkMap).modzuozhi_chunkExecutor();
|
||||
return dev.modzuozhi.core.compat.ManagedFutureAwait.callQueued(
|
||||
() -> original.call(x, z, status, required), executor::tell, server::managedBlock);
|
||||
}
|
||||
if (status != ChunkStatus.FULL) {
|
||||
if (!FineGrainScheduler.inSubTask()) {
|
||||
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();
|
||||
if (status != ChunkStatus.FULL) {
|
||||
ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) this)
|
||||
.modzuozhi_getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
|
||||
ChunkAccess present = holder == null ? null : holder.getChunkIfPresent(status);
|
||||
if (present != null) return present;
|
||||
}
|
||||
// Optional probes must report absence, not manufacture a "loaded" solid chunk.
|
||||
// In particular Sable collision guards can probe the same missing chunk per block.
|
||||
if (!required) return null;
|
||||
// The owner is waiting at our barrier: synchronous generation here would deadlock.
|
||||
String detail = this.level.dimension().location() + " chunk=" + x + "," + z + " status=" + status;
|
||||
FaultGuard.onUnsafeChunkRequest(detail);
|
||||
throw new IllegalStateException("Parallel task requested synchronous chunk generation: " + detail);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,7 +320,7 @@ public abstract class ServerChunkCacheMixin implements IMutableMainThread {
|
||||
return;
|
||||
}
|
||||
MinecraftServer server = this.level.getServer();
|
||||
this.modzuozhi_chunkBarrier = FineGrainScheduler.beginBarrier();
|
||||
this.modzuozhi_chunkBarrier = FineGrainScheduler.beginBarrier("chunk-environment");
|
||||
this.modzuozhi_chunkGate = ShardGate.chunkGate(this.level.dimension());
|
||||
boolean sharded = EntityTickParallel.isSharded();
|
||||
if (sharded) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import dev.modzuozhi.core.compat.PayloadMainThreadDispatch;
|
||||
import net.minecraft.network.protocol.common.ServerCommonPacketListener;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import net.neoforged.neoforge.network.handling.ServerPayloadContext;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Mixin(value = ServerPayloadContext.class, remap = false)
|
||||
public abstract class ServerPayloadContextMixin {
|
||||
@Shadow @Final private ServerCommonPacketListener listener;
|
||||
@Shadow @Final private ResourceLocation payloadId;
|
||||
|
||||
@WrapMethod(method = "enqueueWork(Ljava/lang/Runnable;)Ljava/util/concurrent/CompletableFuture;")
|
||||
private CompletableFuture<Void> dimensionalripper$runnable(Runnable task, Operation<CompletableFuture<Void>> original) {
|
||||
if (!(listener instanceof ServerGamePacketListenerImpl game)) return original.call(task);
|
||||
return dimensionalripper$submit(game, () -> { task.run(); return null; });
|
||||
}
|
||||
|
||||
@WrapMethod(method = "enqueueWork(Ljava/util/function/Supplier;)Ljava/util/concurrent/CompletableFuture;")
|
||||
private <T> CompletableFuture<T> dimensionalripper$supplier(Supplier<T> task, Operation<CompletableFuture<T>> original) {
|
||||
if (!(listener instanceof ServerGamePacketListenerImpl game)) return original.call(task);
|
||||
return dimensionalripper$submit(game, task);
|
||||
}
|
||||
|
||||
@Unique private <T> CompletableFuture<T> dimensionalripper$submit(ServerGamePacketListenerImpl game, Supplier<T> task) {
|
||||
if (game.player.connection != game)
|
||||
return CompletableFuture.failedFuture(new java.util.concurrent.CancellationException("Replaced payload listener"));
|
||||
boolean main = game.getMainThreadEventLoop().isSameThread();
|
||||
var result = PayloadMainThreadDispatch.submit(game.player, payloadId, task);
|
||||
return main ? result : PayloadMainThreadDispatch.guard(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import dev.modzuozhi.core.compat.SableAssemblyDispatch;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.Coerce;
|
||||
|
||||
/** Keep Simulated/Veil optional; intercept before the packet's silent getBlockEntity miss. */
|
||||
@Pseudo
|
||||
@Mixin(targets = "dev.simulated_team.simulated.network.packets.AssemblePacket", remap = false)
|
||||
public abstract class SimulatedAssemblePacketMixin {
|
||||
@WrapMethod(method = "handle(Lfoundry/veil/api/network/handler/ServerPacketContext;)V")
|
||||
private void dimensionalripper$assembleWithOwnership(@Coerce Object context, Operation<Void> original) {
|
||||
final ServerPlayer player;
|
||||
try {
|
||||
// Invoke the public API interface, not a potentially non-public context implementation.
|
||||
Class<?> api = Class.forName("foundry.veil.api.network.handler.ServerPacketContext", false,
|
||||
context.getClass().getClassLoader());
|
||||
player = (ServerPlayer) api.getMethod("player").invoke(context);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Cannot resolve Simulated assembly player", failure);
|
||||
}
|
||||
SableAssemblyDispatch.dispatch(player, () -> original.call(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.modzuozhi.mixin;
|
||||
|
||||
import dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics;
|
||||
import net.minecraft.SystemReport;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(MinecraftServer.class)
|
||||
public abstract class ThreadOwnershipReportMixin {
|
||||
@Inject(method = "fillSystemReport", at = @At("HEAD"))
|
||||
private void dimensionalripper$ownershipReport(SystemReport report, CallbackInfoReturnable<SystemReport> ci) {
|
||||
report.setDetail("DimensionalRipper Thread Ownership", () ->
|
||||
ThreadOwnershipDiagnostics.report((MinecraftServer) (Object) this));
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public abstract class TickingBlockEntityParallelMixin {
|
||||
// 每 tick 重建分片数组(16 槽),防止上一 tick 残留数据串拍
|
||||
this.modzuozhi_teShards = new List[ShardGate.SHARDS];
|
||||
if (this.modzuozhi_teParallel) {
|
||||
this.modzuozhi_teBarrier = FineGrainScheduler.beginBarrier();
|
||||
this.modzuozhi_teBarrier = FineGrainScheduler.beginBarrier("block-entity");
|
||||
this.modzuozhi_teGate = ShardGate.teGate(((ServerLevel) (Object) this).dimension());
|
||||
}
|
||||
}
|
||||
@@ -180,4 +180,4 @@ public abstract class TickingBlockEntityParallelMixin {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"required": true,
|
||||
"minVersion": "0.8.5",
|
||||
"package": "dev.modzuozhi.mixin",
|
||||
"plugin": "dev.modzuozhi.mixin.DimensionalRipperMixinPlugin",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"mixins": [
|
||||
"AttributeInstanceMixin",
|
||||
@@ -12,9 +13,13 @@
|
||||
"BlockDropCaptureMixin",
|
||||
"BrainMixin",
|
||||
"ChunkMapEntityMapMixin",
|
||||
"ChunkMapExecutorAccessor",
|
||||
"ChunkMapLoadOffloadMixin",
|
||||
"ClassInstanceMultiMapMixin",
|
||||
"LithiumEntityGroupsMixin",
|
||||
"CollectingNeighborUpdaterMixin",
|
||||
"ComputerCraftTickSchedulerMixin",
|
||||
"ThreadOwnershipReportMixin",
|
||||
"DistanceManagerMixin",
|
||||
"EntityChangeDimensionMixin",
|
||||
"EntityGetEntityCollisionsMixin",
|
||||
@@ -35,6 +40,8 @@
|
||||
"LevelTicksMixin",
|
||||
"LivingEntityGetInBlockStateMixin",
|
||||
"MinecraftServerDimMixin",
|
||||
"MinecraftServerPayloadMixin",
|
||||
"ServerPayloadContextMixin",
|
||||
"MinecraftServerSaveMixin",
|
||||
"MinecraftServerTimeSyncAccessor",
|
||||
"NavigatingMobsMixin",
|
||||
@@ -60,11 +67,14 @@
|
||||
"ServerLevelPlayerPacketMixin",
|
||||
"ServerPlayerChangeDimensionMixin",
|
||||
"ServerPlayerPrefetchMixin",
|
||||
"SableSubLevelContainerMixin",
|
||||
"SimulatedAssemblePacketMixin",
|
||||
"CreateConfigurationPacketMixin",
|
||||
"TickingBlockEntityParallelMixin",
|
||||
"YieldJobSiteGuardMixin"
|
||||
],
|
||||
"client": [
|
||||
"MixinMinecraftPauseDetector"
|
||||
"IntegratedServerPauseMixin"
|
||||
],
|
||||
"server": [
|
||||
],
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import dev.modzuozhi.core.compat.ManagedFutureAwait;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/** Standalone regression: dimension work needs a callback on the waiting server thread. */
|
||||
public final class ManagedFutureAwaitRegression {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Thread serverThread = Thread.currentThread();
|
||||
BlockingQueue<Runnable> serverTasks = new LinkedBlockingQueue<>();
|
||||
CompletableFuture<Integer> chunk = new CompletableFuture<>();
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
CompletableFuture<Void> physics = new CompletableFuture<>();
|
||||
serverTasks.add(() -> {
|
||||
if (Thread.currentThread() != serverThread) {
|
||||
physics.completeExceptionally(new AssertionError("wrong callback thread"));
|
||||
} else {
|
||||
physics.complete(null);
|
||||
}
|
||||
});
|
||||
physics.get(3, TimeUnit.SECONDS);
|
||||
chunk.complete(42);
|
||||
} catch (Throwable failure) {
|
||||
chunk.completeExceptionally(failure);
|
||||
}
|
||||
}, "test-dimension-worker");
|
||||
worker.start();
|
||||
int value = ManagedFutureAwait.join(chunk, done -> {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (!done.getAsBoolean()) {
|
||||
if (System.nanoTime() > deadline) throw new AssertionError("circular wait");
|
||||
try {
|
||||
Runnable task = serverTasks.poll(10, TimeUnit.MILLISECONDS);
|
||||
if (task != null) task.run();
|
||||
} catch (InterruptedException failure) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
}
|
||||
});
|
||||
worker.join(1000);
|
||||
if (value != 42 || worker.isAlive()) throw new AssertionError("request did not finish");
|
||||
if (ManagedFutureAwait.join(CompletableFuture.completedFuture(7), done -> {
|
||||
throw new AssertionError("completed future pumped callbacks");
|
||||
}) != 7) throw new AssertionError("wrong result");
|
||||
RuntimeException failure = new RuntimeException("expected");
|
||||
try {
|
||||
ManagedFutureAwait.join(CompletableFuture.failedFuture(failure), done -> {});
|
||||
throw new AssertionError("exception swallowed");
|
||||
} catch (CompletionException expected) {
|
||||
if (expected.getCause() != failure) throw new AssertionError("exception changed");
|
||||
}
|
||||
CompletableFuture<Object> cancelled = new CompletableFuture<>();
|
||||
cancelled.cancel(false);
|
||||
try {
|
||||
ManagedFutureAwait.join(cancelled, done -> {});
|
||||
throw new AssertionError("cancellation swallowed");
|
||||
} catch (CancellationException expected) {}
|
||||
// An apparently server-owned cache can switch owner after the entry check.
|
||||
// The request must remain queued regardless; an inline-capable execute() is unsafe.
|
||||
BlockingQueue<Runnable> chunkTasks = new LinkedBlockingQueue<>();
|
||||
java.util.concurrent.atomic.AtomicReference<Thread> owner =
|
||||
new java.util.concurrent.atomic.AtomicReference<>(serverThread);
|
||||
java.util.concurrent.atomic.AtomicBoolean executed = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
int queued = ManagedFutureAwait.callQueued(() -> {
|
||||
if (owner.get() != Thread.currentThread()) throw new AssertionError("wrong chunk owner");
|
||||
executed.set(true);
|
||||
return 19;
|
||||
}, task -> {
|
||||
chunkTasks.add(task);
|
||||
if (executed.get()) throw new AssertionError("request executed inline");
|
||||
}, done -> {
|
||||
Thread dimension = new Thread(() -> {
|
||||
owner.set(Thread.currentThread());
|
||||
chunkTasks.remove().run();
|
||||
});
|
||||
dimension.start();
|
||||
try { dimension.join(2000); } catch (InterruptedException e) { throw new AssertionError(e); }
|
||||
if (!done.getAsBoolean()) throw new AssertionError("queued request stuck");
|
||||
});
|
||||
if (queued != 19 || !executed.get()) throw new AssertionError("lost queued result");
|
||||
System.out.println("PASS: callback handoff, completion, failure and cancellation");
|
||||
System.out.println("PASS: unconditional enqueue survives owner switch after entry");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import dev.modzuozhi.core.compat.PauseOwnership;
|
||||
|
||||
public final class PauseOwnershipRegression {
|
||||
public static void main(String[] args) {
|
||||
for (int mask = 0; mask < 16; mask++) {
|
||||
boolean paused = PauseOwnership.shouldPause((mask & 1) != 0, (mask & 2) != 0,
|
||||
(mask & 4) != 0, (mask & 8) != 0);
|
||||
if (paused != (mask == 15)) throw new AssertionError("Loading screen paused worker: " + mask);
|
||||
}
|
||||
if (PauseOwnership.workerOwns(false, false, false)) throw new AssertionError("Startup routed to absent worker");
|
||||
if (!PauseOwnership.workerOwns(true, false, false)) throw new AssertionError("Running loop lost ownership");
|
||||
if (!PauseOwnership.workerOwns(true, true, true)) throw new AssertionError("In-flight tick lost ownership");
|
||||
if (PauseOwnership.workerOwns(true, true, false)) throw new AssertionError("Idle paused worker kept requests");
|
||||
System.out.println("PASS: loading/login pause gate and drained-pause chunk ownership");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import dev.modzuozhi.core.compat.PayloadWorkQueue;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
|
||||
public class PayloadWorkQueueRegression {
|
||||
static void check(boolean ok, String why) { if (!ok) throw new AssertionError(why); }
|
||||
public static void main(String[] args) throws Exception {
|
||||
fifoAndExactlyOnce(); capacityAndCancellation(); batchCountAndTime();
|
||||
closeAndReentrantCompletion(); concurrentProducers();
|
||||
System.out.println("PAYLOAD QUEUE PASS");
|
||||
}
|
||||
static void fifoAndExactlyOnce() {
|
||||
var q = new PayloadWorkQueue<String>(() -> 0L);
|
||||
var seen = new ArrayList<Integer>();
|
||||
var futures = new ArrayList<CompletableFuture<Integer>>();
|
||||
for (int i=0;i<256;i++) { int n=i; futures.add(q.submit("a", () -> { seen.add(n); return n; })); }
|
||||
while(q.size()>0) q.drainBatch();
|
||||
check(seen.size()==256, "exactly once");
|
||||
for(int i=0;i<256;i++) check(seen.get(i)==i && futures.get(i).join()==i, "FIFO/results");
|
||||
check(q.drainBatch()==0, "no replay");
|
||||
}
|
||||
static void capacityAndCancellation() {
|
||||
var q = new PayloadWorkQueue<Integer>(() -> 0L);
|
||||
var fs = new ArrayList<CompletableFuture<Integer>>();
|
||||
for(int c=0;c<16;c++) for(int i=0;i<256;i++) fs.add(q.submit(c, () -> 1));
|
||||
check(q.size()==4096, "server capacity");
|
||||
check(q.submit(99, () -> 2).isCompletedExceptionally(), "reject server overflow");
|
||||
fs.getFirst().cancel(false);
|
||||
check(q.size()==4095, "cancel returns capacity immediately");
|
||||
check(!q.submit(0, () -> 3).isDone(), "cancel frees connection slot");
|
||||
while(q.size()>0) q.drainBatch();
|
||||
for(int i=0;i<256;i++) q.submit(7, () -> 1);
|
||||
check(q.submit(7, () -> 9).isCompletedExceptionally(), "reject connection overflow");
|
||||
check(!q.submit(8, () -> 1).isDone(), "other connection still allowed");
|
||||
q.close(new IllegalStateException("stop"));
|
||||
}
|
||||
static void batchCountAndTime() {
|
||||
var clock = new AtomicLong(); var q = new PayloadWorkQueue<String>(clock::get);
|
||||
for(int i=0;i<65;i++) q.submit("a", () -> 1);
|
||||
check(q.drainBatch()==64 && q.size()==1, "64 budget"); q.drainBatch();
|
||||
var done = new AtomicInteger();
|
||||
q.submit("a", () -> { clock.addAndGet(2_000_000); return done.incrementAndGet(); });
|
||||
q.submit("a", done::incrementAndGet);
|
||||
check(q.drainBatch()==1 && done.get()==1 && q.size()==1, "2ms boundary");
|
||||
q.drainBatch();
|
||||
var slow = q.submit("a", () -> { clock.addAndGet(100_000_000); return 42; });
|
||||
check(q.drainBatch()==1 && slow.join()==42, "slow task not interrupted");
|
||||
}
|
||||
static void closeAndReentrantCompletion() {
|
||||
var q = new PayloadWorkQueue<String>(() -> 0L);
|
||||
var count = new AtomicInteger(); var f=q.submit("a", count::incrementAndGet);
|
||||
f.thenRun(() -> q.submit("a", count::incrementAndGet)); q.drainBatch();
|
||||
check(count.get()==2, "completion may enqueue");
|
||||
var boom = new IllegalArgumentException("handler failure");
|
||||
var bad=q.submit("a", () -> { throw boom; }); var good=q.submit("a", () -> 42);
|
||||
q.drainBatch(); check(bad.isCompletedExceptionally() && good.join()==42, "isolate exceptions");
|
||||
var pending=q.submit("a", count::incrementAndGet);
|
||||
q.close(boom); check(pending.isCompletedExceptionally() && q.size()==0, "close settles pending");
|
||||
check(q.submit("a", count::incrementAndGet).isCompletedExceptionally(), "close terminal");
|
||||
q.drainBatch(); check(count.get()==2, "closed work never executes");
|
||||
var q2=new PayloadWorkQueue<String>(() -> 0L);
|
||||
var cancel=q2.submit("a", count::incrementAndGet); cancel.cancel(false); q2.drainBatch();
|
||||
check(count.get()==2, "cancelled action skipped");
|
||||
}
|
||||
static void concurrentProducers() throws Exception {
|
||||
var q = new PayloadWorkQueue<Integer>(() -> 0L);
|
||||
var executed=new AtomicInteger(); var futures=new ConcurrentLinkedQueue<CompletableFuture<Integer>>();
|
||||
var pool=Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
var jobs=new ArrayList<Future<?>>();
|
||||
for(int c=0;c<8;c++) { int key=c; jobs.add(pool.submit(() -> {
|
||||
for(int i=0;i<250;i++) { var f=q.submit(key, executed::incrementAndGet); futures.add(f); if(i%2==0) f.cancel(false); }
|
||||
})); }
|
||||
for(var job:jobs) job.get(5,TimeUnit.SECONDS);
|
||||
while(q.size()>0) q.drainBatch();
|
||||
check(executed.get()==1000 && q.size()==0, "concurrent cancel/submit accounting");
|
||||
check(futures.stream().allMatch(CompletableFuture::isDone), "no pending futures");
|
||||
} finally { pool.shutdownNow(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import dev.modzuozhi.core.compat.TaskBarrier;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class TaskBarrierRegression {
|
||||
public static void main(String[] args) throws Exception {
|
||||
// The old one-shot latch is permanently open after the first submission completes.
|
||||
CountDownLatch old = new CountDownLatch(1);
|
||||
old.countDown();
|
||||
if (!old.await(0, TimeUnit.MILLISECONDS)) throw new AssertionError();
|
||||
|
||||
TaskBarrier barrier = new TaskBarrier();
|
||||
barrier.register();
|
||||
barrier.complete(); // Fast task finishes while producer is still submitting.
|
||||
barrier.register(); // A later task MUST still be waited for.
|
||||
CountDownLatch warned = new CountDownLatch(1);
|
||||
AtomicBoolean returned = new AtomicBoolean();
|
||||
AtomicBoolean interruptRestored = new AtomicBoolean();
|
||||
Thread waiter = new Thread(() -> {
|
||||
barrier.await(30, warned::countDown);
|
||||
returned.set(true);
|
||||
interruptRestored.set(Thread.currentThread().isInterrupted());
|
||||
});
|
||||
waiter.setDaemon(true);
|
||||
waiter.start();
|
||||
waiter.interrupt();
|
||||
if (!warned.await(2, TimeUnit.SECONDS)) throw new AssertionError("No slow-task diagnostic");
|
||||
if (returned.get()) throw new AssertionError("Timeout/interruption abandoned a running task");
|
||||
barrier.complete();
|
||||
waiter.join(2000);
|
||||
if (waiter.isAlive() || !returned.get() || !interruptRestored.get()) throw new AssertionError("Drain/interrupt restoration failed");
|
||||
try {
|
||||
barrier.register();
|
||||
throw new AssertionError("Post-seal submission allowed");
|
||||
} catch (IllegalStateException expected) {}
|
||||
|
||||
TaskBarrier empty = new TaskBarrier();
|
||||
empty.await(30, () -> { throw new AssertionError("Empty barrier waited"); });
|
||||
try {
|
||||
empty.complete();
|
||||
throw new AssertionError("Unbalanced completion allowed");
|
||||
} catch (IllegalStateException expected) {}
|
||||
|
||||
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
for (int round = 0; round < 200; round++) {
|
||||
TaskBarrier batch = new TaskBarrier();
|
||||
int[] writes = new int[100];
|
||||
for (int i = 0; i < writes.length; i++) {
|
||||
int index = i;
|
||||
batch.register();
|
||||
workers.execute(() -> {
|
||||
try { writes[index] = 1; } finally { batch.complete(); }
|
||||
});
|
||||
if ((i & 7) == 0) Thread.yield();
|
||||
}
|
||||
batch.await(2000, () -> { throw new AssertionError("Stress test stalled"); });
|
||||
for (int value : writes) if (value != 1) throw new AssertionError("Early return or missing visibility");
|
||||
}
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
}
|
||||
System.out.println("PASS: submission gaps, timeout, interrupt, empty/sealed barrier, 20,000 concurrent completions");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import dev.modzuozhi.core.compat.TickProgress;
|
||||
|
||||
public final class TickProgressRegression {
|
||||
public static void main(String[] args) {
|
||||
TickProgress p = new TickProgress();
|
||||
check(Double.isNaN(p.snapshot(0).tps()), "unobserved must not claim 20 TPS");
|
||||
for (int i = 0; i < 200; i++) {
|
||||
p.begin(i * 50_000_000L);
|
||||
p.finish(i * 50_000_000L + 10_000_000L, 10_000_000L, true);
|
||||
}
|
||||
var normal = p.snapshot(10_000_000_000L);
|
||||
check(normal.tps() >= 19.8 && normal.tps() <= 20.1, "real completion rate");
|
||||
check(normal.averageWorkMs() == 10 && normal.lastWorkMs() == 10, "work MSPT");
|
||||
p.begin(10_000_000_000L);
|
||||
var stuck = p.snapshot(21_000_000_000L);
|
||||
check(stuck.tps() == 0, "stale samples must decay to zero");
|
||||
check(stuck.runningMs() == 11000 && stuck.sinceCompletedMs() > 11000, "live blocked duration");
|
||||
p.finish(22_000_000_000L, 12_000_000_000L, false);
|
||||
var failed = p.snapshot(22_000_000_000L);
|
||||
check(failed.tps() == 0 && failed.failures() == 1, "failed ticks are not successful completions");
|
||||
check(Double.isNaN(failed.runningMs()), "idle has no running tick");
|
||||
p.begin(23_000_000_000L);
|
||||
p.finish(23_010_000_000L, 10_000_000L, true);
|
||||
check(p.snapshot(23_010_000_000L).sinceCompletedMs() == 0, "resume records fresh progress");
|
||||
check(Double.isNaN(new TickProgress().snapshot(24_000_000_000L).tps()), "new world has no stale data");
|
||||
TickProgress slow = new TickProgress();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
slow.begin(i * 200_000_000L);
|
||||
slow.finish(i * 200_000_000L + 1_000_000L, 1_000_000L, true);
|
||||
}
|
||||
check(Math.abs(slow.snapshot(10_000_000_000L).tps() - 5) <= 0.1,
|
||||
"1ms work must not hide 5 TPS scheduling cadence");
|
||||
System.out.println("PASS: actual TPS, no data, stalled tick, stale window, failure and resume");
|
||||
}
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ClientInformation;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Runtime regression fixture; deliberately outside production source sets. */
|
||||
@EventBusSubscriber(modid = "dimensionalripper")
|
||||
public final class ChunkHandoffRegression {
|
||||
private static int rounds;
|
||||
@SubscribeEvent
|
||||
public static void tick(ServerTickEvent.Post event) {
|
||||
var server = event.getServer();
|
||||
if (server.getTickCount() < 10) return;
|
||||
if (rounds % 10 == 0) {
|
||||
DimThreadCore.MANAGER.setPaused(server, true);
|
||||
if (DimThreadCore.MANAGER.isLoopScheduled(server.overworld()))
|
||||
throw new AssertionError("Paused idle loop still owns chunk queue");
|
||||
// Exercise the actual login constructor from the reported waiting stack.
|
||||
new ServerPlayer(server, server.overworld(), new GameProfile(UUID.randomUUID(), "PauseRegression"),
|
||||
ClientInformation.createDefault());
|
||||
server.overworld().getChunkSource().getChunk(20 + rounds, 0, ChunkStatus.FULL, true);
|
||||
DimThreadCore.MANAGER.setPaused(server, false);
|
||||
}
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (server.overworld().getChunkSource().getChunk(0, 0, ChunkStatus.FULL, true) == null)
|
||||
throw new AssertionError("Missing requested chunk");
|
||||
}
|
||||
if (++rounds == 100) {
|
||||
if (!FineGrainScheduler.isEnabled()) throw new AssertionError("Test silently degraded");
|
||||
ModZuozhi.LOGGER.info("REGRESSION PASS: 10 paused login constructors, paused generation, resume, 10000 running chunk requests; fine enabled");
|
||||
server.halt(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import dev.modzuozhi.core.compat.SableAssemblyDispatch;
|
||||
import dev.modzuozhi.core.compat.PayloadMainThreadDispatch;
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
||||
import dev.modzuozhi.core.dimthread.FaultGuard;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Opt-in runtime fixture: no real user world or network connection. */
|
||||
@EventBusSubscriber(modid = "dimensionalripper")
|
||||
public final class InteractionFlowRegression {
|
||||
private static boolean started, completed;
|
||||
private static long startTime;
|
||||
private static final BlockPos SOURCE = new BlockPos(8, 100, 8);
|
||||
@SubscribeEvent
|
||||
public static void tick(ServerTickEvent.Post event) {
|
||||
var server = event.getServer();
|
||||
var level = server.overworld();
|
||||
if (server.getTickCount() < 15) return;
|
||||
if (!started) {
|
||||
started = true;
|
||||
DimThreadCore.MANAGER.setPaused(server, true);
|
||||
level.setChunkForced(0, 0, true);
|
||||
level.getChunk(0, 0);
|
||||
checkMissingChunkProbes(server);
|
||||
for (int x = 4; x <= 12; x++) for (int z = 4; z <= 12; z++) {
|
||||
level.setBlockAndUpdate(new BlockPos(x, 99, z), Blocks.STONE.defaultBlockState());
|
||||
level.setBlockAndUpdate(new BlockPos(x, 100, z), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
level.setBlockAndUpdate(SOURCE.above(3), Blocks.CHEST.defaultBlockState());
|
||||
// A real listener is now part of the shared dispatch's stale-connection contract.
|
||||
var player = new net.neoforged.neoforge.common.util.FakePlayer(level,
|
||||
new GameProfile(UUID.randomUUID(), "Regression"));
|
||||
startTime = level.getGameTime();
|
||||
DimThreadCore.MANAGER.setPaused(server, false);
|
||||
SableAssemblyDispatch.dispatch(player, () -> {
|
||||
if (!server.isSameThread() || !PayloadMainThreadDispatch.owns(server)
|
||||
|| DimThreadCore.MANAGER.ownershipLoopsSnapshot().stream().anyMatch(loop -> loop.isTicking()))
|
||||
throw new AssertionError("Interaction did not obtain exclusive server ownership");
|
||||
if (level.getBlockEntity(SOURCE.above(3)) == null)
|
||||
throw new AssertionError("Main-thread block entity lookup silently failed");
|
||||
level.setBlockAndUpdate(SOURCE, Blocks.WATER.defaultBlockState());
|
||||
completed = true;
|
||||
});
|
||||
}
|
||||
if (server.getTickCount() == 180) {
|
||||
DimThreadCore.MANAGER.setPaused(server, true);
|
||||
if (!completed) throw new AssertionError("Interaction was not dispatched");
|
||||
var progress = DimThreadCore.MANAGER.getLoop(level).progress(System.nanoTime());
|
||||
if (!(progress.tps() > 0) || Double.isNaN(progress.averageWorkMs())
|
||||
|| Double.isNaN(progress.sinceCompletedMs()) || !Double.isNaN(progress.runningMs()))
|
||||
throw new AssertionError("Invalid drained-loop progress: " + progress);
|
||||
ModZuozhi.LOGGER.info("TICK PROGRESS TEST: {}", progress);
|
||||
if (level.getGameTime() <= startTime) throw new AssertionError("World clock stopped");
|
||||
String diagnostic = ((dev.modzuozhi.core.compat.ScheduledTickDiagnostics) level.getFluidTicks())
|
||||
.dimensionalripper$describeTicks(SOURCE, level.getGameTime());
|
||||
if (diagnostic.contains(Long.toString(Long.MIN_VALUE))) throw new AssertionError("Missing tick instrumentation");
|
||||
ModZuozhi.LOGGER.info("FLOW DIAGNOSTIC TEST: {}", diagnostic);
|
||||
if (!level.getFluidState(SOURCE.east()).is(Fluids.FLOWING_WATER))
|
||||
throw new AssertionError("Scheduled water tick did not spread: time=" + level.getGameTime()
|
||||
+ " pending=" + level.getFluidTicks().count());
|
||||
ModZuozhi.LOGGER.info("INTERACTION REGRESSION PASS: exclusive main-thread block entity lookup and scheduled water spread");
|
||||
server.halt(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the actual transformed getChunk from two real scheduler subtasks. */
|
||||
private static void checkMissingChunkProbes(net.minecraft.server.MinecraftServer server) {
|
||||
var level = server.overworld();
|
||||
var cache = level.getChunkSource();
|
||||
var loaded = level.getChunk(0, 0);
|
||||
int loadedCount = cache.getLoadedChunksCount();
|
||||
var failure = new AtomicReference<Throwable>();
|
||||
var completed = new AtomicInteger();
|
||||
var barrier = FineGrainScheduler.beginBarrier("missing-chunk-regression");
|
||||
long began = System.nanoTime();
|
||||
for (int task = 0; task < 2; task++) {
|
||||
barrier.submit(server, () -> {
|
||||
try {
|
||||
if (!FineGrainScheduler.inSubTask()) throw new AssertionError("Not a subtask");
|
||||
for (int i = 0; i < 100_000; i++) {
|
||||
// Alternate coordinates: no fake positive or negative cache entries.
|
||||
int x = 100_000 + (i & 1);
|
||||
if (cache.getChunk(x, -100_000, ChunkStatus.FULL, false) != null)
|
||||
throw new AssertionError("Missing FULL probe returned a fabricated chunk");
|
||||
if (cache.getChunk(x, -100_000, ChunkStatus.EMPTY, false) != null)
|
||||
throw new AssertionError("Missing partial-status probe returned a chunk");
|
||||
if (cache.getChunkNow(x, -100_000) != null)
|
||||
throw new AssertionError("Missing getChunkNow returned a chunk");
|
||||
if (cache.getChunk(0, 0, ChunkStatus.FULL, false) != loaded
|
||||
|| cache.getChunk(0, 0, ChunkStatus.FULL, true) != loaded)
|
||||
throw new AssertionError("Loaded chunk identity lost");
|
||||
}
|
||||
completed.incrementAndGet();
|
||||
} catch (Throwable error) {
|
||||
failure.compareAndSet(null, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
barrier.await();
|
||||
barrier.drain();
|
||||
if (failure.get() != null) throw new AssertionError("Missing chunk regression", failure.get());
|
||||
if (completed.get() != 2 || !FineGrainScheduler.isEnabled())
|
||||
throw new AssertionError("Probes did not complete with parallelism enabled");
|
||||
if (cache.getLoadedChunksCount() != loadedCount)
|
||||
throw new AssertionError("Optional probe loaded chunks");
|
||||
var rabbit = new net.minecraft.world.entity.animal.Rabbit(net.minecraft.world.entity.EntityType.RABBIT, level);
|
||||
rabbit.setPos(8, 101, 8);
|
||||
var routed = new AtomicInteger();
|
||||
dev.modzuozhi.core.dimthread.EntityTickParallel.begin();
|
||||
level.guardEntityTick(entity -> {
|
||||
if (!FineGrainScheduler.inSubTask()) throw new AssertionError("Loaded interior lost parallelism");
|
||||
routed.incrementAndGet();
|
||||
}, rabbit);
|
||||
if (routed.get() != 0) throw new AssertionError("Interior entity was not batched");
|
||||
dev.modzuozhi.core.dimthread.EntityTickParallel.end();
|
||||
if (routed.get() != 1) throw new AssertionError("Interior entity did not run exactly once");
|
||||
rabbit.setDeltaMovement(10_000, 0, 0);
|
||||
dev.modzuozhi.core.dimthread.EntityTickParallel.begin();
|
||||
level.guardEntityTick(entity -> {
|
||||
if (FineGrainScheduler.inSubTask()) throw new AssertionError("Unbounded collision scan on subtask");
|
||||
routed.incrementAndGet();
|
||||
}, rabbit);
|
||||
if (routed.get() != 2) throw new AssertionError("Wide movement did not stay on owner");
|
||||
dev.modzuozhi.core.dimthread.EntityTickParallel.end();
|
||||
if (routed.get() != 2) throw new AssertionError("Owner entity tick duplicated");
|
||||
ModZuozhi.LOGGER.info("ENTITY ROUTING REGRESSION PASS: loaded interior parallel, wide movement on owner, exactly once");
|
||||
ModZuozhi.LOGGER.info("MISSING CHUNK REGRESSION PASS: 600000 absent reads + 400000 loaded reads, {} ms, fine enabled",
|
||||
(System.nanoTime() - began) / 1_000_000);
|
||||
|
||||
// A required miss must fail instead of allocating a fake chunk or blocking forever.
|
||||
var requiredBarrier = FineGrainScheduler.beginBarrier("required-chunk-regression");
|
||||
requiredBarrier.submit(server, () -> {
|
||||
try {
|
||||
for (var status : new ChunkStatus[]{ChunkStatus.FULL, ChunkStatus.EMPTY}) {
|
||||
try {
|
||||
cache.getChunk(100_000, -100_000, status, true);
|
||||
throw new AssertionError("Required missing chunk was fabricated");
|
||||
} catch (IllegalStateException expected) {
|
||||
if (!expected.getMessage().startsWith("Parallel task requested synchronous chunk generation:"))
|
||||
throw expected;
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
failure.compareAndSet(null, error);
|
||||
}
|
||||
});
|
||||
requiredBarrier.await();
|
||||
requiredBarrier.drain();
|
||||
if (failure.get() != null) throw new AssertionError("Required chunk regression", failure.get());
|
||||
if (FineGrainScheduler.isEnabled() || !FaultGuard.isFineDegraded()
|
||||
|| cache.getLoadedChunksCount() != loadedCount)
|
||||
throw new AssertionError("Required miss did not fail safely");
|
||||
FaultGuard.reset();
|
||||
FineGrainScheduler.setEnabled(true);
|
||||
ModZuozhi.LOGGER.info("REQUIRED CHUNK REGRESSION PASS: FULL/EMPTY fail fast, no generation; test-only fault reset");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Local opt-in fixture. Removing the CC safe phase must fail the real identity check. */
|
||||
@EventBusSubscriber(modid = "dimensionalripper")
|
||||
public final class OwnershipRegression {
|
||||
private static boolean ran;
|
||||
private static boolean verified;
|
||||
private static long worldTime;
|
||||
@SubscribeEvent
|
||||
public static void tick(ServerTickEvent.Post event) throws Exception {
|
||||
var server = event.getServer();
|
||||
if (verified && server.getTickCount() >= 80) {
|
||||
DimThreadCore.MANAGER.setPaused(server, true);
|
||||
if (server.overworld().getGameTime() <= worldTime) throw new AssertionError("Parallel worlds did not resume");
|
||||
ModZuozhi.LOGGER.info("OWNERSHIP RESUME PASS: world continued ticking after compatibility phases");
|
||||
verified = false;
|
||||
server.halt(false);
|
||||
}
|
||||
if (ran || server.getTickCount() < 20) return;
|
||||
ran = true;
|
||||
var level = server.overworld();
|
||||
var manager = DimThreadCore.MANAGER;
|
||||
var release = new CountDownLatch(1);
|
||||
float originalRate = server.tickRateManager().tickrate();
|
||||
try {
|
||||
manager.setPaused(server, true);
|
||||
level.setChunkForced(0, 0, true);
|
||||
level.getChunk(0, 0);
|
||||
var pos = new BlockPos(8, 110, 8);
|
||||
var block = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("computercraft:monitor_normal"));
|
||||
level.setBlockAndUpdate(pos, block.defaultBlockState());
|
||||
var monitor = level.getBlockEntity(pos);
|
||||
if (monitor == null) throw new AssertionError("Monitor fixture not installed");
|
||||
var scheduler = Class.forName("dan200.computercraft.shared.util.TickScheduler");
|
||||
var tokenClass = Class.forName("dan200.computercraft.shared.util.TickScheduler$Token");
|
||||
var token = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(monitor);
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, token);
|
||||
var entered = new CountDownLatch(1);
|
||||
var bridgeFailure = new java.util.concurrent.atomic.AtomicReference<Throwable>();
|
||||
manager.getLoop(level).execute(() -> {
|
||||
// A Sable-like main-thread dependency: safe waiting must pump this callback.
|
||||
server.execute(release::countDown);
|
||||
entered.countDown();
|
||||
try {
|
||||
if (!release.await(10, TimeUnit.SECONDS)) throw new AssertionError("Main callback was not pumped");
|
||||
dev.modzuozhi.core.compat.SableMainThreadBridge.runForLevel(level, () -> {
|
||||
if (!server.isSameThread() || level.getBlockEntity(pos) != monitor)
|
||||
throw new AssertionError("Bridge did not obtain main-thread world ownership");
|
||||
});
|
||||
} catch (Throwable e) { bridgeFailure.set(e); }
|
||||
});
|
||||
// Amplify idle polling waste: stopping an idle poll must not wait a 1-second tick budget.
|
||||
server.tickRateManager().setTickRate(1.0F);
|
||||
manager.setPaused(server, false);
|
||||
if (!entered.await(10, TimeUnit.SECONDS)) throw new AssertionError("Worker did not start");
|
||||
if (level.getBlockEntity(pos) != null) throw new AssertionError("Did not reproduce guarded main-thread lookup");
|
||||
for (int i = 0; i < 1000; i++) level.getBlockEntity(pos);
|
||||
long start = System.nanoTime();
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
server.tickRateManager().setTickRate(originalRate);
|
||||
if (System.nanoTime() - start > TimeUnit.MILLISECONDS.toNanos(750))
|
||||
throw new AssertionError("Safe phase waited out idle polling budget after pause request");
|
||||
if (bridgeFailure.get() != null) throw new AssertionError("Sable bridge failed", bridgeFailure.get());
|
||||
ModZuozhi.LOGGER.info("CC PHASE MEASUREMENT: {}", dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics.timing());
|
||||
manager.setPaused(server, true);
|
||||
if (level.getBlockEntity(pos) != monitor) throw new AssertionError("Monitor identity changed");
|
||||
var state = tokenClass.getDeclaredField("$state");
|
||||
state.setAccessible(true);
|
||||
if (!state.get(token).toString().equals("IDLE")) throw new AssertionError("CC token was not completed");
|
||||
// The same scope must preserve an existing pause, even when nested or throwing.
|
||||
var expected = new IllegalArgumentException("regression sentinel");
|
||||
try {
|
||||
manager.runMainThreadPhase(server, "outer-test", () -> manager.runMainThreadPhase(server, "inner-test", () -> { throw expected; }));
|
||||
throw new AssertionError("Phase swallowed exception");
|
||||
} catch (IllegalArgumentException failure) {
|
||||
if (failure != expected) throw failure;
|
||||
}
|
||||
if (!manager.getLoop(level).isPaused()) throw new AssertionError("Existing pause was lost");
|
||||
manager.setPaused(server, false);
|
||||
try {
|
||||
manager.runMainThreadPhase(server, "exception-resume-test", () -> { throw expected; });
|
||||
throw new AssertionError("Running phase swallowed exception");
|
||||
} catch (IllegalArgumentException failure) {
|
||||
if (failure != expected) throw failure;
|
||||
}
|
||||
if (manager.getLoop(level).isPaused()) throw new AssertionError("Failed phase did not resume running world");
|
||||
manager.setPaused(server, true);
|
||||
// Removed owners are valid stale tokens. Real identity mismatches must still throw.
|
||||
var removed = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(pos, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
||||
removed.setLevel(level);
|
||||
var removedToken = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(removed);
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, removedToken);
|
||||
removed.setRemoved();
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
if (!state.get(removedToken).toString().equals("IDLE")) throw new AssertionError("Removed token not discarded");
|
||||
removed.clearRemoved();
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, removedToken);
|
||||
try {
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
throw new AssertionError("Real mismatch was hidden");
|
||||
} catch (java.lang.reflect.InvocationTargetException failure) {
|
||||
if (!(failure.getCause() instanceof IllegalStateException)) throw failure;
|
||||
}
|
||||
var far = new BlockPos(1600000, 100, 1600000);
|
||||
var unloaded = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(far, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
||||
unloaded.setLevel(level);
|
||||
var unloadedToken = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(unloaded);
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, unloadedToken);
|
||||
int chunks = level.getChunkSource().getLoadedChunksCount();
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
if (!state.get(unloadedToken).toString().equals("UNLOADED") || chunks != level.getChunkSource().getLoadedChunksCount())
|
||||
throw new AssertionError("Unloaded token forced chunk loading or was dropped");
|
||||
scheduler.getMethod("onChunkUnload", net.minecraft.world.level.chunk.LevelChunk.class).invoke(null,
|
||||
new net.minecraft.world.level.chunk.LevelChunk(level, new net.minecraft.world.level.ChunkPos(far)));
|
||||
if (!state.get(unloadedToken).toString().equals("IDLE")) throw new AssertionError("Unload callback lost delayed token");
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, unloadedToken);
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
// Ticket promotion requeues a delayed token; unload discards it without masking errors.
|
||||
scheduler.getMethod("onChunkTicketChanged", net.minecraft.server.level.ServerLevel.class, long.class, int.class, int.class)
|
||||
.invoke(null, level, net.minecraft.world.level.ChunkPos.asLong(far), 40, 31);
|
||||
if (!state.get(unloadedToken).toString().equals("SCHEDULED")) throw new AssertionError("Ticket callback lost token");
|
||||
unloaded.setRemoved();
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
var report = server.fillSystemReport(new net.minecraft.SystemReport()).toLineSeparatedString();
|
||||
if (!report.contains("DimensionalRipper Thread Ownership") || !report.contains("observedOwner=")
|
||||
|| !report.contains("8, 110, 8") || !report.contains("rejected=1001"))
|
||||
throw new AssertionError("Crash report lacks ownership evidence: " + report);
|
||||
if (!report.contains("handoff=completed-owner-restored")) throw new AssertionError("Bridge ownership history missing");
|
||||
concurrentCallbacks(server, scheduler, tokenClass, state);
|
||||
ModZuozhi.LOGGER.info("OWNERSHIP EDGE PASS: nested/exception pauses, removed/mismatched/unloaded tokens, ticket callback, crash diagnostics");
|
||||
ModZuozhi.LOGGER.info("OWNERSHIP REGRESSION PASS: real CC monitor identity, callback pumping, scheduled tick; elapsed={}ms", (System.nanoTime()-start)/1_000_000.0);
|
||||
worldTime = level.getGameTime();
|
||||
level.setBlockAndUpdate(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());
|
||||
verified = true;
|
||||
} finally {
|
||||
server.tickRateManager().setTickRate(originalRate);
|
||||
release.countDown();
|
||||
manager.setPaused(server, !verified);
|
||||
if (!verified) server.halt(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void concurrentCallbacks(net.minecraft.server.MinecraftServer server, Class<?> scheduler,
|
||||
Class<?> tokenClass, java.lang.reflect.Field state) throws Exception {
|
||||
var levels = java.util.List.of(server.overworld(), server.getLevel(net.minecraft.world.level.Level.NETHER));
|
||||
var tokens = new Object[2][40];
|
||||
var owners = new net.minecraft.world.level.block.entity.BlockEntity[2][40];
|
||||
for (int d = 0; d < 2; d++) for (int i = 0; i < 40; i++) {
|
||||
var pos = new BlockPos(1600000 + i * 16, 100, 1600000);
|
||||
var owner = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(pos, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
||||
owner.setLevel(levels.get(d));
|
||||
owners[d][i] = owner;
|
||||
tokens[d][i] = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(owner);
|
||||
scheduler.getMethod("schedule", tokenClass).invoke(null, tokens[d][i]);
|
||||
}
|
||||
scheduler.getMethod("tick").invoke(null); // all 80 are deferred, no chunks are loaded
|
||||
var gate = new CountDownLatch(2);
|
||||
var done = new CountDownLatch(2);
|
||||
var failure = new java.util.concurrent.atomic.AtomicReference<Throwable>();
|
||||
for (int d = 0; d < 2; d++) {
|
||||
final int dimension = d;
|
||||
var level = levels.get(d);
|
||||
DimThreadCore.MANAGER.getLoop(level).execute(() -> {
|
||||
try {
|
||||
gate.countDown();
|
||||
if (!gate.await(10, TimeUnit.SECONDS)) throw new AssertionError("Dimension callback overlap not reached");
|
||||
for (int i = 0; i < 40; i++) {
|
||||
var pos = owners[dimension][i].getBlockPos();
|
||||
if (i % 2 == 0) scheduler.getMethod("onChunkUnload", net.minecraft.world.level.chunk.LevelChunk.class)
|
||||
.invoke(null, new net.minecraft.world.level.chunk.LevelChunk(level, new net.minecraft.world.level.ChunkPos(pos)));
|
||||
else scheduler.getMethod("onChunkTicketChanged", net.minecraft.server.level.ServerLevel.class, long.class, int.class, int.class)
|
||||
.invoke(null, level, net.minecraft.world.level.ChunkPos.asLong(pos), 40, 31);
|
||||
}
|
||||
} catch (Throwable error) { failure.set(error); }
|
||||
finally { done.countDown(); }
|
||||
});
|
||||
}
|
||||
DimThreadCore.MANAGER.setPaused(server, false);
|
||||
server.managedBlock(() -> done.getCount() == 0);
|
||||
DimThreadCore.MANAGER.setPaused(server, true);
|
||||
if (failure.get() != null) throw new AssertionError("Concurrent callbacks failed", failure.get());
|
||||
for (int d = 0; d < 2; d++) for (int i = 0; i < 40; i++) {
|
||||
String expected = i % 2 == 0 ? "IDLE" : "SCHEDULED";
|
||||
if (!state.get(tokens[d][i]).toString().equals(expected)) throw new AssertionError("Delayed token lost across dimensions");
|
||||
owners[d][i].setRemoved();
|
||||
}
|
||||
scheduler.getMethod("tick").invoke(null);
|
||||
ModZuozhi.LOGGER.info("OWNERSHIP CALLBACK PASS: 80 deferred tokens, concurrent overworld/nether ticket/unload callbacks");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.neoforged.neoforge.network.handling.ServerPayloadContext;
|
||||
|
||||
/** Actual optional mod handlers, invoked through the shared context. No production dependencies. */
|
||||
public final class ModInteractionRegression {
|
||||
public static void run(MinecraftServer server,ServerPayloadContext ctx) throws Exception {
|
||||
if(!net.neoforged.fml.ModList.get().isLoaded("create"))return;
|
||||
var level=server.overworld();var pos=new BlockPos(12,115,8);var player=ctx.player();
|
||||
player.setPos(12,114,8);
|
||||
var block=BuiltInRegistries.BLOCK.get(ResourceLocation.parse("create:rotation_speed_controller"));
|
||||
level.setBlockAndUpdate(pos,block.defaultBlockState());var be=level.getBlockEntity(pos);
|
||||
PayloadRegression.check(be!=null,"Create speed controller fixture");
|
||||
var speed=be.getClass().getField("targetSpeed").get(be);
|
||||
var valueType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.ValueSettingsBehaviour");
|
||||
int netId=(int)SimulatedHandleRegression.call(valueType,speed,"netId",int.class,new Class<?>[]{});
|
||||
var packetType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.ValueSettingsPacket");
|
||||
var hit=new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false);
|
||||
var packet=packetType.getConstructor(BlockPos.class,int.class,int.class,net.minecraft.world.InteractionHand.class,
|
||||
BlockHitResult.class,Direction.class,boolean.class,int.class).newInstance(pos,0,32,null,hit,Direction.UP,false,netId);
|
||||
var packetBase=Class.forName("com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket");
|
||||
var speedType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueBehaviour");
|
||||
try {
|
||||
ctx.enqueueWork(() -> {
|
||||
try {SimulatedHandleRegression.call(packetBase,packet,"handle",void.class,new Class<?>[]{ServerPlayer.class},player);}
|
||||
catch(Exception e){throw new RuntimeException(e);}
|
||||
}).join();
|
||||
int configured=(int)SimulatedHandleRegression.call(speedType,speed,"getValue",int.class,new Class<?>[]{});
|
||||
PayloadRegression.check(Math.abs(configured)==32,"actual Create speed packet failed: "+configured);
|
||||
player.setPos(10000,115,10000);
|
||||
var denied=packetType.getConstructor(BlockPos.class,int.class,int.class,net.minecraft.world.InteractionHand.class,
|
||||
BlockHitResult.class,Direction.class,boolean.class,int.class).newInstance(pos,0,64,null,hit,Direction.UP,false,netId);
|
||||
ctx.enqueueWork(() -> {
|
||||
try {SimulatedHandleRegression.call(packetBase,denied,"handle",void.class,new Class<?>[]{ServerPlayer.class},player);}
|
||||
catch(Exception e){throw new RuntimeException(e);}
|
||||
}).join();
|
||||
PayloadRegression.check((int)SimulatedHandleRegression.call(speedType,speed,"getValue",int.class,new Class<?>[]{})==configured,"Create distance check bypassed");
|
||||
ModZuozhi.LOGGER.info("CREATE CONFIG PASS: real speed packet and original distance denial");
|
||||
player.setPos(12,114,8);
|
||||
if(net.neoforged.fml.ModList.get().isLoaded("jade"))jade(ctx,pos,be,hit);
|
||||
} finally {level.setBlockAndUpdate(pos,net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());}
|
||||
}
|
||||
static void jade(ServerPayloadContext ctx,BlockPos pos,Object be,BlockHitResult hit) throws Exception {
|
||||
var dataType=Class.forName("snownee.jade.impl.BlockAccessorImpl$SyncData");
|
||||
var data=dataType.getConstructor(boolean.class,BlockHitResult.class,net.minecraft.world.level.block.state.BlockState.class,net.minecraft.world.item.ItemStack.class)
|
||||
.newInstance(false,hit,ctx.player().level().getBlockState(pos),net.minecraft.world.item.ItemStack.EMPTY);
|
||||
var requestType=Class.forName("snownee.jade.network.RequestBlockPacket");
|
||||
var request=requestType.getConstructor(dataType,java.util.List.class).newInstance(data,java.util.List.of());
|
||||
var jadeContext=Class.forName("snownee.jade.network.ServerPayloadContext");
|
||||
var proxy=java.lang.reflect.Proxy.newProxyInstance(jadeContext.getClassLoader(),new Class<?>[]{jadeContext},(self,method,args) -> {
|
||||
if(method.getName().equals("player"))return ctx.player();
|
||||
return java.lang.reflect.InvocationHandler.invokeDefault(self,method,args);
|
||||
});
|
||||
var accessorType=Class.forName("snownee.jade.api.BlockAccessor");
|
||||
var accessorImpl=Class.forName("snownee.jade.impl.BlockAccessorImpl");
|
||||
var seen=new java.util.concurrent.atomic.AtomicInteger();
|
||||
java.util.function.Consumer<net.minecraft.nbt.CompoundTag> consumer=tag -> {
|
||||
PayloadRegression.check(tag.getInt("x")==pos.getX(),"Jade response coordinate");seen.incrementAndGet();
|
||||
};
|
||||
Runnable query=() -> {
|
||||
try {
|
||||
var accessor=SimulatedHandleRegression.call(dataType,data,"unpack",accessorType,new Class<?>[]{ServerPlayer.class},ctx.player());
|
||||
PayloadRegression.check(SimulatedHandleRegression.call(accessorType,accessor,"getBlockEntity",net.minecraft.world.level.block.entity.BlockEntity.class,new Class<?>[]{})==be,"Jade BE identity");
|
||||
SimulatedHandleRegression.call(accessorImpl,null,"handleRequest",void.class,new Class<?>[]{requestType,jadeContext,java.util.function.Consumer.class},request,proxy,consumer);
|
||||
} catch(Exception e){throw new RuntimeException(e);}
|
||||
};
|
||||
ctx.enqueueWork(query).join();PayloadRegression.check(seen.get()==1,"Jade query callback missing");
|
||||
ctx.player().setPos(10000,115,10000);ctx.enqueueWork(query).join();
|
||||
PayloadRegression.check(seen.get()==1,"Jade range guard bypassed");
|
||||
ModZuozhi.LOGGER.info("JADE REQUEST PASS: real lookup/execute/query and original range denial");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
||||
import dev.modzuozhi.core.compat.SableMainThreadBridge;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
|
||||
@EventBusSubscriber(modid="dimensionalripper")
|
||||
public final class PayloadRegression {
|
||||
static boolean ran;
|
||||
static void check(boolean value, String reason) { if(!value) throw new AssertionError(reason); }
|
||||
@SubscribeEvent
|
||||
public static void tick(ServerTickEvent.Post event) throws Exception {
|
||||
var server=event.getServer();
|
||||
if(ran || server.getTickCount()<20) return;
|
||||
ran=true;
|
||||
if(Boolean.getBoolean("dimensionalripper.reviewRegression")) {
|
||||
try {ReviewRegression.run(server);} finally {DimThreadCore.MANAGER.setPaused(server,true);server.halt(false);}
|
||||
return;
|
||||
}
|
||||
var manager=DimThreadCore.MANAGER; var level=server.overworld();
|
||||
var release=new CountDownLatch(1); var entered=new CountDownLatch(1);
|
||||
var failure=new AtomicReference<Throwable>();
|
||||
try {
|
||||
String idleStats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
|
||||
for(int i=0;i<100;i++) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
|
||||
check(idleStats.equals(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server)),"empty drain created payload barrier/statistics");
|
||||
ModZuozhi.LOGGER.info("PAYLOAD IDLE PASS: 100 empty drains created no payload batches");
|
||||
manager.setPaused(server,true); level.setChunkForced(0,0,true); level.getChunk(0,0);
|
||||
var pos=new BlockPos(8,115,8); level.setBlockAndUpdate(pos,Blocks.CHEST.defaultBlockState());
|
||||
var chest=level.getBlockEntity(pos); check(chest!=null,"fixture chest");
|
||||
manager.getLoop(level).execute(() -> {
|
||||
server.execute(release::countDown); entered.countDown();
|
||||
try {
|
||||
check(release.await(5,TimeUnit.SECONDS),"callback not pumped");
|
||||
SableMainThreadBridge.runForLevel(level, () -> check(level.getBlockEntity(pos)==chest,"handoff lookup"));
|
||||
} catch(Throwable e) { failure.set(e); }
|
||||
});
|
||||
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"worker start");
|
||||
check(level.getBlockEntity(pos)==null,"must reproduce guarded null before phase");
|
||||
manager.runMainThreadPhase(server,"payload-test",() -> {
|
||||
check(server.isSameThread() && level.getBlockEntity(pos)==chest,"exclusive lookup");
|
||||
manager.runMainThreadPhase(server,"nested",() -> check(level.getBlockEntity(pos)==chest,"nested lookup"));
|
||||
// Regression: a normal resume during exclusive handling must not drop its lease.
|
||||
manager.setPaused(server,false);
|
||||
manager.getLoop(level).scheduleIfNeeded();
|
||||
try { Thread.sleep(100); } catch(InterruptedException e) { throw new RuntimeException(e); }
|
||||
check(!manager.getLoop(level).isTicking() && level.getBlockEntity(pos)==chest,"menu resume broke exclusive lease");
|
||||
});
|
||||
check(failure.get()==null,"Sable callback failed: "+failure.get());
|
||||
manager.setPaused(server,true);
|
||||
manager.runMainThreadPhase(server,"pre-paused",() -> {});
|
||||
check(manager.getLoop(level).isPaused(),"pre-pause lost");
|
||||
var expected=new IllegalStateException("sentinel");
|
||||
try { manager.runMainThreadPhase(server,"failure",() -> {throw expected;}); throw new AssertionError("swallowed"); }
|
||||
catch(IllegalStateException e) {check(e==expected,"exception identity");}
|
||||
check(manager.getLoop(level).isPaused(),"exception lost pre-pause");
|
||||
ModZuozhi.LOGGER.info("PAYLOAD PHASE PASS");
|
||||
entry(server, pos, chest);
|
||||
} finally { release.countDown(); manager.setPaused(server,true); server.halt(false); }
|
||||
}
|
||||
static void entry(net.minecraft.server.MinecraftServer server, BlockPos pos,
|
||||
net.minecraft.world.level.block.entity.BlockEntity chest) throws Exception {
|
||||
var manager=DimThreadCore.MANAGER; var level=server.overworld();
|
||||
var player=new net.neoforged.neoforge.common.util.FakePlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(), "PayloadTest"));
|
||||
var ctx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(player.connection,
|
||||
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test"));
|
||||
var entered=new CountDownLatch(1); var release=new CountDownLatch(1);
|
||||
manager.getLoop(level).execute(() -> {
|
||||
server.execute(release::countDown); entered.countDown();
|
||||
try { check(release.await(5,TimeUnit.SECONDS),"entry callback pump"); }
|
||||
catch(InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); }
|
||||
});
|
||||
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"entry worker start");
|
||||
try {
|
||||
var future=ctx.enqueueWork(() -> {
|
||||
check(server.isSameThread(),"payload ran off main");
|
||||
check(level.getBlockEntity(pos)==chest,"public payload entry returned guarded null");
|
||||
return ctx.enqueueWork(() -> 42).join();
|
||||
});
|
||||
check(future.join()==42,"nested supplier result");
|
||||
var expected=new IllegalArgumentException("payload sentinel");
|
||||
try { ctx.enqueueWork((Runnable) () -> { throw expected; }); throw new AssertionError("main exception hidden"); }
|
||||
catch(IllegalArgumentException e) { check(e==expected,"main exception identity"); }
|
||||
ModZuozhi.LOGGER.info("PAYLOAD ENTRY PASS");
|
||||
asyncEntry(server,ctx,player);
|
||||
manager.setPaused(server,false);
|
||||
var assembled=new AtomicInteger();
|
||||
dev.modzuozhi.core.compat.SableAssemblyDispatch.dispatch(player,() -> {
|
||||
check(server.isSameThread(),"legacy assembly not main");
|
||||
check(manager.loopsSnapshot().values().stream().noneMatch(loop -> loop.isTicking()),"legacy assembly lacks global ownership");
|
||||
assembled.incrementAndGet();
|
||||
});
|
||||
check(assembled.get()==1,"legacy dispatch did not use common phase");
|
||||
manager.setPaused(server,true);
|
||||
ModInteractionRegression.run(server,ctx);
|
||||
SimulatedHandleRegression.run(server,ctx);
|
||||
var lifecyclePlayer=new net.neoforged.neoforge.common.util.FakePlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(),"LifecycleTest"));
|
||||
lifecycle(server,new net.neoforged.neoforge.network.handling.ServerPayloadContext(lifecyclePlayer.connection,ctx.payloadId()),lifecyclePlayer);
|
||||
} finally { release.countDown(); manager.setPaused(server,true); }
|
||||
}
|
||||
static void asyncEntry(net.minecraft.server.MinecraftServer server,
|
||||
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
|
||||
net.minecraft.server.level.ServerPlayer player) throws Exception {
|
||||
var seen=new java.util.ArrayList<Integer>();
|
||||
var fs=new java.util.ArrayList<CompletableFuture<Integer>>();
|
||||
Thread producer=new Thread(() -> {
|
||||
for(int i=0;i<100;i++) {int n=i; fs.add(ctx.enqueueWork(() -> {check(server.isSameThread(),"async handler main");seen.add(n);return n;}));}
|
||||
},"payload-network-fixture");
|
||||
producer.start(); producer.join(5000); check(!producer.isAlive(),"network submit blocks");
|
||||
check(seen.isEmpty(),"off-thread handler executed inline");
|
||||
while(fs.stream().anyMatch(f -> !f.isDone())) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
|
||||
check(seen.size()==100,"async handler count");
|
||||
for(int i=0;i<100;i++) check(seen.get(i)==i && fs.get(i).join()==i,"async FIFO/result");
|
||||
var cancelled=new AtomicReference<CompletableFuture<Integer>>();var changed=new AtomicInteger();
|
||||
Thread cancelProducer=new Thread(() -> cancelled.set(ctx.enqueueWork(changed::incrementAndGet)));
|
||||
cancelProducer.start();cancelProducer.join(5000);cancelled.get().cancel(false);
|
||||
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
|
||||
check(changed.get()==0,"cancelled public future still mutated world");
|
||||
var configListener=new net.minecraft.server.network.ServerConfigurationPacketListenerImpl(server,
|
||||
new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),
|
||||
net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false));
|
||||
var configCtx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(configListener,ctx.payloadId());
|
||||
check(configCtx.enqueueWork(() -> 17).join()==17,"configuration phase not preserved");
|
||||
var guarded=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread failing=new Thread(() -> guarded.set(ctx.enqueueWork(() -> {throw new IllegalArgumentException("expected test failure");})));
|
||||
failing.start();failing.join(5000);dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
|
||||
check(guarded.get().join()==null,"NeoForge async handler failure contract changed");
|
||||
var raw=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread stale=new Thread(() -> raw.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("stale handler executed");})));
|
||||
stale.start();stale.join(5000);
|
||||
var old=player.serverLevel(); player.setServerLevel(server.getLevel(net.minecraft.world.level.Level.NETHER));
|
||||
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
|
||||
check(raw.get().isCompletedExceptionally(),"stale future pending"); player.setServerLevel(old);
|
||||
check(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server).contains("expired=1"),"stale work not counted separately");
|
||||
var nested=new AtomicReference<Throwable>(); var entered=new CountDownLatch(1);
|
||||
var manager=DimThreadCore.MANAGER;
|
||||
manager.setPaused(server,true);
|
||||
manager.getLoop(old).execute(() -> {
|
||||
entered.countDown();
|
||||
SableMainThreadBridge.runForLevel(old,() -> {
|
||||
try {ctx.enqueueWork(() -> 1); nested.set(new AssertionError("unsafe handoff reentry accepted"));}
|
||||
catch(IllegalStateException expected) {}
|
||||
});
|
||||
});
|
||||
manager.setPaused(server,false);check(entered.await(5,TimeUnit.SECONDS),"handoff fixture start");
|
||||
manager.runMainThreadPhase(server,"handoff-test",() -> {});
|
||||
check(nested.get()==null,"handoff reentry result");
|
||||
manager.setPaused(server,true);
|
||||
var blockedFuture=new AtomicReference<CompletableFuture<Integer>>();var waitDetected=new AtomicBoolean();
|
||||
var waiting=new CountDownLatch(1);
|
||||
manager.getLoop(old).execute(() -> {
|
||||
CompletableFuture<Integer> f=dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("cancelled worker wait action ran");});
|
||||
blockedFuture.set(f);waiting.countDown();
|
||||
try { f.get(250,TimeUnit.MILLISECONDS); }
|
||||
catch(TimeoutException expected) {waitDetected.set(true);f.cancel(false);}
|
||||
catch(Exception e) {throw new RuntimeException(e);}
|
||||
});
|
||||
manager.setPaused(server,false);check(waiting.await(5,TimeUnit.SECONDS),"worker wait fixture start");
|
||||
manager.runMainThreadPhase(server,"unsupported-worker-wait",() -> {});
|
||||
check(waitDetected.get() && blockedFuture.get().isCancelled(),"worker blocking cycle not detected");
|
||||
manager.setPaused(server,true);
|
||||
ModZuozhi.LOGGER.info("PAYLOAD ASYNC PASS");
|
||||
}
|
||||
static void lifecycle(net.minecraft.server.MinecraftServer server,
|
||||
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
|
||||
net.minecraft.server.level.ServerPlayer player) throws Exception {
|
||||
var manager=DimThreadCore.MANAGER; var other=server.getLevel(net.minecraft.world.level.Level.NETHER);
|
||||
manager.setPaused(server,true); manager.removeLoop(other);
|
||||
manager.runMainThreadPhase(server,"new-loop",() -> {
|
||||
var created=manager.getLoop(other);created.scheduleIfNeeded();
|
||||
check(created.isPaused() && !created.isTicking(),"new loop escaped phase");
|
||||
});
|
||||
manager.setPaused(server,true);
|
||||
// Bounded ID labels, not an unbounded record for every packet.
|
||||
for(int i=0;i<1000;i++) {
|
||||
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,
|
||||
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test_"+i),() -> 1).join();
|
||||
}
|
||||
String stats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
|
||||
check(stats.contains("ids=128") && stats.contains("other="),"diagnostics ID cap: "+stats);
|
||||
check(stats.contains("pending=0"),"diagnostic pending count: "+stats);
|
||||
var pending=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread t=new Thread(() -> pending.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("work after stop");})));
|
||||
t.start();t.join(5000);
|
||||
manager.runMainThreadPhase(server,"stop-inside-phase",() -> dev.modzuozhi.core.compat.PayloadMainThreadDispatch.stop(server));
|
||||
check(pending.get().isDone() && pending.get().isCompletedExceptionally(),"stop leaves future pending");
|
||||
check(manager.getLoop(other).isPaused(),"stop restored worker");
|
||||
check(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> 1).isCompletedExceptionally(),"stop accepts new work");
|
||||
ModZuozhi.LOGGER.info("PAYLOAD LIFECYCLE PASS");
|
||||
ModZuozhi.LOGGER.info("PAYLOAD DIAGNOSTICS PASS");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import dev.modzuozhi.core.compat.*;
|
||||
import dev.modzuozhi.core.dimthread.*;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.*;
|
||||
|
||||
public final class ReviewRegression {
|
||||
static final List<String> failures=new ArrayList<>();
|
||||
static void expect(boolean ok,String name){if(!ok){failures.add(name);ModZuozhi.LOGGER.error("REVIEW RED: {}",name);}}
|
||||
public static void run(MinecraftServer server) throws Exception {
|
||||
var manager=DimThreadCore.MANAGER;manager.setPaused(server,true);
|
||||
var player=new net.neoforged.neoforge.common.util.FakePlayer(server.overworld(),
|
||||
new com.mojang.authlib.GameProfile(UUID.randomUUID(),"ReviewTest"));
|
||||
var disconnects=new AtomicInteger();
|
||||
new net.minecraft.server.network.ServerGamePacketListenerImpl(server,
|
||||
new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),player,
|
||||
net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false)) {
|
||||
@Override public void disconnect(net.minecraft.network.chat.Component reason){disconnects.incrementAndGet();}
|
||||
};
|
||||
var ctx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(player.connection,ResourceLocation.parse("dimensionalripper:review"));
|
||||
var result=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread rejected=new Thread(() -> result.set(ctx.enqueueWork(() -> {throw new RejectedExecutionException("handler executor closed");})));
|
||||
rejected.start();rejected.join(5000);PayloadMainThreadDispatch.drain(server);PayloadMainThreadDispatch.drain(server);
|
||||
expect(disconnects.get()==0,"handler rejection must not disconnect player");
|
||||
try{expect(result.get().join()==null,"guard preserves handler rejection result");}
|
||||
catch(CompletionException e){expect(false,"guard preserves handler rejection result");}
|
||||
Thread cancelledHandler=new Thread(() -> result.set(ctx.enqueueWork(() -> {throw new CancellationException("handler cancelled its own computation");})));
|
||||
cancelledHandler.start();cancelledHandler.join(5000);PayloadMainThreadDispatch.drain(server);
|
||||
expect(result.get().join()==null,"guard preserves handler cancellation result");
|
||||
var overflow=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread flood=new Thread(() -> {for(int i=0;i<257;i++)overflow.set(ctx.enqueueWork(() -> 1));});
|
||||
flood.start();flood.join(5000);
|
||||
expect(overflow.get().isCompletedExceptionally(),"capacity rejection remains exceptional");
|
||||
PayloadMainThreadDispatch.drain(server);
|
||||
expect(disconnects.get()==1,"genuine capacity rejection disconnects once");
|
||||
for(int i=0;i<10;i++)PayloadMainThreadDispatch.drain(server);
|
||||
String counters=PayloadDiagnostics.snapshot(server);
|
||||
expect(counters.contains("failed=2") && counters.contains("rejected=1"),
|
||||
"diagnostics distinguish handler failures from capacity rejection");
|
||||
|
||||
var other=server.getLevel(net.minecraft.world.level.Level.NETHER);
|
||||
manager.removeLoop(other);
|
||||
manager.runMainThreadPhase(server,"review-new-loop",() -> manager.getLoop(other).scheduleIfNeeded());
|
||||
expect(manager.getLoop(other).isPaused(),"new loop inherits menu pause after phase exit");
|
||||
manager.setPaused(server,true);
|
||||
|
||||
var loop=manager.getLoop(other);var entered=new CountDownLatch(1);var release=new CountDownLatch(1);
|
||||
var finished=new AtomicBoolean();
|
||||
loop.execute(() -> {
|
||||
server.execute(() -> manager.removeLoop(other));entered.countDown();
|
||||
while(release.getCount()!=0) try{release.await();}catch(InterruptedException ignored){}
|
||||
finished.set(true);
|
||||
});
|
||||
manager.setPaused(server,false);PayloadRegression.check(entered.await(5,TimeUnit.SECONDS),"retiring loop fixture start");
|
||||
Thread releaser=new Thread(() -> {try{Thread.sleep(250);}catch(InterruptedException ignored){}release.countDown();});
|
||||
releaser.start();
|
||||
try{manager.runMainThreadPhase(server,"review-retire",() -> expect(finished.get() && !loop.isTicking(),"retired loop must drain before exclusive phase"));}
|
||||
finally{release.countDown();releaser.join(5000);}
|
||||
manager.setPaused(server,true);
|
||||
|
||||
var labelled=new CountDownLatch(1);var labelRelease=new CountDownLatch(1);
|
||||
var late=new AtomicReference<CompletableFuture<Integer>>();
|
||||
Thread submitter=new Thread(() -> {
|
||||
labelled.countDown();try{labelRelease.await(5,TimeUnit.SECONDS);}catch(InterruptedException e){throw new RuntimeException(e);}
|
||||
// Exact late-registration boundary after the submitter had obtained its queue.
|
||||
var ticket=PayloadDiagnostics.begin(server,"dimensionalripper:shutdown_race");
|
||||
PayloadDiagnostics.end(server,ticket,new CancellationException("stopped"));
|
||||
late.set(PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("late action");}));
|
||||
});
|
||||
submitter.start();PayloadRegression.check(labelled.await(5,TimeUnit.SECONDS),"late submit fixture pause");
|
||||
PayloadMainThreadDispatch.stop(server);PayloadMainThreadDispatch.clear(server);labelRelease.countDown();submitter.join(5000);
|
||||
expect(late.get()!=null && late.get().isCompletedExceptionally(),"late future completed");
|
||||
expect(PayloadDiagnostics.snapshot(server).equals("payloads: pending=0 batches=0"),"late submission must not recreate diagnostics after clear");
|
||||
if(!failures.isEmpty())throw new AssertionError(String.join("; ",failures));
|
||||
ModZuozhi.LOGGER.info("PAYLOAD REVIEW PASS");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package dev.modzuozhi;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.neoforged.neoforge.network.handling.ServerPayloadContext;
|
||||
|
||||
/** Isolated real packet, plot block entity and native constraint lifecycle fixture. */
|
||||
public final class SimulatedHandleRegression {
|
||||
public static void run(MinecraftServer server, ServerPayloadContext ctx) throws Exception {
|
||||
if (!net.neoforged.fml.ModList.get().isLoaded("simulated")) return;
|
||||
var level=server.overworld();
|
||||
var handleId=ResourceLocation.parse("simulated:iron_handle");
|
||||
ModZuozhi.LOGGER.info("Handle fixture block: {}",handleId);
|
||||
var block=BuiltInRegistries.BLOCK.get(handleId);
|
||||
var packet=Class.forName("dev.simulated_team.simulated.network.packets.UpdatePlayerUsingHandlePacket");
|
||||
var ctor=packet.getConstructor(float.class,boolean.class,BlockPos.class);
|
||||
var veilType=Class.forName("foundry.veil.api.network.handler.ServerPacketContext");
|
||||
var veil=Class.forName("foundry.veil.forge.network.NeoForgeServerPacketContext")
|
||||
.getConstructor(net.neoforged.neoforge.network.handling.IPayloadContext.class).newInstance(ctx);
|
||||
var handle=packet.getMethod("handle",veilType);
|
||||
var containerType=Class.forName("dev.ryanhcode.sable.api.sublevel.SubLevelContainer");
|
||||
var subType=Class.forName("dev.ryanhcode.sable.sublevel.SubLevel");
|
||||
var serverSubType=Class.forName("dev.ryanhcode.sable.sublevel.ServerSubLevel");
|
||||
var poseType=Class.forName("dev.ryanhcode.sable.companion.math.Pose3d");
|
||||
var container=call(containerType,null,"getContainer",Class.forName("dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer"),
|
||||
new Class<?>[]{net.minecraft.server.level.ServerLevel.class},level);
|
||||
var sub=call(containerType,container,"allocateNewSubLevel",subType,new Class<?>[]{poseType},poseType.getConstructor().newInstance());
|
||||
var plot=subType.getMethod("getPlot").invoke(sub);
|
||||
var plotType=Class.forName("dev.ryanhcode.sable.sublevel.plot.LevelPlot");
|
||||
var pos=((BlockPos)plotType.getMethod("getCenterBlock").invoke(plot)).atY(115);
|
||||
plotType.getMethod("newEmptyChunk",net.minecraft.world.level.ChunkPos.class).invoke(plot,new net.minecraft.world.level.ChunkPos(pos));
|
||||
level.setBlockAndUpdate(pos,block.defaultBlockState());
|
||||
var be=level.getBlockEntity(pos);
|
||||
PayloadRegression.check(be!=null && be.getClass().getName().endsWith("HandleBlockEntity"),"real handle plot fixture");
|
||||
var players=be.getClass().getDeclaredField("players");players.setAccessible(true);
|
||||
var systemType=Class.forName("dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem");
|
||||
var system=systemType.getMethod("get",net.minecraft.world.level.Level.class).invoke(null,level);
|
||||
var body=systemType.getMethod("getPhysicsHandle",serverSubType).invoke(system,sub);
|
||||
var bodyType=Class.forName("dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle");
|
||||
var jointType=Class.forName("dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle");
|
||||
Object joint=null;
|
||||
var helper=Class.forName("dev.ryanhcode.sable.Sable").getField("HELPER").get(null);
|
||||
var global=(net.minecraft.world.phys.Vec3)call(helper.getClass(),helper,"projectOutOfSubLevel",net.minecraft.world.phys.Vec3.class,
|
||||
new Class<?>[]{net.minecraft.world.level.Level.class,net.minecraft.world.phys.Vec3.class},level,net.minecraft.world.phys.Vec3.atCenterOf(pos));
|
||||
var player=ctx.player();player.setPos(global.x,global.y-1,global.z);player.getAbilities().flying=true;
|
||||
level.addNewPlayer(player);
|
||||
var release=new java.util.concurrent.CountDownLatch(1);
|
||||
try {
|
||||
for(boolean remove:new boolean[]{false,false,true}) {
|
||||
var request=ctor.newInstance(4.0f,remove,pos);
|
||||
if(remove) {
|
||||
var entered=new java.util.concurrent.CountDownLatch(1);
|
||||
var manager=dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER;
|
||||
manager.getLoop(level).execute(() -> {
|
||||
server.execute(release::countDown);entered.countDown();
|
||||
try {PayloadRegression.check(release.await(5,java.util.concurrent.TimeUnit.SECONDS),"native release callback pump");}
|
||||
catch(InterruptedException e){throw new RuntimeException(e);}
|
||||
});
|
||||
manager.setPaused(server,false);
|
||||
PayloadRegression.check(entered.await(5,java.util.concurrent.TimeUnit.SECONDS),"native release worker start");
|
||||
// The old unprotected path clears player holding state but leaves the joint.
|
||||
handle.invoke(request,veil);
|
||||
PayloadRegression.check((boolean)jointType.getMethod("isValid").invoke(joint),"unprotected release did not reproduce stranded joint");
|
||||
ModZuozhi.LOGGER.info("SIMULATED NATIVE RED: unprotected release left joint valid while worker owned plot");
|
||||
}
|
||||
ctx.enqueueWork(() -> {
|
||||
try {handle.invoke(request,veil);} catch(ReflectiveOperationException e) {throw new RuntimeException(e);}
|
||||
}).join();
|
||||
var map=(java.util.Map<?,?>)players.get(be);
|
||||
PayloadRegression.check(remove ? map.isEmpty() : map.containsKey(ctx.player().getUUID()),"real handle grab/release state");
|
||||
if(!remove) {
|
||||
be.getClass().getMethod("sable$physicsTick",serverSubType,bodyType,double.class).invoke(be,sub,body,0.05d);
|
||||
var holding=map.get(player.getUUID());
|
||||
var jointField=holding.getClass().getDeclaredField("constraintHandle");jointField.setAccessible(true);
|
||||
joint=jointField.get(holding);
|
||||
PayloadRegression.check(joint!=null && (boolean)jointType.getMethod("isValid").invoke(joint),"native handle joint created");
|
||||
} else {
|
||||
PayloadRegression.check(joint!=null && !(boolean)jointType.getMethod("isValid").invoke(joint),"native joint survived release packet");
|
||||
dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER.setPaused(server,true);
|
||||
}
|
||||
}
|
||||
ModZuozhi.LOGGER.info("SIMULATED HANDLE PACKET PASS: real packet, BE and native joint invalidated on release");
|
||||
} finally {
|
||||
release.countDown();
|
||||
dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER.setPaused(server,true);
|
||||
level.setBlockAndUpdate(pos,net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());
|
||||
level.removePlayerImmediately(player,net.minecraft.world.entity.Entity.RemovalReason.DISCARDED);
|
||||
var reasonType=Class.forName("dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason");
|
||||
call(containerType,container,"removeSubLevel",void.class,new Class<?>[]{subType,reasonType},sub,reasonType.getField("REMOVED").get(null));
|
||||
}
|
||||
}
|
||||
static Object call(Class<?> owner,Object target,String name,Class<?> result,Class<?>[] types,Object...args) throws Exception {
|
||||
var signature=java.lang.invoke.MethodType.methodType(result,types);
|
||||
var lookup=java.lang.invoke.MethodHandles.publicLookup();
|
||||
var method=target==null?lookup.findStatic(owner,name,signature):lookup.findVirtual(owner,name,signature);
|
||||
var values=new java.util.ArrayList<Object>();if(target!=null)values.add(target);java.util.Collections.addAll(values,args);
|
||||
try{return method.invokeWithArguments(values);}catch(Throwable failure){throw new RuntimeException(failure);}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user