307 lines
8.8 KiB
Go
307 lines
8.8 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
type inventoryItemResource struct {
|
|
ItemIDs []uint32 `json:"item_ids"`
|
|
Lightcones []struct {
|
|
ID uint32 `json:"id"`
|
|
MaxRank uint32 `json:"max_rank"`
|
|
} `json:"lightcones"`
|
|
Relics []struct {
|
|
RelicID uint32 `json:"relic_id"`
|
|
RelicSetID uint32 `json:"relic_set_id"`
|
|
MainAffixID uint32 `json:"main_affix_id"`
|
|
SubAffixes []struct {
|
|
SubAffixID uint32 `json:"sub_affix_id"`
|
|
Count uint32 `json:"count"`
|
|
Step uint32 `json:"step"`
|
|
} `json:"sub_affixes"`
|
|
} `json:"relics"`
|
|
}
|
|
|
|
var inventoryItemsOnce sync.Once
|
|
var inventoryItems []uint32
|
|
var inventoryCatalog inventoryItemResource
|
|
var inventoryRelicIDs map[uint32]struct{}
|
|
var inventoryItemsErr error
|
|
|
|
func loadInventoryItems() ([]uint32, error) {
|
|
inventoryItemsOnce.Do(func() {
|
|
raw, err := common.ReadResourceFile("resources/InventoryItemConfig.json", "InventoryItemConfig.json")
|
|
if err != nil {
|
|
inventoryItemsErr = err
|
|
return
|
|
}
|
|
var resource inventoryItemResource
|
|
if err := json.Unmarshal(raw, &resource); err != nil {
|
|
inventoryItemsErr = err
|
|
return
|
|
}
|
|
if len(resource.ItemIDs) == 0 || len(resource.Lightcones) == 0 || len(resource.Relics) == 0 {
|
|
inventoryItemsErr = fmt.Errorf("inventory item resource is incomplete")
|
|
return
|
|
}
|
|
seen := make(map[uint32]bool, len(resource.ItemIDs))
|
|
for _, id := range resource.ItemIDs {
|
|
if id == 0 || seen[id] {
|
|
inventoryItemsErr = fmt.Errorf("invalid or duplicate inventory item id %d", id)
|
|
return
|
|
}
|
|
seen[id] = true
|
|
}
|
|
seenLightcones := make(map[uint32]bool, len(resource.Lightcones))
|
|
for _, lc := range resource.Lightcones {
|
|
if lc.ID == 0 || lc.MaxRank == 0 || seenLightcones[lc.ID] {
|
|
inventoryItemsErr = fmt.Errorf("invalid or duplicate Light Cone %d in inventory item resource", lc.ID)
|
|
return
|
|
}
|
|
seenLightcones[lc.ID] = true
|
|
}
|
|
seenRelics := make(map[uint32]struct{}, len(resource.Relics))
|
|
for _, relic := range resource.Relics {
|
|
if relic.RelicID == 0 || relic.RelicSetID == 0 || relic.MainAffixID == 0 || len(relic.SubAffixes) != 4 {
|
|
inventoryItemsErr = fmt.Errorf("invalid or duplicate Relic %d in inventory item resource", relic.RelicID)
|
|
return
|
|
}
|
|
if _, exists := seenRelics[relic.RelicID]; exists {
|
|
inventoryItemsErr = fmt.Errorf("invalid or duplicate Relic %d in inventory item resource", relic.RelicID)
|
|
return
|
|
}
|
|
seenRelics[relic.RelicID] = struct{}{}
|
|
}
|
|
inventoryCatalog = resource
|
|
inventoryRelicIDs = seenRelics
|
|
inventoryItems = resource.ItemIDs
|
|
})
|
|
if inventoryItemsErr != nil {
|
|
return nil, fmt.Errorf("load inventory item resource: %w", inventoryItemsErr)
|
|
}
|
|
return inventoryItems, nil
|
|
}
|
|
|
|
func supportedRelicIDs() (map[uint32]struct{}, error) {
|
|
if _, err := loadInventoryItems(); err != nil {
|
|
return nil, err
|
|
}
|
|
return inventoryRelicIDs, nil
|
|
}
|
|
|
|
func filterSupportedRelics(relics []*common.Relic, supported map[uint32]struct{}) []*common.Relic {
|
|
filtered := make([]*common.Relic, 0, len(relics))
|
|
for _, relic := range relics {
|
|
if relic == nil {
|
|
continue
|
|
}
|
|
if _, ok := supported[relic.RelicID]; ok {
|
|
filtered = append(filtered, relic)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func supportedRelicsForAvatar(player *common.FreesrData, avatarID uint32) ([]*common.Relic, error) {
|
|
supported, err := supportedRelicIDs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return filterSupportedRelics(player.FindRelicsFor(avatarID), supported), nil
|
|
}
|
|
|
|
func supportedProtoRelics(relics []common.Relic) ([]*goproto.Relic, int, error) {
|
|
supported, err := supportedRelicIDs()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
result := make([]*goproto.Relic, 0, len(relics))
|
|
omitted := 0
|
|
for i := range relics {
|
|
if _, ok := supported[relics[i].RelicID]; !ok {
|
|
omitted++
|
|
continue
|
|
}
|
|
result = append(result, relics[i].ToProtoRelic())
|
|
}
|
|
return result, omitted, nil
|
|
}
|
|
|
|
// ensureCatalogLightcones makes each client-configured Light Cone available as
|
|
// an unassigned level-1 copy while keeping existing copies and their ownership.
|
|
func ensureCatalogLightcones(player *common.FreesrData) error {
|
|
if _, err := loadInventoryItems(); err != nil {
|
|
return err
|
|
}
|
|
usedItems := make(map[uint32]bool, len(player.Lightcones))
|
|
usedUIDs := make(map[uint32]bool, len(player.Lightcones))
|
|
var nextUID uint32
|
|
for _, item := range player.Lightcones {
|
|
usedItems[item.ItemID] = true
|
|
usedUIDs[item.InternalUID] = true
|
|
if item.InternalUID >= nextUID {
|
|
if item.InternalUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Light Cone inventory UID")
|
|
}
|
|
nextUID = item.InternalUID + 1
|
|
}
|
|
}
|
|
var additions []common.Lightcone
|
|
for _, item := range inventoryCatalog.Lightcones {
|
|
if usedItems[item.ID] {
|
|
continue
|
|
}
|
|
for usedUIDs[nextUID] {
|
|
if nextUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Light Cone inventory UID")
|
|
}
|
|
nextUID++
|
|
}
|
|
additions = append(additions, common.Lightcone{
|
|
ItemID: item.ID, InternalUID: nextUID, Level: 1, Promotion: 0, Rank: 1,
|
|
})
|
|
usedItems[item.ID] = true
|
|
usedUIDs[nextUID] = true
|
|
if nextUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Light Cone inventory UID")
|
|
}
|
|
nextUID++
|
|
}
|
|
if err := common.SaveNewLightcones(additions); err != nil {
|
|
return err
|
|
}
|
|
player.Lightcones = append(player.Lightcones, additions...)
|
|
return nil
|
|
}
|
|
|
|
// ensureCatalogRelics makes each Client-configured Relic type available as an
|
|
// unassigned instance with affixes validated against its own config groups.
|
|
func ensureCatalogRelics(player *common.FreesrData) error {
|
|
if _, err := loadInventoryItems(); err != nil {
|
|
return err
|
|
}
|
|
usedRelics := make(map[uint32]bool, len(player.Relics))
|
|
usedUIDs := make(map[uint32]bool, len(player.Relics))
|
|
var nextUID uint32
|
|
for _, item := range player.Relics {
|
|
usedRelics[item.RelicID] = true
|
|
usedUIDs[item.InternalUID] = true
|
|
if item.InternalUID >= nextUID {
|
|
if item.InternalUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Relic inventory UID")
|
|
}
|
|
nextUID = item.InternalUID + 1
|
|
}
|
|
}
|
|
var additions []common.Relic
|
|
for _, item := range inventoryCatalog.Relics {
|
|
if usedRelics[item.RelicID] {
|
|
continue
|
|
}
|
|
for usedUIDs[nextUID] {
|
|
if nextUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Relic inventory UID")
|
|
}
|
|
nextUID++
|
|
}
|
|
relic := common.Relic{
|
|
RelicID: item.RelicID, RelicSetID: item.RelicSetID,
|
|
MainAffixID: item.MainAffixID, InternalUID: nextUID,
|
|
SubAffixes: make([]common.SubAffix, 0, len(item.SubAffixes)),
|
|
}
|
|
for _, sub := range item.SubAffixes {
|
|
relic.SubAffixes = append(relic.SubAffixes, common.SubAffix{
|
|
SubAffixID: sub.SubAffixID, Count: sub.Count, Step: sub.Step,
|
|
})
|
|
}
|
|
additions = append(additions, relic)
|
|
usedRelics[item.RelicID] = true
|
|
usedUIDs[nextUID] = true
|
|
if nextUID == ^uint32(0) {
|
|
return fmt.Errorf("cannot allocate a Relic inventory UID")
|
|
}
|
|
nextUID++
|
|
}
|
|
if err := common.SaveNewRelics(additions); err != nil {
|
|
return err
|
|
}
|
|
player.Relics = append(player.Relics, additions...)
|
|
return nil
|
|
}
|
|
|
|
func bagMaterials(player *common.FreesrData) ([]*goproto.Material, error) {
|
|
itemIDs, err := loadInventoryItems()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts := make(map[uint32]uint32, len(itemIDs)+64)
|
|
for _, id := range itemIDs {
|
|
counts[id] = 1
|
|
}
|
|
counts[1] = 9999999 // Stellar Jade
|
|
counts[2] = 99999999 // Credit
|
|
counts[101] = 999999 // Normal Pass
|
|
counts[102] = 999999 // Special Pass
|
|
|
|
eidolon, err := eidolonMaterials(player)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range eidolon {
|
|
// A completed rank/EX grant is zero, including when the general
|
|
// inventory catalog also contains this item.
|
|
counts[item.Tid] = item.Num
|
|
}
|
|
foods, err := techniqueMaterials(player)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range foods {
|
|
if item.Num > 0 {
|
|
counts[item.Tid] = item.Num
|
|
}
|
|
}
|
|
cosmetics, err := common.LoadPlayerCosmetics()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, item := range cosmetics.OutfitItems {
|
|
if item.OwnedCount > 0 {
|
|
counts[item.ItemID] = item.OwnedCount
|
|
}
|
|
}
|
|
// Explicit inventory edits from GaC Srtool are authoritative over generated
|
|
// defaults and progression grants. Keeping zero values lets users remove an
|
|
// item from the bag without deleting its catalog entry.
|
|
allowed := make(map[uint32]bool, len(itemIDs)+2)
|
|
allowed[1], allowed[2] = true, true
|
|
for _, id := range itemIDs {
|
|
allowed[id] = true
|
|
}
|
|
for _, item := range eidolon {
|
|
allowed[item.Tid] = true
|
|
}
|
|
for id, count := range player.Materials {
|
|
if allowed[id] {
|
|
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] })
|
|
materials := make([]*goproto.Material, 0, len(ids))
|
|
for _, id := range ids {
|
|
materials = append(materials, &goproto.Material{Tid: id, Num: counts[id]})
|
|
}
|
|
return materials, nil
|
|
}
|