Skip to content

[a0.13.0.0] feat: 卫星实体化与信号语义重构——信号与卫星·第一阶段 - #50

Merged
Xiaobei08 merged 94 commits into
SiliconMod:testfrom
rt334:feat/satellite-v2
Oct 1, 2026
Merged

Xiaobei08 merged 94 commits into
SiliconMod:testfrom
rt334:feat/satellite-v2

Conversation

@rt334

@rt334 rt334 commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator

说明

#34 曾合并后经 #48 回退。本 PR 基于回退后的 test 以 revert-of-revert 重建(1465f2f 恢复完整第一阶段),并叠加自查修复与调参(40f653b、a5a26d8,取代已关闭的 #49)。共 3 个提交。

主要变更

  • 卫星实体化:机型按轨道划分(LEO/MEO/GEO/SSO,SatelliteUnits),确定性轨道控制器 OrbitSatelliteController(位置=相位+时间的纯函数,读档续接不跳位);targetable/hittable=false 全隔离——不可被索敌/伤害/碰撞(含卫星互不攻击、地面单位无法攻击)、玩家与逻辑不可操控、drawMinimap=false 小地图不显示过境、useUnitCap=false 不占队伍单位上限且免疫超限击杀(UnitComp.java:596)
  • 信号语义:星下点覆盖圆 30/45/60/20 格;每星强度 1.5、按发射时固化信道扣同信道干扰、总和扣底噪 0.5——首颗有效强度 1.0,单星即可激活中继器转发(阈值 >0.5),叠星线性提升抗干扰裕度;干扰器可局部打断卫星绑定
  • 名册制 SatelliteRecord(unitId/编码/信道/轨道/相位):控制台存档块 v2 代存(旧档兼容),ResetEvent 清空 + WorldLoadEvent 对账,未绑定兜底记录;相位保存时推进、读档不跳位
  • 配对修复:中枢/控制台 1:1 配对只统计地面覆盖(新增 SignalChannel.inGroundSignalRange),卫星覆盖仅解锁「远方指派」,消除多中枢 MULTI_HUB 自锁
  • 测试卫星沙盒专属:非沙盒模式中枢不显示选项且不生产(testSatelliteAvailable),发射权威端拒绝(新失败码 LAUNCH_TEST_SANDBOX,端到端 toast);SSO 按钮全模式可见,绑定的中枢选择信号卫星时 SSO 即灰化(与生产进度无关)
  • H 键覆盖:逐星绘制星下点覆盖盘(编码专属色、未绑定卫星蓝渐变、被压制格不画);数字模式卫星强度与地面信号同模型合成
  • UI:中枢卫星种类按钮只留名字(信号卫星 / 测试卫星),描述文字移除
  • 发射流程不变:控制台绑定信号 → 地面 1:1 配对 → 扣燃油/缓冲电力 → 实体入轨 + 特效音效

行为变化(需要注意)

  1. 首颗卫星即可激活卫星级联中继器(有效强度 1.0 > 阈值 0.5),叠星提升抗干扰裕度
  2. 卫星不再占用队伍单位上限名额
  3. 旧存档升级:v1 存档的抽象卫星消失,此后卫星随存档持久化
  4. 全部控制台被毁 → 名册丢失,在轨卫星退化为「未绑定」(覆盖强度保留、H 键蓝渐变显示、不参与绑定)
  5. 已在沙盒发射的测试卫星随存档在其他模式读入仍存在(仅关闭新发射入口)

验证

  • javac(JDK17 + 官方 v159.7 jar)在 test 最新演进之上全量编译通过,仅存量 BlockSearch unchecked 提示
  • 自查核对:读档控制器恢复(UnitComp aiController 工厂重建)、名册存取(arc ObjectMap supplier 写入语义)、幽灵登记(onRemoved 注销)、防伪造(applySnapshot 服务端守卫 + sat-launch 鉴权)、跨模式测试卫星拦截链(UI 隐藏 → 生产守卫 → 权威拒绝)

后续调参与修复(v2 分支追加提交)

  • 覆盖与强度重定参:覆盖半径 LEO 40 / MEO 60 / GEO 80 / SSO 25 格,每星强度 LEO 1.5 / MEO 1.3 / GEO 1.1(SSO 特例 1.5)——轨道越高覆盖越大、强度越低;首颗即可激活中继器(首颗有效强度 1.0/0.8/0.6 均过阈值);约 10 颗近似全图(250×250 参考联合覆盖约 98%,角落由轨道扫掠覆盖)
  • 删除弃用的「卫星在轨」状态 buff(含注册、发射/加入时施加、双 bundle 键);中枢「可发射」浮动提示改为程序化标记
  • 右下角悬停面板修复:机型 hitSize 7→24(面板触发窗 = 5 + hitSize/2,原 ~8.5px 几乎无法命中);程序化生成 32×32 uiIcon 替换 error 白方块
  • 机型名双重前缀修复:MappableContent 构造会无条件再加 silicon- 前缀,卫星机型原传入已带前缀名导致内容名变 silicon-silicon-*、bundle 键全部落空(悬停面板显示内部键)——改传未加前缀名,与 Blocks 惯例一致,bundle/贴图键正确命中
  • 世界内名字标签:先常驻、后改悬停显示,最终按要求整体移除

迭代记录(第二轮审查修复后追加)

  • 审查修复(Robin 两轮):sat-launch 全部失败路径回包(含异常兜底)+ 客户端 fail 字符串映射为「发射失败」toast(此前被静默忽略);NetSync 空玩家防御;Lines 笔画复位;console↔hub 绑定校验已在 launch() 内实现(5/6/7 号码)
  • 覆盖半径随图幅短半轴等比缩放(0.32/0.48/0.64/0.20 × 短半轴,与轨道飞行圆共用锚点)——「约 10 颗近似全图」在任意图幅下成立,250×250 基准数值不变
  • 卫星信号改对数叠加:最强一星全额计入,其余合并贡献 ln(1+Σ)——单星语义零变化(首星激活阈值不变),叠星边际递减(10 颗 LEO 4.17 而非 15.00);判定端与显示端共用 stackEff,口径不分叉
  • 卫星信号透明度与信号源一致:移除卫星覆盖盘独立绘制层(原与合成层双重叠画、归一化标度不同),范围模式每格只画最强一路——卫星与地面源共用同一透明度公式与强度标度;卫星获胜格仍用编码专属色/未绑定蓝渐变
  • 移除卫星上空名字标签(按需求撤回,右下角悬停面板保留 hitSize/图标/机型名修复)

迭代记录(第三轮:细节打磨与读档修复)

  • 同步上游 test:合入 fix: robin去重与密钥校验修复;feat: 启用git-democracy投票(3人/100%/10min) #53/fix: 修正voting工作流与配置路径 #55(voting 工作流与 robin 配置),与本分支无文件交集,零冲突
  • 发射中枢 UI 精简:移除方块头顶制造进度条与石油不足图标;石油需求并入右下角面板材料行(图标+数量角标+不足红线,与物品显示同栏同风格)——需求数量取自绑定本中枢的唯一控制台的所选轨道(对称扫描,节流 30 帧),无唯一绑定控制台时回退 LEO 1.0k
  • 修复读档后卫星冻结+无信号:存档读入时 WorldLoadEvent 早于单位实体读入,原「按 getByID 剪除名册」误杀刚恢复的记录 → 卫星永冻+无信号。改为名册不剪除(击落由 UnitDestroyEvent 除名、跨局由 ResetEvent 清空)+ app.post 延迟一拍对账 + 控制器节流兜底(旧档名册丢失时补建未绑定记录)
  • 中继器面板选中态修复:源按钮选中态每帧与 selectedSource 同步——清除绑定/外部变更后不再残留选中黄框
  • README 同步:H 键覆盖单层合成与透明度一致、发射反馈闭环、移除卫星在轨状态效果

迭代记录(第四轮:外部评审修复 · 2026-09-21)

本轮按独立评审(逐条对照 Mindustry v160.1 源码)修掉 2 个阻塞项、1 个多人回归、2 个中危项,均已推送到本分支:

  • B1 环境旗标(阻塞):SatelliteUnits 漏设环境旗标,继承 UnitType 默认 envDisabled = Env.scorching,而环境处死路径不看 hittable/targetable/killable/useUnitCap(UnitComp.java:751-753 → Units.unitEnvDeath → unitDestroy)→ 焦土图(Erekir 全部地图)与太空图发射后一帧卫星消失,并连名册一起删。修:envEnabled = Env.any; envDisabled = Env.none;
  • B2 相位模型(阻塞):原 scanU = phase + Time.time / 周期 依赖「读档后 Time.time 归零」,但运行时 arc 的 Time.clear() 只清 runs(字节码实核),引擎读档不重置时钟 → 同一会话内读档会多绕「存档时运行时长 ÷ 周期」圈,卫星与覆盖圆跳位。修:相位改为自累加存档字段(rec.phase += Time.delta / 周期,GEO 跳过),scanU/phaseForSave 直取 phase,位置成为存档值的纯函数。
  • H1 多人暂停回归:同步上游时误删了 netClient 的 paused 回包处理,Vars.pause.complete 永远为假 → 客机每 60 秒重发一次 pause 请求。修:恢复该回调。
  • M1 名册死记录:onWorldLoaded 增 pruneDead 重载,只在「权威端 + entities 已读入」的 app.post 对账剪除无实体记录(WorldLoadEvent 那次不剪,避免误杀;客机不剪,名册是广播镜像)——避免存档 entity id 重复被引擎重分配(SaveVersion.java:503-514)后旧记录永久残留、在轨数虚高。
  • M5 干扰器口径统一:地面层对多台干扰器求和、卫星层取最强一台(SignalJammer.strengthAt)→ 口径分叉,多台干扰器压不住卫星。修:累加逻辑抽成 SignalChannel.addJammer,地面层 effectiveAll 与单信道 jammerAt 共用同一实现,strengthAt 委托之。
  • 附带:卫星出生点直接落在轨道点上(原为先落中枢位置、下一帧才入轨),并同步 docs/blocks/Signal.md 的卫星层说明。

验证:gradlew clean jar(JDK17 + Mindustry v160.1 依赖)通过;推送提交的 tree sha 与本地 clean build 提交逐一对齐。分支 head 另含一次与 Xiaobei09 分叉 test 的合并(内容零差异、仅补祖先关系,便于两条评审线共用同一分支)。

评审中核对为误报/无需改的项:Robin 的「信道号越界」「频谱并发修改」「网络线程禁 UI」三条在 v160.1 均不成立(ArcNetProvider 已把收包回调 post 到主线程;allSources() 返回缓存 Seq 且 markDirty() 只置脏标志),故未改动。已知仍未修的低优先项:编码格式校验三处统一、sat-launch 速率限制、H 覆盖逐格绘制开销、测试卫星发到 LEO/MEO/GEO 时机型名显示为「信号卫星」。


迭代记录(第五轮:显示端口径与判定端统一 · 2026-09-21)

问题:判定端(中继激活 satelliteStrengthAt > 0.5、控制台绑定 inSignalRange)本来就是逐编码的,而显示端把该队所有编码的卫星先求和再对数叠加(SignalOverlay.bestSignal 遍历全部记录、SignalSpectrum 按信道聚合)——同一点上叠了多个编码的卫星时,格子上的数字高于任何单一编码真实可用的强度,照显示布线会得到与预期不符的结果。

改动(2407d3d,7 文件 +261 −50):

  • SignalChannel.effectiveAll 增「按编码视图」(codeOut/viewCode 参数):viewCode 非空时信号 = 该编码在本信道的最强一路(同码多台视为同一路,不互扰),干扰 = 其余全部身份功率和 + 邻道泄漏 + 干扰器;codeOut 返回每信道结果所属编码(自动模式 = 该信道最强身份的编码)。
  • SatelliteManager.bestSatelliteAt:覆盖显示改为「逐编码分组取最强」——同编码 sum/max 计 stackEff 后取各组最大值,不再跨编码求和;未绑定记录(code == null)作为独立一组参与(出参 null → 蓝色渐变保留);支持按信道过滤,供频谱"无编码视图"逐信道取最强单编码。判定/绑定仍走 satelliteStrengthAt。
  • SignalOverlay:每帧解析一次「当前查看的编码」(① 配置面板打开的建筑 → ② 鼠标悬停的本队建筑 → ③ 自动),地面与卫星都只按该编码聚合;底部提示追加当前范围(· 编码 XXXX / · 自动(每格取最强编码))。
  • SignalSpectrum:标题下新增「聚合范围」行 —— 信号源面板 = 自身编码、中继面板 = 绑定编码、检测器 = 全局(逐信道最强单编码);强度列按编码,占用列仍是信道拥挤度(全部编码 + 干扰器,两者口径不同属预期)。
  • docs/blocks/Signal.md §3/§5 同步;两个 bundle 增 signal.overlay.view.*、block.silicon-signal.spectrum.scope.*。

顺带发现的既有口径差异(本次未改,属玩法语义):地面侧的「绑定/中继激活」用的是原始覆盖(距离 + 供电,inGroundSignalRange 与 updateActive 的 dst <= RADIUS*8),不看 SINR;只有卫星路径是 SINR 阈值(> 0.5)。所以干扰器压不掉地面中继链路、但能压掉卫星中继链路。若希望地面侧也吃 SINR 判定,需要单独确认(会实质改变干扰器的强度)。


迭代记录(第六轮:地面侧 SINR 判定 + 中继实时转发检测 · 2026-09-21)

上一轮遗留的口径差异(地面侧不看 SINR)本轮补齐,并让中继器实时判定:

  • 地面侧也按 SINR:新增 SignalChannel.groundEffAt(team, code, wx, wy) —— 地面层(信号源 + 已激活同编码中继器)在该点的有效强度,与覆盖/频谱同一算法(信号 = 该编码最强一路;干扰 = 其余全部身份功率和 + 邻道泄漏 + 干扰器),编码无存活地面源时为 0;带"本编码是否有发射机覆盖本点"的廉价前置(远处空闲中继不跑完整批算)。inGroundSignalRange 改为 groundEffAt(...) > 0 → 卫星控制台绑定与发射 1:1 配对(hubsInSignal/consolesInSignal)从此都吃 SINR:干扰器覆盖到中枢/控制台所在点即可让配对判定失败。
  • 中继器实时检测(去掉原先"每 20 tick + 几何距离"):updateTile 每 tick 调 updateActive,地面走 groundEffAt、卫星走 satelliteStrengthAt,同一阈值 FORWARD_THRESHOLD = 0.5(与覆盖显示/频谱"可用强度"同口径);干扰器开关、信号源断电/禁用、绑定变更、级联上下游变化都在同一 tick 生效。级联不再需要几何距离特判——已激活的同编码中继器本身就是发射机,自然进入下一跳的信号项。
  • 可观测性:中继器记录实测入站强度 inGroundEff/inSatEff,配置面板与悬停信息新增实时状态行:转发中/信号不足/无电/已关闭/未绑定 · 入站 地面 X / 卫星 Y(阈值 0.5)。
  • 文档 docs/blocks/Signal.md §3/§4(阈值体系 + 实时性 + 有效半径说明)与双 bundle 同步。

行为变化(需要留意):

  1. 净空单源的实际可转发/可绑定半径由 15 格收窄到约 12.5 格(阈值 0.5;放置预览圆仍画 15 格原始半径,以覆盖显示/频谱数值为准)——已有的中继链可能需要补一颗。
  2. 干扰器现在能压断地面链路,也能让卫星发射的配对判定失败(干扰器覆盖到中枢/控制台所在点即可)。
  3. 中继器判定从"3 Hz 几何"变成"每 tick SINR",干扰器开关的效果从原先的延迟/无效变成同一 tick 生效。

迭代记录(第七轮:叠星函数换成非相干功率合成 · 2026-09-21)

把叠星从 max + ln(1 + Σ其余) 换成 √(Σeᵢ²)(非相干功率合成):

  1. 与层间合成同一运算——卫星 × 地面本来就是 √(g² + s²),叠星改用同一个 √(Σ·²) 后整个模型可结合:"先叠星再叠地面"与"把卫星和地面源一起丢进 √(Σ·²)"结果完全相同,不再有两套叠加语义。
  2. 物理正确:多颗卫星是彼此独立的非相干发射源,功率(幅值平方)相加、幅值按平方根合成;线性叠加等于把 N 颗当成一台 N 倍功率发射机(失真),对数叠加则是在强项上再套一层非线性压缩(缺少物理依据)。
  3. 尺度自洽:N 颗同强度 = e·√N(数量翻 4 倍强度才翻倍),单星语义完全不变(首星阈值不变),抗干扰裕度随 √N 单调增长——LEO 单星有效 1.2 时,可容忍的同信道干扰由 j≈0.32(1 星)升到 ≈0.56(4 星)/≈0.80(10 星),递减但持续有效。

数值对照(同 10 颗 LEO,单星有效 1.2):RSS = 3.79;旧对数叠星 = 3.67(大 N 相近、小 N 偏高);线性叠加 = 12.0。
阶梯:1 / 2 / 3 / 4 / 10 星 = 1.20 / 1.70 / 2.08 / 2.40 / 3.79。

改动:SatelliteManager.stackEff(Σeᵢ²) = √(Σeᵢ²)(单参,原双参签名删除;satelliteStrengthAt 累加平方和,bestSatelliteAt 分组缓冲由 {sum,max} 改为 {Σeᵢ²});SignalSpectrum 卫星层缓冲改 satSumSq;docs/blocks/Signal.md §2/§3、README 特性段与更新日志、双 bundle 的卫星控制台描述同步。


迭代记录(第八轮:评审低优先项清尾 · 2026-09-21)

  • 编码校验唯一化:新增 Signal.isValidCode(4 位大写字母/数字),信号源 configure/read、中继器 configure、卫星控制台 configure 全部改用它——此前只有信号源校验,中继器/控制台接受任意字符串,会作为 key 长期驻留 SignalChannel.liveSrcCache 与覆盖颜色缓存并污染按钮文本。
  • sat-launch 限流:每个请求都会做一遍「信号范围 + 1:1 配对」扫描(O(建筑×源)),改造客户端可高频重放;现按控制台 0.5s 限流(Silicon.LAUNCH_REQUEST_COOLDOWN)。
  • 客机不做本地生产/充电:SatelliteLauncher.updateTile 在非权威端直接返回,材料/进度/缓冲电一律以服务器快照为准(消除快照到达前的本地扣料分叉)。
  • H 覆盖裁剪:每帧算一次「本队源/已激活中继/在轨卫星覆盖圆」并集包围盒,盒外格子直接跳过。
  • 读档夹取:SignalJammer.read 的 jamChannel 夹到 [-1, 5];SatelliteConsole.read 名册条目数上限 64;删除无人引用的 block.silicon-signal-relay.signal bundle 键。

至此评审里的 2 阻塞 / 1 高 / 5 中 / 9 低全部有结论(修复或明确不改),可进入正式复审。


