Files
GaCy-GO/internal/common/models.go
T
2026-10-28 18:54:43 +07:00

571 lines
19 KiB
Go

// 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"
"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<u32> (unknown -> Unk).
func MultiPathAvatarFromID(v uint32) MultiPathAvatar {
if m, ok := multiPathByID[v]; ok {
return m
}
return MultiPathUnk
}
// ID mirrors Rust From<MultiPathAvatar> 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"`
}
func (s *SubAffix) UnmarshalJSON(data []byte) error {
type Alias SubAffix
aux := &struct {
Cnt *uint32 `json:"cnt"`
*Alias
}{
Alias: (*Alias)(s),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Cnt != nil && s.Count == 0 {
s.Count = *aux.Cnt
}
return nil
}
type Relic struct {
Level uint32 `json:"level"`
RelicID uint32 `json:"relic_id"`
RelicSetID uint32 `json:"relic_set_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 SP is a ratio in ten-thousandths (PearlSR createBattleAvatar).
// FreeSR imports may instead carry absolute energy and its capacity; use
// their ratio so both absolute and already scaled imports serialize alike.
const battleRatioScale uint32 = 10000
func (a *AvatarJson) BattleSPCur() uint32 {
maximum := a.SPMaxVal()
if maximum == 0 {
return 0
}
current := a.SPCur()
if current > maximum {
current = maximum
}
return uint32(uint64(current) * uint64(battleRatioScale) / uint64(maximum))
}
// ---------------------------------------------------------------------------
// 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 {
WorldLevel *uint32 `json:"world_level,omitempty"`
EnableInvasionSupport bool `json:"enable_invasion_support"`
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}
}
// Tierce teams and best measured node results survive leaving a scene/login.
type TierceNodeProgress struct {
Score uint32 `json:"score"`
Won bool `json:"won"`
Result []byte `json:"result,omitempty"`
}
type TierceProgress struct {
Teams [][]uint32 `json:"teams"`
Buffs []uint32 `json:"buffs"`
Nodes map[uint32]TierceNodeProgress `json:"nodes"`
}
type TeamPreset struct {
Name string `json:"name"`
Avatars map[uint32]uint32 `json:"avatars"`
LeaderSlot uint32 `json:"leader_slot"`
Favourite bool `json:"favourite"`
}
type Persistent struct {
Profile ProfileSettings `json:"profile"`
TeamPresets map[uint32]*TeamPreset `json:"team_presets"`
ActiveTeamPreset uint32 `json:"active_team_preset"`
TierceProgress map[uint32]*TierceProgress `json:"tierce_progress"`
TechniqueFoodStock map[uint32]uint32 `json:"technique_food_stock"`
AutoTechniqueFood *bool `json:"auto_technique_food"`
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"`
PlayerOutfits []uint32 `json:"player_outfits"`
SceneGroupProperties map[uint32]map[uint32]map[string]int32 `json:"scene_group_properties"`
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 {
Profile ProfileSettings `json:"-"`
TeamPresets map[uint32]*TeamPreset `json:"-"`
ActiveTeamPreset uint32 `json:"-"`
TierceProgress map[uint32]*TierceProgress `json:"-"`
TechniqueFoodStock map[uint32]uint32 `json:"-"`
AutoTechniqueFood *bool `json:"-"`
persistentBaseline map[string]any
Lightcones []Lightcone `json:"lightcones"`
Relics []Relic `json:"relics"`
Materials map[uint32]uint32 `json:"materials,omitempty"`
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:"-"`
PlayerOutfits []uint32 `json:"-"`
SceneGroupProperties map[uint32]map[uint32]map[string]int32 `json:"-"`
}
// LoadFreesrData reads freesr-data.json + persistent and merges them,
// mirroring FreesrData::load() from the Rust crate.
func LoadFreesrData() (*FreesrData, error) {
raw, err := readResourceFile("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 err := ValidateAvatarRoster(data.Avatars); err != nil {
return nil, fmt.Errorf("invalid freesr-data.json: %w", err)
}
if data.BattleConfig.Monsters == nil && data.BattleConfig.StageID == 0 {
data.BattleConfig = DefaultBattleConfig()
}
// persistent is optional
pers, err := readResourceFile("persistent")
if err == nil {
var p Persistent
if err := json.Unmarshal(pers, &p); err == nil {
data.Profile = p.Profile
data.Lineups = p.Lineups
data.TeamPresets = p.TeamPresets
data.ActiveTeamPreset = p.ActiveTeamPreset
data.TierceProgress = p.TierceProgress
data.TechniqueFoodStock = p.TechniqueFoodStock
data.AutoTechniqueFood = p.AutoTechniqueFood
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
data.PlayerOutfits = p.PlayerOutfits
data.SceneGroupProperties = p.SceneGroupProperties
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{}
}
// Preserve every Light Cone copy, including unequipped gacha rewards.
// Dropping by inventory size would hide legitimately granted copies on login.
// Keep the existing relic-only import reduction separate.
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 err := ValidateAdventureLineup(data.Avatars, data.Lineups); err != nil {
return nil, fmt.Errorf("invalid active lineup: %w", err)
}
if data.MarchType.ID() > 8000 {
data.MarchType = MultiPathMarchHunt
}
data.persistentBaseline, err = jsonObject(data.persistentSnapshot())
if err != nil {
return nil, err
}
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 normalizes slots in memory. Reading a save must not rewrite it.
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
}
}
}
}
// SavePersistent writes the persistent subset back to the "persistent" file.
func (f *FreesrData) SavePersistent() {
if err := f.SavePersistentChecked(); err != nil {
LogInfo("[SAVE ERROR] %v", err)
}
}
// 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,
}