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 }