Skip to content

Commit

Permalink
Add support for trade up crafting
Browse files Browse the repository at this point in the history
  • Loading branch information
Citrinate committed Dec 12, 2024
1 parent c46222e commit 0e16c6f
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 38 deletions.
2 changes: 1 addition & 1 deletion CS2Interface/CS2Interface.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Authors>Citrinate</Authors>
<AssemblyVersion>1.0.12.1</AssemblyVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TargetFramework>net9.0</TargetFramework>
Expand Down
129 changes: 110 additions & 19 deletions CS2Interface/CSClient/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ internal async Task<bool> Run() {
client_launcher = 0,
steam_launcher = 0
}};
var fetcher = new GCFetcher<CMsgClientHello, CMsgClientWelcome>((uint) EGCBaseClientMsg.k_EMsgGCClientWelcome);
// var fetcher = new GCFetcher<CMsgClientHello, CMsgClientWelcome>((uint) EGCBaseClientMsg.k_EMsgGCClientWelcome);
var fetcher = new GCFetcher((uint) EGCBaseClientMsg.k_EMsgGCClientWelcome);

Bot.ArchiLogger.LogGenericDebug(Strings.SendingHello);

if (fetcher.Fetch(this, msg, resendMsg: true) == null) {
if (fetcher.Fetch<CMsgClientWelcome>(this, msg, resendMsg: true) == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.GCConnectionFailed);
}

Expand Down Expand Up @@ -267,14 +268,18 @@ internal async Task<CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse> In
param_m = param_m
}};

var fetcher = new GCFetcher<CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockRequest, CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse> {
var fetcher = new GCFetcher {
GCResponseMsgType = (uint) ECsgoGCMsg.k_EMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse,
VerifyFunc = response => response.Body.iteminfo.itemid == param_a
VerifyResponse = message => {
var response = new ClientGCMsgProtobuf<CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse>(message);

return response.Body.iteminfo.itemid == param_a;
}
};

Bot.ArchiLogger.LogGenericDebug(String.Format("{0}: s {1} a {2} d {3} m {4}", Strings.InspectingItem, param_s, param_a, param_d, param_m));

var response = fetcher.Fetch(this, msg);
var response = fetcher.Fetch<CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse>(this, msg);
if (response == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}
Expand Down Expand Up @@ -304,14 +309,18 @@ internal async Task<CMsgGCCStrike15_v2_PlayersProfile> RequestPlayerProfile(ulon
request_level = 32
}};

var fetcher = new GCFetcher<CMsgGCCStrike15_v2_ClientRequestPlayersProfile, CMsgGCCStrike15_v2_PlayersProfile>{
var fetcher = new GCFetcher{
GCResponseMsgType = (uint) ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayersProfile,
VerifyFunc = response => response.Body.account_profiles.FirstOrDefault()?.account_id == account_id
VerifyResponse = message => {
var response = new ClientGCMsgProtobuf<CMsgGCCStrike15_v2_PlayersProfile>(message);

return response.Body.account_profiles.FirstOrDefault()?.account_id == account_id;
}
};

Bot.ArchiLogger.LogGenericDebug(String.Format("{0}: {1}", Strings.InspectingPlayer, steam_id));

var response = fetcher.Fetch(this, msg);
var response = fetcher.Fetch<CMsgGCCStrike15_v2_PlayersProfile>(this, msg);
if (response == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}
Expand Down Expand Up @@ -358,14 +367,18 @@ internal async Task<List<InventoryItem>> GetCasketContents(ulong casket_id) {
item_item_id = casket_id
}};

var fetcher = new GCFetcher<CMsgCasketItem, CMsgGCItemCustomizationNotification>{
var fetcher = new GCFetcher{
GCResponseMsgType = (uint) EGCItemMsg.k_EMsgGCItemCustomizationNotification,
VerifyFunc = response => response.Body.item_id.FirstOrDefault() == casket_id && response.Body.request == (uint) EGCItemCustomizationNotification.k_EGCItemCustomizationNotification_CasketContents
VerifyResponse = message => {
var response = new ClientGCMsgProtobuf<CMsgGCItemCustomizationNotification>(message);

return response.Body.item_id.FirstOrDefault() == casket_id && response.Body.request == (uint) EGCItemCustomizationNotification.k_EGCItemCustomizationNotification_CasketContents;
}
};

Bot.ArchiLogger.LogGenericDebug(String.Format(Strings.OpeningCasket, casket_id));

if (fetcher.Fetch(this, msg) == null) {
if (fetcher.Fetch<CMsgGCItemCustomizationNotification>(this, msg) == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}

Expand Down Expand Up @@ -395,8 +408,13 @@ internal async Task<bool> AddItemToCasket(ulong casket_id, ulong item_id) {
throw new ClientException(EClientExceptionType.Failed, Strings.InventoryNotLoaded);
}

if (Inventory.Values.FirstOrDefault(x => x.ItemInfo.id == item_id) == null) {
throw new ClientException(EClientExceptionType.BadRequest, Strings.InventoryItemNotFound);
InventoryItem? item = Inventory.Values.FirstOrDefault(x => x.ItemInfo.id == item_id);
if (item == null) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InventoryItemNotFound, item_id));
}

