From fec57e0fff13dc4e176f79bdda42764f4050057a Mon Sep 17 00:00:00 2001 From: Archi Date: Fri, 11 Feb 2022 00:05:43 +0100 Subject: [PATCH] Preserve CachedCardCountsForGame across ASF runs --- .../ObservableConcurrentDictionary.cs | 121 ++++++++++++++++++ .../Steam/Integration/ArchiWebHandler.cs | 7 +- ArchiSteamFarm/Storage/GlobalDatabase.cs | 13 +- 3 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs diff --git a/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs b/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs new file mode 100644 index 000000000..95fa0eae7 --- /dev/null +++ b/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs @@ -0,0 +1,121 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2022 Ɓ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; +using System.Collections.Concurrent; +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.Collections; + +[PublicAPI] +public sealed class ObservableConcurrentDictionary : IDictionary, IReadOnlyDictionary where TKey : notnull { + public event EventHandler? OnModified; + + public int Count => BackingDictionary.Count; + public bool IsEmpty => BackingDictionary.IsEmpty; + public bool IsReadOnly => false; + + [JsonProperty(Required = Required.DisallowNull)] + private readonly ConcurrentDictionary BackingDictionary = new(); + + int ICollection>.Count => BackingDictionary.Count; + int IReadOnlyCollection>.Count => BackingDictionary.Count; + IEnumerable IReadOnlyDictionary.Keys => BackingDictionary.Keys; + ICollection IDictionary.Keys => BackingDictionary.Keys; + IEnumerable IReadOnlyDictionary.Values => BackingDictionary.Values; + ICollection IDictionary.Values => BackingDictionary.Values; + + public TValue this[TKey key] { + get => BackingDictionary[key]; + set { + if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer.Default.Equals(savedValue, value)) { + return; + } + + BackingDictionary[key] = value; + OnModified?.Invoke(this, EventArgs.Empty); + } + } + + public void Add(KeyValuePair item) { + (TKey key, TValue value) = item; + + Add(key, value); + } + + public void Add(TKey key, TValue value) => TryAdd(key, value); + + public void Clear() { + if (BackingDictionary.IsEmpty) { + return; + } + + BackingDictionary.Clear(); + OnModified?.Invoke(this, EventArgs.Empty); + } + + public bool Contains(KeyValuePair item) => ((ICollection>) BackingDictionary).Contains(item); + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>) BackingDictionary).CopyTo(array, arrayIndex); + public IEnumerator> GetEnumerator() => BackingDictionary.GetEnumerator(); + + public bool Remove(KeyValuePair item) { + ICollection> collection = BackingDictionary; + + if (!collection.Remove(item)) { + return false; + } + + OnModified?.Invoke(this, EventArgs.Empty); + + return true; + } + + public bool Remove(TKey key) { + if (!BackingDictionary.TryRemove(key, out _)) { + return false; + } + + OnModified?.Invoke(this, EventArgs.Empty); + + return true; + } + + bool IDictionary.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key); + bool IReadOnlyDictionary.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool IReadOnlyDictionary.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!); + bool IDictionary.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!); + + public bool TryAdd(TKey key, TValue value) { + if (!BackingDictionary.TryAdd(key, value)) { + return false; + } + + OnModified?.Invoke(this, EventArgs.Empty); + + return true; + } + + public bool TryGetValue(TKey key, out TValue? value) => BackingDictionary.TryGetValue(key, out value); +} diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 754429f68..34afaef88 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -20,7 +20,6 @@ // limitations under the License. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; @@ -67,8 +66,6 @@ public sealed class ArchiWebHandler : IDisposable { [PublicAPI] public static Uri SteamStoreURL => new("https://store.steampowered.com"); - private static readonly ConcurrentDictionary CachedCardCountsForGame = new(); - private static ushort WebLimiterDelay => ASF.GlobalConfig?.WebLimiterDelay ?? GlobalConfig.DefaultWebLimiterDelay; [PublicAPI] @@ -1705,7 +1702,7 @@ public sealed class ArchiWebHandler : IDisposable { throw new ArgumentOutOfRangeException(nameof(appID)); } - if (CachedCardCountsForGame.TryGetValue(appID, out byte result)) { + if (ASF.GlobalDatabase?.CardCountsPerGame.TryGetValue(appID, out byte result) == true) { return result; } @@ -1727,7 +1724,7 @@ public sealed class ArchiWebHandler : IDisposable { return 0; } - CachedCardCountsForGame.TryAdd(appID, result); + ASF.GlobalDatabase?.CardCountsPerGame.TryAdd(appID, result); return result; } diff --git a/ArchiSteamFarm/Storage/GlobalDatabase.cs b/ArchiSteamFarm/Storage/GlobalDatabase.cs index 58ffcde03..835370593 100644 --- a/ArchiSteamFarm/Storage/GlobalDatabase.cs +++ b/ArchiSteamFarm/Storage/GlobalDatabase.cs @@ -28,6 +28,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; @@ -48,6 +49,9 @@ public sealed class GlobalDatabase : SerializableFile { [PublicAPI] public IReadOnlyDictionary? AppIDs)> PackagesDataReadOnly => PackagesData; + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ObservableConcurrentDictionary CardCountsPerGame = new(); + [JsonProperty(Required = Required.DisallowNull)] internal readonly InMemoryServerListProvider ServerListProvider = new(); @@ -107,7 +111,10 @@ public sealed class GlobalDatabase : SerializableFile { } [JsonConstructor] - private GlobalDatabase() => ServerListProvider.ServerListUpdated += OnObjectModified; + private GlobalDatabase() { + CardCountsPerGame.OnModified += OnObjectModified; + ServerListProvider.ServerListUpdated += OnObjectModified; + } [PublicAPI] public void DeleteFromJsonStorage(string key) { @@ -159,6 +166,9 @@ public sealed class GlobalDatabase : SerializableFile { [UsedImplicitly] public bool ShouldSerializeBackingLastChangeNumber() => LastChangeNumber != 0; + [UsedImplicitly] + public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame.IsEmpty; + [UsedImplicitly] public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty; @@ -174,6 +184,7 @@ public sealed class GlobalDatabase : SerializableFile { protected override void Dispose(bool disposing) { if (disposing) { // Events we registered + CardCountsPerGame.OnModified -= OnObjectModified; ServerListProvider.ServerListUpdated -= OnObjectModified; // Those are objects that are always being created if constructor doesn't throw exception