summaryrefslogtreecommitdiff
path: root/docs/维护说明.md
blob: 4635b6de3d95ad572c98009c03658481f46d729e (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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 维护说明

结论先行:主分支保持 0 警告 0 错误 + Desktop 79/79、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。
  - `waterFrameTickAt tick x y = (tick / 32 + x + 2y) % 3` → Water / WaterSpriteB / WaterSpriteC。
    相位按**世界 tile 坐标**偏移,所以同一帧里相邻河面砖显示不同相位;`waterFrameTick` 仍保留为
    全局相位(旧断言)。
  - `smokeFrameTick = (tick / 40) % 3` → SmokeSpriteA/B/C。炊烟在 `drawWorld` 的**末尾单独一遍**
    绘制,位置 `offsetTile (house door) 0 -3`(屋脊正上方),保证画在屋瓦、竹子等所有 prop 之上。
- 以上帧选择只用 `tick` 与「是否移动」,无墙钟/`System.Random`;同 tick 必得同帧。

## 预乘 alpha 与夜间光晕/炊烟接线(P14)

- **机理**:MonoGame `SpriteBatch` 默认 `BlendState.AlphaBlend` 按**预乘**语义合成
  (source blend = `One`,destination = `InverseSourceAlpha`)。因此直传未预乘的
  `Color(r,g,b,a)` 会把 rgb 按原值直接加进帧缓冲:多层暖色叠加在核心区饱和成
  **不透明纯白方块**(P13 夜晚灯笼的 75×75 白块即由此而来;`Color(255,0,0,60)` 实测渲染为
  `(255,63,68)`)。光晕、色调等任何 alpha<255 的前景绘制都必须**按自身 alpha 预乘 rgb**。
- **正确写法**:`VillageArt.premultiply r g b a`(每通道 `round(c * a / 255)`,四舍五入;
  alpha=255 为恒等)。预乘后单通道 ≤ alpha,alpha<250 时不可能出现纯白。
- **夜间光晕**:`VillageArt.lanternGlowLayers cx cy radius rings r g b peakAlpha` 用同心圆
  pixel run 近似径向衰减,每层色值预乘、核心多层叠加≈`peakAlpha`、边缘单层最暗;
  `Game.fs` 只在 `renderPlan.Props` 的 `RiverLantern` 实体处绘制,无灯笼处不产生光斑。
- **炊烟**:见上「动画语义」——烟雾是独立的后置绘制遍,与灯笼光晕同理。
- **角色站位**:`characterDestination` 把精灵下移 `characterFeetOffset = tilePixels/2`(16px),
  使脚底贴合地砖底线;`drawGroundShadow` 用 `ellipseRuns` 画预乘半透明椭圆接触阴影。
- **回归**:Desktop 单测 `PremultiplyScalesEachChannelByItsOwnAlpha`、
  `LanternGlowLayersArePremultipliedAndRadial`、`WaterRipplePhaseIsDistributedPerTile`、
  `CharacterFeetSitOnTheTileGroundLine`;图像级 `scripts/check-no-white-blocks.py`
  对夜晚录帧断言不存在 ≥64×64、每通道 ≥250、alpha ≥250 的方块(P13 旧帧 42/69 命中,修复后 0 命中)。

## 性能基线命令

```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)即走路逐帧采样。
- 夜晚白块图像级回归(0 命中 = 通过;对 P13 旧帧应报 42/69 命中):

```bash
python3 scripts/check-no-white-blocks.py evidence/night-run
```

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

- `/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/`)
- `/tmp/opencode/lv-p13-evidence/`(P14 修复后重录:`day-panorama.png`、`night-lantern.png`、
  `day-night-compare.png`、`chimney-closeup.png`、`night-lantern-closeup.png`、
  `white-block-check.txt`、`day-run/`、`night-run/`;同步一份到 `evidence/`)
- `artifacts/perf-independent/<TS>/`(digest 3 联测回读)