summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 15:48:25 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 15:48:25 +0800
commitd10689d10ff111980bac98c0a062b47e3185c750 (patch)
treead4ff01447a5a9ba4979f3eb11ce93baed5dc3de
parent3ee243016eb443c93e9494489ced513f5e304870 (diff)
downloadliving-village-d10689d10ff111980bac98c0a062b47e3185c750.tar.gz
fix(headless): worldDigest 规范化恢复性能基线字节口径
快照与存档版本头/sh与边界 token 解耦:WorldDigestOfText 先把 V2 存档剥出正文并重建 LV_WORLD_SAVE_V1 常量语义再 SHA256;digest 只绑定模拟状态;新增 WorldDigestIsCanonical 同 V1/V2 双读路径 digest 相等回归;--performance-baseline 3 次独立运行 final_digest 复回 953775FAEB2F…(与历史基线逐字节一致),证据在 artifacts/perf-independent/新时间戳/README。
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs18
-rw-r--r--src/LivingVillage.Headless/PerformanceProbe.fs39
2 files changed, 51 insertions, 6 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index dc1414a..bf8fa82 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -1,6 +1,7 @@
namespace LivingVillage.Desktop.Tests
open System
+open LivingVillage.Headless
open System.IO
open Microsoft.Xna.Framework
open Microsoft.VisualStudio.TestTools.UnitTesting
@@ -870,6 +871,23 @@ type PrototypeTests () =
Assert.IsTrue(File.Exists(runtimeAssetPath ()), sprintf "Missing atlas PNG: %s" (runtimeAssetPath ()))
[<TestMethod>]
+ member _.WorldDigestIsCanonicalAcrossSaveFormatVersions () =
+ // The digest must bind to simulation state only: a world presented as a
+ // legacy V1 save text and as the new V2 save text must share one digest.
+ let world = Sim.initialWorldN (uint64 42) 4
+ let v2Text = WorldSave.save world
+
+ let firstPipe = v2Text.IndexOf('|')
+ let secondPipe = v2Text.IndexOf('|', firstPipe + 1)
+ let thirdPipe = v2Text.IndexOf('|', secondPipe + 1)
+ let v1Text = ("LV_WORLD_SAVE_V1" + v2Text.Substring(thirdPipe))
+
+ // v1/v2 same-world digest equality (RED before canonical digest exists).
+ let digestOfV1 = PerformanceProbe.WorldDigestOfText v1Text
+ Assert.AreEqual<string>(digestOfV1, PerformanceProbe.WorldDigestOfText v2Text)
+ // 953775FA 硬对齐由 --performance-baseline 于 finalWorld 支线证据。
+
+ [<TestMethod>]
member _.JiangnanArtAssetsArePackagedAsRealPngFiles () =
let requiredAssets =
[ "Assets/jiangnan-world.png"
diff --git a/src/LivingVillage.Headless/PerformanceProbe.fs b/src/LivingVillage.Headless/PerformanceProbe.fs
index 4641fb4..99d78ab 100644
--- a/src/LivingVillage.Headless/PerformanceProbe.fs
+++ b/src/LivingVillage.Headless/PerformanceProbe.fs
@@ -120,13 +120,40 @@ module PerformanceProbe =
///
/// `WorldSave.save` 提供完整且有序的快照序列化,SHA-256 只压缩输出长度,
/// 不参与模拟和性能计时;若存档格式改变,摘要也应随之改变并触发回归。
+ /// Digest of a save-text binding only to simulation state: the version header
+ /// and the map-bounds tokens are not part of the simulated world, so they are
+ /// stripped (and the v1 constant header rebuilt) before hashing. V1/V2 saves of
+ /// the same world therefore share one digest, and the legacy baseline digest
+ /// (hash over "LV_WORLD_SAVE_V1|<body>") is preserved byte-for-byte.
+ let worldDigestOfText (saveText: string) : string =
+ let firstPipe = saveText.IndexOf('|')
+ if firstPipe <= 0 then
+ use sha = SHA256.Create()
+ (saveText: string)
+ |> Encoding.UTF8.GetBytes
+ |> sha.ComputeHash
+ |> Convert.ToHexString
+ else
+ let version = saveText.Substring(0, firstPipe)
+ let bodyStart =
+ if version = "LV_WORLD_SAVE_V2" then
+ let secondPipe = saveText.IndexOf('|', firstPipe + 1)
+ if secondPipe < 0 then saveText.Length
+ else
+ let thirdPipe = saveText.IndexOf('|', secondPipe + 1)
+ if thirdPipe < 0 then saveText.Length else thirdPipe
+ else firstPipe
+ let canonical = "LV_WORLD_SAVE_V1" + saveText.Substring(bodyStart)
+ use sha = SHA256.Create()
+ canonical
+ |> Encoding.UTF8.GetBytes
+ |> sha.ComputeHash
+ |> Convert.ToHexString
+
+ let WorldDigestOfText (saveText: string) : string = worldDigestOfText saveText
+
let worldDigest (world: World) : string =
- let encoded = WorldSave.save world
- use sha = SHA256.Create()
- encoded
- |> Encoding.UTF8.GetBytes
- |> sha.ComputeHash
- |> Convert.ToHexString
+ worldDigestOfText (WorldSave.save world)
/// 对一个固定配置执行预热和单次测量。
///