summaryrefslogtreecommitdiff
path: root/docs/维护说明.md
blob: 6978a9a19423976a9007d684db367a51b85741a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# 维护说明

结论先行:主分支保持 0 警告 0 错误 + Desktop 75/75、Kernel 79/79 全绿;性能基线
final_digest 固定 953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C。
本文记录模块职责、不变量与逐字可复制的验收命令。

## 模块职责

- `src/LivingVillage.Kernel/`:纯确定性模拟(需求/关系/谣言/年鉴/交易),无渲染依赖。
  - `Sim.fs`:世界结构、`Sim.step` 主推进、对话/交易/谣言决策、地图边界
    `configureBounds`(默认 64×48,可到 512×384)。
  - `WorldSave.fs`:存档序列化。V2 头携带地图宽高,V1 旧档读入自动回退 64×48。
  - `Personality.fs / Interaction.fs(fsm) / M6*.fs`:NPC 行为状态机与里程碑数据。
- `src/LivingVillage.Desktop/`:MonoGame 渲染与交互层。
  - `VillageArt.fs`:图集加载与世界/人物/室内绘制;CC0 覆盖层(LV_ASSET_PACK=cc0)。
  - `Interaction.fs`:M5 视图与情境解析(FloatingPrompt / panelLines / 情境提示)。
  - `M6Presentation.fs`:昼夜光照纯函数(lightingBlend)与渲染档(Blend/LanternGlow)。
  - `TitleScreen.fs / Game.fs`:标题布局与主循环;LV Autos(sample / flow / record / map tour)。
  - `ProceduralMap.fs`:512×384 确定性地形(splitmix 随机游走河道 + 三倍频值噪声 + 泊松布点)。
  - `SampleScript.fs`:自动演示脚本(靠近→提示→交互→关闭→离开全流程)。
- `src/LivingVillage.Headless/`:headless 入口 + PerformanceProbe(digest、基跑)。
- `scripts/make-jiangnan-art.py / make-cc0-art.py`:原创与 CC0 素材确定性生成。
- `third_party/`:Kenney Tiny Farm、Puny Characters(均 CC0 1.0,来源与 sha256 在 PROVENANCE.md)。

## 模拟状态不变量(验收依据)

1. **数组快照**:`Sim.world.Npcs` 数组与 `InteractionResolver.resolve` / `refreshPrompt` 的
   调用之间必须保持恒定——选择/提示路径从不 mutation(PrototypeTests 快照回归)。
2. **确定性 RNG**:世界推进只随 `World.Rng`(splitmix)与 `(seed)` 派生,禁止墙钟与
   `System.Random`。昼夜/波纹/入场/动画帧全是 `tick → 值` 纯函数。
3. **存档 v1/v2**:`WorldSave` v2 头携带 `width height`,V1 读入回退默认。`worldDigest`
   对版本头与边界 token 不敏感(剥头后重构 V1 常量语义再 SHA256),摘要只绑定模拟状态。
4. **地图 512×384 大世界**:`ProceduralMap.generate` 同 seed 两次生成逐字节一致;
   村核矩形(24..38 × 16..34)由生成器不变式保证可通行。

## 动画语义(P13,全部为 `tick → 帧` 纯函数)

- 角色图集 `Assets/jiangnan-characters.png`:3840×48,每个 (变体,方向) 占 **6** 格
  (4 走路 + 2 待机呼吸),格宽 32 高 48。
  - `cellIndex = (variantIndex * 4 + directionIndex) * 6 + frameIndex`;
    方向序 North/South/West/East = 0/1/2/3,变体序 Indigo/Ochre/Jade/Grey/StrawHat = 0..4。
  - frameIndex:Walk 0..3 = 0..3,IdleOne/IdleTwo = 4/5。
  - `animationFrame = (tick / 4) % 4`(16 tick 一循环);`idleFrame = (tick / 24) % 2`(48 tick 一循环)。
  - `characterFrame isMoving tick`:移动取走路帧,静止取呼吸帧。Avatar 用 `Game.fs` 的
    `avatarMoving`(比较前后位置);NPC 用 `npcSpriteSpecAtTarget`(目标向量 ≥0.5px 视为移动)。
