chore: checkpoint stable battle and eidolon progression
This commit is contained in:
1 parent
365e66ffe0
commit
a681affdb3
23 files changed
+34511
-11310
No files matched your search
@@ -0,0 +1,198 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var persistentWriteMu sync.Mutex
|
||||
|
||||
func ReadPersistent() (*Persistent, error) {
|
||||
raw, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p Persistent
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return nil, fmt.Errorf("parse persistent: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func jsonObject(value any) (map[string]any, error) {
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var object map[string]any
|
||||
err = json.Unmarshal(raw, &object)
|
||||
return object, err
|
||||
}
|
||||
|
||||
func (f *FreesrData) persistentSnapshot() Persistent {
|
||||
ex := make(map[uint32]bool)
|
||||
for id := range AvatarExRankMap {
|
||||
if avatar := f.Avatars[id]; avatar != nil {
|
||||
ex[id] = avatar.ExRankUnlocked
|
||||
}
|
||||
}
|
||||
return Persistent{Lineups: f.Lineups, Position: f.Position, Scene: f.Scene,
|
||||
MainCharacter: f.MainCharacter, MarchType: f.MarchType,
|
||||
EnableSWGlobal: f.EnableSWGlobal, EnableCastoriceGlobal: f.EnableCastoriceGlobal,
|
||||
EnableAhaGlobal: f.EnableAhaGlobal, DressedSkins: f.DressedSkins, ExRanksUnlocked: ex}
|
||||
}
|
||||
|
||||
// Apply only locally changed fields. A stale game session saving its position
|
||||
// must not undo an EX switch written by the SDK process. Nested maps are merged
|
||||
// per key so different avatars' switches do not overwrite one another.
|
||||
func mergePersistentChanges(existing, baseline, next map[string]any) {
|
||||
for key, value := range next {
|
||||
old, known := baseline[key]
|
||||
if known && reflect.DeepEqual(old, value) {
|
||||
continue
|
||||
}
|
||||
newMap, newOK := value.(map[string]any)
|
||||
oldMap, oldOK := old.(map[string]any)
|
||||
if newOK && oldOK {
|
||||
dest, ok := existing[key].(map[string]any)
|
||||
if !ok {
|
||||
dest = make(map[string]any)
|
||||
}
|
||||
mergePersistentChanges(dest, oldMap, newMap)
|
||||
for removed := range oldMap {
|
||||
if _, present := newMap[removed]; !present {
|
||||
delete(dest, removed)
|
||||
}
|
||||
}
|
||||
existing[key] = dest
|
||||
} else {
|
||||
existing[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONAtomic(path string, value any) error {
|
||||
raw, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.CreateTemp(".", ".gac-save-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := file.Name()
|
||||
defer os.Remove(name)
|
||||
if _, err := file.Write(raw); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, path)
|
||||
}
|
||||
|
||||
func (f *FreesrData) SavePersistentChecked() error {
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
next, err := jsonObject(f.persistentSnapshot())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing := make(map[string]any)
|
||||
if raw, err := os.ReadFile("persistent"); err == nil {
|
||||
if err := json.Unmarshal(raw, &existing); err != nil {
|
||||
return fmt.Errorf("refuse to overwrite invalid persistent: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
mergePersistentChanges(existing, f.persistentBaseline, next)
|
||||
if err := writeJSONAtomic("persistent", existing); err != nil {
|
||||
return err
|
||||
}
|
||||
// Track our own observed snapshot, not external changes we have not loaded.
|
||||
f.persistentBaseline = next
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshExRanks imports persisted switches without replacing inventory,
|
||||
// lineup, scene or the configured battle. Call under the session state lock.
|
||||
func (f *FreesrData) RefreshExRanks(p *Persistent) []uint32 {
|
||||
var changed []uint32
|
||||
for id := range AvatarExRankMap {
|
||||
avatar := f.Avatars[id]
|
||||
if avatar == nil {
|
||||
continue
|
||||
}
|
||||
unlocked := p.ExRanksUnlocked[id]
|
||||
if avatar.ExRankUnlocked != unlocked {
|
||||
avatar.ExRankUnlocked = unlocked
|
||||
changed = append(changed, id)
|
||||
}
|
||||
}
|
||||
if current, err := jsonObject(f.persistentSnapshot()); err == nil {
|
||||
if f.persistentBaseline == nil {
|
||||
f.persistentBaseline = make(map[string]any)
|
||||
}
|
||||
f.persistentBaseline["ex_ranks_unlocked"] = current["ex_ranks_unlocked"]
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// Patch only the selected avatar's rank in the latest import. Unknown FreeSR
|
||||
// fields and all inventory IDs/affixes retain their original JSON values.
|
||||
func SaveAvatarRank(id, rank uint32) error {
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &root); err != nil {
|
||||
return err
|
||||
}
|
||||
var avatars map[uint32]json.RawMessage
|
||||
if err := json.Unmarshal(root["avatars"], &avatars); err != nil {
|
||||
return err
|
||||
}
|
||||
var avatar map[string]json.RawMessage
|
||||
if err := json.Unmarshal(avatars[id], &avatar); err != nil {
|
||||
return err
|
||||
}
|
||||
var data map[string]json.RawMessage
|
||||
if err := json.Unmarshal(avatar["data"], &data); err != nil {
|
||||
return err
|
||||
}
|
||||
data["rank"], err = json.Marshal(rank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatar["data"], err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatars[id], err = json.Marshal(avatar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root["avatars"], err = json.Marshal(avatars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
Reference in new issue
Block a user