// _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | // Copyright 2015-2020 Ɓ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.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Helpers; using JetBrains.Annotations; using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper { internal sealed class GlobalCache : SerializableFile { [NotNull] private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, nameof(SteamTokenDumper) + ".cache"); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary AppChangeNumbers = new ConcurrentDictionary(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary AppTokens = new ConcurrentDictionary(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary DepotKeys = new ConcurrentDictionary(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary PackageTokens = new ConcurrentDictionary(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet SubmittedAppIDs = new ConcurrentHashSet(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet SubmittedDepotIDs = new ConcurrentHashSet(); [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet SubmittedPackageIDs = new ConcurrentHashSet(); [JsonProperty(Required = Required.DisallowNull)] internal uint LastChangeNumber { get; private set; } internal GlobalCache() => FilePath = SharedFilePath; internal ulong GetAppToken(uint appID) => AppTokens[appID]; [NotNull] internal Dictionary GetAppTokensForSubmission() => AppTokens.Where(appToken => !SubmittedAppIDs.Contains(appToken.Key)).ToDictionary(appToken => appToken.Key, appToken => appToken.Value); [NotNull] internal Dictionary GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => !SubmittedDepotIDs.Contains(depotKey.Key)).ToDictionary(depotKey => depotKey.Key, depotKey => depotKey.Value); [NotNull] internal Dictionary GetPackageTokensForSubmission() => PackageTokens.Where(packageToken => !SubmittedPackageIDs.Contains(packageToken.Key)).ToDictionary(packageToken => packageToken.Key, packageToken => packageToken.Value); [ItemNotNull] internal static async Task Load() { if (!File.Exists(SharedFilePath)) { return new GlobalCache(); } GlobalCache globalCache = null; try { string json = await RuntimeCompatibility.File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false); if (!string.IsNullOrEmpty(json)) { globalCache = JsonConvert.DeserializeObject(json); } } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } if (globalCache == null) { ASF.ArchiLogger.LogGenericError($"{nameof(GlobalCache)} could not be loaded, a fresh instance will be initialized."); globalCache = new GlobalCache(); } return globalCache; } internal async Task OnPICSChanges(uint currentChangeNumber, [NotNull] IReadOnlyCollection> appChanges) { if ((currentChangeNumber == 0) || (appChanges == null)) { throw new ArgumentNullException(nameof(appChanges)); } if (currentChangeNumber <= LastChangeNumber) { return; } ASF.ArchiLogger.LogGenericTrace($"{LastChangeNumber} => {currentChangeNumber}"); LastChangeNumber = currentChangeNumber; foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) { if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (appData.ChangeNumber <= previousChangeNumber)) { continue; } AppChangeNumbers.TryRemove(appID, out _); ASF.ArchiLogger.LogGenericTrace($"App needs refresh: {appID}"); } await Save().ConfigureAwait(false); } internal async Task OnPICSChangesRestart(uint currentChangeNumber) { if (currentChangeNumber == 0) { throw new ArgumentNullException(); } if (currentChangeNumber <= LastChangeNumber) { return; } ASF.ArchiLogger.LogGenericDebug($"RESET {LastChangeNumber} => {currentChangeNumber}"); LastChangeNumber = currentChangeNumber; AppChangeNumbers.Clear(); await Save().ConfigureAwait(false); } internal bool ShouldRefreshAppInfo(uint appID) => !AppChangeNumbers.ContainsKey(appID); internal bool ShouldRefreshDepotKey(uint depotID) => !DepotKeys.ContainsKey(depotID); internal async Task UpdateAppChangeNumbers([NotNull] IReadOnlyCollection> appChangeNumbers) { if (appChangeNumbers == null) { throw new ArgumentNullException(nameof(appChangeNumbers)); } bool save = false; foreach ((uint appID, uint changeNumber) in appChangeNumbers) { if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber == changeNumber)) { continue; } AppChangeNumbers[appID] = changeNumber; save = true; } if (save) { await Save().ConfigureAwait(false); } } internal async Task UpdateAppTokens([NotNull] IReadOnlyCollection> appTokens, [NotNull] IReadOnlyCollection publicAppIDs) { if ((appTokens == null) || (publicAppIDs == null)) { throw new ArgumentNullException(nameof(appTokens) + " || " + nameof(publicAppIDs)); } bool save = false; foreach ((uint appID, ulong appToken) in appTokens) { if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == appToken)) { continue; } AppTokens[appID] = appToken; if (appToken == 0) { // Backend is not interested in zero access tokens SubmittedAppIDs.Add(appID); } save = true; } foreach (uint appID in publicAppIDs) { if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == 0)) { continue; } AppTokens[appID] = 0; // Backend is not interested in zero access tokens SubmittedAppIDs.Add(appID); save = true; } if (save) { await Save().ConfigureAwait(false); } } internal async Task UpdateDepotKeys([NotNull] IReadOnlyCollection depotKeyResults) { if (depotKeyResults == null) { throw new ArgumentNullException(nameof(depotKeyResults)); } bool save = false; foreach (SteamApps.DepotKeyCallback depotKeyResult in depotKeyResults) { if ((depotKeyResult == null) || (depotKeyResult.Result != EResult.OK)) { continue; } string depotKey = BitConverter.ToString(depotKeyResult.DepotKey).Replace("-", ""); if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string previousDepotKey) && (previousDepotKey == depotKey)) { continue; } DepotKeys[depotKeyResult.DepotID] = depotKey; if (string.IsNullOrEmpty(depotKey)) { // Backend is not interested in zero depot keys SubmittedDepotIDs.Add(depotKeyResult.DepotID); } save = true; } if (save) { await Save().ConfigureAwait(false); } } internal async Task UpdatePackageTokens([NotNull] IReadOnlyCollection> packageTokens) { if (packageTokens == null) { throw new ArgumentNullException(nameof(packageTokens)); } bool save = false; foreach ((uint packageID, ulong packageToken) in packageTokens) { if (PackageTokens.TryGetValue(packageID, out ulong previousPackageToken) && (previousPackageToken == packageToken)) { continue; } PackageTokens[packageID] = packageToken; if (packageToken == 0) { // Backend is not interested in zero access tokens SubmittedPackageIDs.Add(packageID); } save = true; } if (save) { await Save().ConfigureAwait(false); } } internal async Task UpdateSubmittedData([NotNull] IReadOnlyCollection appIDs, [NotNull] IReadOnlyCollection packageIDs, [NotNull] IReadOnlyCollection depotIDs) { if ((appIDs == null) || (packageIDs == null) || (depotIDs == null)) { throw new ArgumentNullException(nameof(appIDs) + " || " + nameof(packageIDs) + " || " + nameof(depotIDs)); } bool save = false; foreach (uint _ in appIDs.Where(appID => SubmittedAppIDs.Add(appID))) { save = true; } foreach (uint _ in packageIDs.Where(packageID => SubmittedPackageIDs.Add(packageID))) { save = true; } foreach (uint _ in depotIDs.Where(depotID => SubmittedDepotIDs.Add(depotID))) { save = true; } if (save) { await Save().ConfigureAwait(false); } } } }