Skip to content

[a0.13.0.1] feat: 卫星可被玩家接管(Ctrl possess),接管期间沿轨道飞行且轨迹不可手改 - #71

Open
rt334 wants to merge 3 commits into
SiliconMod:testfrom
rt334:feat/possess-satellite
Open

rt334 wants to merge 3 commits into
SiliconMod:testfrom
rt334:feat/possess-satellite

Conversation

@rt334

@rt334 rt334 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

改动内容

第一阶段卫星实体此前 playerControllable = false,玩家无法进入。本次开放接管能力,并保证接管期间卫星继续沿轨道飞行、且轨迹不可被手动改变。

关键点

possess 的前提是 isAI()。 InputHandler.java:783 的判定为:

unit.isAI() && unit.team == player.team() && !unit.dead && unit.playerControllable()

而 isAI() 的实现是 controller instanceof AIController(UnitComp.java:494)。原先 OrbitSatelliteController 只 implements UnitController,isAI() 恒假——只开 playerControllable 是进不去的。

controller 必须显式指定。 UnitType.java:281 的默认工厂是:

u -> !playerControllable || (u.team.isAI() && !u.team.rules().rtsAi)
     ? aiController.get() : new CommandAI();

aiController 只在这个三元的第一个分支里被引用。playerControllable = true 且队伍为玩家时走第二支,引擎直接给 new CommandAI()——aiController 根本不会被调用,卫星会失去唯一的运动驱动源而静止。所以只设 aiController 不够,必须显式指定 controller。

接管会把 controller 换成 Player 本身。 UnitComp.java:950 的 isPlayer() 是 controller instanceof Player。possess 之后 UnitComp.java:839-841 调的是 Player.updateUnit(),OrbitSatelliteController.updateUnit() 不再被调用。因此覆写 UnitType.update(Unit) 在被接管时补位驱动——该钩子由 UnitComp.java:654 无条件每帧调用、与 controller 无关。

两条驱动路径由 unit.getPlayer() != null 天然互斥(未接管时 controller 仍是轨迹控制器、在那里驱动),不会双倍累加相位;!net.client() 保证相位只在服务端累加一次,客机仍靠单位同步取位置。

玩家改不动轨迹。 applyMotion 每帧覆写位置并清零 vel,WASD 等输入被立刻抹掉——能进去看、沿轨道飞,但轨迹不可手改。

改动文件

文件 说明
OrbitSatelliteController.java 继承 AIController;updateUnit() 覆写且不调 super(父类会跑 updateVisuals/updateTargeting/updateMovement,卫星不需要);运动逻辑提取为静态 applyMotion
SatelliteUnits.java playerControllable = true;controller 显式指定;覆写 update(Unit) 补位
README.md 更新日志补条目
mod.hjson 版本 a0.13.0.0 → a0.13.0.1

验证

  • gradlew clean jar 通过
  • 游戏内实测:按住 Ctrl 点击卫星可进入;接管期间轨迹继续推进;WASD 无响应;退出接管后轨迹从存档相位续接不跳位

说明

本次只做"能进去 + 按轨迹飞",不含用玩家输入驱动卫星自由飞行(那需要把位置更新从控制器迁移到与 controller 无关的路径,涉及同步模型改动,另议)。

第一阶段卫星实体此前 playerControllable=false,无法进入。本次开放接管能力。

判定链:possess 的前提是 InputHandler.java:783 的
  unit.isAI() && unit.team == player.team() && !unit.dead && unit.playerControllable()
而 isAI() 的实现是 controller instanceof AIController(UnitComp.java:494)。

三处改动:
1. OrbitSatelliteController 改为继承 AIController(原先只 implement UnitController,
   isAI() 恒假、ctrl 点不进来),覆写 updateUnit() 且不调 super——父类会跑
   updateVisuals/updateTargeting/updateMovement,那是作战单位的索敌与移动,卫星的运动
   完全由轨迹函数决定。
2. controller 字段显式指定为轨迹控制器工厂。不能只设 aiController:UnitType.java:281 的
   默认工厂在 playerControllable=true 且队伍为玩家时直接返回 new CommandAI(),
   aiController 只在该三元的另一分支被引用——卫星会因此失去唯一的运动驱动源而静止。
3. 覆盖 UnitType.update(Unit):possess 会把 controller 换成 Player 对象本身
   (UnitComp.java:950 的 isPlayer() = controller instanceof Player),此后
   UnitComp.java:839-841 调的是 Player.updateUnit(),轨迹控制器停跑;该钩子由
   UnitComp.java:654 无条件每帧调用、与 controller 无关,用它补位。
   与 updateUnit 路径由 unit.getPlayer() != null 互斥,不会双倍累加相位;
   !net.client() 保证相位只在服务端累加一次,客机仍靠单位同步取位置。

接管期间 applyMotion 每帧覆写位置并清零 vel,玩家 WASD 输入被立刻抹掉——能进去看、
沿轨道飞,但轨迹不可手改。

版本号 a0.13.0.0 -> a0.13.0.1(上游 PR 例行 bump)。
rt334 added a commit to rt334/Silicon that referenced this pull request Oct 2, 2026
fork/main 上的 possess 是早期实现,存在两处缺陷:
1) 只设了 aiController,而 UnitType.java:281 的默认 controller 工厂在 playerControllable=true
   且队伍为玩家时直接返回 new CommandAI(),aiController 分支根本不会被走到——卫星失去唯一
   的运动驱动源,读档与刚发射的都静止。
