From 3a9e9de596abb4eb629d70a8ae8d59cd5386dcbd Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 5 Nov 2023 23:22:15 +0100 Subject: [PATCH] Closes #2980 --- .../Localization/Strings.Designer.cs | 6 ++ ArchiSteamFarm/Localization/Strings.resx | 4 + ArchiSteamFarm/Steam/Bot.cs | 80 ++++++++++++++++--- ArchiSteamFarm/Storage/PackageData.cs | 9 ++- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index 22352b11b..bb479a250 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1214,5 +1214,11 @@ namespace ArchiSteamFarm.Localization { return ResourceManager.GetString("WarningNoLicense", resourceCulture); } } + + public static string WarningRegionRestrictedPackage { + get { + return ResourceManager.GetString("WarningRegionRestrictedPackage", resourceCulture); + } + } } } diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index ee0b87e99..474e407c9 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -748,4 +748,8 @@ Process uptime: {1} You've attempted to use paid feature {0} but you don't have a valid LicenseID set in the ASF global config. Please review your configuration, as the functionality won't work without additional details. {0} will be replaced by feature name (e.g. MatchActively) + + ASF is unable to play app {0} as it has region-related restriction for {1} country that lasts until {2}. + {0} will be replaced by app ID (number), {1} will be replaced by short country code (string, such as "PL"), {2} will be replaced by human-readable date (string). + diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index dca979d17..5c27fc6ef 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -69,6 +69,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private const byte MaxLoginFailures = WebBrowser.MaxTries; // Max login failures in a row before we determine that our credentials are invalid (because Steam wrongly returns those, of course)course) private const byte MinimumAccessTokenValidityMinutes = 10; private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam + private const byte RegionRestrictionPlayableBlockMonths = 3; [PublicAPI] public static IReadOnlyDictionary? BotsReadOnly => Bots; @@ -241,6 +242,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private Timer? ConnectionFailureTimer; private bool FirstTradeSent; private Timer? GamesRedeemerInBackgroundTimer; + private string? IPCountryCode; private EResult LastLogOnResult; private DateTime LastLogonSessionReplaced; private bool LibraryLocked; @@ -1120,6 +1122,56 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } } + // Check region restrictions + if (!string.IsNullOrEmpty(IPCountryCode)) { + DateTime? regionRestrictedUntil = null; + + DateTime safePlayableBefore = DateTime.UtcNow.AddMonths(-RegionRestrictionPlayableBlockMonths); + + foreach (uint packageID in packageIDs) { + if (!OwnedPackageIDs.TryGetValue(packageID, out (EPaymentMethod PaymentMethod, DateTime TimeCreated) ownedPackageData)) { + // We don't own that packageID, keep checking + continue; + } + + if (ownedPackageData.TimeCreated < safePlayableBefore) { + // Our package is older than required, this is playable + regionRestrictedUntil = null; + + break; + } + + // We've got a package that was activated recently, we should check if we have any playable restrictions on it + if ((ASF.GlobalDatabase == null) || !ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out PackageData? packageData)) { + // No information about that package, try again later + return (0, DateTime.MaxValue, true); + } + + if ((packageData.ProhibitRunInCountries == null) || packageData.ProhibitRunInCountries.IsEmpty) { + // No restrictions, we're good to go + regionRestrictedUntil = null; + + break; + } + + if (packageData.ProhibitRunInCountries.Contains(IPCountryCode)) { + // We are restricted by this package, we can only be saved by another package that is not restricted + DateTime regionRestrictedUntilPackage = ownedPackageData.TimeCreated.AddMonths(RegionRestrictionPlayableBlockMonths); + + if (!regionRestrictedUntil.HasValue || (regionRestrictedUntilPackage < regionRestrictedUntil.Value)) { + regionRestrictedUntil = regionRestrictedUntilPackage; + } + } + } + + if (regionRestrictedUntil.HasValue) { + // We can't play this game for now + ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningRegionRestrictedPackage, appID, IPCountryCode, regionRestrictedUntil.Value)); + + return (0, regionRestrictedUntil.Value, false); + } + } + SteamApps.PICSTokensCallback? tokenCallback = null; for (byte i = 0; (i < WebBrowser.MaxTries) && (tokenCallback == null) && IsConnectedAndLoggedOn; i++) { @@ -1300,33 +1352,38 @@ public sealed class Bot : IAsyncDisposable, IDisposable { if (productInfo.KeyValues == KeyValue.Invalid) { ArchiLogger.LogNullError(productInfo); - return null; + continue; } uint changeNumber = productInfo.ChangeNumber; + HashSet? appIDs = null; - try { - KeyValue appIDsKv = productInfo.KeyValues["appids"]; - - if (appIDsKv == KeyValue.Invalid) { - continue; - } + KeyValue appIDsKv = productInfo.KeyValues["appids"]; + if (appIDsKv != KeyValue.Invalid) { appIDs = new HashSet(appIDsKv.Children.Count); foreach (string? appIDText in appIDsKv.Children.Select(static app => app.Value)) { if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) { ArchiLogger.LogNullError(appID); - return null; + continue; } appIDs.Add(appID); } - } finally { - result[productInfo.ID] = new PackageData(changeNumber, validUntil, appIDs?.ToImmutableHashSet()); } + + string[]? prohibitRunInCountries = null; + + string? prohibitRunInCountriesText = productInfo.KeyValues["extended"]["prohibitrunincountries"].AsString(); + + if (!string.IsNullOrEmpty(prohibitRunInCountriesText)) { + prohibitRunInCountries = prohibitRunInCountriesText.Split(' ', StringSplitOptions.RemoveEmptyEntries); + } + + result[productInfo.ID] = new PackageData(changeNumber, validUntil, appIDs?.ToImmutableHashSet(), prohibitRunInCountries?.ToImmutableHashSet(StringComparer.Ordinal)); } return result; @@ -2350,7 +2407,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } AccountFlags = EAccountFlags.NormalUser; - AvatarHash = Nickname = null; + AvatarHash = IPCountryCode = Nickname = null; MasterChatGroupID = 0; RequiredInput = ASF.EUserInputType.None; WalletBalance = 0; @@ -3131,6 +3188,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } AccountFlags = callback.AccountFlags; + IPCountryCode = callback.IPCountryCode; SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID)); ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}")); diff --git a/ArchiSteamFarm/Storage/PackageData.cs b/ArchiSteamFarm/Storage/PackageData.cs index ce1bd0e5b..12bfe96e4 100644 --- a/ArchiSteamFarm/Storage/PackageData.cs +++ b/ArchiSteamFarm/Storage/PackageData.cs @@ -33,10 +33,13 @@ public sealed class PackageData { [JsonProperty(Required = Required.Always)] public uint ChangeNumber { get; private set; } + [JsonProperty] + public ImmutableHashSet? ProhibitRunInCountries { get; private set; } + [JsonProperty(Required = Required.Always)] public DateTime ValidUntil { get; private set; } - internal PackageData(uint changeNumber, DateTime validUntil, ImmutableHashSet? appIDs = null) { + internal PackageData(uint changeNumber, DateTime validUntil, ImmutableHashSet? appIDs = null, ImmutableHashSet? prohibitRunInCountries = null) { if (changeNumber == 0) { throw new ArgumentOutOfRangeException(nameof(changeNumber)); } @@ -48,6 +51,7 @@ public sealed class PackageData { ChangeNumber = changeNumber; ValidUntil = validUntil; AppIDs = appIDs; + ProhibitRunInCountries = prohibitRunInCountries; } [JsonConstructor] @@ -55,4 +59,7 @@ public sealed class PackageData { [UsedImplicitly] public bool ShouldSerializeAppIDs() => AppIDs is { IsEmpty: false }; + + [UsedImplicitly] + public bool ShouldSerializeProhibitRunInCountries() => ProhibitRunInCountries is { IsEmpty: false }; }