Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!

Expand Down
74 changes: 74 additions & 0 deletions csgo_gc/gc_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -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);
}
5 changes: 5 additions & 0 deletions csgo_gc/gc_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
163 changes: 163 additions & 0 deletions csgo_gc/inventory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t>(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<uint32_t>(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<CMsgSOSingleObject> &update)
{
CSOEconItem &item = CreateItem(defIndex, ItemOriginPurchased, UnacknowledgedPurchased);
Expand Down
31 changes: 31 additions & 0 deletions csgo_gc/inventory.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ class Inventory
CMsgSOSingleObject &destroy,
CMsgGCItemCustomizationNotification &notification);

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<CMsgSOSingleObject> &update);
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions csgo_gc/item_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ class ItemSchema

enum Item
{
ItemCasket = 1201,
ItemSticker = 1209,
ItemMusicKit = 1314,
ItemSpray = 1348,
ItemSprayPaint = 1349,
ItemPatch = 4609
Expand Down Expand Up @@ -223,6 +226,11 @@ class ItemSchema

AttributeSpraysRemaining = 232,
AttributeSprayTintId = 233,

AttributeCasketItemsCount = 270,
AttributeCasketModificationDate = 271,
AttributeCasketIdLow = 272,
AttributeCasketIdHigh = 273,
};

private:
Expand Down