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"`
}