1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2024 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
16 // http://www.apache.org/licenses/LICENSE-2.0
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
25 using System
.Collections
.Concurrent
;
26 using System
.Collections
.Generic
;
27 using System
.Globalization
;
30 using System
.Text
.Json
;
31 using System
.Text
.Json
.Serialization
;
32 using System
.Threading
;
33 using System
.Threading
.Tasks
;
34 using ArchiSteamFarm
.Collections
;
35 using ArchiSteamFarm
.Core
;
36 using ArchiSteamFarm
.Helpers
.Json
;
37 using ArchiSteamFarm
.Localization
;
38 using ArchiSteamFarm
.Steam
;
39 using ArchiSteamFarm
.Steam
.SteamKit2
;
40 using JetBrains
.Annotations
;
42 namespace ArchiSteamFarm
.Storage
;
44 public sealed class GlobalDatabase
: GenericDatabase
{
47 public IReadOnlyDictionary
<uint, ulong> PackageAccessTokensReadOnly
=> PackagesAccessTokens
;
51 public IReadOnlyDictionary
<uint, PackageData
> PackagesDataReadOnly
=> PackagesData
;
53 private readonly SemaphoreSlim PackagesRefreshSemaphore
= new(1, 1);
57 public Guid Identifier { get; private init; }
= Guid
.NewGuid();
61 internal ConcurrentHashSet
<ulong> CachedBadBots { get; private init; }
= [];
65 internal ObservableConcurrentDictionary
<uint, byte> CardCountsPerGame { get; private init; }
= new();
67 internal uint CellID
{
71 if (BackingCellID
== value) {
75 BackingCellID
= value;
76 Utilities
.InBackground(Save
);
80 internal uint LastChangeNumber
{
81 get => BackingLastChangeNumber
;
84 if (BackingLastChangeNumber
== value) {
88 BackingLastChangeNumber
= value;
89 Utilities
.InBackground(Save
);
95 internal InMemoryServerListProvider ServerListProvider { get; private init; }
= new();
98 [JsonPropertyName($"_{nameof(CellID)}")]
99 private uint BackingCellID { get; set; }
102 [JsonPropertyName($"_{nameof(LastChangeNumber)}")]
103 private uint BackingLastChangeNumber { get; set; }
107 private ConcurrentDictionary
<uint, ulong> PackagesAccessTokens { get; init; }
= new();
111 private ConcurrentDictionary
<uint, PackageData
> PackagesData { get; init; }
= new();
113 private GlobalDatabase(string filePath
) : this() {
114 ArgumentException
.ThrowIfNullOrEmpty(filePath
);
120 private GlobalDatabase() {
121 CachedBadBots
.OnModified
+= OnObjectModified
;
122 CardCountsPerGame
.OnModified
+= OnObjectModified
;
123 ServerListProvider
.ServerListUpdated
+= OnObjectModified
;
127 public void DeleteFromJsonStorage(string key
) {
128 ArgumentException
.ThrowIfNullOrEmpty(key
);
130 DeleteFromJsonStorage(this, key
);
134 public void SaveToJsonStorage
<T
>(string key
, T
value) where T
: notnull
{
135 ArgumentException
.ThrowIfNullOrEmpty(key
);
136 ArgumentNullException
.ThrowIfNull(value);
138 SaveToJsonStorage(this, key
, value);
142 public void SaveToJsonStorage(string key
, JsonElement
value) {
143 ArgumentException
.ThrowIfNullOrEmpty(key
);
145 if (value.ValueKind
== JsonValueKind
.Undefined
) {
146 throw new ArgumentOutOfRangeException(nameof(value));
149 SaveToJsonStorage(this, key
, value);
153 public bool ShouldSerializeBackingCellID() => BackingCellID
!= 0;
156 public bool ShouldSerializeBackingLastChangeNumber() => BackingLastChangeNumber
!= 0;
159 public bool ShouldSerializeCachedBadBots() => CachedBadBots
.Count
> 0;
162 public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame
.IsEmpty
;
165 public bool ShouldSerializePackagesAccessTokens() => !PackagesAccessTokens
.IsEmpty
;
168 public bool ShouldSerializePackagesData() => !PackagesData
.IsEmpty
;
171 public bool ShouldSerializeServerListProvider() => ServerListProvider
.ShouldSerializeServerRecords();
173 protected override void Dispose(bool disposing
) {
175 // Events we registered
176 CachedBadBots
.OnModified
-= OnObjectModified
;
177 CardCountsPerGame
.OnModified
-= OnObjectModified
;
178 ServerListProvider
.ServerListUpdated
-= OnObjectModified
;
180 // Those are objects that are always being created if constructor doesn't throw exception
181 PackagesRefreshSemaphore
.Dispose();
185 base.Dispose(disposing
);
188 protected override Task
Save() => Save(this);
190 internal static async Task
<GlobalDatabase
?> CreateOrLoad(string filePath
) {
191 ArgumentException
.ThrowIfNullOrEmpty(filePath
);
193 if (!File
.Exists(filePath
)) {
194 GlobalDatabase result
= new(filePath
);
196 Utilities
.InBackground(() => Save(result
));
201 GlobalDatabase
? globalDatabase
;
204 string json
= await File
.ReadAllTextAsync(filePath
).ConfigureAwait(false);
206 if (string.IsNullOrEmpty(json
)) {
207 ASF
.ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorIsEmpty
, nameof(json
)));
212 globalDatabase
= json
.ToJsonObject
<GlobalDatabase
>();
213 } catch (Exception e
) {
214 ASF
.ArchiLogger
.LogGenericException(e
);
219 if (globalDatabase
== null) {
220 ASF
.ArchiLogger
.LogNullError(globalDatabase
);
225 globalDatabase
.FilePath
= filePath
;
227 return globalDatabase
;
230 internal HashSet
<uint> GetPackageIDs(uint appID
, IEnumerable
<uint> packageIDs
, int limit
= int.MaxValue
) {
231 ArgumentOutOfRangeException
.ThrowIfZero(appID
);
232 ArgumentNullException
.ThrowIfNull(packageIDs
);
234 HashSet
<uint> result
= [];
236 foreach (uint packageID
in packageIDs
.Where(static packageID
=> packageID
!= 0)) {
237 if (!PackagesData
.TryGetValue(packageID
, out PackageData
? packageEntry
) || (packageEntry
.AppIDs
?.Contains(appID
) != true)) {
241 result
.Add(packageID
);
243 if (result
.Count
>= limit
) {
251 internal async Task
OnPICSChangesRestart(uint currentChangeNumber
) {
252 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
254 if (Bot
.Bots
== null) {
255 throw new InvalidOperationException(nameof(Bot
.Bots
));
258 if (currentChangeNumber
<= LastChangeNumber
) {
262 LastChangeNumber
= currentChangeNumber
;
264 Bot
? refreshBot
= Bot
.Bots
.Values
.FirstOrDefault(static bot
=> bot
.IsConnectedAndLoggedOn
);
266 if (refreshBot
== null) {
270 if (PackagesData
.IsEmpty
) {
274 Dictionary
<uint, uint> packageIDs
= PackagesData
.Keys
.ToDictionary(static packageID
=> packageID
, _
=> currentChangeNumber
);
276 await RefreshPackages(refreshBot
, packageIDs
).ConfigureAwait(false);
279 internal void RefreshPackageAccessTokens(IReadOnlyDictionary
<uint, ulong> packageAccessTokens
) {
280 if ((packageAccessTokens
== null) || (packageAccessTokens
.Count
== 0)) {
281 throw new ArgumentNullException(nameof(packageAccessTokens
));
286 foreach ((uint packageID
, ulong currentAccessToken
) in packageAccessTokens
) {
287 if (!PackagesAccessTokens
.TryGetValue(packageID
, out ulong previousAccessToken
) || (previousAccessToken
!= currentAccessToken
)) {
288 PackagesAccessTokens
[packageID
] = currentAccessToken
;
294 Utilities
.InBackground(Save
);
298 internal async Task
RefreshPackages(Bot bot
, IReadOnlyDictionary
<uint, uint> packages
) {
299 ArgumentNullException
.ThrowIfNull(bot
);
301 if ((packages
== null) || (packages
.Count
== 0)) {
302 throw new ArgumentNullException(nameof(packages
));
305 await PackagesRefreshSemaphore
.WaitAsync().ConfigureAwait(false);
308 DateTime now
= DateTime
.UtcNow
;
310 HashSet
<uint> packageIDs
= packages
.Where(package
=> (package
.Key
!= 0) && (!PackagesData
.TryGetValue(package
.Key
, out PackageData
? previousData
) || (previousData
.ChangeNumber
< package
.Value
) || (previousData
.ValidUntil
< now
))).Select(static package
=> package
.Key
).ToHashSet();
312 if (packageIDs
.Count
== 0) {
316 Dictionary
<uint, PackageData
>? packagesData
= await bot
.GetPackagesData(packageIDs
).ConfigureAwait(false);
318 if (packagesData
== null) {
319 bot
.ArchiLogger
.LogGenericWarning(Strings
.WarningFailed
);
324 foreach ((uint packageID
, PackageData packageData
) in packagesData
) {
325 PackagesData
[packageID
] = packageData
;
328 Utilities
.InBackground(Save
);
330 PackagesRefreshSemaphore
.Release();
334 private async void OnObjectModified(object? sender
, EventArgs e
) {
335 if (string.IsNullOrEmpty(FilePath
)) {
339 await Save().ConfigureAwait(false);