package gameserver import ( "encoding/json" "fmt" "sort" "sync" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) type eidolonResources struct { Avatars map[uint32]struct { MaxRank uint32 `json:"max_rank"` Ranks []struct { Rank uint32 `json:"rank"` Cost []struct { ItemID uint32 ItemNum uint32 } `json:"cost"` } `json:"ranks"` } `json:"avatars"` ExMaterials map[uint32]struct { ItemID uint32 `json:"item_id"` Count uint32 `json:"count"` } `json:"ex_materials"` } var eidolonOnce sync.Once var eidolonData eidolonResources var eidolonError error func loadEidolonResources() (*eidolonResources, error) { eidolonOnce.Do(func() { raw, err := common.ReadResourceFile("resources/EidolonConfig.json", "EidolonConfig.json") if err != nil { eidolonError = err return } if err := json.Unmarshal(raw, &eidolonData); err != nil { eidolonError = err return } for id, avatar := range eidolonData.Avatars { if id == 0 || avatar.MaxRank == 0 || len(avatar.Ranks) != int(avatar.MaxRank) { eidolonError = fmt.Errorf("invalid Eidolon ranks for avatar %d", id) return } seen := map[uint32]bool{} for _, rank := range avatar.Ranks { if rank.Rank == 0 || rank.Rank > avatar.MaxRank || seen[rank.Rank] || len(rank.Cost) == 0 { eidolonError = fmt.Errorf("invalid rank costs for avatar %d", id) return } seen[rank.Rank] = true for _, cost := range rank.Cost { if cost.ItemID == 0 || cost.ItemNum == 0 { eidolonError = fmt.Errorf("empty rank cost for avatar %d", id) return } } } } for id, material := range eidolonData.ExMaterials { if common.AvatarExRankMap[id] == 0 || material.ItemID == 0 || material.Count == 0 { eidolonError = fmt.Errorf("invalid EX material for avatar %d", id) return } } }) if eidolonError != nil { return nil, fmt.Errorf("load EidolonConfig.json: %w", eidolonError) } return &eidolonData, nil } // The configured sandbox grant covers the remaining ranks. Its remaining // quantity follows saved progression, so reconnecting does not refill used ranks. func eidolonMaterials(player *common.FreesrData) ([]*goproto.Material, error) { resources, err := loadEidolonResources() if err != nil { return nil, err } counts := map[uint32]uint32{} for id, rules := range resources.Avatars { // Rank-up materials belong to the catalog, not to the currently owned // avatar list. Include every configured avatar so the player can obtain // its Eidolon materials before unlocking that character. var currentRank uint32 if avatar := player.Avatars[id]; avatar != nil { currentRank = avatar.Data.Rank } for _, rank := range rules.Ranks { for _, cost := range rank.Cost { if _, present := counts[cost.ItemID]; !present { counts[cost.ItemID] = 0 } if rank.Rank > currentRank { counts[cost.ItemID] += cost.ItemNum } } } } for id, grant := range resources.ExMaterials { avatar := player.Avatars[id] if avatar == nil { continue } counts[grant.ItemID] = 0 if !avatar.ExRankUnlocked { counts[grant.ItemID] = grant.Count } } // Explicit inventory quantities are separate from progression. Preserve // retained stones (and explicit zero) through rank/EX notifications too. for id, count := range player.Materials { if _, known := counts[id]; known { counts[id] = count } } ids := make([]uint32, 0, len(counts)) for id := range counts { ids = append(ids, id) } sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) var materials []*goproto.Material for _, id := range ids { materials = append(materials, &goproto.Material{Tid: id, Num: counts[id]}) } return materials, nil }