Skip to content

Commit 50fb5c7

Browse files
authored
feat: Allow Uploading/Importing Game Settings data to/from Database (#893)
# Main Goal Allowing user to synchronize game settings data including: - Game settings - Collapse game/region specific settings - Collapse playtime data The data gets uploaded to database server that user sets in app settings
2 parents 440746e + 7b3dc92 commit 50fb5c7

13 files changed

Lines changed: 776 additions & 248 deletions

File tree

‎CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs‎

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using CollapseLauncher.Helper;
2+
using CollapseLauncher.Helper.Database;
3+
using CollapseLauncher.Helper.Metadata;
24
using CollapseLauncher.Interfaces;
35
using Hi3Helper;
6+
using Hi3Helper.Data;
47
using Hi3Helper.EncTool;
58
using Hi3Helper.UABT;
69
using Hi3Helper.UABT.Binary;
@@ -70,11 +73,11 @@ public RegistryKey? RegistryRoot
7073
return RegistryRoot;
7174
}
7275

73-
public async Task<Exception?> ImportSettings(string? gameBasePath = null)
76+
public async Task<Exception?> ImportSettings(string? gameBasePath = null, string? path = null)
7477
{
7578
try
7679
{
77-
string path = await FileDialogNative.GetFilePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);
80+
path ??= await FileDialogNative.GetFilePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);
7881

7982
if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1);
8083

@@ -190,11 +193,11 @@ private void ReadV3Values(Stream fs, string? gameBasePath)
190193
ImportStreamToFiles(stream, gameBasePath);
191194
}
192195

193-
public async Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null)
196+
public async Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null, string? path = null)
194197
{
195198
try
196199
{
197-
string path = await FileDialogNative.GetFileSavePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle);
200+
path ??= await FileDialogNative.GetFileSavePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle);
198201
EnsureFileSaveHasExtension(ref path, ".clreg");
199202

200203
if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1);
@@ -533,5 +536,70 @@ protected virtual void ReadBinary(EndianBinaryReader reader, string valueName)
533536
_ = reader.Read(val, 0, len);
534537
RegistryRoot?.SetValue(valueName, val, RegistryValueKind.Binary);
535538
}
539+
540+
541+
# region database
542+
543+
private string GameTypeValue => (GameVersionManager?.GameType is not GameNameType.Plugin
544+
? GameVersionManager?.GameType.ToString() : GameVersionManager?.GameName.Replace(" ", "")) ?? "UNKNOWN";
545+
private string KeySettings => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs";
546+
private string KeyLastUpdated => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs-lu";
547+
548+
public async Task<Exception?> PushToDatabase()
549+
{
550+
try
551+
{
552+
string path = Path.GetTempFileName();
553+
_ = await ExportSettings(false, null, null, path);
554+
555+
if (!File.Exists(path)) return null;
556+
557+
var fi = new FileInfo(path);
558+
if (fi.Length == 0) return null;
559+
byte[] fileBytes = await File.ReadAllBytesAsync(path);
560+
await DbHandler.StoreKeyValue(KeySettings, "", true, true, fileBytes);
561+
await DbHandler.StoreKeyValue(KeyLastUpdated, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), true);
562+
fi.Delete();
563+
}
564+
catch (Exception ex)
565+
{
566+
Console.WriteLine(ex);
567+
return ex;
568+
}
569+
570+
return null;
571+
}
572+
573+
public async Task<Exception?> GetFromDatabase()
574+
{
575+
try
576+
{
577+
var retval = await DbHandler.QueryKey(KeySettings, true, true);
578+
579+
if (retval == null) throw new NullReferenceException();
580+
581+
string path = Path.GetTempFileName();
582+
await File.WriteAllBytesAsync(path, Convert.FromHexString(retval));
583+
584+
string? gameBasePath = null;
585+
if (GameVersionManager?.GameType == GameNameType.Zenless)
586+
{
587+
gameBasePath = ConverterTool.NormalizePath(GameVersionManager?.GameDirPath);
588+
}
589+
590+
await ImportSettings(gameBasePath, path);
591+
592+
File.Delete(path);
593+
}
594+
catch (Exception ex)
595+
{
596+
Console.WriteLine(ex);
597+
return ex;
598+
}
599+
600+
return null;
601+
}
602+
603+
#endregion
536604
}
537605
}

