pre cw with probe and a bunch script to read game behavior

This commit is contained in:
CAtlas0784 committed 2026-10-21 20:40:13 +07:00
1 parent 5120219ae2
commit da8a32f60d
310 files changed
+219693 -872

No files matched your search

+209 -2
View File
@@ -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
}