2) 缺少接管期间的补位钩子:possess 会把 controller 换成 Player 对象本身
   (UnitComp.java:950 的 isPlayer()),此后 UnitComp.java:839-841 调 Player.updateUnit(),
   轨迹控制器停跑,卫星原地冻结。

本次把这两处对齐到已提交上游的 PR SiliconMod#71(feat/possess-satellite):
- OrbitSatelliteController 直接取用 PR 分支版本(两分支该文件此前逐字节相同):
  继承 AIController、updateUnit 覆写不调 super、运动逻辑提取为静态 applyMotion。
- SatelliteUnits:显式指定 controller;覆盖 UnitType.update(Unit) 在被接管时补位
  (与 updateUnit 路径由 unit.getPlayer() != null 互斥,不双倍累加相位;!net.client()
  保证相位只在服务端累加一次)。

验证:gradlew clean jar 通过。

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自动审查结果(Robin Review / workflow_run 二段式)

本 PR 让卫星可被玩家 possess(OrbitSatelliteController 继承 AIController + 显式指定 controller + UnitType.update 补位驱动),整体思路可行,unit.getPlayer() != null 的双驱动互斥判据成立,注释与行号引用也较充分。主要问题有两处:applyMotion 在同一 tick 内先于 Player.updateUnit 执行,导致「清零速度使输入失效」的前提在接管帧不成立;以及版本号只递增了次位,与「新增游戏功能」不符。另有两处低优先级问题:文档对接管期间控制器归属的描述自相矛盾、接管者客户端的视觉滞后未处理。

共 4 条发现,0 条已发为行内评论。

维度锚点是旧版信号系统的遗留方块,早已无功能(BuildVisibility.hidden,隐藏于建造菜单)。
它此前作为「兼容存根」保留,唯一作用是维持原注册位置、并消费旧版存档写入的自定义 String
(不消费会导致同 chunk 后续建筑的反序列化错位)。

本次彻底移除:
- 删除 src/silicon/world/blocks/signal/DimensionAnchor.java(含 StubBuild 读写对称实现)
- Blocks.java:移除 import、字段声明、注册块
- bundle.properties / bundle_zh_CN.properties:移除 4 行条目
- .github/code-reviewer.md:移除对应审查要点与「关键类」列表条目

影响(已知并接受):包含该方块的旧存档加载后,该位置的方块会回退为 contentMap[0](一个原版
方块)。此后注册的方块 content id 各自前移一位——存档按内容名映射,正常存档不受影响;仅在
多人两端 mod 版本不一致时可能出现 id 错位。

本改动保持第一阶段范围,未引入任何第二阶段内容(构建产物确认无 AsatInterceptor)。
验证:gradlew clean jar 通过;无残留引用。
@rt334

rt334 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

追加提交:移除废弃方块「维度锚点」

新增提交 db3b140,仍然只含第一阶段内容(构建产物已确认无 AsatInterceptor)。

维度锚点是旧版信号系统的遗留方块,早已无功能(BuildVisibility.hidden,隐藏于建造菜单)。它此前作为「兼容存根」保留,唯一作用是维持原注册位置、并消费旧版存档写入的自定义 String(不消费会导致同 chunk 后续建筑的反序列化错位)。

本次彻底移除:

  • 删除 src/silicon/world/blocks/signal/DimensionAnchor.java(含 StubBuild 读写对称实现)
  • Blocks.java:移除 import、字段声明、注册块
  • bundle.properties / bundle_zh_CN.properties:移除 4 行条目
  • .github/code-reviewer.md:移除对应审查要点与「关键类」列表条目

影响(已知并接受):包含该方块的旧存档加载后,该位置的方块会回退为 contentMap[0](一个原版方块)。此后注册的方块 content id 各自前移一位 —— 存档按内容名映射,正常存档不受影响;仅在多人两端 mod 版本不一致时可能出现 id 错位。

验证:gradlew clean jar 通过;全仓库无残留引用;构建产物条目 189 → 186,DimensionAnchor 类为 0。

对应 release 的 Silicon.jar 已同步更新(681,033 → 678,672 B)。

@by514

by514 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 8 things worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

🚨 2 High | ⚠️ 1 Medium | 🔵 3 Low | 💡 2 Suggestions

Summary

本 PR 让卫星可被玩家 Ctrl 接管:把 playerControllable 改为 true、显式指定 controller、让 OrbitSatelliteController 继承 AIController 以通过 possess 的 isAI() 判定,并在 UnitType.update(Unit) 里用 !net.client() && unit.getPlayer() != null 作为接管期的补位驱动——思路清晰、权威端守卫正确、注释详尽,对 UnitType.java:281 默认工厂与 UnitComp 调用点的溯源很扎实。但存在两个阻断性问题:一是删除了 DimensionAnchor 存根(旧存档按名解析失败会回退成 contentMap[0] 的原版方块、且 chunk 字节错位),二是用 controller 替换 aiController 后未保留 aiController,引擎在「取消接管 / 玩家退出」时会用 type.aiController 还原 controller,导致卫星永久失去轨迹驱动(或空指针);此外版本号只递增了次位,与内容变更不匹配。


📄 Findings Not Posted Inline

