Skip to content

Releases: DionysusDWI/SCAPI-Branch_Skyline

SCAPI Skyline Project v0.1.156

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 13:24

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 —— 既偏快、又是"越高越快");
  • 范围到近地平线:CloudMaxDistance 6000 → 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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 09:38

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 09:16

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 09:13

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 09:07

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 08:57

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 08:51

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 08:40

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 08:33

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

Choose a tag to compare

@DionysusDWI DionysusDWI released this 29 Sep 08:19

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 字符。