Files

133 lines
3.7 KiB
Go

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
}