Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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