diff options
Diffstat (limited to 'src/LivingVillage.Kernel')
| -rw-r--r-- | src/LivingVillage.Kernel/Occupation.fs | 20 | ||||
| -rw-r--r-- | src/LivingVillage.Kernel/WorldSave.fs | 151 |
2 files changed, 129 insertions, 42 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs index e0507ae..a3c37bd 100644 --- a/src/LivingVillage.Kernel/Occupation.fs +++ b/src/LivingVillage.Kernel/Occupation.fs @@ -43,15 +43,6 @@ module Occupation = | Peddler -> { Kind = Peddler; InitialMoney = 90; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 6; Sim.Spice, 4 ] } | Scholar -> { Kind = Scholar; InitialMoney = 70; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 10; Sim.Scroll, 2 ] } - /// Placeholder carried in save v3 until the task-system slice lands (design §3). - type State = - { Profile: Profile - TaskToken: int - StoryStage: int } - - let stateOf (kind: Kind) : State = - { Profile = profileOf kind; TaskToken = 0; StoryStage = 0 } - // ---- 每日任务(design §3):Kernel 侧纯函数片,不进 Sim.step 数值路径 ---- type TaskState = @@ -169,3 +160,14 @@ module Occupation = OfferedTick = offered DueTick = offered + Sim.ticksPerDay - 1L State = Offered } + + /// 挂在玩家侧的职业状态,随存档 v3 保存(design §2)。 + /// Today = 当日任务(§3);TaskToken 保留为 v3 前缀占位扩展位(恒 0,暂未启用)。 + type State = + { Profile: Profile + TaskToken: int + Today: DailyTask option + StoryStage: int } + + let stateOf (kind: Kind) : State = + { Profile = profileOf kind; TaskToken = 0; Today = None; StoryStage = 0 } diff --git a/src/LivingVillage.Kernel/WorldSave.fs b/src/LivingVillage.Kernel/WorldSave.fs index 2d3088c..1ce1dcb 100644 --- a/src/LivingVillage.Kernel/WorldSave.fs +++ b/src/LivingVillage.Kernel/WorldSave.fs @@ -407,6 +407,82 @@ module WorldSave = Kind = readAnnalKind reader (label + ".kind") Summary = readText reader (label + ".summary") } + let private writeTaskTemplateId tokens template = + match template with + | Occupation.DeliverGrain -> add tokens "deliver-grain" + | Occupation.HelpWork -> add tokens "help-work" + | Occupation.TillSoil -> add tokens "till-soil" + | Occupation.NightCatch -> add tokens "night-catch" + | Occupation.SellFish -> add tokens "sell-fish" + | Occupation.MarketInquiry -> add tokens "market-inquiry" + + let private readTaskTemplateId (reader: TokenReader) label = + match reader.Take(label) with + | "deliver-grain" -> Occupation.DeliverGrain + | "help-work" -> Occupation.HelpWork + | "till-soil" -> Occupation.TillSoil + | "night-catch" -> Occupation.NightCatch + | "sell-fish" -> Occupation.SellFish + | "market-inquiry" -> Occupation.MarketInquiry + | _ -> invalid (sprintf "invalid %s" label) + + let private writeTaskState tokens state = + match state with + | Occupation.Offered -> add tokens "offered" + | Occupation.Active -> add tokens "active" + | Occupation.Done -> add tokens "done" + | Occupation.Failed -> add tokens "failed" + + let private readTaskState (reader: TokenReader) label = + match reader.Take(label) with + | "offered" -> Occupation.Offered + | "active" -> Occupation.Active + | "done" -> Occupation.Done + | "failed" -> Occupation.Failed + | _ -> invalid (sprintf "invalid %s" label) + + /// v3 today-task 尾段(design §3):紧跟 annals 之后,仅在有任务时写入。 + /// 旧 v3(P10 版式)无此尾段 → 新 reader 读出 Today = None。 + let private writeDailyTask tokens (task: Occupation.DailyTask) = + add tokens "task-today" + writeTaskTemplateId tokens task.TemplateId + writeTaskState tokens task.State + addInt64 tokens task.OfferedTick + addInt64 tokens task.DueTick + match task.TargetNpc with + | None -> add tokens "npc-none" + | Some npc -> + add tokens "npc-some" + writeNpcId tokens npc + match task.TargetTile with + | None -> add tokens "tile-none" + | Some (tileX, tileY) -> + add tokens "tile-some" + addInt tokens tileX + addInt tokens tileY + + let private readDailyTask (reader: TokenReader) : Occupation.DailyTask = + let template = readTaskTemplateId reader "task.template" + let state = readTaskState reader "task.state" + let offeredTick = readInt64 reader "task.offered-tick" + let dueTick = readInt64 reader "task.due-tick" + let targetNpc = + match reader.Take("task.target-npc") with + | "npc-none" -> None + | "npc-some" -> Some(readNpcId reader "task.target-npc.id") + | _ -> invalid "invalid task.target-npc" + let targetTile = + match reader.Take("task.target-tile") with + | "tile-none" -> None + | "tile-some" -> Some(readInt reader "task.target-tile.x", readInt reader "task.target-tile.y") + | _ -> invalid "invalid task.target-tile" + { TemplateId = template + TargetNpc = targetNpc + TargetTile = targetTile + OfferedTick = offeredTick + DueTick = dueTick + State = state } + let private writeMind tokens (value: Mind) = writeNeeds tokens value.Needs writePersonality tokens value.Personality @@ -478,6 +554,8 @@ module WorldSave = /// 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. + /// When the occupation carries Today (design §3), a today-task tail segment + /// is appended after the annals. let saveWith (occupation: Occupation.State option) (world: World) : string = let tokens = ResizeArray<string>() match occupation with @@ -504,9 +582,19 @@ module WorldSave = world.Rumors |> List.iter (writeRumor tokens) addInt tokens world.Annals.Length world.Annals |> List.iter (writeAnnal tokens) + match occupation with + | Some state -> + match state.Today with + | Some task -> writeDailyTask tokens task + | None -> () + | None -> () String.Join("|", tokens) - let load (text: string) : Result<World, string> = + /// Full parser: world plus the v3 occupation companion (None for v1/v2 or + /// v3 without a recognized kind). The optional today-task tail after the + /// annals is validated in both cases; only its value is dropped when there + /// is no occupation state to attach it to. + let private parse (text: string) : Result<World * Occupation.State option, string> = if isNull text then Error "save text is null" else @@ -535,7 +623,7 @@ module WorldSave = | "peddler" -> Some Occupation.Peddler | "scholar" -> Some Occupation.Scholar | _ -> None - occupationOption <- kind |> Option.map (fun kind -> { Profile = Occupation.profileOf kind; TaskToken = task; StoryStage = stage }) + occupationOption <- kind |> Option.map (fun kind -> { Profile = Occupation.profileOf kind; TaskToken = task; Today = None; StoryStage = stage }) | _ -> () let tick = readInt64 reader "tick" let time = readFloat reader "time" @@ -550,24 +638,37 @@ module WorldSave = let rumors = List.init rumorCount (fun index -> readRumor reader (sprintf "rumor[%d]" index)) let annalCount = readCount reader "annals" let annals = List.init annalCount (fun index -> readAnnal reader (sprintf "annal[%d]" index)) + let todayTask = + if reader.Remaining > 0 then + match reader.Take("task.today") with + | "task-today" -> Some(readDailyTask reader) + | _ -> invalid "trailing save data" + else None if reader.Remaining <> 0 then invalid "trailing save data" + let occupation = + occupationOption + |> Option.map (fun state -> { state with Today = todayTask }) Ok - { Tick = tick - Time = time - Rng = rng - Avatar = avatar - NoHost = noHost - Npcs = npcs - Events = events - Rumors = rumors - Annals = annals } + ({ Tick = tick + Time = time + Rng = rng + Avatar = avatar + NoHost = noHost + Npcs = npcs + Events = events + Rumors = rumors + Annals = annals }, + occupation) with | SaveParseError message -> Error message | :? FormatException as ex -> Error(sprintf "invalid save: %s" ex.Message) | :? OverflowException as ex -> Error(sprintf "invalid save: %s" ex.Message) | :? ArgumentException as ex -> Error(sprintf "invalid save: %s" ex.Message) + let load (text: string) : Result<World, string> = + parse text |> Result.map (fun (world, _) -> world) + /// v2 output (no occupation) — byte-identical to the historic format. let save (world: World) : string = saveWith None world @@ -577,29 +678,13 @@ module WorldSave = 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> = + /// v3 companion reader: (world, occupation option incl. today task tail); + /// None for v1/v2 or v3 w/o kind. + let loadFromFileWith (path: string) : Result<World * Occupation.State option, string> = try - File.ReadAllText(path) |> load + File.ReadAllText path |> parse 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) + let loadFromFile (path: string) : Result<World, string> = + loadFromFileWith path |> Result.map (fun (world, _) -> world) |
