diff options
| -rw-r--r-- | docs/维护说明.md | 28 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 14 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 26 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/M6Presentation.fs | 7 |
4 files changed, 55 insertions, 20 deletions
diff --git a/docs/维护说明.md b/docs/维护说明.md index 024248f..3e055c3 100644 --- a/docs/维护说明.md +++ b/docs/维护说明.md @@ -188,10 +188,19 @@ dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll 1,正午/午夜 0)驱动 `backgroundAtWarmth/worldTintAtWarmth`,`M6RenderProfile` 增 `SkyWarmth`。 - ④回归:更新/新增确定性测试(行走节奏、光照锚点与暖色、图集 28 格与 PNG 尺寸、证据起时纯函数); Desktop 107 / Kernel 89 全绿(未触碰 Kernel/Sim.fs 数值路径)。 -- ⑤证据:`/tmp/opencode/lv-p17-art/`(`p17-day-0{1,2}.png`、`p17-dusk-0{1,2}.png`、`p17-night-0{1,2}.png`、 - `p17-diff-before-dawn.png` vs `p17-diff-after-dawn.png`(同一 6:00 tick,P17 前/后各 1 帧)、`sample.mp4` - 13.1 s);`check-no-white-blocks.py` 对所选 6 帧 0 命中。起时用新增环境钩子 `LV_AUTOPLAY_START_HOUR` - (纯函数 `M6Presentation.startTickForHour`,桌面演示专用,不进 Kernel)。 +- ⑤证据:`/tmp/opencode/lv-p17-art/`(`p17-day-12h-0{1,2}.png`、`p17-dusk-20h45-0{1,2}.png`、 + `p17-night-00h-0{1,2}.png`、`p17-diff-before-dawn.png` vs `p17-diff-after-dawn.png`(同一 6:00 tick, + P17 前/后各 1 帧)、`sample.mp4` 13.1 s);`check-no-white-blocks.py` 对所选 6 帧 0 命中。起时用新增 + 环境钩子 `LV_AUTOPLAY_START_HOUR`(纯函数 `M6Presentation.startTickForHour`,桌面演示专用,不进 Kernel)。 +- ⑥(P17-FIX 起时钩子修复)旧 `p17-day-0{1,2}` / `p17-dusk-0{1,2}` 帧的钩子在 sample/autoplay 取证路径下 + 失效:`StartNewGame` 重新创建世界时按 `daylightAutoplay` 把 Tick 重设为 0/6:00,覆盖了 + `createInitialWorld` 设好的值,导致黄昏/昼取证实际全为「夜晚 00:00」,P17 光照精修未被真实帧展示。 + 修复:抽纯函数 `M6Presentation.resolveStartTick (startHour option) (startAtDaylight)`(显式小时优先, + 否则 daylight 6:00,否则 0),`createInitialWorld` 与 `StartNewGame` 统一经 `evidenceStartTick`/`withStartTick` + 取同一 startTick(占位/任务日一致),`new-game=ok` 日志追加 `tick=` 便于机器核验;非取证人工路径不受影响。 + 回归:`StartTickResolutionHonoursEvidenceHourThenDaylightThenMidnight`(Desktop 107→108), + 实测 `tick`:昼 12.0→2,592,000、黄昏 20.75→4,482,000、夜 0→0,`sample.mp4` 为黄昏 20:45。 + 以上新文件名(`p17-day-12h-*` / `p17-dusk-20h45-*` / `p17-night-00h-*`)为准,旧名作废。 - 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 一致、`performance_determinism=PASS`。 @@ -228,10 +237,10 @@ timeout 90 xvfb-run -a -s "-screen 0 1280x720x24" env \ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll # P17 取证(街边细节/人物节奏/昼夜精修;LV_AUTOPLAY_START_HOUR 定起时:昼 12 / 黄昏 20.75 / 夜 0) -for spec in "day 12.0" "dusk 20.75" "night 0.0"; do set -- $spec; \ +for pair in day:12.0 dusk:20.75 night:0.0; do name=${pair%%:*}; hour=${pair##*:}; \ timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \ - LV_AUTOPLAY_START_HOUR=$2 LV_AUTOPLAY_RECORD=1 \ - LV_RECORD_DIR=/tmp/opencode/lv-p17-art/$1 LV_RECORD_EVERY=2 \ + LV_AUTOPLAY_START_HOUR=$hour LV_AUTOPLAY_RECORD=1 \ + LV_RECORD_DIR=/tmp/opencode/lv-p17-art/$name LV_RECORD_EVERY=2 \ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done # 街边细节图集再生成(确定性,二次运行 byte-identical) python3 scripts/make-jiangnan-art.py @@ -266,8 +275,9 @@ python3 scripts/check-no-white-blocks.py evidence/night-run - `/tmp/opencode/lv-p13/evidence/`(P13:`walk-idle-frames.png`、`water-3phase.png`、 `smoke-3phase.png`、`water-2frame.png`、`smoke-2frame.png`、`day-panorama.png`、 `night-lantern.png`、`day-night-compare.png`、`walk-real-run.png`、`day-run/`、`night-run/`) -- `/tmp/opencode/lv-p17-art/`(P17:`p17-day-0{1,2}.png`、`p17-dusk-0{1,2}.png`、`p17-night-0{1,2}.png`、 - `p17-diff-before-dawn.png`/`p17-diff-after-dawn.png`、`sample.mp4`;同一份拷入 `evidence/`) +- `/tmp/opencode/lv-p17-art/`(P17:`p17-day-12h-0{1,2}.png`、`p17-dusk-20h45-0{1,2}.png`、 + `p17-night-00h-0{1,2}.png`、`p17-diff-before-dawn.png`/`p17-diff-after-dawn.png`、`sample.mp4`; + 同一份拷入 `evidence/`) - `/tmp/opencode/lv-p16-art/`(P16:`frame-day-0{1,2,3}.png`、`frame-night-0{1,2,3}.png`、 `frame-splash-01.png`、`frame-menu-0{1,2}.png`、`hud-bar-day.png`、`hud-bar-night.png`、 `sample.mp4`、`final_verification.txt`;沙箱不允许 `/tmp/lv-p16-art`,故落此路径) diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index 4d2c895..b250824 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -902,6 +902,20 @@ type PrototypeTests () = Assert.AreEqual<int64>(6L * ticksPerHour, M6Presentation.startTickForHour 6.0) [<TestMethod>] + member _.StartTickResolutionHonoursEvidenceHourThenDaylightThenMidnight () = + let ticksPerHour = Sim.ticksPerDay / 24L + + // P17-FIX: the evidence hour must win on the sample/autoplay path too, otherwise + // StartNewGame re-created the world at 00:00 and dusk/day shots were actually night. + Assert.AreEqual<int64>(M6Presentation.startTickForHour 20.75, M6Presentation.resolveStartTick (Some 20.75) false) + Assert.AreEqual<int64>(int64 20 * ticksPerHour + ticksPerHour * 3L / 4L, M6Presentation.resolveStartTick (Some 20.75) false) + // Explicit hour overrides the daylight bootstrap. + Assert.AreEqual<int64>(12L * ticksPerHour, M6Presentation.resolveStartTick (Some 12.0) true) + // No override: daylight -> 6:00, otherwise midnight. + Assert.AreEqual<int64>(6L * ticksPerHour, M6Presentation.resolveStartTick None true) + Assert.AreEqual<int64>(0L, M6Presentation.resolveStartTick None false) + + [<TestMethod>] member _.TitleEntranceProgressIsDeterministicAndCompleteAfterTwoSeconds () = // 120 frames at the fixed 60Hz step = 2s entrance, pure function of frame count. Assert.AreEqual<float32>(1.0f, TitleScreen.entranceProgress 0L) diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 4760d4e..765cea8 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -134,13 +134,16 @@ type LivingVillageGame() as this = match Double.TryParse(value, Globalization.NumberStyles.Float, Globalization.CultureInfo.InvariantCulture) with | true, hour -> Some hour | _ -> None + // Single source of truth for the starting tick so the sample/autoplay path (which + // re-creates the world via StartNewGame) honours the evidence hour too. + let evidenceStartTick () = + M6Presentation.resolveStartTick startHourOverride daylightAutoplay + let withStartTick (world: World) : World = + let tick = evidenceStartTick () + { world with Tick = tick; Time = float tick * Sim.dtSeconds } let createInitialWorld () = - let baseWorld = WorldBootstrap.initialWorldForDaylight daylightAutoplay 42UL Sim.npcCount - match startHourOverride with - | Some hour -> - let tick = M6Presentation.startTickForHour hour - { baseWorld with Tick = tick; Time = float tick * Sim.dtSeconds } - | None -> baseWorld + WorldBootstrap.initialWorldForDaylight daylightAutoplay 42UL Sim.npcCount + |> withStartTick let mutable world = createInitialWorld () let mutable camera = Vector2.Zero let mutable avatarFacing = VillageArt.SouthFacing @@ -223,17 +226,18 @@ type LivingVillageGame() as this = this.CenterCamera() member private this.StartNewGame (occupation: Occupation.Kind option) = + let startTick = evidenceStartTick () let state = occupation - |> Option.map (fun kind -> - let startTick = if daylightAutoplay then 6L * Sim.ticksPerDay / 24L else 0L - WorldBootstrap.occupationStateFor 42UL kind startTick) - world <- WorldBootstrap.initialWorldWithOccupation daylightAutoplay 42UL Sim.npcCount state + |> Option.map (fun kind -> WorldBootstrap.occupationStateFor 42UL kind startTick) + world <- + WorldBootstrap.initialWorldWithOccupation daylightAutoplay 42UL Sim.npcCount state + |> withStartTick this.ResetWorldView() m5View <- { m5View with Task = state } menu <- menu |> MenuState.setCurrentWorld true |> MenuState.enterGame let occupationName = occupation |> Option.map Occupation.nameOf |> Option.defaultValue "none" - printfn $"new-game=ok seed=42 npcs={Sim.npcCount} occupation={occupationName}" + printfn $"new-game=ok seed=42 npcs={Sim.npcCount} occupation={occupationName} tick={world.Tick}" member private this.SaveWorld (prefix: string) : bool = try diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs index a64d494..752034a 100644 --- a/src/LivingVillage.Desktop/M6Presentation.fs +++ b/src/LivingVillage.Desktop/M6Presentation.fs @@ -104,6 +104,13 @@ module M6Presentation = let startTickForHour (hour: float) : int64 = int64 (hour * float (Sim.ticksPerDay / 24L)) + /// Start-tick decision shared by bootstrap and new-game: an explicit evidence hour wins, + /// else daylight starts at 6:00, else midnight. Pure function, no side effects. + let resolveStartTick (startHour: float option) (startAtDaylight: bool) : int64 = + match startHour with + | Some hour -> startTickForHour hour + | None -> if startAtDaylight then 6L * Sim.ticksPerDay / 24L else 0L + let private hourOf (tick: int64) : int = int ((tick / (Sim.ticksPerDay / 24L)) % 24L) |
