Make descriptions optional, open constructors for plugins

In rare occurances, we might not have a description assigned to the item. This is most notable in inactive trade offers, but we permit this to happen even in inventory fetches.

Assigning "default" description is unwanted if caller wants to have a way to determine that description wasn't there to begin with. It makes more sense to make it nullable and *expect* it to be null, then caller can do appropriate checking and decide what they want to do with that.

Also open constructors for plugins usage in case they'd like to construct assets manually, e.g. for sending.
This commit is contained in:
Archi
2024-03-18 11:53:14 +01:00
parent 91b09dc43f
commit 5a07f8a2a3
4 changed files with 15 additions and 61 deletions

View File

@@ -28,45 +28,34 @@ using JetBrains.Annotations;
namespace ArchiSteamFarm.Steam.Data;
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset
[PublicAPI]
public sealed class Asset {
[PublicAPI]
public const uint SteamAppID = 753;
[PublicAPI]
public const ulong SteamCommunityContextID = 6;
[PublicAPI]
public const ulong SteamPointsShopInstanceID = 3865004543;
[JsonIgnore]
[PublicAPI]
public bool IsSteamPointsShopItem => !Tradable && (InstanceID == SteamPointsShopInstanceID);
[JsonIgnore]
[PublicAPI]
public bool Marketable => Description?.Marketable ?? false;
[JsonIgnore]
[PublicAPI]
public EAssetRarity Rarity => Description?.Rarity ?? EAssetRarity.Unknown;
[JsonIgnore]
[PublicAPI]
public uint RealAppID => Description?.RealAppID ?? 0;
[JsonIgnore]
[PublicAPI]
public bool Tradable => Description?.Tradable ?? false;
[JsonIgnore]
[PublicAPI]
public EAssetType Type => Description?.Type ?? EAssetType.Unknown;
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("amount")]
[JsonRequired]
[PublicAPI]
public uint Amount { get; internal set; }
[JsonInclude]
@@ -76,29 +65,24 @@ public sealed class Asset {
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("assetid")]
[PublicAPI]
public ulong AssetID { get; private init; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("classid")]
[PublicAPI]
public ulong ClassID { get; private init; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("contextid")]
[PublicAPI]
public ulong ContextID { get; private init; }
[JsonIgnore]
[PublicAPI]
public InventoryDescription? Description { get; internal set; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("instanceid")]
[PublicAPI]
public ulong InstanceID { get; private init; }
[JsonInclude]
@@ -109,19 +93,19 @@ public sealed class Asset {
init => AssetID = value;
}
internal Asset(uint appID, ulong contextID, ulong classID, uint amount, InventoryDescription description, ulong assetID = 0, ulong instanceID = 0) {
[PublicAPI]
public Asset(uint appID, ulong contextID, ulong classID, uint amount, InventoryDescription? description = null, ulong assetID = 0, ulong instanceID = 0) {
ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(contextID);
ArgumentOutOfRangeException.ThrowIfZero(classID);
ArgumentOutOfRangeException.ThrowIfZero(amount);
ArgumentNullException.ThrowIfNull(description);
AppID = appID;
ContextID = contextID;
ClassID = classID;
Amount = amount;
Description = description;
Description = description;
AssetID = assetID;
InstanceID = instanceID;
}

View File

@@ -512,14 +512,13 @@ public sealed class InventoryDescription {
private uint? CachedRealAppID;
private EAssetType? CachedType;
internal InventoryDescription(CEconItem_Description description) {
public InventoryDescription(CEconItem_Description description) {
ArgumentNullException.ThrowIfNull(description);
Body = description;
}
// For self-created stubs
internal InventoryDescription(uint appID, ulong classID, ulong instanceID = 0, bool marketable = false, bool tradable = false, uint realAppID = 0, EAssetType type = EAssetType.Unknown, EAssetRarity rarity = EAssetRarity.Unknown) {
public InventoryDescription(uint appID, ulong classID, ulong instanceID = 0, bool marketable = false, bool tradable = false, uint realAppID = 0, EAssetType type = EAssetType.Unknown, EAssetRarity rarity = EAssetRarity.Unknown) {
ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(classID);

View File

@@ -230,28 +230,15 @@ public sealed class ArchiHandler : ClientMsgHandler {
descriptions.Add(key, new InventoryDescription(description));
}
foreach (CEcon_Asset? asset in response.assets) {
if (!assetIDs.Add(asset.assetid)) {
continue;
}
(ulong ClassID, ulong InstanceID) key = (asset.classid, asset.instanceid);
if (!descriptions.TryGetValue(key, out InventoryDescription? description)) {
// Best effort only
description = new InventoryDescription(appID, asset.classid, asset.instanceid);
descriptions.Add(key, description);
}
foreach (CEcon_Asset? asset in response.assets.Where(asset => assetIDs.Add(asset.assetid))) {
InventoryDescription? description = descriptions.GetValueOrDefault((asset.classid, asset.instanceid));
// Extra bulletproofing against Steam showing us middle finger
if ((tradableOnly && !description.Tradable) || (marketableOnly && !description.Marketable)) {
if ((tradableOnly && (description?.Tradable != true)) || (marketableOnly && (description?.Marketable != true))) {
continue;
}
Asset convertedAsset = new(asset.appid, asset.contextid, asset.classid, (uint) asset.amount, description, asset.assetid, asset.instanceid);
yield return convertedAsset;
yield return new Asset(asset.appid, asset.contextid, asset.classid, (uint) asset.amount, description, asset.assetid, asset.instanceid);
}
if (!response.more_items) {

View File

@@ -362,22 +362,11 @@ public sealed class ArchiWebHandler : IDisposable {
descriptions.TryAdd(key, description);
}
foreach (Asset asset in response.Content.Assets) {
if (!assetIDs.Add(asset.AssetID)) {
continue;
foreach (Asset asset in response.Content.Assets.Where(asset => assetIDs.Add(asset.AssetID))) {
if (descriptions.TryGetValue((asset.ClassID, asset.InstanceID), out InventoryDescription? description)) {
asset.Description = description;
}
(ulong ClassID, ulong InstanceID) key = (asset.ClassID, asset.InstanceID);
if (!descriptions.TryGetValue(key, out InventoryDescription? description)) {
// Best effort only
description = new InventoryDescription(appID, asset.ClassID, asset.InstanceID);
descriptions.Add(key, description);
}
asset.Description = description;
yield return asset;
}
@@ -2390,14 +2379,9 @@ public sealed class ArchiWebHandler : IDisposable {
foreach (Asset asset in assets) {
(uint AppID, ulong ClassID, ulong InstanceID) key = (asset.AppID, asset.ClassID, asset.InstanceID);
if (!descriptions.TryGetValue(key, out InventoryDescription? description)) {
// Best effort only - we can guarantee tradable property at best, and only at the time of the trade offer
description = new InventoryDescription(asset.AppID, asset.ClassID, asset.InstanceID, tradable: true);
descriptions.Add(key, description);
if (descriptions.TryGetValue(key, out InventoryDescription? description)) {
asset.Description = description;
}
asset.Description = description;
}
}