package models import ( "encoding/json" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} // Wallet + backpack + gacha counters kept in memory for the lifetime of a login. type PlayerProgress struct { Money float64 `json:"money"` Gold float64 `json:"gold"` BindingGold float64 `json:"bindingGold"` Items []PlayerProgressItem `json:"items"` GachaDrawsSinceGrand map[uint32]uint32 `json:"gachaDrawsSinceGrand"` // Worn phone-skin suit per character (FightSpiritConfig template id). PhoneSkins map[uint32]PhoneSkinRecord4229938 `json:"phoneSkins"` // Last equipped weapon instance id per character (FightSpiritConfig template id). LastWeaponBySpirit map[uint32]uint64 `json:"lastWeaponBySpirit"` // // Fight style chosen per (character, style type). The runtime key is a C# tuple, which // System.Text.Json cannot use as a dictionary key, so the save file flattens it to // "spiritId:typeId". // SpiritStyleOverrides map[string]uint32 `json:"spiritStyleOverrides"` // Fight style bound to one weapon instance id. WeaponStyleOverrides map[uint64]uint32 `json:"weaponStyleOverrides"` // Worn outfit per character (FightSpiritConfig template id). Fashions map[uint32]FashionRecord4229938 `json:"fashions"` // // Dye (染色) state per fashion id. Only fashions the player actually dyed get an entry; // an absent entry means "every plan slot unlocked at the configured default, no scheme saved". // Colorings map[uint32]FashionColoringRecord4229938 `json:"colorings"` // // Livehouse music-game clears per song id (2026-10-04). Absent entry = never played. // LivehouseRecords map[uint32]LivehouseRecord4229938 `json:"livehouseRecords"` // True once the client has been told FirstPlay = true for Livehouse. LivehouseFirstPlayDone bool `json:"livehouseFirstPlayDone"` // // Equipped weapon skins per character (spiritId -> weapon sceneItemId -> skinId). Only // characters the player actually dressed get an entry; an absent character renders the // authored default skins. // WeaponSkins map[uint32]WeaponSkinRecord4229938 `json:"weaponSkins"` // // Personal weapon wheel per character (FightSpiritConfig template id): the weapon instance id // occupying each slot, zero meaning "empty slot". Only wheels the player actually edited are // written, so an absent entry means "rebuild from the authored loadout". // This has to be persisted together with LastWeaponBySpirit: CombatCodec.SpiritWeaponSnapshot // silently rewrites CurrentWeaponUid back to slot 0 when the equipped instance is not part of // the wheel, so restoring the choice without the wheel would still hand the player the default. // WeaponSlotsBySpirit map[uint32][]uint64 `json:"weaponSlotsBySpirit"` // // Where the player was standing when the snapshot was written. There is no session-end hook, // so the movement handler throttles a save while the player walks around; this is the value it // writes. // ⛑ 2026-09-27:RaidId 不再是「必须等于主城才能恢复」的门卫,而是**要回到哪张图**的记载。 // 旧逻辑把它当守卫用,结果玩家一离开主城(GM 面板切 23300999 凌云/重霄最典型), // 每次保存都因为 raid 不匹配而写 null ⇒ 已有的坐标被抹掉 ⇒ 下次登录掉回配置出生点。 // 现在 raid / instance / universe 一起落盘,登录时原样回到那张图的那个点。 // 唯一还要拦的是搜打撤副本(见 PlayerProgressStore.IsPersistableRaidForPosition)。 // HasSavedPosition bool `json:"hasSavedPosition"` SavedRaidId uint32 `json:"savedRaidId"` SavedInstanceId uint64 `json:"savedInstanceId"` SavedUniverseId uint32 `json:"savedUniverseId"` SavedPositionX float32 `json:"savedPositionX"` SavedPositionY float32 `json:"savedPositionY"` SavedPositionZ float32 `json:"savedPositionZ"` SavedFacing float32 `json:"savedFacing"` // // Gameplay talent trees (天赋) keyed by TalentTreeGamePlayConfig id. Only trees the player // actually touched are written, so an absent entry means "seed a fresh tree on next login". // GameplayTalents map[uint32]GameplayTalentRecord4229938 `json:"gameplayTalents"` // // 已购买的房产(HouseConfig.Id)。2026-09-30 新增。 // 为什么必须有这一份:客户端判断「这套房是不是我的」只有一条路径 —— // gBuyHouseUtils.CheckHasBuyTheHouse(houseId),它读的是 // gPlayerManager.infoMinor.bindData.housesInfo.HouseInfoList // (= 登录包 PlayerClientInfoMinor.housesInfo,见 // PlayerInfoMinorData.lua:54)。购买时的「已购买」动画完全是客户端乐观行为 // (BuyHouseUtils.BuyTheHouse 拿到 error=0 就自己放动画、开 // HOUSE_REWARD_PANEL),服务端不回任何东西也不会被它发现; // 但一走进房子 / 一开大地图,客户端就用上面那条路径重新判一次 —— 服务端没写过, // 于是「未购买」。所以所有权必须由服务端落盘 + 在登录包里回填。 // 只存 id。版本号 / 家具索引在「只做所有权落地」这一轮恒为 0 // (见 GameRouter.House.cs 的 PushSyncAddHouseAsync)。 // OwnedHouseIds []uint32 `json:"ownedHouseIds"` // // 车库停车指派:houseId → (车位号 → 车辆配置 id)。2026-09-30 新增。 // 车位号是 0 基(= 线格式里的 ParkingSpaceIndex;客户端 Lua 侧 // 会 +1 变成它自己的 parkingId)。这一份对应 // HouseActiveInfo.ParkingSpaceVehicleIdDict(Dict<i32,u32>)—— // 登录包回填它,客户端 GarageManager.CheckHouseInfo 才认得出「哪个车位有车」。 // ⛔ 这一份只记「哪个车位停了哪辆车(configId)」,不记 EntityId —— // 车辆实体是每局重新 spawn 的,EntityId 由 SyncHouseParkingEntityBinding 运行时下发。 // GarageParking map[uint32]map[int32]uint32 `json:"garageParking"` RestoredFromDisk bool `json:"restoredFromDisk"` } type PlayerProgressItem struct { TemplateId uint32 `json:"templateId"` Count uint32 `json:"count"` IsBind bool `json:"isBind"` UniqueId uint64 `json:"uniqueId"` } // // One gameplay talent tree as the live session holds it. Mirrors the part of // PlayerClientInfo.GameplayTalentInfos the talent panel reads: the point balance, the tree exp/level // and, per talent node, how many layers are activated. // type GameplayTalentState4229938 struct { Point uint32 `json:"point"` Exp uint32 `json:"exp"` Level uint32 `json:"level"` // TalentTreeTalentConfig id -> activated layers. Nodes map[uint32]uint32 `json:"nodes"` } // Save-file shape of GameplayTalentState4229938. type GameplayTalentRecord4229938 struct { Point uint32 `json:"point"` Exp uint32 `json:"exp"` Level uint32 `json:"level"` Nodes map[uint32]uint32 `json:"nodes"` } // The three part ids the phone is actually wearing for one character. type PhoneSkinRecord4229938 struct { Wallpaper uint32 `json:"wallpaper"` Decoration uint32 `json:"decoration"` Pendant uint32 `json:"pendant"` } // // What one character is wearing. Mirrors the part of SpiritWearFashionsInfo the wardrobe panel // actually round-trips: the suit, the source stamp, worn fashion ids and hidden parts. // WearFashionEditInfoList (per-part scale/rotation/offset) is deliberately not persisted — it is // a niche editing feature and carrying it would only add a failure mode on restore. // type FashionRecord4229938 struct { FunctionSuitId uint32 `json:"functionSuitId"` Source int16 `json:"source"` SourceId uint32 `json:"sourceId"` IsTryWear bool `json:"isTryWear"` HiddenParts byte `json:"hiddenParts"` FashionIds []uint32 `json:"fashionIds"` } // // What the player dyed on one fashion: every saved colour plan, which plan is currently applied // and how many plan slots are unlocked. // ⛑ The wire keys are bytes (plan id / coloring type), but System.Text.Json cannot use byte as // a dictionary key — it would throw on save and take the whole economy snapshot with it. The // save shape therefore widens every key to uint; FashionColoringStore narrows them back. // type FashionColoringRecord4229938 struct { Schemes map[uint32]map[uint32]uint32 `json:"schemes"` AppliedScheme uint32 `json:"appliedScheme"` UnlockedSlots uint32 `json:"unlockedSlots"` } // // Which weapon skin each weapon of one character is wearing (sceneItemId -> skinId). Same rule as // the dye record: every dictionary key is uint because System.Text.Json cannot use byte as a // dictionary key and would take the whole economy snapshot down with it. // type WeaponSkinRecord4229938 struct { SkinBySceneItem map[uint32]uint32 `json:"skinBySceneItem"` } // // Minimal character economy persistence for build 4229938. The retail server keeps this in the // account database; this private server writes one JSON snapshot per player next to // config/private-server.json and replays it through PlayerInfo at login. // Writes happen on every wallet/backpack mutation because no reliable session-end hook exists, and // the file is tiny. Failures are logged and never break gameplay. // type PlayerProgressStore struct { Gate any `json:"gate"` Options any `json:"options"` // // 背包道具是否落盘。2026-09-23 用户定:**关**。 // 背景:点过一次「发放全道具」后 3783 个道具被写进 `config/player-progress.json`, // 之后每次登录都要恢复它们,`SyncPlayerInfo` 登录包因此涨到 **214 KB**(实测 2 次共 214569 B)。 // 这些道具本来就不是"玩家攒出来的",没必要持久化。 // 关掉之后:钱包三种货币 / 抽卡保底计数 / 手机壳 / 时装 / 武器槽 / 天赋**照旧持久化**, // 只有背包道具变成"每次登录从空开始"。GM 面板「发放全道具」在本次会话内照样能用, // 只是重登不会带过来。 // 想恢复持久化:把这个常量改回 true 即可(读、写两侧的分支都已经写好)。 // // 用 static readonly 而不是 const:const false 会让编译器把两个分支判成死代码(CS0162), // 刷两条 warning 出来。改成 readonly 后分支照常编译,改回 true 也不用动别处。 PersistBackpackItems bool `json:"persistBackpackItems"` FilePath string `json:"filePath"` } type SaveFile struct { Pid uint64 `json:"pid"` AccountId string `json:"accountId"` Money float64 `json:"money"` Gold float64 `json:"gold"` BindingGold float64 `json:"bindingGold"` SavedAtUtc string `json:"savedAtUtc"` Items []SaveItem `json:"items"` GachaDraws map[uint32]uint32 `json:"gachaDraws"` // Worn phone-skin suit per character. Absent in saves written before this field // existed; the deserializer then leaves the (empty) default, which just means "wear the // default suit". PhoneSkins map[uint32]SavePhoneSkin `json:"phoneSkins"` // Equipped weapon instance id per character. Zero/empty = keep the authored default. LastWeaponBySpirit map[uint32]uint64 `json:"lastWeaponBySpirit"` // Fight style per (character, style type), key flattened to "spiritId:typeId". SpiritStyleOverrides map[string]uint32 `json:"spiritStyleOverrides"` // Fight style per weapon instance id. WeaponStyleOverrides map[uint64]uint32 `json:"weaponStyleOverrides"` // Worn outfit per character. Absent = wear the authored default. Fashions map[uint32]SaveFashion `json:"fashions"` // Dye per fashion id. Absent in saves written before this field existed; // the deserializer then leaves the (empty) default, which means "slots at the configured // default, no scheme saved". Colorings map[uint32]SaveFashionColoring `json:"colorings"` // Livehouse music-game clears: song id -> counts. Absent = never cleared. LivehouseRecords map[uint32]LivehouseRecord4229938 `json:"livehouseRecords"` // True once the Livehouse intro timeline has been handed out. LivehouseFirstPlayDone bool `json:"livehouseFirstPlayDone"` // Equipped weapon skins per character. Absent = authored default skins. WeaponSkins map[uint32]map[uint32]uint32 `json:"weaponSkins"` // Edited weapon wheels per character. Zero = empty slot; absent = authored loadout. WeaponSlotsBySpirit map[uint32][]uint64 `json:"weaponSlotsBySpirit"` // Last known world transform, together with the scene it was captured in. The // raid/instance/universe triple is what lets a re-login land back on that scene instead of // the configured spawn. All three are absent (0) in saves written before 2026-09-27; the // restore path back-fills them from the scene presets. Position *SaveTransform `json:"position"` // Gameplay talent trees per TalentTreeGamePlayConfig id. Absent in saves written // before this field existed; the deserializer then leaves the (empty) default, which just // means "seed every tree from talentPointSeed". GameplayTalents map[uint32]SaveGameplayTalent `json:"gameplayTalents"` // 已购买的房产(HouseConfig.Id)。缺失 = 一套都没有,这正是旧存档的正确语义。 OwnedHouseIds []uint32 `json:"ownedHouseIds"` // 车库停车指派:houseId → (车位号(0 基) → 车辆 configId)。 // 缺失 = 车库全空,正是旧存档的正确语义。 GarageParking map[uint32]map[int32]uint32 `json:"garageParking"` } type SaveGameplayTalent struct { Point uint32 `json:"point"` Exp uint32 `json:"exp"` Level uint32 `json:"level"` Nodes map[uint32]uint32 `json:"nodes"` } type SaveTransform struct { RaidId uint32 `json:"raidId"` InstanceId uint64 `json:"instanceId"` UniverseId uint32 `json:"universeId"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` Facing float32 `json:"facing"` } type SaveFashion struct { FunctionSuitId uint32 `json:"functionSuitId"` Source int16 `json:"source"` SourceId uint32 `json:"sourceId"` IsTryWear bool `json:"isTryWear"` HiddenParts byte `json:"hiddenParts"` FashionIds []uint32 `json:"fashionIds"` } // Save shape of FashionColoringRecord4229938: planId -> coloringType -> // colorCfgId, plus the applied plan and the unlocked slot count. All keys are uint (see the // record for why byte would break the serializer). type SaveFashionColoring struct { Schemes map[uint32]map[uint32]uint32 `json:"schemes"` AppliedScheme uint32 `json:"appliedScheme"` UnlockedSlots uint32 `json:"unlockedSlots"` } type SavePhoneSkin struct { Wallpaper uint32 `json:"wallpaper"` Decoration uint32 `json:"decoration"` Pendant uint32 `json:"pendant"` } type SaveItem struct { TemplateId uint32 `json:"templateId"` Count uint32 `json:"count"` IsBind bool `json:"isBind"` UniqueId uint64 `json:"uniqueId"` }