133 lines
3.7 KiB
Go
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
|
|
}
|