From deb12e334694799a92abca7abcebd2676431ee28 Mon Sep 17 00:00:00 2001 From: drico <132640189+dricotec@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:16:51 +0400 Subject: [PATCH 1/8] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bad5c16..a1a249d 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 +- Storage Units - 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 (trade ups, souvenirs, 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 01201f28acd449d8dbe70dd4baac7041e07ad7e1 Mon Sep 17 00:00:00 2001 From: drico <132640189+dricotec@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:17:30 +0400 Subject: [PATCH 2/8] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bad5c16..37e381f 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 +- StatTrak Swaps - 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 (trade ups, souvenirs, Storage Units...) 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 042561523380c049ae463d224507810584fff1f0 Mon Sep 17 00:00:00 2001 From: dricotec Date: Wed, 29 Apr 2026 11:23:00 +0400 Subject: [PATCH 3/8] Implemented Storage Units --- csgo_gc/gc_client.cpp | 74 +++++++++++++++++++ csgo_gc/gc_client.h | 5 ++ csgo_gc/inventory.cpp | 163 ++++++++++++++++++++++++++++++++++++++++++ csgo_gc/inventory.h | 31 ++++++++ csgo_gc/item_schema.h | 8 +++ 5 files changed, 281 insertions(+) diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 255d735..210ada1 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -96,6 +96,18 @@ void ClientGC::HandleMessage(uint32_t type, const void *data, uint32_t size) StorePurchaseFinalize(messageRead); break; + case k_EMsgGCCasketItemLoadContents: + ProcessStorageInspect(messageRead); + break; + + case k_EMsgGCCasketItemAdd: + ProcessStorageDeposit(messageRead); + break; + + case k_EMsgGCCasketItemExtract: + ProcessStorageWithdraw(messageRead); + break; + default: Platform::Print("ClientGC::HandleMessage: unhandled protobuf message %s\n", MessageName(messageRead.TypeUnmasked())); @@ -747,3 +759,65 @@ void ClientGC::RemoveItemName(GCMessageRead &messageRead) assert(false); } } + +void ClientGC::DispatchStorageResult(const Inventory::StorageTransaction &tx) +{ + using SR = Inventory::StorageResult; + + switch (tx.outcome) + { + case SR::Success: + case SR::CapacityExceeded: + { + CMsgGCItemCustomizationNotification notice; + notice.set_request(tx.notificationType); + notice.add_item_id(tx.affectedContainerId); + + if (tx.Succeeded()) + { + SendMessageToGame(false, k_ESOMsg_Update, tx.itemData); + SendMessageToGame(false, k_ESOMsg_Update, tx.containerData); + } + SendMessageToGame(false, k_EMsgGCItemCustomizationNotification, notice); + } + break; + + case SR::ContainerNotFound: + case SR::ItemNotFound: + case SR::InvalidContainerType: + case SR::InternalError: + break; + } +} + +void ClientGC::ProcessStorageInspect(GCMessageRead &messageRead) +{ + CMsgCasketItem msg; + if (!messageRead.ReadProtobuf(msg)) + return; + + CMsgGCItemCustomizationNotification notice; + notice.set_request(k_EGCItemCustomizationNotification_CasketContents); + notice.add_item_id(msg.casket_item_id()); + SendMessageToGame(false, k_EMsgGCItemCustomizationNotification, notice); +} + +void ClientGC::ProcessStorageDeposit(GCMessageRead &messageRead) +{ + CMsgCasketItem msg; + if (!messageRead.ReadProtobuf(msg)) + return; + + auto tx = m_inventory.DepositItemToStorage(msg.casket_item_id(), msg.item_item_id()); + DispatchStorageResult(tx); +} + +void ClientGC::ProcessStorageWithdraw(GCMessageRead &messageRead) +{ + CMsgCasketItem msg; + if (!messageRead.ReadProtobuf(msg)) + return; + + auto tx = m_inventory.WithdrawItemFromStorage(msg.casket_item_id(), msg.item_item_id()); + DispatchStorageResult(tx); +} diff --git a/csgo_gc/gc_client.h b/csgo_gc/gc_client.h index 1e52c7b..cf79f74 100644 --- a/csgo_gc/gc_client.h +++ b/csgo_gc/gc_client.h @@ -40,6 +40,11 @@ class ClientGC final : public SharedGC void NameBaseItem(GCMessageRead &messageRead); void RemoveItemName(GCMessageRead &messageRead); + void ProcessStorageInspect(GCMessageRead &messageRead); + void ProcessStorageDeposit(GCMessageRead &messageRead); + void ProcessStorageWithdraw(GCMessageRead &messageRead); + void DispatchStorageResult(const Inventory::StorageTransaction &tx); + void BuildMatchmakingHello(CMsgGCCStrike15_v2_MatchmakingGC2ClientHello &message); void BuildClientWelcome(CMsgClientWelcome &message, const CMsgCStrike15Welcome &csWelcome, const CMsgGCCStrike15_v2_MatchmakingGC2ClientHello &matchmakingHello); diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 60c3a51..a9d3a46 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -1085,6 +1085,169 @@ bool Inventory::RemoveItemName(uint64_t itemId, return true; } +Inventory::StorageItemPair Inventory::ResolveStorageItems(uint64_t storageId, uint64_t targetId) +{ + StorageItemPair result{ nullptr, nullptr }; + + auto storageIt = m_items.find(storageId); + auto targetIt = m_items.find(targetId); + + if (storageIt != m_items.end()) + result.storage = &storageIt->second; + if (targetIt != m_items.end()) + result.target = &targetIt->second; + + return result; +} + +void Inventory::EmbedStorageReference(CSOEconItem &item, uint64_t storageId) +{ + auto *attrLow = item.add_attribute(); + auto *attrHigh = item.add_attribute(); + + attrLow->set_def_index(ItemSchema::AttributeCasketIdLow); + attrHigh->set_def_index(ItemSchema::AttributeCasketIdHigh); + + m_itemSchema.SetAttributeUint32(attrLow, storageId & 0xFFFFFFFF); + m_itemSchema.SetAttributeUint32(attrHigh, storageId >> 32); + + item.clear_equipped_state(); +} + +void Inventory::StripStorageReference(CSOEconItem &item) +{ + auto *attrs = item.mutable_attribute(); + + for (int i = attrs->size() - 1; i >= 0; --i) + { + uint32_t idx = attrs->Get(i).def_index(); + bool isStorageAttr = (idx == ItemSchema::AttributeCasketIdLow || + idx == ItemSchema::AttributeCasketIdHigh); + if (isStorageAttr) + attrs->DeleteSubrange(i, 1); + } +} + +bool Inventory::ModifyStorageCounter(CSOEconItem &storage, int delta) +{ + int countIdx = -1, dateIdx = -1; + + for (int i = 0; i < storage.attribute_size(); ++i) + { + switch (storage.attribute(i).def_index()) + { + case ItemSchema::AttributeCasketItemsCount: + countIdx = i; + break; + case ItemSchema::AttributeCasketModificationDate: + dateIdx = i; + break; + } + } + + if (countIdx < 0) return false; + + auto *countAttr = storage.mutable_attribute(countIdx); + int32_t current = static_cast(m_itemSchema.AttributeUint32(countAttr)); + int32_t updated = current + delta; + + if (updated < 0 || updated > 1000) + return false; + + m_itemSchema.SetAttributeUint32(countAttr, updated); + + if (dateIdx >= 0) + { + auto *dateAttr = storage.mutable_attribute(dateIdx); + m_itemSchema.SetAttributeUint32(dateAttr, static_cast(time(nullptr))); + } + + return true; +} + +Inventory::StorageTransaction Inventory::DepositItemToStorage(uint64_t storageId, uint64_t itemId) +{ + StorageTransaction tx{}; + tx.affectedContainerId = storageId; + + auto [storage, target] = ResolveStorageItems(storageId, itemId); + + if (!storage) + { + tx.outcome = StorageResult::ContainerNotFound; + return tx; + } + + if (!target) + { + tx.outcome = StorageResult::ItemNotFound; + return tx; + } + + if (storage->def_index() != ItemSchema::ItemCasket) + { + tx.outcome = StorageResult::InvalidContainerType; + return tx; + } + + if (!ModifyStorageCounter(*storage, +1)) + { + tx.outcome = StorageResult::CapacityExceeded; + tx.notificationType = k_EGCItemCustomizationNotification_CasketTooFull; + return tx; + } + + EmbedStorageReference(*target, storageId); + + ToSingleObject(tx.itemData, *target); + ToSingleObject(tx.containerData, *storage); + tx.notificationType = k_EGCItemCustomizationNotification_CasketAdded; + tx.outcome = StorageResult::Success; + + return tx; +} + +Inventory::StorageTransaction Inventory::WithdrawItemFromStorage(uint64_t storageId, uint64_t itemId) +{ + StorageTransaction tx{}; + tx.affectedContainerId = storageId; + + auto [storage, target] = ResolveStorageItems(storageId, itemId); + + if (!storage) + { + tx.outcome = StorageResult::ContainerNotFound; + return tx; + } + + if (!target) + { + tx.outcome = StorageResult::ItemNotFound; + return tx; + } + + if (storage->def_index() != ItemSchema::ItemCasket) + { + tx.outcome = StorageResult::InvalidContainerType; + return tx; + } + + if (!ModifyStorageCounter(*storage, -1)) + { + tx.outcome = StorageResult::InternalError; + return tx; + } + + StripStorageReference(*target); + + ToSingleObject(tx.itemData, *target); + ToSingleObject(tx.containerData, *storage); + tx.notificationType = k_EGCItemCustomizationNotification_CasketRemoved; + tx.outcome = StorageResult::Success; + + return tx; +} + uint64_t Inventory::PurchaseItem(uint32_t defIndex, std::vector &update) { CSOEconItem &item = CreateItem(defIndex, ItemOriginPurchased, UnacknowledgedPurchased); diff --git a/csgo_gc/inventory.h b/csgo_gc/inventory.h index cad21a0..16a5e5c 100644 --- a/csgo_gc/inventory.h +++ b/csgo_gc/inventory.h @@ -68,6 +68,31 @@ class Inventory CMsgSOSingleObject &destroy, CMsgGCItemCustomizationNotification ¬ification); + enum class StorageResult + { + Success, + CapacityExceeded, + ItemNotFound, + ContainerNotFound, + InvalidContainerType, + InternalError + }; + + struct StorageTransaction + { + CMsgSOSingleObject itemData; + CMsgSOSingleObject containerData; + EGCItemCustomizationNotification notificationType; + uint64_t affectedContainerId; + StorageResult outcome; + + bool Succeeded() const { return outcome == StorageResult::Success; } + bool ReachedCapacity() const { return outcome == StorageResult::CapacityExceeded; } + }; + + StorageTransaction DepositItemToStorage(uint64_t storageId, uint64_t itemId); + StorageTransaction WithdrawItemFromStorage(uint64_t storageId, uint64_t itemId); + // 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); @@ -123,6 +148,12 @@ class Inventory ToSingleObject(message, SOTypeDefaultEquippedDefinitionInstanceClient, object); } + struct StorageItemPair { CSOEconItem* storage; CSOEconItem* target; }; + StorageItemPair ResolveStorageItems(uint64_t storageId, uint64_t targetId); + void EmbedStorageReference(CSOEconItem &item, uint64_t storageId); + void StripStorageReference(CSOEconItem &item); + bool ModifyStorageCounter(CSOEconItem &storage, int delta); + const uint64_t m_steamId; ItemSchema m_itemSchema; Random m_random; diff --git a/csgo_gc/item_schema.h b/csgo_gc/item_schema.h index 6c573b1..55b0e6d 100644 --- a/csgo_gc/item_schema.h +++ b/csgo_gc/item_schema.h @@ -177,6 +177,9 @@ class ItemSchema enum Item { + ItemCasket = 1201, + ItemSticker = 1209, + ItemMusicKit = 1314, ItemSpray = 1348, ItemSprayPaint = 1349, ItemPatch = 4609 @@ -223,6 +226,11 @@ class ItemSchema AttributeSpraysRemaining = 232, AttributeSprayTintId = 233, + + AttributeCasketItemsCount = 270, + AttributeCasketModificationDate = 271, + AttributeCasketIdLow = 272, + AttributeCasketIdHigh = 273, }; private: From 13845e91497e279d97f1c4d6415c9268484455b0 Mon Sep 17 00:00:00 2001 From: dricotec Date: Wed, 29 Apr 2026 11:27:24 +0400 Subject: [PATCH 4/8] Implemented Stattrak Swaps --- csgo_gc/gc_client.cpp | 39 +++++++++++++++++++ csgo_gc/gc_client.h | 3 ++ csgo_gc/inventory.cpp | 90 +++++++++++++++++++++++++++++++++++++++++++ csgo_gc/inventory.h | 25 ++++++++++++ 4 files changed, 157 insertions(+) diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 255d735..d15a47f 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -96,6 +96,10 @@ void ClientGC::HandleMessage(uint32_t type, const void *data, uint32_t size) StorePurchaseFinalize(messageRead); break; + case k_EMsgGCStatTrakSwap: + HandleCounterSwapRequest(messageRead); + break; + default: Platform::Print("ClientGC::HandleMessage: unhandled protobuf message %s\n", MessageName(messageRead.TypeUnmasked())); @@ -747,3 +751,38 @@ void ClientGC::RemoveItemName(GCMessageRead &messageRead) assert(false); } } + +void ClientGC::BroadcastSwapOutcome(const Inventory::CounterSwapResult &outcome) +{ + using Status = Inventory::CounterSwapStatus; + + if (outcome.status != Status::Completed) + return; + + if (outcome.toolRemoval.has_type_id()) + SendMessageToGame(true, k_ESOMsg_Destroy, outcome.toolRemoval); + + SendMessageToGame(true, k_ESOMsg_Update, outcome.weaponAUpdate); + SendMessageToGame(true, k_ESOMsg_Update, outcome.weaponBUpdate); + + CMsgGCItemCustomizationNotification notification; + notification.set_request(k_EGCItemCustomizationNotification_StatTrakSwap); + notification.add_item_id(outcome.weaponAId); + notification.add_item_id(outcome.weaponBId); + SendMessageToGame(false, k_EMsgGCItemCustomizationNotification, notification); +} + +void ClientGC::HandleCounterSwapRequest(GCMessageRead &messageRead) +{ + CMsgApplyStatTrakSwap request; + if (!messageRead.ReadProtobuf(request)) + return; + + auto outcome = m_inventory.PerformCounterSwap( + request.tool_item_id(), + request.item_1_item_id(), + request.item_2_item_id() + ); + + BroadcastSwapOutcome(outcome); +} diff --git a/csgo_gc/gc_client.h b/csgo_gc/gc_client.h index 1e52c7b..49f1f68 100644 --- a/csgo_gc/gc_client.h +++ b/csgo_gc/gc_client.h @@ -40,6 +40,9 @@ class ClientGC final : public SharedGC void NameBaseItem(GCMessageRead &messageRead); void RemoveItemName(GCMessageRead &messageRead); + void HandleCounterSwapRequest(GCMessageRead &messageRead); + void BroadcastSwapOutcome(const Inventory::CounterSwapResult &outcome); + void BuildMatchmakingHello(CMsgGCCStrike15_v2_MatchmakingGC2ClientHello &message); void BuildClientWelcome(CMsgClientWelcome &message, const CMsgCStrike15Welcome &csWelcome, const CMsgGCCStrike15_v2_MatchmakingGC2ClientHello &matchmakingHello); diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 60c3a51..5222306 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -1085,6 +1085,96 @@ bool Inventory::RemoveItemName(uint64_t itemId, return true; } +uint32_t* Inventory::GetKillCounterPtr(CSOEconItem &weapon) +{ + int attrCount = weapon.attribute_size(); + for (int i = 0; i < attrCount; ++i) + { + auto *attr = weapon.mutable_attribute(i); + if (attr->def_index() != ItemSchema::AttributeKillEater) + continue; + + static thread_local uint32_t valueHolder; + valueHolder = m_itemSchema.AttributeUint32(attr); + return &valueHolder; + } + return nullptr; +} + +void Inventory::ConsumeToolItem(uint64_t toolId, CMsgSOSingleObject &removalMsg) +{ + if (!GetConfig().DestroyUsedItems()) + return; + + auto it = m_items.find(toolId); + if (it == m_items.end()) + return; + + DestroyItem(it, removalMsg); +} + +Inventory::CounterSwapResult Inventory::PerformCounterSwap(uint64_t toolId, uint64_t weaponAId, uint64_t weaponBId) +{ + CounterSwapResult result{}; + result.weaponAId = weaponAId; + result.weaponBId = weaponBId; + + auto itA = m_items.find(weaponAId); + auto itB = m_items.find(weaponBId); + + bool weaponsExist = (itA != m_items.end()) && (itB != m_items.end()); + if (!weaponsExist) + { + result.status = CounterSwapStatus::WeaponMissing; + return result; + } + + CSOEconItem &weaponA = itA->second; + CSOEconItem &weaponB = itB->second; + + CSOEconItemAttribute *attrA = nullptr; + CSOEconItemAttribute *attrB = nullptr; + + for (int i = 0; i < weaponA.attribute_size(); ++i) + { + if (weaponA.attribute(i).def_index() == ItemSchema::AttributeKillEater) + { + attrA = weaponA.mutable_attribute(i); + break; + } + } + + for (int i = 0; i < weaponB.attribute_size(); ++i) + { + if (weaponB.attribute(i).def_index() == ItemSchema::AttributeKillEater) + { + attrB = weaponB.mutable_attribute(i); + break; + } + } + + bool bothHaveCounters = attrA && attrB; + if (!bothHaveCounters) + { + result.status = CounterSwapStatus::CounterAttributeAbsent; + return result; + } + + uint32_t valA = m_itemSchema.AttributeUint32(attrA); + uint32_t valB = m_itemSchema.AttributeUint32(attrB); + + m_itemSchema.SetAttributeUint32(attrA, valB); + m_itemSchema.SetAttributeUint32(attrB, valA); + + ConsumeToolItem(toolId, result.toolRemoval); + + ToSingleObject(result.weaponAUpdate, weaponA); + ToSingleObject(result.weaponBUpdate, weaponB); + result.status = CounterSwapStatus::Completed; + + return result; +} + uint64_t Inventory::PurchaseItem(uint32_t defIndex, std::vector &update) { CSOEconItem &item = CreateItem(defIndex, ItemOriginPurchased, UnacknowledgedPurchased); diff --git a/csgo_gc/inventory.h b/csgo_gc/inventory.h index cad21a0..09699c2 100644 --- a/csgo_gc/inventory.h +++ b/csgo_gc/inventory.h @@ -68,6 +68,28 @@ class Inventory CMsgSOSingleObject &destroy, CMsgGCItemCustomizationNotification ¬ification); + enum class CounterSwapStatus + { + Completed, + ToolMissing, + WeaponMissing, + CounterAttributeAbsent + }; + + struct CounterSwapResult + { + CounterSwapStatus status; + CMsgSOSingleObject toolRemoval; + CMsgSOSingleObject weaponAUpdate; + CMsgSOSingleObject weaponBUpdate; + uint64_t weaponAId; + uint64_t weaponBId; + + bool IsValid() const { return status == CounterSwapStatus::Completed; } + }; + + CounterSwapResult PerformCounterSwap(uint64_t toolId, uint64_t weaponAId, uint64_t weaponBId); + // 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); @@ -123,6 +145,9 @@ class Inventory ToSingleObject(message, SOTypeDefaultEquippedDefinitionInstanceClient, object); } + uint32_t* GetKillCounterPtr(CSOEconItem &weapon); + void ConsumeToolItem(uint64_t toolId, CMsgSOSingleObject &removalMsg); + const uint64_t m_steamId; ItemSchema m_itemSchema; Random m_random; 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 5/8] 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 6/8] 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 7/8] 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; } }; From b88f47152502448fe75deef2f1e6bd8224d698ca Mon Sep 17 00:00:00 2001 From: GT610 Date: Sun, 10 May 2026 11:28:48 +0800 Subject: [PATCH 8/8] Fix trade-up reviewer issues --- csgo_gc/inventory.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 7dada57..6b5db96 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -1721,7 +1721,7 @@ bool Inventory::TradeUp(const std::vector &inputItemIds, return false; } - uint32_t roll = m_random.Integer(0, 9); + uint32_t roll = m_random.Integer(0, static_cast(weightedCollections.size() - 1)); 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()); @@ -1827,11 +1827,6 @@ bool Inventory::TradeUp(const std::vector &inputItemIds, *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);