‎CollapseLauncher/Classes/Helper/Database/DBHandler.cs‎

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Hi3Helper.SentryHelper;
33
using Libsql.Client;
44
using System;
5+
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.Linq;
78
using System.Text;
@@ -29,7 +30,7 @@ public static bool? IsEnabled
2930
field = value;
3031
DbConfig.DbEnabled = value ?? false;
3132

32-
_isFirstInit = true; // Force first init
33+
IsInitialized = false; // Force first init
3334
if (!(value ?? false)) Dispose(); // Dispose instance if user disabled database function globally
3435
}
3536
}
@@ -45,11 +46,11 @@ public static string? Uri
4546
}
4647
set
4748
{
48-
if (value != field) _isFirstInit = true; // Force first init if value changed
49+
if (value != field) IsInitialized = false; // Force first init if value changed
4950

5051
field = value;
5152
DbConfig.DbUrl = value;
52-
_isFirstInit = true;
53+
IsInitialized = false;
5354
}
5455
}
5556

@@ -65,11 +66,11 @@ public static string? Token
6566
}
6667
set
6768
{
68-
if (value != field) _isFirstInit = true; // Force first init if value changed
69+
if (value != field) IsInitialized = false; // Force first init if value changed
6970

7071
field = value;
7172
DbConfig.DbToken = value;
72-
_isFirstInit = true;
73+
IsInitialized = false;
7374
}
7475
}
7576

@@ -93,25 +94,26 @@ public static string? UserId
9394
}
9495
set
9596
{
96-
if (value != field) _isFirstInit = true; // Force first init if value changed
97+
if (value != field) IsInitialized = false; // Force first init if value changed
9798

9899
field = value;
99100
DbConfig.UserGuid = value;
100101

101102
if (string.IsNullOrWhiteSpace(value))
102103
{
103104
_userIdHash = null;
104-
_isFirstInit = true;
105+
IsInitialized = false;
105106
return;
106107
}
107108

108109
var byteUidH = System.IO.Hashing.XxHash64.Hash(Encoding.ASCII.GetBytes(value));
109110
_userIdHash = Convert.ToHexStringLower(byteUidH);
110-
_isFirstInit = true;
111+
IsInitialized = false;
111112
}
112113
}
114+
115+
public static bool IsInitialized { get; private set; } = false;
113116

114-
private static bool _isFirstInit = true;
115117
#endregion
116118

117119
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
@@ -156,14 +158,18 @@ public static async Task Init(bool redirectThrow = false, bool bypassEnableFlag
156158
opts.AuthToken = Token;
157159
});
158160

159-
if (_isFirstInit)
161+
if (!IsInitialized)
160162
{
161163
LogWriteLine("[DbHandler::Init] Initializing database system...");
162164
// Ensure table exist at first initialization
163165
await
164166
_database
165167
.Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' TEXT)");
166-
_isFirstInit = false;
168+
169+
await
170+
_database
171+
.Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}-blob\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' BLOB)");
172+
IsInitialized = true;
167173
}
168174
else LogWriteLine("[DbHandler::Init] Reinitializing database system...");
169175
}
@@ -218,7 +224,7 @@ private static void Dispose()
218224

219225
private const int MaxAttempts = 5;
220226

221-
public static async Task<string?> QueryKey(string key, bool redirectThrow = false)
227+
public static async Task<string?> QueryKey(string key, bool redirectThrow = false, bool isBlob = false)
222228
{
223229
if (!(IsEnabled ?? false)) return null;
224230
#if DEBUG
@@ -229,16 +235,24 @@ private static void Dispose()
229235
#endif
230236
for (var i = 0; i < MaxAttempts; i++)
231237
{
232-
var retVal = await QueryKeyInternal(key
238+
var retVal = await QueryKeyInternal(key, isBlob
233239
#if DEBUG
234240
, sId
235241
#endif
236242
);
237243
if (retVal.result == 200)
238244
{
239245
#if DEBUG
240-
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}",
241-
LogType.Debug, true);
246+
if (isBlob)
247+
{
248+
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB",
249+
LogType.Debug, true);
250+
}
251+
else
252+
{
253+
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}",
254+
LogType.Debug, true);
255+
}
242256
#endif
243257
return retVal.returnedValue;
244258
}
@@ -278,24 +292,43 @@ private static void Dispose()
278292
return null;
279293
}
280294

