// Package save läser och skriver sparfilen: kunskap, plånbok, debuffar, // progression och den pågående omgångens bana + seed. JSON med // versionsfält så att gamla filer kan migreras. package save import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "time" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim" ) // Version är sparfilens formatversion. const Version = 1 // AppDir är mappnamnet under användarens konfigmapp. const AppDir = "metro-puzzle-jp" // RunRef pekar ut en pågående omgång så att den kan återupptas med samma // slump (minispel, händelser, läsning). type RunRef struct { LevelID string `json:"levelId"` Seed [2]uint64 `json:"seed"` Started time.Time `json:"started"` } // State är allt som sparas. type State struct { Version int `json:"version"` SavedAt time.Time `json:"savedAt"` Know *lang.Knowledge `json:"knowledge"` Wallet *sim.Wallet `json:"wallet"` Profile *sim.Profile `json:"profile"` Progress *progress.Progress `json:"progress"` Current *RunRef `json:"current,omitempty"` } // New är ett nytt spel. func New() *State { return &State{Version: Version, Know: lang.NewKnowledge(), Wallet: sim.NewWallet(), Profile: &sim.Profile{}, Progress: progress.New()} } // DefaultPath är sparfilens plats: ~/.config/metro-puzzle-jp/save.json på // Linux, %AppData%\metro-puzzle-jp\save.json på Windows. func DefaultPath() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, AppDir, "save.json"), nil } // Load läser sparfilen. Saknas den returneras ett nytt spel (och exists=false). func Load(path string) (st *State, exists bool, err error) { b, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { return New(), false, nil } if err != nil { return nil, false, err } var s State if err := json.Unmarshal(b, &s); err != nil { return nil, true, fmt.Errorf("sparfilen %s är trasig: %w", path, err) } if err := migrate(&s); err != nil { return nil, true, err } return &s, true, nil } // migrate lyfter äldre versioner till den aktuella och fyller i luckor. func migrate(s *State) error { if s.Version > Version { return fmt.Errorf("sparfilen är från en nyare version (%d > %d)", s.Version, Version) } // Version 0 (saknat fält) och 1 har samma form; här läggs framtida steg. s.Version = Version if s.Know == nil { s.Know = lang.NewKnowledge() } if s.Know.Words == nil { s.Know.Words = map[string]bool{} } if s.Know.Progress == nil { s.Know.Progress = map[string]int{} } if s.Wallet == nil { s.Wallet = sim.NewWallet() } if s.Profile == nil { s.Profile = &sim.Profile{} } if s.Progress == nil { s.Progress = progress.New() } return nil } // Store skriver sparfilen atomiskt (tempfil + byte av namn). func Store(path string, s *State) error { s.Version = Version s.SavedAt = time.Now() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } b, err := json.MarshalIndent(s, "", " ") if err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, path) }