- 世界图集 `Assets/jiangnan-world.png`:800×32,25 格。水面 3 帧在 slot 1/13/21,
  炊烟 3 帧在 slot 22/23/24。
  - `waterFrameTick = (tick / 32) % 3` → Water / WaterSpriteB / WaterSpriteC;
  - `smokeFrameTick = (tick / 40) % 3` → SmokeSpriteA/B/C,画在民居屋顶上方 `offsetTile pos -2 -3`。
- 以上帧选择只用 `tick` 与「是否移动」,无墙钟/`System.Random`;同 tick 必得同帧。

## 性能基线命令

```bash
dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll --performance-baseline
```

- 口径:seed=42,npc=4,warmup=120,measure=6000,repetitions=3(见首行 `performance_config`)。
- 门槛:`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C` 三次一致 + `performance_determinism=PASS`。
- digest 变化处理:先比对 `WorldSave.save world` 全文文本(版本头变化不构成失败即 digest 规范化后等价);
  再查 Sim.step 数值路径是否被改动(不允许)。历史归属见 `artifacts/perf-independent/*/README.md`。

## 已验证命令

```bash
# 完整重建(0 警告 0 错误)
dotnet clean LivingVillage.sln 2>&1 | tail -n1
rm -rf src/*/bin src/*/obj
dotnet restore LivingVillage.sln
dotnet build LivingVillage.sln -c Release 2>&1 | tail -n3

# 全量测试
dotnet test src/LivingVillage.Desktop.Tests -c Release --no-build
dotnet test src/LivingVillage.Kernel.Tests -c Release --no-build

# 素材再生成(确定性,输出 byte-identical)
python3 scripts/make-jiangnan-art.py && python3 scripts/make-cc0-art.py

# Xvfb 录屏取证(沿用既有 pN harness 外参即可;harness 不入库)
pkill -f "Xvfb :99"; Xvfb :99 -screen 0 1280x720x24 & sleep 2
ffmpeg -y -f x11grab -video_size 1280x720 -framerate 10 -i :99 -pix_fmt yuv420p out.mp4
LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_DAYLIGHT=1 LV_AUTOPLAY_RECORD=1 \
  dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll

# P13 动画证据(确定性接触表 + 真实运行帧)
python3 scripts/make-animation-evidence.py        # -> evidence/walk-idle-frames.png 等
# 真实运行帧(白天 / 夜晚各一次;record 目录与间隔可用环境变量覆盖)
timeout 45 xvfb-run -a -s "-screen 0 1280x720x24" env \
  LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_DAYLIGHT=1 LV_AUTOPLAY_RECORD=1 \
  LV_RECORD_DIR="$PWD/evidence/day-run" LV_RECORD_EVERY=2 \
  dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
timeout 45 xvfb-run -a -s "-screen 0 1280x720x24" env \
  LV_AUTOPLAY_SAMPLE=1 LV_AUTOPLAY_RECORD=1 \
  LV_RECORD_DIR="$PWD/evidence/night-run" LV_RECORD_EVERY=2 \
  dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
```

- `LV_RECORD_DIR` 默认 `/tmp/lv-p5-prompt`,`LV_RECORD_EVERY` 默认 25(每 N 次绘制存一帧)。
  跑完 `sample result=ok`;连续移动帧(如 day-run 的 0136..0146)即走路逐帧采样。

## 取证快照位置(近期)

- `/tmp/lv-p2-ui/`、`/tmp/lv-p3-fixes/`(标题/键位/P5 提示与修)
- `/tmp/lv-p6-art/`(昼夜光照/四帧行走/标题入场)
- `/tmp/lv-p7-map/`(512×384 大世界巡游帧 + 夜灯对照)
- `/tmp/lv-p8-cc0/`(CC0 同 seed 对照帧)
- `/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/`)
- `artifacts/perf-independent/<TS>/`(digest 3 联测回读)