迭代记录(第九轮:同步上游 #64 + 两处信号修复 + 干扰器/卫星贴图与轨迹收敛 · 2026-09-26)

1. 同步上游 test@bd44662(#64「退回电力保护器」)——冲突处理:

  • PowerProtector.java 采用上游回退版(417 行),PR 不再携带新版实现;
  • Blocks.java 保留本 PR 的方块(signalJammer / satelliteLauncher / satelliteConsole / signalDetector)与 signalDetector 注册,并入上游的 petroleumRefinery 字段与「已暂时禁用注册」注释块——炼油厂/润滑油当时保持上游的未注册状态([第十一轮]已改为直接删除,见下);
  • 双语 bundle 保留本 PR 早先删除的铥(Thulium)遗留键(双方代码里都已无该物品);docs/blocks/PowerProtector.md 当时交还上游([第十三轮]改为与 fork/main 同步、保留过期警告)。

2. fix(signal) 信号源/中继器缓存自愈:拆除流程里 Building.onRemoved() 早于 Building.remove()(建筑真正离开 Groups.build),这期间任何一次列表查询(中继器每 tick 的实时判定、频谱、H 覆盖、控制台面板)都会把尚未离开 Groups.build 的死源带进缓存并清掉 dirty——已拆除的信号源会永久留在控制台/中继器列表里。现在:removed 标记(onRemoved 置位并从缓存摘除、onProximityAdded 复位、rebuildCache 跳过)+ allSources/allRelays 按 removed || !isValid() 自愈剔除,剔除时同步失效上行门控缓存。

3. fix(signal) 绑定信号在编码消失时自动清除:编码是随机四位串,源被拆/被打掉后无法再建出同名源——控制台 selectedSignal 与中继器 selectedSource 在「本队已无该编码存活源」时自动置空并经 tileConfig 同步各端;判定只看「编码是否还存在」,源仍在但断电/超距/被干扰时保留绑定(状态行红字提示);只在权威端判定,避免客机因网络延迟误清。

4. fix(signal) 信号干扰器贴图:SignalJammer 注册名 signal-jammer 但仓库从来没有对应贴图文件(Block.region 默认 Core.atlas.find(name),找不到即落 error 占位),补 32×32 assets/sprites/blocks/signal-jammer.png(同族配色 + 天线/信号波/红斜杠)。

5. 卫星轨迹收敛(边界内缩 + 回绕淡入淡出):

  • 正弦轴按覆盖半径内缩(scanInset = clamp(coverageRadius, 0, 0.8·half),下限 8px):旧实现固定内缩 8px,卫星贴边时大半个覆盖圆跑到图外,名义覆盖率打对折;现在覆盖圆与图边相切。回绕轴保持全幅(卫星本就该从一侧进场、另一侧离场)。
  • 回绕淡入淡出(WRAP_FADE = 0.06 + presence/presenceOf):每圈首尾 6% 圈长(LEO 5.4s / MEO 9s / SSO 4.2s)存在度线性升降,同时作用于卫星绘制透明度与 satelliteRawAt 原始强度——覆盖显示、SINR 判定、中继激活同步变化,消除回绕瞬间的"瞬移",也不会出现"图没了信号还在"。

6. 卫星贴图与文档:新增 assets/sprites/units/satellite-{leo,meo,geo,sso}.png(48×48 = 12 世界单位;本体 + 两侧太阳能板,四型按轨道区分轮廓与轨道色),SatelliteUnits.load() 依次尝试「内容名(含 mod 前缀)→ 去前缀名」两种图集键,绘制时中心补队色点,两者都缺时仍走程序化兜底(不会出现 error 白方块);新增 docs/blocks/Satellite.md(轨迹公式 / 淡入淡出 / 覆盖与强度 / 发射与名册 / 视觉 / 常量速查);README 卫星章节改为扫描轨迹模型(周期 90/150/70s、GEO 定点整图、去掉"圆轨道"与 GEO 0.64 比例)。

验证:每轮均 gradlew clean jar 通过 + tests/ 三套件全绿(12/12、31/31、优先级路由);CI Build Mod / Fork Publish 均为 success。

迭代记录(第十轮:H 覆盖半格错位根因定位与修复 · 2026-09-27)

症状:H 覆盖的数字相对方块/地格整体偏移("向方块右上角偏移"),覆盖范围与实际范围看起来不一致。

定位过程(借助一个临时调试方块「单格信号探针」):探针半径压到 0.5 格,理论上只有它自己那一格会显示数字。实测却是整格没有数字,而方块上方的自评读数显示 raw99 cell99 ON——即探针自身与其所在格的可用度计算都正常。诊断读数进一步给出 off-0.50,-0.50:本格中心与覆盖层采样点(gx*8+4)相差半格。这也解释了探针为何"不发射":四个采样点到探针中心的距离都是 √2/2 ≈ 0.707 格 > 0.5,每个采样点都在覆盖之外。

根因:引擎里地格的世界坐标就是索引 × 8(1×1 建筑中心在 8 的整数倍上;官方 PowerNode.drawPlace 也写作 x * 8f + offset,不加 4)。而 SignalOverlay 的逐格采样用的是 gx*8 + 4,采的是格角,整整错半格:普通信号源表现为"数字偏移",半径刚好 0.5 格的探针则整格采不到。

修复:

  1. 数字模式与范围模式的采样点统一改为 gx * 8f / gy * 8f(= 建筑所在网格),使「采样点 = 建筑中心 = 填色格中心」三者一致;
  2. 数字的垂直锚点修正:Font.draw(str, x, y, Align.left) 的锚点是文字的上边缘(不是基线),原写法把 y 减了半个字高,等于把整行数字又压低约 0.47 格;改为 y + capHeight/2,数字视觉中心才落在格心(水平方向本来就是左边缘 + 宽/2,保持不变);
  3. SignalSource / SignalRelay 的放置预览圆按官方写法改为 x * 8f + offset。

调试方块已移除:用于定位的「信号探针」(SignalProbe + 贴图 + 双语键)及其只为它引入的「每方块半径」管线(SignalSource.radius、radius()、带半径的 strengthAt 重载)全部还原,正式版本不再包含调试方块。

迭代记录(第十一轮:移除炼油厂/润滑油,与 fork/main 对齐 · 2026-09-27)

上游 #64 以「注册注释掉」的形式带进了石油炼化厂 + 润滑油;本 PR 此前整套保留(类 / 液体 / 贴图 / 双语键),
而 fork/main 早已删除 —— 两个分支不一致。本轮统一为删除:

  • 删除 src/silicon/world/blocks/production/PetroleumRefinery.java、src/silicon/world/Liquids.java、
    assets/sprites/blocks/petroleum-refinery.png、assets/sprites/liquids/lubricant-oil.png;
  • 引用清理:Blocks.java(import / 字段 / 注释注册)、Silicon.java(silicon.world.Liquids.load())、
    双语 bundle 的 block.silicon-petroleum-refinery.name/description;
  • 同步把 fork/main 上遗留的那两个 bundle 键也清掉,两分支内容现已完全一致。

⚠️ 需要评审知悉:这等于把上游 #64 里这套未启用内容从本 PR 移除。若上游日后正式启用炼油厂,
同步上游时会重新引入(或按上游实现重新对接)——如评审认为不该由本 PR 删除,我可以立刻还原。

另外:fork 侧的发布/自动化基础设施(.github/workflows/fork-publish.yml、scripts/*、docs/WORKFLOW.md)
只存在于 fork/main,不进本 PR。

验证:gradlew clean jar 通过;tests/ 三套件全绿;jar 内无 PetroleumRefinery / world/Liquids / lubricant-oil 条目。

迭代记录(第十二轮:适配 Mindustry v160.5 · 2026-09-27)

上游本体已发布到 v160.5(Steam 客户端实测 steam build 160.5)。本 PR 同步把构建依赖提上去:

  • build.gradle:mindustryVersion v160.1 → v160.5(零源码改动,改动只有这 1 行);
  • README:顶部兼容版本改为 v160.5+、构建依赖说明同步,更新日志补一条适配记录;
  • 结论:同一份源码下,v160.5 与 v160.1 依赖的构建产物逐字节相同 —— 160.2~160.5 未触及本模组用到的 API
    (发布内容均为缺陷修复与 UI 调整:target dummy、逻辑处理器、数据补丁、连锁闪电等),无破坏性变更;
  • minGameVersion 维持 155.4,向后兼容不变。

验证:

  • 本地 gradlew clean jar(JDK 17 + v160.5 依赖)通过,产物 679,918 B(与 v160.1 依赖构建同尺寸);tests/ 三套件全绿;
  • CI:本分支 head 的 Build Mod = success(证明 CI 上也能解析 v160.5 依赖);
  • 该适配此前已在 chore/adapt-v160.5 分支(commit a028eb9)验证过一轮,CI 同样 success。

本 PR 只取这一个适配提交的改动(2 个文件);fork 侧的发布/自动化基础设施仍然只存在于 fork/main,不进本 PR。

迭代记录(第十三轮:与 fork/main 内容对齐 + 分支同步约定 · 2026-09-27)

按维护者要求,PR 分支与 fork 的 main 分支内容保持一致,唯一允许的差异是 fork 的发布/自动化基础设施。

  • docs/blocks/PowerProtector.md:此前本 PR 把它「交还上游」(不带警告),而 fork/main 上有 8 行
    「本文档已过期」警告——现统一为带警告版(该警告在 upstream 侧同样成立:文档描述的是 v14 时代架构,
    与 v19 重构后的实现严重脱节),两分支此文件完全相同;
  • README.md、build.gradle、Blocks.java、Silicon.java、双语 bundle、信号/卫星全部源码:
    两分支现已逐字一致;
  • 分支同步约定写进了 fork 侧的 docs/WORKFLOW.md(该文件属基础设施,不进本 PR):
    改动先落 PR 分支(不含任何 fork 专属内容),再原样同步到 fork/main;反向同步必须剔除 fork 专属内容。

目前两分支的差异只剩:.github/workflows/fork-publish.yml、scripts/*(6 个脚本 + 2 个 json)、
docs/WORKFLOW.md、以及 README.md 里由发布流程自动维护的 FORK-BUILD 区块——全部属于
「fork 的发布/自动化基础设施」,按约定不进本 PR。

迭代记录(第十四轮:对照评审逐条自查修复 · 2026-09-28)

对本 PR 做了一轮逐文件审核(含用引擎 jar 反汇编核对行为),修掉以下缺陷(提交见分支 head):

[阻塞] 存档错位 —— 名册读写字节数不相等
SatelliteConsole 写侧 write.i(list.size) 无上限、读侧 Math.min(..., 64) 截断:名册 >64 条时读侧少消费
(n-64)×(16+码长) 字节,而反汇编确认引擎 SaveFileReader.readChunk 是「读长度 → 交给 runner → 直接
return len」,不做任何按声明长度的对齐 → 后续每个 tile 的 chunk 前缀都会被当数据读、整档解析错位
(沙盒测试卫星 1 硅/1 秒即可刷过 64 颗)。现读写共用 ROSTER_MAX = 64,且读侧把写入的 n 条全部消费
(超限只解析不恢复),流位置与写侧严格一致;已有 >64 条的存档也能正确读出。

显示端与判定端口径不一致(本 PR 的核心承诺是"看得见的强度就是能否转发的强度")

  • SignalChannel.usableAll 的「按编码」分支缺卫星上行门控(自动分支、satelliteStrengthAt、频谱占用计数
    都有)→ 地面源被拆光后 H 覆盖/频谱仍显示该编码卫星强度,与判定端打架;
  • SignalRelay.updateActive 用 max(地面, 卫星) 判定,而显示端(H 覆盖、频谱"可用度"列)用非相干合成
    √(g²+s²) —— 两者恒不相等(合成值 ≥ max),阈值 3.3 附近会出现"H 上显示 3.4、中继却不转发"。
    两处都统一到显示端的合成值;顺带修掉「地面有信号时面板卫星列恒为 0」。

生命周期与越界

  • SignalRelayBuild 漏覆写 changeTeam → 换队/夺取后中继缓存不失效(旧队仍把它当发射机、新队漏掉它);
  • SignalJammer 全局缓存无 removed 过滤、无自愈剔除 → 借「onRemoved 早于建筑离开 Groups.build」的窗口,
    死干扰器会被固化进缓存并持续压制该信道;
  • SatelliteManager.onWorldLoaded 剪除只判 getByID == null(id 被复用给别的单位时留幽灵记录)、
    补建分支缺权威端守卫;restoreRecord 对 orbit/channel/phase/code 零夹取;
  • SignalJammer.jamChannel 放行 0(会给信道 1/2 泄漏 40%/12% 干扰却在占用列隐身);
    SignalSource.read / SignalRelay.read 不夹取信道、不校验绑定编码。

性能与清理

  • SignalSource.strengthAt 每次调用重算常量 ln(1+R/σ) → 提为 static final;
  • 卫星控制台面板的绑定刷新 8 → 30 tick(原值每次都要遍历全部建筑 × 逐建筑信源扫描);
  • 卫星回绕绘制里 Team.color 被就地改 alpha(漏了 cpy)→ 改为 tc.cpy().a(alpha);
  • 注释/文档同步(SignalSource/SignalJammer 的"线性/高斯衰减"、SignalSpectrum 的"15 tick"、
    SignalRelay 的"约 12.5 格"→按公式解 ≈12.8、mod.hjson 简介、README 三处、docs/blocks/Signal.md),
    并删掉本 PR 带回但无任何引用的死贴图 assets/sprites/effects/satellite-buff.png。

验证:gradlew clean jar(JDK 17 + v160.5 依赖)通过;tests/ 三套件全绿;两分支 CI buildJar 均 success。
(同日补充)逐格热路径与面板开销

  • 身份串缓存:Signal.id(构造时算一次)与 SignalRelayBuild.cacheId()(绑定/坐标派生 id 各缓存一份)
    ——H 覆盖每帧数万格、每格都要取身份串比较,原先每格现拼字符串(每帧几十万次临时分配,
    与 SignalOverlay 里"预计算字符串避免每帧分配"的注释自相矛盾);
  • SignalSpectrum 的占用计数从「信道循环内 5×(全表源 + 全表中继 + 全表干扰器)」改为循环外一次算完,
    signalChannel() 对每台中继的求值从 5 次降为 1 次;
  • sat-launch 的限流分支改用独立结果码 busy(原先与真实失败共用 fail,合法双击的第二次会弹
    "发射失败",把"点太快"误导成"配置有问题"),双语补齐 block.silicon-satellite-console.busy;
  • 频谱面板注明两列口径差异(有意):「占用」只数直接打在该信道的干扰器,而"干扰"列含邻信道泄漏
    与全信道干扰器,因此一台只打信道 2 的干扰器会在信道 1 的干扰列显示非 0、占用列显示 0。

@Xiaobei09

Copy link
Copy Markdown
Collaborator

/review

@by514

by514 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 3 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

Summary

The diff adds a satellite system with new blocks (Satellite Launch Hub and Satellite Console), units, and status effects. It updates documentation, strings, and the mod version. The satellite launch packet handler in Silicon.java has reliability issues: it fails to send responses in the team mismatch and exception cases, which could leave clients hanging. It also misses a validation for the console being bound to a launch hub, potentially leading to less specific error messages.


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

Comment thread src/silicon/Silicon.java Outdated
if (cb.team != p.team()) {
// 只能操作本队控制台;越权请求记日志(可能是修改客户端)
SiliconLog.info("sat-launch: team mismatch from " + p.name);
return;

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: high

The packet handler for 'sat-launch' returns without sending a response packet when the player's team does not match the console's team, leaving the client waiting for a response.

Recommendation: Send a failure response (e.g., 'fail') before returning.

Call.clientPacketReliable(p.con, "sat-result", "fail");

Comment thread src/silicon/Silicon.java Outdated
if (result != SatelliteManager.LAUNCH_OK) {
Call.clientPacketReliable(p.con, "sat-result", String.valueOf(result));
}
} catch (Exception e) {

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: high

The packet handler for 'sat-launch' catches an exception and logs it but does not send a response packet, leaving the client waiting for a response.

Recommendation: Send a failure response (e.g., 'fail') in the catch block.

Call.clientPacketReliable(p.con, "sat-result", "fail");

Comment thread src/silicon/Silicon.java Outdated
return;
}
silicon.world.blocks.satellite.SatelliteConsole.SatelliteConsoleBuild cb =
(silicon.world.blocks.satellite.SatelliteConsole.SatelliteConsoleBuild) tile.build;

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

The packet handler for 'sat-launch' does not check that the console is bound to exactly one launch hub before attempting to launch a satellite. This may lead to unnecessary processing and less specific error messages.

Recommendation: Add a check for the bound launch hub (e.g., by verifying that there is exactly one launch hub in the console's signal range) and return an appropriate error code if the check fails.

/* Example: if (cb.getLinkedHubs().size() != 1) { Call.clientPacketReliable(p.con, "sat-result", "nohub"); return; } */

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Voting is closed 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

@Xiaobei09 Xiaobei09 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

测试

@Xiaobei09 Xiaobei09 closed this Sep 4, 2026
@rt334 rt334 reopened this Sep 4, 2026
@by514

by514 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 2 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 Medium | 🔵 1 Low

Summary

本 PR 添加了卫星系统,包括新方块(卫星发射中枢、卫星控制台、信号干扰器)、新单位(四种轨道卫星)和相关功能。总体实现正确,遵循了模组的约定。然而,在 NetSync 工具中发现了可靠性问题(缺少玩家空指针检查),在卫星单位绘制中发现了性能问题(Lines 笔画未重置)。


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

Comment thread src/silicon/util/NetSync.java Outdated

/** 把建筑配置下发到该建筑所属队伍的所有客户端(主机本地同步执行,幂等) */
public static void sendTeamConfig(Building build, Object value) {
for (Player p : Groups.player) {

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

遍历 Groups.player 时未对玩家进行空指针检查,可能导致在玩家断开连接时出现空指针异常。

Recommendation: 在访问玩家属性前添加空指针检查。

for (Player p : Groups.player) {
            if (p == null) continue;
            if (p.team() == build.team) {
                Call.tileConfig(p, build, value);
            }
        }

Draw.color(tc);
Fill.circle(unit.x, unit.y, r * 0.45f);
Fill.circle(unit.x, unit.y, r * 0.2f + (float) Math.abs(Mathf.sin(unit.id + Time.time / 40f)) * r * 0.15f);
Draw.reset();

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

Lines 笔画在使用后未重置为默认值,可能影响后续的 Lines 调用。

Recommendation: 在 Draw.reset() 后添加 Lines.stroke(1f) 以重置笔画宽度。

Lines.stroke(1f);

@rt334

rt334 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

审查修复回应(e36ef97)

感谢两轮 Robin 审查,逐条回应:

Round 1

  1. 🔴 HIGH · Silicon.java sat-launch 队伍不匹配不回包 → 已修:回 "fail"(具体原因只进服务器日志,不向可疑客户端透出)。
  2. 🔴 HIGH · Silicon.java sat-launch 异常路径不回包 → 已修:catch 中回 "fail",外再兜一层防回包本身抛异常;同时补齐了审查未点名的两条静默路径(包字段数不足 / 坐标解析失败也回 "fail")。
  3. 🟡 MEDIUM · 未校验控制台绑定中枢 → 已实现于 SatelliteManager.launch():launch() 在消耗任何资源前完成绑定校验并返回 LAUNCH_NO_HUB(5) / LAUNCH_MULTI_HUB(6) / LAUNCH_MULTI_CONSOLE(7),客户端 sat-result 处理器把这些码映射为具体提示;处理器层无需重复预检。

Round 2

  1. 🟡 MEDIUM · NetSync.sendTeamConfig 玩家空指针 → 已修:if (p == null) continue;。
  2. 🔵 LOW · SatelliteUnits.drawFallback Lines 笔画未复位 → 已修:绘制结束前 Lines.stroke(1f)(Draw.reset() 只复位颜色)。

顺带发现:客户端 sat-result 处理器此前把 "fail" 字符串静默忽略(NumberFormatException → ignored),服务端补发再多 "fail" 客户端也无感——现已映射为「发射失败」toast(block.silicon-satellite-console.fail,双语言 bundle 已有键)。

@by514

by514 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

🏹 Robin

🔄 This review run was replaced by a newer Robin run.

No action needed — the newer run posts its own result when it finishes.

删除(真死代码):
- src/silicon/content/item/Items.java —— 空类,`load()` 方法体为空,不注册任何内容;
  连同 Silicon.loadContent() 里的调用与 import 一并移除。

修正注释(方块本身保留):
- DimensionAnchor 的存根注释把理由写错了。原文称「Mindustry 存档按方块 ID 存储,
  删除会使其后所有方块 ID 前移、内容错位」—— 核对引擎源码后确认不成立:
  存档头按**内容名**记录,SaveVersion.readContentHeader 把每个旧 ID **独立按名解析**
  进 temporaryMapper,不存在"ID 前移"。
  真实后果是:名字解析不到时 ContentLoader.getByID 返回 contentMap[0],
  于是旧存档里这个位置的方块会**变成一个原版方块**(SaveFileReader.fallback 只兜底
  原版方块名,不含 mod 方块)—— 比留着存根更糟。
  所以存根是对的,只是理由要改对;Blocks.java 注册处的注释同步修正。

顺带核查(结论:无需改动):
- MessageTest:沙盒调试方块,仍在注册且用途明确,保留;
- bundle 键:静态扫描出的"无引用键"绝大多数由运行时前缀拼接或引擎自动查找
  (category.*.name 由 KeyBind.add 的 category 参数解析、setting.*、
  block.<name>.name 由内容名自动查找),逐条核对后无真正失效项;
- sprites:扫描出的 23 个"未出现"文件名全是多区域方块(dual-purpose-storager-*)
  与动画帧(mine-converter-0..17、switch-on/off),由引擎按后缀自动查找;
- testSso / TYPE_TEST:本分支保留测试卫星(与 fork/main 的第二阶段不同),非失效内容。

验证:gradlew clean jar 通过;tests/ 三套件全绿。
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
3 👍
0 👎

Acceptance Criteria:

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

@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 为卫星系统第一阶段的大型功能 PR(新增卫星发射中枢/控制台、信号干扰器、信号检测器、4 种卫星单位、轨道控制器与 SINR 信号模型),版本号由 a0.12.4.4 递增至 a0.13.0.0 符合新增方块/单位的中位递增规范。多人同步链路(sat-launch 授权+限流+失败回包、sat-state 广播、暂停包处理器迁到 init修好专属服务器失效)设计较完整,但内容注册顺序上移除了液体加载且卫星单位晚于方块加载,存在加载期空引用风险;服务端限流使用了 Time.time。

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

@by514

by514 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 1, 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.

⚠️ 1 Medium | 🔵 3 Low | 💡 4 Suggestions

Summary

本次 diff 是一次大型功能更新(卫星系统第一阶段 + 信号系统重构),整体设计清晰,网络同步与存档时序考虑充分,版本号与构建适配也与内容变更匹配。新增的卫星实体、轨迹控制器、管理器与信号频谱 UI 在注释中对关键隔离(targetable/hittable/physics、环境放行、dedicated 守卫)均有明确说明。文档与双语 bundle 同步更新,删除废弃物品条目清理干净。少量实现细节存在轻微的时序/可维护性问题,但在 diff 可见范围内未发现确定的崩溃或多人不同步的高危缺陷。


📄 Findings Not Posted Inline

🔵 1 (src/silicon/util/SatelliteUnits.java:194) — 程序化兜底绘制(drawFallback)在闪烁计算中使用了 unit.id + Time.time / 40f:unit.id 是整数,Time.time 是全局游戏时间(float)。虽然仅用于纯装饰闪烁且 alpha 已做淡入淡出裁剪,但在大量卫星同时绘制时每帧都会对每个单位做 Mathf.sin 计算并产生少量浮点运算。当前卫星数量通常不大,但属于可缓存的小型优化点。

将每个单位的闪烁相位按 unit.id 固定偏移(例如 phase = (unit.id * 0.37f) % 1f),再用 (Mathf.sin((phase + Time.time / 40f) * Mathf.PI2)) 计算,或直接缓存上一次值(非必要)。最简做法是将偏移提前到常量因子,避免在表达式中隐含可读性问题。

💡 2 (src/silicon/util/SatelliteManager.java) — SatelliteManager 是本次新增最大文件(diff 截断),类头注释对轨迹模型(星下点扫描、正弦摆动、淡入淡出、GEO 定点)与名册语义描述详尽,值得肯定。但对于“名册 vs 实体”对账的多个入口(reset/onWorldLoaded/reconcileMissing/onUnitDestroyed/periodicBroadcastAll)缺少一小段流程图式的时序说明,后续维护时容易误用入口时机。

在类头补充「对账时序」小节:列出 ResetEvent/WorldLoadEvent/entities 读入/app.post/单位销毁/周期广播各自职责与调用频率,标注哪些入口是服务端专属、哪些是幂等的。


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

SatelliteManager.SatelliteRecord rec = SatelliteManager.recordOf(u.id);
if (rec == null) {
SatelliteManager.reconcileMissing();
return;

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

当名册记录 rec == null 时(读档时序窗口或旧档名册丢失),控制器在 updateUnit() 中直接调用 SatelliteManager.reconcileMissing()。控制器是每帧按存活单位调用的路径,reconcileMissing() 是全局对账/补建操作(遍历单位名册与 Groups.unit),在极少数时序窗口下可能产生不必要的全局扫描或在单位创建流程尚未稳定时被反复触发。

Recommendation: 将 reconcileMissing() 的触发从控制器的每帧路径改为延迟一次的全局回调(例如在 WorldLoadEvent/app.post 或仅在首个单位发现缺失时标记一个“待对账”布尔位并在下一帧统一执行一次),避免控制器内直接执行全局性维护操作。

if (rec == null) {
    SatelliteManager.reconcileMissing(); // 全局操作,可能在控制器高频路径触发
    return;
}

Comment thread src/silicon/Silicon.java Outdated
// 注:hub network id 计数器不再在此 reset——读档顺序是构造(占号)→read 用存档 id
// 覆盖→WorldLoadEvent,reset 反而制造撞号;现由 ItemTransferHubBuild.read() 调
// ItemTransferHubNetwork.updateCounterAfterLoad 按 max 推进。
// 卫星名册的清空挂在 ResetEvent(存档读入前/返回主菜单/新开局都会触发)——

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

ResetEvent 注册了 SatelliteManager.reset()(105 行附近注释说明“名册的清空挂在 ResetEvent…”),但与 WorldLoadEvent/app.post 的对账时序(112-119 行)注释较长且分散。对于读档流程(ResetEvent 清空 → 控制台存档块读入 → WorldLoadEvent → entities 读入 → app.post 延迟对账)的顺序虽然正确,但注释中“entities 区域读入单位 → app.post 延迟一拍做真正生效的对账”与 115-119 的实际调用顺序需要读者多次推理,略有歧义。

Recommendation: 将读档时序整理为简短的编号步骤(1. ResetEvent -> reset 名册;2. 地图存档块读入;3. WorldLoadEvent -> onWorldLoaded(false);4. 单位实体读入;5. app.post -> onWorldLoaded(true)),并将注释放在紧邻相关调用处,减少注释跨行跳跃。


// colspan 动态取宿主当前列数(信号源面板=5 个信道按钮列;不同宿主列数不同,
// 固定 colspan 会只跨部分列、把宿主前几列撑宽导致按钮/标题错位)
parent.add(spec).growX().minWidth(SECTION_MIN).colspan(parent.getColumns()).pad(1f);

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

buildSection() 在向 parent 添加 spec 前调用 parent.getColumns() 作为 colspan(153 行):Table.getColumns() 返回当前表的列数。如果 parent 此时尚未设置列数或布局尚未确定,可能返回 0 或与预期不符。虽然调用方通常在构建配置面板时先定义列结构,但这一依赖是隐式的。

Recommendation: 将 colspan 固定为一个显式值(例如 5 或由调用方传入参数),或者在添加前确保 parent 的列数已确定(调用 parent.defaults().colspan(...) 或显式传参)。最小改动是为 buildSection 增加一个 int colspan 参数,避免对 Table 布局时序的隐式假设。

parent.add(spec).growX().minWidth(SECTION_MIN).colspan(parent.getColumns()).pad(1f);

public static void sendTeamConfig(Building build, Object value) {
for (Player p : Groups.player) {
if (p == null) continue; // 防御性空判:玩家断开清理间隙不抛 NPE
if (p.team() == build.team) {

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

NetSync.sendTeamConfig 遍历 Groups.player 全量列表并逐个判断 team() == build.team。玩家数量较大时(大房间)这是 O(M) 的简单循环,且在配置频繁变化的建筑(如中继器状态每 tick 下发的场景)容易被多次调用。当前设计是按需调用,问题不大,但可考虑按队维护玩家子集以降低常数。

Recommendation: 优先按 build.team 获取该队玩家列表(Groups.player.copy() 过滤或遍历时短路)或缓存队伍->玩家集合(Groups 提供按队分组的访问方式)。对于每 tick 高频下发的 case,建议在调用点进一步节流(已有 5 tick 节流的 UI 已处理大部分可视面板)。

Comment thread src/silicon/content/SatelliteUnits.java Outdated
// (MappableContent → ContentLoader.transformName),于是 sprites/units/satellite-leo.png
// 与 silicon-satellite-leo.png 两种命名都要能命中。super.load() 已试过带前缀的内容名,
// 这里再剥掉前缀试一次;两者都没有时 region.found() == false,交给 draw() 的程序化兜底。
if (!region.found()) {

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

load() 方法中对 region 的兜底(148-152 行)通过 name.indexOf('-') 截取后缀再查找 Core.atlas.find(name.substring(i+1))。这种“去掉 mod 前缀”的处理是为兼容 sprites/units/<裸名>.png 的命名惯例,注释也说明了原因。建议将裸名常量化或提取为私有方法,避免魔法逻辑散落并便于未来扩展。

Recommendation: 将 String base = name.contains("-") ? name.substring(name.lastIndexOf('-')+1) : name; region = Core.atlas.find(base); 封装为 findUnitRegion(String name),并在注释中补充“优先查找带 silicon- 前缀(super.load 已尝试),失败则回退裸名”。

Comment thread src/silicon/Silicon.java Outdated
/** 卫星状态周期广播计时(约 30 tick / 0.5s) */
private static int satelliteBroadcastTick = 0;
/** sat-launch 速率限制(tick):同一控制台两次请求的最小间隔,挡客户端重放刷扫描 */
public static final float LAUNCH_REQUEST_COOLDOWN = 30f;

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

静态字段 LAUNCH_REQUEST_COOLDOWN 定义为 30f(77 行),用于服务端 sat-launch 限流。限流单位是 tick(Time.time 比较用的是 Time.time,服务端以 tick 更新),值 30 意味 0.5 秒,注释也说明了目的(挡客户端重放刷扫描)。建议将其标注为“tick 数”而非秒,或改名为 LAUNCH_REQUEST_COOLDOWN_TICKS 以避免语义混淆。

Recommendation: 重命名为 public static final int LAUNCH_REQUEST_COOLDOWN_TICKS = 30;(与 Time.time 单位匹配:Time.time 是 tick 计数器,见注释中 Time.time - cb.lastLaunchRequest < LAUNCH_REQUEST_COOLDOWN 的用法),并更新注释为“最小请求间隔(ticks)”。

同轮另有 2 条经核实不成立,已在 PR 评论中说明理由。

- Silicon.java:LAUNCH_REQUEST_COOLDOWN(float 30f)→ LAUNCH_REQUEST_COOLDOWN_TICKS(int 30)。
  单位本就是 tick(与 Time.time 同量纲),注释里也写了「(tick)」,
  但常量名没体现,值 30f 容易被读成 30 秒。
- Silicon.java:读档时序注释由「跨 112-119 行分散描述五步」整理为编号步骤 1-5,
  步骤号就近标在对应调用处(// 步骤 3 / // 步骤 5),消除来回跳读。
- SatelliteUnits.java:load() 里「剥前缀找裸名」的兜底提取为 findUnitRegion(current, name),
  doc 注释补齐「super.load() 已试过带前缀的内容名」这一前因。
- NetSync.java:sendTeamConfig 的 for-each 改为索引循环(arc 的 Seq 用 for-each 会分配迭代器,
  而本方法在信号源/中继器状态变化时一帧可能多次调用)。
  未按建议引入「队伍 → 玩家」缓存:调用点是按需触发的变更路径而非每帧热路径,
  玩家数少,加缓存不值当再维护一层失效逻辑。

验证:gradlew clean jar 通过;tests/ 三套件全绿。
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
3 👍
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 1, 2026

Copy link
Copy Markdown
Collaborator Author

第二轮意见处理结果:4 条已改,2 条经核实不成立

已处理(51349cd)

① Silicon.java:77 常量语义 —— 按建议改名并改类型:

- public static final float LAUNCH_REQUEST_COOLDOWN = 30f;
+ public static final int LAUNCH_REQUEST_COOLDOWN_TICKS = 30;

单位本来就是 tick(与 Time.time 同量纲),注释里也写了「(tick)」,但常量名没体现,30f 容易被读成 30 秒。引用点同步更新。

② Silicon.java:105 读档时序注释 —— 整理为编号步骤,并把步骤号就近标在调用处:

// 读档时序(顺序很重要,注释就近标在对应调用处):
//   1. ResetEvent          → SatelliteManager.reset() 清空名册
//   2. map 区域读入        → 各控制台的存档块重建名册
//   3. WorldLoadEvent      → SatelliteManager.onWorldLoaded()     此时单位实体尚未读入
//   4. entities 区域读入   → 卫星实体进入 Groups.unit
//   5. app.post(延迟一拍)→ SatelliteManager.onWorldLoaded(true)  entities 已读完,可剪除死行

// 步骤 3 / // 步骤 5 直接贴在对应调用行尾,不再需要跨行推理。

③ SatelliteUnits.java:148 region 兜底 —— 提取为具名方法,前因写进 doc:

@Override public void load() {
    super.load();
    region = findUnitRegion(region, name);
}

/** super.load() 已试过带前缀的内容名;这里在它没找到时再剥掉第一段前缀试一次。
 *  两者都没有时返回的 region 其 found() 仍为 false,交给 draw() 的程序化兜底。 */
static TextureRegion findUnitRegion(TextureRegion current, String name) {
    if (current != null && current.found()) return current;
    int i = name.indexOf('-');
    return i > 0 ? Core.atlas.find(name.substring(i + 1)) : current;
}

④ NetSync.java:25 遍历 —— for-each 改为索引循环(arc 的 Seq 用 for-each 会分配迭代器,而本方法在信号源/中继器状态变化时一帧可能调用多次):

for (int i = 0; i < Groups.player.size(); i++) {
    Player p = Groups.player.index(i);
    ...
}

未引入「队伍 → 玩家」缓存:调用点是按需触发的变更路径而非每帧热路径,玩家数也少,O(M) 扫描足够;加缓存反而要多维护一层失效逻辑(换队/加入/离开都要同步)。这一点与建议里的「按需调用,问题不大」判断一致。

经核实不成立(未改动)

⑤ OrbitSatelliteController.java:56(MEDIUM)—— 全局对账已有节流,不是每帧执行

意见认为控制器每帧「直接执行全局性维护操作」、「可能被反复触发」。但 reconcileMissing() 内部就有节流:

private static int reconcileTick = 0;

/** 控制器兜底:卫星实体找不到名册记录时调用,节流 60 帧做一次全局对账 */
public static void reconcileMissing() {
    if (++reconcileTick < 60) return;   // ← 每帧只是自增一个 int
    reconcileTick = 0;
    onWorldLoaded();                    // 真正对账 60 帧最多一次 ≈ 每秒一次
}

调用点确实是每帧(控制器路径),但真正跑全局对账的上限是每秒一次。建议的「标记待对账布尔位、下一帧统一执行」与现有节流效果等价,却要多维护一个状态位,因此不改。

⑥ SignalSpectrum.java:153(LOW)—— getColumns() 是刻意的,改成固定值会造成错位

意见建议把 colspan(parent.getColumns()) 换成显式固定值。但紧邻的注释正是为这个写法写的:

// colspan 动态取宿主当前列数(信号源面板=5 个信道按钮列;不同宿主列数不同,
// 固定 colspan 会只跨部分列、把宿主前几列撑宽导致按钮/标题错位)
parent.add(spec).growX().minWidth(SECTION_MIN).colspan(parent.getColumns()).pad(1f);

buildSection 是给多个宿主复用的(信号源面板、中继器面板、检测器面板列数各不相同),固定 colspan 恰好会产生意见里描述的那种错位。这里对 Table 布局时序的依赖是有意为之:调用方都是先定好列结构再调 buildSection。


验证:gradlew clean jar 通过;tests/ 三套件全绿。

@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 二段式)

本次 diff 主体是卫星系统第一阶段(新增 4 个方块 + 4 个卫星单位机型、信号语义从 015 改为 099 对数衰减、SINR 干扰模型、sat-launch/sat-state 多人同步),mod.hjson 版本由 a0.12.4.4 提升至 a0.13.0.0,与「新增方块/单位 → Minor 位 +1」的规范一致,版本检查通过。sat-launch 处理器的入参校验(字段数、坐标空判、团队越权、编码正则、轨道范围、每控制台限流、失败路径统一回包)设计完整,未发现确定崩溃或多人不同步问题。主要问题是 OrbitSatelliteController 在名册缺失分支上对每颗卫星每 tick 调用一次全局对账 reconcileMissing()(注释称「节流」但代码无节流),其余为 Bar 值逐帧字符串分配、未使用 import、冗余空判、重复 netServer 判空块、服务端限流用 Time.time、静默吞 Throwable 等低危项。注意:diff 被截断,SatelliteManager 主体、SatelliteConsole/SatelliteLauncher、SignalChannel/SignalJammer/SignalOverlay 等文件不可见,其 write()/read() 字段追加顺序与 SINR 计算核心逻辑本轮无法核验。

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

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
3 👍
0 👎

Acceptance Criteria:

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

1 similar comment
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
3 👍
0 👎

Acceptance Criteria:

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

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

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

@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 是卫星系统的第一阶段大改(新增 4 个方块、卫星实体单位、SINR 信号模型、主机权威发射链路)。整体架构方向正确:发射请求在 init() 注册包处理器(专服可用)、逐项校验 tile/队伍/速率、失败路径统一回包、按队定向 tileConfig 避免情报泄露,且 mod.hjson 版本号 a0.12.4.4 → a0.13.0.0 与新增内容匹配。发现 1 个中等问题(服务器端速率限制用 Time.time)与若干低级问题,未见确定性崩溃或多人不同步的高危问题。注意 diff 被截断,SatelliteManager 后半段、SatelliteConsole/Launcher、SignalOverlay、SignalChannel、SignalRelay/Source/Jammer 等核心文件未包含在本次审查范围内。

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

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

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

Silicon.init() 里两处 `if (netServer != null) { ... }` 是无意义的守卫:

- Vars.netServer 是静态字段,在本方法执行前一定已就位 ——
  ClientLauncher:182 与 ServerLauncher:76 都早于各自的 mods.eachClass(Mod::init);
- 全代码库没有任何地方把它置回 null:net.closeServer() 只把 net.server/net.active
  置 false 并踢掉连接,netServer 对象本身常驻。

所以守卫恒为真,等价于无条件注册。去掉后行为不变。

客户端上多注册一份也无害:handler 的触发路径自带服务器判定 ——
NetServer.serverPacketReliable(@Remote(targets = Loc.client))只在服务器收到客户端包时被走到。

同时更正此前 PR 评论里的说法:那里写"守卫有效"并不准确,有效的是 handler 触发路径自带判定,
守卫本身是多余的(我原先只核到 init() 时 netServer 非 null,没有继续核"它会不会被重置")。

验证:gradlew clean jar 通过;tests/ 三套件全绿。
@rt334

rt334 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

更正:我前一条关于 netServer 守卫的表述不准确

我在前面那条「两条 MEDIUM 均不成立」的评论里写了这么一句:

所以 if (netServer != null) 在 init() 里读到的是非 null 值,守卫有效。

「守卫有效」这个说法是错的,我更正一下 —— 感谢指出。

我当时只核到「init() 执行时 netServer 非 null」这一层,没有继续核「它会不会被重置」。补核之后:

// Vars.java:299 —— 静态字段
public static NetServer netServer;

// 全代码库唯一的赋值点
ClientLauncher.java:182   add(netServer = new NetServer());
ServerLauncher.java:76    Core.app.addListener(netServer = new NetServer());
// (另有 ApplicationTests.java:87、MapFixer.java:50 两处工具/测试路径)

// 全代码库没有任何 netServer = null
// Net.java:200 —— closeServer 只改标志位,netServer 对象本身常驻
public void closeServer(){
    for(NetConnection con : getConnections()) Call.kick(con, KickReason.serverClose);
    provider.closeServer();
    server = false;
    active = false;
}

所以那个守卫恒为真,等价于无条件注册,并不表达任何判断。

但 robin 那条 MEDIUM 的结论仍然不成立:它说「init() 内 netServer 恒为 null,这批处理器一个都注册不上」—— 方向正好相反,不是注册不上,而是总会注册。

真正让「无条件注册」成立的是 handler 的触发路径,不是守卫:

// NetServer.java:635 —— 自定义包 handler 的唯一触发入口
@Remote(targets = Loc.client)
public static void serverPacketReliable(Player player, String type, String contents){
    if(netServer.customPacketHandlers.containsKey(type)){
        for(Cons2<Player, String> c : netServer.customPacketHandlers.get(type)){
            c.get(player, contents);
        }
    }
}

这条路径只在服务器收到客户端包时被走到。纯客户端上 NetServer 实例虽然存在、handler 虽然注册了,但没有代码会去查它 —— 所以「无条件注册」是安全的,这也正是当前代码的实际行为。

已按此清理(6f77e96)

两处 if (netServer != null) { ... }(sat-launch 与四个 pause*)删掉了守卫,并在注释里写清上面这条理由,避免后来者再把它当成「服务器判定」加回来。

行为不变:5 处 addPacketHandler 从「恒真守卫下的注册」变成「无条件注册」,客户端上多注册的那份永远不会被触发。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

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

@by514

by514 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🏹 Robin

✅ Review done. I flagged 14 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 Review / workflow_run 二段式)

卫星系统第一阶段的整体实现结构清晰:UnitType 隔离旗标(targetable/hittable/physics/useUnitCap 等)、相位纯函数轨迹、按队定向 tileConfig 下发、存档由控制台代存等关键不变量都有明确论证。主要风险集中在两处:服务端 sat-launch 速率限制用 Time.time(该时钟在专服上不由服务器自身驱动,可能导致限流永不解除)、以及 silicon.world.Liquids.load() 被删除但原注释明确记录了「方块构造依赖 lubricant」的时序契约。其余为可加固项(netServer 无条件解引用、headless 下 KeyBind 为 null、单位贴图兜底查找路径少了 units/ 前缀)。版本号 a0.12.4.4 → a0.13.0.0 与新增方块/单位(Minor 位 +1)相符,符合规范。

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

@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 Medium | 🔵 8 Low | 💡 4 Suggestions

Summary

本次 PR 引入卫星系统(LEO/MEO/GEO/SSO 四型实体单位 + 发射中枢/控制台)、信号可用度重构(SINR 门控、0~99 对数衰减、频谱面板)以及一批健壮性修复,做得比较扎实的地方包括:把 pause/sat-launch 包处理器从 ClientLoadEvent 移到 init()(确实修好了 dedicated 服务器上处理器永不生效的问题)、KeyBind 注册加 headless 守卫、NetSync 改为按队定向 tileConfig(避免把电量/进度广播给敌队)、卫星机型显式放行 Env.any/Env.none 并注释了 env 处死路径、SignalSpectrum 用静态缓冲 + 5 tick 节流避免逐帧遍历、README 与中英双语 bundle 的数值口径(0.32/0.48/0.20×短半轴、9.9/8.58/7.26、√N 合成、阈值 3.3)前后一致,且 mod.hjson 版本号 a0.12.4.4 → a0.13.0.0 与「新增方块 + 新功能」完全吻合。主要问题集中在新加的服务端 sat-launch 处理器:它在网络线程里直接读写世界状态(与同文件中客户端处理器 post 回主线程的做法自相矛盾),以及用 tileConfig 复用 configure() 通道下发中继器运行态,存在类型/通道混用风险。


📄 Findings Not Posted Inline

💡 1 (src/silicon/Silicon.java:160) — deny 回调使用了全限定名 java.util.function.BiConsumer,与本仓库 import 顺序规范(java > arc > mindustry > silicon,按包分组)不一致,阅读时容易在长处理器里被忽略。

把 java.util.function.BiConsumer 提到文件顶部 import 区并按 java 组放在最前,处理器体内改用简名。

💡 2 (src/silicon/util/SatelliteManager.java) — 本次改动的核心是可验证的纯函数:log 衰减 raw = 99·(1 − ln(1+d/3)/ln(1+15/3))、SINR 质量折算、√(Σeᵢ²) 多星合成(1/2/3/4/10 星 = 7.92/11.20/13.72/15.84/25.05)、扫描几何(sin 幅度 = 覆盖半径内缩、6% 圈长淡入淡出、SCAN_DRIFT 失谐)。这些公式散落在 Manager/Controller/UI 三处且互相引用,改动时极易出现两处口径不一致(正如 H 覆盖与频谱面板必须共用同一函数那样)。

把上述数值锚点加进 scripts\hub-deep-check.ps1(该脚本本就是项目的「代码锚点检查」载体,README 已从 17 项扩到 30 项),至少锁定几个关键点(d=0→99、d=15→0、N 颗合成倍率、覆盖半径缩放比),让 CI 在公式被改动时报错而不是靠人工比对。


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

Comment thread src/silicon/Silicon.java
// (launch 首行即返回 LAUNCH_NO_HUB)——把同步时序问题伪装成"没绑中枢/无信号"。
// cb 就在手里,直接用它的真值。见 SatelliteConsole.launch 的注释(同一个约定)。
String effSig = (sig == null || sig.isEmpty()) ? cb.selectedSignal : sig;
int result = SatelliteManager.launch(p.team(),

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

服务端 sat-launch 处理器直接在世界/实体状态上执行权威逻辑(world.tile()、SatelliteManager.launch() 扣石油与缓冲电力、创建单位实体、Call 广播),而同一文件中客户端的 sat-state / sat-result 处理器明确注释「包处理器在网络线程回调——一切状态/UI 操作必须 post 回主线程」并 Core.app.post()。同一份代码里两套线程模型不一致:网络线程内改 Groups/Tile 集合会与主线程 update 循环竞争(可能抛 ConcurrentModificationException、扣料时读到半更新状态导致同一请求在不同客户端看到不同结果)。同理新移上来的 pause 处理器也直接在网络线程调用 state.set()。

Recommendation: 把 sat-launch 整个处理体(含校验与 launch 调用)包进 Core.app.post(() -> {...}),只把包解析留在网络线程;pause 处理器同样 post 回主线程再做 state.set()。若因时序原因必须在主线程外判定,至少把状态变更与 Call 广播部分提取到单独方法并 post 执行。

netServer.addPacketHandler("sat-launch", (p, data) -> {
    Core.app.post(() -> handleLaunchRequest(p, data));
});

Comment thread src/silicon/Silicon.java
// 补发该队所有中继器当前 active(active 是自定义字段不随实体同步;新玩家加入时已稳定的
// 中继器不会再有变化事件,必须全量发一次,否则客机 H 键覆盖缺中继器级联段)
for (SignalRelay.SignalRelayBuild rb : SignalRelay.allRelays(e.player.team())) {
Call.tileConfig(e.player, rb, rb.active);

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

PlayerJoin 时用 Call.tileConfig(e.player, rb, rb.active) 把中继器的运行态 active(Boolean)塞进配置通道。tileConfig 的接收端会调用 build.configure(config);中继器的 configure 通常还承担「用户改信道/绑定编码」的 payload 语义,若其实现对 config 做直接强转或对非预期类型产生副作用,则客机与主机本地执行(Loc.both)都会抛 ClassCastException 或把 active 当配置写回。属于「用户配置」与「运行状态」共用一条通道的设计混用。

Recommendation: 为运行态新增专用 @Remote(targets = Loc.client) 方法(如 Call.relayState(Player, Building, boolean)),不要复用 tileConfig/configure 通道;或者在 SignalRelayBuild.configure 开头显式按类型分支,只接受自身 payload 类型,其余忽略。

for(SignalRelay.SignalRelayBuild rb : SignalRelay.allRelays(e.player.team())){
    Call.relayState(e.player, rb, rb.active);
}

Comment thread src/silicon/Silicon.java
// 速率限制:每个请求都会做一遍"信号范围 + 1:1 配对"扫描(O(建筑×源)),
// 改造客户端可高频重放刷 CPU;这里按控制台 0.5s 限流(合法双击本来也会因 produced
// 已清空而失败,限流只挡重放,不影响正常操作)
if (Time.time - cb.lastLaunchRequest < LAUNCH_REQUEST_COOLDOWN_TICKS) {

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

服务端限流用 Time.time 做基准。项目规范要求服务器安全计时使用 Time.globalTime(Time.time 是客户端语义、受速度缩放的时间源)。当前限流在暂停/变速等状态下可能与预期窗口不一致,且与本 mod 自己的多人暂停功能存在耦合。

Recommendation: 改为 Time.globalTime 与同样量纲的常量比较(保持 tick 单位语义不变),或在注释中说明为什么服务端必须用 Time.time。

if(Time.globalTime - cb.lastLaunchRequest < LAUNCH_REQUEST_COOLDOWN_TICKS){ ... }

Comment thread src/silicon/Silicon.java
try {
String[] parts = data.split("\\|", -1);
if (parts.length != 3) {
deny.accept("fail", "malformed packet (fields)");

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

限流判断放在包格式/坐标/队伍/enabled 等校验之后,而这些失败分支都会 SiliconLog.info 一次。改造客户端可用畸形包或错队包高频刷屏,使「每包一条日志」绕过 0.5s 限流,长期可能撑爆服务器日志。

Recommendation: 把「按 (p.con, 控制台) 的请求时间戳」限流提到所有校验之前,或把畸形/越权类日志降频(同一连接 N 秒内只记一次)并使用 warn 级别。

if(!allowRequest(p, cb)){ deny.accept("busy", null); return; }  // 置于最前

Comment thread src/silicon/Silicon.java
deny.accept(String.valueOf(result), null);
}
} catch (Exception e) {
SiliconLog.info("sat-launch: handler error: " + e);

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

handler 异常用 SiliconLog.info("..." + e) 记录:级别偏低(异常被当成普通信息),且只拼接 e.toString() 不输出堆栈,多人环境下排查发射失败几乎拿不到有效线索。

Recommendation: 改用 warn 级别并把 Throwable 直接传下去(如 SiliconLog.warn("sat-launch: handler error", e)),或至少加 SiliconLog.err(..., e)。

}catch(Exception e){
    SiliconLog.err("sat-launch: handler error", e);
    deny.accept("fail", null);
}

// 引擎读档不会重置 Time.time(arc Time.clear() 只清 runs),若用「相位 + 全局时钟/周期」
// 结算,同一次会话内读档会多绕「存档时运行时长/周期」圈(实测踩坑)。
if (orbit != SatelliteConsole.ORBIT_GEO) {
rec.phase += Time.delta / SatelliteManager.orbitPeriod(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 — 🔵 LOW · confidence: low

rec.phase 无界自累加。若它是 float(或作为 float 序列化),长时间连续游戏后相位量级变大,float 有效位约 7 位,累加粒度会粗于 1e-3 圈量级,导致卫星位置出现可见的台阶/顿挫;同时存档里的相位也会无界增长。

Recommendation: 相位定义为 double,并对相位取模回 [0,1):rec.phase = (rec.phase + Time.delta / SatelliteManager.orbitPeriod(orbit)) % 1f;(取模前确认 scanX/scanY 对相位是纯周期依赖)。

rec.phase = (rec.phase + Time.delta / SatelliteManager.orbitPeriod(orbit)) % 1f;

Comment thread README.md
| SSO | 8000 | 南北回绕 + 东西摆动(极轨) | 0.20×短半轴 = 25 格 | 70s |

- **测试卫星**:与信号卫星效果完全相同,仅需 1 硅、1 秒生产,可发射至任意轨道(信号卫星限 LEO/MEO/GEO,SSO 不对信号卫星开放)——用于验证发射链路或廉价快速叠星;**仅在沙盒模式可用**,其他模式不出现(中枢不显示该选项、不生产,发射请求被权威端拒绝);
- **覆盖与强度**:卫星为其所属编码提供星下点覆盖圆;轨道越高覆盖越大、强度越低——覆盖半径随**图幅短半轴**等比缩放(比例 LEO 0.32 / MEO 0.48 / SSO 0.20,250×250 基准即 40 / 60 / 25 格;GEO 为**定点整图**:半径 = 图心到最远角点 + 5% 短半轴的定点环偏移),每星强度 LEO 9.9 / MEO 8.58 / GEO 7.26(与地面同一 0~99 标度),干扰按信噪比(SINR)折算成质量因子后,多星按**非相干功率合成 √(Σeᵢ²)** 叠加(N 颗同强度 = √N 倍,净空 LEO 单星有效 7.92、2 星 11.20、3 星 13.72、4 星 15.84、10 星 25.05);单星即可激活中继器转发(阈值 >3.3),叠星按 √N 提升抗干扰裕度;**约 10 颗卫星覆盖全图,且任意图幅下均成立**(10 颗 GEO 均布时任意时刻联合覆盖约 98%,角落由扫描轨迹周期性覆盖;极端长宽比地图长轴方向按比例增星);

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

「约 10 颗卫星覆盖全图」一段自相矛盾:括号里写「10 颗 GEO 均布时任意时刻联合覆盖约 98%,角落由扫描轨迹周期性覆盖」,但本文件上方表格与第 39 行都明确 GEO 是「定点悬停(与时间无关)」、没有扫描轨迹,因此「GEO 角落靠扫描轨迹覆盖」不成立。

Recommendation: 把「角落由扫描轨迹周期性覆盖」限定为 LEO/MEO/SSO,GEO 单独说明其静态覆盖范围(是否真能覆盖四角需给出几何条件),避免读者按错误模型估算星数。

约 10 颗卫星覆盖全图(LEO/MEO/SSO 由扫描轨迹周期性补齐角落;GEO 为定点覆盖,角落余量需按地图长宽比另算)

// size = 2;
// health = 320;
//}};
// 信号检测器:新方块一律追加在末尾,与上游注册序保持一致(纯测量设备,无游戏逻辑)。

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

注释写「新方块一律追加在末尾……插队会移动其后所有方块的 content id」,但同一段里 signalJammer / satelliteLauncher / satelliteConsole 实际插在 messageTest 之前,signalDetector 又在末尾。文件内两条注册规则并存且互相矛盾,后续维护者容易照着错误的那条改。

Recommendation: 统一成一条规则并显式说明例外:例如「与上游 test 对齐的方块插在 messageTest 之前(仅为减少同步上游的改动),其余一律追加末尾」。

// 注册序规则:① 与上游对齐的方块固定插在 messageTest 之前;② 其余一律追加末尾。
// 存档按内容名映射(兼容),但插队会移动其后所有方块的 content id。

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

// 免疫全部状态效果(含本 mod 的卫星 buff——buff 只上玩家单位,这里只是防御性兜底)
Vars.content.statusEffects().each(effect -> immunities.add(effect));

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: medium

immunities 在 loadContent(内容创建阶段)遍历 Vars.content.statusEffects() 全量拷贝。base content 此时已就绪,但排在 Silicon 之后加载的模组添加的 StatusEffect 不会被拷贝进来,卫星对那些效果不再免疫。

Recommendation: 若确实需要覆盖其他模组的状态效果,改为在世界加载后(WorldLoadEvent / 首个 Trigger.update)再补一次 immunities;否则在注释里写明「仅免疫 base content 状态效果」以免误判。

Events.on(EventType.WorldLoadEvent.class, e ->
    Vars.content.statusEffects().each(eff -> Vars.content.unitTypes().each(t -> t.immunities.add(eff))));

// 名册未就绪(读档时序/旧档名册丢失):本帧悬停,节流触发全局对账补建记录后恢复运动
SatelliteManager.SatelliteRecord rec = SatelliteManager.recordOf(u.id);
if (rec == null) {
SatelliteManager.reconcileMissing();

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: low

updateUnit() 是每帧每卫星调用的路径,记录缺失时会进入 SatelliteManager.reconcileMissing()。注释声称内部已节流,但调用点没有任何节流保护;一旦对账因故(名册代存方被毁、版本不匹配)长期无法补建记录,就会退化为每帧全表对账。

Recommendation: 把节流下沉到调用点(用 recordOf 失败次数或全局计时器控制,如每 60 tick 或每 30 帧才允许触发一次对账),避免依赖被调用方的隐式节流。

if(rec == null){
    if((Time.globalTime & 63) < 2) SatelliteManager.reconcileMissing();
    return;
}

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

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

1 similar comment
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

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

@Xiaobei08
Xiaobei08 merged commit 0bc9a51 into SiliconMod:test Oct 1, 2026
4 of 6 checks passed
rt334 added a commit to rt334/Silicon that referenced this pull request Oct 2, 2026
本轮**不是** merge satellite-fixes —— 那条分支基于第一阶段(SiliconMod#50),与 fork/main
(第二阶段)是两条平行演进:名册格式(5 段 vs 6 段含 type)、类型系统(测试卫星
已删;lockedType vs selectedType)、LOIC 与拦截塔的有无都不同。直接 merge 会产生
115 块冲突,而解冲突等于在两套实现之间逐块选边、会倒推第二阶段的成果。
所以改为以 fork/main 为基座,按每条修复的**意图**重新落地:

- README:修正 GEO 自相矛盾 —— 原句主语是「10 颗 GEO」,谓词却说「角落由扫描轨迹
  周期性覆盖」,而 GEO 是定点悬停、没有扫描轨迹。改为四角余量按图幅长宽比另算、
  实际补齐四角靠 LEO/MEO/SSO 的扫描轨迹。
- SiliconLog 新增 err(String, Throwable):显式转发 arc 的 Log.err 以保留堆栈,
  不走 log(level, text, args)(那只是文本格式化,Throwable 会被压成 e.toString())。
  sat-launch 与 sat-loic 两处 handler 异常改用它。
- Silicon.java:常量 LAUNCH_REQUEST_COOLDOWN(float 30f)→
  LAUNCH_REQUEST_COOLDOWN_TICKS(int 30),单位本就是 tick。
- Silicon.java:sat-launch 限流提前到「拿到 cb 之后的第一件事」;去掉畸形/无效包
  (fields / coords / 非控制台 tile)的日志 —— 它们在限流之前(限流依赖 cb,而 cb
  要先解析 tile),恰恰是刷日志的口子。
- Silicon.java:删掉两处恒为真的 if (netServer != null) 守卫 —— Vars.netServer 是
  静态字段,ClientLauncher:182 与 ServerLauncher:76 都早于各自的 mods.eachClass(
  Mod::init),且全代码库无 netServer = null(closeServer 只改 net.server/active)。
  handler 的触发路径自带服务器判定(NetServer.serverPacketReliable)。
- Blocks.java:注册序注释原本四处都说「注册在最后」,但实际都不在最后(中继器、
  检测器、拦截塔后面都还有方块)。统一成「按添加时间推进 + 上游对齐例外」一条规则。
- SignalSpectrum:Bar 的 Prov<CharSequence> 是每帧求值的,原先在 provider 里现调
  fmtEff(Strings.fixed → BigDecimal + 新建 String)会每帧分配字符串。改为节流刷新
  时预填 effStr 缓存、provider 只返回引用。

版本号 a0.13.0.0 -> a0.13.0.1。

验证:gradlew clean jar 通过;tests/ 三套件全绿;双语键 378/378 对称。
ARCloud217 pushed a commit to ARCloud217/Silicon-ARCforked that referenced this pull request Oct 2, 2026
* Reapply "[a0.13.0.0] feat: 卫星实体化与信号语义重构——信号与卫星·第一阶段"

This reverts commit 7050ca2.

* [a0.13.0.0] fix: 卫星自查修复与首星激活中继——useUnitCap=false、未绑定覆盖补绘、每星强度 1.5

* [a0.13.0.0] fix: SSO 选中信号卫星即灰化 + 中枢卫星按钮只留名字

* [a0.13.0.0] balance: 卫星覆盖与强度重定参——轨道越高覆盖越大强度越低(40/60/80/25 格,1.5/1.3/1.1),10 颗近似全图

* [a0.13.0.0] fix: 卫星上空常驻机型名标签 + 删除弃用的卫星在轨 buff(可发射提示改程序化标记)

* [a0.13.0.0] fix: 卫星右下角悬停面板可正常显示名称——hitSize 24 扩大悬停触发窗 + 程序化 uiIcon + 名字标签改悬停显示

* [a0.13.0.0] fix: 卫星机型名去掉已带前缀——MappableContent 会再加一次 silicon- 前缀致 bundle 键落空、悬停面板显示内部键

* [a0.13.0.0] fix: 移除卫星上空的名字标签绘制

* [a0.13.0.0] fix: PR 审查修复——sat-launch 全失败路径回包(fail)含异常兜底+客户端 fail 映射、NetSync 空玩家防御、Lines 笔画复位、launch 注释清理

* [a0.13.0.0] feat: 覆盖半径随图幅短半轴等比缩放(0.32/0.48/0.64/0.20,与轨道圆共用锚点)——任意图幅 10 颗近似全图

* [a0.13.0.0] feat: 卫星信号改对数叠加——最强一星全额+其余 ln(1+Σ),单星语义不变、叠星边际递减;判定与显示共用 stackEff

* [a0.13.0.0] fix: 卫星信号透明度与信号源一致——移除卫星覆盖盘独立绘制层,范围模式逐格合成单一绘制(同一公式+同一强度标度,消除双重叠画)

* [a0.13.0.0] docs: README 同步——H 键覆盖单层合成与透明度一致、发射反馈闭环、移除卫星在轨状态效果

* [a0.13.0.0] ui: 移除卫星发射中枢方块头顶的制造进度条(保留原版状态条与石油不足图标;面板内进度显示不变)

* [a0.13.0.0] ui: 移除中枢方块石油不足图标——石油需求并入右下角面板材料行(图标+区间数量角标+不足红线,与物品显示同栏同风格)

* [a0.13.0.0] ui: 石油需求数量取自绑定控制台的所选轨道——中枢对称扫描绑定控制台(节流30帧),无唯一绑定时回退区间显示

* [a0.13.0.0] ui: 石油需求无唯一绑定控制台时回退显示 LEO 1.0k(不再显示区间)

* [a0.13.0.0] fix: 读档后卫星冻结+无信号——对账时序修复:WorldLoadEvent 早于单位实体读入,原按 getByID 剪除误杀恢复的名册;改为名册不剪除+app.post 延迟对账+控制器节流兜底

* [a0.13.0.0] fix: 中继器配置面板源按钮选中态实时同步 selectedSource——清除绑定/外部变更后不再残留选中黄框

* [a0.13.0.0] fix: 删除信号源后卫星与中继器不再停止传输——上行门控:卫星广播编码X的前提是本队存在存活地面源X,删源即断链(中继器去活/覆盖消失),重放同编码源自动恢复

* [a0.13.0.0] feat: 卫星轨迹改拟真星下点模型——LEO/MEO 东西向回绕扫描+纬度正弦摆动、SSO 南北向极轨、GEO 定点悬停(覆盖=地图对角线,单星全图);波形(1+δ)失谐西漂使轨迹族铺满全图(含四角);单相位字段复用,存档/联机兼容

* [a0.13.0.0] refactor: 移除发射中枢可发射的地图悬浮标记(保留悬停状态栏可发射提示)

* [a0.13.0.0] feat: 重做发射特效——加色垂直光柱(辉光/主体/内核三层收束)+白闪冲击环+种子化上升粒子流(替代逐帧随机抖动)+星体双翼剪影升空渐淡+贴地喷射云

* [a0.13.0.0] feat: 发射特效追加原版 launchPod(发射舱升空+烟迹)

* [a0.13.0.0] fix: ①中继器源按钮组 minCheckCount=0(未绑定时不再残留首个按钮黄框)②信号覆盖改挂 drawOver 并锁定 z(射击加色光不再冲淡覆盖层)③发射特效补原版 launchAccelerator/launch 大环(launchPod 细条纹被光柱淹没)

* [a0.13.0.0] fix: 信号源/干扰器 changeTeam(夺取换队)时失效缓存——否则旧队依据残留 (team,code) 缓存继续广播已不属于它的编码

* [a0.13.0.0] fix: 卫星控制台信号选择/轨道按钮组 minCheckCount=0——未选择信号时不再残留首个按钮黄框(与中继器同源问题)

* [a0.13.0.0] feat: 信号强度改 SINR 比值制——有效强度=信号功率×质量因子(SINR≤1无信号/SINR≥3.5满质量),干扰压信噪比而非扣幅度,强信号天然抗弱干扰;高斯归一化(R处精确归零,消除覆盖边缘悬崖);卫星层同口径(逐星SINR折算后对数叠星,底噪进质量因子);阈值校准:GEO单星0.53/LEO 1.2仍>0.5激活

* [a0.13.0.0] feat: 覆盖绘制卫星×地面改 RSS 功率合成 total=√(g²+s²)——卫星对已有地面覆盖区域成为真实增益(不再取max互斥),着色归属保持贡献较大一方

* [a0.13.0.0] fix: 卫星机型补 physics=false 退出异步物理系统——单位间推挤由 PhysicsProcess 的 layerFlying 物理体实现(与 hittable 无关),此前卫星被编入物理层与飞行单位互相推挤

* [a0.13.0.0] feat: 信号频谱面板——源/中继配置界面内嵌本点5信道占用/干扰I/可用强度条(SINR制对策入口,当前信道行高亮,15tick节流);effectiveAll增干扰分母输出重载;新增docs/blocks/Signal.md收录SINR比值制/RSS合成/阈值体系全模型

* [a0.13.0.0] fix: 补提交频谱面板接线——SignalSource/SignalRelay 配置界面内嵌 buildSection,SignalChannel 增 intOut 干扰分母重载(上一提交因 git add 路径笔误遗漏)

* [a0.13.0.0] revert: 移除万向交叉器新版UI(非卫星与信号线内容)——删除 UniversalJunctionDialog/新版UI文档/设置项/29个无主键,方块回经典UI(键名统一 universal-junction.* 前缀);保留 dex 脱糖修复与 applyConfig 网络加固

* [a0.13.0.0] feat: H键视角走官方KeyBind(默认H可重绑定)+新增信号检测器方块——1x1纯测量设备,配置界面内嵌本点5信道频谱(复用SignalSpectrum,无当前信道高亮),程序化32x32图标,注册在Blocks末尾不占旧存档ID

* [a0.13.0.0] fix: 信号检测器崩溃修复——方块 region 必须是图集 AtlasRegion(createIcons 打包期对 TextureRegion 强转崩溃),改走 mod sprites 管线:新增 assets/sprites/blocks/signal-detector.png(32x32),移除程序化 load() 覆写

* [a0.13.0.0] fix: 频谱面板信道5越界崩溃——CH_COLORS 长5为0基索引,信道号1基,色板索引统一减一(实测:点开源配置即崩)

* [a0.13.0.0] fix: 频谱lambda捕获编译错误——effBuf 索引改用每轮最终副本 ci(上一提交仅推送未编译通过,本提交为正确实现)

* [a0.13.0.0] fix: 检测器点击不弹UI+频谱I标签小数重叠——showConfig 经 configTapped() 放行(默认=配置类表非空),注册布尔占位配置类;I 标签预格式化 Strings.fixed 1位小数(0.0 显示 --),bundle.format 直塞原始 float 会渲染全精度溢出单元格

* [a0.13.0.0] fix: 检测器无实体+频谱表头错位——hasBuilding()=destructible||update 全 false 导致 Tile 不建 Building(悬停/点击全无反应,实测踩坑),置 destructible=true;频谱表头改嵌套 Table 与数据行同构同列宽(平铺列宽不一致会错位重叠)

* [a0.13.0.0] fix: Robin R4 审查修复——KeyBind注册headless守卫(专用服务器安全);sat-state/sat-result包处理器Core.app.post回主线程(网络线程禁UI/状态);sat-launch信号编码[A-Za-z0-9]{4}校验;频谱回调isValid守卫+占用计数改emitting()(与strengthAt前置条件一致)

* [a0.13.0.0] fix: 频谱接入卫星层+布局防重叠重构——绑定信道的卫星按信道聚合(stackEff)并与地面RSS合成进强度条,计入占用;频谱改单嵌套表固定列宽(打开时空标签pack定宽,刷新填文本外层不加宽导致居中文本溢出互渗,实测踩坑),当前行高亮改信道号变色

* fix: 投票工作流触发与配置路径修复

- voting.yml: 触发补齐 pull_request_review/submitted + issue_comment/created,使审批投票实时重算;action 切回 myyk/git-democracy@v2 并 configPath 改为 .github/workflows(安全路径,防 PR 篡改投票规则)
- .voting.yml/.voters.yml 已同步至 .github/workflows/(保留根目录副本兼容旧 fork),确保 pull_request_target 安全读取

* fix: 投票配置路径回退至根目录(.github/workflows/ 不放会被识别)

- 移除 .github/workflows/.voting.yml / .voters.yml(该目录仅放 workflow,会被 GitHub 误识别)
- voting.yml: 保留 pull_request_review+issue_comment 触发补齐,action 保持 Xiaobei09/git-democracy@main + configPath: .(根目录配置,避免 PR 篡改)

* [a0.12.4.3] release: 投票工作流触发补齐与配置路径修复(robin 审查修复+定版)

* [a0.13.0.0] fix: 信号源面板按钮贴左+频谱表头防渗透——频谱段colspan改动态取宿主列数(原固定4只跨5按钮列的前4列,撑宽列致按钮1偏离左缘),信道按钮行左对齐;信道列28→34容信道二字,表头居中,占用/干扰列加宽

* [a0.13.0.0] fix: 信号源按钮行growX铺满(1贴左5贴右)+信道号列内居中+占用/干扰列按实测中文字形加宽(96/60px,杜绝0源0扰微渗透)

* [a0.13.0.0] fix: 频谱占用计数列内居中

* [a0.13.0.0] fix: 频谱表头显式Label+标签内文本setAlignment(center)双保险居中(仅Cell.center()实测未生效)

* [a0.13.0.0] fix: Align 包路径修正(arc.util.Align)——上一提交未编译通过,本提交为可构建版本

* [a0.13.0.0] fix: 频谱数据文字统一Label内部setAlignment居中——截图实测cell.center在该arc版不可靠,干扰I数据左对齐与居中列头错位;信道号/占用/干扰全部改标签内居中(与列头同机制)

* [a0.13.0.0] fix: 频谱去掉格子水平pad(1f)——占用/干扰列间缝隙在深灰面板上透出竖线,改垂直pad保持行距

* [a0.13.0.0] fix: 频谱整段铺不透明grayPanel底色——列边界竖线仍存,疑似底层像素透出,纯色底覆盖根治

* [a0.13.0.0] fix: 去掉干扰数据I前缀——像素级取证证实大写I竖笔被看成占用/干扰列间分隔线(bundle spectrum.i 改为纯数值,列头已有干扰标注)

* [a0.13.0.0] fix: 审查必修三项——环境旗标、相位自累加、恢复 paused 回包

① SatelliteUnits 补环境旗标 envEnabled=Env.any / envDisabled=Env.none:
   UnitType 默认 envDisabled=Env.scorching(UnitType.java:51-53),于是卫星在焦土图(Erekir 全部地图,
   Planets.java:56)与太空图(Planets.java:182)会被引擎环境处死——该路径不看 hittable/targetable/
   killable/useUnitCap(UnitComp.java:751-753 → Units.unitEnvDeath → dead + Call.unitDestroy),
   表现为发射后一帧卫星消失、UnitDestroyEvent 连名册一起删。轨道实体与环境无关,取 any/none
   (原版 assembly-drone 同写法,UnitTypes.java:4617-4618)。

② 相位改为自累加(读档精确续接,不再依赖全局时钟):
   controller 每帧 rec.phase += Time.delta/周期(GEO 定点跳过),scanU/phaseForSave 直接取 phase,
   onWorldLoaded 兜底重建与 launch 初始相位同步改写。原实现用 phase + Time.time/周期,其前提
   「读档 Time.time 归零」不成立——运行时 arc Time.clear() 只清 runs、引擎读档不重置时钟
   (Logic.java:299-302),同一会话内读档会多绕「存档时运行时长/周期」圈,覆盖圆随之偏移。

③ Silicon 恢复 netClient "paused" 回包处理:该回调在同步上游时被误删,导致 Vars.pause.complete
   永远为假、客机每 60 秒重发一次 pause 请求(pauseMode=0 时服务器还不回包)。

④ launch 出生点直接落在轨道点上(原为先落在发射中枢位置、下一帧才入轨)。

验证:gradlew clean jar(JDK17 + Mindustry v160.1 依赖)BUILD SUCCESSFUL。

* [a0.13.0.0] fix: 审查中危两项——名册死记录剪除 + 干扰器口径统一

① 名册死记录剪除(M1):onWorldLoaded 增加 pruneDead 重载,只在「权威端 + 单位已读入」的那次
   对账(Silicon 里 Core.app.post(...true))剪除 Groups.unit.getByID(unitId) == null 的行。
   WorldLoadEvent 那次仍传 false——该事件在存档 map 区域结束时触发,早于 entities 读入,
   此时剪除会误杀刚从控制台恢复的名册(卫星冻结+无信号的老 bug)。客机不剪除:名册本就是
   sat-state 广播镜像(applyState 整表替换),且其实体到达更晚。
   兜底场景:存档 entity id 重复被引擎重新分配(SaveVersion.java:503-514)后旧记录永久残留、
   launchedCount 虚高。

② 干扰器口径统一(M5):把「同信道 + 邻信道泄漏」的累加抽成 SignalChannel.addJammer,
   地面层 effectiveAll 与新增的单信道查询 SignalChannel.jammerAt 共用同一实现;
   SignalJammer.strengthAt 改为委托 jammerAt(原为「取最强一台」)。此前地面层求和、
   卫星层取最大 → 多台干扰器叠不住卫星,正是判定端/显示端口径分叉。
   顺带更新 SignalJammer 类注释(线性制措辞)与 docs/blocks/Signal.md 的卫星层说明。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] fix: 显示端(H 覆盖/频谱)改按「当前查看的编码」聚合——与判定端口径统一

问题:判定端(中继激活 satelliteStrengthAt>0.5、控制台绑定 inSignalRange)本来就是逐编码的,
而显示端把该队**所有编码**的卫星先求和再对数叠加(SignalOverlay.bestSignal 遍历全部记录、
SignalSpectrum 按信道聚合)——同一个点上叠了多个编码的卫星时,格子上的数字会高于任何单一编码
真实可用的强度,玩家按显示布线会得到与预期不符的结果。

改动:
- SignalChannel.effectiveAll 增「按编码视图」(第 7/8 参 codeOut/viewCode):viewCode 非空时
  信号 = 该编码在本信道的最强一路(同码多台视为同一路),干扰 = 其余全部身份功率和 + 邻道泄漏 +
  干扰器;codeOut 返回每信道结果所属编码(自动模式 = 该信道最强身份的编码)。
- SatelliteManager.bestSatelliteAt(...):覆盖显示用的「逐编码分组取最强」——同编码 sum/max 计
  stackEff 后取各组最大,替代跨编码求和;未绑定记录(code==null)作为独立一组参与(出参 null →
  蓝色渐变保留);支持按信道过滤,供频谱"无编码视图"逐信道取最强单编码。判定仍走 satelliteStrengthAt。
- SignalOverlay:每帧解析一次「当前查看的编码」(配置面板打开的建筑 → 鼠标悬停的本队建筑 → 自动),
  地面与卫星都只按该编码聚合;底部提示追加当前范围(· 编码 XXXX / · 自动)。
- SignalSpectrum:标题下新增「聚合范围」行——信号源面板 = 自身编码、中继面板 = 绑定编码、
  检测器 = 全局(逐信道最强单编码);强度列按编码,占用列仍是信道拥挤度(全部编码 + 干扰器)。
- docs/blocks/Signal.md §3/§5 同步;两个 bundle 增 signal.overlay.view.* 与 spectrum.scope.*。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] feat: 地面侧改按 SINR 判定 + 中继器每 tick 实时检测能否转发(面板/悬停显示入站强度与原因)

① 地面侧也是 SINR 判定了(此前只有卫星链路是):
   - 新增 SignalChannel.groundEffAt(team, code, wx, wy):地面层(信号源 + 已激活同编码中继器)在该点的
     有效强度,和覆盖/频谱同一算法(信号 = 该编码最强一路;干扰 = 其余全部身份功率和 + 邻道泄漏 + 干扰器),
     编码无存活地面源时为 0;带"本编码是否有发射机覆盖本点"的廉价前置,远处空闲中继不跑完整批算。
   - inGroundSignalRange 改为 groundEffAt(...) > 0:建筑(卫星控制台)绑定与卫星发射的 1:1 配对
     (hubsInSignal/consolesInSignal)从此都吃 SINR——干扰器覆盖到中枢/控制台所在点即可让配对判定失败。
② 中继器实时检测(去掉原先"每 20 tick + 几何距离"的判定):
   - SignalRelay.updateTile 每 tick 调 updateActive:地面 groundEffAt、卫星 satelliteStrengthAt,
     同一阈值 FORWARD_THRESHOLD=0.5(与覆盖显示/频谱"可用强度"同口径);干扰器开关、源断电/禁用、
     绑定变更、级联上下游变化都在同一 tick 生效。
   - 级联不再需要几何距离特判:已激活的同编码中继器本身是发射机,自然进入下一跳的信号项。
   - 记录实测入站强度 inGroundEff/inSatEff;配置面板与悬停信息新增实时状态行
     "转发中/信号不足/无电/已关闭/未绑定 · 入站 地面 X / 卫星 Y(阈值 0.5)"。
③ 文档与文案:docs/blocks/Signal.md §3/§4(阈值体系 + 实时性 + 有效半径约 12.5 格说明);
   双 bundle 更新中继器描述并新增 status.line/weak/off/noPower。

行为变化:净空单源的实际可转发/可绑定半径由 15 格收窄到约 12.5 格(阈值 0.5);干扰器现在能压断地面链路,
也能让卫星发射的配对判定失败(放置预览圆仍画 15 格原始半径,以覆盖显示/频谱数值为准)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] balance: 卫星叠星改非相干功率合成 √(Σeᵢ²)(替换原对数叠加 max+ln(1+Σ其余))

理由(也是这次换函数的三个判据):
① 与层间合成同一运算:卫星×地面本来就是 √(g² + s²),叠星改用同一个 √(Σ·²) 后整个模型可结合——
   "先叠星再叠地面"与"把卫星和地面源一起丢进 √(Σ·²)"结果完全相同,不再存在两套叠加语义;
② 物理正确:多颗卫星是彼此独立的非相干发射源,功率(幅值平方)相加、幅值按平方根合成;
   线性叠加等于把 N 颗当成一台 N 倍功率发射机(失真),对数叠加则是在强项上再套一层非线性压缩;
③ 尺度自洽:N 颗同强度 = e·√N(数量翻 4 倍强度才翻倍),单星语义完全不变(首星阈值不变),
   抗干扰裕度随 √N 单调增长——LEO 单星有效 1.2 时,可容忍的同信道干扰由 j≈0.32(1 星)
   提升到 ≈0.56(4 星)/≈0.80(10 星),递减但持续有效。

改动:
- SatelliteManager.stackEff(Σeᵢ²) = √(Σeᵢ²)(单参,原 sum/max 双参签名删除);
  satelliteStrengthAt 累加平方和;bestSatelliteAt 的分组缓冲由 {sum,max} 改为 {Σeᵢ²};
- SignalSpectrum 卫星层缓冲 satSum/satMax → satSumSq(非相干功率累加),出值开方;
- docs/blocks/Signal.md §2(公式 + 三个理由 + 新旧数值对照)、§3 锁定设计表述;
  README 特性段与更新日志条目、双 bundle 的卫星控制台描述同步。

数值对照(同 10 颗 LEO,单星有效 1.2):RSS = 3.79;旧对数叠星 = 3.67(大 N 相近、小 N 偏高);
线性叠加 = 12.0。阶梯:1/2/3/4/10 星 = 1.20 / 1.70 / 2.08 / 2.40 / 3.79。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] ui: 术语统一「信号可用度」+ 删掉频谱「(强度 = 该编码可用值)」

- H 覆盖底部提示:正在显示信号强度 → 正在显示信号可用度(EN: Showing signal usability)
- 频谱"可用强度"列头 → "可用度"(EN: Usability)
- 频谱聚合范围行:删掉"(强度 = 该编码可用值)",只留"聚合编码 XXXX"(EN: code XXXX)
- 同口径文案同步:数字透明度设置、H 键切换设置、信号源/中继器/干扰器/检测器描述;
  干扰器描述顺带改成 SINR 措辞(原文还是线性制"减去干扰"的旧说法)
- 中继器统计键 EN 与中文对齐(Relay Signal Usability)

* [a0.13.0.0] balance: 强度标度 15 → 99;距离衰减改对数衰减;卫星强度按同比例 ×6.6 同步

① 标度与衰减形状
   - SignalSource.MAX_STRENGTH 15 → 99;新增 LOG_SIGMA = 3 格,strengthAt 由归一化高斯改为**对数距离衰减**:
     raw(d) = 99 · (1 − ln(1 + d/3) / ln(1 + 15/3)),中心 99、R=15 格处精确归零(无边缘悬崖)。
     形状是"近场快、远场慢"的对数损耗:1 格 83 / 3 格 61 / 5 格 45 / 7.5 格 30 / 10 格 18 / 12 格 10 / 13.5 格 5。
   - 底噪 SignalChannel.NOISE_FLOOR 0.5 → 3.3、转发阈值 SignalRelay.FORWARD_THRESHOLD 0.5 → 3.3
     (都是「标度上限 / 30」,即换标度只让数字更细,**模型相对行为不变**)。
② 卫星强度按同比例(×6.6,保持与地面同一 0~99 标度)
   - LEO/SSO 1.5 → 9.9、MEO 1.3 → 8.58、GEO 1.1 → 7.26;
     净空单星有效值 7.92 / 5.49 / 3.48(阈值 3.3,GEO 依旧刚好过线,与旧标度 0.53 vs 0.5 的相对关系一致);
     叠星 √(Σeᵢ²):LEO 1/2/3/4/10 星 = 7.92 / 11.20 / 13.72 / 15.84 / 25.05。
③ 显示与文档
   - H 覆盖数字表改为按 MAX_STRENGTH 生成(0~99),clamp 上限随之;两位数字的居中偏移按位数补偿。
   - 频谱强度条分母改用 SignalSource.MAX_STRENGTH;Signal(world.meta)默认/夹取也用该常量。
   - docs/blocks/Signal.md §2(新公式 + 距离-强度表 + 新常数)、§4(阈值 3.3、有效半径约 14.0 格)、§5;
     README 特性表与 a0.13.0.0 条目;双 bundle 的信号源/中继器/卫星控制台描述同步。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] ui: H 覆盖数字对齐到方块中心;中继器悬停面板只留原版 bar

① 数字居中:此前采样与绘字都取 gx*8(格子左下角),所以数字整体偏向方块的左下。
   改为取格子中心 gx*8+4;横向按字体度量居中(数字步进 × 位数,1 位/2 位都居中),
   纵向沿用原基准字号调好的 -1.6 偏移(按字号倍率缩放)。范围模式的逐格 Fill.rect 同样
   改到格子中心,格子与世界格网对齐(此前整体偏 -4,-4)。
② 中继器悬停面板(右下角信息栏)删除自定义两行(绑定编号 + 实时转发状态),
   只保留原版 bar(生命/电力);绑定编号与实时状态仍在**配置面板**里显示,功能不变。

* [a0.13.0.0] fix: H 覆盖与频谱改为同一函数(数字必然一致)+ 数字用 Align.center 精确居中

① 一致性(此前两处各写一套,数字会对不上)
   - 新增 SignalChannel.usableAll:单点 5 信道的**最终可用度**(地面 SINR ⊕ 同编码卫星非相干功率合成),
     显示端唯一实现。H 覆盖取五行里的最大值并复用其归属(建筑色/编码色),频谱面板直接显示这五行——
     因此 H 上的数字必然等于频谱里最高的那一行。
   - 顺带修掉一处结构性差异:旧 H 覆盖在自动模式下只把"地面最强信道"那一个编码的卫星算进去,
     当卫星与地面不在同一信道时(很常见:源在信道 1、卫星固化在信道 3)会低报,而检测器逐信道算能看到;
     现在两边逐信道同口径。
   - 频谱刷新 15 tick → 5 tick(约 12 Hz):卫星是移动的,节流太长会让面板数字明显落后于每帧重算的 H 覆盖。
② 数字居中
   - 横向改用 arc 的 Align.center(与绘制同一套布局代码,1 位/2 位数都精确居中),不再自己按字体度量猜偏移;
     采样与绘字都取格子中心(tile 覆盖 [gx*8, gx*8+8),中心 +4)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] chore: 评审低优先项清尾(编码校验统一、发射限流、客机不本地生产、覆盖裁剪、读档夹取)

- **编码校验唯一化**:新增 Signal.isValidCode(4 位大写字母/数字),信号源 configure/read、中继器 configure、
  卫星控制台 configure 全部改用它——此前只有信号源校验,中继器/控制台接受任意字符串,会作为 key 长期
  驻留 SignalChannel.liveSrcCache 与颜色缓存并污染按钮文本(同队客户端可反复下发不同长串撑大缓存)。
- **sat-launch 限流**:每个请求都会做一遍「信号范围 + 1:1 配对」扫描(O(建筑×源)),改造客户端可高频重放;
  按控制台 0.5s 限流(Silicon.LAUNCH_REQUEST_COOLDOWN,控制台记 lastLaunchRequest,不落存档),
  合法双击本来也会因 produced 已清空而失败,限流只挡重放。
- **客机不做本地生产/充电**:SatelliteLauncher.updateTile 在非权威端直接返回——材料/进度/缓冲电一律以
  服务器快照(applySnapshot)为准,避免快照到达前本地扣料与推进进度造成短暂分叉。
- **H 覆盖覆盖包围盒裁剪**:每帧算一次「本队源/已激活中继/在轨卫星覆盖圆」的并集包围盒,盒外格子直接跳过,
  不再每格跑完整 SINR 批算(卫星 GEO 覆盖全图时等价于原行为)。
- **读档夹取**:SignalJammer.read 的 jamChannel 夹到 [-1, 5](越界值会让干扰器永远匹配不到信道而静默失效);
  SatelliteConsole.read 的名册条目数上限 64(损坏存档不再能触发长循环)。
- 删除无人引用的 bundle 键 block.silicon-signal-relay.signal(中英各一行)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] docs: README 同步信号中继器新行为(SINR 阈值转发 + 状态行)

Robin 评审(README.md:24)指出表格里中继器一行仍写着旧的「进入覆盖即激活 / 强度随距离衰减」,
与 a0.13.0.0 的实现和游戏内描述(bundle_zh_CN.properties:151)不一致。改为:

- 每 tick 实时判定,绑定编号的地面信号可用度(SINR)或同编码卫星强度 > 3.3(0~99 标度)才转发;
- 干扰器、同信道异编码源可把有效强度压到阈值以下从而断链;
- 配置面板状态行实时显示入站地面/卫星强度与未转发原因(信号不足 / 已关闭 / 无电)。

仅文档改动,无代码变更。

* fix(signal): 信号源/中继器缓存自愈——已拆除的建筑不再留在列表里

现象:放 2 个信号源再拆掉 1 个,卫星控制台的信号列表里被拆掉的那个编码仍在。

原因:`SignalSource.allSources` / `SignalRelay.allRelays` 走的是「Groups.build 快照 + dirty 标记」
缓存,而 Mindustry 的拆除顺序是 `preChanged()`(先 `onRemoved()` → markDirty)→ `changeBuild()`
(才 `Building.remove()` 真正离开 Groups.build)。只要在这两步之间有任何一次列表查询
(中继器每 tick 的实时判定、频谱面板、H 覆盖、控制台面板每帧刷新),重建出来的缓存就会把
尚未离开 Groups.build 的死源一起带回来,同时把 dirty 清掉——此后死源永久留在列表里,
直到下一次增删源才纠正。

修复(两道保险,任何一次查询之后缓存都不含死源):
- `SignalSourceBuild` / `SignalRelayBuild` 新增 `removed` 标记:`onRemoved()` 里置位并立即把
  自己从缓存 Seq 中摘掉,`onProximityAdded()` 里复位;`rebuildCache()` 跳过带标记的建筑,
  拆除窗口内发生重建也不会把死源加回来。
- `allSources()` / `allRelays()` 增加自愈剔除:按 `removed || !isValid()`(即"是否仍是本格建筑
  且未被标记死亡")逐项过滤,并在真的剔除了条目时同步失效 `SignalChannel` 的上行门控缓存
  (否则 `hasLiveSource` 仍会按旧结果放行卫星广播)。每队信号源数量级很小,开销可忽略。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): 绑定信号在编码消失时自动清除(控制台 / 中继器)

现象(用户实测):拆掉信号源后,卫星控制台的「当前卫星所属信号」仍挂着那个已删除的编码,
中继器面板同样一直显示「转发信号源:XXXX」+ 信号不足。

语义:信号编码是随机生成的四位串,源被拆掉/被打掉后**无法再建出同名源**,保留绑定只会永久
显示一个不存在的信号。因此改成自动清除:

- `SatelliteConsoleBuild.updateTile()`:`selectedSignal` 在本队已无存活信号源
  (`SignalChannel.hasLiveSource` 为 false)→ 置空并 `configure("")` 同步给各端。
- `SignalRelayBuild.updateTile()`:`selectedSource` 同理清除后照常走 `updateActive()`。
- 判定只看「编码是否还存在」,**不是**「是否在范围内」:源仍在但断电 / 超距 / 被干扰时保留绑定,
  由绑定状态行红字提示(避免把暂时性问题当成源被删)。
- 只在权威端判定(`!Vars.net.active() || SatelliteManager.isAuthority()`),客机跟随 tileConfig
  下发,避免网络延迟导致的误清。
- 文档:`docs/blocks/Signal.md` §4 判定表新增「绑定自动失效」一行。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* chore: PowerProtector 文档跟随上游回退(本 PR 不再改这份文档)

上游 SiliconMod#64 已把 PowerProtector 核心回退到 417 行版本,fork 之前加在这份文档顶部的
「新版架构」警示横幅随之失效;本 PR 主题是卫星/信号,不顺手改它。

* fix(signal): 补上信号干扰器缺失的贴图

`SignalJammer` 注册的方块名是 `signal-jammer`,构造函数里没有任何 `region`/`uiIcon` 覆写,
但仓库里一直没有 `assets/sprites/blocks/signal-jammer.png`(只有 signal-source / signal-relay /
signal-detector 三张)。`Block.region` 默认取 `Core.atlas.find(name)`,找不到就落到 error 贴图,
所以干扰器在建造菜单与地图上都是占位方块。

新增 32×32 的 `signal-jammer.png`:沿用同族配色(主体 (70,74,88) / 深边 (45,48,60) /
亮部 (160,170,190),与 satellite-launcher、signal-detector 一致),图形为「天线 + 信号波 +
红色斜杠」,一眼可辨"干扰"。

* feat(satellite): 轨迹边界内缩 + 回绕淡入淡出;补卫星贴图与轨迹文档

① 轨迹边界(`SatelliteManager.scanInset`):正弦轴的内缩由固定 8px 改为**按该轨道的覆盖半径**
   (`clamp(coverageRadius, 0, 0.8·half)`,下限 `SCAN_MARGIN`)——旧值让卫星贴边时大半个覆盖圆跑到图外,
   名义覆盖率打对折。回绕轴保持全幅(卫星本就该从一侧进场、另一侧离场)。
② 回绕淡入淡出(`WRAP_FADE = 0.06` + `SatelliteManager.presence/presenceOf`):`frac(u)` 落在每圈首尾
   6% 时存在度线性升/降,同时作用于 ① 卫星本体的绘制透明度 ② `satelliteRawAt` 的原始强度——
   覆盖显示、SINR 判定、中继激活随之同步升降,消除回绕瞬间的"瞬移"观感,也不会出现"图没了信号还在"。
③ 贴图:新增 `assets/sprites/units/satellite-{leo,meo,geo,sso}.png`(48×48 = 12 世界单位,本体 + 两侧太阳能板,
   四型按轨道区分轮廓与轨道色)。`SatelliteUnits.load()` 依次尝试「内容名(含 mod 前缀)→ 去前缀名」
   两种图集键;绘制时中心补一个队色点(贴图不含队伍信息);两者都缺时仍走原程序化兜底,不会出 error 白方块。
④ 文档:新增 `docs/blocks/Satellite.md`(轨迹公式 / 淡入淡出 / 覆盖与强度 / 发射与名册 / 视觉 / 常量速查);
   README 卫星章节同步为扫描轨迹模型:周期 90/150/70s、GEO 定点整图(不再是 80 格 / 240s)、
   去掉"圆轨道"与 GEO 0.64 比例,补一条"扫描几何"说明。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): H 覆盖的数字重新对齐地格(去掉锚点残留的 -1.6px 纵向偏移)

**问题**:H 数字模式下数字整体相对地格上移——k=1 时 1.6px(≈0.2 格),字号倍率大时 k=2.5 → 4px(半格),
看起来就是"数字对不齐地面格子",连带着整片覆盖也像被挪了半格。

**原因**:`SignalOverlay.drawNumbersOverlay` 的 `Fonts.def.draw(num, wx, wy - 1.6f * k, Align.center)`——
纵向那个 `-1.6f * k` 是切到 `Align.center` **之前**的锚点残留:横向早已改成 `Align.center`(1/2 位数都精确
居中),纵向却还在手工补偿。而 arc 的 `GlyphLayout` 在 valign 分支本来就按 `FontData.capHeight` 做垂直居中
(`GlyphLayout.setText`),`Align.center` 下文字的视觉中心已经等于传入点,**任何手工偏移都会破坏对齐**。

**改动**:`Fonts.def.draw(num, wx, wy, Align.center)` —— 采样点与绘字点都取格子中心 `gx*8+4`
(tile 索引 gx 覆盖世界坐标 `[gx*8, gx*8+8)`),删除已无用的 `k`,并把"对齐由 arc 的 capHeight 居中负责、
不要再叠加像素偏移"写进注释。范围模式本就用 `Fill.rect(wx, wy, 8f, 8f)` 以格子中心为锚,不受影响。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿。

* fix(signal): H 覆盖数字按字体度量自算锚点(对齐格子中心)+ 新增单格调试探针

**问题(实测确认)**:`Fonts.def.draw(num, wx, wy, Align.center)` 并没有把数字居中——arc 的
`Font.draw(str, x, y, align)` 只把 align 当 halign 用、纵向锚在**基线**上,于是 (x,y) 实际是"文字左下角",
数字整体落在格子中心的**右上角**(实测:"向方块右上角偏移")。上一次删掉 `-1.6f * k` 只是去掉了额外的
手工误差,锚点本身没解决。

**修复**:用 `GlyphLayout` 量宽度、`Font.getCapHeight()` 当高度,把左下角锚点挪到
`(格子中心 − 尺寸/2)` —— 对齐不再依赖 arc 的 Align 语义;`GlyphLayout` 静态复用,无逐格分配。

**新增调试方块 `signal-probe`(信号探针,仅沙盒可见)**:
- `SignalSource.radius` 由写死的常量改为**每方块字段**(默认 15 格),覆盖判定/范围绘制/H 覆盖包围盒
  统一读它;探针取 `0.5` 格 ⇒ 只有它自己那一格的中心在覆盖内、四邻 1 格均为 0。
- 按住 H 后全图只会出现这一格的「99」,方块贴图自带**十字准星**,数字中心与格心是否重合一眼可判。
- 探针覆写父类"必须有电才发射"的闸门(`consPower = null`),沙盒里放下即用(仍需设一个 4 位编码)。
- 双语 bundle 补 name/description;贴图 `assets/sprites/blocks/signal-probe.png`(32×32,调试绿 + 准星)。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(probe): 探针彻底去电力 + 选中/放置画真实覆盖 + 常驻自评读数

**电力(真因)**:父类构造里的 `consumePower(150/60)` 只是把 `ConsumePower` 加进 `consumeBuilder`,
而 `Block.init()` 阶段会从 builder 重建 `consPower` 并置 `hasPower = true`——所以只在构造期写
`consPower = null` 会被覆盖回去:方块信息面板照样显示耗电、没接电时照样画断电图标,看起来就是
"探针需要电"。现在改用 `removeConsumers(c -> true)` 直接清空 builder,`init()` 不再生成任何消费者,
`hasPower/consumesPower` 保持 false;`enableDrawStatus = false` 再兜一层(不画任何状态图标)。
发射判定(`strengthAt`/`emitting`)继续覆写掉父类的 `hasPower()` 闸门,放下即用、不接电网。

**真实覆盖可视化**:`drawSelect` / `drawPlace` 不再画父类的 15 格圆(对 0.5 格半径纯属误导),
改画本格 8×8 边框 + 格心十字——正好当"数字是否落在格心"的参照物。

**常驻自评读数**:方块上方一行 `code=XXXX ch1 r0.5 ON raw99`(编码/信道/半径/开关/本格原始强度),
不依赖 H 覆盖就能判断探针自身状态、定位"为什么不发射";文字宽度用 `GlyphLayout` 量、左锚点居中
(与 H 数字同一套锚点算法),绘制前后保存并恢复 z / 字号 / 字色,不留全局副作用。

**描述**:双语 bundle 更正——编码由放置时自动生成(父类 `placed()` 逻辑),无需手动设置。

* fix(probe): 自诊断读数(cell/off)+ 采样点标记

探针读数扩展为 `code ch r ON raw cell off`:
- `cell` = 用 H 覆盖的同一函数 `SignalChannel.usableAll` 在本格中心算出的可用强度(应等于 raw);
- `off` = 本格中心与最近覆盖层采样点(世界坐标 ≡ 4 (mod 8),即覆盖层逐格采样的位置)的差(单位:格)。
  若非 0,说明采样网格与建筑中心错位——半径 0.5 格的探针会因此整格采不到数字。
另在最近的采样点画一个红圈,便于肉眼比对(贴图中心的绿点/准星就是本格中心)。

* fix(signal): H 覆盖采样点修正半格错位(gx*8+4 → gx*8)——数字终于落在格心

**根因(实测定位)**:调试探针读数给出 `off-0.50,-0.50` —— 覆盖层逐格采样的世界坐标是 `gx*8 + 4`,
而引擎里地格的坐标就是**索引 × 8**(1×1 建筑中心在 8 的整数倍上;官方 `PowerNode.drawPlace` 也是
`x * 8f + offset`,不加 4)。两者整整差半格,后果分两级:

1. 普通信号源:数字(与范围色块)整体相对方块/地格偏移半格——就是「数字向方块右上角偏移、
   显示范围与实际范围不一致」那个老问题;
2. 半径 0.5 格的调试探针:四个采样点到探针中心的距离都是 √2/2 ≈ 0.707 格 > 0.5,**每个采样点都在
   覆盖外** → 整格采不到值,表现为「探针不发射信号」(其实 `raw99`、`cell99` 都正常)。

**改动**:
- `SignalOverlay` 数字模式与范围模式的采样点统一改为 `gx * 8f` / `gy * 8f`(= 建筑所在网格),
  使「采样点 = 建筑中心 = 填色格中心」三者一致;
- `SignalSource` / `SignalRelay` 的放置预览圆按官方写法改为 `x * 8f + offset`;
- 探针诊断的 `off` 基准同步改到新网格(预期恒为 0.00,0.00),红圈应正好套在方块中心绿点上。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): H 覆盖数字的垂直锚点修正(锚点是文字上边缘,不是基线)

探针截图逐像素量:红圈(采样点)中心 (899.5, 279),数字 `99` 的 bbox 上边缘 300、中心 319 ——
数字整个沉在格心**下方**约 3.8 世界单位(≈0.47 格),而它的上边缘恰好落在我传入的锚点上。

结论:`Font.draw(str, x, y, Align.left)` 的锚点是文字的**上边缘**(GlyphLayout/FontCache 内部再按
ascent 折算基线)。上一版按"基线"理解、把 y 减了半个字高,等于把整行数字又往下压了半个字。

改动:锚点改为 `(采样点.x − 宽/2, 采样点.y + capHeight/2)` —— 上边缘抬到格心上方半个字高,
数字的**视觉中心**才落在格心上;水平方向本来就是左边缘 + 宽/2,实测已居中(偏差 <0.5px/10),不动。

验证:`gradlew clean jar` 通过。

* chore(signal): 删除调试方块「信号探针」及其只为它引入的半径管线

对齐问题(采样点半格错位 + 数字垂直锚点)已定位并修好,探针完成使命,整体移除:

- 删除 `SignalProbe.java`、`assets/sprites/blocks/signal-probe.png`,`Blocks.java` 去掉注册/字段/import,
  双语 bundle 删除 `block.silicon-signal-probe.name/description`;
- 还原 `SignalSource` 的「每方块半径」改造(`radius` 字段、`radius()` 访问器、带半径的 `strengthAt` 重载)
  ——它当初只为 0.5 格的探针而加,现在恢复常量 `RADIUS`;
- `SignalOverlay.computeCoverBounds` 同步回到统一半径。

**保留**(与探针无关的真修复):H 覆盖采样点由 `gx*8+4` 改为 `gx*8`(建筑网格,修正半格错位)、
数字垂直锚点改为「上边缘 + 半字高」、`SignalSource`/`SignalRelay` 放置预览圆改为官方写法 `x * 8f + offset`。

注:探针是最后注册的方块,删除它不影响其余方块 ID;已有存档里的探针按名字映射失效,会变成空格子(不崩)。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿;jar 内无 SignalProbe/signal-probe 残留。

* style: 行尾归一化为 LF(9 个文件此前被 API 推送写成了 CRLF)

这 9 个文件是用 GitHub API 直接推的、上传的是本地工作区内容,而本仓库工作区 `core.autocrlf=true`
会把检出内容变成 CRLF —— 于是远端存成了 CRLF,而上游各文件都是 LF。后果是 PR 里这些文件显示为
**整文件重写**(例如 SignalSource.java 的 diff 变成 +321/-293),评审时噪音极大。

本提交只做一件事:把这两个分支上这 9 个文件的 CRLF 全部转回 LF,**不改任何代码/文字内容**
(README 在两分支本就不同,各自按自己的内容转换)。以后所有 API 推送都会先做 LF 归一化。

* refactor: 移除炼油厂与润滑油(与 fork/main 对齐)

上游 SiliconMod#64 把石油炼化厂以「注册注释掉」的形式放进了仓库,本分支此前整套保留着,而 fork/main 已删除。
本次让两边一致,删除:

- `src/silicon/world/blocks/production/PetroleumRefinery.java`
- `src/silicon/world/Liquids.java`
- `assets/sprites/blocks/petroleum-refinery.png`、`assets/sprites/liquids/lubricant-oil.png`

以及全部引用:`Blocks.java` 的 import/字段/注释注册、`Silicon.java` 的 `silicon.world.Liquids.load()`、
双语 bundle 的 `block.silicon-petroleum-refinery.name/description`。

说明:这等于把上游 SiliconMod#64 里这套**未启用**的内容从本 PR 移除(上游若日后正式启用,同步时会再引入);
fork 的发布/自动化基础设施(`.github/workflows/fork-publish.yml`、`scripts/*`、`docs/WORKFLOW.md`)
只存在于 fork/main,**不进本 PR**。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿;jar 内无 PetroleumRefinery / world/Liquids / lubricant-oil 条目。

* chore: 适配 Mindustry v160.5(依赖 v160.1 → v160.5,零源码改动)

- `build.gradle`:`mindustryVersion` 由 `v160.1` 提升至 `v160.5`(Steam 客户端现为 `steam build 160.5`);
- README:顶部兼容版本(`v160.5+`)、构建依赖说明同步为 v160.5,更新日志补一条适配记录;
- 结论:同一份源码下 v160.5 与 v160.1 依赖的**构建产物逐字节相同**——160.2~160.5 未触及本模组用到的 API
  (发布内容均为缺陷修复与 UI 调整:target dummy、逻辑处理器、数据补丁、连锁闪电等),无破坏性变更;
- `minGameVersion` 维持 `155.4`,向后兼容不变;
- 验证:本地 `gradlew clean jar`(JDK 17 + v160.5 依赖)通过;`tests/` 三套件全绿;该适配此前在
  `chore/adapt-v160.5` 分支上的 CI(Build Mod @ a028eb9)亦为 success。

本 PR 只取这一个适配提交的改动(改动 2 个文件);fork 侧的发布/自动化基础设施仍然只存在于 fork/main,不进本 PR。

* docs: PowerProtector.md 同步 main 版(保留「本文档已过期」警告)

该文件此前在 PR 分支被「交还上游」(不带警告),而 fork/main 上有 8 行过期警告 —— 两边不一致。
现按「PR 与 main 内容保持一致」的约定统一:以 main 版为准(警告内容是事实——文档描述的是 v14 时代
架构、与 v19 重构后的实现严重脱节,upstream 侧同样成立),两分支此文件完全相同。

* fix(signal/satellite): 修审核发现的口径与生命周期缺陷(含一处存档错位)

自查 + 独立复核发现的问题,按严重度修复。

[阻塞] SatelliteConsole 名册读写不对称 → 存档错位
写侧 write.i(list.size) 无上限、读侧 Math.min(..., 64) 截断:名册 >64 条时读侧少消费
(n-64)×(16+码长) 字节。反汇编确认引擎 SaveFileReader.readChunk 是「读长度 → 交给 runner →
直接 return len」,**不做**任何按声明长度的对齐,因此后续每个 tile 的 chunk 前缀都会被当数据读,
整档解析错位。现读写共用 ROSTER_MAX=64,且读侧把写入的 n 条**全部消费**(超限只解析不恢复),
流位置与写侧严格一致,已有 >64 条的存档也能正确读出。

[高] SatelliteUnits.drawFallback 的 tc.a(alpha) 直接改写共享 Team.color 的 alpha(上一行有 cpy、这行漏了)
→ 卫星回绕淡入段把该队全局队色改淡且不恢复。改 tc.cpy().a(alpha)。
[高] SignalChannel.usableAll 的「按编码」分支缺卫星上行门控(自动分支/判定端/频谱占用计数都有)
→ 地面源被拆光后 H 覆盖与频谱仍显示该编码卫星强度,与判定端打架。补 hasLiveSource 门控。
[高] SignalRelay.updateActive 判定用 max(地面,卫星),显示端用非相干合成 √(g²+s²),两者恒不相等
→ 阈值 3.3 附近出现"H 显示 3.4 却不转发"。统一到显示端的合成值(顺带让面板「卫星」列在地面有信号时
不再恒为 0)。
[高] SignalRelayBuild 缺 changeTeam → 换队/夺取后中继缓存不失效(旧队仍当发射机、新队漏掉它)
→ 补 changeTeam + markDirty(与 SignalSource/SignalJammer 同款)。
[高] SignalJammer 全局缓存无 removed 过滤、无自愈剔除 → 借「onRemoved 早于离开 Groups.build」的窗口,
死干扰器会被固化进缓存并持续压制该信道。加 removed 标记 + 重建过滤 + allJammers 自愈剔除。

[中] SignalJammer 的 jamChannel 允许 0(下界错写为 -1):会给信道 1/2 泄漏 40%/12% 干扰却在占用列隐身
→ configure/read 统一 (== ALL) ? ALL : clamp(1..5)。
[中] SignalSource.read / SignalRelay.read 不夹取 channel、不校验绑定编码 → clamp 到 [1,5]、编码走 Signal.isValidCode。
[中] SatelliteManager.onWorldLoaded 剪除只判 getByID==null:id 被复用给别的单位时留幽灵记录 → 追加控制器类型判定。
[中] SatelliteManager.restoreRecord 对 orbit/channel/phase/code 零夹取 → 统一夹取(phase 规整到 [0,1) 并过滤 NaN/Inf)。
[中] SatelliteManager.onWorldLoaded 的补建分支缺权威端守卫 → 加 isAuthority(),客机名册完全交给 sat-state。
[中] Silicon 的 sat-launch 处理:空编码没有回退到控制台自身的 selectedSignal → 补回退(cb 就在手里)。
[中] SatelliteConsole UI_REFRESH 8 → 30 tick(原值每次刷新都要跑「遍历全部建筑 × 逐建筑信源扫描」)。
[中] SignalSource.strengthAt 每次调用重算常量 ln(1+R/σ) → 提为 static final LOG_SCALE。

[低] 注释/文档同步:SignalSource 与 SignalJammer 类注释仍是「线性/高斯衰减」、SignalSpectrum 注释写
「15 tick 一轮」(实际 5)、SignalRelay 类注释「约 12.5 格」(按公式解为约 12.8)、mod.hjson 简介
「15 格正态衰减」;README 三处(卫星"沿地图中心圆轨道飞行"→扫描轨迹、"40/60/80/25"→40/60/25 + GEO 整图、
"每格只画最强一路"→√(g²+s²) 合成);docs/blocks/Signal.md「约 14.0 格」→ 约 12.8 格。

[清理] 删除本 PR 带回但无任何代码引用的死贴图 assets/sprites/effects/satellite-buff.png(该状态效果已移除)。

验证:gradlew clean jar(JDK 17 + v160.5 依赖)通过;tests/ 三套件全绿;jar 内无已删内容残留。

* perf(signal): 逐格热路径零分配 + 频谱占用计数一次算完;限流回包与真实失败区分

- 身份串缓存:`Signal.id`("S"+名称,构造时算一次)、`SignalRelayBuild.cacheId()`(绑定时缓存 "S"+编码,
  未绑定用坐标派生 id 并缓存)——H 覆盖每帧数万格、每格都要取身份串做比较,原先每格现拼字符串
  (几十万次临时分配/帧,与 SignalOverlay 里"预计算字符串避免每帧分配"的注释自相矛盾)。
  `SignalChannel.effectiveAll` 与 `SignalOverlay.relayColor` 同步改用缓存。
- `SignalSpectrum` 的占用计数从「信道循环内 5×(全表源 + 全表中继 + 全表干扰器)」改为循环外一次算完
  (新增 srcCnt/jamCnt 缓冲);`signalChannel()` 对每台中继的求值从 5 次降为 1 次。
- `sat-launch` 的限流分支回独立结果码 `busy`(原先与真实失败共用 `fail`,合法双击的第二次会弹
  "发射失败",把"点太快"误导成"配置有问题"),客机侧补对应提示;双语新增
  `block.silicon-satellite-console.busy`。
- 在频谱面板注明两列口径差异(有意):"占用"只数直接打在该信道的干扰器,而"干扰"列含邻信道泄漏
  与全信道干扰器,因此一台只打信道 2 的干扰器会在信道 1 的干扰列显示非 0、占用列显示 0。

验证:gradlew clean jar(JDK 17 + v160.5)通过;tests/ 三套件全绿;jar 内双语含新键。

* fix(satellite): SatelliteLauncher 补存档版本声明 + 档位夹取

与该 PR 里 SatelliteConsole 的 v2 机制对齐:
- 覆写 `version()` 返回 1,并在 `read` 里显式写 `revision <= 1` 分支——历史存档 revision 恒为 0(当初未声明版本),
  字段集与 v1 相同;**今后增删字段必须新增 revision 分支**,否则多读/少读的字节会让后续 tile 的 chunk
  前缀被当数据读(引擎 readChunk 不做按长度对齐,与 SatelliteConsole 名册那条同源)。
- `selectedType` 读入时夹到 [TYPE_SIGNAL, TYPE_TEST],畸形档不会把越界档位带进 UI。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* fix(misc): 补审"顺带修复"文件发现的缺陷(此前三轮评审未覆盖)

- `UpdateChecker`:内容校验失败(截断/空响应;第三方反代典型故障是"200 + HTML 错误页")原先直接判死,
  与"CDN 优先、直连兜底"的声明矛盾 → 改为与网络错误一样继续下一个源(最后一源仍失败才收尾)。
- `UpdateChecker`:`check(force)` 重置 `downloading` → 下载途中点手动检查会让按钮回到初始态、
  再点一次即起第二个并发下载(同一 `.part`/`moveTo`/`delete` 竞争,可能留下截断 jar 且两边都报成功)
  → 不再外部重置,各结束路径自己复位。
- `GeneratorPump`:自举守卫只看 `getLastPowerProduced()`——满负荷(有电池且唯一产者产量被吃满)时它同样为 0,
  会被误判成冷启动、绕过欠压停转从电池倒抽电 → 补 `getLastPowerStored()` 判断。
- `RollGenerator` / `ItemTransferHub`:两处注释与实现不符("随 sync 快照分发"实为随存档读写;
  "客户端挂起项由服务端回包消费"实为本地有意提前消费)→ 改正,并补上"断电不等于停 updateTile"的依据。

复核后判为不成立:「客户端提前删挂起项导致主机有线、成员无线」——客户端本就被禁止 configure,
重试对它只是空转,本地状态由 tileConfig 广播更新,与挂起项无关。

验证:gradlew clean jar(JDK 17 + v160.5)通过;tests/ 三套件全绿。

* fix(review): 处理 robin 在 SiliconMod#50 上的意见(分节注释 / 重复代码 / import 顺序 / 注释措辞)

按 robin 审查的 LOW/SUGGESTION 处理(两条 MEDIUM 已核实为误报,见 PR 评论):

- bundle 分节注释把工作分支名泄漏进了面向用户的资源文件
  (`# ====== satellite-v2 (our branch only) ======`),且该节开头夹了
  power-protector 的两个键 —— 节名改为「卫星与信号」,两个电力键归位到电力方块节,
  并按主题补齐子节(卫星发射控制台 / 发射中枢 / 频谱面板 / 信号检测器 / 信号干扰器 /
  信号中继器 / 信号源 / 快捷键与播报 / 卫星实体)。中英两侧同步。
- Silicon.java:sat-launch 的 5 处「回包 + 记日志」抽成统一出口
  `deny(result, why)`,异常兜底也复用它;pause 的三段重复
  (admin/author、pauseMode==1、pauseMode==2+白名单)收敛为一次 allowed 判定 + 一次动作。
  **语义保持不变**:admin/author 先放行且不受 pauseMode 限制(pauseMode==0 也放行)。
- Blocks.java / SatelliteUnits.java:import 按「包名 + 类名」字典序重排。
- Blocks.java:两处方块注册注释改为说明真实约束 —— Mindustry 存档按**内容名**映射
  (ContentLoader 头表),注册顺序不决定旧存档兼容;顺序真正影响的是各内容的 content id。

未处理(核实后不成立,已在 PR 评论中说明):
- GEO 覆盖口径不一致:bundle 里 GEO 只出现在轨道名(`orbit.geo` / `unit...geo.name`),
  并无覆盖描述,不存在三种说法;
- Liquids 残留:`silicon.world.Liquids` 类不存在,命中的全是引擎的
  `mindustry.content.Liquids`(水 / 冷冻液 / 石油),属正常使用。
- SatelliteManager 拆分归入第二阶段前置重构,不在本 PR 范围。

验证:gradlew clean jar 通过;tests/ 三套件全绿;双语键 332/332 对称。

* chore: 清理死代码,并修正兼容存根注释里写错的理由

删除(真死代码):
- src/silicon/content/item/Items.java —— 空类,`load()` 方法体为空,不注册任何内容;
  连同 Silicon.loadContent() 里的调用与 import 一并移除。

修正注释(方块本身保留):
- DimensionAnchor 的存根注释把理由写错了。原文称「Mindustry 存档按方块 ID 存储,
  删除会使其后所有方块 ID 前移、内容错位」—— 核对引擎源码后确认不成立:
  存档头按**内容名**记录,SaveVersion.readContentHeader 把每个旧 ID **独立按名解析**
  进 temporaryMapper,不存在"ID 前移"。
  真实后果是:名字解析不到时 ContentLoader.getByID 返回 contentMap[0],
  于是旧存档里这个位置的方块会**变成一个原版方块**(SaveFileReader.fallback 只兜底
  原版方块名,不含 mod 方块)—— 比留着存根更糟。
  所以存根是对的,只是理由要改对;Blocks.java 注册处的注释同步修正。

顺带核查(结论:无需改动):
- MessageTest:沙盒调试方块,仍在注册且用途明确,保留;
- bundle 键:静态扫描出的"无引用键"绝大多数由运行时前缀拼接或引擎自动查找
  (category.*.name 由 KeyBind.add 的 category 参数解析、setting.*、
  block.<name>.name 由内容名自动查找),逐条核对后无真正失效项;
- sprites:扫描出的 23 个"未出现"文件名全是多区域方块(dual-purpose-storager-*)
  与动画帧(mine-converter-0..17、switch-on/off),由引擎按后缀自动查找;
- testSso / TYPE_TEST:本分支保留测试卫星(与 fork/main 的第二阶段不同),非失效内容。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* refactor(review): 处理 robin 第二轮意见(4 条整洁性改进)

同轮另有 2 条经核实不成立,已在 PR 评论中说明理由。

- Silicon.java:LAUNCH_REQUEST_COOLDOWN(float 30f)→ LAUNCH_REQUEST_COOLDOWN_TICKS(int 30)。
  单位本就是 tick(与 Time.time 同量纲),注释里也写了「(tick)」,
  但常量名没体现,值 30f 容易被读成 30 秒。
- Silicon.java:读档时序注释由「跨 112-119 行分散描述五步」整理为编号步骤 1-5,
  步骤号就近标在对应调用处(// 步骤 3 / // 步骤 5),消除来回跳读。
- SatelliteUnits.java:load() 里「剥前缀找裸名」的兜底提取为 findUnitRegion(current, name),
  doc 注释补齐「super.load() 已试过带前缀的内容名」这一前因。
- NetSync.java:sendTeamConfig 的 for-each 改为索引循环(arc 的 Seq 用 for-each 会分配迭代器,
  而本方法在信号源/中继器状态变化时一帧可能多次调用)。
  未按建议引入「队伍 → 玩家」缓存:调用点是按需触发的变更路径而非每帧热路径,
  玩家数少,加缓存不值当再维护一层失效逻辑。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* refactor: 删掉恒为真的 netServer 守卫,并更正此前评论里的表述

Silicon.init() 里两处 `if (netServer != null) { ... }` 是无意义的守卫:

- Vars.netServer 是静态字段,在本方法执行前一定已就位 ——
  ClientLauncher:182 与 ServerLauncher:76 都早于各自的 mods.eachClass(Mod::init);
- 全代码库没有任何地方把它置回 null:net.closeServer() 只把 net.server/net.active
  置 false 并踢掉连接,netServer 对象本身常驻。

所以守卫恒为真,等价于无条件注册。去掉后行为不变。

客户端上多注册一份也无害:handler 的触发路径自带服务器判定 ——
NetServer.serverPacketReliable(@Remote(targets = Loc.client))只在服务器收到客户端包时被走到。

同时更正此前 PR 评论里的说法:那里写"守卫有效"并不准确,有效的是 handler 触发路径自带判定,
守卫本身是多余的(我原先只核到 init() 时 netServer 非 null,没有继续核"它会不会被重置")。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

---------

Co-authored-by: xiaobei08 <89635391+xiaobei08@users.noreply.github.com>
ARCloud217 pushed a commit to ARCloud217/Silicon-ARCforked that referenced this pull request Oct 2, 2026
* Reapply "[a0.13.0.0] feat: 卫星实体化与信号语义重构——信号与卫星·第一阶段"

This reverts commit 7050ca2.

* [a0.13.0.0] fix: 卫星自查修复与首星激活中继——useUnitCap=false、未绑定覆盖补绘、每星强度 1.5

* [a0.13.0.0] fix: SSO 选中信号卫星即灰化 + 中枢卫星按钮只留名字

* [a0.13.0.0] balance: 卫星覆盖与强度重定参——轨道越高覆盖越大强度越低(40/60/80/25 格,1.5/1.3/1.1),10 颗近似全图

* [a0.13.0.0] fix: 卫星上空常驻机型名标签 + 删除弃用的卫星在轨 buff(可发射提示改程序化标记)

* [a0.13.0.0] fix: 卫星右下角悬停面板可正常显示名称——hitSize 24 扩大悬停触发窗 + 程序化 uiIcon + 名字标签改悬停显示

* [a0.13.0.0] fix: 卫星机型名去掉已带前缀——MappableContent 会再加一次 silicon- 前缀致 bundle 键落空、悬停面板显示内部键

* [a0.13.0.0] fix: 移除卫星上空的名字标签绘制

* [a0.13.0.0] fix: PR 审查修复——sat-launch 全失败路径回包(fail)含异常兜底+客户端 fail 映射、NetSync 空玩家防御、Lines 笔画复位、launch 注释清理

* [a0.13.0.0] feat: 覆盖半径随图幅短半轴等比缩放(0.32/0.48/0.64/0.20,与轨道圆共用锚点)——任意图幅 10 颗近似全图

* [a0.13.0.0] feat: 卫星信号改对数叠加——最强一星全额+其余 ln(1+Σ),单星语义不变、叠星边际递减;判定与显示共用 stackEff

* [a0.13.0.0] fix: 卫星信号透明度与信号源一致——移除卫星覆盖盘独立绘制层,范围模式逐格合成单一绘制(同一公式+同一强度标度,消除双重叠画)

* [a0.13.0.0] docs: README 同步——H 键覆盖单层合成与透明度一致、发射反馈闭环、移除卫星在轨状态效果

* [a0.13.0.0] ui: 移除卫星发射中枢方块头顶的制造进度条(保留原版状态条与石油不足图标;面板内进度显示不变)

* [a0.13.0.0] ui: 移除中枢方块石油不足图标——石油需求并入右下角面板材料行(图标+区间数量角标+不足红线,与物品显示同栏同风格)

* [a0.13.0.0] ui: 石油需求数量取自绑定控制台的所选轨道——中枢对称扫描绑定控制台(节流30帧),无唯一绑定时回退区间显示

* [a0.13.0.0] ui: 石油需求无唯一绑定控制台时回退显示 LEO 1.0k(不再显示区间)

* [a0.13.0.0] fix: 读档后卫星冻结+无信号——对账时序修复:WorldLoadEvent 早于单位实体读入,原按 getByID 剪除误杀恢复的名册;改为名册不剪除+app.post 延迟对账+控制器节流兜底

* [a0.13.0.0] fix: 中继器配置面板源按钮选中态实时同步 selectedSource——清除绑定/外部变更后不再残留选中黄框

* [a0.13.0.0] fix: 删除信号源后卫星与中继器不再停止传输——上行门控:卫星广播编码X的前提是本队存在存活地面源X,删源即断链(中继器去活/覆盖消失),重放同编码源自动恢复

* [a0.13.0.0] feat: 卫星轨迹改拟真星下点模型——LEO/MEO 东西向回绕扫描+纬度正弦摆动、SSO 南北向极轨、GEO 定点悬停(覆盖=地图对角线,单星全图);波形(1+δ)失谐西漂使轨迹族铺满全图(含四角);单相位字段复用,存档/联机兼容

* [a0.13.0.0] refactor: 移除发射中枢可发射的地图悬浮标记(保留悬停状态栏可发射提示)

* [a0.13.0.0] feat: 重做发射特效——加色垂直光柱(辉光/主体/内核三层收束)+白闪冲击环+种子化上升粒子流(替代逐帧随机抖动)+星体双翼剪影升空渐淡+贴地喷射云

* [a0.13.0.0] feat: 发射特效追加原版 launchPod(发射舱升空+烟迹)

* [a0.13.0.0] fix: ①中继器源按钮组 minCheckCount=0(未绑定时不再残留首个按钮黄框)②信号覆盖改挂 drawOver 并锁定 z(射击加色光不再冲淡覆盖层)③发射特效补原版 launchAccelerator/launch 大环(launchPod 细条纹被光柱淹没)

* [a0.13.0.0] fix: 信号源/干扰器 changeTeam(夺取换队)时失效缓存——否则旧队依据残留 (team,code) 缓存继续广播已不属于它的编码

* [a0.13.0.0] fix: 卫星控制台信号选择/轨道按钮组 minCheckCount=0——未选择信号时不再残留首个按钮黄框(与中继器同源问题)

* [a0.13.0.0] feat: 信号强度改 SINR 比值制——有效强度=信号功率×质量因子(SINR≤1无信号/SINR≥3.5满质量),干扰压信噪比而非扣幅度,强信号天然抗弱干扰;高斯归一化(R处精确归零,消除覆盖边缘悬崖);卫星层同口径(逐星SINR折算后对数叠星,底噪进质量因子);阈值校准:GEO单星0.53/LEO 1.2仍>0.5激活

* [a0.13.0.0] feat: 覆盖绘制卫星×地面改 RSS 功率合成 total=√(g²+s²)——卫星对已有地面覆盖区域成为真实增益(不再取max互斥),着色归属保持贡献较大一方

* [a0.13.0.0] fix: 卫星机型补 physics=false 退出异步物理系统——单位间推挤由 PhysicsProcess 的 layerFlying 物理体实现(与 hittable 无关),此前卫星被编入物理层与飞行单位互相推挤

* [a0.13.0.0] feat: 信号频谱面板——源/中继配置界面内嵌本点5信道占用/干扰I/可用强度条(SINR制对策入口,当前信道行高亮,15tick节流);effectiveAll增干扰分母输出重载;新增docs/blocks/Signal.md收录SINR比值制/RSS合成/阈值体系全模型

* [a0.13.0.0] fix: 补提交频谱面板接线——SignalSource/SignalRelay 配置界面内嵌 buildSection,SignalChannel 增 intOut 干扰分母重载(上一提交因 git add 路径笔误遗漏)

* [a0.13.0.0] revert: 移除万向交叉器新版UI(非卫星与信号线内容)——删除 UniversalJunctionDialog/新版UI文档/设置项/29个无主键,方块回经典UI(键名统一 universal-junction.* 前缀);保留 dex 脱糖修复与 applyConfig 网络加固

* [a0.13.0.0] feat: H键视角走官方KeyBind(默认H可重绑定)+新增信号检测器方块——1x1纯测量设备,配置界面内嵌本点5信道频谱(复用SignalSpectrum,无当前信道高亮),程序化32x32图标,注册在Blocks末尾不占旧存档ID

* [a0.13.0.0] fix: 信号检测器崩溃修复——方块 region 必须是图集 AtlasRegion(createIcons 打包期对 TextureRegion 强转崩溃),改走 mod sprites 管线:新增 assets/sprites/blocks/signal-detector.png(32x32),移除程序化 load() 覆写

* [a0.13.0.0] fix: 频谱面板信道5越界崩溃——CH_COLORS 长5为0基索引,信道号1基,色板索引统一减一(实测:点开源配置即崩)

* [a0.13.0.0] fix: 频谱lambda捕获编译错误——effBuf 索引改用每轮最终副本 ci(上一提交仅推送未编译通过,本提交为正确实现)

* [a0.13.0.0] fix: 检测器点击不弹UI+频谱I标签小数重叠——showConfig 经 configTapped() 放行(默认=配置类表非空),注册布尔占位配置类;I 标签预格式化 Strings.fixed 1位小数(0.0 显示 --),bundle.format 直塞原始 float 会渲染全精度溢出单元格

* [a0.13.0.0] fix: 检测器无实体+频谱表头错位——hasBuilding()=destructible||update 全 false 导致 Tile 不建 Building(悬停/点击全无反应,实测踩坑),置 destructible=true;频谱表头改嵌套 Table 与数据行同构同列宽(平铺列宽不一致会错位重叠)

* [a0.13.0.0] fix: Robin R4 审查修复——KeyBind注册headless守卫(专用服务器安全);sat-state/sat-result包处理器Core.app.post回主线程(网络线程禁UI/状态);sat-launch信号编码[A-Za-z0-9]{4}校验;频谱回调isValid守卫+占用计数改emitting()(与strengthAt前置条件一致)

* [a0.13.0.0] fix: 频谱接入卫星层+布局防重叠重构——绑定信道的卫星按信道聚合(stackEff)并与地面RSS合成进强度条,计入占用;频谱改单嵌套表固定列宽(打开时空标签pack定宽,刷新填文本外层不加宽导致居中文本溢出互渗,实测踩坑),当前行高亮改信道号变色

* fix: 投票工作流触发与配置路径修复

- voting.yml: 触发补齐 pull_request_review/submitted + issue_comment/created,使审批投票实时重算;action 切回 myyk/git-democracy@v2 并 configPath 改为 .github/workflows(安全路径,防 PR 篡改投票规则)
- .voting.yml/.voters.yml 已同步至 .github/workflows/(保留根目录副本兼容旧 fork),确保 pull_request_target 安全读取

* fix: 投票配置路径回退至根目录(.github/workflows/ 不放会被识别)

- 移除 .github/workflows/.voting.yml / .voters.yml(该目录仅放 workflow,会被 GitHub 误识别)
- voting.yml: 保留 pull_request_review+issue_comment 触发补齐,action 保持 Xiaobei09/git-democracy@main + configPath: .(根目录配置,避免 PR 篡改)

* [a0.12.4.3] release: 投票工作流触发补齐与配置路径修复(robin 审查修复+定版)

* [a0.13.0.0] fix: 信号源面板按钮贴左+频谱表头防渗透——频谱段colspan改动态取宿主列数(原固定4只跨5按钮列的前4列,撑宽列致按钮1偏离左缘),信道按钮行左对齐;信道列28→34容信道二字,表头居中,占用/干扰列加宽

* [a0.13.0.0] fix: 信号源按钮行growX铺满(1贴左5贴右)+信道号列内居中+占用/干扰列按实测中文字形加宽(96/60px,杜绝0源0扰微渗透)

* [a0.13.0.0] fix: 频谱占用计数列内居中

* [a0.13.0.0] fix: 频谱表头显式Label+标签内文本setAlignment(center)双保险居中(仅Cell.center()实测未生效)

* [a0.13.0.0] fix: Align 包路径修正(arc.util.Align)——上一提交未编译通过,本提交为可构建版本

* [a0.13.0.0] fix: 频谱数据文字统一Label内部setAlignment居中——截图实测cell.center在该arc版不可靠,干扰I数据左对齐与居中列头错位;信道号/占用/干扰全部改标签内居中(与列头同机制)

* [a0.13.0.0] fix: 频谱去掉格子水平pad(1f)——占用/干扰列间缝隙在深灰面板上透出竖线,改垂直pad保持行距

* [a0.13.0.0] fix: 频谱整段铺不透明grayPanel底色——列边界竖线仍存,疑似底层像素透出,纯色底覆盖根治

* [a0.13.0.0] fix: 去掉干扰数据I前缀——像素级取证证实大写I竖笔被看成占用/干扰列间分隔线(bundle spectrum.i 改为纯数值,列头已有干扰标注)

* [a0.13.0.0] fix: 审查必修三项——环境旗标、相位自累加、恢复 paused 回包

① SatelliteUnits 补环境旗标 envEnabled=Env.any / envDisabled=Env.none:
   UnitType 默认 envDisabled=Env.scorching(UnitType.java:51-53),于是卫星在焦土图(Erekir 全部地图,
   Planets.java:56)与太空图(Planets.java:182)会被引擎环境处死——该路径不看 hittable/targetable/
   killable/useUnitCap(UnitComp.java:751-753 → Units.unitEnvDeath → dead + Call.unitDestroy),
   表现为发射后一帧卫星消失、UnitDestroyEvent 连名册一起删。轨道实体与环境无关,取 any/none
   (原版 assembly-drone 同写法,UnitTypes.java:4617-4618)。

② 相位改为自累加(读档精确续接,不再依赖全局时钟):
   controller 每帧 rec.phase += Time.delta/周期(GEO 定点跳过),scanU/phaseForSave 直接取 phase,
   onWorldLoaded 兜底重建与 launch 初始相位同步改写。原实现用 phase + Time.time/周期,其前提
   「读档 Time.time 归零」不成立——运行时 arc Time.clear() 只清 runs、引擎读档不重置时钟
   (Logic.java:299-302),同一会话内读档会多绕「存档时运行时长/周期」圈,覆盖圆随之偏移。

③ Silicon 恢复 netClient "paused" 回包处理:该回调在同步上游时被误删,导致 Vars.pause.complete
   永远为假、客机每 60 秒重发一次 pause 请求(pauseMode=0 时服务器还不回包)。

④ launch 出生点直接落在轨道点上(原为先落在发射中枢位置、下一帧才入轨)。

验证:gradlew clean jar(JDK17 + Mindustry v160.1 依赖)BUILD SUCCESSFUL。

* [a0.13.0.0] fix: 审查中危两项——名册死记录剪除 + 干扰器口径统一

① 名册死记录剪除(M1):onWorldLoaded 增加 pruneDead 重载,只在「权威端 + 单位已读入」的那次
   对账(Silicon 里 Core.app.post(...true))剪除 Groups.unit.getByID(unitId) == null 的行。
   WorldLoadEvent 那次仍传 false——该事件在存档 map 区域结束时触发,早于 entities 读入,
   此时剪除会误杀刚从控制台恢复的名册(卫星冻结+无信号的老 bug)。客机不剪除:名册本就是
   sat-state 广播镜像(applyState 整表替换),且其实体到达更晚。
   兜底场景:存档 entity id 重复被引擎重新分配(SaveVersion.java:503-514)后旧记录永久残留、
   launchedCount 虚高。

② 干扰器口径统一(M5):把「同信道 + 邻信道泄漏」的累加抽成 SignalChannel.addJammer,
   地面层 effectiveAll 与新增的单信道查询 SignalChannel.jammerAt 共用同一实现;
   SignalJammer.strengthAt 改为委托 jammerAt(原为「取最强一台」)。此前地面层求和、
   卫星层取最大 → 多台干扰器叠不住卫星,正是判定端/显示端口径分叉。
   顺带更新 SignalJammer 类注释(线性制措辞)与 docs/blocks/Signal.md 的卫星层说明。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] fix: 显示端(H 覆盖/频谱)改按「当前查看的编码」聚合——与判定端口径统一

问题:判定端(中继激活 satelliteStrengthAt>0.5、控制台绑定 inSignalRange)本来就是逐编码的,
而显示端把该队**所有编码**的卫星先求和再对数叠加(SignalOverlay.bestSignal 遍历全部记录、
SignalSpectrum 按信道聚合)——同一个点上叠了多个编码的卫星时,格子上的数字会高于任何单一编码
真实可用的强度,玩家按显示布线会得到与预期不符的结果。

改动:
- SignalChannel.effectiveAll 增「按编码视图」(第 7/8 参 codeOut/viewCode):viewCode 非空时
  信号 = 该编码在本信道的最强一路(同码多台视为同一路),干扰 = 其余全部身份功率和 + 邻道泄漏 +
  干扰器;codeOut 返回每信道结果所属编码(自动模式 = 该信道最强身份的编码)。
- SatelliteManager.bestSatelliteAt(...):覆盖显示用的「逐编码分组取最强」——同编码 sum/max 计
  stackEff 后取各组最大,替代跨编码求和;未绑定记录(code==null)作为独立一组参与(出参 null →
  蓝色渐变保留);支持按信道过滤,供频谱"无编码视图"逐信道取最强单编码。判定仍走 satelliteStrengthAt。
- SignalOverlay:每帧解析一次「当前查看的编码」(配置面板打开的建筑 → 鼠标悬停的本队建筑 → 自动),
  地面与卫星都只按该编码聚合;底部提示追加当前范围(· 编码 XXXX / · 自动)。
- SignalSpectrum:标题下新增「聚合范围」行——信号源面板 = 自身编码、中继面板 = 绑定编码、
  检测器 = 全局(逐信道最强单编码);强度列按编码,占用列仍是信道拥挤度(全部编码 + 干扰器)。
- docs/blocks/Signal.md §3/§5 同步;两个 bundle 增 signal.overlay.view.* 与 spectrum.scope.*。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] feat: 地面侧改按 SINR 判定 + 中继器每 tick 实时检测能否转发(面板/悬停显示入站强度与原因)

① 地面侧也是 SINR 判定了(此前只有卫星链路是):
   - 新增 SignalChannel.groundEffAt(team, code, wx, wy):地面层(信号源 + 已激活同编码中继器)在该点的
     有效强度,和覆盖/频谱同一算法(信号 = 该编码最强一路;干扰 = 其余全部身份功率和 + 邻道泄漏 + 干扰器),
     编码无存活地面源时为 0;带"本编码是否有发射机覆盖本点"的廉价前置,远处空闲中继不跑完整批算。
   - inGroundSignalRange 改为 groundEffAt(...) > 0:建筑(卫星控制台)绑定与卫星发射的 1:1 配对
     (hubsInSignal/consolesInSignal)从此都吃 SINR——干扰器覆盖到中枢/控制台所在点即可让配对判定失败。
② 中继器实时检测(去掉原先"每 20 tick + 几何距离"的判定):
   - SignalRelay.updateTile 每 tick 调 updateActive:地面 groundEffAt、卫星 satelliteStrengthAt,
     同一阈值 FORWARD_THRESHOLD=0.5(与覆盖显示/频谱"可用强度"同口径);干扰器开关、源断电/禁用、
     绑定变更、级联上下游变化都在同一 tick 生效。
   - 级联不再需要几何距离特判:已激活的同编码中继器本身是发射机,自然进入下一跳的信号项。
   - 记录实测入站强度 inGroundEff/inSatEff;配置面板与悬停信息新增实时状态行
     "转发中/信号不足/无电/已关闭/未绑定 · 入站 地面 X / 卫星 Y(阈值 0.5)"。
③ 文档与文案:docs/blocks/Signal.md §3/§4(阈值体系 + 实时性 + 有效半径约 12.5 格说明);
   双 bundle 更新中继器描述并新增 status.line/weak/off/noPower。

行为变化:净空单源的实际可转发/可绑定半径由 15 格收窄到约 12.5 格(阈值 0.5);干扰器现在能压断地面链路,
也能让卫星发射的配对判定失败(放置预览圆仍画 15 格原始半径,以覆盖显示/频谱数值为准)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] balance: 卫星叠星改非相干功率合成 √(Σeᵢ²)(替换原对数叠加 max+ln(1+Σ其余))

理由(也是这次换函数的三个判据):
① 与层间合成同一运算:卫星×地面本来就是 √(g² + s²),叠星改用同一个 √(Σ·²) 后整个模型可结合——
   "先叠星再叠地面"与"把卫星和地面源一起丢进 √(Σ·²)"结果完全相同,不再存在两套叠加语义;
② 物理正确:多颗卫星是彼此独立的非相干发射源,功率(幅值平方)相加、幅值按平方根合成;
   线性叠加等于把 N 颗当成一台 N 倍功率发射机(失真),对数叠加则是在强项上再套一层非线性压缩;
③ 尺度自洽:N 颗同强度 = e·√N(数量翻 4 倍强度才翻倍),单星语义完全不变(首星阈值不变),
   抗干扰裕度随 √N 单调增长——LEO 单星有效 1.2 时,可容忍的同信道干扰由 j≈0.32(1 星)
   提升到 ≈0.56(4 星)/≈0.80(10 星),递减但持续有效。

改动:
- SatelliteManager.stackEff(Σeᵢ²) = √(Σeᵢ²)(单参,原 sum/max 双参签名删除);
  satelliteStrengthAt 累加平方和;bestSatelliteAt 的分组缓冲由 {sum,max} 改为 {Σeᵢ²};
- SignalSpectrum 卫星层缓冲 satSum/satMax → satSumSq(非相干功率累加),出值开方;
- docs/blocks/Signal.md §2(公式 + 三个理由 + 新旧数值对照)、§3 锁定设计表述;
  README 特性段与更新日志条目、双 bundle 的卫星控制台描述同步。

数值对照(同 10 颗 LEO,单星有效 1.2):RSS = 3.79;旧对数叠星 = 3.67(大 N 相近、小 N 偏高);
线性叠加 = 12.0。阶梯:1/2/3/4/10 星 = 1.20 / 1.70 / 2.08 / 2.40 / 3.79。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] ui: 术语统一「信号可用度」+ 删掉频谱「(强度 = 该编码可用值)」

- H 覆盖底部提示:正在显示信号强度 → 正在显示信号可用度(EN: Showing signal usability)
- 频谱"可用强度"列头 → "可用度"(EN: Usability)
- 频谱聚合范围行:删掉"(强度 = 该编码可用值)",只留"聚合编码 XXXX"(EN: code XXXX)
- 同口径文案同步:数字透明度设置、H 键切换设置、信号源/中继器/干扰器/检测器描述;
  干扰器描述顺带改成 SINR 措辞(原文还是线性制"减去干扰"的旧说法)
- 中继器统计键 EN 与中文对齐(Relay Signal Usability)

* [a0.13.0.0] balance: 强度标度 15 → 99;距离衰减改对数衰减;卫星强度按同比例 ×6.6 同步

① 标度与衰减形状
   - SignalSource.MAX_STRENGTH 15 → 99;新增 LOG_SIGMA = 3 格,strengthAt 由归一化高斯改为**对数距离衰减**:
     raw(d) = 99 · (1 − ln(1 + d/3) / ln(1 + 15/3)),中心 99、R=15 格处精确归零(无边缘悬崖)。
     形状是"近场快、远场慢"的对数损耗:1 格 83 / 3 格 61 / 5 格 45 / 7.5 格 30 / 10 格 18 / 12 格 10 / 13.5 格 5。
   - 底噪 SignalChannel.NOISE_FLOOR 0.5 → 3.3、转发阈值 SignalRelay.FORWARD_THRESHOLD 0.5 → 3.3
     (都是「标度上限 / 30」,即换标度只让数字更细,**模型相对行为不变**)。
② 卫星强度按同比例(×6.6,保持与地面同一 0~99 标度)
   - LEO/SSO 1.5 → 9.9、MEO 1.3 → 8.58、GEO 1.1 → 7.26;
     净空单星有效值 7.92 / 5.49 / 3.48(阈值 3.3,GEO 依旧刚好过线,与旧标度 0.53 vs 0.5 的相对关系一致);
     叠星 √(Σeᵢ²):LEO 1/2/3/4/10 星 = 7.92 / 11.20 / 13.72 / 15.84 / 25.05。
③ 显示与文档
   - H 覆盖数字表改为按 MAX_STRENGTH 生成(0~99),clamp 上限随之;两位数字的居中偏移按位数补偿。
   - 频谱强度条分母改用 SignalSource.MAX_STRENGTH;Signal(world.meta)默认/夹取也用该常量。
   - docs/blocks/Signal.md §2(新公式 + 距离-强度表 + 新常数)、§4(阈值 3.3、有效半径约 14.0 格)、§5;
     README 特性表与 a0.13.0.0 条目;双 bundle 的信号源/中继器/卫星控制台描述同步。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] ui: H 覆盖数字对齐到方块中心;中继器悬停面板只留原版 bar

① 数字居中:此前采样与绘字都取 gx*8(格子左下角),所以数字整体偏向方块的左下。
   改为取格子中心 gx*8+4;横向按字体度量居中(数字步进 × 位数,1 位/2 位都居中),
   纵向沿用原基准字号调好的 -1.6 偏移(按字号倍率缩放)。范围模式的逐格 Fill.rect 同样
   改到格子中心,格子与世界格网对齐(此前整体偏 -4,-4)。
② 中继器悬停面板(右下角信息栏)删除自定义两行(绑定编号 + 实时转发状态),
   只保留原版 bar(生命/电力);绑定编号与实时状态仍在**配置面板**里显示,功能不变。

* [a0.13.0.0] fix: H 覆盖与频谱改为同一函数(数字必然一致)+ 数字用 Align.center 精确居中

① 一致性(此前两处各写一套,数字会对不上)
   - 新增 SignalChannel.usableAll:单点 5 信道的**最终可用度**(地面 SINR ⊕ 同编码卫星非相干功率合成),
     显示端唯一实现。H 覆盖取五行里的最大值并复用其归属(建筑色/编码色),频谱面板直接显示这五行——
     因此 H 上的数字必然等于频谱里最高的那一行。
   - 顺带修掉一处结构性差异:旧 H 覆盖在自动模式下只把"地面最强信道"那一个编码的卫星算进去,
     当卫星与地面不在同一信道时(很常见:源在信道 1、卫星固化在信道 3)会低报,而检测器逐信道算能看到;
     现在两边逐信道同口径。
   - 频谱刷新 15 tick → 5 tick(约 12 Hz):卫星是移动的,节流太长会让面板数字明显落后于每帧重算的 H 覆盖。
② 数字居中
   - 横向改用 arc 的 Align.center(与绘制同一套布局代码,1 位/2 位数都精确居中),不再自己按字体度量猜偏移;
     采样与绘字都取格子中心(tile 覆盖 [gx*8, gx*8+8),中心 +4)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] chore: 评审低优先项清尾(编码校验统一、发射限流、客机不本地生产、覆盖裁剪、读档夹取)

- **编码校验唯一化**:新增 Signal.isValidCode(4 位大写字母/数字),信号源 configure/read、中继器 configure、
  卫星控制台 configure 全部改用它——此前只有信号源校验,中继器/控制台接受任意字符串,会作为 key 长期
  驻留 SignalChannel.liveSrcCache 与颜色缓存并污染按钮文本(同队客户端可反复下发不同长串撑大缓存)。
- **sat-launch 限流**:每个请求都会做一遍「信号范围 + 1:1 配对」扫描(O(建筑×源)),改造客户端可高频重放;
  按控制台 0.5s 限流(Silicon.LAUNCH_REQUEST_COOLDOWN,控制台记 lastLaunchRequest,不落存档),
  合法双击本来也会因 produced 已清空而失败,限流只挡重放。
- **客机不做本地生产/充电**:SatelliteLauncher.updateTile 在非权威端直接返回——材料/进度/缓冲电一律以
  服务器快照(applySnapshot)为准,避免快照到达前本地扣料与推进进度造成短暂分叉。
- **H 覆盖覆盖包围盒裁剪**:每帧算一次「本队源/已激活中继/在轨卫星覆盖圆」的并集包围盒,盒外格子直接跳过,
  不再每格跑完整 SINR 批算(卫星 GEO 覆盖全图时等价于原行为)。
- **读档夹取**:SignalJammer.read 的 jamChannel 夹到 [-1, 5](越界值会让干扰器永远匹配不到信道而静默失效);
  SatelliteConsole.read 的名册条目数上限 64(损坏存档不再能触发长循环)。
- 删除无人引用的 bundle 键 block.silicon-signal-relay.signal(中英各一行)。

验证:gradlew clean jar(JDK17 + v160.1)BUILD SUCCESSFUL。

* [a0.13.0.0] docs: README 同步信号中继器新行为(SINR 阈值转发 + 状态行)

Robin 评审(README.md:24)指出表格里中继器一行仍写着旧的「进入覆盖即激活 / 强度随距离衰减」,
与 a0.13.0.0 的实现和游戏内描述(bundle_zh_CN.properties:151)不一致。改为:

- 每 tick 实时判定,绑定编号的地面信号可用度(SINR)或同编码卫星强度 > 3.3(0~99 标度)才转发;
- 干扰器、同信道异编码源可把有效强度压到阈值以下从而断链;
- 配置面板状态行实时显示入站地面/卫星强度与未转发原因(信号不足 / 已关闭 / 无电)。

仅文档改动,无代码变更。

* fix(signal): 信号源/中继器缓存自愈——已拆除的建筑不再留在列表里

现象:放 2 个信号源再拆掉 1 个,卫星控制台的信号列表里被拆掉的那个编码仍在。

原因:`SignalSource.allSources` / `SignalRelay.allRelays` 走的是「Groups.build 快照 + dirty 标记」
缓存,而 Mindustry 的拆除顺序是 `preChanged()`(先 `onRemoved()` → markDirty)→ `changeBuild()`
(才 `Building.remove()` 真正离开 Groups.build)。只要在这两步之间有任何一次列表查询
(中继器每 tick 的实时判定、频谱面板、H 覆盖、控制台面板每帧刷新),重建出来的缓存就会把
尚未离开 Groups.build 的死源一起带回来,同时把 dirty 清掉——此后死源永久留在列表里,
直到下一次增删源才纠正。

修复(两道保险,任何一次查询之后缓存都不含死源):
- `SignalSourceBuild` / `SignalRelayBuild` 新增 `removed` 标记:`onRemoved()` 里置位并立即把
  自己从缓存 Seq 中摘掉,`onProximityAdded()` 里复位;`rebuildCache()` 跳过带标记的建筑,
  拆除窗口内发生重建也不会把死源加回来。
- `allSources()` / `allRelays()` 增加自愈剔除:按 `removed || !isValid()`(即"是否仍是本格建筑
  且未被标记死亡")逐项过滤,并在真的剔除了条目时同步失效 `SignalChannel` 的上行门控缓存
  (否则 `hasLiveSource` 仍会按旧结果放行卫星广播)。每队信号源数量级很小,开销可忽略。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): 绑定信号在编码消失时自动清除(控制台 / 中继器)

现象(用户实测):拆掉信号源后,卫星控制台的「当前卫星所属信号」仍挂着那个已删除的编码,
中继器面板同样一直显示「转发信号源:XXXX」+ 信号不足。

语义:信号编码是随机生成的四位串,源被拆掉/被打掉后**无法再建出同名源**,保留绑定只会永久
显示一个不存在的信号。因此改成自动清除:

- `SatelliteConsoleBuild.updateTile()`:`selectedSignal` 在本队已无存活信号源
  (`SignalChannel.hasLiveSource` 为 false)→ 置空并 `configure("")` 同步给各端。
- `SignalRelayBuild.updateTile()`:`selectedSource` 同理清除后照常走 `updateActive()`。
- 判定只看「编码是否还存在」,**不是**「是否在范围内」:源仍在但断电 / 超距 / 被干扰时保留绑定,
  由绑定状态行红字提示(避免把暂时性问题当成源被删)。
- 只在权威端判定(`!Vars.net.active() || SatelliteManager.isAuthority()`),客机跟随 tileConfig
  下发,避免网络延迟导致的误清。
- 文档:`docs/blocks/Signal.md` §4 判定表新增「绑定自动失效」一行。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* chore: PowerProtector 文档跟随上游回退(本 PR 不再改这份文档)

上游 SiliconMod#64 已把 PowerProtector 核心回退到 417 行版本,fork 之前加在这份文档顶部的
「新版架构」警示横幅随之失效;本 PR 主题是卫星/信号,不顺手改它。

* fix(signal): 补上信号干扰器缺失的贴图

`SignalJammer` 注册的方块名是 `signal-jammer`,构造函数里没有任何 `region`/`uiIcon` 覆写,
但仓库里一直没有 `assets/sprites/blocks/signal-jammer.png`(只有 signal-source / signal-relay /
signal-detector 三张)。`Block.region` 默认取 `Core.atlas.find(name)`,找不到就落到 error 贴图,
所以干扰器在建造菜单与地图上都是占位方块。

新增 32×32 的 `signal-jammer.png`:沿用同族配色(主体 (70,74,88) / 深边 (45,48,60) /
亮部 (160,170,190),与 satellite-launcher、signal-detector 一致),图形为「天线 + 信号波 +
红色斜杠」,一眼可辨"干扰"。

* feat(satellite): 轨迹边界内缩 + 回绕淡入淡出;补卫星贴图与轨迹文档

① 轨迹边界(`SatelliteManager.scanInset`):正弦轴的内缩由固定 8px 改为**按该轨道的覆盖半径**
   (`clamp(coverageRadius, 0, 0.8·half)`,下限 `SCAN_MARGIN`)——旧值让卫星贴边时大半个覆盖圆跑到图外,
   名义覆盖率打对折。回绕轴保持全幅(卫星本就该从一侧进场、另一侧离场)。
② 回绕淡入淡出(`WRAP_FADE = 0.06` + `SatelliteManager.presence/presenceOf`):`frac(u)` 落在每圈首尾
   6% 时存在度线性升/降,同时作用于 ① 卫星本体的绘制透明度 ② `satelliteRawAt` 的原始强度——
   覆盖显示、SINR 判定、中继激活随之同步升降,消除回绕瞬间的"瞬移"观感,也不会出现"图没了信号还在"。
③ 贴图:新增 `assets/sprites/units/satellite-{leo,meo,geo,sso}.png`(48×48 = 12 世界单位,本体 + 两侧太阳能板,
   四型按轨道区分轮廓与轨道色)。`SatelliteUnits.load()` 依次尝试「内容名(含 mod 前缀)→ 去前缀名」
   两种图集键;绘制时中心补一个队色点(贴图不含队伍信息);两者都缺时仍走原程序化兜底,不会出 error 白方块。
④ 文档:新增 `docs/blocks/Satellite.md`(轨迹公式 / 淡入淡出 / 覆盖与强度 / 发射与名册 / 视觉 / 常量速查);
   README 卫星章节同步为扫描轨迹模型:周期 90/150/70s、GEO 定点整图(不再是 80 格 / 240s)、
   去掉"圆轨道"与 GEO 0.64 比例,补一条"扫描几何"说明。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): H 覆盖的数字重新对齐地格(去掉锚点残留的 -1.6px 纵向偏移)

**问题**:H 数字模式下数字整体相对地格上移——k=1 时 1.6px(≈0.2 格),字号倍率大时 k=2.5 → 4px(半格),
看起来就是"数字对不齐地面格子",连带着整片覆盖也像被挪了半格。

**原因**:`SignalOverlay.drawNumbersOverlay` 的 `Fonts.def.draw(num, wx, wy - 1.6f * k, Align.center)`——
纵向那个 `-1.6f * k` 是切到 `Align.center` **之前**的锚点残留:横向早已改成 `Align.center`(1/2 位数都精确
居中),纵向却还在手工补偿。而 arc 的 `GlyphLayout` 在 valign 分支本来就按 `FontData.capHeight` 做垂直居中
(`GlyphLayout.setText`),`Align.center` 下文字的视觉中心已经等于传入点,**任何手工偏移都会破坏对齐**。

**改动**:`Fonts.def.draw(num, wx, wy, Align.center)` —— 采样点与绘字点都取格子中心 `gx*8+4`
(tile 索引 gx 覆盖世界坐标 `[gx*8, gx*8+8)`),删除已无用的 `k`,并把"对齐由 arc 的 capHeight 居中负责、
不要再叠加像素偏移"写进注释。范围模式本就用 `Fill.rect(wx, wy, 8f, 8f)` 以格子中心为锚,不受影响。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿。

* fix(signal): H 覆盖数字按字体度量自算锚点(对齐格子中心)+ 新增单格调试探针

**问题(实测确认)**:`Fonts.def.draw(num, wx, wy, Align.center)` 并没有把数字居中——arc 的
`Font.draw(str, x, y, align)` 只把 align 当 halign 用、纵向锚在**基线**上,于是 (x,y) 实际是"文字左下角",
数字整体落在格子中心的**右上角**(实测:"向方块右上角偏移")。上一次删掉 `-1.6f * k` 只是去掉了额外的
手工误差,锚点本身没解决。

**修复**:用 `GlyphLayout` 量宽度、`Font.getCapHeight()` 当高度,把左下角锚点挪到
`(格子中心 − 尺寸/2)` —— 对齐不再依赖 arc 的 Align 语义;`GlyphLayout` 静态复用,无逐格分配。

**新增调试方块 `signal-probe`(信号探针,仅沙盒可见)**:
- `SignalSource.radius` 由写死的常量改为**每方块字段**(默认 15 格),覆盖判定/范围绘制/H 覆盖包围盒
  统一读它;探针取 `0.5` 格 ⇒ 只有它自己那一格的中心在覆盖内、四邻 1 格均为 0。
- 按住 H 后全图只会出现这一格的「99」,方块贴图自带**十字准星**,数字中心与格心是否重合一眼可判。
- 探针覆写父类"必须有电才发射"的闸门(`consPower = null`),沙盒里放下即用(仍需设一个 4 位编码)。
- 双语 bundle 补 name/description;贴图 `assets/sprites/blocks/signal-probe.png`(32×32,调试绿 + 准星)。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(probe): 探针彻底去电力 + 选中/放置画真实覆盖 + 常驻自评读数

**电力(真因)**:父类构造里的 `consumePower(150/60)` 只是把 `ConsumePower` 加进 `consumeBuilder`,
而 `Block.init()` 阶段会从 builder 重建 `consPower` 并置 `hasPower = true`——所以只在构造期写
`consPower = null` 会被覆盖回去:方块信息面板照样显示耗电、没接电时照样画断电图标,看起来就是
"探针需要电"。现在改用 `removeConsumers(c -> true)` 直接清空 builder,`init()` 不再生成任何消费者,
`hasPower/consumesPower` 保持 false;`enableDrawStatus = false` 再兜一层(不画任何状态图标)。
发射判定(`strengthAt`/`emitting`)继续覆写掉父类的 `hasPower()` 闸门,放下即用、不接电网。

**真实覆盖可视化**:`drawSelect` / `drawPlace` 不再画父类的 15 格圆(对 0.5 格半径纯属误导),
改画本格 8×8 边框 + 格心十字——正好当"数字是否落在格心"的参照物。

**常驻自评读数**:方块上方一行 `code=XXXX ch1 r0.5 ON raw99`(编码/信道/半径/开关/本格原始强度),
不依赖 H 覆盖就能判断探针自身状态、定位"为什么不发射";文字宽度用 `GlyphLayout` 量、左锚点居中
(与 H 数字同一套锚点算法),绘制前后保存并恢复 z / 字号 / 字色,不留全局副作用。

**描述**:双语 bundle 更正——编码由放置时自动生成(父类 `placed()` 逻辑),无需手动设置。

* fix(probe): 自诊断读数(cell/off)+ 采样点标记

探针读数扩展为 `code ch r ON raw cell off`:
- `cell` = 用 H 覆盖的同一函数 `SignalChannel.usableAll` 在本格中心算出的可用强度(应等于 raw);
- `off` = 本格中心与最近覆盖层采样点(世界坐标 ≡ 4 (mod 8),即覆盖层逐格采样的位置)的差(单位:格)。
  若非 0,说明采样网格与建筑中心错位——半径 0.5 格的探针会因此整格采不到数字。
另在最近的采样点画一个红圈,便于肉眼比对(贴图中心的绿点/准星就是本格中心)。

* fix(signal): H 覆盖采样点修正半格错位(gx*8+4 → gx*8)——数字终于落在格心

**根因(实测定位)**:调试探针读数给出 `off-0.50,-0.50` —— 覆盖层逐格采样的世界坐标是 `gx*8 + 4`,
而引擎里地格的坐标就是**索引 × 8**(1×1 建筑中心在 8 的整数倍上;官方 `PowerNode.drawPlace` 也是
`x * 8f + offset`,不加 4)。两者整整差半格,后果分两级:

1. 普通信号源:数字(与范围色块)整体相对方块/地格偏移半格——就是「数字向方块右上角偏移、
   显示范围与实际范围不一致」那个老问题;
2. 半径 0.5 格的调试探针:四个采样点到探针中心的距离都是 √2/2 ≈ 0.707 格 > 0.5,**每个采样点都在
   覆盖外** → 整格采不到值,表现为「探针不发射信号」(其实 `raw99`、`cell99` 都正常)。

**改动**:
- `SignalOverlay` 数字模式与范围模式的采样点统一改为 `gx * 8f` / `gy * 8f`(= 建筑所在网格),
  使「采样点 = 建筑中心 = 填色格中心」三者一致;
- `SignalSource` / `SignalRelay` 的放置预览圆按官方写法改为 `x * 8f + offset`;
- 探针诊断的 `off` 基准同步改到新网格(预期恒为 0.00,0.00),红圈应正好套在方块中心绿点上。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿(12/12、31/31、优先级路由)。

* fix(signal): H 覆盖数字的垂直锚点修正(锚点是文字上边缘,不是基线)

探针截图逐像素量:红圈(采样点)中心 (899.5, 279),数字 `99` 的 bbox 上边缘 300、中心 319 ——
数字整个沉在格心**下方**约 3.8 世界单位(≈0.47 格),而它的上边缘恰好落在我传入的锚点上。

结论:`Font.draw(str, x, y, Align.left)` 的锚点是文字的**上边缘**(GlyphLayout/FontCache 内部再按
ascent 折算基线)。上一版按"基线"理解、把 y 减了半个字高,等于把整行数字又往下压了半个字。

改动:锚点改为 `(采样点.x − 宽/2, 采样点.y + capHeight/2)` —— 上边缘抬到格心上方半个字高,
数字的**视觉中心**才落在格心上;水平方向本来就是左边缘 + 宽/2,实测已居中(偏差 <0.5px/10),不动。

验证:`gradlew clean jar` 通过。

* chore(signal): 删除调试方块「信号探针」及其只为它引入的半径管线

对齐问题(采样点半格错位 + 数字垂直锚点)已定位并修好,探针完成使命,整体移除:

- 删除 `SignalProbe.java`、`assets/sprites/blocks/signal-probe.png`,`Blocks.java` 去掉注册/字段/import,
  双语 bundle 删除 `block.silicon-signal-probe.name/description`;
- 还原 `SignalSource` 的「每方块半径」改造(`radius` 字段、`radius()` 访问器、带半径的 `strengthAt` 重载)
  ——它当初只为 0.5 格的探针而加,现在恢复常量 `RADIUS`;
- `SignalOverlay.computeCoverBounds` 同步回到统一半径。

**保留**(与探针无关的真修复):H 覆盖采样点由 `gx*8+4` 改为 `gx*8`(建筑网格,修正半格错位)、
数字垂直锚点改为「上边缘 + 半字高」、`SignalSource`/`SignalRelay` 放置预览圆改为官方写法 `x * 8f + offset`。

注:探针是最后注册的方块,删除它不影响其余方块 ID;已有存档里的探针按名字映射失效,会变成空格子(不崩)。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿;jar 内无 SignalProbe/signal-probe 残留。

* style: 行尾归一化为 LF(9 个文件此前被 API 推送写成了 CRLF)

这 9 个文件是用 GitHub API 直接推的、上传的是本地工作区内容,而本仓库工作区 `core.autocrlf=true`
会把检出内容变成 CRLF —— 于是远端存成了 CRLF,而上游各文件都是 LF。后果是 PR 里这些文件显示为
**整文件重写**(例如 SignalSource.java 的 diff 变成 +321/-293),评审时噪音极大。

本提交只做一件事:把这两个分支上这 9 个文件的 CRLF 全部转回 LF,**不改任何代码/文字内容**
(README 在两分支本就不同,各自按自己的内容转换)。以后所有 API 推送都会先做 LF 归一化。

* refactor: 移除炼油厂与润滑油(与 fork/main 对齐)

上游 SiliconMod#64 把石油炼化厂以「注册注释掉」的形式放进了仓库,本分支此前整套保留着,而 fork/main 已删除。
本次让两边一致,删除:

- `src/silicon/world/blocks/production/PetroleumRefinery.java`
- `src/silicon/world/Liquids.java`
- `assets/sprites/blocks/petroleum-refinery.png`、`assets/sprites/liquids/lubricant-oil.png`

以及全部引用:`Blocks.java` 的 import/字段/注释注册、`Silicon.java` 的 `silicon.world.Liquids.load()`、
双语 bundle 的 `block.silicon-petroleum-refinery.name/description`。

说明:这等于把上游 SiliconMod#64 里这套**未启用**的内容从本 PR 移除(上游若日后正式启用,同步时会再引入);
fork 的发布/自动化基础设施(`.github/workflows/fork-publish.yml`、`scripts/*`、`docs/WORKFLOW.md`)
只存在于 fork/main,**不进本 PR**。

验证:`gradlew clean jar` 通过;`tests/` 三套件全绿;jar 内无 PetroleumRefinery / world/Liquids / lubricant-oil 条目。

* chore: 适配 Mindustry v160.5(依赖 v160.1 → v160.5,零源码改动)

- `build.gradle`:`mindustryVersion` 由 `v160.1` 提升至 `v160.5`(Steam 客户端现为 `steam build 160.5`);
- README:顶部兼容版本(`v160.5+`)、构建依赖说明同步为 v160.5,更新日志补一条适配记录;
- 结论:同一份源码下 v160.5 与 v160.1 依赖的**构建产物逐字节相同**——160.2~160.5 未触及本模组用到的 API
  (发布内容均为缺陷修复与 UI 调整:target dummy、逻辑处理器、数据补丁、连锁闪电等),无破坏性变更;
- `minGameVersion` 维持 `155.4`,向后兼容不变;
- 验证:本地 `gradlew clean jar`(JDK 17 + v160.5 依赖)通过;`tests/` 三套件全绿;该适配此前在
  `chore/adapt-v160.5` 分支上的 CI(Build Mod @ a028eb9)亦为 success。

本 PR 只取这一个适配提交的改动(改动 2 个文件);fork 侧的发布/自动化基础设施仍然只存在于 fork/main,不进本 PR。

* docs: PowerProtector.md 同步 main 版(保留「本文档已过期」警告)

该文件此前在 PR 分支被「交还上游」(不带警告),而 fork/main 上有 8 行过期警告 —— 两边不一致。
现按「PR 与 main 内容保持一致」的约定统一:以 main 版为准(警告内容是事实——文档描述的是 v14 时代
架构、与 v19 重构后的实现严重脱节,upstream 侧同样成立),两分支此文件完全相同。

* fix(signal/satellite): 修审核发现的口径与生命周期缺陷(含一处存档错位)

自查 + 独立复核发现的问题,按严重度修复。

[阻塞] SatelliteConsole 名册读写不对称 → 存档错位
写侧 write.i(list.size) 无上限、读侧 Math.min(..., 64) 截断:名册 >64 条时读侧少消费
(n-64)×(16+码长) 字节。反汇编确认引擎 SaveFileReader.readChunk 是「读长度 → 交给 runner →
直接 return len」,**不做**任何按声明长度的对齐,因此后续每个 tile 的 chunk 前缀都会被当数据读,
整档解析错位。现读写共用 ROSTER_MAX=64,且读侧把写入的 n 条**全部消费**(超限只解析不恢复),
流位置与写侧严格一致,已有 >64 条的存档也能正确读出。

[高] SatelliteUnits.drawFallback 的 tc.a(alpha) 直接改写共享 Team.color 的 alpha(上一行有 cpy、这行漏了)
→ 卫星回绕淡入段把该队全局队色改淡且不恢复。改 tc.cpy().a(alpha)。
[高] SignalChannel.usableAll 的「按编码」分支缺卫星上行门控(自动分支/判定端/频谱占用计数都有)
→ 地面源被拆光后 H 覆盖与频谱仍显示该编码卫星强度,与判定端打架。补 hasLiveSource 门控。
[高] SignalRelay.updateActive 判定用 max(地面,卫星),显示端用非相干合成 √(g²+s²),两者恒不相等
→ 阈值 3.3 附近出现"H 显示 3.4 却不转发"。统一到显示端的合成值(顺带让面板「卫星」列在地面有信号时
不再恒为 0)。
[高] SignalRelayBuild 缺 changeTeam → 换队/夺取后中继缓存不失效(旧队仍当发射机、新队漏掉它)
→ 补 changeTeam + markDirty(与 SignalSource/SignalJammer 同款)。
[高] SignalJammer 全局缓存无 removed 过滤、无自愈剔除 → 借「onRemoved 早于离开 Groups.build」的窗口,
死干扰器会被固化进缓存并持续压制该信道。加 removed 标记 + 重建过滤 + allJammers 自愈剔除。

[中] SignalJammer 的 jamChannel 允许 0(下界错写为 -1):会给信道 1/2 泄漏 40%/12% 干扰却在占用列隐身
→ configure/read 统一 (== ALL) ? ALL : clamp(1..5)。
[中] SignalSource.read / SignalRelay.read 不夹取 channel、不校验绑定编码 → clamp 到 [1,5]、编码走 Signal.isValidCode。
[中] SatelliteManager.onWorldLoaded 剪除只判 getByID==null:id 被复用给别的单位时留幽灵记录 → 追加控制器类型判定。
[中] SatelliteManager.restoreRecord 对 orbit/channel/phase/code 零夹取 → 统一夹取(phase 规整到 [0,1) 并过滤 NaN/Inf)。
[中] SatelliteManager.onWorldLoaded 的补建分支缺权威端守卫 → 加 isAuthority(),客机名册完全交给 sat-state。
[中] Silicon 的 sat-launch 处理:空编码没有回退到控制台自身的 selectedSignal → 补回退(cb 就在手里)。
[中] SatelliteConsole UI_REFRESH 8 → 30 tick(原值每次刷新都要跑「遍历全部建筑 × 逐建筑信源扫描」)。
[中] SignalSource.strengthAt 每次调用重算常量 ln(1+R/σ) → 提为 static final LOG_SCALE。

[低] 注释/文档同步:SignalSource 与 SignalJammer 类注释仍是「线性/高斯衰减」、SignalSpectrum 注释写
「15 tick 一轮」(实际 5)、SignalRelay 类注释「约 12.5 格」(按公式解为约 12.8)、mod.hjson 简介
「15 格正态衰减」;README 三处(卫星"沿地图中心圆轨道飞行"→扫描轨迹、"40/60/80/25"→40/60/25 + GEO 整图、
"每格只画最强一路"→√(g²+s²) 合成);docs/blocks/Signal.md「约 14.0 格」→ 约 12.8 格。

[清理] 删除本 PR 带回但无任何代码引用的死贴图 assets/sprites/effects/satellite-buff.png(该状态效果已移除)。

验证:gradlew clean jar(JDK 17 + v160.5 依赖)通过;tests/ 三套件全绿;jar 内无已删内容残留。

* perf(signal): 逐格热路径零分配 + 频谱占用计数一次算完;限流回包与真实失败区分

- 身份串缓存:`Signal.id`("S"+名称,构造时算一次)、`SignalRelayBuild.cacheId()`(绑定时缓存 "S"+编码,
  未绑定用坐标派生 id 并缓存)——H 覆盖每帧数万格、每格都要取身份串做比较,原先每格现拼字符串
  (几十万次临时分配/帧,与 SignalOverlay 里"预计算字符串避免每帧分配"的注释自相矛盾)。
  `SignalChannel.effectiveAll` 与 `SignalOverlay.relayColor` 同步改用缓存。
- `SignalSpectrum` 的占用计数从「信道循环内 5×(全表源 + 全表中继 + 全表干扰器)」改为循环外一次算完
  (新增 srcCnt/jamCnt 缓冲);`signalChannel()` 对每台中继的求值从 5 次降为 1 次。
- `sat-launch` 的限流分支回独立结果码 `busy`(原先与真实失败共用 `fail`,合法双击的第二次会弹
  "发射失败",把"点太快"误导成"配置有问题"),客机侧补对应提示;双语新增
  `block.silicon-satellite-console.busy`。
- 在频谱面板注明两列口径差异(有意):"占用"只数直接打在该信道的干扰器,而"干扰"列含邻信道泄漏
  与全信道干扰器,因此一台只打信道 2 的干扰器会在信道 1 的干扰列显示非 0、占用列显示 0。

验证:gradlew clean jar(JDK 17 + v160.5)通过;tests/ 三套件全绿;jar 内双语含新键。

* fix(satellite): SatelliteLauncher 补存档版本声明 + 档位夹取

与该 PR 里 SatelliteConsole 的 v2 机制对齐:
- 覆写 `version()` 返回 1,并在 `read` 里显式写 `revision <= 1` 分支——历史存档 revision 恒为 0(当初未声明版本),
  字段集与 v1 相同;**今后增删字段必须新增 revision 分支**,否则多读/少读的字节会让后续 tile 的 chunk
  前缀被当数据读(引擎 readChunk 不做按长度对齐,与 SatelliteConsole 名册那条同源)。
- `selectedType` 读入时夹到 [TYPE_SIGNAL, TYPE_TEST],畸形档不会把越界档位带进 UI。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* fix(misc): 补审"顺带修复"文件发现的缺陷(此前三轮评审未覆盖)

- `UpdateChecker`:内容校验失败(截断/空响应;第三方反代典型故障是"200 + HTML 错误页")原先直接判死,
  与"CDN 优先、直连兜底"的声明矛盾 → 改为与网络错误一样继续下一个源(最后一源仍失败才收尾)。
- `UpdateChecker`:`check(force)` 重置 `downloading` → 下载途中点手动检查会让按钮回到初始态、
  再点一次即起第二个并发下载(同一 `.part`/`moveTo`/`delete` 竞争,可能留下截断 jar 且两边都报成功)
  → 不再外部重置,各结束路径自己复位。
- `GeneratorPump`:自举守卫只看 `getLastPowerProduced()`——满负荷(有电池且唯一产者产量被吃满)时它同样为 0,
  会被误判成冷启动、绕过欠压停转从电池倒抽电 → 补 `getLastPowerStored()` 判断。
- `RollGenerator` / `ItemTransferHub`:两处注释与实现不符("随 sync 快照分发"实为随存档读写;
  "客户端挂起项由服务端回包消费"实为本地有意提前消费)→ 改正,并补上"断电不等于停 updateTile"的依据。

复核后判为不成立:「客户端提前删挂起项导致主机有线、成员无线」——客户端本就被禁止 configure,
重试对它只是空转,本地状态由 tileConfig 广播更新,与挂起项无关。

验证:gradlew clean jar(JDK 17 + v160.5)通过;tests/ 三套件全绿。

* fix(review): 处理 robin 在 SiliconMod#50 上的意见(分节注释 / 重复代码 / import 顺序 / 注释措辞)

按 robin 审查的 LOW/SUGGESTION 处理(两条 MEDIUM 已核实为误报,见 PR 评论):

- bundle 分节注释把工作分支名泄漏进了面向用户的资源文件
  (`# ====== satellite-v2 (our branch only) ======`),且该节开头夹了
  power-protector 的两个键 —— 节名改为「卫星与信号」,两个电力键归位到电力方块节,
  并按主题补齐子节(卫星发射控制台 / 发射中枢 / 频谱面板 / 信号检测器 / 信号干扰器 /
  信号中继器 / 信号源 / 快捷键与播报 / 卫星实体)。中英两侧同步。
- Silicon.java:sat-launch 的 5 处「回包 + 记日志」抽成统一出口
  `deny(result, why)`,异常兜底也复用它;pause 的三段重复
  (admin/author、pauseMode==1、pauseMode==2+白名单)收敛为一次 allowed 判定 + 一次动作。
  **语义保持不变**:admin/author 先放行且不受 pauseMode 限制(pauseMode==0 也放行)。
- Blocks.java / SatelliteUnits.java:import 按「包名 + 类名」字典序重排。
- Blocks.java:两处方块注册注释改为说明真实约束 —— Mindustry 存档按**内容名**映射
  (ContentLoader 头表),注册顺序不决定旧存档兼容;顺序真正影响的是各内容的 content id。

未处理(核实后不成立,已在 PR 评论中说明):
- GEO 覆盖口径不一致:bundle 里 GEO 只出现在轨道名(`orbit.geo` / `unit...geo.name`),
  并无覆盖描述,不存在三种说法;
- Liquids 残留:`silicon.world.Liquids` 类不存在,命中的全是引擎的
  `mindustry.content.Liquids`(水 / 冷冻液 / 石油),属正常使用。
- SatelliteManager 拆分归入第二阶段前置重构,不在本 PR 范围。

验证:gradlew clean jar 通过;tests/ 三套件全绿;双语键 332/332 对称。

* chore: 清理死代码,并修正兼容存根注释里写错的理由

删除(真死代码):
- src/silicon/content/item/Items.java —— 空类,`load()` 方法体为空,不注册任何内容;
  连同 Silicon.loadContent() 里的调用与 import 一并移除。

修正注释(方块本身保留):
- DimensionAnchor 的存根注释把理由写错了。原文称「Mindustry 存档按方块 ID 存储,
  删除会使其后所有方块 ID 前移、内容错位」—— 核对引擎源码后确认不成立:
  存档头按**内容名**记录,SaveVersion.readContentHeader 把每个旧 ID **独立按名解析**
  进 temporaryMapper,不存在"ID 前移"。
  真实后果是:名字解析不到时 ContentLoader.getByID 返回 contentMap[0],
  于是旧存档里这个位置的方块会**变成一个原版方块**(SaveFileReader.fallback 只兜底
  原版方块名,不含 mod 方块)—— 比留着存根更糟。
  所以存根是对的,只是理由要改对;Blocks.java 注册处的注释同步修正。

顺带核查(结论:无需改动):
- MessageTest:沙盒调试方块,仍在注册且用途明确,保留;
- bundle 键:静态扫描出的"无引用键"绝大多数由运行时前缀拼接或引擎自动查找
  (category.*.name 由 KeyBind.add 的 category 参数解析、setting.*、
  block.<name>.name 由内容名自动查找),逐条核对后无真正失效项;
- sprites:扫描出的 23 个"未出现"文件名全是多区域方块(dual-purpose-storager-*)
  与动画帧(mine-converter-0..17、switch-on/off),由引擎按后缀自动查找;
- testSso / TYPE_TEST:本分支保留测试卫星(与 fork/main 的第二阶段不同),非失效内容。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* refactor(review): 处理 robin 第二轮意见(4 条整洁性改进)

同轮另有 2 条经核实不成立,已在 PR 评论中说明理由。

- Silicon.java:LAUNCH_REQUEST_COOLDOWN(float 30f)→ LAUNCH_REQUEST_COOLDOWN_TICKS(int 30)。
  单位本就是 tick(与 Time.time 同量纲),注释里也写了「(tick)」,
  但常量名没体现,值 30f 容易被读成 30 秒。
- Silicon.java:读档时序注释由「跨 112-119 行分散描述五步」整理为编号步骤 1-5,
  步骤号就近标在对应调用处(// 步骤 3 / // 步骤 5),消除来回跳读。
- SatelliteUnits.java:load() 里「剥前缀找裸名」的兜底提取为 findUnitRegion(current, name),
  doc 注释补齐「super.load() 已试过带前缀的内容名」这一前因。
- NetSync.java:sendTeamConfig 的 for-each 改为索引循环(arc 的 Seq 用 for-each 会分配迭代器,
  而本方法在信号源/中继器状态变化时一帧可能多次调用)。
  未按建议引入「队伍 → 玩家」缓存:调用点是按需触发的变更路径而非每帧热路径,
  玩家数少,加缓存不值当再维护一层失效逻辑。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

* refactor: 删掉恒为真的 netServer 守卫,并更正此前评论里的表述

Silicon.init() 里两处 `if (netServer != null) { ... }` 是无意义的守卫:

- Vars.netServer 是静态字段,在本方法执行前一定已就位 ——
  ClientLauncher:182 与 ServerLauncher:76 都早于各自的 mods.eachClass(Mod::init);
- 全代码库没有任何地方把它置回 null:net.closeServer() 只把 net.server/net.active
  置 false 并踢掉连接,netServer 对象本身常驻。

所以守卫恒为真,等价于无条件注册。去掉后行为不变。

客户端上多注册一份也无害:handler 的触发路径自带服务器判定 ——
NetServer.serverPacketReliable(@Remote(targets = Loc.client))只在服务器收到客户端包时被走到。

同时更正此前 PR 评论里的说法:那里写"守卫有效"并不准确,有效的是 handler 触发路径自带判定,
守卫本身是多余的(我原先只核到 init() 时 netServer 非 null,没有继续核"它会不会被重置")。

验证:gradlew clean jar 通过;tests/ 三套件全绿。

---------

Co-authored-by: xiaobei08 <89635391+xiaobei08@users.noreply.github.com>
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.

5 participants