diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs index e907634086..50b1e78d8a 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs @@ -1,6 +1,9 @@ using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Database; +using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Interfaces; using Hi3Helper; +using Hi3Helper.Data; using Hi3Helper.EncTool; using Hi3Helper.UABT; using Hi3Helper.UABT.Binary; @@ -70,11 +73,11 @@ public RegistryKey? RegistryRoot return RegistryRoot; } - public async Task ImportSettings(string? gameBasePath = null) + public async Task ImportSettings(string? gameBasePath = null, string? path = null) { try { - string path = await FileDialogNative.GetFilePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle); + path ??= await FileDialogNative.GetFilePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle); if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1); @@ -190,11 +193,11 @@ private void ReadV3Values(Stream fs, string? gameBasePath) ImportStreamToFiles(stream, gameBasePath); } - public async Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null) + public async Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null, string? path = null) { try { - string path = await FileDialogNative.GetFileSavePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle); + path ??= await FileDialogNative.GetFileSavePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle); EnsureFileSaveHasExtension(ref path, ".clreg"); if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1); @@ -533,5 +536,70 @@ protected virtual void ReadBinary(EndianBinaryReader reader, string valueName) _ = reader.Read(val, 0, len); RegistryRoot?.SetValue(valueName, val, RegistryValueKind.Binary); } + + + # region database + + private string GameTypeValue => (GameVersionManager?.GameType is not GameNameType.Plugin + ? GameVersionManager?.GameType.ToString() : GameVersionManager?.GameName.Replace(" ", "")) ?? "UNKNOWN"; + private string KeySettings => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs"; + private string KeyLastUpdated => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs-lu"; + + public async Task PushToDatabase() + { + try + { + string path = Path.GetTempFileName(); + _ = await ExportSettings(false, null, null, path); + + if (!File.Exists(path)) return null; + + var fi = new FileInfo(path); + if (fi.Length == 0) return null; + byte[] fileBytes = await File.ReadAllBytesAsync(path); + await DbHandler.StoreKeyValue(KeySettings, "", true, true, fileBytes); + await DbHandler.StoreKeyValue(KeyLastUpdated, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), true); + fi.Delete(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + return ex; + } + + return null; + } + + public async Task GetFromDatabase() + { + try + { + var retval = await DbHandler.QueryKey(KeySettings, true, true); + + if (retval == null) throw new NullReferenceException(); + + string path = Path.GetTempFileName(); + await File.WriteAllBytesAsync(path, Convert.FromHexString(retval)); + + string? gameBasePath = null; + if (GameVersionManager?.GameType == GameNameType.Zenless) + { + gameBasePath = ConverterTool.NormalizePath(GameVersionManager?.GameDirPath); + } + + await ImportSettings(gameBasePath, path); + + File.Delete(path); + } + catch (Exception ex) + { + Console.WriteLine(ex); + return ex; + } + + return null; + } + + #endregion } } diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs index 69b5bb6e6e..e5f5e467fc 100644 --- a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs +++ b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs @@ -2,6 +2,7 @@ using Hi3Helper.SentryHelper; using Libsql.Client; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; @@ -29,7 +30,7 @@ public static bool? IsEnabled field = value; DbConfig.DbEnabled = value ?? false; - _isFirstInit = true; // Force first init + IsInitialized = false; // Force first init if (!(value ?? false)) Dispose(); // Dispose instance if user disabled database function globally } } @@ -45,11 +46,11 @@ public static string? Uri } set { - if (value != field) _isFirstInit = true; // Force first init if value changed + if (value != field) IsInitialized = false; // Force first init if value changed field = value; DbConfig.DbUrl = value; - _isFirstInit = true; + IsInitialized = false; } } @@ -65,11 +66,11 @@ public static string? Token } set { - if (value != field) _isFirstInit = true; // Force first init if value changed + if (value != field) IsInitialized = false; // Force first init if value changed field = value; DbConfig.DbToken = value; - _isFirstInit = true; + IsInitialized = false; } } @@ -93,7 +94,7 @@ public static string? UserId } set { - if (value != field) _isFirstInit = true; // Force first init if value changed + if (value != field) IsInitialized = false; // Force first init if value changed field = value; DbConfig.UserGuid = value; @@ -101,17 +102,18 @@ public static string? UserId if (string.IsNullOrWhiteSpace(value)) { _userIdHash = null; - _isFirstInit = true; + IsInitialized = false; return; } var byteUidH = System.IO.Hashing.XxHash64.Hash(Encoding.ASCII.GetBytes(value)); _userIdHash = Convert.ToHexStringLower(byteUidH); - _isFirstInit = true; + IsInitialized = false; } } + + public static bool IsInitialized { get; private set; } = false; - private static bool _isFirstInit = true; #endregion [DebuggerBrowsable(DebuggerBrowsableState.Never)] @@ -156,14 +158,18 @@ public static async Task Init(bool redirectThrow = false, bool bypassEnableFlag opts.AuthToken = Token; }); - if (_isFirstInit) + if (!IsInitialized) { LogWriteLine("[DbHandler::Init] Initializing database system..."); // Ensure table exist at first initialization await _database .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' TEXT)"); - _isFirstInit = false; + + await + _database + .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}-blob\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' BLOB)"); + IsInitialized = true; } else LogWriteLine("[DbHandler::Init] Reinitializing database system..."); } @@ -218,7 +224,7 @@ private static void Dispose() private const int MaxAttempts = 5; - public static async Task QueryKey(string key, bool redirectThrow = false) + public static async Task QueryKey(string key, bool redirectThrow = false, bool isBlob = false) { if (!(IsEnabled ?? false)) return null; #if DEBUG @@ -229,7 +235,7 @@ private static void Dispose() #endif for (var i = 0; i < MaxAttempts; i++) { - var retVal = await QueryKeyInternal(key + var retVal = await QueryKeyInternal(key, isBlob #if DEBUG , sId #endif @@ -237,8 +243,16 @@ private static void Dispose() if (retVal.result == 200) { #if DEBUG - LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}", - LogType.Debug, true); + if (isBlob) + { + LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB", + LogType.Debug, true); + } + else + { + LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}", + LogType.Debug, true); + } #endif return retVal.returnedValue; } @@ -278,24 +292,43 @@ private static void Dispose() return null; } - public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false) + public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false, + bool isBlob = false, byte[]? blobValue = null) { if (!(IsEnabled ?? false)) return; #if DEBUG var t = Stopwatch.StartNew(); var r = new Random(); var sId = Math.Abs(r.Next(0, 1000).ToString().GetHashCode()); - LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug, - true); + if (isBlob) + { + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug, + true); + } + else + { + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug, + true); + } + #endif for (var i = 0; i < MaxAttempts; i++) { - var retVal = await StoreKeyValueInternal(key, value); + var retVal = await StoreKeyValueInternal(key, value, isBlob, blobValue); if (retVal.result == 200) { #if DEBUG - LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}", - LogType.Debug, true); + if (isBlob) + { + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tIS BLOB", + LogType.Debug, true); + } + else + { + LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}", + LogType.Debug, true); + } + #endif return; } @@ -338,7 +371,7 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh #region Private Methods - private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key + private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key, bool isBlob #if DEBUG , int sId = 0 #endif @@ -347,22 +380,53 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh try { if (_database == null) await Init(true); + var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : ""); // Get table row for exact key var rs = await _database! - .Execute($"SELECT value FROM \"uid-{_userIdHash}\" WHERE key = ?", key); + .Execute($"SELECT value FROM \"{tableName}\" WHERE key = ?", key); if (rs == null) { return (200, null, null); } + + string str = ""; - // freaking black magic to convert the column row to the value - var str = - string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString())))); + if (isBlob) + { + var firstRow = rs.Rows.FirstOrDefault(); + object? rcv = firstRow?.FirstOrDefault(); + + if (rcv is Blob { Value: IEnumerable byteEnumerable }) + { + str = Convert.ToHexString(byteEnumerable.ToArray()); + } + // ReSharper disable once ConvertTypeCheckPatternToNullCheck + else if (rcv is Blob { Value: byte[] directBytes }) + { + str = Convert.ToHexString(directBytes); + } + } + else + { + // freaking black magic to convert the column row to the value + str = + string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString())))); + } + #if DEBUG - LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug, - true); + if (isBlob) + { + LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug, + true); + } + else + { + LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug, + true); + } + #endif return (200, str, null); // 200: OK, return value } @@ -386,16 +450,18 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh } } - private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value) + private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value, bool isBlob = false, byte[]? blobValue = null) { try { if (_database == null) await Init(true); + var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : ""); + object dbValue = isBlob && blobValue != null ? blobValue : value; // Create key for storing value, if key already exist, just update the value (key column is set to UNIQUE) - var command = $"INSERT INTO \"uid-{_userIdHash}\" (key, value) VALUES (?, ?) " + + var command = $"INSERT INTO \"{tableName}\" (key, value) VALUES (?, ?) " + $"ON CONFLICT(key) DO UPDATE SET value = ?"; - var parameters = new object[] { key, value, value }; + var parameters = new object[] { key, dbValue, dbValue }; await _database!.Execute(command, parameters); return (200, null); // 200: OK diff --git a/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs index 46281787a5..0d5be1c1b9 100644 --- a/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs +++ b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs @@ -22,7 +22,12 @@ public interface IGameSettingsExportable RegistryKey? RegistryRoot { get; } RegistryKey? RefreshRegistryRoot(); - Task ImportSettings(string? gameBasePath = null); - Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null); + Task ImportSettings(string? gameBasePath = null, string? path = null); + + Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, + string[]? relativePathToImport = null, string? path = null); + + Task PushToDatabase(); + Task GetFromDatabase(); } } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs index 548c2d905e..13d45482e3 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs @@ -1,10 +1,12 @@ -using CollapseLauncher.Extension; +using CollapseLauncher.Helper.Database; +using CollapseLauncher.Extension; using CollapseLauncher.Helper; using CollapseLauncher.Interfaces; using CollapseLauncher.RegistryUtils; using CollapseLauncher.Statics; using Hi3Helper; using Hi3Helper.SentryHelper; +using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; @@ -94,7 +96,9 @@ protected GameSettingsPageBase(IGameSettings? settings, RegistryKey? gameRegistr VerticalAlignment = VerticalAlignment.Center, Style = UIElementExtensions.GetApplicationResource + \ No newline at end of file diff --git a/CollapseLauncher/XAMLs/Theme/Button/TransparentDropDownButton.xaml b/CollapseLauncher/XAMLs/Theme/Button/TransparentDropDownButton.xaml new file mode 100644 index 0000000000..644baad54d --- /dev/null +++ b/CollapseLauncher/XAMLs/Theme/Button/TransparentDropDownButton.xaml @@ -0,0 +1,73 @@ + + + \ No newline at end of file diff --git a/CollapseLauncher/XAMLs/Theme/StylesGeneric.xaml b/CollapseLauncher/XAMLs/Theme/StylesGeneric.xaml index bf4cd5a415..857daeb65c 100644 --- a/CollapseLauncher/XAMLs/Theme/StylesGeneric.xaml +++ b/CollapseLauncher/XAMLs/Theme/StylesGeneric.xaml @@ -3,6 +3,7 @@ + @@ -10,6 +11,7 @@ + diff --git a/Hi3Helper.Core/Lang/en_US.json b/Hi3Helper.Core/Lang/en_US.json index 271407c0d0..f5599a7f96 100644 --- a/Hi3Helper.Core/Lang/en_US.json +++ b/Hi3Helper.Core/Lang/en_US.json @@ -435,6 +435,19 @@ "RegImportTitle": "Import", "RegImportTooltip": "Import registry keys from a Collapse Registry file", + "DBImportExport": "Upload/Restore to DB", + "DBImportExportTooltip": "Restore/Upload Game Settings from/to Database", + "DBUploadTitle": "Upload", + "DBUploadTooltip": "Upload Game Settings registry to Database set in App Settings", + "DBRestoreTitle": "Restore", + "DBRestoreTooltip": "Restore Game Settings data to Database set in App Settings", + + "SettingsDBExported": "Settings data has been uploaded!", + "SettingsDBExportTitle": "Upload Settings data to Database", + "SettingsDBImported": "Settings data has been imported from DB! (A launcher restart is required to apply the imported changes)", + "SettingsDBNotSetup": "Database function is not yet enabled. Check app settings!", + "SettingsDBNotInitialized": "Database subsystem is not initialized or has failed, check logs/console!", + "OverlayNotInstalledTitle": "You cannot use this feature as the region isn't installed or needs to be updated!", "OverlayNotInstalledSubtitle": "Please download/update the game first on the game's launcher page!", "OverlayGameRunningTitle": "Game is Currently Running!",