pre cw with probe and a bunch script to read game behavior
This commit is contained in:
1 parent
5120219ae2
commit
da8a32f60d
310 files changed
+219693
-872
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -39,10 +40,10 @@ func (f *FreesrData) persistentSnapshot() Persistent {
|
||||
ex[id] = avatar.ExRankUnlocked
|
||||
}
|
||||
}
|
||||
return Persistent{TeamPresets: f.TeamPresets, ActiveTeamPreset: f.ActiveTeamPreset, TierceProgress: f.TierceProgress, TechniqueFoodStock: f.TechniqueFoodStock, AutoTechniqueFood: f.AutoTechniqueFood, Lineups: f.Lineups, Position: f.Position, Scene: f.Scene,
|
||||
return Persistent{Profile: f.Profile, TeamPresets: f.TeamPresets, ActiveTeamPreset: f.ActiveTeamPreset, TierceProgress: f.TierceProgress, TechniqueFoodStock: f.TechniqueFoodStock, AutoTechniqueFood: f.AutoTechniqueFood, 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}
|
||||
EnableAhaGlobal: f.EnableAhaGlobal, DressedSkins: f.DressedSkins, PlayerOutfits: f.PlayerOutfits, SceneGroupProperties: f.SceneGroupProperties, ExRanksUnlocked: ex}
|
||||
}
|
||||
|
||||
// Apply only locally changed fields. A stale game session saving its position
|
||||
@@ -196,3 +197,209 @@ func SaveAvatarRank(id, rank uint32) error {
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
|
||||
// SaveEquipmentAssignments patches only equip_avatar on the explicitly changed
|
||||
// inventory entries. It preserves all other equipment, relic stats and unknown
|
||||
// FreeSR fields, so an in-game equip action never replaces the full inventory.
|
||||
func SaveEquipmentAssignments(lightcones, relics map[uint32]uint32) error {
|
||||
if len(lightcones) == 0 && len(relics) == 0 {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
for _, collection := range []struct {
|
||||
field string
|
||||
owners map[uint32]uint32
|
||||
}{{"lightcones", lightcones}, {"relics", relics}} {
|
||||
if len(collection.owners) == 0 {
|
||||
continue
|
||||
}
|
||||
var entries []json.RawMessage
|
||||
if err := json.Unmarshal(root[collection.field], &entries); err != nil {
|
||||
return fmt.Errorf("read %s: %w", collection.field, err)
|
||||
}
|
||||
found := make(map[uint32]bool, len(collection.owners))
|
||||
for i, entry := range entries {
|
||||
var item map[string]json.RawMessage
|
||||
if err := json.Unmarshal(entry, &item); err != nil {
|
||||
return fmt.Errorf("decode %s entry: %w", collection.field, err)
|
||||
}
|
||||
var uid uint32
|
||||
if err := json.Unmarshal(item["internal_uid"], &uid); err != nil {
|
||||
return fmt.Errorf("read %s internal_uid: %w", collection.field, err)
|
||||
}
|
||||
owner, update := collection.owners[uid]
|
||||
if !update {
|
||||
continue
|
||||
}
|
||||
item["equip_avatar"], err = json.Marshal(owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries[i], err = json.Marshal(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found[uid] = true
|
||||
}
|
||||
for uid := range collection.owners {
|
||||
if !found[uid] {
|
||||
return fmt.Errorf("%s item internal_uid %d not found", collection.field, uid)
|
||||
}
|
||||
}
|
||||
root[collection.field], err = json.Marshal(entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
|
||||
// SaveNewLightcones appends catalog entries that are absent from the latest
|
||||
// import while preserving existing inventory records and unknown fields.
|
||||
func SaveNewLightcones(additions []Lightcone) error {
|
||||
if len(additions) == 0 {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
entries, err := readInventoryEntries(root, "lightcones")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read lightcones: %w", err)
|
||||
}
|
||||
var existing []Lightcone
|
||||
for _, entry := range entries {
|
||||
var item Lightcone
|
||||
if err := json.Unmarshal(entry, &item); err != nil {
|
||||
return fmt.Errorf("decode lightcone: %w", err)
|
||||
}
|
||||
existing = append(existing, item)
|
||||
}
|
||||
usedIDs := make(map[uint32]bool, len(existing))
|
||||
usedUIDs := make(map[uint32]bool, len(existing))
|
||||
for _, item := range existing {
|
||||
usedIDs[item.ItemID] = true
|
||||
usedUIDs[item.InternalUID] = true
|
||||
}
|
||||
for _, item := range additions {
|
||||
if usedIDs[item.ItemID] || usedUIDs[item.InternalUID] {
|
||||
continue
|
||||
}
|
||||
encoded, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, encoded)
|
||||
usedIDs[item.ItemID] = true
|
||||
usedUIDs[item.InternalUID] = true
|
||||
}
|
||||
root["lightcones"], err = json.Marshal(entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
|
||||
// SaveNewRelics appends catalog instances absent from the latest import.
|
||||
func SaveNewRelics(additions []Relic) error {
|
||||
if len(additions) == 0 {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
entries, err := readInventoryEntries(root, "relics")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read relics: %w", err)
|
||||
}
|
||||
var existing []Relic
|
||||
for _, entry := range entries {
|
||||
var item Relic
|
||||
if err := json.Unmarshal(entry, &item); err != nil {
|
||||
return fmt.Errorf("decode relic: %w", err)
|
||||
}
|
||||
existing = append(existing, item)
|
||||
}
|
||||
usedIDs := make(map[uint32]bool, len(existing))
|
||||
usedUIDs := make(map[uint32]bool, len(existing))
|
||||
for _, item := range existing {
|
||||
usedIDs[item.RelicID] = true
|
||||
usedUIDs[item.InternalUID] = true
|
||||
}
|
||||
for _, item := range additions {
|
||||
if usedIDs[item.RelicID] || usedUIDs[item.InternalUID] {
|
||||
continue
|
||||
}
|
||||
encoded, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, encoded)
|
||||
usedIDs[item.RelicID] = true
|
||||
usedUIDs[item.InternalUID] = true
|
||||
}
|
||||
root["relics"], err = json.Marshal(entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
|
||||
// Empty initial saves may contain account settings without inventory arrays.
|
||||
// Treat absent or null collections as empty, while still rejecting malformed
|
||||
// collections so a bad import cannot be silently replaced.
|
||||
func readInventoryEntries(root map[string]json.RawMessage, field string) ([]json.RawMessage, error) {
|
||||
if root == nil {
|
||||
return nil, fmt.Errorf("inventory data must be a JSON object")
|
||||
}
|
||||
raw, ok := root[field]
|
||||
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
||||
return []json.RawMessage{}, nil
|
||||
}
|
||||
var entries []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []json.RawMessage{}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
Reference in new issue
Block a user