// Package common ports the Rust `common` crate: the JSON data model shared by // the sdkserver (srtools upload), the file watcher and the gameserver. JSON // field names match the original FreeSR-format files byte-for-byte so the // existing freesr-data.json / persistent / res.json / teleports.json keep // working without conversion. package common import ( "encoding/json" "fmt" "os" "sort" ) // --------------------------------------------------------------------------- // MultiPathAvatar — string-serialized C-like enum in JSON, numeric in logic. // --------------------------------------------------------------------------- type MultiPathAvatar uint32 const ( MultiPathUnk MultiPathAvatar = 0 MultiPathMarchPreservation MultiPathAvatar = 1001 MultiPathMarchHunt MultiPathAvatar = 1224 MultiPathMalePhysical MultiPathAvatar = 8001 MultiPathFemalePhysical MultiPathAvatar = 8002 MultiPathMalePreservation MultiPathAvatar = 8003 MultiPathFemalePreservation MultiPathAvatar = 8004 MultiPathMaleHarmony MultiPathAvatar = 8005 MultiPathFemaleHarmony MultiPathAvatar = 8006 MultiPathMaleRemembrance MultiPathAvatar = 8007 MultiPathFemaleRemembrance MultiPathAvatar = 8008 MultiPathMaleElation MultiPathAvatar = 8009 MultiPathFemaleElation MultiPathAvatar = 8010 ) var multiPathByName = map[string]MultiPathAvatar{ "MalePyhsical": MultiPathMalePhysical, // typo kept from the Rust enum "FemalePhysical": MultiPathFemalePhysical, "MalePreservation": MultiPathMalePreservation, "FemalePreservation": MultiPathFemalePreservation, "MaleHarmony": MultiPathMaleHarmony, "FemaleHarmony": MultiPathFemaleHarmony, "MaleRemembrance": MultiPathMaleRemembrance, "FemaleRemembrance": MultiPathFemaleRemembrance, "MaleElation": MultiPathMaleElation, "FemaleElation": MultiPathFemaleElation, "MarchHunt": MultiPathMarchHunt, "MarchPreservation": MultiPathMarchPreservation, "Unk": MultiPathUnk, } var multiPathByID = map[uint32]MultiPathAvatar{ 8001: MultiPathMalePhysical, 8002: MultiPathFemalePhysical, 8003: MultiPathMalePreservation, 8004: MultiPathFemalePreservation, 8005: MultiPathMaleHarmony, 8006: MultiPathFemaleHarmony, 8007: MultiPathMaleRemembrance, 8008: MultiPathFemaleRemembrance, 8009: MultiPathMaleElation, 8010: MultiPathFemaleElation, 1224: MultiPathMarchHunt, 1001: MultiPathMarchPreservation, } // MultiPathAvatarFromID mirrors Rust From (unknown -> Unk). func MultiPathAvatarFromID(v uint32) MultiPathAvatar { if m, ok := multiPathByID[v]; ok { return m } return MultiPathUnk } // ID mirrors Rust From for u32 (Unk -> 8006). func (m MultiPathAvatar) ID() uint32 { if m == MultiPathUnk { return 8006 } return uint32(m) } func (m MultiPathAvatar) String() string { for name, v := range multiPathByName { if v == m { return name } } return "Unk" } func (m MultiPathAvatar) MarshalJSON() ([]byte, error) { return json.Marshal(m.String()) } func (m *MultiPathAvatar) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { // numeric fallback var n uint32 if err2 := json.Unmarshal(data, &n); err2 == nil { *m = MultiPathAvatarFromID(n) return nil } return err } v, ok := multiPathByName[s] if !ok { v = MultiPathUnk } *m = v return nil } // MultiPathAvatarType in the loaded schema uses the avatar id itself as the // enum value (Mar7thKnightType=1001, BoyWarriorType=8001, ...), so the proto // value is simply the numeric id (0 for Unk). func (m MultiPathAvatar) ProtoType() uint32 { return uint32(m) } func IsMainCharacter(avatarID uint32) bool { return avatarID >= 8001 && avatarID <= 8010 } func IsMarch7th(avatarID uint32) bool { return avatarID == 1001 || avatarID == 1224 } func BaseAvatarID(avatarID uint32) uint32 { if IsMainCharacter(avatarID) { return 8001 } if IsMarch7th(avatarID) { return 1001 } return avatarID } // --------------------------------------------------------------------------- // Item structs (freesr-data.json) // --------------------------------------------------------------------------- type Lightcone struct { Level uint32 `json:"level"` ItemID uint32 `json:"item_id"` EquipAvatar uint32 `json:"equip_avatar"` Rank uint32 `json:"rank"` Promotion uint32 `json:"promotion"` InternalUID uint32 `json:"internal_uid"` } type SubAffix struct { SubAffixID uint32 `json:"sub_affix_id"` Count uint32 `json:"count"` Step uint32 `json:"step"` } type Relic struct { Level uint32 `json:"level"` RelicID uint32 `json:"relic_id"` MainAffixID uint32 `json:"main_affix_id"` SubAffixes []SubAffix `json:"sub_affixes"` InternalUID uint32 `json:"internal_uid"` EquipAvatar uint32 `json:"equip_avatar"` } func (r *Relic) Slot() uint32 { return r.RelicID % 10 } func (r *Relic) UniqueID() uint32 { return r.InternalUID } func (l *Lightcone) UniqueID() uint32 { return 3000 + l.InternalUID } // --------------------------------------------------------------------------- // Avatar (freesr-data.json) // --------------------------------------------------------------------------- type AvatarData struct { Rank uint32 `json:"rank"` Skills map[uint32]uint32 `json:"skills"` SkillsByAnchorType map[uint32]uint32 `json:"skills_by_anchor_type"` } type AvatarJson struct { AvatarID uint32 `json:"avatar_id"` Data AvatarData `json:"data"` Level uint32 `json:"level"` Promotion uint32 `json:"promotion"` Techniques []uint32 `json:"techniques"` SPValue *uint32 `json:"sp_value"` SPMax *uint32 `json:"sp_max"` EnhancedID *uint32 `json:"enhanced_id"` ExRankUnlocked bool `json:"ex_rank_unlocked,omitempty"` } func (a *AvatarJson) SPCur() uint32 { if a.SPValue != nil { return *a.SPValue } return a.SPMaxVal() / 2 } func (a *AvatarJson) SPMaxVal() uint32 { if a.SPMax != nil { return *a.SPMax } return 10000 } // --------------------------------------------------------------------------- // Battle config (freesr-data.json) // --------------------------------------------------------------------------- type BattleType string const ( BattleTypeDefault BattleType = "Default" BattleTypeMOC BattleType = "MOC" BattleTypePF BattleType = "PF" BattleTypeSU BattleType = "SU" BattleTypeAS BattleType = "AS" BattleTypeAA BattleType = "AA" ) type Monster struct { Level uint32 `json:"level"` MonsterID uint32 `json:"monster_id"` MaxHP uint32 `json:"max_hp"` } type DynamicKey struct { Key string `json:"key"` Value uint32 `json:"value"` } type BattleBuffJson struct { Level uint32 `json:"level"` ID uint32 `json:"id"` DynamicKey *DynamicKey `json:"dynamic_key"` DynamicValues []DynamicKey `json:"dynamic_values"` } type RogueMagicComponentType uint32 const ( RogueMagicPassive RogueMagicComponentType = 3 RogueMagicActive RogueMagicComponentType = 4 RogueMagicAttach RogueMagicComponentType = 5 ) type RogueMagicComponent struct { ID uint32 `json:"id"` Level uint32 `json:"level"` ComponentType RogueMagicComponentType `json:"component_type"` } type RogueMagicScepter struct { Level uint32 `json:"level"` ID uint32 `json:"id"` Components []RogueMagicComponent `json:"components"` } type BattleConfig struct { BattleType BattleType `json:"battle_type"` Monsters [][]Monster `json:"monsters"` Blessings []BattleBuffJson `json:"blessings"` StageID uint32 `json:"stage_id"` CycleCount uint32 `json:"cycle_count"` PathResonanceID uint32 `json:"path_resonance_id"` CustomStats []SubAffix `json:"custom_stats"` Scepters []RogueMagicScepter `json:"scepters"` CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"` } func DefaultBattleConfig() BattleConfig { return BattleConfig{ BattleType: BattleTypeDefault, Monsters: [][]Monster{{{Level: 60, MonsterID: 3014022}}}, StageID: 201012311, } } // --------------------------------------------------------------------------- // Persistent (file: "persistent") // --------------------------------------------------------------------------- type Position struct { X int32 `json:"x"` Y int32 `json:"y"` Z int32 `json:"z"` RotY int32 `json:"rot_y"` } func DefaultPosition() Position { return Position{X: -26968, Y: 78953, Z: 14457, RotY: 11858} } type Scene struct { PlaneID uint32 `json:"plane_id"` FloorID uint32 `json:"floor_id"` EntryID uint32 `json:"entry_id"` } func DefaultScene() Scene { return Scene{PlaneID: 20411, FloorID: 20411001, EntryID: 2041101} } type Persistent struct { Lineups map[uint32]uint32 `json:"lineups"` Position Position `json:"position"` Scene Scene `json:"scene"` MainCharacter MultiPathAvatar `json:"main_character"` MarchType MultiPathAvatar `json:"march_type"` EnableSWGlobal *bool `json:"enable_sw_global"` EnableCastoriceGlobal *bool `json:"enable_castorice_global"` EnableAhaGlobal *bool `json:"enable_aha_global"` DressedSkins map[uint32]uint32 `json:"dressed_skins"` ExRanksUnlocked map[uint32]bool `json:"ex_ranks_unlocked"` } // DefaultPersistent mirrors the Rust Default impl. func DefaultPersistent() Persistent { sw := true cg := true aha := true return Persistent{ Lineups: map[uint32]uint32{0: 1313, 1: 1006, 2: 8001, 3: 1405}, Position: DefaultPosition(), MainCharacter: MultiPathFemaleRemembrance, Scene: DefaultScene(), MarchType: MultiPathMarchHunt, EnableSWGlobal: &sw, EnableCastoriceGlobal: &cg, EnableAhaGlobal: &aha, DressedSkins: map[uint32]uint32{}, ExRanksUnlocked: map[uint32]bool{}, } } // --------------------------------------------------------------------------- // FreesrData // --------------------------------------------------------------------------- type FreesrData struct { Lightcones []Lightcone `json:"lightcones"` Relics []Relic `json:"relics"` Avatars map[uint32]*AvatarJson `json:"avatars"` BattleConfig BattleConfig `json:"battle_config"` // Fields stored in "persistent", merged on load (not serialized here). Lineups map[uint32]uint32 `json:"-"` Position Position `json:"-"` Scene Scene `json:"-"` MainCharacter MultiPathAvatar `json:"-"` MarchType MultiPathAvatar `json:"-"` EnableSWGlobal *bool `json:"-"` EnableCastoriceGlobal *bool `json:"-"` EnableAhaGlobal *bool `json:"-"` DressedSkins map[uint32]uint32 `json:"-"` } // LoadFreesrData reads freesr-data.json + persistent and merges them, // mirroring FreesrData::load() from the Rust crate. func LoadFreesrData() (*FreesrData, error) { raw, err := os.ReadFile("freesr-data.json") if err != nil { return nil, fmt.Errorf("failed to read freesr-data.json: %w", err) } var data FreesrData if err := json.Unmarshal(raw, &data); err != nil { return nil, fmt.Errorf("freesr-data.json is broken: %w, pls redownload", err) } if data.Avatars == nil { data.Avatars = map[uint32]*AvatarJson{} } if data.BattleConfig.Monsters == nil && data.BattleConfig.StageID == 0 { data.BattleConfig = DefaultBattleConfig() } // persistent is optional pers, err := os.ReadFile("persistent") if err == nil { var p Persistent if err := json.Unmarshal(pers, &p); err == nil { data.Lineups = p.Lineups data.Position = p.Position data.Scene = p.Scene data.MainCharacter = p.MainCharacter data.MarchType = p.MarchType data.EnableSWGlobal = p.EnableSWGlobal data.EnableCastoriceGlobal = p.EnableCastoriceGlobal data.EnableAhaGlobal = p.EnableAhaGlobal data.DressedSkins = p.DressedSkins for aid, unlocked := range p.ExRanksUnlocked { if av, ok := data.Avatars[aid]; ok { av.ExRankUnlocked = unlocked } } } } if data.DressedSkins == nil { data.DressedSkins = map[uint32]uint32{} } // remove unequipped lightcones / relics (keeps huge FreeSR dumps usable) if len(data.Lightcones) > 1500 { kept := data.Lightcones[:0] for _, v := range data.Lightcones { if v.EquipAvatar != 0 { kept = append(kept, v) } } data.Lightcones = kept } if len(data.Relics) > 3000 { kept := data.Relics[:0] for _, v := range data.Relics { if v.EquipAvatar != 0 { kept = append(kept, v) } } data.Relics = kept } data.VerifyLineup() if data.MarchType.ID() > 8000 { data.MarchType = MultiPathMarchHunt } return &data, nil } // SortedLineupKeys returns lineup slots in ascending order (BTreeMap order). func (f *FreesrData) SortedLineupKeys() []uint32 { keys := make([]uint32, 0, len(f.Lineups)) for k := range f.Lineups { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) return keys } // VerifyLineup pads the lineup to 4 slots, defaulting to {0:8001}, then saves. func (f *FreesrData) VerifyLineup() { if len(f.Lineups) == 0 { f.Lineups = map[uint32]uint32{0: 8001, 1: 0, 2: 0, 3: 0} } else if len(f.Lineups) < 4 { for i := uint32(len(f.Lineups)); i < 4; i++ { if _, ok := f.Lineups[i]; !ok { f.Lineups[i] = 0 } } } f.SavePersistent() } // SavePersistent writes the persistent subset back to the "persistent" file. func (f *FreesrData) SavePersistent() { exUnlocked := make(map[uint32]bool) for aid, av := range f.Avatars { if av.ExRankUnlocked { exUnlocked[aid] = true } } p := 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: exUnlocked, } out, err := json.MarshalIndent(p, "", " ") if err != nil { return } if err := os.WriteFile("persistent", out, 0o644); err == nil { LogInfo("persistent saved") } } // FindLightconeFor returns the lightcone equipped by the given avatar (first match). func (f *FreesrData) FindLightconeFor(avatarID uint32) *Lightcone { for i := range f.Lightcones { if f.Lightcones[i].EquipAvatar == avatarID { return &f.Lightcones[i] } } return nil } // FindRelicsFor returns the relics equipped by the given avatar. func (f *FreesrData) FindRelicsFor(avatarID uint32) []*Relic { var out []*Relic for i := range f.Relics { if f.Relics[i].EquipAvatar == avatarID { out = append(out, &f.Relics[i]) } } return out } // BaseAvatarIDs contains all playable base avatar IDs var BaseAvatarIDs = []uint32{ 8001, 1001, 1002, 1003, 1004, 1005, 1006, 1008, 1009, 1013, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1217, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1312, 1315, 1310, 1314, 1218, 1221, 1220, 1222, 1223, 1317, 1313, 1225, 1402, 1401, 1404, 1403, 1405, 1407, 1406, 1409, 1014, 1015, 1408, 1410, 1412, 1413, 1414, 1415, 1321, 1501, 1502, 1503, 1504, 1505, 1506, 1507, 1508, 1509, 1510, 1511, 1512, 1513, }