if (item.CasketID != null) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InventoryItemFoundInCrate, item_id));
}

InventoryItem? casket = Inventory.Values.FirstOrDefault(x => x.ItemInfo.id == casket_id);
Expand All @@ -419,9 +437,11 @@ internal async Task<bool> AddItemToCasket(ulong casket_id, ulong item_id) {
item_item_id = item_id
}};

var fetcher = new GCFetcher<CMsgCasketItem, CMsgSOSingleObject>{
var fetcher = new GCFetcher{
GCResponseMsgType = (uint) ESOMsg.k_ESOMsg_Destroy,
VerifyFunc = response => {
VerifyResponse = message => {
var response = new ClientGCMsgProtobuf<CMsgSOSingleObject>(message);

if (response.Body.type_id != 1) {
// Ignore non-inventory changes
return false;
Expand All @@ -440,7 +460,7 @@ internal async Task<bool> AddItemToCasket(ulong casket_id, ulong item_id) {

Bot.ArchiLogger.LogGenericDebug(String.Format(Strings.AddingItemToCasket, item_id, casket_id));

if (fetcher.Fetch(this, msg) == null) {
if (fetcher.Fetch<CMsgSOSingleObject>(this, msg) == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}

Expand Down Expand Up @@ -474,9 +494,11 @@ internal async Task<bool> RemoveItemFromCasket(ulong casket_id, ulong item_id) {
item_item_id = item_id
}};

var fetcher = new GCFetcher<CMsgCasketItem, CMsgSOSingleObject>{
var fetcher = new GCFetcher{
GCResponseMsgType = (uint) ESOMsg.k_ESOMsg_Create,
VerifyFunc = response => {
VerifyResponse = message => {
var response = new ClientGCMsgProtobuf<CMsgSOSingleObject>(message);

if (response.Body.type_id != 1) {
// Ignore non-inventory changes
return false;
Expand All @@ -495,7 +517,7 @@ internal async Task<bool> RemoveItemFromCasket(ulong casket_id, ulong item_id) {

Bot.ArchiLogger.LogGenericDebug(String.Format(Strings.RemovingItemFromCasket, item_id, casket_id));

if (fetcher.Fetch(this, msg) == null) {
if (fetcher.Fetch<CMsgSOSingleObject>(this, msg) == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}

Expand All @@ -504,6 +526,75 @@ internal async Task<bool> RemoveItemFromCasket(ulong casket_id, ulong item_id) {
GCSemaphore.Release();
}
}

internal async Task<GCMsg.MsgCraftResponse> Craft(ushort recipe, List<ulong> item_ids) {
if (!HasGCSession) {
throw new ClientException(EClientExceptionType.Failed, Strings.ClientNotConnectedToGC);
}

if (Inventory == null) {
throw new ClientException(EClientExceptionType.Failed, Strings.InventoryNotLoaded);
}

if (item_ids.Count > ushort.MaxValue) {
throw new ClientException(EClientExceptionType.BadRequest, Strings.InvalidCraftTooManyInputs);
}

{
HashSet<ulong> duplicateIDCheck = new();
foreach (ulong item_id in item_ids) {
InventoryItem? inventoryItem = Inventory.Values.FirstOrDefault(x => x.ItemInfo.id == item_id);

if (inventoryItem == null) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InventoryItemNotFound, item_id));
}

if (inventoryItem.CasketID != null) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InventoryItemFoundInCrate, item_id));
}

if (inventoryItem.Moveable != true) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InvalidCraftInput, item_id));
}

if (!duplicateIDCheck.Add(item_id)) {
throw new ClientException(EClientExceptionType.BadRequest, String.Format(Strings.InvalidCraftDuplicateInput, item_id));
}
}
}

await GCSemaphore.WaitAsync().ConfigureAwait(false);

try {
var msg = new ClientGCMsg<GCMsg.MsgCraft>() {
Body = {
Recipe = recipe,
ItemCount = (ushort) item_ids.Count,
ItemIDs = item_ids
}
};

var fetcher = new GCFetcher{
GCResponseMsgType = (uint) EGCItemMsg.k_EMsgGCCraftResponse,
VerifyResponse = message => {
var response = new ClientGCMsg<GCMsg.MsgCraftResponse>(message);

return response.Body.Recipe == recipe || response.Body.Recipe == GCMsg.MsgCraft.UnknownRecipe;
}
};

Bot.ArchiLogger.LogGenericDebug(String.Format(Strings.CraftingItem, recipe, String.Join(", ", item_ids)));

var response = fetcher.RawFetch<GCMsg.MsgCraftResponse>(this, msg);
if (response == null) {
throw new ClientException(EClientExceptionType.Timeout, Strings.RequestTimeout);
}

return response.Body;
} finally {
GCSemaphore.Release();
}
}
}

[Flags]
Expand Down
38 changes: 27 additions & 11 deletions CS2Interface/CSClient/GCFetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,45 @@
using ProtoBuf;
using SteamKit2;
using SteamKit2.GC;
using SteamKit2.Internal;

namespace CS2Interface {
internal sealed class GCFetcher<TMsg, TResponse> where TMsg : IExtensible, new() where TResponse : IExtensible, new() {
internal sealed class GCFetcher {
internal int TTLSeconds { get; set; } = 30;
internal uint GCResponseMsgType { get; set; }
internal Func<ClientGCMsgProtobuf<TResponse>, bool>? VerifyFunc { get; set; }
internal Func<IPacketGCMsg, bool>? VerifyResponse { get; set; }
private bool GotMatch = false;
IPacketGCMsg? PacketMsg;

internal GCFetcher() {}

internal GCFetcher(uint gcResponseMsgType, int? ttlSeconds = null, Func<ClientGCMsgProtobuf<TResponse>, bool>? verify = null) {
internal GCFetcher(uint gcResponseMsgType, int? ttlSeconds = null, Func<IPacketGCMsg, bool>? verifyResponse = null) {
GCResponseMsgType = gcResponseMsgType;
TTLSeconds = ttlSeconds ?? TTLSeconds;
VerifyFunc = verify;
VerifyResponse = verifyResponse;
}

internal ClientGCMsgProtobuf<TResponse>? Fetch(Client client, ClientGCMsgProtobuf<TMsg> msg, bool resendMsg = false) {
internal ClientGCMsgProtobuf<TResponse>? Fetch<TResponse>(Client client, IClientGCMsg msg, bool resendMsg = false) where TResponse : IExtensible, new() {
GetResponse(client, msg, resendMsg);

if (!GotMatch || PacketMsg == null) {
return null;
}

return new ClientGCMsgProtobuf<TResponse>(PacketMsg);
}

internal ClientGCMsg<TResponse>? RawFetch<TResponse>(Client client, IClientGCMsg msg, bool resendMsg = false) where TResponse : IGCSerializableMessage, new() {
GetResponse(client, msg, resendMsg);

if (!GotMatch || PacketMsg == null) {
return null;
}

return new ClientGCMsg<TResponse>(PacketMsg);
}

private void GetResponse(Client client, IClientGCMsg msg, bool resendMsg = false) {
client.OnGCMessageRecieved += CheckMatch;
client.GameCoordinator.Send(msg, Client.AppID);

Expand All @@ -41,19 +62,14 @@ internal GCFetcher(uint gcResponseMsgType, int? ttlSeconds = null, Func<ClientGC
}

client.OnGCMessageRecieved -= CheckMatch;
if (!GotMatch || PacketMsg == null) {
return null;
}

return new ClientGCMsgProtobuf<TResponse>(PacketMsg);
}

internal void CheckMatch(SteamGameCoordinator.MessageCallback callback) {
if (callback.EMsg != GCResponseMsgType) {
return;
}

if (VerifyFunc != null && !VerifyFunc(new ClientGCMsgProtobuf<TResponse>(callback.Message))) {
if (VerifyResponse != null && !VerifyResponse(callback.Message)) {
return;
}

Expand Down
86 changes: 86 additions & 0 deletions CS2Interface/CSClient/GCMsg.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
using SteamKit2.GC.CSGO.Internal;
using SteamKit2.Internal;

namespace CS2Interface {
public class GCMsg {
public class MsgCraft : IGCSerializableMessage {
public const ushort UnknownRecipe = 0xFFFF;

public ushort Recipe;
public ushort ItemCount;
public List<ulong> ItemIDs;

public MsgCraft() {
Recipe = 0;
ItemCount = 0;
ItemIDs = [];
}

public uint GetEMsg() {
return (uint) EGCItemMsg.k_EMsgGCCraft;
}

public void Serialize(Stream stream) {
BinaryWriter bw = new BinaryWriter(stream);

bw.Write(Recipe);
bw.Write(ItemCount);
foreach(ulong ItemID in ItemIDs) {
bw.Write(ItemID);
}
}

public void Deserialize(Stream stream) {
throw new NotImplementedException();
}
}

public class MsgCraftResponse : IGCSerializableMessage {
[JsonInclude]
[JsonPropertyName("recipe")]
public ushort Recipe;

[JsonInclude]
[JsonPropertyName("unknown")]
public uint Unknown;

[JsonInclude]
[JsonPropertyName("itemcount")]
public ushort ItemCount;

[JsonInclude]
[JsonPropertyName("itemids")]
public List<ulong> ItemIDs;

public MsgCraftResponse() {
Recipe = 0;
Unknown = 0;
ItemCount = 0;
ItemIDs = [];
}

public uint GetEMsg() {
return (uint) EGCItemMsg.k_EMsgGCCraftResponse;
}

public void Serialize(Stream stream) {
throw new NotImplementedException();
}

public void Deserialize(Stream stream) {
BinaryReader br = new BinaryReader(stream);

Recipe = br.ReadUInt16();
Unknown = br.ReadUInt32();
ItemCount = br.ReadUInt16();
for (int i = 0; i < ItemCount; i++) {
ItemIDs.Add(br.ReadUInt64());
}
}
}
}
}
Loading

0 comments on commit 0e16c6f

Please sign in to comment.