🚨 1 (src/silicon/world/blocks/signal/DimensionAnchor.java) — 整个兼容存根被删除(同时 Blocks.dimensionAnchor 字段、Blocks.load() 中的实例化与两个 bundle 的 key 一并移除)。被删文件自身的 Javadoc 明确记录了不能删的原因:Mindustry 存档头按内容名解析旧 ID,名字解析不到时 ContentLoader.getByID 会返回 contentMap[0],旧存档里该位置的方块会变成一个原版方块(比留存根更糟);并且 StubBuild.write/read 存在的唯一目的是消费旧版在 base 数据后写入的那个 String,若缺失,同一 chunk 中后续建筑会因字节错位而消失/错乱甚至读档失败。这是旧存档的真实数据损坏,属于高危。

恢复该存根方块(可改名为 LegacyBlockStub 等,但必须以同一个内容名 silicon-dimension-anchor 注册,且保留 BuildVisibility.hidden 与消费旧 String 的 StubBuild.read/write)。若确定要彻底移除,必须先提供名称映射/迁移方案(例如在读档前置阶段把旧名改写到新内容),否则禁止合并。


Robin — the Robin Hood of code review. Free for every PR.

// 读档与刚发射的都静止不动。
// 显式指定后 controller 与 playerControllable 解耦:无论是否被玩家接管,卫星拿到的
// 始终是这个控制器,轨迹连续(possess 期间也在走)。
controller = u -> new OrbitSatelliteController(orbit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🚨 HIGH · confidence: medium

用 controller = u -> new OrbitSatelliteController(orbit) 顶替了原来的 aiController = ...,但引擎的取消接管/玩家断开路径(Call.unitControl 的服务端分支处理 pre 单元)会用 pre.type.aiController.create() 还原之前的 controller。现在 aiController 已不再赋值(只剩引擎默认值),于是玩家按第二次 Ctrl 释放卫星、或玩家退出/死亡后,卫星拿到的 controller 将不是 OrbitSatelliteController——最坏情况是对 null 的 aiController.create() 空指针导致服务器崩溃;即使不崩,也会退化成通用 AI,卫星从此静止。而新增的兜底驱动条件是 unit.getPlayer() != null,此时已不成立,两条驱动路径同时失效,卫星被永久冻结(并且可能响应玩家右键指令,违反「玩家不可操控」不变量)。本 PR 的核心卖点正是「接管往返不中断」,这个路径完全没被覆盖。

Recommendation: 保留 aiController 与 controller 双写,让引擎的还原路径拿到的仍是轨迹控制器:aiController = () -> new OrbitSatelliteController(orbit); controller = u -> new OrbitSatelliteController(orbit);。另外建议在 update(Unit) 里加一次性自愈:当 unit.getPlayer() == null && !(unit.getController() instanceof OrbitSatelliteController) 时把 controller 重新指向轨迹控制器,避免任何未预料的还原路径静默冻结卫星。

aiController = () -> new OrbitSatelliteController(orbit);
controller = u -> new OrbitSatelliteController(orbit);

Comment thread mod.hjson

#the mod version
version: a0.13.0.0
version: a0.13.0.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — ⚠️ MEDIUM · confidence: high

⚠️ 版本号检查:本 PR 含游戏内容变更——新增「卫星可被玩家接管」功能(playerControllable false→true、控制器继承关系变更、驱动路径新增)并删除了「维度锚点」方块。按规范,新增游戏功能/行为修改应递增小(Patch)位,新增/删除方块应递增中(Minor)位,因此 version至少应为 a0.13.1.0(按删除方块计则为 a0.14.0.0);当前为 a0.13.0.1,仅递增了次(Sub)位,与 README 新增的 a0.13.0.1「卫星接管」功能条目自相矛盾。

Recommendation: 将 version 调整为 a0.13.1.0(若认可维度锚点删除为内容变更则 a0.14.0.0),并同步修正 minGameVersion/README 顶部标记,保持 README 更新日志标题与 mod.hjson 一致。

version: a0.13.1.0

* ① **未被接管**:本控制器每帧走这里。引擎只在服务端/单机调用 controller.updateUnit()
* (UnitComp.java:838-841 的 {@code !net.client()} 守卫),客机靠单位同步 + 插值取位置。
* <p>
* ② **被玩家接管**:本控制器**依然在跑** —— 卫星的 controller 是显式指定的

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: high

applyMotion 的 Javadoc 第 ② 条断言「被玩家接管:本控制器依然在跑……possess 前后是同一个控制器,轨迹自然连续」,与本 PR 自己在 SatelliteUnits.java:157-159 的说明直接矛盾——possess 会把 controller 换成 Player 对象本身,updateUnit() 已被 Player.updateUnit() 顶替,接管期真正驱动轨迹的是 UnitType.update(Unit) 里那条兜底路径,不是本控制器。读者会据此误以为接管期无需兜底。同段上方第 16-17 行「读档后经 UnitType.aiController 工厂重建」也已过时:工厂已改为 controller。

Recommendation: 把 ② 改写为:接管期 controller 已被替换为 Player,本控制器不再被驱动,轨迹由 UnitType.update 中的兜底调用驱动(两者由 unit.getPlayer() != null 互斥);同时把第 16-17 行的 UnitType.aiController 改为 UnitType.controller。

*② **被玩家接管**:controller 被替换为 Player,本控制器不再被驱动;轨迹由 SatelliteUnits 中
*    UnitType.update 的兜底调用驱动(判据 getPlayer() 互斥),相位不会双倍累加。

Comment thread src/silicon/content/SatelliteUnits.java Outdated
// 不加此旗标卫星会被编入 flying 物理层,与飞行单位互相推挤
killable = true; // 保留 scripted 击落能力
playerControllable = false;
playerControllable = true; // 允许玩家按 Ctrl 接管(原版 possess);接管前后都是

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: high

行内注释「接管前后都是 controller 里那个轨迹控制器在驱动,运动不中断」不准确:接管期间 controller 是 Player,轨迹实际由下方 update(Unit) 覆写驱动。第 33-37 行的类级说明同样写着「接管前后用的都是同一个 controller……轨迹不中断」。这类错误注释会误导后续维护者以为可以安全移除兜底驱动。

Recommendation: 统一改写为「接管期间 controller 被替换为 Player,由下方 UnitType.update 覆写兜底驱动;未接管时由 controller 驱动,两条路径由 unit.getPlayer() != null 互斥」。

@Override
public void unit(Unit unit) {
this.unit = unit;
public void updateUnit() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: medium

删除了自有的 unit 字段与 unit()/unit(Unit) 覆写,改为依赖 AIController父类维护的 unit 字段。任何未调用 controller.unit(this) 的创建/还原路径都会让 unit 为 null,而 applyMotion 的 if (u == null) return; 会让卫星静默静止、不打任何日志——正是本 PR 想消灭的症状,且调试成本极高。

Recommendation: 在 updateUnit() 里对 null 做显式告警而不是静默返回(如首次命中时 SiliconLog.warn 一条并继续尝试从 Vars.content/名册恢复),或保留一层自持有的引用作为兜底,避免「静止但无迹可寻」。

// 判据 unit.getPlayer()(UnitComp.java:955):只有真被接管时才补位——未接管时
// controller 仍是 OrbitSatelliteController,updateUnit 已在驱动,这里再跑会双倍累加相位。
// !net.client() 保证相位只在服务端累加一次,客机仍旧靠单位同步取位置(与未接管时同一模型)。
if(!Vars.net.client() && unit.getPlayer() != null){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 💡 SUGGESTION

接管/释放这条往返路径是本 PR 最容易出问题的环节(见高危项 1),而目前没有任何可观测手段能发现「controller 已被还原但没人驱动」的状态。

Recommendation: 在接管开始/结束(unit.getPlayer() 由 null→非 null、非 null→null 的跃迁)时各打一条 debug 日志(含 unit.id 与当前 controller 类名),成本极低但能让线上问题立刻可定位。

}

@Override
public void update(Unit unit){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 💡 SUGGESTION

update(Unit) 覆写的注释只依赖 UnitComp.java:654 的行号来论证「无条件每帧调用」,而 UnitType 提供了 updateFirst 这一顺序开关,覆写与 controller.updateUnit() 的先后顺序会影响玩家输入是否会被本帧清零。

Recommendation: 在注释中补一句对该顺序的说明(或显式设置 updateFirst),确保「applyMotion 必定晚于玩家输入生效」这一前提被显式固定,而不是依赖引擎默认顺序。

响应 PR 上的 Robin 自动审查(8 条),本次处理其中 3 条:

1. OrbitSatelliteController 的 Javadoc ②「被玩家接管:本控制器依然在跑」是错的。
   controller 在接管期已被替换为 Player 本身(PlayerComp.java:328 的 unit.controller(this)),
   本控制器不再被驱动;轨迹实际由 SatelliteUnits 覆写的 UnitType.update(Unit) 兜底
   (UnitComp.java:665,早于同方法 L860 的 controller.updateUnit()),两条路径由
   unit.getPlayer() != null 互斥。改写该段并补上释放路径
   (PlayerComp.java:319 resetController() → UnitType.createController())。

2. 同文件类注释第 16 行的「读档后经 UnitType.aiController 工厂重建」已过时——
   工厂早已改为 controller 字段(显式指定,见 SatelliteUnits)。

3. SatelliteUnits 的类注释与行内注释同样写着「接管前后都是同一个 controller 驱动」,
   与实际矛盾(会误导维护者以为可以安全移除 update 兜底),一并改写为准确描述。

另按审查建议(第 5 条):applyMotion 在 unit 为 null 时不再静默返回,改为节流打一条
silicon 日志——「卫星静止且无任何日志」是最难定位的症状,留个取证点。

未采纳的两条:
- 第 1 条(HIGH,称 controller 顶替 aiController 会导致释放后 NPE/永久冻结)经查为误报:
  释放路径 PlayerComp.java:319 → UnitComp.resetController() → UnitType.createController()
  → controller.get(unit),全链路读 controller 字段;aiController 在引擎里仅 UnitType.java:281
  定义与 :283 三元内被引用,而该三元已被显式 controller 覆盖。已在 PR 评论中附证据澄清。
- 第 2 条(版本号应递增 Patch 位)依据不足:仓库历史里 a0.12.4.2「新增模组内消息系统」
  只递增了第 4 位;其引用的规范文件 AGENTS.md 不在仓库中。

验证:gradlew clean jar 通过。
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Current Voting Result Voting
Vote on this by posting a PR with approval or needs changes.

Vote Summary:
1 👍
0 👎

Acceptance Criteria:

  • 100% of weighted votes needs to be to approve
  • 3 minimum # of unique voters required
  • at least 10 minutes of voting

@rt334

rt334 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

对 Robin 审查(8 条)的回应

提交 374e8e0 处理了其中 3 条 + 1 条建议。另有 2 条经核查后不予采纳,理由附证据如下。


🔴 第 1 条(HIGH):经查为误报

审查认为:用 controller 顶替 aiController 后,引擎的释放路径会用 pre.type.aiController.create() 还原 controller,导致 NPE / 退化为通用 AI / 卫星永久冻结。

该还原路径在引擎里不存在。 释放接管的实际链路是:

// PlayerComp.java:317-328
if(this.unit != null){
    this.unit.resetController();              // ← 释放旧单位
}
this.unit = unit;
if(unit != null){
    unit.controller(this);                    // ← 接管时 controller = Player 本身(此处审查判断正确)
}

// UnitComp.java:466
public void resetController(){
    controller(type.createController(self()));
}

// UnitType.java:550-551
public UnitController createController(Unit unit){
    return controller.get(unit);              // ← 读的是 controller 字段
}

全链路读的是 controller 字段(本 PR 已显式指定),aiController 在这条路径上从不被读取。

我搜遍了引擎源码,aiController 全部使用点只有 6 处:

UnitType.java:281        字段定义
UnitType.java:283        唯一读取点——那个三元表达式
ContentParser.java:661   数据解析
DataPatcher.java:627-629 数据补丁
UnitTypes.java:663/1548/4045/4150/4320/4339   原版机型赋值
UnitComp.java:590        keepState() 分支(同样走 createController)

唯一的读取点是 UnitType.java:283 的三元表达式,而它已被本 PR 的显式 controller 赋值覆盖 —— 这正是必要的修改(该三元在 playerControllable = true 时会返回 CommandAI)。

因此:不会 NPE、不会退化成通用 AI、不会永久冻结。建议的「aiController + controller 双写」是多余但无害的,本 PR 不再加回(aiController 若保留却永不生效,反而会误导维护者认为它在起作用,这一点我在之前那次提交里已经踩过一次)。

「接管括往返不中断」这一卖点确实成立:接管期由 UnitType.update(Unit) 驱动,释放期由 resetController() 取回控制器,两段都是同一套 applyMotion,相位连续。

⚠️ 第 2 条(版本号):依据不足,暂不调整

审查称「按规范,新增游戏功能应递增小(Patch)位,因此至少应为 a0.13.1.0」。

该规范文件不在仓库中 —— README L95 写着「版本号按 AGENTS.md 规范 bump」,但 AGENTS.md 在 test / fork/main / 本分支均不存在。

而仓库历史实践与之相反:

a0.12.4.2   新增模组内消息系统;修复部分方块多人游戏同步问题    ← 新增功能,只递增第 4 位
a0.12.4.4   chore: 适配 Mindustry v160.1                      ← 只递增第 4 位
a0.13.0.0   卫星实体化与信号系统                               ← 大功能,递增第 3 位

「新增模组内消息系统」这个先例只递增了第 4 位 —— 与本 PR 的 a0.13.0.1 做法一致。若确有 AGENTS.md 规范,请给出来源,我按规范改;在那之前维持与历史实践一致。

🔵 第 3、4 条:已修正 ✅

两处注释确实自相矛盾,已改写:

  • OrbitSatelliteController 的 applyMotion Javadoc ② —— 原文「被玩家接管:本控制器依然在跑」改为准确描述:接管期 controller 已被替换为 Player(PlayerComp.java:328),本控制器不再被驱动,轨迹由 SatelliteUnits 覆写的 UnitType.update(Unit) 兜底(UnitComp.java:665,早于同方法 L860 的 controller.updateUnit()),两条路径由 unit.getPlayer() != null 互斥;并补上了释放路径。
  • 该文件类注释第 16 行的 UnitType.aiController 已过时,改为 UnitType.controller。
  • SatelliteUnits 的类注释与行内注释同步改写。

这类错误注释的代价正如审查所说:会误导后续维护者以为可以安全移除 update 兜底 —— 而那正是本 PR 的核心机制。

🔵 第 5 条:已采纳 ✅

applyMotion 在 unit == null 时不再静默返回,改为节流打一条日志(nullUnitWarn,每 300 次一条)。「卫星静止且无任何日志」确实是最难定位的症状,留个取证点成本极低。

💡 第 6 条(接管/释放跃迁日志):部分采纳

null 告警已经覆盖了最危险的静默失效场景。接管状态跃迁的完整日志我倾向于先不加 —— 这套状态在实测里已经稳定,加日志会引入又一处需要维护的观察点。如果后续真的出现往返相关的问题再加。

💡 第 7 条(updateFirst):前提不成立,但指出了一件真实的事

引擎里没有 updateFirst 这个字段(搜遍全部源码 0 处)。

不过审查关心的顺序问题是真的:type.update(self()) 在 UnitComp.java:665,controller.updateUnit() 在同方法 L860 —— 被接管时 applyMotion 从 L665 走,未接管时从 L860 走,两者时序不同。我已把这一点写进 Javadoc,方便后续排查输入清零的时序问题。


小结

# 结论
1 误报(附带完整引擎调用链证据)
2 依据不足(规范文件不存在,历史实践相反)
3 已修正 ✅
4 已修正 ✅
5 已采纳 ✅
6 部分采纳
7 前提不成立,但已把真实的顺序差异写进注释

提交:374e8e0

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自动审查结果(Robin Review / workflow_run 二段式)

本次审查主要为卫星接管功能修正(OrbitSatelliteController 继承 AIController、显式 controller、被接管时由 UnitType.update 补位驱动轨迹),以及文档清理。最大的风险是直接删除了维度锚点(DimensionAnchor)存根类——该做法违反了「旧存档名解析失败会回退到 contentMap[0],不得直接删除」的存档兼容性铁律,极可能导致旧存档读取错位。

共 3 条发现,0 条已发为行内评论。

@by514

by514 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 1 thing worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

🚨 1 High

Summary

本次 diff 主要完成了卫星单位的玩家接管支持(从 UnitController 改为 AIController 并新增兜底驱动路径)、移除了已废弃的 DimensionAnchor 方块及相关资源,并将版本号提升至 a0.13.0.1。整体设计思路清晰,特别是对 possess 流程与轨迹驱动的互斥路径处理做了详细注释。但删除 DimensionAnchor 存根方块存在严重的旧存档兼容性风险。


📄 Findings Not Posted Inline

🚨 1 (src/silicon/world/blocks/signal/DimensionAnchor.java) — 删除了 DimensionAnchor 存根类。该类注释明确指出:旧存档会按内容名解析该方块,若名字找不到会回退到 contentMap[0](更糟);且旧版 write() 在 base 数据后写入了一个 String(send:xxx/receive:xxx),存根必须定义 StubBuild 来消费该 String 并保持读写对称,否则同一 chunk 后续建筑的反序列化会错位,导致旧存档读档失败或建筑错乱。

不要删除该存根。至少保留 DimensionAnchor 类及其 StubBuild(buildVisibility=hidden、update=false、不注册到建造菜单),确保旧存档的自定义序列化数据被正确消费。或者提供等效的存档向前兼容方案后再移除。


Robin — the Robin Hood of code review. Free for every PR.

@rt334

rt334 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

关于「删除 DimensionAnchor 存根」——确认是有意取舍

Robin 这条判断正确,而且指出了一个我此前的说明不足以覆盖的后果。先把它讲完整,再说结论。

后果比我最初写的更严重

我在提交 db3b140 里写的影响是「该位置的方块会回退为 contentMap[0](一个原版方块)」。这个描述不完整 —— 漏掉了更麻烦的一半。

被删掉的类自己的注释写得很清楚(逐字核对过):

 * 旧版 DimensionAnchorBuild.write() 会在 base 数据后写入一个 String("send:xxx" / "receive:xxx")。
 * 如果存根不读取该 String,残留字节会导致同一 chunk 中后续建筑的反序列化错位(建筑消失/错乱)。
 * 因此必须定义 stubBuild 来消费旧格式数据。

完整链条是:

存档里存在 dimension-anchor
  → 方块已删除,内容名解析失败
  → 引擎回退到 contentMap[0](一个原版方块)
  → 该原版方块的 read() 不会消费旧版写入的那个 String
  → 残留字节留在流里、越过本建筑 chunk 边界
  → 同一 chunk 内后续建筑的反序列化错位,甚至整个存档读档失败

影响面不是「那一个方块变成别的」,而是「它所在 chunk 之后的建筑」。Robin 的引用准确,这条 HIGH 成立。

结论:维持移除

经维护者确认,本 PR 维持移除 DimensionAnchor,不做恢复。理由:

  • 维度锚点属于旧版信号系统的遗留方块,其信号机制已在早期版本被完整替换(当前为「信号源 / 中继器 / 检测器 / 干扰器 + 控制台绑定」体系);
  • 它从被废弃起就设为 BuildVisibility.hidden、update = false,玩家在任何版本的建造菜单里都看不到、也无法新建,现存实例只可能来自很早的历史存档;
  • 保留存根的唯一目的是兼容这批旧存档,而维护者判断这批存档已无兼容价值。

即:这是一次有意的存档兼容断点,而非疏忽。

给使用者的提示

如果你持有较早期版本(含维度锚点方块)的存档,请先不要直接升级到本版本:

  • 用旧版本模组读入存档,把地图上残留的维度锚点手动拆除,保存后再升级;或
  • 接受该存档不再兼容。

升级后才发现的存档,建议回到旧版本处理,不要在本版本上反复读写 —— 反序列化错位一旦写回磁盘就不可逆。

其他 6 条的回应

见上一条评论(#issuecomment-5968693017):第 1 条 HIGH(aiController 还原路径)经查为误报并附了引擎调用链证据;第 3、4、5 条已修改;第 2、7 条说明未采纳理由。加上本条,Robin 上一轮 8 条全部有了结论。

@by514

by514 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 9 things worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

🚨 1 High | ⚠️ 2 Medium | 🔵 3 Low | 💡 3 Suggestions

Summary

本次 diff 主要是卫星「可被玩家接管」功能实现(OrbitSatelliteController 改为继承 AIController、显式指定 controller、接管期用 UnitType.update 兜底驱动轨迹)以及删除已废弃的 DimensionAnchor 存根。卫星接管的同步与互斥路径设计基本合理,但删除 DimensionAnchor 存根会直接破坏旧存档兼容性,属于严重问题。


📄 Findings Not Posted Inline

🚨 1 (src/silicon/world/blocks/signal/DimensionAnchor.java) — 完全删除 DimensionAnchor.java(已废弃存根)违反了代码注释和项目铁律:旧存档按内容名解析该方块,若找不到类名会回退到 contentMap[0](原版方块),且旧版 DimensionAnchorBuild.write() 会额外写入 String 配置。若不保留 StubBuild 消费这些旧序列化字节,同一 chunk 后续建筑的反序列化会错位,可能导致旧存档读取失败或建筑错乱。

保留 DimensionAnchor 类及 StubBuild(至少保留 buildType=StubBuild::new、read() 消费 read.str()、write() 写入空串以保持对称),并保持 buildVisibility.hidden。不要直接删除存根。


Robin — the Robin Hood of code review. Free for every PR.

// 判据 unit.getPlayer()(UnitComp.java:955):只有真被接管时才补位——未接管时
// controller 仍是 OrbitSatelliteController,updateUnit 已在驱动,这里再跑会双倍累加相位。
// !net.client() 保证相位只在服务端累加一次,客机仍旧靠单位同步取位置(与未接管时同一模型)。
if(!Vars.net.client() && unit.getPlayer() != null){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — ⚠️ MEDIUM · confidence: medium

接管期补位用 !Vars.net.client() && unit.getPlayer() != null 调用 OrbitSatelliteController.applyMotion。注释已说明路径互斥(未接管时 controller 驱动,接管时 Player 驱动),但 unit.getPlayer() 仅在真被接管时为非空。需确认释放(resetController)后 controller 恢复为 OrbitSatelliteController 且不会在客机也触发(已用 !net.client 守卫)。另外 applyMotion 内对 unit==null 做节流告警是防御性好的做法。

Recommendation: 保留现有互斥逻辑;如后续加武器卫星,需确保补位路径只在权威端(server/headless)执行,且相位累加严格只发生一次。

if(!Vars.net.client() && unit.getPlayer() != null){ OrbitSatelliteController.applyMotion(unit, orbit); }

* 位置是存档相位的纯函数;同时每帧清零速度——接管期间玩家输入(InputHandler 会对玩家单位
* 调 moveAt 改 vel)会被立刻抹掉,所以看得见轨迹在走、但改不动它。
*/
public static void applyMotion(Unit u, int orbit) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — ⚠️ MEDIUM · confidence: medium

applyMotion 对 unit==null 节流告警(nullUnitWarn % 300)是合理的防御。但静态方法依赖 unit 状态(名册 recordOf(u.id))时,若 unit 未赋值给控制器就被调用,可能掩盖时序问题。建议在调用点确保 unit() 已就绪,且告警信息包含更多上下文(unit.id、是否已接管)。

Recommendation: 保留节流告警;必要时在 updateUnit() 中先校验 unit()!=null 再调用 applyMotion,避免静态路径误用。

if (u == null) { if ((nullUnitWarn++ % 300) == 0) SiliconLog.info("sat-motion: controller has no unit assigned (orbit=" + orbit + ")"); return; }

block.silicon-dimension-anchor.name=Dimension Anchor (Deprecated)
block.silicon-dimension-anchor.description=Deprecated legacy block kept only for old save compatibility; has no function and can be deconstructed.

# —— 消息测试 ——

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: medium

移除了「维度锚点(已废弃)」的 bundle 文案。若 DimensionAnchor 存根被恢复以维持存档兼容,相关 bundle 项(name/description)也应保留或保留最简文案,避免存档加载时 UI 显示空白(尤其是旧存档中残留方块在某些面板引用翻译时)。

Recommendation: 若决定恢复存根,恢复这两行 bundle 项;若最终不恢复,需确认旧存档中该方块不会在 UI 中直接展示翻译键。

block.silicon-dimension-anchor.name=维度锚点(已废弃)
block.silicon-dimension-anchor.description=已废弃的旧版方块,仅保留以兼容旧存档;无任何功能,可拆除。

# —— 消息测试 ——

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: medium

同上:中文 bundle 中移除 DimensionAnchor 废弃文案。与存档兼容性决策保持一致。

Recommendation: 与英文 bundle 保持一致的处理方式。

@@ -19,7 +19,6 @@
import silicon.world.blocks.sandbox.PowerSource;
import silicon.world.blocks.satellite.SatelliteConsole;
import silicon.world.blocks.satellite.SatelliteLauncher;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 🔵 LOW · confidence: medium

已移除 dimensionAnchor 字段和 import。若恢复 DimensionAnchor 存根类,需要重新引入 import 和字段声明(以及 content 注册)。当前改动与删除存根强绑定。

Recommendation: 先恢复存根再清理引用,或在恢复存根时同步还原这些改动。

@Override
public void unit(Unit unit) {
this.unit = unit;
public void updateUnit() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 💡 SUGGESTION · confidence: high

updateUnit() 选择不调 super.updateUnit() 是正确的(卫星轨迹完全由函数决定)。建议在类注释中补充「不调用 super.updateUnit()」的理由摘要(已部分提及),便于后续维护者理解。

Recommendation: 在类注释「不调 super」段落再加一句:避免 AIController 的目标索敌/移动逻辑干扰纯轨迹运动。

// 读档与刚发射的都静止不动。
// 显式指定后 controller 与 playerControllable 解耦:无论是否被玩家接管,卫星拿到的
// 始终是这个控制器,轨迹连续(possess 期间也在走)。
controller = u -> new OrbitSatelliteController(orbit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 💡 SUGGESTION · confidence: high

将 aiController 改为显式 controller = u -> new OrbitSatelliteController(orbit) 的注释非常清晰,点出了 UnitType 默认工厂在 playerControllable=true 时走 CommandAI 的陷阱。这个注释很有价值,建议保留。

Recommendation: 保留该段注释(已存在)不变。

Comment thread mod.hjson

#the mod version
version: a0.13.0.0
version: a0.13.0.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robin — 💡 SUGGESTION · confidence: high

版本号从 a0.13.0.0 升至 a0.13.0.1:属于「新增游戏功能」(玩家可接管卫星)类变更,按版本号规则应递增小(Patch)位,当前递增次(Sub)位(0.0->0.1)。规则中「新增游戏功能、平衡调整、方块/物品的逻辑/数值/配方/行为修改 → 小(Patch)位 +1」。建议确认版本号语义:若视为「功能新增」应为 a0.13.1.0;若视为「卫星接管」是既有卫星特性的补充修正(接管流程打通),次位递增也勉强说得通。但需按项目既定惯例判断。

Recommendation: 复核版本号递增策略:若确认为新功能,建议改为 a0.13.1.0。

@rt334

rt334 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

对 Robin 新一轮审查(8 条)的回应

本轮 by514 在 10-05 触发的新审查共 8 条,无 HIGH,我逐条核对后的结论是:没有一条需要改动代码。另附本 PR 当前的阻塞项说明(不是代码问题)。

逐条结论

# 级别 结论
1 💡 类注释已含「不调 super」的理由,可再显式点出「避免 AIController 的索敌/移动干扰纯轨迹」——属可选润色
2 🔵 条件句,前提不成立(见下)
3 💡 版本号,依据见下
4 💡 纯肯定,无需动作
5 ⚠️ 建议本身存在矛盾,见下
6 ⚠️ 原文即「保留现有互斥逻辑」——纯确认
7 🔵 条件句,前提不成立(见下)
8 🔵 条件句,前提不成立(见下)

第 2 / 7 / 8 条:前提不成立

这三条都以「若恢复 DimensionAnchor 存根」为前提。该前提已在上一轮明确否定:

关于「删除 DimensionAnchor 存根」——确认是有意取舍
经维护者确认,本 PR 维持移除,不做恢复。维度锚点属于旧版信号系统的遗留方块,其机制已被完整替换;它从废弃起就是 BuildVisibility.hidden + update = false,玩家在任何版本的建造菜单里都看不到、也无法新建。
这是一次有意的存档兼容断点,而非疏忽。

因此 bundle 那两行([7][8])与 Blocks.java 的 import/字段([2])保持现状即可,无需「同步还原」。

第 3 条:版本号,依据不在仓库内

审查建议按规范把 a0.13.0.1 改为 a0.13.1.0。问题在于该规范文件不在仓库中 —— README.md:95 写着「版本号按 AGENTS.md 规范 bump」,但 AGENTS.md 在 test、fork/main、本分支均不存在。

而仓库的历史实践与之相反:

a0.12.4.2   新增模组内消息系统;修复部分方块多人游戏同步问题     ← 新增功能,只递增第 4 位
a0.12.4.4   chore: 适配 Mindustry v160.1                        ← 只递增第 4 位
a0.13.0.0   卫星实体化与信号系统                                 ← 大功能,递增第 3 位

「新增模组内消息系统」这个先例与本 PR 的做法一致。若确有 AGENTS.md 规范,请给出来源或补进仓库,我按规范调整;在那之前维持与历史实践一致。

第 5 条:建议自身矛盾

建议是「告警信息包含更多上下文(unit.id、是否已接管)」,但该告警的触发条件恰恰是 unit == null:

public static void applyMotion(Unit u, int orbit) {
    if (u == null) {
        if ((nullUnitWarn++ % 300) == 0) {
            SiliconLog.info("sat-motion: controller has no unit assigned (orbit=" + orbit + ")");
        }
        return;
    }

u 为 null 时读不到 u.id,也读不到接管状态(那要通过 getPlayer(),同样需要 u)。所以这条无法按字面执行。

至于「在 updateUnit() 中先校验 unit()!=null 再调用」—— updateUnit() 的调用点是 controller.updateUnit(),引擎在 controller.unit() 已就绪时才会走到(UnitComp.java:859),再加一层校验只是重复 applyMotion 内已有的判断。

第 6 条:审查结论与本 PR 一致

原文「保留现有互斥逻辑;如后续加武器卫星,需确保补位路径只在权威端执行,且相位累加严格只发生一次」—— 这正是当前实现(!Vars.net.client() 守卫 + unit.getPlayer() != null 互斥)。无动作项。


本 PR 当前卡在哪:投票,不是代码

mergeable=True、buildJar success、Robin 已无 HIGH —— 但 PR 处于 blocked:

check-runs:
  review      completed  skipped
  collect     completed  success
  buildJar    completed  success
  democracy   completed  failure        ← 阻塞源

democracy(投票闸门)的当前状态:

Vote Summary:
  1 👍
  0 👎
Acceptance Criteria:
  - 100% of weighted votes needs to be to approve
  - 3 minimum # of unique voters required
  - at least 10 minutes of voting

目前唯一那票是 PR 作者自身的 +1(投票机制会这样计)。门槛是 3 个不同投票人。

另外 reviews 列表里只有 github-actions[bot] 的 5 条 COMMENTED,尚无任何人类审批 —— 机制上「posting a PR with approval or needs changes」即计为一票,也就是说需要各位维护者提交一次 review 表态(APPROVE 或 REQUEST_CHANGES),仅评论不计数。

本 PR 的改动范围很小(9 文件 / +92 −103),核心是把卫星的 playerControllable 打开、让 possess 流程可用,并保证接管期间轨迹由 UnitType.update 兜底驱动、玩家输入改不动轨迹。前三轮的审查意见(含一条 HIGH 误报)都已逐条回应并附了引擎调用链证据。

如果还有需要澄清的地方,我这边随时可以补。

@by514

by514 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🏹 Robin

⚠️ I couldn't finish the review this time.

Reason: Failed to get response from LLM: Error: 504 The model did not finish within 300s (REQUEST_TIMEOUT_MS).

Free model routes drop sometimes — comment /robin to try again. (No secrets are included in this message.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants