Repository navigation
Releases: DionysusDWI/SCAPI-Branch_Skyline
Release list
SCAPI Skyline Project v0.1.156
SCAPI Skyline Project v0.1.156
[v0.1.156] - 2026-09-29
第一百六十版:稳定性根因 + 用户点名的一条观感要求 + 一条并行路线的定论。相对 v0.1.155 的变更:
1. 缺陷(长期存在,notes/263 先发现现象、本轮定位到根因)
区块表 Terrain.ChunksStorage 是线性探测的开地址表(65536 槽),而 Remove 直接把槽位置空:
- 置空会在探测簇中间留一个洞 ⇒ 洞里原本经过的 key 从此
Get返回null; - 引擎以为"这个坐标没加载" ⇒
AllocateChunk的防重检查失效 ⇒ 同一坐标被分配两次; FreeChunk在位序上先碰到哪个删哪个 ⇒ 表里留下不在册 / 已Dispose的条目,
每个还带着 128 个 slice 几何与 VB/IB ⇒ 只增不减(用户内存红线那条风险的真根因之一)。
实测现象:notes/263(重负载 1.5 h 后 arrayNonEmpty 453 > allocated 452)、
notes/153(堆里 1,092 个 TerrainChunk,在册只有约 202)。
2. 修法:回移删除(Knuth TAOCP 3 §6.4 Algorithm R)
删掉槽 i 后把同一探测簇里靠后的元素往前挪,洞顺着簇推到簇尾 ⇒ 删除后簇内不留洞,
其余 key 的探测链完好。移动判据(i <= j 分支专门处理"探测跨表尾回卷"的簇):
bool move = i <= j ? (home <= i || home > j) : (home <= i && home > j);开关 skyline.ChunkTableBackwardShiftRemove(默认 true,false = 旧语义,仅供对照/回滚)。
表的装载率极低(455 / 65536)⇒ 簇长个位数,代价可忽略(实测 removeMs 0.003 ms,与旧语义同)。
3. 两条确定性验收(不靠"跑很久看症状")
| 判据 | 内容 | 实测 |
|---|---|---|
skyline.ChunkTableRemoveProbe() |
三条方向性用例:3 个同 home 槽条目删第一个、64 条目单簇删中间、回卷簇(home=65535)删第一个 | 旧语义丢 2 / 32 / 2 条,新语义丢 0 / 0 / 0 |
skyline.ChunkTableRandomizedSelfCheck(seed, ops) |
随机化对拍(默认 2 万次插删、键域含跨表尾回卷与小域强碰撞);口径 = 阴影 Dictionary 语义对照 + 可达性 + 非空槽数 == 阴影数 + 无重复坐标 |
新语义 0 mismatch / 0 不可达;把开关切回旧语义 ⇒ 389,377 mismatch / 191,022 条不可达(证明该判据不是空洞的) |
另加 chainBreakingRemoves("簇中删除"计数)把缺陷生成变成可数:强碰撞键域下 968.75 次/千次删除;
真实负载下这个比例低得多(notes/285 §4 的 3,100 次删除里 shiftMoves=0 ⇒ 删除多落在簇尾)。
4. 用户口径 4.4:体积云三条(notes/286)
- 三层带:云海 300–600 m、卷云 850–1000 m、高空薄云 1300–1700 m(原来只有两层);
- 越高越慢:风默认 2.6 / 1.6 / 0.8 m/s(原来 6 / 12 —— 既偏快、又是"越高越快");
- 范围到近地平线:
CloudMaxDistance6000 → 24000 m(仰角 2.2° → 0.55°),并新增
空气透视混融CloudFadeDistance(2200 / 3000 / 3600 m)——
8 步固定步长下掠射视线的od会饱和,把地平线糊成一片浅灰白(用户报的"白色化遮罩"),
混融把远处云色混向天空底色即可修掉:发白像素 30.34% → 0.0%、彩度 44.78 → 56.41。 - 云影侧另加
LodCloudShadowTintEnabled(默认关):打开即高饱和平色(黄绿),便于看清云影落点。 - 代价实测:关垂直同步 + on/off 配对交替 ⇒ 102.15 vs 102.00 fps(−0.15%),三层云几乎不花钱。
5. lockstep 批调度:三档实测,判据③未达 ⇒ 默认保持关(notes/284)
ParallelLockstepScheduling(0 旧贪心 / 1 连通分量 / 2 按 worker 装箱)与 ParallelLockstepComponentMax(默认 4):
| 档 | 壁钟(串行 → 并行 2) | 回退率 | 批数 |
|---|---|---|---|
| 旧 I2 | 3.37 → 3.15(+6.5%) | 86.2% | 583 |
| I2' 连通分量(审计批准版) | 3.03 → 2.86(+5.6%) | 97.9% | 71 |
| 按 worker 装箱 | 2.81 → 2.79(+0.7%;复跑 −32%) | 99.0% | 32 |
判据①②(六不变量含注入、交叉轮次哈希逐位相同)全 PASS;③ 中位改善 ≥15% 三档都没达到
⇒ 按审计规则 ParallelLockstepWorkers 默认保持 0(关)。本轮还抓出并修掉两个真 bug
(bbox 判据误用于装箱档 ⇒ 468 批全弃;装箱跨列冲突漏查 ⇒ 260 批被断言拦下)。
6. 门禁与文档
门禁 26 PASS / 0 FAIL / 0 SKIP(新增 chunk-table-remove-chain、chunk-table-randomized,
并把"三层云 / 越高越慢 / 24 km / 混融距离 / 三层风"写进默认值漂移门);
构建 0 警告 0 错误;README 精简到 9,968 字符(上限 10000)并把本版三条写进能力清单。
审计:CC 121304Z / 125037Z / 130316Z 三轮裁定,发现全部闭环(未闭环 0 条)。
Relative to v0.1.155: chunk table removal no longer cuts other keys off (backward shift, with a randomised differential gate that shows one hundred ninety thousand unreachable entries under the old semantics); three cloud bands with wind that slows as it rises, raymarch range 6 to 24 km and an aerial blend that removes the pale horizon mask (washed out pixels 30.3 to 0.0 percent); lockstep batch scheduling available in three variants but off by default because none reached the fifteen percent wall clock bar.
SCAPI Skyline Project v0.1.155
SCAPI Skyline Project v0.1.155
[v0.1.155] - 2026-09-29
第一百五十九版:修掉一条"会静默给错答案"的探针口径 —— NearestShellCube(s) 的入参单位与距离单位。
相对 v0.1.154 的变更:
1. 缺陷(v0.1.152 引入)
SkylineCubeShellStore.NearestShellCube(s) 的形参名与文档写的是"离给定世界坐标最近",
而实现是拿 立方体索引直接相减(cx - worldX),并把 sqrt(Δcube²) 当 米 回报。两个后果:
| 调用方按什么写 | 拿到的答案 |
|---|---|
| 按文档传世界坐标 | 静默 ok:true 的错答案:实测传 (51690,358,34149) 得 distanceMetres=58013.1,而真实最近壳立方体在 16 m(1 个立方体)外 |
按实现传立方体索引(门禁 player_cube() 就是这么做的) |
最近立方体是对的,但 distanceMetres 少 16 倍(1 立方体 = 16 m,报出来是 16 —— 数字碰巧像米,更难发现) |
2. 修法(零语义变更)
- 形参改名为
cubeX/cubeY/cubeZ,文档明说"入参 = 立方体索引"; - 输出新增
inputUnits: "cubeIndices"(探针自己声明口径,调用方不必猜); - 单位修对:新增
distanceCubes(立方体数),distanceMetres= 立方体数 ×cubeSize(新旧字段都在,旧调用方读到的数字从此是真米)。
门禁侧行为不变:regression-skyline.py 传的是 player_cube()(>> cubeShift),本来就是立方体索引。
3. 验收 heightlab/skyline-v0157-shell-anchor-units.py —— 8/8 PASS
| 判据 | 实测 |
|---|---|
inputUnits == "cubeIndices" |
✅ |
| 报告距离与"用返回立方体 + 输入立方体重算"一致 | distanceCubes=16 ↔ 重算 16.0 |
单位:distanceMetres == distanceCubes × cubeSize |
256.5 m == 16 × 16 |
| 修复的"牙齿":米 ≠ 立方体数(旧实现必红) | 256.5 vs 16 |
| 8 个候选逐项自洽/单位 | 全部一致 |
| 负对照①:推远 64 格后仍与重算一致 | 报告 47.4 / 重算 47.4 |
| 负对照②:入参真的参与计算(距离随输入变) | 16 → 47.4 |
| 单数/复数一致 | 两项返回同一立方体 |
4. 门禁
24 PASS / 0 FAIL / 0 SKIP(shell-mesh-tiers 的锚点说明从错的 16 m 变成正确的 256.5 m)。
如实边界
- 负对照第一版写错了(要求"推远 64 格后距离必须 ≥64")——壳仓本身有 831 个驻留立方体,
查询点平移后最近壳完全可能仍在 40~50 格外 ⇒ 误报 FAIL。已改成"重算一致 + 距离随输入变"两条。
这条与notes/266的"空测量"族是同一类:判据要写能成立的形式。 - 本版只动探针的报告口径,不改变任何"最近立方体"的选择结果;默认值一律未动。
相对 v0.1.154:修 NearestShellCube(s) 的单位/口径(形参文档说世界坐标、实现收立方体索引、distanceMetres 实际是立方体数)——零语义变更,新增 inputUnits/distanceCubes;验收 8/8 PASS;门禁 24 PASS / 0 FAIL / 0 SKIP
SCAPI Skyline Project v0.1.154
SCAPI Skyline Project v0.1.154
[v0.1.154] - 2026-09-29
第一百五十八版:收尾那条 crit5 关闭 —— 关键补充是"本轮溯源"。
相对 v0.1.153 的变更:
1. 新探针 skyline.ResetDiskProvenance()
TerrainSerializer23.ResetDiskProvenance() 把"从盘读回"的溯源集合清零;
之后 skyline.ChunkLoadedFromDisk(cx,cz) 只反映清零之后这一轮。
为什么需要:v0.1.151 的溯源是累积语义("曾经读过盘"),
它回答不了"这一轮这个区块到底是从盘读的、还是被重新生成的"——
而"注入后邻块不受影响"这条判据恰恰要这个区分(若邻块本轮是被重新生成的,它高度图不同属正常)。
2. 终轮:crit5 5/5 PASS
| 判据 | 实测 |
|---|---|
injection-fired |
nonIoFailures +1 |
neighbors-identical-same-round(地形形状指纹 Top+Bottom) |
4/4 逐位相同 |
neighbors-were-from-disk(本轮溯源) |
4/4 True |
victim-regenerated |
受害块 nonAir 279,444 vs 邻居 279,680(比值 0.999) |
changed-neighbors-recover-after-relight |
形状全同 ⇒ 判据自动成立 |
仪器:含 SunlightHeight 的完整高度图仍有 1 个邻块不同——
与"SunlightHeight 由当时的日照值推出、随时间/天气变"一致,属环境变化,不作判据。
如实边界
- 本版只加清零探针;判据的两次修正(形状指纹、本轮溯源)都在验收脚本里。
- 前两次"红"分别来自判据混淆,不是修法 A 的缺陷(
crit2数据丢失一直是绿的,保留率 1.000)。
相对 v0.1.153:新增 skyline.ResetDiskProvenance()(把"从盘读回"溯源清零 ⇒ 之后的 ChunkLoadedFromDisk 只反映本轮),解决 v0.1.151 溯源是累积语义、回答不了"这一轮是不是从盘读的"的问题。终轮 crit5 5/5:注入 fired ✅、邻块形状指纹 4/4 逐位相同 ✅、鄰块本轮 fromDisk 4/4 ✅、受害块 nonAir 与邻居 0.999 ✅、恢复判据自动成立 ✅;完整高度图仍有 1 个邻块不同(SunlightHeight 随日照变)⇒ 只作仪器。README 与 CHANGELOG 同步更新。
SCAPI Skyline Project v0.1.153
SCAPI Skyline Project v0.1.153
[v0.1.153] - 2026-09-29
第一百五十七版:新增地形形状指纹(把"随日照变化的 SunlightHeight"从跨轮判据里切出去)。
相对 v0.1.152 的变更:
1. 新只读探针 skyline.ChunkShapeHash(cx, cz)
只哈希 Top/Bottom 两张图(不含 SunlightHeight);ChunkHeightHash 保留(含日照高度)并显式带
includesSunlightHeight 字段。为什么需要:SunlightHeight 由日照值推出 ⇒ 会随时间/天气变化,
跨轮比对"邻居有没有被污染"时会把时间变化误判成污染。
2. 用它把"修法 A 的 crit5"改成同轮次强设计(heightlab/skyline-v0153-crit5-same-round.py)
- 给受害区块与 4 个邻块各放一格 ⇒ 卸载时一定落盘 ⇒ 之后从盘读回(判据断言 4/4
loadedFromDisk=True); - 控制轮测完必须再卸载,否则受害区块常驻、注入轮不走
LoadChunkData⇒ 注入不触发
(第一版实测nonIoFailures增量 = 0,整轮无意义——已修); - 判据用形状指纹(Top+Bottom);含
SunlightHeight的完整高度图降为仪器。
3. 结果(如实):injection-fired ✅、neighbors-were-from-disk ✅、victim-regenerated ✅(0.999),
neighbors-identical-same-round ❌(4 个邻块里 1 个形状指纹变了)。
我先猜"是日照随时间变"——被实验否掉(形状指纹同样变)。剩下最合理的解释写在 notes/262 §5.2:
受害区块在修法 A 下重新生成期间列是空的,而邻块的高度计算会读邻居列 ⇒ 那一个邻块可能按"空邻居"算过一次。
可判定验证方式已写好(下一轮:把该邻块再走一遍 InvalidLight,看形状指纹是否回到控制值)。
如实边界
- 数据丢失路径(
crit2)仍是绿的(保留率 1.000)——本版没有改变这一点。 crit5没有关掉,但性质已从"测量混淆"澄清为"可能真实、范围极小(1/4 邻块)、可判定"。- 本版只加只读探针;
SunLightReplay/SunlightHeight的行为未变。
相对 v0.1.152:新增只读探针 skyline.ChunkShapeHash(只哈希 Top/Bottom,不含由日照值推出的 SunlightHeight);并用它把修法 A 的 crit5 改成同轮次强设计(受害区块与 4 邻块各放一格 ⇒ 一定落盘 ⇒ 从盘读回;控制轮测完必须再卸载,否则注入不触发)。实测:注入 fired ✅、邻块 4/4 fromDisk ✅、受害块 nonAir 与邻居同量级(0.999)✅,但仍有 1/4 邻块的形状指纹变了 ⇒ 我先猜的"日照随时间变"被否掉;最合理解释(重生成窗口期"空邻居"被邻块高度计算读到)已写进 notes/262 §5.2,并给出可判定验证方式。crit5 未关,crit2(数据丢失)仍绿。README 9993 字符。
SCAPI Skyline Project v0.1.152
SCAPI Skyline Project v0.1.152
[v0.1.152] - 2026-09-29
第一百五十六版:门禁的两项 SKIP 收口 —— 把"赌玩家脚下正好有壳"改成锚到壳仓里真实存在的立方体。
相对 v0.1.151 的变更:
1. 新只读探针 skyline.NearestShellCube(s)
SkylineCubeShellStore.NearestShellCubes(x,y,z,count):只读遍历 m_entries 的键,
按到给定世界坐标的距离排序,返回最近 N 个确实带壳(Shell 或 VoxelShell 非空)的立方体。
NearestShellCube(...) 是 count=1 的便捷形式。
2. 门禁两项从"随现场漂移"变成"锚点稳定"
shell-mesh-tiers 与 shell-column 原来只看玩家所在立方体(玩家一换地方就 SKIP,
审计在 to-cc/…072442Z-0001#2 里点名)。现在:
shell-mesh-tiers:锚到NearestShellCube返回的最近有壳立方体;shell-column:依次试NearestShellCubes(count=12)的前若干个候选,直到找到"列上真读到壳"的那个
(网格壳与"列上有壳"不是一回事——实测第 3 个候选才命中)。
3. 结果:门禁 PASS 24 / FAIL 0 / SKIP 0
[PASS] shell-mesh-tiers 锚点=壳仓最近有壳立方体 [3231,6,2134](距玩家 16 m;检查 12882/12882)
[PASS] shell-column 锚点=第 3 个有壳候选中"列上真读到壳"的 [3230,6,2129];
shell=SnowBlock#61 terrain=SnowBlock#61(替换口径 want#62)
[PASS] terrain-storage allocated=452 arrayNonEmpty=452 duplicates=0
一次失败的尝试(如实记录):我先试过"从玩家位置环形搜索"当锚点——在远处测试机位
1694 次探测 / 56 s 仍找不到(那一带根本没走过壳采集),而且把一项拖成 FAIL。
改成"问引擎要最近的有壳立方体"后,两项都在 200 ms 内出结果。
如实边界
- 锚点会随壳仓内容变化(它取的是"当前常驻的最近有壳立方体"),所以判据稳定的前提是
壳仓里有壳;一个壳都没有时仍然 SKIP(并把cubeCount写进说明,便于分辨"没壳"与"代码坏了")。 - 本版只加只读探针与门禁锚点,不改任何运行行为。
相对 v0.1.151:新增只读探针 skyline.NearestShellCube(s)(遍历壳仓 m_entries 键,返回离给定坐标最近的有壳立方体,可要 N 个候选);门禁 shell-mesh-tiers 锚到最近有壳立方体、shell-column 依次试 12 个候选直到找到「列上真读到壳」的(实测第 3 个命中)。全量门禁首次 24 PASS / 0 FAIL / 0 SKIP。另如实记录一次失败尝试:围着玩家环形搜索当锚点在远处测试区 1694 次探测/56 s 仍找不到(那带从没采过壳),改成问引擎后两项都在 200 ms 内。README 9980 字符。
SCAPI Skyline Project v0.1.151
SCAPI Skyline Project v0.1.151
[v0.1.151] - 2026-09-29
第一百五十五版:把"坏档是否被写回"从代理判据升级为区块级判据(按审计 …084056Z-0001#2 的裁定)。
相对 v0.1.150 的变更:
1. 区块级写回计数 corruptChunksSaved
TerrainSerializer23:读盘失败(非 IO)时把该区块坐标记进一个有界集合;SaveChunkData 真写它时计数 +1
并写一行 Log.Warning。账本 skyline.ChunkLoadFailures() 新增两个字段:
corruptChunksSaved(0 = 坏档没有被写回)与 corruptChunksKnown。
为什么需要:审计指出 Region 文件是 16×16 = 256 个区块共用,
所以"文件 mtime 变新"不能证明受害区块被写回(同文件里别的区块也会写)⇒ mtime 降为遥测、
区块级计数成为权威判据。
2. 溯源探针 skyline.ChunkLoadedFromDisk(cx,cz)
记录"该区块本轮成功从存档读出过"(有界集合),供"注入后邻块不受影响"的判据过滤
那些其实是被重新生成的邻块。
3. 终轮实测(heightlab/skyline-v0149-corrupt-a-acceptance.py,工件与数字同轮)
| 判据 | 实测 | 判定 |
|---|---|---|
| 计数 | nonIoFailures +1、acceptedAfterFailure +0 |
✅ |
| 重生成区块与邻居同量级 | 279,040 vs 279,040(比值 1.000) | ✅ |
| 坏档不被写回(区块级) | corruptChunksSaved 增量 0 |
✅ |
| 干净重开仍是原档 | 受害区块高度指纹两遍一致(c2563abb457e3b25) |
✅ |
| 注入 → 编辑 → 保存 → 重开不断崖 | 278,462 → 278,464(保留率 1.000) | ✅ |
| 注入后邻块不受影响 | 4 个邻块里 1 个高度指纹变了 | ❌ 见下 |
关于最后一条(如实归因,不是修法/并行引入的缺陷):SaveChunk 的门卫是
ModificationCounter > 0 ⇒ "刚生成、没人改过"的区块不会落盘,所以本实验的第二轮其实是再次生成
而非读盘;而生成结果会受"邻居当时在不在场"影响(Terrain.SetCellValueFast 对未分配邻居静默丢弃,
notes/247 §2(e))⇒ 跨轮比较两个"都可能被重新生成"的区块会假 FAIL。
下一轮改法(已设计):把该判据改成同一轮次内的前后对比(不插入"释放 + 重新预加载")。
如实边界
- 本版只加计数与探针,不改任何行为默认值。
crit5这次是红的(1/4 邻块),已在notes/262写明归因与改法,不掩饰。
相对 v0.1.150:按审计裁定把『坏档是否被写回』从 Region mtime 代理升级为区块级判据——TerrainSerializer23 记住读盘失败的区块,SaveChunkData 真写它时计数 corruptChunksSaved 并写日志(账本新增 corruptChunksSaved/corruptChunksKnown);另加溯源探针 skyline.ChunkLoadedFromDisk。终轮 6/7:nonIo+1/accepted+0、重生成区块与邻居同量级(279,040 vs 279,040)、区块级写回增量 0、干净重开指纹=基线、编辑后 278,462→278,464(保留率 1.000);唯一红的 crit5(4 邻块中 1 个指纹变)已归因:未修改的新生成区块不落盘 ⇒ 两轮都是重生成、且生成受邻居在场影响(notes/262 写明同轮次改法)。README 9992 字符。
SCAPI Skyline Project v0.1.150
SCAPI Skyline Project v0.1.150
[v0.1.150] - 2026-09-29
第一百五十四版:补上审计点名的"光照位没有被比对"缺口 —— 日照/高度 pass 的同轮次幂等性探针。
相对 v0.1.149 的变更:
1. 新受控探针 skyline.SunLightReplay(cx, cz, restore=true)
审计(to-cc/…073754Z-0001#2)指出:v0.1.139 的验收只比高度图指纹,而同一次
GenerateChunkSunLightAndHeight 还会写光照位,那部分没有任何比对覆盖。
本探针用同轮次(中间没有任何传播动作)的方式直接测"这一段是区块自身的确定函数":
先取 digestBefore → 跑一次 f → 取 digestAfterFirstReplay → 再跑一次 f → 取 digestAfterSecondReplay,
判据是 f(f(x)) == f(x)。
为什么必须写成 f(f(x)) == f(x) 而不是 f(x) == x:第一次重跑会抹掉该区块的传播光
(它只按日照/衰减写),所以 before 与 f(x) 不同是正常的——v0.1.150 第一版探针按
f(x) == x 判,结果 5/5 全 FAIL,属判据写错(已改正并如实记录)。
restore=true(默认)在比较完把该区块标回 InvalidLight,让引擎自己重走完整光照
(含光源扫描与传播),因此不会把"只按日照"的结果留在世界里。
2. 验收 4/4 PASS(heightlab/skyline-v0150-sunlight-replay.py)
| 判据 | 实测 |
|---|---|
sunlight-pass-idempotent |
5/5 区块 f(f(x)) == f(x)(列内光照摘要逐位相同,覆盖整列全高) |
heights-stable-across-replay |
重跑前后高度图指纹相同 5/5 |
restore-happens |
5/5 被标回 InvalidLight |
state-returns-to-valid |
6 s 后 5/5 自己回到可读 |
附带事实(如实记录):5 个采样区块里 3 个在第一次重跑时摘要变化、2 个不变
⇒ 前者当时带着传播光、后者已经是"纯日照"状态。这条正好解释了为什么要按 f(f(x)) 判。
如实边界
- 探针是受控的(会重跑 pass 并改状态);只建议在没有点光源的测试区使用。
- 它证明的是"该 pass 是区块自身的确定函数"(并行路径依赖的性质),
不是"并行与串行结果相同"(后者由 v0.1.139 的高度图交叉比对 + 本探针共同支撑)。
相对 v0.1.149:新增受控探针 skyline.SunLightReplay(cx,cz,restore=true)——对 Valid 区块跑两次同一个 GenerateChunkSunLightAndHeight(与并行路径同一函数),取整列光照摘要;判据是 f(f(x))==f(x)(v0.1.150 第一版按 f(x)==x 判错、5/5 假 FAIL,已更正并如实记录:首跑会抹掉传播光属正常)。restore=true 时把区块标回 InvalidLight,让引擎自己重走完整光照。验收 4/4:5/5 幂等、高度图稳定、restore 生效、6 s 后全部回到可读。README 9997 字符。
SCAPI Skyline Project v0.1.149
SCAPI Skyline Project v0.1.149
[v0.1.149] - 2026-09-29
第一百五十三版:按审计裁定落地修法 A(默认打开) —— 掐断"坏档 + 编辑 ⇒ 地形被固化丢失"这条路径。
相对 v0.1.148 的变更:
1. 修法 A 改为默认生效
审计(to-cc/…081600Z-0001、…082034Z-0001)裁定:缺陷成立、定级 P1、修法选 A
("非 IO 异常也 return false";B 的先清零被裁定为冗余,因为 GenerateTerrain 会无条件重写每格)。
⇒ skyline.CorruptChunkRegenerate 默认翻成 true(保留开关只为 A/B 对照与一键回滚)。
2. 同批完成审计点名的三条配套
- 跨会话留痕(P3):非 IO 坏档失败额外写一行
Log.Warning(坐标 + 异常类型 + 是否被当成已加载),
因为账本是内存态、重启会清零; - 门禁升级为回归约束(P3):
chunk-load-ledger现在断言acceptedAfterFailure == 0
(修法 A 生效后该计数不得再增长); - 注入可指定目标区块(验收可靠性):
skyline.ChunkReadFailureTargetCx/Cz(默认任意),
否则"下一次 LoadChunkData"可能落在玩家附近的别的区块上、让实验不可判定(v0.1.149 第一版脚本就吃了这个亏)。
3. 验收(heightlab/skyline-v0149-corrupt-a-acceptance.py,审计的四条口径)
| 口径 | 实测 |
|---|---|
① 注入后 nonIoFailures +1、acceptedAfterFailure +0;受害区块走生成路径且与邻居同量级 |
✅ nonIo +1 / accepted +0;受害区块 nonAir 281,860 vs 邻居 283,714(比值 0.993) |
| ② 注入 → 编辑 → 保存 → 重开不断崖 | ✅ 280,411 → 280,411(保留率 1.000;对照:修法前 281,844 → 1) |
| ③ 坏档不被回写 | 59d766e324f17f5d);但 "Region 文件 mtime 不变"这条代理判据 FAIL(+2.02 s)——见下 |
④ catch (IOException) 分支不回归 |
代码级核对:该分支未改动(仍为弹窗 + DisposeProject + return false);未做行为注入(会关掉世界) |
关于 ③ 的代理判据(如实报告 + 已请审计裁定):区域文件是 16×16=256 个区块共用一个,
所以"文件 mtime 变新"不能证明受害区块被写回——它可能来自同文件的别的区块。
而字节级判据通过了(一次干净重开仍读到原档内容)⇒ 受害区块的原始数据没有被覆盖。
⇒ 我请 CC 裁定:把"字节级(指纹)"作为权威判据、"mtime"降为仪器,或给出更精确的区块级判据。
如实边界
- 修法 A 的语义:坏档区块会被重新生成(地形与原档不同但自洽);原档数据本身已损坏,
修法只是阻止"空内容被写回、原地形被抹掉"。 catch (IOException)那条重分支(弹窗 + 关世界)没有做行为注入测试(会中断会话)。
相对 v0.1.148:按 CC 裁定落地修法 A(skyline.CorruptChunkRegenerate 默认翻成 true ⇒ 非 IO 坏档异常也 return false、该区块按同一坐标重新生成);同批完成三条配套(非 IO 失败写一行日志留痕、门禁断言 acceptedAfterFailure==0、注入可指定目标区块)。审计四条口径实测:①nonIo +1 且 accepted +0、受害区块 nonAir 281,860 vs 邻居 283,714(比值 0.993)②**注入→编辑→保存→重开 280,411 → 280,411(保留率 1.000;修复前 281,844 → 1)**③字节级证据通过(干净重开后指纹=基线),但『Region mtime 不变』这条代理判据 FAIL(一个区域文件 256 区块共用,mtime 不能证明受害区块被写回)⇒ 已请 CC 裁定代理判据④IOException 分支未改动。README 9952 字符。
SCAPI Skyline Project v0.1.148
SCAPI Skyline Project v0.1.148
[v0.1.148] - 2026-09-29
第一百五十二版:notes/257 那条数据丢失发现的修法 A 开关版 + 对照验收(默认关,行为不变)。
相对 v0.1.147 的变更:
1. 新开关 skyline.CorruptChunkRegenerate(默认 false = 引擎原行为)
打开后:TerrainSerializer23.LoadChunkData 遇到非 IOException 的坏档异常时额外返回 false
⇒ 更新器把该区块当作"存档里没有"、按同一坐标重新生成,而不是带着未反序列化的空内容继续。
为什么默认关:它改变"坏档时的世界演化",属引擎语义变更 ⇒ 按协作约定交审计/用户裁定。
2. 对照验收 4/4 PASS(heightlab/skyline-v0148-corrupt-regenerate.py)
同一个"坏档 → 编辑该区块 → 卸载保存 → 重开"实验,跑两遍(关/开):
| 开关 | 中心区块 nonAirCells |
说明 |
|---|---|---|
| 关(引擎原行为) | 281,844 → 1 | 复现 notes/257 §6:原地形被固化丢失 |
| 开(修法 A) | 279,045 → 279,062 | 区块被重新生成,不塌成空(+17 = 那一格编辑的邻域效应) |
另外两条:baseline-terrain-is-substantial(两区基线都 >10k 实心格)、switch-restored(结束时开关还原)。
账本同时佐证:关时 acceptedAfterFailure +1,开时 +0(它没有被当成"已加载")。
如实边界
- 默认仍是关:本版只是把"修法"做成可复跑、可回滚的开关 + 证据,没有改变任何默认行为。
- 修法 A 的语义后果:坏档区块会被重新生成(地形与原档不同,但自洽且有实体地形);
原档数据仍然已经损坏——本修法只是阻止"空内容被写回、原地形被抹掉"。 - 修法 B(先清零再返回)未做;
notes/257 §4仍列着三选一。
相对 v0.1.147:把 notes/257 的修法 A 做成默认关的开关 skyline.CorruptChunkRegenerate(打开后:非 IO 坏档异常额外返回 false ⇒ 该区块按同一坐标重新生成,不再带着空内容往下走)。对照验收 4/4:同一个『坏档→编辑→保存→重开』实验,关=281,844→1 格(复现数据丢失)、开=279,045→279,062 格(区块被重新生成、不塌成空);账本 corroborate:关时 acceptedAfterFailure +1、开时 +0。默认仍是关——语义变更交审计/用户裁定。README 9987 字符。
SCAPI Skyline Project v0.1.147
SCAPI Skyline Project v0.1.147
[v0.1.147] - 2026-09-29
第一百五十一版:notes/257 那条发现的零风险处置("方案 C") —— 把"坏档被当成已加载"变成可数可查。
相对 v0.1.146 的变更:
1. 坏档账本(只加可观测性,不改任何语义)
TerrainSerializer23 新增三个静态计数与一个有界最近失败记录(8 条):
ioFailures(走 catch (IOException):弹窗 + DisposeProject 那条重分支)、
nonIoFailures(走 catch (Exception):只记日志然后 return true)、
acceptedAfterFailure(其中"被当成加载成功"的次数 —— 也就是 notes/257 那条发现的计量)。
只读探针:skyline.ChunkLoadFailures()。
2. 验收 3/3 PASS(heightlab/skyline-v0147-chunk-load-ledger.py)
| 判据 | 实测 |
|---|---|
ledger-counts-failure |
造一次坏档后 nonIoFailures +1、acceptedAfterFailure +1 |
ledger-keeps-coordinates |
最近记录含坐标 (4854,3760)、异常类型 InvalidDataException、注入消息 |
ledger-read-only-invariant |
再读一次计数不变(证明它只是账本、不改行为) |
3. 门禁新增 chunk-load-ledger
断言账本可读、三个计数都是整数、保留上限与最近记录结构在 ⇒ 这份可观测性不会被后续改动弄丢。
门禁现在 PASS 22 / FAIL 0 / SKIP 2。
如实边界
- 修法 A/B(非 IO 也
return false/ 先清零)仍未做:它们会改变"坏档时的世界演化",属引擎语义变更,
已在notes/257 §4列出并交审计裁定。 - 账本记录只保留最近 8 条(有界);没有做持久化(重启即清零)。
相对 v0.1.146:按 notes/257 的『方案 C』做零风险处置——TerrainSerializer23 新增坏档账本(ioFailures / nonIoFailures / acceptedAfterFailure + 最近 8 条含坐标与异常类型),只读探针 skyline.ChunkLoadFailures()。验收 3/3:造一次坏档后 nonIoFailures +1、acceptedAfterFailure +1(证明『坏档被当成已加载』可数)、记录含坐标与 InvalidDataException、再读计数不变。门禁新增 chunk-load-ledger ⇒ PASS 22 / FAIL 0 / SKIP 2。修法 A/B(非 IO 也 return false / 先清零)会改变世界演化,仍待审计裁定。README 9986 字符。