Files
GaCy-GO/internal/gameserver/inventory_items.go
T

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
}