Skip to content
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -70,11 +73,11 @@ public RegistryKey? RegistryRoot
return RegistryRoot;
}

public async Task<Exception?> ImportSettings(string? gameBasePath = null)
public async Task<Exception?> ImportSettings(string? gameBasePath = null, string? path = null)
{
try
{
string path = await FileDialogNative.GetFilePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);
path ??= await FileDialogNative.GetFilePicker(new Dictionary<string, string> { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);

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

Expand Down Expand Up @@ -190,11 +193,11 @@ private void ReadV3Values(Stream fs, string? gameBasePath)
ImportStreamToFiles(stream, gameBasePath);
}

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

if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1);
Expand Down Expand Up @@ -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<Exception?> 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<Exception?> 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
}
}
128 changes: 97 additions & 31 deletions CollapseLauncher/Classes/Helper/Database/DBHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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;
}
}

Expand All @@ -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;
}
}

Expand All @@ -93,25 +94,26 @@ 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;

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)]
Expand Down Expand Up @@ -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...");
}
Expand Down Expand Up @@ -218,7 +224,7 @@ private static void Dispose()

private const int MaxAttempts = 5;

public static async Task<string?> QueryKey(string key, bool redirectThrow = false)
public static async Task<string?> QueryKey(string key, bool redirectThrow = false, bool isBlob = false)
{
if (!(IsEnabled ?? false)) return null;
#if DEBUG
Expand All @@ -229,16 +235,24 @@ 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
);
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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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<byte> 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
}
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions CollapseLauncher/Classes/Interfaces/IGameSettingsUniversal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ public interface IGameSettingsExportable
RegistryKey? RegistryRoot { get; }

RegistryKey? RefreshRegistryRoot();
Task<Exception?> ImportSettings(string? gameBasePath = null);
Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null);
Task<Exception?> ImportSettings(string? gameBasePath = null, string? path = null);

Task<Exception?> ExportSettings(bool isCompressed = true, string? parentPathToImport = null,
string[]? relativePathToImport = null, string? path = null);

Task<Exception?> PushToDatabase();
Task<Exception?> GetFromDatabase();
}
}
Loading
Loading