diff --git a/ArchiSteamFarm/Helpers/SerializableFile.cs b/ArchiSteamFarm/Helpers/SerializableFile.cs index 0b813aed3..93c77acc5 100644 --- a/ArchiSteamFarm/Helpers/SerializableFile.cs +++ b/ArchiSteamFarm/Helpers/SerializableFile.cs @@ -28,6 +28,8 @@ using Newtonsoft.Json; namespace ArchiSteamFarm.Helpers { public abstract class SerializableFile : IDisposable { + private static readonly SemaphoreSlim GlobalFileSemaphore = new(1, 1); + private readonly SemaphoreSlim FileSemaphore = new(1, 1); protected string? FilePath { get; set; } @@ -77,9 +79,7 @@ namespace ArchiSteamFarm.Helpers { string json = JsonConvert.SerializeObject(this, Debugging.IsUserDebugging ? Formatting.Indented : Formatting.None); if (string.IsNullOrEmpty(json)) { - ASF.ArchiLogger.LogNullError(nameof(json)); - - return; + throw new InvalidOperationException(nameof(json)); } string newFilePath = FilePath + ".new"; @@ -116,5 +116,38 @@ namespace ArchiSteamFarm.Helpers { FileSemaphore.Release(); } } + + internal static async Task Write(string filePath, string json) { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + if (string.IsNullOrEmpty(json)) { + throw new ArgumentNullException(nameof(json)); + } + + string newFilePath = filePath + ".new"; + + await GlobalFileSemaphore.WaitAsync().ConfigureAwait(false); + + try { + // We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist + await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); + + if (System.IO.File.Exists(filePath)) { + System.IO.File.Replace(newFilePath, filePath, null); + } else { + System.IO.File.Move(newFilePath, filePath); + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + + return false; + } finally { + GlobalFileSemaphore.Release(); + } + + return true; + } } } diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index 9755a46b5..cc288ec45 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -87,6 +87,15 @@ namespace ArchiSteamFarm.Localization { } } + /// + /// Looks up a localized string similar to {0} config file will be migrated to the latest syntax.... + /// + public static string AutomaticFileMigration { + get { + return ResourceManager.GetString("AutomaticFileMigration", resourceCulture); + } + } + /// /// Looks up a localized string similar to ASF will automatically check for new versions every {0}.. /// diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index 12a49aa81..05b54cb58 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -746,4 +746,8 @@ Process uptime: {1} Idling selected {0}: {1} {0} will be replaced by internal name of the config property (e.g. "GamesPlayedWhileIdle"), {1} will be replaced by comma-separated list of appIDs that user has chosen + + {0} config file will be migrated to the latest syntax... + {0} will be replaced with the relative path to the affected config file + diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index e21877a5c..a95645b17 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -1091,7 +1091,7 @@ namespace ArchiSteamFarm.Steam { return (0, DateTime.MaxValue, true); } - if ((hoursPlayed < CardsFarmer.HoursForRefund) && !BotConfig.IdleRefundableGames) { + if ((hoursPlayed < CardsFarmer.HoursForRefund) && BotConfig.FarmNonRefundableGamesOnly) { DateTime mostRecent = DateTime.MinValue; foreach (uint packageID in packageIDs) { @@ -1469,7 +1469,7 @@ namespace ArchiSteamFarm.Steam { return; } - BotConfig? botConfig = await BotConfig.Load(configFile).ConfigureAwait(false); + (BotConfig? botConfig, _) = await BotConfig.Load(configFile).ConfigureAwait(false); if (botConfig == null) { await Destroy().ConfigureAwait(false); @@ -1571,7 +1571,7 @@ namespace ArchiSteamFarm.Steam { return; } - BotConfig? botConfig = await BotConfig.Load(configFilePath).ConfigureAwait(false); + (BotConfig? botConfig, string? latestJson) = await BotConfig.Load(configFilePath).ConfigureAwait(false); if (botConfig == null) { ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorBotConfigInvalid, configFilePath)); @@ -1583,6 +1583,17 @@ namespace ArchiSteamFarm.Steam { ASF.ArchiLogger.LogGenericDebug(configFilePath + ": " + JsonConvert.SerializeObject(botConfig, Formatting.Indented)); } + if (!string.IsNullOrEmpty(latestJson)) { + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.AutomaticFileMigration, configFilePath)); + + await SerializableFile.Write(configFilePath, latestJson!).ConfigureAwait(false); + + // A little extra time in order to avoid unnecessary "config changed" event + await Task.Delay(5000).ConfigureAwait(false); + + ASF.ArchiLogger.LogGenericInfo(Strings.Done); + } + string databaseFilePath = GetFilePath(botName, EFileType.Database); if (string.IsNullOrEmpty(databaseFilePath)) { diff --git a/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs b/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs index c482747fb..cddfd227a 100644 --- a/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs +++ b/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs @@ -255,7 +255,7 @@ namespace ArchiSteamFarm.Steam.Cards { return; } - if (!Bot.CanReceiveSteamCards || (Bot.BotConfig.IdlePriorityQueueOnly && (Bot.BotDatabase.IdlingPriorityAppIDs.Count == 0))) { + if (!Bot.CanReceiveSteamCards || (Bot.BotConfig.FarmPriorityQueueOnly && (Bot.BotDatabase.IdlingPriorityAppIDs.Count == 0))) { Bot.ArchiLogger.LogGenericInfo(Strings.NothingToIdle); await Bot.OnFarmingFinished(false).ConfigureAwait(false); @@ -440,7 +440,7 @@ namespace ArchiSteamFarm.Steam.Cards { continue; } - if (SalesBlacklist.Contains(appID) || (ASF.GlobalConfig?.Blacklist.Contains(appID) == true) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.IdlePriorityQueueOnly && !Bot.IsPriorityIdling(appID))) { + if (SalesBlacklist.Contains(appID) || (ASF.GlobalConfig?.Blacklist.Contains(appID) == true) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.FarmPriorityQueueOnly && !Bot.IsPriorityIdling(appID))) { // We're configured to ignore this appID, so skip it continue; } diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index e36fef3d9..e916986ee 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -1504,7 +1504,7 @@ namespace ArchiSteamFarm.Steam.Interaction { } switch (Bot.CardsFarmer.NowFarming) { - case false when Bot.BotConfig.IdlePriorityQueueOnly: + case false when Bot.BotConfig.FarmPriorityQueueOnly: Utilities.InBackground(Bot.CardsFarmer.StartFarming); break; diff --git a/ArchiSteamFarm/Steam/Storage/BotConfig.cs b/ArchiSteamFarm/Steam/Storage/BotConfig.cs index cd4e010ff..ed655b77f 100644 --- a/ArchiSteamFarm/Steam/Storage/BotConfig.cs +++ b/ArchiSteamFarm/Steam/Storage/BotConfig.cs @@ -19,15 +19,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if NETFRAMEWORK +using ArchiSteamFarm.Compatibility; +using File = System.IO.File; +#else +using System.IO; +#endif using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using ArchiSteamFarm.Compatibility; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; @@ -59,15 +63,15 @@ namespace ArchiSteamFarm.Steam.Storage { [PublicAPI] public const bool DefaultEnabled = false; + [PublicAPI] + public const bool DefaultFarmNonRefundableGamesOnly = false; + + [PublicAPI] + public const bool DefaultFarmPriorityQueueOnly = false; + [PublicAPI] public const byte DefaultHoursUntilCardDrops = 3; - [PublicAPI] - public const bool DefaultIdlePriorityQueueOnly = false; - - [PublicAPI] - public const bool DefaultIdleRefundableGames = true; - [PublicAPI] public const EPersonaState DefaultOnlineStatus = EPersonaState.Online; @@ -141,8 +145,6 @@ namespace ArchiSteamFarm.Steam.Storage { [PublicAPI] public static readonly ImmutableHashSet DefaultTransferableTypes = ImmutableHashSet.Create(Asset.EType.BoosterPack, Asset.EType.FoilTradingCard, Asset.EType.TradingCard); - private static readonly SemaphoreSlim WriteSemaphore = new(1, 1); - [JsonProperty(Required = Required.DisallowNull)] public bool AcceptGifts { get; private set; } = DefaultAcceptGifts; @@ -167,18 +169,18 @@ namespace ArchiSteamFarm.Steam.Storage { [JsonProperty(Required = Required.DisallowNull)] public ImmutableList FarmingOrders { get; private set; } = DefaultFarmingOrders; + [JsonProperty(Required = Required.DisallowNull)] + public bool FarmNonRefundableGamesOnly { get; private set; } = DefaultFarmNonRefundableGamesOnly; + + [JsonProperty(Required = Required.DisallowNull)] + public bool FarmPriorityQueueOnly { get; private set; } = DefaultFarmPriorityQueueOnly; + [JsonProperty(Required = Required.DisallowNull)] public ImmutableHashSet GamesPlayedWhileIdle { get; private set; } = DefaultGamesPlayedWhileIdle; [JsonProperty(Required = Required.DisallowNull)] public byte HoursUntilCardDrops { get; private set; } = DefaultHoursUntilCardDrops; - [JsonProperty(Required = Required.DisallowNull)] - public bool IdlePriorityQueueOnly { get; private set; } = DefaultIdlePriorityQueueOnly; - - [JsonProperty(Required = Required.DisallowNull)] - public bool IdleRefundableGames { get; private set; } = DefaultIdleRefundableGames; - [JsonProperty(Required = Required.DisallowNull)] public ImmutableHashSet LootableTypes { get; private set; } = DefaultLootableTypes; @@ -305,6 +307,26 @@ namespace ArchiSteamFarm.Steam.Storage { private string? BackingSteamParentalCode = DefaultSteamParentalCode; private string? BackingSteamPassword = DefaultSteamPassword; + [Obsolete] + [JsonProperty(Required = Required.DisallowNull)] + private bool IdlePriorityQueueOnly { + set { + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningDeprecated, nameof(IdlePriorityQueueOnly), nameof(FarmPriorityQueueOnly))); + + FarmPriorityQueueOnly = value; + } + } + + [Obsolete] + [JsonProperty(Required = Required.DisallowNull)] + private bool IdleRefundableGames { + set { + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningDeprecated, nameof(IdleRefundableGames), nameof(FarmNonRefundableGamesOnly))); + + FarmNonRefundableGamesOnly = !value; + } + } + [JsonProperty(PropertyName = SharedInfo.UlongCompatibilityStringPrefix + nameof(SteamMasterClanID), Required = Required.DisallowNull)] private string SSteamMasterClanID { get => SteamMasterClanID.ToString(CultureInfo.InvariantCulture); @@ -334,27 +356,8 @@ namespace ArchiSteamFarm.Steam.Storage { } string json = JsonConvert.SerializeObject(botConfig, Formatting.Indented); - string newFilePath = filePath + ".new"; - await WriteSemaphore.WaitAsync().ConfigureAwait(false); - - try { - await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - - if (System.IO.File.Exists(filePath)) { - System.IO.File.Replace(newFilePath, filePath, null); - } else { - System.IO.File.Move(newFilePath, filePath); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - - return false; - } finally { - WriteSemaphore.Release(); - } - - return true; + return await SerializableFile.Write(filePath, json).ConfigureAwait(false); } internal (bool Valid, string? ErrorMessage) CheckValidation() { @@ -423,37 +426,38 @@ namespace ArchiSteamFarm.Steam.Storage { return !Enum.IsDefined(typeof(ArchiHandler.EUserInterfaceMode), UserInterfaceMode) ? (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(UserInterfaceMode), UserInterfaceMode)) : (true, null); } - internal static async Task Load(string filePath) { + internal static async Task<(BotConfig? BotConfig, string? LatestJson)> Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { throw new ArgumentNullException(nameof(filePath)); } - if (!System.IO.File.Exists(filePath)) { - return null; + if (!File.Exists(filePath)) { + return (null, null); } + string json; BotConfig? botConfig; try { - string json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false); + json = await Compatibility.File.ReadAllTextAsync(filePath).ConfigureAwait(false); if (string.IsNullOrEmpty(json)) { ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); - return null; + return (null, null); } botConfig = JsonConvert.DeserializeObject(json); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); - return null; + return (null, null); } if (botConfig == null) { ASF.ArchiLogger.LogNullError(nameof(botConfig)); - return null; + return (null, null); } (bool valid, string? errorMessage) = botConfig.CheckValidation(); @@ -463,10 +467,12 @@ namespace ArchiSteamFarm.Steam.Storage { ASF.ArchiLogger.LogGenericError(errorMessage!); } - return null; + return (null, null); } - return botConfig; + string latestJson = JsonConvert.SerializeObject(botConfig, Formatting.Indented); + + return (botConfig, json != latestJson ? latestJson : null); } public enum EAccess : byte { @@ -537,10 +543,10 @@ namespace ArchiSteamFarm.Steam.Storage { public bool ShouldSerializeCustomGamePlayedWhileIdle() => ShouldSerializeDefaultValues || (CustomGamePlayedWhileIdle != DefaultCustomGamePlayedWhileIdle); public bool ShouldSerializeEnabled() => ShouldSerializeDefaultValues || (Enabled != DefaultEnabled); public bool ShouldSerializeFarmingOrders() => ShouldSerializeDefaultValues || ((FarmingOrders != DefaultFarmingOrders) && !FarmingOrders.SequenceEqual(DefaultFarmingOrders)); + public bool ShouldSerializeFarmNonRefundableGamesOnly() => ShouldSerializeDefaultValues || (FarmNonRefundableGamesOnly != DefaultFarmNonRefundableGamesOnly); + public bool ShouldSerializeFarmPriorityQueueOnly() => ShouldSerializeDefaultValues || (FarmPriorityQueueOnly != DefaultFarmPriorityQueueOnly); public bool ShouldSerializeGamesPlayedWhileIdle() => ShouldSerializeDefaultValues || ((GamesPlayedWhileIdle != DefaultGamesPlayedWhileIdle) && !GamesPlayedWhileIdle.SetEquals(DefaultGamesPlayedWhileIdle)); public bool ShouldSerializeHoursUntilCardDrops() => ShouldSerializeDefaultValues || (HoursUntilCardDrops != DefaultHoursUntilCardDrops); - public bool ShouldSerializeIdlePriorityQueueOnly() => ShouldSerializeDefaultValues || (IdlePriorityQueueOnly != DefaultIdlePriorityQueueOnly); - public bool ShouldSerializeIdleRefundableGames() => ShouldSerializeDefaultValues || (IdleRefundableGames != DefaultIdleRefundableGames); public bool ShouldSerializeLootableTypes() => ShouldSerializeDefaultValues || ((LootableTypes != DefaultLootableTypes) && !LootableTypes.SetEquals(DefaultLootableTypes)); public bool ShouldSerializeMatchableTypes() => ShouldSerializeDefaultValues || ((MatchableTypes != DefaultMatchableTypes) && !MatchableTypes.SetEquals(DefaultMatchableTypes)); public bool ShouldSerializeOnlineStatus() => ShouldSerializeDefaultValues || (OnlineStatus != DefaultOnlineStatus); diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index 6de5a7cc9..e900c7933 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -26,7 +26,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Net; -using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; @@ -127,8 +126,6 @@ namespace ArchiSteamFarm.Storage { [PublicAPI] public static readonly ImmutableHashSet DefaultBlacklist = ImmutableHashSet.Empty; - private static readonly SemaphoreSlim WriteSemaphore = new(1, 1); - [JsonIgnore] [PublicAPI] public WebProxy? WebProxy { @@ -395,27 +392,8 @@ namespace ArchiSteamFarm.Storage { } string json = JsonConvert.SerializeObject(globalConfig, Formatting.Indented); - string newFilePath = filePath + ".new"; - await WriteSemaphore.WaitAsync().ConfigureAwait(false); - - try { - await Compatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - - if (File.Exists(filePath)) { - File.Replace(newFilePath, filePath, null); - } else { - File.Move(newFilePath, filePath); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - - return false; - } finally { - WriteSemaphore.Release(); - } - - return true; + return await SerializableFile.Write(filePath, json).ConfigureAwait(false); } public enum EOptimizationMode : byte {