mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 06:00:46 +00:00
550 lines
15 KiB
C#
550 lines
15 KiB
C#
// ----------------------------------------------------------------------------------------------
|
|
// _ _ _ ____ _ _____
|
|
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
|
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
|
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
|
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
|
// ----------------------------------------------------------------------------------------------
|
|
// |
|
|
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
|
// Contact: JustArchi@JustArchi.net
|
|
// |
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// |
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
// |
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
using System;
|
|
using System.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text.Json.Serialization;
|
|
using ArchiSteamFarm.Core;
|
|
using ArchiSteamFarm.Helpers.Json;
|
|
using ArchiSteamFarm.Localization;
|
|
using JetBrains.Annotations;
|
|
using ProtoBuf;
|
|
using SteamKit2.Internal;
|
|
|
|
namespace ArchiSteamFarm.Steam.Data;
|
|
|
|
[PublicAPI]
|
|
public sealed class InventoryDescription {
|
|
[JsonIgnore]
|
|
public CEconItem_Description Body { get; } = new();
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("appid")]
|
|
[JsonRequired]
|
|
public uint AppID {
|
|
get => (uint) Body.appid;
|
|
private init => Body.appid = (int) value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("background_color")]
|
|
public string BackgroundColor {
|
|
get => Body.background_color;
|
|
private init => Body.background_color = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
|
[JsonPropertyName("classid")]
|
|
[JsonRequired]
|
|
public ulong ClassID {
|
|
get => Body.classid;
|
|
private init => Body.classid = value;
|
|
}
|
|
|
|
[JsonConverter(typeof(BooleanNumberConverter))]
|
|
[JsonInclude]
|
|
[JsonPropertyName("commodity")]
|
|
public bool Commodity {
|
|
get => Body.commodity;
|
|
private init => Body.commodity = value;
|
|
}
|
|
|
|
[JsonConverter(typeof(BooleanNumberConverter))]
|
|
[JsonInclude]
|
|
[JsonPropertyName("currency")]
|
|
public bool Currency {
|
|
get => Body.currency;
|
|
private init => Body.currency = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("descriptions")]
|
|
public ImmutableHashSet<ItemDescription> Descriptions {
|
|
get => Body.descriptions.Select(static description => new ItemDescription(description.type, description.value, description.color, description.label)).ToImmutableHashSet();
|
|
|
|
private init {
|
|
Body.descriptions.Clear();
|
|
|
|
Body.descriptions.AddRange(
|
|
value.Select(
|
|
static description => new CEconItem_DescriptionLine {
|
|
color = description.Color,
|
|
label = description.Label,
|
|
type = description.Type,
|
|
value = description.Value
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
#pragma warning disable CA1056 // This property is not guaranteed to have parsable Uri
|
|
[JsonInclude]
|
|
[JsonPropertyName("icon_url")]
|
|
public string IconURL {
|
|
get => Body.icon_url;
|
|
private init => Body.icon_url = value;
|
|
}
|
|
#pragma warning restore CA1056 // This property is not guaranteed to have parsable Uri
|
|
|
|
#pragma warning disable CA1056 // This property is not guaranteed to have parsable Uri
|
|
[JsonInclude]
|
|
[JsonPropertyName("icon_url_large")]
|
|
public string IconURLLarge {
|
|
get => Body.icon_url_large;
|
|
private init => Body.icon_url_large = value;
|
|
}
|
|
#pragma warning restore CA1056 // This property is not guaranteed to have parsable Uri
|
|
|
|
[JsonInclude]
|
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
|
[JsonPropertyName("instanceid")]
|
|
public ulong InstanceID {
|
|
get => Body.instanceid;
|
|
private init => Body.instanceid = value;
|
|
}
|
|
|
|
[JsonConverter(typeof(BooleanNumberConverter))]
|
|
[JsonInclude]
|
|
[JsonPropertyName("marketable")]
|
|
[JsonRequired]
|
|
public bool Marketable {
|
|
get => Body.marketable;
|
|
private init => Body.marketable = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("market_fee_app")]
|
|
public uint MarketFeeApp {
|
|
get => (uint) Body.market_fee_app;
|
|
private init => Body.market_fee_app = (int) value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("market_hash_name")]
|
|
public string MarketHashName {
|
|
get => Body.market_hash_name;
|
|
private init => Body.market_hash_name = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("market_name")]
|
|
public string MarketName {
|
|
get => Body.market_name;
|
|
private init => Body.market_name = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("name")]
|
|
public string Name {
|
|
get => Body.name;
|
|
private init => Body.name = value;
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("owner_actions")]
|
|
public ImmutableHashSet<ItemAction> OwnerActions {
|
|
get => Body.owner_actions.Select(static action => new ItemAction(action.link, action.name)).ToImmutableHashSet();
|
|
|
|
private init {
|
|
Body.owner_actions.Clear();
|
|
|
|
Body.owner_actions.AddRange(
|
|
value.Select(
|
|
static action => new CEconItem_Action {
|
|
link = action.Link,
|
|
name = action.Name
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
[JsonInclude]
|
|
[JsonPropertyName("type")]
|
|
public string TypeText {
|
|
get => Body.type;
|
|
private init => Body.type = value;
|
|
}
|
|
|
|
internal EAssetRarity Rarity {
|
|
get {
|
|
if (CachedRarity.HasValue) {
|
|
return CachedRarity.Value;
|
|
}
|
|
|
|
foreach (CEconItem_Tag? tag in Body.tags) {
|
|
switch (tag.category) {
|
|
case "droprate":
|
|
switch (tag.internal_name) {
|
|
case "droprate_0":
|
|
CachedRarity = EAssetRarity.Common;
|
|
|
|
return CachedRarity.Value;
|
|
case "droprate_1":
|
|
CachedRarity = EAssetRarity.Uncommon;
|
|
|
|
return CachedRarity.Value;
|
|
case "droprate_2":
|
|
CachedRarity = EAssetRarity.Rare;
|
|
|
|
return CachedRarity.Value;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.internal_name), tag.internal_name));
|
|
|
|
CachedRarity = EAssetRarity.Unknown;
|
|
|
|
return CachedRarity.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
CachedRarity = EAssetRarity.Unknown;
|
|
|
|
return CachedRarity.Value;
|
|
}
|
|
|
|
private init {
|
|
if (!Enum.IsDefined(value)) {
|
|
throw new InvalidEnumArgumentException(nameof(value), (int) value, typeof(EAssetRarity));
|
|
}
|
|
|
|
CachedRarity = value;
|
|
|
|
if (value == EAssetRarity.Unknown) {
|
|
return;
|
|
}
|
|
|
|
CEconItem_Tag? tag = Body.tags.FirstOrDefault(static tag => tag.category == "droprate");
|
|
|
|
if (tag == null) {
|
|
tag = new CEconItem_Tag { category = "droprate" };
|
|
|
|
Body.tags.Add(tag);
|
|
}
|
|
|
|
tag.internal_name = value switch {
|
|
EAssetRarity.Common => "droprate_0",
|
|
EAssetRarity.Uncommon => "droprate_1",
|
|
EAssetRarity.Rare => "droprate_2",
|
|
_ => throw new InvalidOperationException(nameof(value))
|
|
};
|
|
}
|
|
}
|
|
|
|
internal uint RealAppID {
|
|
get {
|
|
if (CachedRealAppID.HasValue) {
|
|
return CachedRealAppID.Value;
|
|
}
|
|
|
|
foreach (CEconItem_Tag? tag in Body.tags) {
|
|
switch (tag.category) {
|
|
case "Game":
|
|
if (string.IsNullOrEmpty(tag.internal_name) || (tag.internal_name.Length <= 4) || !tag.internal_name.StartsWith("app_", StringComparison.Ordinal)) {
|
|
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.internal_name), tag.internal_name));
|
|
|
|
break;
|
|
}
|
|
|
|
string appIDText = tag.internal_name[4..];
|
|
|
|
if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
|
|
ASF.ArchiLogger.LogNullError(appID);
|
|
|
|
break;
|
|
}
|
|
|
|
CachedRealAppID = appID;
|
|
|
|
return CachedRealAppID.Value;
|
|
}
|
|
}
|
|
|
|
CachedRealAppID = 0;
|
|
|
|
return CachedRealAppID.Value;
|
|
}
|
|
|
|
private init {
|
|
CachedRealAppID = value;
|
|
|
|
if (value == 0) {
|
|
return;
|
|
}
|
|
|
|
CEconItem_Tag? tag = Body.tags.FirstOrDefault(static tag => tag.category == "Game");
|
|
|
|
if (tag == null) {
|
|
tag = new CEconItem_Tag { category = "Game" };
|
|
|
|
Body.tags.Add(tag);
|
|
}
|
|
|
|
tag.internal_name = $"app_{value}";
|
|
}
|
|
}
|
|
|
|
[JsonDisallowNull]
|
|
[JsonInclude]
|
|
[JsonPropertyName("tags")]
|
|
internal ImmutableHashSet<Tag> Tags {
|
|
get => Body.tags.Select(static tag => new Tag(tag.category, tag.internal_name, tag.localized_category_name, tag.localized_tag_name, tag.color)).ToImmutableHashSet();
|
|
|
|
private init {
|
|
Body.tags.Clear();
|
|
|
|
Body.tags.AddRange(
|
|
value.Select(
|
|
tag => new CEconItem_Tag {
|
|
appid = AppID,
|
|
category = tag.Identifier,
|
|
color = tag.Color,
|
|
internal_name = tag.Value,
|
|
localized_category_name = tag.LocalizedIdentifier,
|
|
localized_tag_name = tag.LocalizedValue
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
[JsonConverter(typeof(BooleanNumberConverter))]
|
|
[JsonInclude]
|
|
[JsonPropertyName("tradable")]
|
|
[JsonRequired]
|
|
internal bool Tradable {
|
|
get => Body.tradable;
|
|
private init => Body.tradable = value;
|
|
}
|
|
|
|
internal EAssetType Type {
|
|
get {
|
|
if (CachedType.HasValue) {
|
|
return CachedType.Value;
|
|
}
|
|
|
|
EAssetType type = EAssetType.Unknown;
|
|
|
|
foreach (CEconItem_Tag? tag in Body.tags) {
|
|
switch (tag.category) {
|
|
case "cardborder":
|
|
switch (tag.internal_name) {
|
|
case "cardborder_0":
|
|
CachedType = EAssetType.TradingCard;
|
|
|
|
return CachedType.Value;
|
|
case "cardborder_1":
|
|
CachedType = EAssetType.FoilTradingCard;
|
|
|
|
return CachedType.Value;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.internal_name), tag.internal_name));
|
|
|
|
CachedType = EAssetType.Unknown;
|
|
|
|
return CachedType.Value;
|
|
}
|
|
case "item_class":
|
|
switch (tag.internal_name) {
|
|
case "item_class_2":
|
|
if (type == EAssetType.Unknown) {
|
|
// This is a fallback in case we'd have no cardborder available to interpret
|
|
type = EAssetType.TradingCard;
|
|
}
|
|
|
|
continue;
|
|
case "item_class_3":
|
|
CachedType = EAssetType.ProfileBackground;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_4":
|
|
CachedType = EAssetType.Emoticon;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_5":
|
|
CachedType = EAssetType.BoosterPack;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_6":
|
|
CachedType = EAssetType.Consumable;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_7":
|
|
CachedType = EAssetType.SteamGems;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_8":
|
|
CachedType = EAssetType.ProfileModifier;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_10":
|
|
CachedType = EAssetType.SaleItem;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_11":
|
|
CachedType = EAssetType.Sticker;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_12":
|
|
CachedType = EAssetType.ChatEffect;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_13":
|
|
CachedType = EAssetType.MiniProfileBackground;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_14":
|
|
CachedType = EAssetType.AvatarProfileFrame;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_15":
|
|
CachedType = EAssetType.AnimatedAvatar;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_16":
|
|
CachedType = EAssetType.KeyboardSkin;
|
|
|
|
return CachedType.Value;
|
|
case "item_class_17":
|
|
CachedType = EAssetType.StartupVideo;
|
|
|
|
return CachedType.Value;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.internal_name), tag.internal_name));
|
|
|
|
CachedType = EAssetType.Unknown;
|
|
|
|
return CachedType.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
CachedType = type;
|
|
|
|
return CachedType.Value;
|
|
}
|
|
|
|
private init {
|
|
if (!Enum.IsDefined(value)) {
|
|
throw new InvalidEnumArgumentException(nameof(value), (int) value, typeof(EAssetType));
|
|
}
|
|
|
|
CachedType = value;
|
|
|
|
switch (value) {
|
|
case EAssetType.Unknown:
|
|
return;
|
|
case EAssetType.TradingCard:
|
|
case EAssetType.FoilTradingCard:
|
|
CEconItem_Tag? cardTag = Body.tags.FirstOrDefault(static tag => tag.category == "cardborder");
|
|
|
|
if (cardTag == null) {
|
|
cardTag = new CEconItem_Tag { category = "cardborder" };
|
|
|
|
Body.tags.Add(cardTag);
|
|
}
|
|
|
|
cardTag.internal_name = value switch {
|
|
EAssetType.TradingCard => "cardborder_0",
|
|
EAssetType.FoilTradingCard => "cardborder_1",
|
|
_ => throw new InvalidOperationException(nameof(value))
|
|
};
|
|
|
|
// We're still going to add item_class tag below
|
|
break;
|
|
}
|
|
|
|
CEconItem_Tag? tag = Body.tags.FirstOrDefault(static tag => tag.category == "item_class");
|
|
|
|
if (tag == null) {
|
|
tag = new CEconItem_Tag { category = "item_class" };
|
|
|
|
Body.tags.Add(tag);
|
|
}
|
|
|
|
tag.internal_name = value switch {
|
|
EAssetType.TradingCard => "item_class_2",
|
|
EAssetType.FoilTradingCard => "item_class_2",
|
|
EAssetType.ProfileBackground => "item_class_3",
|
|
EAssetType.Emoticon => "item_class_4",
|
|
EAssetType.BoosterPack => "item_class_5",
|
|
EAssetType.Consumable => "item_class_6",
|
|
EAssetType.SteamGems => "item_class_7",
|
|
EAssetType.ProfileModifier => "item_class_8",
|
|
EAssetType.SaleItem => "item_class_10",
|
|
EAssetType.Sticker => "item_class_11",
|
|
EAssetType.ChatEffect => "item_class_12",
|
|
EAssetType.MiniProfileBackground => "item_class_13",
|
|
EAssetType.AvatarProfileFrame => "item_class_14",
|
|
EAssetType.AnimatedAvatar => "item_class_15",
|
|
EAssetType.KeyboardSkin => "item_class_16",
|
|
EAssetType.StartupVideo => "item_class_17",
|
|
_ => throw new InvalidOperationException(nameof(value))
|
|
};
|
|
}
|
|
}
|
|
|
|
private EAssetRarity? CachedRarity;
|
|
private uint? CachedRealAppID;
|
|
private EAssetType? CachedType;
|
|
|
|
public InventoryDescription(CEconItem_Description description) {
|
|
ArgumentNullException.ThrowIfNull(description);
|
|
|
|
Body = description;
|
|
}
|
|
|
|
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);
|
|
|
|
if (!Enum.IsDefined(type)) {
|
|
throw new InvalidEnumArgumentException(nameof(type), (int) type, typeof(EAssetType));
|
|
}
|
|
|
|
if (!Enum.IsDefined(rarity)) {
|
|
throw new InvalidEnumArgumentException(nameof(rarity), (int) rarity, typeof(EAssetRarity));
|
|
}
|
|
|
|
AppID = appID;
|
|
ClassID = classID;
|
|
|
|
InstanceID = instanceID;
|
|
Marketable = marketable;
|
|
Tradable = tradable;
|
|
RealAppID = realAppID;
|
|
Type = type;
|
|
Rarity = rarity;
|
|
}
|
|
|
|
[JsonConstructor]
|
|
private InventoryDescription() { }
|
|
|
|
public InventoryDescription DeepClone() => new(Serializer.DeepClone(Body));
|
|
}
|