package common import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "strconv" ) const currencyWarsSaveFormat = 1 // Version of this repository's save envelope. var ErrCurrencyWarsSaveConflict = errors.New("CW save changed in another session") // WireData is a serialized GridFightGetDataScRsp, retaining unknown protocol // fields. UID and Client identity prevent restoring a different account/schema. type CurrencyWarsSnapshot struct { FormatVersion uint32 `json:"format_version"` UID uint32 `json:"uid"` ClientVersion string `json:"client_version"` AssemblySHA256 string `json:"assembly_sha256"` Revision uint64 `json:"revision"` WireData []byte `json:"wire_data"` } func (s *CurrencyWarsSnapshot) validate() error { if s == nil || s.FormatVersion != currencyWarsSaveFormat || s.UID == 0 || s.ClientVersion == "" || len(s.WireData) == 0 { return fmt.Errorf("invalid CW snapshot envelope") } if digest, err := hex.DecodeString(s.AssemblySHA256); err != nil || len(digest) != sha256.Size { return fmt.Errorf("invalid CW snapshot source hash") } return nil } // ReadCurrencyWarsSnapshot never creates or rewrites persistent. A revision // mismatch is surfaced rather than silently replacing progress with defaults. func ReadCurrencyWarsSnapshot(uid uint32, version, assemblySHA string) (*CurrencyWarsSnapshot, error) { root, err := readCurrencyWarsPersistentRoot() if err != nil { return nil, err } var accounts map[string]json.RawMessage if raw, ok := root["currency_wars"]; ok { if err := json.Unmarshal(raw, &accounts); err != nil || accounts == nil { return nil, fmt.Errorf("invalid CW account store") } } raw, found := accounts[strconv.FormatUint(uint64(uid), 10)] if !found { return nil, nil } var snapshot CurrencyWarsSnapshot if err := json.Unmarshal(raw, &snapshot); err != nil { return nil, fmt.Errorf("decode CW snapshot: %w", err) } if err := snapshot.validate(); err != nil { return nil, err } if snapshot.UID != uid || snapshot.ClientVersion != version || snapshot.AssemblySHA256 != assemblySHA || snapshot.Revision == 0 { return nil, fmt.Errorf("CW snapshot account/Client revision does not match") } return &snapshot, nil } func readCurrencyWarsPersistentRoot() (map[string]json.RawMessage, error) { raw, err := os.ReadFile("persistent") if os.IsNotExist(err) { return make(map[string]json.RawMessage), nil } if err != nil { return nil, err } var root map[string]json.RawMessage if err := json.Unmarshal(raw, &root); err != nil || root == nil { return nil, fmt.Errorf("invalid persistent object; CW save refused") } return root, nil } // CommitCurrencyWarsSnapshot updates only this UID's CW value, sharing the // existing cross-process lock and atomic writer. expectedRevision prevents a // stale game session from overwriting a later run. The caller must validate // the protocol payload before committing; reads do not perform migration. func CommitCurrencyWarsSnapshot(expectedRevision uint64, next CurrencyWarsSnapshot) (uint64, error) { if err := next.validate(); err != nil { return 0, err } persistentWriteMu.Lock() defer persistentWriteMu.Unlock() unlock, err := lockPersistentFile() if err != nil { return 0, err } defer unlock() root, err := readCurrencyWarsPersistentRoot() if err != nil { return 0, err } accounts := make(map[string]json.RawMessage) if raw, exists := root["currency_wars"]; exists { if err := json.Unmarshal(raw, &accounts); err != nil || accounts == nil { return 0, fmt.Errorf("invalid CW account store; save refused") } } key := strconv.FormatUint(uint64(next.UID), 10) var revision uint64 if raw, exists := accounts[key]; exists { var previous CurrencyWarsSnapshot if err := json.Unmarshal(raw, &previous); err != nil { return 0, err } if err := previous.validate(); err != nil { return 0, err } if previous.UID != next.UID || previous.ClientVersion != next.ClientVersion || previous.AssemblySHA256 != next.AssemblySHA256 || previous.Revision == 0 { return 0, fmt.Errorf("CW snapshot source differs; migration is required") } revision = previous.Revision } if revision != expectedRevision { return 0, ErrCurrencyWarsSaveConflict } if revision == ^uint64(0) { return 0, fmt.Errorf("CW snapshot revision exhausted") } next.Revision = revision + 1 accounts[key], err = json.Marshal(next) if err != nil { return 0, err } root["currency_wars"], err = json.Marshal(accounts) if err != nil { return 0, err } if err := writeJSONAtomic("persistent", root); err != nil { return 0, err } return next.Revision, nil } func NewCurrencyWarsSnapshot(uid uint32, version, assemblySHA string, wire []byte) CurrencyWarsSnapshot { return CurrencyWarsSnapshot{FormatVersion: currencyWarsSaveFormat, UID: uid, ClientVersion: version, AssemblySHA256: assemblySHA, WireData: append([]byte(nil), wire...)} }