281-
public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false)
295+
public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false,
296+
bool isBlob = false, byte[]? blobValue = null)
282297
{
283298
if (!(IsEnabled ?? false)) return;
284299
#if DEBUG
285300
var t = Stopwatch.StartNew();
286301
var r = new Random();
287302
var sId = Math.Abs(r.Next(0, 1000).ToString().GetHashCode());
288-
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug,
289-
true);
303+
if (isBlob)
304+
{
305+
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug,
306+
true);
307+
}
308+
else
309+
{
310+
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug,
311+
true);
312+
}
313+
290314
#endif
291315
for (var i = 0; i < MaxAttempts; i++)
292316
{
293-
var retVal = await StoreKeyValueInternal(key, value);
317+
var retVal = await StoreKeyValueInternal(key, value, isBlob, blobValue);
294318
if (retVal.result == 200)
295319
{
296320
#if DEBUG
297-
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}",
298-
LogType.Debug, true);
321+
if (isBlob)
322+
{
323+
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tIS BLOB",
324+
LogType.Debug, true);
325+
}
326+
else
327+
{
328+
LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}",
329+
LogType.Debug, true);
330+
}
331+
299332
#endif
300333
return;
301334
}
@@ -338,7 +371,7 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
338371

339372
#region Private Methods
340373

341-
private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key
374+
private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key, bool isBlob
342375
#if DEBUG
343376
, int sId = 0
344377
#endif
@@ -347,22 +380,53 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
347380
try
348381
{
349382
if (_database == null) await Init(true);
383+
var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : "");
350384
// Get table row for exact key
351385
var rs =
352386
await
353387
_database!
354-
.Execute($"SELECT value FROM \"uid-{_userIdHash}\" WHERE key = ?", key);
388+
.Execute($"SELECT value FROM \"{tableName}\" WHERE key = ?", key);
355389
if (rs == null)
356390
{
357391
return (200, null, null);
358392
}
393+
394+
string str = "";
359395

360-
// freaking black magic to convert the column row to the value
361-
var str =
362-
string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString()))));
396+
if (isBlob)
397+
{
398+
var firstRow = rs.Rows.FirstOrDefault();
399+
object? rcv = firstRow?.FirstOrDefault();
400+
401+
if (rcv is Blob { Value: IEnumerable<byte> byteEnumerable })
402+
{
403+
str = Convert.ToHexString(byteEnumerable.ToArray());
404+
}
405+
// ReSharper disable once ConvertTypeCheckPatternToNullCheck
406+
else if (rcv is Blob { Value: byte[] directBytes })
407+
{
408+
str = Convert.ToHexString(directBytes);
409+
}
410+
}
411+
else
412+
{
413+
// freaking black magic to convert the column row to the value
414+
str =
415+
string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString()))));
416+
}
417+
363418
#if DEBUG
364-
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug,
365-
true);
419+
if (isBlob)
420+
{
421+
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug,
422+
true);
423+
}
424+
else
425+
{
426+
LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug,
427+
true);
428+
}
429+
366430
#endif
367431
return (200, str, null); // 200: OK, return value
368432
}
@@ -386,16 +450,18 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
386450
}
387451
}
388452

389-
private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value)
453+
private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value, bool isBlob = false, byte[]? blobValue = null)
390454
{
391455
try
392456
{
393457
if (_database == null) await Init(true);
458+
var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : "");
459+
object dbValue = isBlob && blobValue != null ? blobValue : value;
394460

395461
// Create key for storing value, if key already exist, just update the value (key column is set to UNIQUE)
396-
var command = $"INSERT INTO \"uid-{_userIdHash}\" (key, value) VALUES (?, ?) " +
462+
var command = $"INSERT INTO \"{tableName}\" (key, value) VALUES (?, ?) " +
397463
$"ON CONFLICT(key) DO UPDATE SET value = ?";
398-
var parameters = new object[] { key, value, value };
464+
var parameters = new object[] { key, dbValue, dbValue };
399465
await _database!.Execute(command, parameters);
400466

401467
return (200, null); // 200: OK

‎CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ public interface IGameSettingsExportable
2222
RegistryKey? RegistryRoot { get; }
2323

2424
RegistryKey? RefreshRegistryRoot();
25-
Task<Exception?> ImportSettings(string? gameBasePath = null);
26-
Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null);
25+
Task<Exception?> ImportSettings(string? gameBasePath = null, string? path = null);
26+
27+
Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null,
28+
string[]? relativePathToImport = null, string? path = null);
29+
30+
Task<Exception?> PushToDatabase();
31+
Task<Exception?> GetFromDatabase();
2732
}
2833
}

0 commit comments

Comments
 (0)