From 2fd2bc7b2be674cde45e0c512128819e0114681a Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Sun, 5 Jul 2026 23:56:52 +0700 Subject: [PATCH 1/8] [DB] Add blob table --- CollapseLauncher/Classes/Helper/Database/DBHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs index 69b5bb6e6e..5add617718 100644 --- a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs +++ b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs @@ -163,6 +163,10 @@ public static async Task Init(bool redirectThrow = false, bool bypassEnableFlag await _database .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' TEXT)"); + + await + _database + .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}-blob\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' BLOB)"); _isFirstInit = false; } else LogWriteLine("[DbHandler::Init] Reinitializing database system..."); From 6340502b40b67856370cb0af0b3c4f2ca9646224 Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Sun, 5 Jul 2026 23:57:14 +0700 Subject: [PATCH 2/8] [DB] Allow blob upload --- .../Classes/Helper/Database/DBHandler.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs index 5add617718..f44a32f097 100644 --- a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs +++ b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs @@ -282,24 +282,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; } @@ -390,16 +409,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 From 14fe04dc75dc069444a0fa9554e3139ef7f5ad44 Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Sun, 5 Jul 2026 23:58:16 +0700 Subject: [PATCH 3/8] [GSP] Allow Uploading Game Settings to Database WARNING - Broken UI - Static text --- .../BaseClass/ImportExportBase.cs | 41 ++++++++++++- .../Interfaces/IGameSettingsUniversal.cs | 6 +- .../GameSettingsPages/GameSettingsPageBase.cs | 58 +++++++++++++++++++ .../GenshinGameSettingsPage.xaml | 39 +++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs index e907634086..345a6b1c9e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs @@ -1,4 +1,6 @@ using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Database; +using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Interfaces; using Hi3Helper; using Hi3Helper.EncTool; @@ -190,11 +192,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 +535,40 @@ 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; + } + + #endregion } } diff --git a/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs index 46281787a5..47559ff999 100644 --- a/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs +++ b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs @@ -23,6 +23,10 @@ public interface IGameSettingsExportable RegistryKey? RefreshRegistryRoot(); Task ImportSettings(string? gameBasePath = null); - Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null); + + Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, + string[]? relativePathToImport = null, string? path = null); + + Task PushToDatabase(); } } diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs index 548c2d905e..0e26dd658c 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs @@ -196,6 +196,64 @@ async Task Impl() } } } + + protected virtual async void OnRegistryDbUploadButtonClick(object sender, RoutedEventArgs args) + { + await SuspendRegistryMonitorOnActionAsync(Impl); + return; + + async Task Impl() + { + try + { + Exception? exc = await (Settings?.PushToDatabase() ?? Task.FromResult(null)); + + if (exc != null) throw exc; + SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegExported"); + } + catch (OperationCanceledException) + { + SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegErr1", true); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[GSP Module] An error has occurred while trying to exporting the registry!\r\n{ex}", LogType.Error, true); + SetApplyTextStatus(ex.Message, true); + ErrorSender.SendException(ex); + SentryHelper.ExceptionHandler(ex); + } + } + } + + protected virtual async void OnRegistryDbDownloadButtonClick(object sender, RoutedEventArgs args) + { + await SuspendRegistryMonitorOnActionAsync(Impl); + return; + + async Task Impl() + { + try + { + throw new NotImplementedException(); + Exception? exc = await (Settings?.PushToDatabase() ?? Task.FromResult(null)); + + if (exc != null) throw exc; + SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegExported"); + } + catch (OperationCanceledException) + { + SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegErr1", true); + } + catch (Exception ex) + { + Logger.LogWriteLine($"[GSP Module] An error has occurred while trying to exporting the registry!\r\n{ex}", LogType.Error, true); + SetApplyTextStatus(ex.Message, true); + ErrorSender.SendException(ex); + SentryHelper.ExceptionHandler(ex); + } + } + } + protected virtual void SuspendRegistryMonitorOnAction(Action action) { diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml index 0c3aae292a..4bdba9e211 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml @@ -1149,6 +1149,7 @@ + + + + + From ce88b3f50db8c59d987f7d278fb75de684a38f1a Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Mon, 6 Jul 2026 00:44:24 +0700 Subject: [PATCH 4/8] [DB] Handle BLOB Query --- .../Classes/Helper/Database/DBHandler.cs | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs index f44a32f097..50fd1d623e 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; @@ -222,7 +223,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 @@ -233,7 +234,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 @@ -241,8 +242,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; } @@ -361,7 +370,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 @@ -370,22 +379,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 } From 65dc8d5a4d74dc2b6bed7010a0dcb316124fba8e Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Mon, 6 Jul 2026 00:44:54 +0700 Subject: [PATCH 5/8] [GSP] Allow Applying Game Settings From Database --- .../BaseClass/ImportExportBase.cs | 35 +++++++++++++++++-- .../Interfaces/IGameSettingsUniversal.cs | 3 +- .../GameSettingsPages/GameSettingsPageBase.cs | 5 +-- .../GenshinGameSettingsPage.xaml | 1 + 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs index 345a6b1c9e..50b1e78d8a 100644 --- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs +++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs @@ -3,6 +3,7 @@ using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Interfaces; using Hi3Helper; +using Hi3Helper.Data; using Hi3Helper.EncTool; using Hi3Helper.UABT; using Hi3Helper.UABT.Binary; @@ -72,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); @@ -569,6 +570,36 @@ protected virtual void ReadBinary(EndianBinaryReader reader, string valueName) 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/Interfaces/IGameSettingsUniversal.cs b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs index 47559ff999..0d5be1c1b9 100644 --- a/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs +++ b/CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs @@ -22,11 +22,12 @@ public interface IGameSettingsExportable RegistryKey? RegistryRoot { get; } RegistryKey? RefreshRegistryRoot(); - Task ImportSettings(string? gameBasePath = 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 0e26dd658c..9ce2f0a880 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GameSettingsPageBase.cs @@ -209,6 +209,7 @@ async Task Impl() Exception? exc = await (Settings?.PushToDatabase() ?? Task.FromResult(null)); if (exc != null) throw exc; + // TODO: Apply localization SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegExported"); } catch (OperationCanceledException) @@ -234,10 +235,10 @@ async Task Impl() { try { - throw new NotImplementedException(); - Exception? exc = await (Settings?.PushToDatabase() ?? Task.FromResult(null)); + Exception? exc = await (Settings?.GetFromDatabase() ?? Task.FromResult(null)); if (exc != null) throw exc; + // TODO: Apply localization SetApplyTextStatus("Lang._GameSettingsPage.SettingsRegExported"); } catch (OperationCanceledException) diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml index 4bdba9e211..993184eb18 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/GameSettingsPages/GenshinGameSettingsPage.xaml @@ -1173,6 +1173,7 @@ +