Links
Tags
Creators
Details
1.2.5.2
Compatibility
Required content
Changes
v1.2.5.2 — 修复:动力传感器扳手旋转(同轴滚转 · 点上/下保倾偏航)· 贴地放置修正 · 倒置朝下时屏幕读数翻正 · 行走时视角摇晃导致 HUD 虚像晃动 · 切换游戏语言后 HUD 文字变乱线 · 节点分类重构 · 视口裁剪 / 菜单命中 · 数控齿轮箱轴面常在 / 扳手回归官方 · 变速器过载后输出恢复 · 输出指令正反转与负行程 · 编码器清零改节点体引脚 · PID/PID_POWER 积分死区 · 频道名旧草稿回写(#10)· 去掉整图保存覆盖(#17)· 封装子图节点数据同步 · 封装子图展开状态跨玩家同步 · 参数输入框实时同步 · 封装子图顶栏合并 · 占用封装禁删 · 键位系统扩容(删除连线 + 像素编辑器动作)· 统一键鼠序列绑定 · 旧键位配置迁移 · 删节点撤销/重做恢复连线(pinId + 占用检测)· 变速器代理态显示(值盒「(代理)」+ 滚轮拒收) / Fix: Kinetic Gauge Wrench Rotation (Shaft Roll · Tilt-Preserving Yaw) · Floor Placement · Inverted Mount Text Upright · View Bobbing Wobble & Garbled HUD Text After a Language Switch · Node Category Refactor · Viewport Cull / Menu Hit · CNC Gearbox Shaft Faces Always Present / Wrench Back to Official · Transmission Output Recovers After Overload · Output-Command Forward-Reverse & Negative Travel · Encoder Reset on the Node Body · PID/PID_POWER Integral Deadband · channel-name stale-draft write-back (#10) · drop whole-graph save overwrite (#17) · sub-graph node data sync · sub-graph expansion state sync across players · live param-box sync · sub-graph top-bar merge · occupied-encapsulation delete guard · display-mode top-bar merge · display lock follows selection · smooth remote display-layout drags · key-binding system gains delete-wire and pixel-editor actions · unified sequence bindings (legacy key-binding configs migrate) · undo/redo node-delete restores wires (pinId + occupancy check) · transmission proxy display (value-box "(proxy)" + wheel refused while node-driven)
🔧 动力传感器扳手 / Kinetic Gauge Wrench
| Change / 变更 | Description / 说明 |
|---|---|
| 🧰 行为变更:三语义 | ① 轴端面(点击轴 ∥ 旋转轴)→ 同轴四态按角点序(右上→右下→左下→左上)滚转 90°,轴不动——对官方的有意偏离(官方端面点击会翻轴断连);② 点上/下 → 沿当前倾侧偏航 90°,屏幕保持朝上/朝下不翻面(第二处偏离);③ 其余面 → 与 Create IWrenchable 默认逐字相同(绕所点面的轴转;facing 同轴面翻 axis_along_first)。环表提纯在 KineticGaugeStates.nextInShaftRoll/nextInYaw(纯函数),KineticGaugePlacementTest 钉死每步绕轴 90°、getClockWise 同向、4 步闭合 / ① shaft-end: roll through the four same-shaft states in corner order, 90° per step, shaft fixed (deliberate deviation — official pivots the shaft); ② Y-face: yaw staying on the current tilt (second deviation); ③ other faces verbatim Create's IWrenchable default. Rings are pure functions in KineticGaugeStates, pinned for 90° steps, getClockWise sense and 4-cycle closure. |
| 🐛 Y 轴滚转环序 (bug 修复) | 竖置轴的滚转环曾写成 NW→SW→NE→SE——第 2/4 步是 180° 对角跳(「互不重合」钉子查不出顺序错)。现改为 getClockWise 环(NW→NE→SE→SW),并由环序钉子(每步法线绕轴恰转 90°)锁死 / The vertical-shaft roll ring hopped 180° on steps 2 and 4; now a getClockWise cycle, locked by ring-order pins. |
| 🐛 横置点上/下偏航 (bug 修复) | ① 竖置 facing 点上/下以前只在两个 along_first 态来回 → 现沿朝下环保倾偏航(倒置屏不翻回朝上);② 四个横置朝向里有两个屏幕朝下反了 → 重排 blockstate:W/N/E/S 全部 x=0 上仰讲台(up-west/north/east/south),偏航四步屏幕都朝玩家。KineticGaugePlacementTest 钉死 / Vertical-facing Y-click used to toggle two states; horizontal yaw had two inverted screens. Table now gives all four horizontals an up-tilted lectern pose. |
| 🐛 同轴四态旋转重合 (bug 修复) | up+false 与 west+false 曾同为 x=0,y=0,north+true 与 up+true 同为 x=0,y=90 —— 扳手绕轴循环里连着两步长得一样(实测「右上→右上→左下→右下」)。现同一旋转轴的 4 个状态落到 4 个互不重合的 (model,x,y)(轴 Z:(0,0)/(0,180)/(180,180)/(180,0);轴 X:y∈{90,270}×x∈{0,180}),与 Direction.getClockWise 同向;KineticGaugeStatesTest.eachShaftAxisHasFourDistinctRotations 钉死 / Two states used to share x=0,y=0 (and another pair x=0,y=90), so a wrench roll around the shaft stuttered. Each shaft axis now maps its 4 states to 4 distinct (model,x,y) triples, matching getClockWise; pinned by eachShaftAxisHasFourDistinctRotations. |
| 🧹 清理 | wrenchAction 分类纯函数与中版的刚体反查(rigidSideTarget/isDisplayFace/isSteepLook)删除;环表提纯为 KineticGaugeStates 纯函数并补环序钉子 / The classifier and the interim rigid-lookup helpers are gone; the rings are pure functions with order pins. |
🧭 贴地/贴顶放置 / Floor & Ceiling Placement
| Change / 变更 | Description / 说明 |
|---|---|
| 🧭 放置重排 | facing 恒为水平(贴墙=点击面,贴地/贴顶=水平正对玩家,不再因陡视变 up/down——2 态换不出 4 偏航角);横置/竖置由 axis_along_first 承担:平视=横置讲台,陡视(nearestLooking 轴竖直)=竖直轴平板;有结构轴时优先对齐(RotatedPillarKineticBlock.getPreferredAxis)。俯仰判定用 nearest-looking 而非 getXRot——Sable 会把 orderedByNearest mixin 到子世界局部系,世界俯仰角在旋转结构上会错(2026-09 实机) / Facing is always horizontal on placement; lectern vs vertical-shaft is the steep-look gate on axis_along_first; structure axis preferred when present; nearest-looking instead of world pitch for Sable-rotated structures. |
🖥️ 倒置屏幕读数 / Inverted Mount Readout
| Fix / 修复 | Description / 说明 |
|---|---|
| 🙃 倒置/朝下读数翻正 (bug 修复) | facing=DOWN 走 blockstate x:180,蓝屏正确朝下,但动态读数随整机一起颠倒(倒置挂墙/朝下时上下翻转)。渲染入口改为 KineticGaugeStates.displayPanel:x:180 时把字形右/上在面板平面内再转 180°(法线不动、右手系保持),从屏幕外侧看文字恢复正立;12 个状态的世界空间文字上方向 Y>0 由 KineticGaugePlacementTest 钉死 / facing=DOWN applies blockstate x:180, so the blue face correctly points down but the dynamic readout flipped with the whole unit. Rendering now goes through KineticGaugeStates.displayPanel, which spins the glyph right/up another 180° in the panel plane on x:180 (normal untouched, still right-handed) so text reads upright from outside. All 12 states' world-space text-up Y>0 is pinned in KineticGaugePlacementTest. |
🔌 编码器清零改节点体引脚 / Encoder Reset on the Node Body
| Change / 变更 | Description / 说明 |
|---|---|
| 🔌 行为变更 | ENCODER 的清零从编辑区参数改为节点体输入引脚(电平触发语义不变:拉高清零、持续拉高保持归零;未接线 = 0)。旧连线 pinId 本就是索引 "0",V5→V6 迁移显式钉到 0 号输入并清掉过期 reset 参数 / The ENCODER reset moves from an edit-area parameter to a body input pin (level-triggered semantics unchanged; unwired = 0). Old wires already carry pinId "0"; the V5→V6 migration pins them to input 0 and drops the stale reset param. |
🎛️ PID / PID_POWER 积分死区 / Integral Deadband
| Change / 变更 | Description / 说明 |
|---|---|
| 🎛️ 积分死区 deadband | 新增可编辑/可连线参数 deadband(默认 0.001):` |
🎛️ 输出指令正/反转与负行程 / Output-Command Forward-Reverse & Negative Travel
| Change / 变更 | Description / 说明 |
|---|---|
| 🐛 负数恒无效 (bug 修复) | MotionQuota.of 用 Math.max(0f, amount) 把负行程打成 0 配额 → 负数 ROTATE/MOVE 当帧完成、形同假值。现行程量取 Math.abs,符号只表示方向 / Negative ROTATE/MOVE quotas were clamped to 0 and completed instantly. Travel is now Math.abs; the sign only encodes direction. |
| 🔄 负行程 = 反转 | ROTATE/MOVE 入栈快照的负数值 → 执行期间输出面 getRotationSpeedModifier = -1(官方 Gearshift 反转语义),编码器积分/瞬时速度同步取输出符号;WAIT / 空闲 / CLUTCH 常接合不反转 / A snapshotted negative ROTATE/MOVE runs the output face at modifier -1 (official Gearshift reverse) and the encoder books the output sign; WAIT / idle / standing CLUTCH never reverse. |
| 🔘 正/反转按钮 | 位移 / 旋转 / 目标转速 / 转速控制编辑区新增「正转/反转」独立 rev 开关(不改数值 EditBox——数值可被引脚覆盖):MOVE/ROTATE 在入栈时对快照值取反;TX_OUT/SPEED_CTRL 在求值结果上取反(与 SPEED_CTRL 的 dir 引脚 XOR)/ Independent rev toggle on Move/Rotate/Target RPM/Speed Control that does not edit the value EditBox (the value is wire-overridable). MOVE/ROTATE negate the snapshotted value at enqueue; TX_OUT/SPEED_CTRL negate the eval result (XOR with SPEED_CTRL's dir pin). |
🔧 数控齿轮箱扳手回归官方 / CNC Gearbox Wrench Back to Official
| Change / 变更 | Description / 说明 |
|---|---|
| 🧹 行为变更 | 扳手点轴端面「翻转输入端」与 action bar 字幕(input face: …)已移除——输入/输出面由放置感知 + autoSenseInputFace 自动识别(官方从动件同款)。任意面扳手走 IWrenchable 默认旋转;disengageForFlip / resyncKineticsAfterFlip 一并删除 / Wrench-on-end-face "flip input end" and its action-bar subtitle are gone — input/output faces are auto-sensed (placement + autoSenseInputFace), like a vanilla driven member. Wrenching any face is the official IWrenchable rotate; disengageForFlip / resyncKineticsAfterFlip deleted with it. |
🎛️ 变速器过载后输出不恢复 / Transmission Output Stays Dead After Overload
| Fix / 修复 | Description / 说明 |
|---|---|
| 🐛 过载拆输出 (bug 修复) | 输出端负载超容 → 整网 overStressed,官方 getSpeed() 恒 0。cleanOrphanedKineticState 用 getSpeed() 判源死活,把仍在转的源打成失速,每 tick detach 拆掉输出侧子树(「过载后自动断开应力」)。输入端应力恢复后 getSpeed() 变回非 0,预检早退认为健康,不会接回已拆下游;只有改转速触发 updateTargetRotation 全量拆建才恢复(「改转速才可以刷新输出」)。现源健康/幻影速判定改用 getTheoreticalSpeed()(= speed 字段,官方 validateKinetics 同款)/ Output load past capacity trips network-wide overStressed, and official getSpeed() is hard-zeroed. The orphaned-state pre-check used getSpeed() for source health, so a live source looked dead and every tick detached, tearing down the output subtree. Once capacity recovered the pre-check saw a spinning source and early-returned — never re-attaching what it tore down; only a target change's full rebuild did. Health/phantom checks now use getTheoreticalSpeed() (the raw field, matching official validateKinetics). |
⚙️ 数控齿轮箱输出端轴面常在 / CNC Gearbox Output Shaft Face Always Present
| Fix / 修复 | Description / 说明 |
|---|---|
| 🐛 放轴不吸附 (bug 修复) | 无指令/空闲自动分离时 hasShaftTowards 把输出端轴面整段否掉——Create 的放置吸附(RotatedPillarKineticBlock.getPreferredAxis)与传动耦合(RotationPropagator 要求双方轴面)都认它,贴上去的传动轴不吸、不连。现改为官方 AbstractEncasedShaftBlock 同款:两端轴面恒在(只看轴向);离合隔离改走官方 SplitShaftBlockEntity.getRotationSpeedModifier(输出面分离时 0)+ 既有 detach/attachKinetics。BE 基类升为 SplitShaftBlockEntity / While idle the clutch used to return false from hasShaftTowards on the output face, which Create's placement snap and axis coupling both consult — shafts placed against the block would not attach. Now axis-only like official AbstractEncasedShaftBlock; isolation moves to official SplitShaftBlockEntity.getRotationSpeedModifier (0 on the output face while disengaged) plus the existing detach/attachKinetics. The BE base class is now SplitShaftBlockEntity. |
🗂️ 节点分类与方块名单 / Node Categories & Per-Block Allowances
| Change / 变更 | Description / 说明 |
|---|---|
| 🗂️ 分类 14→21 (UI 变更) | 节点菜单分组重划:input 拆为 input_ctrl/input_view/input_motion/input_pose,sequential 拆为 sequential_acc/sequential_state;新增 transmission/speed/radar。ROUND 归 math_basic,SQRT/LN/LOG/EXP 移出三角,POSE_CONVERT/SPLIT 归 input_pose。 |
| 📋 名单机制 | 10 处逐节点枚举改为 NodeCategory + NodeAllowance(分类白名单 + 类内黑名单);新增节点落入已允许分类即自动可用。 |
| ➕ 能力补齐 | 程序计算机 / 数控齿轮箱净增 23 项:基础运算 11(ADD…ROUND)、比较与 OR 6、FORMULA/INTERP、控制类 4(PID/PID_POWER/CLAMP/MAP)。传感器净增 POSE_CONVERT/SPLIT。 |
| ➖ 能力收紧 | 蓝图计算机失去 STRESS/RPM(对齐文档「仅动力宿主图」);已有图中节点不删除,仅不能新建。 |
| 🧪 回归 | NodeAllowanceMigrationTest:覆盖比对 + 允许集差异必须等于声明变更 + 例外最小化。 |
🖱️ 图编辑器命中与裁剪 / Graph Editor Hit & Cull
| Fix / 修复 | Description / 说明 |
|---|---|
| 🐛 添加节点菜单命中 (bug 修复) | 菜单面板(含压在顶栏上的顶部)内点击归菜单,不再被顶栏改名框/设置钮误吞;菜单改为最后渲染、盖在顶栏上,与命中顺序一致。 |
| 🐛 展开节点视口裁剪 (bug 修复) | 节点体滚出屏幕、编辑区仍在画面时不再整节点剔除:体/编辑区分别判交;展开高度取有/无 EditState 的较大值(FORMULA MLE 动态行、ACCUMULATOR 动态字段)。 |
🖥️ HUD 虚像与视角摇晃 / HUD Virtual Image & View Bobbing
| Fix / 修复 | Description / 说明 |
|---|---|
| 🚶 行走时虚像晃动 (bug 修复) | Minecraft 把 bobHurt/bobView 写进一个 PoseStack 后乘进投影矩阵(GameRenderer.renderLevel: matrix4f.mul(posestack.last().pose())),它作用于 view space,从不进入 BER 的 poseStack。行走时 HUD 虚像内容随之晃动(关闭视频设置里的「视角摇晃」即完全消失,已实测确认)。数值诊断(ViewBobWobbleDiagTest,16 相位采样整圈步态)定位根因:深度锚定在预 bob 坐标上做,GPU 侧把 bob 乘回顶点时,bob 的视空间平移按玻璃深度(近,~3 格)做透视除 → 虚像按近物视差随玻璃晃动(0.038 NDC ≈ 1.3°),与设计文档 §9.1「无限远聚焦」矛盾。修复(用户 2026-09-19 选定方案 C「远处虚像」):锚定改为沿 bob 后射线(MonitorClipMath.anchoredEmit,先乘 bob 再锚定,发射时预乘 viewRotInv·B⁻¹ 让 GPU 的 bob 恰好抵消)——屏幕位置保持远处画布投影(平移视差按 D+gz 除,小 30+ 倍),只剩与全世界一致的旋转分量(0.0113 NDC,共形下限 0.0123);玩家屏幕定位遮罩的投影眼改为视觉眼 B⁻¹·0(bob 后视线束在预 bob 视空间的汇聚点)——裁剪窗口与玻璃的落差精确为 0.0000,窗口贴住玻璃;深度仍落玻璃平面,遮挡关系不变。文字字形路径(anchorTextVertex)同步修改,锚定数学迁入 MonitorClipMath 纯函数 / Minecraft folds bob into the PROJECTION matrix, acting in view space; it never reaches the BER poseStack — and the HUD virtual-image content wobbled while walking (disabling "View Bobbing" removes it entirely, confirmed). Numeric diagnosis (ViewBobWobbleDiagTest, 16-phase sampling of a full gait cycle) found the root cause: anchoring in pre-bob coords makes the GPU-side bob translation perspective-divide at the glass depth (near, ~3 blocks) → the image swayed with near parallax, glued to the glass (0.038 NDC ≈ 1.3°), contradicting design-doc §9.1 infinite focus. Fix (user-chosen option C, "far virtual image"): anchor along the post-bob ray (MonitorClipMath.anchoredEmit — apply bob first, then anchor; emit premultiplied by viewRotInv·B⁻¹ so the GPU's bob lands exactly on the desired view position) — the screen position keeps the far-canvas projection (translation parallax divides at D+gz, 30+× steadier) and only the world-rotation sway remains (0.0113 NDC, conformal floor 0.0123); the player-screen mask now projects from the visual eye B⁻¹·0 (where the bobed view rays converge) — the clip window tracks the glass with a gap of exactly 0.0000; depth still lands on the glass plane, occlusion unchanged. The text-glyph path (anchorTextVertex) is fixed the same way, and the anchoring math moved into MonitorClipMath pure functions. |
设计决策(用户 2026-09-19 选定,钉在
ViewBobWobbleDiagTest/ViewBobAnchorTest) Design decisions (user-chosen 2026-09-19, pinned in ViewBobWobbleDiagTest / ViewBobAnchorTest)
- 虚像内容 = 远处物体:行走时只随全世界一起转(bob 的旋转分量,远物参照 0.0123 NDC), bob 平移分量必须消失。开发中曾先尝试只把遮罩眼挪到
+B·0,实测否决——遮罩眼与 锚定方式必须配对(沿 bob 后射线锚定 ↔ 视觉眼B⁻¹·0;预 bob 锚定 ↔ 原点眼),+B·0与任何锚定都不封闭(Y 向窗口滑差 0.046 NDC),且它没触及内容摆动的根因。 The virtual image behaves as a far object: only the world-rotation sway remains; the mask eye must be PAIRED with the anchoring (post-bob anchor ↔ visual eye B⁻¹·0; pre-bob anchor ↔ the origin eye) — the earlier +B·0 attempt closed with nothing (Y window slide 0.046 NDC) and never touched the root cause.- 「补偿更差 3.7 倍」的旧测量是稻草人对比:拿静态内容对比摇晃玻璃,量到的本就是 玻璃自己的晃动,不是虚像的性质。 The old "compensating drifts 3.7× more" measurement was a strawman: static content measured against a bobbing glass captures the glass's own motion, not the image's.
- 站定(amp=0)时 B = I,路径与修复前逐字节一致。 Standing still (amp=0) keeps bob the identity: the path is unchanged.
已知范围外 / Known limitation: 同一 PoseStack 里的
bobHurt(受伤抖动)依赖 GameRenderer 私有的hurtTime,无法从外部重建,受伤瞬间那一下抖动不在本次修复范围 (持续不到 1 秒、幅度小)。bobHurtsits in the same PoseStack but depends on GameRenderer's privatehurtTimeand cannot be rebuilt from outside; that brief hurt wobble is out of scope.
🔤 手动字形与语言切换 / Manual Glyphs & Language Switch
| Fix / 修复 | Description / 说明 |
|---|---|
| 🔤 切语言后 HUD 文字变乱线 (bug 修复) | 手动字形路径(不走 MultiBufferSource,直接读 BakedGlyph 几何/UV 生成顶点)曾把取到的 FontSet 与字形 RenderType 各缓存一份,且只判 == null(永不刷新)。主界面切换游戏语言(或任何资源重载)时,旧缓存的字形几何与 UV 仍指向已被替换的 atlas,文字画成一团乱线。修复粒度是 FontSet 本身:getFontSet 现在每次实取(它只是 this.fonts.apply(id) 一次 map 查找,成本可忽略);字形 RenderType 缓存绑定取到的 FontSet 实例 / The manual-glyph path cached the fetched FontSet and the glyph RenderType on == null (never refreshed). A language switch / reload replaces the atlas, so the stale glyph geometry and UV drew the text as a tangle of lines. Fixed at FontSet granularity: getFontSet is now fetched every time (it is just this.fonts.apply(id), a map lookup — negligible), and the glyph RenderType cache is bound to the fetched FontSet instance. |
为什么不能按
Font实例做缓存守卫(第一版踩的坑,已改正) Why the cache guard must not key on theFontinstance (the first version's mistake)Minecraft.font只在构造器赋值一次(this.font = this.fontManager.createFont()), 注册为重载监听器的是fontManager—— 所以语言切换时 Font 实例不变,被重建的是 FontManager 内部的FontSet(FontManager.reload:fontSets.clear()后new FontSet, 旧对象已close())。绑 Font 的守卫永远不会再次触发,等于没修。Minecraft.fontis assigned once in the constructor, and it isfontManager— not font — that is registered as the reload listener. A language switch therefore leaves the Font instance untouched and rebuilds theFontSetinside FontManager (fontSets.clear()thennew FontSet; the old one is closed). A Font-bound guard never fires again. 附注:hudTextRenderType旧注释写着"资源重载后自动更新",实现却是if (== null)—— 注释与实现不符,已按代码实际行为改正 / The old comment claimed "refreshed after a resource reload" while the code wasif (== null); corrected.
无回归测试 / No regression test: 该修复依赖 Minecraft 的资源重载(切换语言), 沙箱内无法驱动,故未钉单测 —— 需游戏内实测:主菜单切换语言 → 进世界看 HUD 文字。 This fix depends on Minecraft's resource reload (a language switch), which cannot be driven in the sandbox, so it is not pinned by a unit test — verify in game: switch the language in the main menu, then enter a world and look at the HUD text.
✨ 删节点撤销/重做恢复连线 / Node-Delete Undo-Redo Wire Restore
| Change / 变更 | Description / 说明 |
|---|---|
| ✨ 删节点撤销/重做恢复连线(含 pinId 占用检测) | 删节点(X 单删 / Delete 多选,含多选节点间互连)原先只入栈 REMOVE_NODE,removeNode 剪掉的连线一去不返——Ctrl+Z 只回来节点空壳。现删除前把触及被删节点的连线(去重)逐条记为携带稳定 pinId 的 REMOVE_CONN(同批 + 同步协作者),批次逆序撤销 = 先 ADD_NODE 恢复节点、再按 pinId 接线;ADD_CONN 恢复时走 addConnectionWithPinIds:pinId 解析不到或输入脚已被占用则静默跳过(不覆盖现有线)。pinId 编入 op stringValue(协议零扩面);删线/连线创建同步携带 pinId,普通删线撤销同样受益。测试:GraphOpTest(打包/解析)+ NodeGraphTest(解析、占用跳过、删后按 pinId 回接、按 pinId 删线)/ Deleting a node (X / Delete, including wires between multi-selected nodes) used to record only REMOVE_NODE — removeNode pruned the wires and Ctrl+Z brought back a bare node. Wires touching the doomed set are now recorded first (de-duplicated) as pinId-carrying REMOVE_CONN ops in the same undo batch and synced to collaborators; the reverse batch restores nodes first, then rewires by pinId. ADD_CONN restore goes through addConnectionWithPinIds: unresolvable pinId or occupied input pin is skipped silently (never clobbers). PinIds ride in the op's stringValue (no wire-format change); wire create/delete carry pinIds too, so ordinary wire-delete undo benefits as well. |
🛠️ 图编辑器与多人协作 / Graph Editor & Collaboration
| Change / 变更 | Description / 说明 |
|---|---|
| 🐛 #10 频道名旧草稿回写 (bug 修复) | 输入框聚焦时面板重建会保留改名前草稿(有意为之,不打断打字),但随后防抖把旧文本当作新改名写回服务端,把权威名顶回去。现引入 busNameUserDirty:只有用户敲键才标脏;程序装入(初值 / 保留草稿 / 远端刷新)一律不得提交。非 dirty 的差异在失焦/关屏/编译时丢弃并提示「已丢弃未确认的频道名草稿」。守卫收在 GraphBusEditor.shouldCommitBusName 一处 / Focused busBox drafts preserved across panel rebuilds used to be re-committed by the debounce, overwriting the authoritative name. Now only real keystrokes set busNameUserDirty; programmatic loads never commit. Stale drafts are discarded with a chat hint. |
| 🐛 #17 整图保存覆盖 (bug 修复) | 编译/保存原先是 BlueprintSavePacket 整图 NBT 整体替换服务端图——客户端副本一旧就把服务端回退,静默冲掉并发编辑。现改为 GraphSaveRequestPacket(只带坐标):服务端在自己的权威图上执行编译复位(GATE/T_FLIPFLOP/LATCH params[1]=params[0],含子图)+ 环检测 + 落盘 + 全量同步。BlueprintSavePacket 与孤儿 loadGraphFromBytes / loadEditorTag 链一并退役删除 / Save/compile used to replace the server graph with the client snapshot. It is now a position-only request; the server runs compile semantics on its own graph. BlueprintSavePacket and the orphaned loadGraphFromBytes / loadEditorTag chain are retired. |
| 🐛 封装子图节点数据同步 (bug 修复) | 子图内编辑公式时,响应器用 host.getGraph().findNode(主图)找节点——找不到就静默 return,SET_FORMULA 根本不发送,本地框却仍显示输入;两端因此各显示各的公式/参数。另:远端 SET_PARAM 刷新用 fieldParamIndices.get(paramIndex) 把字段位次当下标,fi==-1(busBox 槽)时 fields.get(-1) 抛 IOOBE 中断刷新;UI 刷新未校验 ownerNodeId 作用域,子图/主图同 id 互相污染。现改为 ed.getGraph() + indexOf 反查 + 同作用域才刷 UI / Sub-graph formula edits silently dropped (wrong graph lookup), remote SET_PARAM UI refresh crashed on field-slot reverse lookup, and UI refresh ignored scope. |
| 🐛 封装子图展开状态跨玩家同步 (bug 修复) | 远端 EXPAND_NODE/COLLAPSE_NODE 把图数据字段 n.expanded(随 NBT 持久化,也是进入子图时 init 恢复的唯一依据)一起门在 sameScope 里——玩家停在主图时收到的子图展开/折叠 op 被整条忽略,随后进入子图就恢复出过期状态(别人展开的显示折叠、已折叠的显示展开)。现拆数据半边(n.expanded 恒应用到自己子图副本)与 UI 半边(展开集合/编辑状态仍仅同作用域) / Cross-scope remote expand/collapse ops left the local sub-graph copy's n.expanded stale (the flag was gated behind the same-scope UI guard), so entering the sub-graph restored the wrong expansion state. The data half now always applies; the UI half stays scope-gated. |
| 🐛 参数输入框实时同步 (bug 修复) | ① 远端只回显 ff3(float),与正在输入的字符串不一致;② 清空/未完成输入 parseFloat 失败被吞、不发包,对端停在默认值;③ ff3 用 Math.round(float)(返回 int),|v|>2147483.647 时饱和成 Integer.MAX_VALUE,大数被压成 2147483.x。现:SET_PARAM 携带草稿原文(含空串)驱动对端 EditBox 显示(本端聚焦时不覆盖);空/半截输入保持上次权威值只同步草稿;参数值不再进编辑状态指纹(值一变就 createEditState+ff3 会把 000 刷成 0.0、大数刷成科学计数);失焦才归位为 ff3 规范格式并同步给对端,不改权威值;ff3 改 double 舍入;OpExecutor 对值相同的 SET_PARAM 跳过 bumpGeneration / Peers showed ff3(float) instead of the in-progress string; empty/partial input never shipped; ff3's Math.round(float) saturated large magnitudes. SET_PARAM now carries the raw draft (incl. clear); empty input keeps the last committed number; param values are out of the edit-state fingerprint so drafts survive rebuilds; blur normalizes to ff3 without changing the value; OpExecutor skips bumpGeneration when unchanged. |
| 🐛 封装子图顶栏合并 (bug 修复) | 子图的「封装编辑模式」横条(y=2..22)与每帧都画的顶栏重叠、右上 Back 按钮与设置按钮同槽互盖,且 Back 渲染在 y=4、命中却在 TOP_BAR_H+2(渲染/命中脱节,点画的按钮没反应)。现两作用域合并为同一根顶栏(同底色同高度):主图 = 名称 + 设置;子图 = 模式标识「◈ 封装编辑模式 ◈ (n/N)」(超限红色 ⚠ + 提示放得下才画)+ Back 占用设置按钮的同一槽位(渲染/命中几何一致);子图隐藏图名框并在进入时交出焦点 / The sub-graph mode strip (y=2..22) overlapped the always-drawn top bar, the Back button collided with Settings, and Back's render (y=4) and hit region (TOP_BAR_H+2) diverged. Both scopes now share one top bar: main = name + settings; sub = mode indicator (count / over-limit) + Back in the settings slot with matching render/hit geometry; the name box hides in the sub-graph and releases focus on entry. |
| 🐛 占用封装禁删 (bug 修复) | 主图按 X(悬停删除)/ DEL(批量删除)可以直接删除有玩家正在其子图内编辑的封装节点——对端的编辑器被连人带图拽出来。现删除前经 GraphPresenceTracker.encapOccupied 判定(只认节点编辑模式 presence):占用则跳过/拒绝并给动作栏提示「无法删除:该封装内有玩家正在编辑」。与节点软锁同为建议性 presence 守卫(服务端对 presence 是纯中继);批量删除时占用封装跳过、其余照删 / Pressing X (hover-delete) or DEL (bulk delete) on the main graph could delete an encapsulation while players were editing inside its sub-graph, yanking their editor out. Deletion now consults GraphPresenceTracker.encapOccupied (node-editor-mode presence only): occupied encapsulations are refused/skipped with an action-bar hint. Same advisory presence trust level as the node soft lock (the server merely relays presence); in bulk deletes only the occupied ones are skipped. |
| 🐛 显示布局锁跟随选择 (bug 修复) | 显示布局模式的软锁只有拖动才生效、松手即解锁——但本端选择框还在,队友看到的是「无锁可抢」,双方可同时选中/抢占同一元素。现选中元素经 presence selectedNodeId 上报,锁判定改为拖拽 id 或选中 id 任一命中即锁(与节点模式「选中 = 软锁」语义一致);mode 1 时节点图字段(选择/多选/连线拖拽)一律置空,节点图锁列表叠加层过滤 mode 1——显示元素 id 不再以假锁污染相同 id 的节点图节点;协作叠加层的软锁描边同步改为拖拽或选中即常驻显示(此前只画拖拽,选中占锁在队友屏幕上不可见)/ The collaboration overlay's lock outline also renders while merely selected (it previously drew only during a drag, making a selection-held lock invisible to teammates); The display-layout soft lock only applied WHILE dragging and released on mouse-up, yet the local selection box stayed up — teammates saw a grabbable element and both sides could claim the same one. The selected element now rides presence selectedNodeId; the lock hits on dragged OR selected id (matching the node editor's selection-lock semantics). Node-graph fields are zeroed in mode 1 and the node lock overlay filters mode 1, so a display element id can no longer phantom-lock a same-id graph node. |
| 🧹 显示布局远端拖拽平滑 (行为变更) | 节点图远端拖拽有 smoothstep 插值(remote* 字段 + 每帧推进),显示布局的 SET_DISPLAY_LAYOUT 却直接落地坐标——队友拖组件在别人屏幕上一跳一跳。现对齐节点模式:位置变化超阈值(归一化 0.005)时启动插值(GraphNode 新增 layoutLerpT/layoutStart*/layoutTarget*,OpExecutor 在 animateMoves 时置起点/目标),客户端每帧推进 layoutX/Y 并在插值期间强制重建元素缓存(插值不 bump 代际,缓存不强制重建会吞掉动画);缩放/旋转不插值。监视器以 100ms 节流流式发送,多帧插值让远端观感连续 / Remote display-layout drags used to land coordinates instantly and looked jumpy on other clients, while the node graph has smoothstep interpolation. SET_DISPLAY_LAYOUT now starts the interpolation past a normalized 0.005 threshold (new layoutLerpT/layoutStart*/layoutTarget* on GraphNode, armed by OpExecutor under animateMoves), the client advances layoutX/Y per frame and force-rebuilds the element cache while animating (a lerp doesn't bump the generation); scale/rotation don't lerp. With the monitor's 100ms throttled stream, remote motion reads as continuous. |
| 🧹 显示布局模式并入合并顶栏 (行为变更) | 显示器显示布局模式原在顶栏下方自绘一条工具条(< 编辑 / 设置 / S·R)。现并入合并顶栏(与主图/封装子图同一 chrome):左 = 「◈ 显示布局编辑 ◈」+ 选中元素的 S/R 内联编辑,右 = 设置(面板开关)+ 返回两枚 46 宽按钮;渲染与命中几何一致。旧工具条占位行收回——显示区与图层面板整体上移,可视画布更高 / The monitor display-layout mode used to draw its own strip below the top bar (< Graph / Settings / S·R). It is now part of the merged top bar (same chrome as the main graph / encapsulation sub-graph): left = 「◈ Display Layout Edit ◈」+ the selected element's S/R inline editors, right = Settings (panel toggle) and Back as two 46-wide buttons with matching render/hit geometry. The old strip's reserved row is reclaimed — the display area and the layer panel move up, giving a taller canvas. |
⌨️ 键位系统 / Key Bindings
| Change / 变更 | Description / 说明 |
|---|---|
| 🎛️ 键位系统扩容:删除连线 + 像素编辑器 (行为变更) | 键位设置系统新增 11 个可绑定动作:① 删除连线(默认 W,悬停连线按键即删;原 Tab+左键 组合保留);② 像素编辑器的 画笔(B)/橡皮(E)/填充(F)/取色(I)/直线(L)/矩形(R)/抓手(H)/网格(G)/笔刷变小([)/变大(]) 全部改走共享键位系统,出厂键与旧硬编码逐项一致(唯一例外:[ / ] 的默认键取 GLFW 真值 91/93——旧硬编码的 219/221 是 Windows VK 码,经 GLFW keyPressed 永不命中,即 [ / ] 笔刷快捷键借此首次真正可用);1..7 按轨道顺序选工具保持固定。冲突规则(等长全同必拒等)覆盖全部新动作,设置界面即时生效;键位升级为统一序列模型(键鼠交错)——每个动作只有一条触发序列,步骤既可以是按键也可以是鼠标点击,设置里直接录入任意交错队列:点键帽追加键步、点鼠标键把按钮加入队列、再次点击移出(预览行实时显示整条队列,如「Tab → 左键 → A → 右键」),「确定绑定」整条提交;引擎分纯键域/混合域独立推进——键步 keyPressed 推进、鼠标步点击推进,任一事件完成即触发,纯右键=单鼠标步、Ctrl+Z=纯键步、Tab → 左键=出厂删线组合,与框选 Tab 键序跨域并存;上下文菜单恢复键序触发(按绑定键在光标处打开,且吞掉开菜单键的字符——搜索输入框不再吃到这部分输入;吞字符标志在下一按键事件先行作废、不滞留误吞;菜单打开时引擎不喂键——模态菜单持有键盘,搜索框打字不再误触动作);删除连线出厂即恢复老组合 Tab+左键(W 退役)——组合的键盘半边由混合域缓冲推进(Tab 按键触发框选的同时为组合入混合缓冲,事件被引擎消费),与框选键 Tab 的纯键序分域并存、互不清理;平移 = 按住抓图组合键:绑定单键后按住它移动鼠标即拖图、成为唯一触发(绑键后左键拖动停用;出厂未绑默认左键拖动;panByKey 与 mouseMoved 物理键态轮询保证松键即停、切屏/失焦不卡死);纯鼠标与组合可共享同一按钮(出厂 PAN 左键拖动 + 删线 Tab+左键 并存),同钮冲突经前缀歧义校验——等长全同对与修饰 ⊆ 前缀对必拒,修饰可区分的不等长对可共存(如 Ctrl+左键 与 左键→右键 各自只在己方修饰态命中);旧配置迁移——旧版保存从不写标记,配置新增 editorKeys.format=2 标记用于新表;加载时无标记不再整表回出厂:seq 键序值原样保留(历史脏数据——早期构建未初始化的鼠标槽默认 0=左键并写盘、设置里一堆「/ 左键」的根源——只在 .mouse 鼠标槽字段,读取时忽略、保存时移除并写回标记);冲突校验:键序前缀歧义拒绝、PAN 抓图键对所有序列的键步独占 / 像素编辑器操作指南的键位文案改为动态读取当前绑定(模板 + EditorKeys.seqText,重绑后指南同步;模板必须走 I18n.get(key, args...) 带参重载——无参重载对 %s 模板先零参 format,整行渲染成 "Format error: …"),/ Conflict rules cover every new action and the settings tab applies them live; the bindings upgrade to the unified sequence model (interleaved key+mouse) - each action holds ONE trigger sequence whose steps may be KEY presses or MOUSE clicks; the settings record arbitrary interleaved queues: keycap clicks append key steps, a mouse-chip click adds the button and a second click removes it (the preview line shows the whole queue live, "Tab → left-click → A → right-click"), and Bind commits it wholesale; the engine advances two independent domains - key steps via keyPressed, mouse steps via clicks, fired by whichever event completes them (a bare right-click = a single mouse step, Ctrl+Z = a pure key sequence, Tab → left-click = the factory wire chord coexisting cross-domain with the box-select Tab sequence); the context menu regains its key trigger (opens at the cursor on the bound key, swallowing the opening key's character so the search box never eats that input - the swallow flag is voided on the next key event so it never lingers; the engine is not fed while the menu is open - the modal menu owns the keyboard, so typing in the search box can no longer mis-fire actions); delete-wire ships as the legacy Tab+left-click chord again (W retired) - a combo's keyboard half advances through the mixed-domain buffer (the Tab press fires box-select while arming the chord, the key event consumed by the engine), coexisting with the box-select Tab sequence across independent domains; pan = the hold-grab combo: bind one key, hold it and move the mouse to drag the canvas as the only trigger (button-drag panning disabled while bound; unbound by factory with left-drag as the default; panByKey plus a physical-key poll in mouseMoved guarantee release-ends-it and no stuck pan across screen switches or focus loss); a plain and a combo may share one button (factory: PAN left-drag + the wire Tab+left chord coexist), same-button clashes go through the prefix-ambiguity check - equal-length all-equal pairs and mods-contained prefixes are refused, modifier-distinguishable pairs coexist (Ctrl+left-click vs left-click → right-click each fire only in their own modifier state); legacy config migration - the old save() never wrote a marker; configs now carry an editorKeys.format=2 marker, and a marker-less table no longer resets wholesale: its seq values carry over verbatim, while the historical dirty data (early builds' uninitialized mouse slots defaulting to 0 = left button leaking into saves - the source of the screenshot's pile of "/ left-click" rows) lived only in the .mouse slot fields, which are ignored on load and removed on save with the marker written back; conflict checks: key-prefix ambiguity refused, PAN's grab key exclusive against the key steps of every sequence; the pixel editor's guide popup now reads its key text live from the current bindings (templates + EditorKeys.seqText, staying in sync after rebinds; the templates go through the parameterized I18n.get(key, args...) - the no-arg overload zero-arg-formats a %s template first and renders "Format error: ..."); The binding system gains 11 bindable actions: ① Delete Wire (default W, press on a hovered wire; the Tab+click chord stays); ② the pixel editor's Brush(B)/Eraser(E)/Fill(F)/Eyedropper(I)/Line(L)/Rect(R)/Hand(H)/Grid(G)/brush Smaller([)/Bigger()] all move to the shared binding system with factory keys identical to the old hardcodes (one exception: [ / ] default to the true GLFW codes 91/93 - the old hardcode's 219/221 were Windows VK codes that GLFW's keyPressed never delivers, so those shortcuts become functional for the first time); the 1..7 rail-order picks stay fixed. Conflict rules cover every new action and the settings tab applies them live. |
🎛️ 变速器代理态显示 / Transmission Proxy State
| Change / 变更 | Description / 说明 |
|---|---|
| 🎛️ 变速器代理态显示 (行为变更) | 可编程变速器此前无论目标来自节点图还是滚轮值盒,值盒都显示滚轮值——节点控制时玩家盯着的是一个不生效的数字,而滚轮照改(改完立刻被图覆盖,谁说了算看不出来)。现引入代理态:图运行且图中存在 TX_OUT(存储序首个;该规则收敛到 NodeGraph.firstNodeIdOfType,代理态判定与目标取值同处发生,避免两条手写遍历漂移成「盒里写(代理)、实际听滚轮」的死守卫)。此时 ① 值盒显示「已应用目标 (代理)」——取 CscTxApplied(已同步的生效值),不取滚轮值、也不新增"节点意图值"字段;两种态都取绝对值(正负号是"以输出面为参考"的另一套语义,不混进这个格子);② 滚轮/右键拒收输入,走 Create 自带的双边闸门 acceptsValueSettings(客户端输入 ValueSettingsInputHandler:82、服务端收包 ValueSettingsPacket:60、剪贴板读写 ValueSettingsBehaviour:57-69 三处真检查——第三处顺带堵住"用剪贴板把滚轮值灌回来"的旁路),setValueSettings 另做防御性早退;③ 悬停提示第一行改说「目标转速 · 由节点控制」(Create 硬编码的第二行「按住以编辑」删不掉,故由第一行纠正)。代理态由服务端判定并随方块实体包同步(CscTxProxy,仅当代理态或已应用值真的变化时发包,受 4 tick 拆建冷却限频,与官方 setValue → sendData 同量级),客户端只消费、不按本地图推导(本地图会被未 ACK 的编辑带偏)。图停止或图中无 TX_OUT 时完全回到手动态:盒内裸数字、滚轮生效。文案走 lang key 双语(zh (代理) / en (proxy))。代理态下盒内数字受 4 tick 冷却与包延迟影响,可能短暂滞后于节点当前输出;闸门同样滞后 —— 手动态→代理态翻转后、包到达前,客户端仍按旧值放行片刻,玩家可能看到一瞬「可编辑」的假象(值不会落地:服务端按自己的权威值再查一次闸门,客户端先行的输入被拒)。 / The transmission's value box showed the wheel value no matter who owned the target — while the graph drove it the player stared at a number that did nothing and the wheel stayed editable (the graph overwrote it immediately, so ownership was invisible). New proxied state: while the graph runs with a TX_OUT node (first in storage order; the rule now lives in NodeGraph.firstNodeIdOfType, and the proxy flag and the target value are decided at the same site, so two hand-rolled scans cannot drift into a "box says proxied, wheel still wins" dead guard) — (1) the box reads "applied target (proxy)" from CscTxApplied (the synced effective value), never the inert wheel value and with no extra "node intent" field; both modes take the magnitude, since the sign is output-face-relative and belongs to another concept; (2) the wheel / right-click refuses input through Create's own two-sided gate acceptsValueSettings (really checked in three places: client input, the server packet handler and clipboard read/write — the third also closes the "paste the old wheel value back" bypass), with a defensive early return in setValueSettings; (3) the hover tip's first line becomes "Target speed · Node-controlled" (Create's hardcoded "hold to edit" line cannot be removed, so the first line corrects it). The state is server-decided and shipped in the block-entity packet (CscTxProxy, sent only when the flag or the applied value actually changes, cooldown-limited by the 4-tick rebuild — the same order as the official setValue -> sendData), and the client consumes it instead of deriving it from its local graph, which local un-ACKed edits skew. With the graph stopped, or no TX_OUT at all, everything returns to manual mode: bare magnitude in the box, live wheel. Wording rides bilingual lang keys (zh (代理) / en (proxy)). While proxied the number can briefly lag the node's current output because of the 4-tick cooldown and packet latency. The gate lags the same way: between the manual→proxied flip and the packet's arrival the client still lets the interaction through, so the box can look briefly editable — no value lands, because the server re-checks the gate against its own authoritative flag and rejects the client's optimistic input. |
🧪 测试 / Tests
| Change / 变更 | Description / 说明 |
|---|---|
| 🧪 测试 | CompileResetAndBusNameGateTest 钉死 dirty 门闩(含工厂装入/保留/响应器执行路径)与编译复位(含子图递归)+ fieldIndexOf 反查 + ff3 大数不饱和 + 草稿文本随 op;EncapSubGraphExpandSyncTest 钉死展开状态跨作用域数据同步、「进入子图 → init 恢复」全链路(renderBg 恢复块抽为 restoreOrRebuildEditStates 作测试缝隙)+ encapOccupied 占用判定(节点编辑模式才算、显示模式/异作用域/非法 id 不算)/ Pins the dirty gate (including the factory load/preserve/responder path), compile reset (with sub-graph recursion) and field reverse-lookup, ff3 large-magnitude safety and draft text on ops; EncapSubGraphExpandSyncTest pins cross-scope expansion data sync, the full enter-sub-graph → restore chain (the renderBg restore block extracted as restoreOrRebuildEditStates) and the encapOccupied policy (node-editor mode only; display mode / other scopes / invalid ids never count);EditorKeysPixelAndWireTest / EditorKeysSequenceTest 钉死出厂触发(含 [ / ] 的 GLFW 91/93)、键鼠交错队列、能力/同域冲突、PAN 抓图/拖动切换与旧配置迁移(legacy seq 原样保留、.mouse 槽字段弃用并写回标记); EditorKeysPixelAndWireTest / EditorKeysSequenceTest pin the factory triggers (incl. GLFW 91/93 for [ / ]), the interleaved key+mouse queues, the capability/same-domain clashes, the PAN grab/drag switch and the legacy config migration (seq values carry over verbatim; .mouse slots dropped and the marker written back). TransmissionScrollDisplayTest 钉死值盒文本形态(手动态 = 裸绝对值;代理态 = 绝对值 + 空格 + 后缀,含空后缀、Integer 极值与「符号不外泄」),GearboxNodesEvalTest 增补 firstNodeIdOfType(存储序首个 TX_OUT、删掉首个由次个顶上、全无则 -1 = 回落手动)/ TransmissionScrollDisplayTest pins the box-text shape (bare magnitude when manual; magnitude + space + suffix when proxied, incl. the empty suffix, the Integer extremes and the no-sign-leak rule) and GearboxNodesEvalTest now covers firstNodeIdOfType (first TX_OUT in storage order, the next one taking over once the first is removed, -1 when none remain = manual fallback). 游戏内单客户端实测通过(2026-09-27):图 running 且 TX_OUT 在控(节点要 -10、滚轮值停在 256)时,值盒显示 10 (代理)、悬停提示第一行「目标转速 · 由节点控制」、滚轮与右键均无反应;同刻存档 NBT 直读为 CscTxProxy=1 / CscTxApplied=-10 / CscTxScroll=256(region 文件逐块解压核对)。未覆盖:停图回落手动态、图 running 但图中无 TX_OUT、TX_OUT 存在但未接线(按实现应为 0 (代理))。 / Verified in-game on a single client (2026-09-27): with the graph running and a TX_OUT in control (node asking -10 while the wheel sat at 256) the box read 10 (proxy), the hover tip's first line read "Target speed · Node-controlled", and both the wheel and right-click did nothing; the save's own NBT at that moment held CscTxProxy=1 / CscTxApplied=-10 / CscTxScroll=256 (read straight out of the decompressed region chunks). Not covered yet: stopping the graph (fallback to manual), a running graph with no TX_OUT, and a TX_OUT with nothing wired to it (expected 0 (proxy)). |
Optional dependencies
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:1bUvJALH:2BZdLygl"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:1bUvJALH:2BZdLygl"
}

