# 存读档(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`、`loadFromFile path : Result`。 - `loadFromFileWith path : Result`(读 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: "`;`null` 文本 → `Error "save text is null"`。 - 文件读取失败 → `Error "could not read save: "`(捕获 `IOException`)。 - 背包数量 `< 0` → 解析失败(`invalid backpack[i].quantity`)。 ## 算法与性能取舍 - 手写 token 序列化(`ResizeArray` 拼接),无反射/无 JSON 依赖;浮点用 `CultureInfo.InvariantCulture` 格式以保证跨机一致。 - 版本化采用「头 + 可选段」而非新格式文件:读端按头分派,v2 路径零额外开销。 - 存档不是热路径;解析失败一律以 `Result` 返回,不抛出到调用方。