package sdkserver import ( "encoding/json" "fmt" "os" "gac-hkrpg-go/internal/common" ) // POST patches fields; PUT explicitly replaces a complete player document. // Keep raw JSON values so unrelated IDs and numeric values are not rounded. func preparePlayerDataUpdate(existing, payload []byte, replace bool) ([]byte, error) { var patch map[string]json.RawMessage if err := json.Unmarshal(payload, &patch); err != nil || patch == nil { return nil, fmt.Errorf("player data must be a JSON object") } result := make(map[string]json.RawMessage) if replace { for _, key := range []string{"avatars", "lightcones", "relics", "battle_config"} { if _, present := patch[key]; !present { return nil, fmt.Errorf("full replacement is missing %s; use POST for a partial update", key) } } // FreeSR exports do not necessarily include the inventory editor's // material overrides. Omission preserves them; an explicit materials // object (including zero counts or {}) still replaces that field. if _, present := patch["materials"]; !present && len(existing) != 0 { var current map[string]json.RawMessage if err := json.Unmarshal(existing, ¤t); err != nil || current == nil { return nil, fmt.Errorf("existing player data is invalid; restore it before applying updates") } if value, present := current["materials"]; present { result["materials"] = value } } } else if err := json.Unmarshal(existing, &result); err != nil || result == nil { return nil, fmt.Errorf("existing player data is invalid; restore it before applying updates") } for key, value := range patch { if key == "avatars" && !replace { var incoming map[string]json.RawMessage if err := json.Unmarshal(value, &incoming); err != nil || len(incoming) == 0 { return nil, fmt.Errorf("avatars update must be a non-empty object") } current := make(map[string]json.RawMessage) if raw, found := result[key]; found { if err := json.Unmarshal(raw, ¤t); err != nil || current == nil { return nil, fmt.Errorf("existing avatars data is invalid") } } for id, avatar := range incoming { current[id] = avatar } merged, err := json.Marshal(current) if err != nil { return nil, err } result[key] = merged } else { result[key] = value } } out, err := json.MarshalIndent(result, "", " ") if err != nil { return nil, err } var data common.FreesrData if err := json.Unmarshal(out, &data); err != nil { return nil, fmt.Errorf("invalid player data: %w", err) } if err := common.ValidateAvatarRoster(data.Avatars); err != nil { return nil, err } if saved, err := common.ReadPersistent(); err == nil { if err := common.ValidateAdventureLineup(data.Avatars, saved.Lineups); err != nil { return nil, err } } else if !os.IsNotExist(err) { return nil, fmt.Errorf("cannot validate active lineup: %w", err) } return out, nil } func writePlayerDataAtomic(data []byte) error { file, err := os.CreateTemp(".", ".gac-freesr-*.tmp") if err != nil { return err } name := file.Name() defer os.Remove(name) if _, err := file.Write(data); err != nil { file.Close() return err } if err := file.Sync(); err != nil { file.Close() return err } if err := file.Close(); err != nil { return err } return os.Rename(name, "freesr-data.json") }