From d6561d2447e9d38b2447d8913640dd06266f045f Mon Sep 17 00:00:00 2001 From: drico <132640189+dricotec@users.noreply.github.com> Date: Mon, 4 May 2026 09:05:23 +0400 Subject: [PATCH 1/3] Update Readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bad5c16..eb35eaa 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ While it's still possible to connect CS:GO to CS2's GC by spoofing the version n - Opening cases (including sticker capsules, patch packs, graffiti boxes and music kit boxes) - Graffiti support - Weapon StatTrak support +- Trade ups - Stickers and patches - Name tags - Music kits @@ -27,7 +28,7 @@ While it's still possible to connect CS:GO to CS2's GC by spoofing the version n - Networking using Steam's P2P interface ## Planned features -- Rest of the core features (trade ups, souvenirs, storage units, StatTrak swaps...) +- Rest of the core features (souvenirs, storage units, StatTrak swaps...) I'm still looking for the **full** CS:GO Item Schema. If you have a relatively recent copy of it and are willing to share it, let me know! From ea6be47ad360da101c9164c6af324913a369ec77 Mon Sep 17 00:00:00 2001 From: dricotec Date: Mon, 4 May 2026 09:15:54 +0400 Subject: [PATCH 2/3] Add trade-up chat broadcast Added trade-up completion chat broadcast. --- csgo_gc/gc_client.cpp | 165 ++++++++++++++++++++ csgo_gc/gc_client.h | 1 + csgo_gc/inventory.cpp | 328 ++++++++++++++++++++++++++++++++++++++++ csgo_gc/inventory.h | 20 +++ csgo_gc/item_schema.cpp | 184 ++++++++++++++++++++++ csgo_gc/item_schema.h | 23 +++ 6 files changed, 721 insertions(+) diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 255d735..1d47c93 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -2,6 +2,65 @@ #include "gc_client.h" #include "graffiti.h" #include "keyvalue.h" +#include "steam/steam_api.h" + +static std::string GetLocalPlayerName() +{ + const char *name = nullptr; + if (SteamFriends()) + { + name = SteamFriends()->GetPersonaName(); + } + + if (name && name[0]) + { + return name; + } + + return "Player"; +} + +static bool GetItemPaintKitDefIndex(const CSOEconItem &item, const ItemSchema &schema, uint32_t &paintKitDefIndex) +{ + for (const CSOEconItemAttribute &attr : item.attribute()) + { + if (attr.def_index() == ItemSchema::AttributeTexturePrefab) + { + paintKitDefIndex = schema.AttributeUint32(&attr); + return true; + } + } + + return false; +} + +static std::string GetItemCollectionId(const CSOEconItem &item, const ItemSchema &schema) +{ + uint32_t paintKitDefIndex = 0; + if (!GetItemPaintKitDefIndex(item, schema, paintKitDefIndex)) + { + return {}; + } + + std::vector collections; + if (!schema.GetCollectionsForPaintedItem(item.def_index(), paintKitDefIndex, collections)) + { + return {}; + } + + std::sort(collections.begin(), collections.end()); + return collections.front(); +} + +static std::string GetCollectionName(const ItemSchema &schema, std::string_view collectionId) +{ + if (collectionId.empty()) + { + return "Unknown"; + } + + return schema.GetCollectionDisplayName(collectionId); +} ClientGC::ClientGC(uint64_t steamId) : m_steamId{ steamId } @@ -114,6 +173,10 @@ void ClientGC::HandleMessage(uint32_t type, const void *data, uint32_t size) UnlockCrate(messageRead); break; + case k_EMsgGCCraft: + Craft(messageRead); + break; + case k_EMsgGCNameItem: NameItem(messageRead); break; @@ -717,6 +780,108 @@ void ClientGC::NameBaseItem(GCMessageRead &messageRead) } } +void ClientGC::Craft(GCMessageRead &messageRead) +{ + // Trade-up contract message format: + // int16_t recipe (-2 for trade-up) + // int16_t itemCount (should be 10) + // uint64_t itemIds[itemCount] + + int16_t recipe = static_cast(messageRead.ReadUint16()); + int16_t itemCount = static_cast(messageRead.ReadUint16()); + + if (!messageRead.IsValid()) + { + Platform::Print("Parsing CMsgGCCraft header failed, ignoring\n"); + return; + } + + Platform::Print("TRADE-UP CONTRACT: recipe=%d, itemCount=%d\n", recipe, itemCount); + + // Trade-up recipes are -2 and 12 (remove restriction on 12) + if (recipe != -2 && recipe != 12) + { + Platform::Print("Unsupported craft recipe %d, ignoring\n", recipe); + return; + } + + // Read all item IDs + std::vector inputItemIds; + inputItemIds.reserve(itemCount); + + for (int i = 0; i < itemCount; i++) + { + uint64_t itemId = messageRead.ReadUint64(); + if (!messageRead.IsValid()) + { + Platform::Print("Parsing CMsgGCCraft item %d failed, ignoring\n", i); + return; + + } + inputItemIds.push_back(itemId); + } + + Platform::Print("Input items:\n"); + for (uint64_t itemId : inputItemIds) + { + const CSOEconItem* item = m_inventory.GetItem(itemId); + if (item) + { + std::string collectionId = GetItemCollectionId(*item, m_inventory.GetItemSchema()); + Platform::Print(" Item %llu: def_index %u, rarity %u, quality %u, collection %s (%s)\n", + itemId, item->def_index(), item->rarity(), item->quality(), collectionId.c_str(), + GetCollectionName(m_inventory.GetItemSchema(), collectionId).c_str()); + } + else + { + Platform::Print(" Item %llu: not found in inventory\n", itemId); + } + } + + std::vector destroyItems; + CMsgSOSingleObject newItem; + CMsgGCItemCustomizationNotification notification; + CSOEconItem *craftedItem = nullptr; + + if (m_inventory.TradeUp(inputItemIds, destroyItems, newItem, notification, &craftedItem)) + { + // Destroy all input items + for (auto &destroy : destroyItems) + { + SendMessageToGame(true, k_ESOMsg_Destroy, destroy); + } + + // Create the new item + SendMessageToGame(true, k_ESOMsg_Create, newItem); + + // Send notification + SendMessageToGame(false, k_EMsgGCItemCustomizationNotification, notification); + + if (craftedItem) + { + const ItemInfo *itemInfo = m_inventory.GetItemSchema().ItemInfoByDefIndex(craftedItem->def_index()); + std::string itemName = itemInfo ? itemInfo->m_name : "Unknown Item"; + + std::string chatMessage = GetLocalPlayerName(); + chatMessage += " has fulfilled a contract and received: "; + chatMessage += itemName; + + CMsgGCCStrike15_v2_GC2ClientTextMsg textMsg; + textMsg.set_id(0); + textMsg.set_type(0); + textMsg.set_payload(chatMessage); + + SendMessageToGame(true, k_EMsgGCCStrike15_v2_GC2ClientTextMsg, textMsg); + } + + Platform::Print("Trade-up completed successfully!\n"); + } + else + { + Platform::Print("Trade-up failed: input validation failed\n"); + } +} + void ClientGC::RemoveItemName(GCMessageRead &messageRead) { uint64_t itemId = messageRead.ReadUint64(); diff --git a/csgo_gc/gc_client.h b/csgo_gc/gc_client.h index 1e52c7b..9215714 100644 --- a/csgo_gc/gc_client.h +++ b/csgo_gc/gc_client.h @@ -36,6 +36,7 @@ class ClientGC final : public SharedGC void DeleteItem(GCMessageRead &messageRead); void UnlockCrate(GCMessageRead &messageRead); + void Craft(GCMessageRead &messageRead); void NameItem(GCMessageRead &messageRead); void NameBaseItem(GCMessageRead &messageRead); void RemoveItemName(GCMessageRead &messageRead); diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 60c3a51..53819a3 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -87,6 +87,28 @@ uint32_t Inventory::AccountId() const return m_steamId & 0xffffffff; } +const CSOEconItem *Inventory::GetItem(uint64_t itemId) const +{ + auto it = m_items.find(itemId); + if (it == m_items.end()) + { + return nullptr; + } + + return &it->second; +} + +const CSOEconItem *Inventory::GetItem(uint64_t itemId) const +{ + auto it = m_items.find(itemId); + if (it == m_items.end()) + { + return nullptr; + } + + return &it->second; +} + CSOEconItem &Inventory::AllocateItem(uint32_t highItemId) { // Players fuck up their inventory files constantly and end up with item id collisions... @@ -567,6 +589,49 @@ static int ItemWearLevel(float wearFloat) return 4; } +static bool GetItemPaintKitDefIndex(const CSOEconItem &item, const ItemSchema &schema, uint32_t &paintKitDefIndex) +{ + for (const CSOEconItemAttribute &attr : item.attribute()) + { + if (attr.def_index() == ItemSchema::AttributeTexturePrefab) + { + paintKitDefIndex = schema.AttributeUint32(&attr); + return true; + } + } + + return false; +} + +static std::string GetItemCollectionId(const CSOEconItem &item, const ItemSchema &schema) +{ + uint32_t paintKitDefIndex = 0; + if (!GetItemPaintKitDefIndex(item, schema, paintKitDefIndex)) + { + return {}; + } + + std::vector collections; + if (!schema.GetCollectionsForPaintedItem(item.def_index(), paintKitDefIndex, collections)) + { + return {}; + } + + std::sort(collections.begin(), collections.end()); + return collections.front(); +} + +static std::string GetCollectionName(const ItemSchema &schema, std::string_view collectionId) +{ + if (collectionId.empty()) + { + return "Unknown"; + } + + return schema.GetCollectionDisplayName(collectionId); +} + + void Inventory::ItemToPreviewDataBlock(const CSOEconItem &item, CEconItemPreviewDataBlock &block) { block.set_accountid(item.account_id()); @@ -1183,3 +1248,266 @@ void Inventory::DestroyItem(ItemMap::iterator iterator, CMsgSOSingleObject &mess m_items.erase(iterator); } + +bool Inventory::TradeUp(const std::vector &inputItemIds, + std::vector &destroyItems, + CMsgSOSingleObject &newItem, + CMsgGCItemCustomizationNotification ¬ification, + CSOEconItem **outCraftedItem) + CSOEconItem **outCraftedItem) +{ + if (inputItemIds.size() != 10) + { + Platform::Print("Trade-up requires exactly 10 items, got %zu\n", inputItemIds.size()); + return false; + } + + std::vector inputItems; + inputItems.reserve(10); + + uint32_t inputRarity = 0; + bool statTrakSet = false; + bool hasStatTrak = false; + float totalWear = 0.0f; + int wearCount = 0; + + std::map collectionCounts; + + for (uint64_t itemId : inputItemIds) + { + auto it = m_items.find(itemId); + if (it == m_items.end()) + { + Platform::Print("Trade-up item %llu not found\n", itemId); + return false; + } + + const CSOEconItem &item = it->second; + inputItems.push_back(it); + + uint32_t paintKitDefIndex = 0; + if (!GetItemPaintKitDefIndex(item, m_itemSchema, paintKitDefIndex)) + { + Platform::Print("Trade-up item %llu has no paint kit\n", itemId); + return false; + } + + uint32_t rarity = m_itemSchema.GetPaintedRarity(item.def_index(), paintKitDefIndex, item.rarity()); + if (rarity < ItemSchema::RarityCommon || rarity > ItemSchema::RarityLegendary) + { + Platform::Print("Trade-up item %llu has invalid rarity %u\n", itemId, rarity); + return false; + } + + if (inputRarity == 0) + { + inputRarity = rarity; + } + else if (rarity != inputRarity) + { + Platform::Print("Trade-up items must all be same rarity (expected %u, got %u)\n", inputRarity, rarity); + return false; + } + + std::vector collections; + if (!m_itemSchema.GetCollectionsForPaintedItem(item.def_index(), paintKitDefIndex, collections)) + { + if (!m_itemSchema.GetCollectionsForPaintKit(paintKitDefIndex, collections)) + { + Platform::Print("Trade-up item %llu has no collection mapping (def %u, paint %u)\n", + itemId, item.def_index(), paintKitDefIndex); + return false; + } + } + + std::sort(collections.begin(), collections.end()); + const std::string &collectionId = collections.front(); + collectionCounts[collectionId]++; + + Platform::Print("Trade-up item %llu: collection %s (%s), quality %u\n", itemId, + collectionId.c_str(), GetCollectionName(m_itemSchema, collectionId).c_str(), item.quality()); + + bool itemStatTrak = false; + for (const CSOEconItemAttribute &attr : item.attribute()) + { + if (attr.def_index() == ItemSchema::AttributeKillEater) + { + itemStatTrak = true; + } + else if (attr.def_index() == ItemSchema::AttributeTextureWear) + { + totalWear += m_itemSchema.AttributeFloat(&attr); + wearCount++; + } + } + + if (!statTrakSet) + { + hasStatTrak = itemStatTrak; + statTrakSet = true; + } + else if (itemStatTrak != hasStatTrak) + { + Platform::Print("Trade-up items must all be StatTrak or all non-StatTrak\n"); + return false; + } + } + + float avgWear = 0.15f; + if (wearCount > 0) + { + avgWear = totalWear / wearCount; + if (avgWear < 0.0f) avgWear = 0.0f; + if (avgWear > 1.0f) avgWear = 1.0f; + } + + uint32_t outputRarity = inputRarity + 1; + if (outputRarity > ItemSchema::RarityAncient) + { + Platform::Print("Cannot trade up items of rarity %u (max output is ancient)\n", inputRarity); + return false; + } + + std::vector weightedCollections; + for (const auto &pair : collectionCounts) + { + const std::string &collection = pair.first; + std::vector candidates; + if (!m_itemSchema.GetTradeUpCandidates(collection, outputRarity, candidates)) + { + continue; + } + + int count = pair.second; + for (int i = 0; i < count; i++) + { + weightedCollections.push_back(collection); + } + + float percentage = (float)count / 10.0f * 100.0f; + Platform::Print("%s Collection: %.1f%%\n", GetCollectionName(m_itemSchema, collection).c_str(), percentage); + } + + if (weightedCollections.empty()) + { + Platform::Print("No valid trade-up collections with rarity %u\n", outputRarity); + return false; + } + + uint32_t roll = m_random.Integer(0, 9); + const std::string &selectedCollection = weightedCollections[roll]; + Platform::Print("RNG roll: %u, selected collection %s (%s)\n", roll, selectedCollection.c_str(), + GetCollectionName(m_itemSchema, selectedCollection).c_str()); + + std::vector outputCandidates; + if (!m_itemSchema.GetTradeUpCandidates(selectedCollection, outputRarity, outputCandidates)) + { + Platform::Print("No trade-up candidates for collection %s at rarity %u\n", + selectedCollection.c_str(), outputRarity); + return false; + } + + std::vector validCandidates; + validCandidates.reserve(outputCandidates.size()); + + for (const LootListItem *candidate : outputCandidates) + { + if (!candidate || !candidate->paintKitInfo) + { + continue; + } + + float minWear = candidate->paintKitInfo->m_minFloat; + float maxWear = candidate->paintKitInfo->m_maxFloat; + float mappedWear = minWear + avgWear * (maxWear - minWear); + + if (mappedWear < minWear || mappedWear > maxWear) + { + continue; + } + + validCandidates.push_back(candidate); + } + + if (validCandidates.empty()) + { + Platform::Print("No valid trade-up candidates after float filtering for collection %s\n", + selectedCollection.c_str()); + return false; + } + + uint32_t candidateIndex = m_random.Integer(0, static_cast(validCandidates.size() - 1)); + const LootListItem *selectedCandidate = validCandidates[candidateIndex]; + + CSOEconItem &outputItem = AllocateItem(0); + + outputItem.set_def_index(selectedCandidate->itemInfo->m_defIndex); + outputItem.set_inventory(InventoryUnacknowledged(UnacknowledgedRecycling)); + outputItem.set_quantity(1); + outputItem.set_level(1); + outputItem.set_origin(ItemOriginCrate); + outputItem.set_rarity(outputRarity); + outputItem.set_quality(hasStatTrak ? ItemSchema::QualityStrange : ItemSchema::QualityUnique); + outputItem.set_flags(0); + outputItem.set_in_use(false); + + uint32_t paintKitId = selectedCandidate->paintKitInfo->m_defIndex; + + CSOEconItemAttribute *paintAttr = outputItem.add_attribute(); + paintAttr->set_def_index(ItemSchema::AttributeTexturePrefab); + m_itemSchema.SetAttributeUint32(paintAttr, paintKitId); + + CSOEconItemAttribute *seedAttr = outputItem.add_attribute(); + seedAttr->set_def_index(ItemSchema::AttributeTextureSeed); + m_itemSchema.SetAttributeUint32(seedAttr, m_random.Integer(0, 1000)); + + float outputWear = avgWear; + if (selectedCandidate->paintKitInfo) + { + float minWear = selectedCandidate->paintKitInfo->m_minFloat; + float maxWear = selectedCandidate->paintKitInfo->m_maxFloat; + outputWear = minWear + avgWear * (maxWear - minWear); + } + CSOEconItemAttribute *wearAttr = outputItem.add_attribute(); + wearAttr->set_def_index(ItemSchema::AttributeTextureWear); + m_itemSchema.SetAttributeFloat(wearAttr, outputWear); + + if (hasStatTrak) + { + CSOEconItemAttribute *killAttr = outputItem.add_attribute(); + killAttr->set_def_index(ItemSchema::AttributeKillEater); + m_itemSchema.SetAttributeUint32(killAttr, 0); + + CSOEconItemAttribute *scoreTypeAttr = outputItem.add_attribute(); + scoreTypeAttr->set_def_index(ItemSchema::AttributeKillEaterScoreType); + m_itemSchema.SetAttributeUint32(scoreTypeAttr, 0); + } + + destroyItems.reserve(inputItems.size()); + for (auto it : inputItems) + { + CMsgSOSingleObject &destroy = destroyItems.emplace_back(); + DestroyItem(it, destroy); + } + + ToSingleObject(newItem, outputItem); + + notification.add_item_id(outputItem.id()); + notification.set_request(k_EGCItemCustomizationNotification_UnlockCrate); + + if (outCraftedItem) + { + *outCraftedItem = &outputItem; + } + + if (outCraftedItem) + { + *outCraftedItem = &outputItem; + } + + Platform::Print("Trade-up complete: created item %llu from collection %s (%s), def %u, rarity %u, wear %.4f, stattrak=%d\n", + outputItem.id(), selectedCollection.c_str(), GetCollectionName(m_itemSchema, selectedCollection).c_str(), + outputItem.def_index(), selectedCandidate->rarity, outputWear, hasStatTrak ? 1 : 0); + + return true; +} diff --git a/csgo_gc/inventory.h b/csgo_gc/inventory.h index cad21a0..1fda5fa 100644 --- a/csgo_gc/inventory.h +++ b/csgo_gc/inventory.h @@ -68,6 +68,18 @@ class Inventory CMsgSOSingleObject &destroy, CMsgGCItemCustomizationNotification ¬ification); + const CSOEconItem *GetItem(uint64_t itemId) const; + const ItemSchema &GetItemSchema() const { return m_itemSchema; } + + // Trade-up contract: craft 10 items of same rarity into 1 item of next rarity + // Returns true on success, false on validation failure + bool TradeUp(const std::vector &inputItemIds, + std::vector &destroyItems, + CMsgSOSingleObject &newItem, + CMsgGCItemCustomizationNotification ¬ification, + CSOEconItem **outCraftedItem = nullptr); + CSOEconItem **outCraftedItem = nullptr); + // returns the item id and adds the item to the provided CMsgSOMultipleObjects // on failure returns 0 and does nothing uint64_t PurchaseItem(uint32_t defIndex, std::vector &update); @@ -129,4 +141,12 @@ class Inventory uint32_t m_lastHighItemId{}; ItemMap m_items; std::vector m_defaultEquips; + +public: + const CSOEconItem* GetItem(uint64_t itemId) const { + auto it = m_items.find(itemId); + return it != m_items.end() ? &it->second : nullptr; + } + + const ItemSchema& GetItemSchema() const { return m_itemSchema; } }; diff --git a/csgo_gc/item_schema.cpp b/csgo_gc/item_schema.cpp index 8a9809e..9e36baa 100644 --- a/csgo_gc/item_schema.cpp +++ b/csgo_gc/item_schema.cpp @@ -155,6 +155,12 @@ ItemSchema::ItemSchema() ParsePaintKitRarities(paintKitsRarityKey); } + const KeyValue *itemSetsKey = itemsGame->GetSubkey("item_sets"); + if (itemSetsKey) + { + ParseItemSets(itemSetsKey); + } + const KeyValue *musicDefinitionsKey = itemsGame->GetSubkey("music_definitions"); if (musicDefinitionsKey) { @@ -764,6 +770,36 @@ void ItemSchema::ParsePaintKits(const KeyValue *paintKitsKey) } } +void ItemSchema::ParseItemSets(const KeyValue *itemSetsKey) +{ + m_itemSets.reserve(itemSetsKey->SubkeyCount()); + + for (const KeyValue &itemSetKey : *itemSetsKey) + { + ItemSet itemSet; + itemSet.name = std::string{ itemSetKey.GetString("name", itemSetKey.Name()) }; + itemSet.isCollection = itemSetKey.GetNumber("is_collection", false); + + const KeyValue *itemsKey = itemSetKey.GetSubkey("items"); + if (itemsKey) + { + itemSet.items.reserve(itemsKey->SubkeyCount()); + for (const KeyValue &itemKey : *itemsKey) + { + LootListItem item; + if (ParseLootListItem(item, itemKey.Name())) + { + itemSet.items.push_back(item); + } + } + } + + m_itemSets.emplace(std::piecewise_construct, + std::forward_as_tuple(itemSetKey.Name()), + std::forward_as_tuple(std::move(itemSet))); + } +} + void ItemSchema::ParsePaintKitRarities(const KeyValue *raritiesKey) { for (const KeyValue &key : *raritiesKey) @@ -1039,6 +1075,17 @@ ItemInfo *ItemSchema::ItemInfoByName(std::string_view name) return nullptr; } +const ItemInfo *ItemSchema::ItemInfoByDefIndex(uint32_t defIndex) const +{ + auto it = m_itemInfo.find(defIndex); + if (it == m_itemInfo.end()) + { + return nullptr; + } + + return &it->second; +} + StickerKitInfo *ItemSchema::StickerKitInfoByName(std::string_view name) { auto it = m_stickerKitInfo.find(std::string{ name }); @@ -1063,6 +1110,19 @@ PaintKitInfo *ItemSchema::PaintKitInfoByName(std::string_view name) return &it->second; } +const PaintKitInfo *ItemSchema::PaintKitInfoByDefIndex(uint32_t defIndex) const +{ + for (const auto &pair : m_paintKitInfo) + { + if (pair.second.m_defIndex == defIndex) + { + return &pair.second; + } + } + + return nullptr; +} + MusicDefinitionInfo *ItemSchema::MusicDefinitionInfoByName(std::string_view name) { auto it = m_musicDefinitionInfo.find(std::string{ name }); @@ -1074,3 +1134,127 @@ MusicDefinitionInfo *ItemSchema::MusicDefinitionInfoByName(std::string_view name return &it->second; } + +bool ItemSchema::GetCollectionsForPaintedItem(uint32_t defIndex, uint32_t paintKitDefIndex, + std::vector &outCollections) const +{ + outCollections.clear(); + + for (const auto &pair : m_itemSets) + { + const ItemSet &itemSet = pair.second; + if (!itemSet.isCollection) + { + continue; + } + + for (const LootListItem &item : itemSet.items) + { + if (!item.itemInfo || !item.paintKitInfo) + { + continue; + } + + if (item.itemInfo->m_defIndex == defIndex && item.paintKitInfo->m_defIndex == paintKitDefIndex) + { + outCollections.push_back(pair.first); + break; + } + } + } + + return !outCollections.empty(); +} + +bool ItemSchema::GetCollectionsForPaintKit(uint32_t paintKitDefIndex, + std::vector &outCollections) const +{ + outCollections.clear(); + + for (const auto &pair : m_itemSets) + { + const ItemSet &itemSet = pair.second; + if (!itemSet.isCollection) + { + continue; + } + + for (const LootListItem &item : itemSet.items) + { + if (!item.paintKitInfo) + { + continue; + } + + if (item.paintKitInfo->m_defIndex == paintKitDefIndex) + { + outCollections.push_back(pair.first); + break; + } + } + } + + return !outCollections.empty(); +} + +std::string ItemSchema::GetCollectionDisplayName(std::string_view collectionName) const +{ + auto it = m_itemSets.find(std::string{ collectionName }); + if (it == m_itemSets.end()) + { + return std::string{ collectionName }; + } + + if (it->second.name.empty()) + { + return std::string{ collectionName }; + } + + return it->second.name; +} + +bool ItemSchema::GetTradeUpCandidates(std::string_view collectionName, uint32_t outputRarity, + std::vector &outCandidates) const +{ + outCandidates.clear(); + + auto it = m_itemSets.find(std::string{ collectionName }); + if (it == m_itemSets.end()) + { + return false; + } + + const ItemSet &itemSet = it->second; + for (const LootListItem &item : itemSet.items) + { + if (item.type != LootListItemPaintable) + { + continue; + } + + if (!item.itemInfo || !item.paintKitInfo) + { + continue; + } + + if (item.rarity == outputRarity) + { + outCandidates.push_back(&item); + } + } + + return !outCandidates.empty(); +} + +uint32_t ItemSchema::GetPaintedRarity(uint32_t defIndex, uint32_t paintKitDefIndex, uint32_t fallbackRarity) const +{ + const ItemInfo *itemInfo = ItemInfoByDefIndex(defIndex); + const PaintKitInfo *paintKitInfo = PaintKitInfoByDefIndex(paintKitDefIndex); + + if (!itemInfo || !paintKitInfo) + { + return fallbackRarity; + } + + return PaintedItemRarity(itemInfo->m_rarity, paintKitInfo->m_rarity); +} diff --git a/csgo_gc/item_schema.h b/csgo_gc/item_schema.h index 6c573b1..72dae58 100644 --- a/csgo_gc/item_schema.h +++ b/csgo_gc/item_schema.h @@ -104,6 +104,13 @@ struct LootList bool isUnusual{}; }; +struct ItemSet +{ + std::string name; + bool isCollection{}; + std::vector items; +}; + class ItemSchema { public: @@ -131,6 +138,19 @@ class ItemSchema // item creation: id and account id not set, needs to be done by the caller bool CreateItem(uint32_t defIndex, ItemOrigin origin, UnacknowledgedType unacknowledgedType, CSOEconItem &econItem) const; + // trade-up helpers + const ItemInfo *ItemInfoByDefIndex(uint32_t defIndex) const; + const PaintKitInfo *PaintKitInfoByDefIndex(uint32_t defIndex) const; + bool GetCollectionsForPaintedItem(uint32_t defIndex, uint32_t paintKitDefIndex, + std::vector &outCollections) const; + bool GetCollectionsForPaintKit(uint32_t paintKitDefIndex, + std::vector &outCollections) const; + std::string GetCollectionDisplayName(std::string_view collectionName) const; + bool GetTradeUpCandidates(std::string_view collectionName, uint32_t outputRarity, + std::vector &outCandidates) const; + uint32_t GetPaintedRarity(uint32_t defIndex, uint32_t paintKitDefIndex, uint32_t fallbackRarity) const; + + public: // these could be parsed from the item schema but reduce code complexity by hardcoding them enum Rarity @@ -233,6 +253,7 @@ class ItemSchema void ParsePaintKits(const KeyValue *paintKitsKey); void ParsePaintKitRarities(const KeyValue *raritiesKey); void ParseMusicDefinitions(const KeyValue *musicDefinitionsKey); + void ParseItemSets(const KeyValue *itemSetsKey); void ParseLootLists(const KeyValue *lootListsKey, bool unusual); void ParseRevolvingLootLists(const KeyValue *revolvingLootListsKey); @@ -252,5 +273,7 @@ class ItemSchema std::unordered_map m_musicDefinitionInfo; std::unordered_map m_lootLists; + std::unordered_map m_itemSets; + std::unordered_map m_revolvingLootLists; }; From 9c2132066954a6057fc5214017ed3d0e6d8fe31d Mon Sep 17 00:00:00 2001 From: dricotec Date: Mon, 4 May 2026 09:30:46 +0400 Subject: [PATCH 3/3] Fix trade-up build Fix build errors. --- csgo_gc/gc_client.cpp | 20 ++------------------ csgo_gc/inventory.cpp | 12 ------------ csgo_gc/inventory.h | 11 +---------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 1d47c93..a6884a8 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -2,23 +2,6 @@ #include "gc_client.h" #include "graffiti.h" #include "keyvalue.h" -#include "steam/steam_api.h" - -static std::string GetLocalPlayerName() -{ - const char *name = nullptr; - if (SteamFriends()) - { - name = SteamFriends()->GetPersonaName(); - } - - if (name && name[0]) - { - return name; - } - - return "Player"; -} static bool GetItemPaintKitDefIndex(const CSOEconItem &item, const ItemSchema &schema, uint32_t &paintKitDefIndex) { @@ -862,7 +845,8 @@ void ClientGC::Craft(GCMessageRead &messageRead) const ItemInfo *itemInfo = m_inventory.GetItemSchema().ItemInfoByDefIndex(craftedItem->def_index()); std::string itemName = itemInfo ? itemInfo->m_name : "Unknown Item"; - std::string chatMessage = GetLocalPlayerName(); + uint32_t accountId = m_steamId & 0xffffffff; + std::string chatMessage = "Player " + std::to_string(accountId); chatMessage += " has fulfilled a contract and received: "; chatMessage += itemName; diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 53819a3..a7639a3 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -98,17 +98,6 @@ const CSOEconItem *Inventory::GetItem(uint64_t itemId) const return &it->second; } -const CSOEconItem *Inventory::GetItem(uint64_t itemId) const -{ - auto it = m_items.find(itemId); - if (it == m_items.end()) - { - return nullptr; - } - - return &it->second; -} - CSOEconItem &Inventory::AllocateItem(uint32_t highItemId) { // Players fuck up their inventory files constantly and end up with item id collisions... @@ -1254,7 +1243,6 @@ bool Inventory::TradeUp(const std::vector &inputItemIds, CMsgSOSingleObject &newItem, CMsgGCItemCustomizationNotification ¬ification, CSOEconItem **outCraftedItem) - CSOEconItem **outCraftedItem) { if (inputItemIds.size() != 10) { diff --git a/csgo_gc/inventory.h b/csgo_gc/inventory.h index 1fda5fa..5df74fd 100644 --- a/csgo_gc/inventory.h +++ b/csgo_gc/inventory.h @@ -76,8 +76,7 @@ class Inventory bool TradeUp(const std::vector &inputItemIds, std::vector &destroyItems, CMsgSOSingleObject &newItem, - CMsgGCItemCustomizationNotification ¬ification, - CSOEconItem **outCraftedItem = nullptr); + CMsgGCItemCustomizationNotification ¬ification, CSOEconItem **outCraftedItem = nullptr); // returns the item id and adds the item to the provided CMsgSOMultipleObjects @@ -141,12 +140,4 @@ class Inventory uint32_t m_lastHighItemId{}; ItemMap m_items; std::vector m_defaultEquips; - -public: - const CSOEconItem* GetItem(uint64_t itemId) const { - auto it = m_items.find(itemId); - return it != m_items.end() ? &it->second : nullptr; - } - - const ItemSchema& GetItemSchema() const { return m_itemSchema; } };