diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 17:18:11 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 17:18:11 +0800 |
| commit | cf214889490a62ba4fe84fb0055bc2133776e8a2 (patch) | |
| tree | 0408fbd82e95ed55f39f8da3ccd3d2259e999313 /src/LivingVillage.Kernel/WorldSave.fs | |
| parent | 87bdfe273555c5ae5f698b3b89fa6c39d84d0c33 (diff) | |
| download | living-village-cf214889490a62ba4fe84fb0055bc2133776e8a2.tar.gz | |
feat(occupation): 职业系统第一片 — Kernel 结构 / bootstrap 注入 / 存档 v3
Occupation.fs(Kind/Profile/State + 农夫/渔夫/货郎/书生 中文名常量,按设计 §1 表给初始 Money/Energy/Inventory);ItemKind 扩出 Fish/Spice/Scroll(basePrice 分支补齐,Food 10.0f 数值路径不变 → 无职业 digest 不动);WorldBootstrap.initialWorldWithOccupation 默认 None = 旧行为(Money/Energy 覆盖,库存字段先留数据侧、物理挂接留交易单——取舍见 design §2-C 反对加字段);WorldSave: v3 头仅在有职业时写入(保存 saveToFileWith / loadFromFileWith,v1/v2/v3 无职业类型默认,token 顺序 version|w|h|kind|task|story|…),默认路径 save 输出逐字节等同现行 v2;P10 测试 5 例(§1 表值、v3 round-trip、v1/v2 不吞档、双 seed 逐字节、中文名);Kernel.Tests 引用 Desktop 以驱动 WorldBootstrap(取舍:不做测试专用 shim)。TaskToken/StoryStage 为 v3 占位(任务系统后续单实现)。
Diffstat (limited to 'src/LivingVillage.Kernel/WorldSave.fs')
| -rw-r--r-- | src/LivingVillage.Kernel/WorldSave.fs | 72 |
1 files changed, 68 insertions, 4 deletions
diff --git a/src/LivingVillage.Kernel/WorldSave.fs b/src/LivingVillage.Kernel/WorldSave.fs index e68a45f..2d3088c 100644 --- a/src/LivingVillage.Kernel/WorldSave.fs +++ b/src/LivingVillage.Kernel/WorldSave.fs @@ -113,10 +113,16 @@ module WorldSave = let private writeItemKind tokens item = match item with | Food -> add tokens "food" + | Fish -> add tokens "fish" + | Spice -> add tokens "spice" + | Scroll -> add tokens "scroll" let private readItemKind (reader: TokenReader) label = match reader.Take(label) with | "food" -> Food + | "fish" -> Fish + | "spice" -> Spice + | "scroll" -> Scroll | _ -> invalid (sprintf "invalid %s" label) let private writeActionKind tokens action = @@ -467,11 +473,24 @@ module WorldSave = Inventory = readInventory reader (sprintf "npc[%d].inventory" index) Mind = readMind reader (sprintf "npc[%d].mind" index) } - let save (world: World) : string = + /// v3 saves are produced only when an occupation is attached (design task + /// requires the default (occupation-free) path to stay byte-identical to + /// today's v2 output). + /// v3 saves are emitted only when an occupation is attached; the default + /// (occupation-free) path stays byte-identical to today's v2 output. + let saveWith (occupation: Occupation.State option) (world: World) : string = let tokens = ResizeArray<string>() - add tokens "LV_WORLD_SAVE_V2" + match occupation with + | None -> add tokens "LV_WORLD_SAVE_V2" + | Some state -> add tokens "LV_WORLD_SAVE_V3" addInt tokens mapWidthTiles addInt tokens mapHeightTiles + match occupation with + | Some state -> + add tokens (Occupation.saveToken state.Profile.Kind) + addInt tokens state.TaskToken + addInt tokens state.StoryStage + | None -> () addInt64 tokens world.Tick addFloat tokens world.Time addUInt64 tokens world.Rng.State @@ -494,12 +513,30 @@ module WorldSave = try let tokens = text.Split([| '|' |], StringSplitOptions.None) let reader = TokenReader(tokens) - match reader.Take("format") with - | "LV_WORLD_SAVE_V2" -> + let mutable occupationOption : Occupation.State option = None + let versionTaken = reader.Take("format") + match versionTaken with + | "LV_WORLD_SAVE_V3" -> + configureBounds (readInt reader "bounds.w") (readInt reader "bounds.h") + | v2 when v2 = "LV_WORLD_SAVE_V2" -> configureBounds (readInt reader "bounds.w") (readInt reader "bounds.h") | v1 when v1 = formatVersion -> configureBounds 64 48 | _ -> invalid "unsupported save format" + match versionTaken with + | "LV_WORLD_SAVE_V3" -> + let kindToken = reader.Take("occupation.kind") + let task = readInt reader "occupation.task" + let stage = readInt reader "occupation.stage" + let kind = + match kindToken with + | "farmer" -> Some Occupation.Farmer + | "fisher" -> Some Occupation.Fisher + | "peddler" -> Some Occupation.Peddler + | "scholar" -> Some Occupation.Scholar + | _ -> None + occupationOption <- kind |> Option.map (fun kind -> { Profile = Occupation.profileOf kind; TaskToken = task; StoryStage = stage }) + | _ -> () let tick = readInt64 reader "tick" let time = readFloat reader "time" let rng = { State = readUInt64 reader "rng" } @@ -531,11 +568,38 @@ module WorldSave = | :? OverflowException as ex -> Error(sprintf "invalid save: %s" ex.Message) | :? ArgumentException as ex -> Error(sprintf "invalid save: %s" ex.Message) + /// v2 output (no occupation) — byte-identical to the historic format. + let save (world: World) : string = saveWith None world + let saveToFile (path: string) (world: World) : unit = File.WriteAllText(path, save world, UTF8Encoding(false)) + let saveToFileWith (occupation: Occupation.State option) (path: string) (world: World) : unit = + File.WriteAllText(path, saveWith occupation world, UTF8Encoding(false)) + let loadFromFile (path: string) : Result<World, string> = try File.ReadAllText(path) |> load with | :? IOException as ex -> Error(sprintf "could not read save: %s" ex.Message) + + /// v3 companion reader: (world, occupation option); None for v1/v2 or v3 w/o kind. + let loadFromFileWith (path: string) : Result<World * Occupation.State option, string> = + match loadFromFile path with + | Error e -> Error e + | Ok world -> + try + let text = File.ReadAllText path + let tokens = text.Split([| '|' |]) + if tokens.[0] <> "LV_WORLD_SAVE_V3" then + Ok(world, None) + else + match tokens.[3] with + | "farmer" -> Ok(world, Some (Occupation.stateOf Occupation.Farmer)) + | "fisher" -> Ok(world, Some (Occupation.stateOf Occupation.Fisher)) + | "peddler" -> Ok(world, Some (Occupation.stateOf Occupation.Peddler)) + | "scholar" -> Ok(world, Some (Occupation.stateOf Occupation.Scholar)) + | _ -> Ok(world, None) + with + | :? IOException as ex -> Error(sprintf "could not read save: %s" ex.Message) + | :? FormatException as ex -> Error(sprintf "invalid save: %s" ex.Message) |
