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! 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: