summaryrefslogtreecommitdiff
path: root/docs/api-reference/world-save.md
blob: a8f474f5353d990c8281cc45e64bab4007ebbf5e (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
# 存读档(WorldSave)

源码:`src/LivingVillage.Kernel/WorldSave.fs`(`module WorldSave`)。

## 职责

把 `Sim.World`(可选附 `Occupation.State`)序列化为稳定的 `|` 分隔文本并解析回来,
兼容 v1/v2/v3 三种版式;不负责文件路径策略与 UI(由 Desktop 调用方决定)。

## 对外 API 概览

- `save world : string` = `saveWith None world`(无职业,产 **v2**,与历史格式逐字节一致)。
- `saveWith occupation world : string`:`Some state` → **v3**(含职业段与可选尾段)。
- `load text : Result<World, string>`、`loadFromFile path : Result<World, string>`。
- `loadFromFileWith path : Result<World * Occupation.State option, string>`(读 v3 职业伴随数据)。
- `saveToFile path world` / `saveToFileWith occupation path world`(UTF-8 无 BOM)。
- 内部 token 读写:`writeDailyTask`(`task-today` 段)、`writeBackpack`(`backpack` 段)、
  `writeAnnal`(年鉴,含 `story`)。

## 单位与量纲

- 版式头:`LV_WORLD_SAVE_V1`(无宽高,读回默认 64×48)、`LV_WORLD_SAVE_V2`(带 `width height`)、
  `LV_WORLD_SAVE_V3`(带职业段)。文本以 `|` 分隔。
- v3 职业段:`occupation.kind`(`farmer|fisher|peddler|scholar` 之一,未知 → `None`)、
  `occupation.task`(int)、`occupation.stage`(int)。
- 尾段(annals 之后,按需出现):`task-today`(模板/状态/`OfferedTick`/`DueTick`/
  `npc-none|npc-some (+id)`/`tile-none|tile-some (+x,y)`)、`backpack`(`count` + 逐项
  `item quantity`)、`streak`(仅 `> 0` 时)。
- 任务模板 token:`deliver-grain/help-work/till-soil/night-catch/sell-fish/market-inquiry/
  buy-goods/resell/observe-notes/reason-debate`;状态 token:`offered/active/done/failed`。

## 不变量

1. **v2 逐字节稳定**:`save world` 的输出在相同 `World` 下不变;`saveWith None world` 与 `save world`
   完全一致。
2. **无职业 = 旧行为**:不带职业时 v2 序列化不产生任何 v3 专有段。
3. **v1 向后兼容**:读到 `LV_WORLD_SAVE_V1` 时回退地图边界 64×48。
4. **派生量不入档**:`RumorCount` / `RumorOldestDay` 不写;读档用 `rumorWorkingSetStats` 重建。
5. **尾段可缺省**:旧 v3 无 `task-today`/`backpack`/`streak` 时,读回 `Today = None`、
   `Backpack` 用 `profileOf` 默认值、`Streak = 0`。
6. **完整消费**:解析后 `reader.Remaining = 0`,否则视为 `trailing save data`。

## 失败分支

- 头未知 → `Error "unsupported save format"`。
- token 非法/缺字段/多余尾数据 → `SaveParseError`,归一为 `Error`。
- 数值越界/格式错误 → 捕获 `FormatException` / `OverflowException` / `ArgumentException`,
  返回 `Error "invalid save: <message>"`;`null` 文本 → `Error "save text is null"`。
- 文件读取失败 → `Error "could not read save: <message>"`(捕获 `IOException`)。
- 背包数量 `< 0` → 解析失败(`invalid backpack[i].quantity`)。

## 算法与性能取舍

- 手写 token 序列化(`ResizeArray<string>` 拼接),无反射/无 JSON 依赖;浮点用
  `CultureInfo.InvariantCulture` 格式以保证跨机一致。
- 版本化采用「头 + 可选段」而非新格式文件:读端按头分派,v2 路径零额外开销。
- 存档不是热路径;解析失败一律以 `Result` 返回,不抛出到调用方。