diff --git a/CollapseLauncher/Assets/Images/GameIcon/icon-genshin.ico b/CollapseLauncher/Assets/Images/GameIcon/icon-genshin.ico new file mode 100644 index 000000000..4d435545d Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameIcon/icon-genshin.ico differ diff --git a/CollapseLauncher/Assets/Images/GameIcon/icon-honkai.ico b/CollapseLauncher/Assets/Images/GameIcon/icon-honkai.ico new file mode 100644 index 000000000..80c364355 Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameIcon/icon-honkai.ico differ diff --git a/CollapseLauncher/Assets/Images/GameIcon/icon-starrail.ico b/CollapseLauncher/Assets/Images/GameIcon/icon-starrail.ico new file mode 100644 index 000000000..b04def290 Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameIcon/icon-starrail.ico differ diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index 137cc3545..1e02f262b 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -125,7 +125,8 @@ private static List CopyReturn(IEnumerable Source) SubChannelID = GamePreset.SubChannelID, LauncherID = GamePreset.LauncherID, LauncherPluginURL = GamePreset.LauncherPluginURL, - GameDataTemplates = new Dictionary(GamePreset.GameDataTemplates) + GameDataTemplates = new Dictionary(GamePreset.GameDataTemplates), + ZoneSteamAssets = new Dictionary(GamePreset.ZoneSteamAssets), }; #endregion diff --git a/CollapseLauncher/Classes/ShortcutCreator/ShortcutCreator.cs b/CollapseLauncher/Classes/ShortcutCreator/ShortcutCreator.cs new file mode 100644 index 000000000..db35aa23b --- /dev/null +++ b/CollapseLauncher/Classes/ShortcutCreator/ShortcutCreator.cs @@ -0,0 +1,108 @@ +using Hi3Helper.Preset; +using Microsoft.Win32; +using System.IO; +using System.Linq; +using static Hi3Helper.Logger; +using static Hi3Helper.Shared.Region.LauncherConfig; + +namespace CollapseLauncher.ShortcutUtils +{ + public static class ShortcutCreator + { + public static void CreateShortcut(string path, PresetConfigV2 preset, bool play = false) + { + string shortcutName = string.Format("{0} ({1}) - Collapse Launcher.url", preset.GameName, preset.ZoneName).Replace(":", ""); + string url = string.Format("collapse://open -g \"{0}\" -r \"{1}\"", preset.GameName, preset.ZoneName); + + if (play) + url += " -p"; + + string icon = Path.Combine(Path.GetDirectoryName(AppExecutablePath), "Assets/Images/GameIcon/" + preset.GameType switch + { + GameType.StarRail => "icon-starrail.ico", + GameType.Genshin => "icon-genshin.ico", + _ => "icon-honkai.ico", + }); + + string fullPath = Path.Combine(path, shortcutName); + + using (StreamWriter writer = new StreamWriter(fullPath, false)) + { + writer.WriteLine(string.Format("[InternetShortcut]\nURL={0}\nIconIndex=0\nIconFile={1}", url, icon)); + } + } + + /// Heavily based on Heroic Games Launcher "Add to Steam" feature. + /// + /// Source: + /// https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher/blob/8bdee1383446d3b81e240a4300baaf337d48ec92/src/backend/shortcuts/nonesteamgame/nonesteamgame.ts + + public static bool AddToSteam(PresetConfigV2 preset, bool play) + { + var paths = GetShortcutsPath(); + + if (paths == null || paths.Length == 0) + return false; + + foreach (string path in paths) + { + SteamShortcutParser parser = new SteamShortcutParser(path); + + var splitPath = path.Split('\\'); + string userId = splitPath[splitPath.Length - 3]; + + parser.Insert(preset, play); + + parser.Save(); + LogWriteLine(string.Format("Added shortcut for {0} - {1} for Steam3ID {2} ", preset.GameName, preset.ZoneName, userId)); + } + + return true; + } + + public static bool IsAddedToSteam(PresetConfigV2 preset) + { + var paths = GetShortcutsPath(); + + if (paths == null || paths.Length == 0) + return false; + + foreach (string path in paths) + { + SteamShortcutParser parser = new SteamShortcutParser(path); + + if (!parser.Contains(new SteamShortcut(preset))) + return false; + } + + return true; + } + + private static string[] GetShortcutsPath() + { + RegistryKey reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\Valve\Steam", false); + + if (reg == null) + return null; + + string steamPath = (string)reg.GetValue("InstallPath", "C:\\Program Files (x86)\\Steam"); + + string steamUserData = steamPath + @"\userdata"; + + if (!Directory.Exists(steamUserData)) + return null; + + var res = Directory.GetDirectories(steamUserData) + .Where(x => + !(x.EndsWith("ac") || x.EndsWith("0") || x.EndsWith("anonymous")) + ).ToArray(); + + for (int i = 0; i < res.Length; i++) + { + res[i] = Path.Combine(res[i], @"config\shortcuts.vdf"); + } + + return res; + } + } +} diff --git a/CollapseLauncher/Classes/ShortcutCreator/SteamShortcut.cs b/CollapseLauncher/Classes/ShortcutCreator/SteamShortcut.cs new file mode 100644 index 000000000..5d44f6e05 --- /dev/null +++ b/CollapseLauncher/Classes/ShortcutCreator/SteamShortcut.cs @@ -0,0 +1,216 @@ +using Hi3Helper.Data; +using Hi3Helper.Preset; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using static Hi3Helper.Logger; +using static Hi3Helper.Shared.Region.LauncherConfig; + +namespace CollapseLauncher.ShortcutUtils +{ + public sealed class SteamShortcut + { + /// Based on CorporalQuesadilla's documentation on Steam Shortcuts. + /// + /// Source: + /// https://github.com/CorporalQuesadilla/Steam-Shortcut-Manager/wiki/Steam-Shortcuts-Documentation + + public string preliminaryAppID = ""; + + #region Shortcut fields + public string entryID = ""; + public string appid = ""; + public string AppName = ""; + public string Exe = ""; + public string StartDir = ""; + public string icon = ""; + public string ShortcutPath = ""; + public string LaunchOptions = ""; + public bool IsHidden = false; + public bool AllowDesktopConfig = false; + public bool AllowOverlay = false; + public bool OpenVR = false; + public bool Devkit = false; + public string DevkitGameID = ""; + public bool DevkitOverrideAppID = false; + public string LastPlayTime = "\x00\x00\x00"; + public string FlatpakAppID = ""; + public string tags = ""; + #endregion + + public SteamShortcut() { } + + public SteamShortcut(PresetConfigV2 preset, bool play = false) + { + AppName = string.Format("{0} - {1}", preset.GameName, preset.ZoneName); + Exe = AppExecutablePath; + var id = BitConverter.GetBytes(GenerateAppId(Exe, AppName)); + appid = SteamShortcutParser.ANSI.GetString(id, 0, id.Length); + + icon = Path.Combine(Path.GetDirectoryName(AppExecutablePath), "Assets/Images/GameIcon/" + preset.GameType switch + { + GameType.StarRail => "icon-starrail.ico", + GameType.Genshin => "icon-genshin.ico", + _ => "icon-honkai.ico", + }); + + preliminaryAppID = GeneratePreliminaryId(Exe, AppName).ToString(); + + StartDir = Path.GetDirectoryName(AppExecutablePath); + + LaunchOptions = string.Format("open -g \"{0}\" -r \"{1}\"", preset.GameName, preset.ZoneName); + if (play) + LaunchOptions += " -p"; + } + + private static char BoolToByte(bool b) => b ? '\x01' : '\x00'; + + public string ToEntry(int entryID = -1) + { + return '\x00' + (entryID >= 0 ? entryID.ToString() : this.entryID) + '\x00' + + '\x02' + "appid" + '\x00' + appid + + '\x01' + "AppName" + '\x00' + AppName + '\x00' + + '\x01' + "Exe" + '\x00' + Exe + '\x00' + + '\x01' + "StartDir" + '\x00' + StartDir + '\x00' + + '\x01' + "icon" + '\x00' + icon + '\x00' + + '\x01' + "ShortcutPath" + '\x00' + ShortcutPath + '\x00' + + '\x01' + "LaunchOptions" + '\x00' + LaunchOptions + '\x00' + + '\x02' + "IsHidden" + '\x00' + BoolToByte(IsHidden) + "\x00\x00\x00" + + '\x02' + "AllowDesktopConfig" + '\x00' + BoolToByte(AllowDesktopConfig) + "\x00\x00\x00" + + '\x02' + "AllowOverlay" + '\x00' + BoolToByte(AllowOverlay) + "\x00\x00\x00" + + '\x02' + "OpenVR" + '\x00' + BoolToByte(OpenVR) + "\x00\x00\x00" + + '\x02' + "Devkit" + '\x00' + BoolToByte(Devkit) + "\x00\x00\x00" + + '\x01' + "DevkitGameID" + '\x00' + DevkitGameID + '\x00' + + '\x02' + "DevkitOverrideAppID" + '\x00' + BoolToByte(DevkitOverrideAppID) + "\x00\x00\x00" + + '\x02' + "LastPlayTime" + '\x00' + LastPlayTime + '\x00' + + '\x01' + "FlatpakAppID" + '\x00' + FlatpakAppID + '\x00' + + '\x00' + "tags" + '\x00' + tags + "\x08\x08"; + } + + + private static uint GeneratePreliminaryId(string exe, string appname) + { + string key = exe + appname; + var crc32 = new System.IO.Hashing.Crc32(); + crc32.Append(SteamShortcutParser.ANSI.GetBytes(key)); + uint top = BitConverter.ToUInt32(crc32.GetCurrentHash()) | 0x80000000; + return (top << 32) | 0x02000000; + } + + public static uint GenerateAppId(string exe, string appname) + { + uint appId = GeneratePreliminaryId(exe, appname); + + return appId >> 32; + } + + private static uint GenerateGridId(string exe, string appname) + { + uint appId = GeneratePreliminaryId(exe, appname); + + return (appId >> 32) - 0x10000000; + } + + public void MoveImages(string path, PresetConfigV2 preset) + { + if (preset == null) return; + + path = Path.GetDirectoryName(path); + string gridPath = Path.Combine(path, "grid"); + if (!Directory.Exists(gridPath)) + Directory.CreateDirectory(gridPath); + + Dictionary assets = preset.ZoneSteamAssets; + + // Game background + GetImageFromUrl(gridPath, assets["Hero"], "_hero"); + + // Game logo + GetImageFromUrl(gridPath, assets["Logo"], "_logo"); + + // Vertical banner + // Shows when viewing all games of category or in the Home page + GetImageFromUrl(gridPath, assets["Banner"], "p"); + + // Horizontal banner + // Appears in Big Picture mode when the game is the most recently played + GetImageFromUrl(gridPath, assets["Preview"], ""); + } + + private static string MD5Hash(string path) + { + if (!File.Exists(path)) + return ""; + FileStream stream = File.OpenRead(path); + var hash = System.Security.Cryptography.MD5.Create().ComputeHash(stream); + stream.Close(); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLower(); + } + + private async void GetImageFromUrl(string gridPath, SteamGameProp asset, string steamSuffix) + { + string steamPath = Path.Combine(gridPath, preliminaryAppID + steamSuffix + ".png"); + + string hash = MD5Hash(steamPath); + + if (hash.ToLower() == asset.MD5) return; + + for (int i = 0; i < 3; i++) + { + FileInfo info = new FileInfo(steamPath); + await DownloadImage(info, asset.URL, new CancellationToken()); + + hash = MD5Hash(steamPath); + + if (hash.ToLower() == asset.MD5) return; + + File.Delete(steamPath); + + LogWriteLine(string.Format("Invalid checksum for file {0}! {1} does not match {2}.", steamPath, hash, asset.MD5), Hi3Helper.LogType.Error); + } + + LogWriteLine("After 3 tries, " + asset.URL + " could not be downloaded successfully.", Hi3Helper.LogType.Error); + return; + } + + private static async ValueTask DownloadImage(FileInfo fileInfo, string url, CancellationToken token) + { + byte[] buffer = ArrayPool.Shared.Rent(4 << 10); + try + { + // Try get the remote stream and download the file + using Stream netStream = await FallbackCDNUtil.GetHttpStreamFromResponse(url, token); + using Stream outStream = fileInfo.Open(new FileStreamOptions() + { + Access = FileAccess.Write, + Mode = FileMode.Create, + Share = FileShare.ReadWrite, + Options = FileOptions.Asynchronous + }); + + // Get the file length + long fileLength = netStream.Length; + + // Copy (and download) the remote streams to local + LogWriteLine($"Start downloading resource from: {url}", Hi3Helper.LogType.Default, true); + int read = 0; + while ((read = await netStream.ReadAsync(buffer, token)) > 0) + await outStream.WriteAsync(buffer, 0, read, token); + + LogWriteLine($"Downloading resource from: {url} has been completed and stored locally into:" + + $"\"{fileInfo.FullName}\" with size: {ConverterTool.SummarizeSizeSimple(fileLength)} ({fileLength} bytes)", Hi3Helper.LogType.Default, true); + } + catch (Exception ex) + { + ErrorSender.SendException(ex, ErrorType.Connection); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/CollapseLauncher/Classes/ShortcutCreator/SteamShortcutParser.cs b/CollapseLauncher/Classes/ShortcutCreator/SteamShortcutParser.cs new file mode 100644 index 000000000..c8027e818 --- /dev/null +++ b/CollapseLauncher/Classes/ShortcutCreator/SteamShortcutParser.cs @@ -0,0 +1,222 @@ +using Hi3Helper.Preset; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using static Hi3Helper.Logger; + +namespace CollapseLauncher.ShortcutUtils +{ + public sealed class SteamShortcutParser + { + public static Encoding ANSI; + private string _path; + private List _shortcuts = []; + + public SteamShortcutParser(string path) + { + _path = path; + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + ANSI = Encoding.GetEncoding(1252); + + Load(); + } + + public bool Contains(SteamShortcut shortcut) => _shortcuts.FindIndex(x => x.preliminaryAppID == shortcut.preliminaryAppID) != -1; + + public void Insert(PresetConfigV2 preset, bool play = false) + { + SteamShortcut shortcut = new SteamShortcut(preset, play); + + if (Contains(shortcut)) + { + _shortcuts.RemoveAll(x => x.preliminaryAppID == shortcut.preliminaryAppID); + } + + _shortcuts.Add(shortcut); + shortcut.MoveImages(_path, preset); + } + + private void Load() + { + _shortcuts.Clear(); + + if (!File.Exists(_path)) + return; + + var contents = File.ReadAllText(_path, ANSI); + contents = string.Concat(contents.Skip(11)); + + foreach (string line in contents.Split("\x08\x08")) + { + if (line == "") continue; + SteamShortcut steamShortcut = ParseShortcut(line + '\x08'); + if (steamShortcut == null) continue; + _shortcuts.Add(steamShortcut); + } + } + + public void Save() + { + if (File.Exists(_path)) + File.Move(_path, _path + "_old", true); + + FileStream fs = File.OpenWrite(_path); + StreamWriter sw = new StreamWriter(fs, ANSI); + + sw.Write("\x00shortcuts\x00"); + + int entryCount = 0; + foreach (SteamShortcut st in _shortcuts) + { + sw.Write(st.ToEntry(entryCount)); + entryCount++; + } + sw.Write("\x08\x08"); + sw.Close(); + fs.Close(); + } + + #region Individual Shortcut Parser + public SteamShortcut ParseShortcut(string line) + { + byte[] ln = ANSI.GetBytes(line); + SteamShortcut newShortcut = new SteamShortcut(); + + List strRes = new List(); + List boolRes = new List(); + + List buffer = []; + ParseType parse = ParseType.NameStr; + for (int i = 0; i < ln.Length; i++) + { + if (ln[0] != 0) + return null; + switch (parse) + { + case ParseType.FindType: + if (ln[i] == 0) + parse = ParseType.NameTags; + if (ln[i] == 1) + parse = ParseType.NameStr; + if (ln[i] == 2) + parse = ParseType.NameBool; + continue; + case ParseType.NameStr: + case ParseType.NameBool: + if (ln[i] == 0) + { + parse = parse == ParseType.NameStr ? ParseType.ValueStr : ParseType.ValueBool; + string key = ANSI.GetString(buffer.ToArray(), 0, buffer.Count); + if (key == "LastPlayTime") + parse = ParseType.ValueTime; + if (key == "appid") + parse = ParseType.ValueAppid; + buffer = []; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.ValueTime: + if (ln[i] == 0) + { + newShortcut.LastPlayTime = buffer.Count != 0 ? ANSI.GetString(buffer.ToArray(), 0, buffer.Count) : "\x00\x00\x00"; + buffer = []; + parse = ParseType.FindType; + while (i < ln.Length - 1 && ln[i + 1] == 0) + i++; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.ValueAppid: + if (ln[i] == 1) + { + newShortcut.appid = ANSI.GetString(buffer.ToArray(), 0, buffer.Count); + buffer = []; + parse = ParseType.NameStr; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.NameTags: + if (ln[i] == 0) + { + parse = ParseType.ValueTags; + buffer = []; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.ValueTags: + if (ln[i] == 8) + { + newShortcut.tags = ANSI.GetString(buffer.ToArray(), 0, buffer.Count); + buffer = []; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.ValueStr: + if (ln[i] == 0) + { + strRes.Add(ANSI.GetString(buffer.ToArray(), 0, buffer.Count)); + buffer = []; + parse = ParseType.FindType; + continue; + } + buffer.Add(ln[i]); + break; + case ParseType.ValueBool: + boolRes.Add(ln[i] == 1); + if (ln[i + 3] == 0) + i += 3; + parse = ParseType.FindType; + break; + } + } + + if (strRes.Count != 9 || boolRes.Count != 6) + { + LogWriteLine("Invalid shortcut! Skipping...", Hi3Helper.LogType.Error); + return null; + } + + newShortcut.entryID = strRes[0]; + newShortcut.AppName = strRes[1]; + newShortcut.Exe = strRes[2]; + newShortcut.StartDir = strRes[3]; + newShortcut.icon = strRes[4]; + newShortcut.ShortcutPath = strRes[5]; + newShortcut.LaunchOptions = strRes[6]; + newShortcut.DevkitGameID = strRes[7]; + newShortcut.FlatpakAppID = strRes[8]; + + newShortcut.IsHidden = boolRes[0]; + newShortcut.AllowDesktopConfig = boolRes[1]; + newShortcut.AllowOverlay = boolRes[2]; + newShortcut.OpenVR = boolRes[3]; + newShortcut.Devkit = boolRes[4]; + newShortcut.DevkitOverrideAppID = boolRes[5]; + + newShortcut.preliminaryAppID = SteamShortcut.GenerateAppId(newShortcut.Exe, newShortcut.AppName).ToString(); + + return newShortcut; + } + + private enum ParseType + { + FindType, + NameStr, + ValueStr, + ValueAppid, + NameBool, + ValueBool, + ValueTime, + NameTags, + ValueTags + } + #endregion + } +} diff --git a/CollapseLauncher/CollapseLauncher.csproj b/CollapseLauncher/CollapseLauncher.csproj index 475011a61..07d68f810 100644 --- a/CollapseLauncher/CollapseLauncher.csproj +++ b/CollapseLauncher/CollapseLauncher.csproj @@ -100,6 +100,7 @@ + diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs b/CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs index 9de94cb17..d4593751e 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/Dialogs/SimpleDialogs.cs @@ -698,6 +698,121 @@ async void CopyTextToClipboard(object sender, RoutedEventArgs e) } } + #region Shortcut Creator Dialogs + public static async Task> Dialog_ShortcutCreationConfirm(UIElement Content, string path) + { + StackPanel panel = new StackPanel { Orientation = Orientation.Vertical, MaxWidth = 500 }; + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.ShortcutCreationConfirmSubtitle1, Margin = new Thickness(0, 2, 0, 4), HorizontalAlignment = HorizontalAlignment.Center }); + TextBlock pathText = new TextBlock { HorizontalAlignment = HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 4) }; + pathText.Inlines.Add(new Run() { Text = path, FontWeight = FontWeights.Bold }); + panel.Children.Add(pathText); + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.ShortcutCreationConfirmSubtitle2, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 4), HorizontalAlignment = HorizontalAlignment.Center }); + + CheckBox playOnLoad = new CheckBox() { + Content = new TextBlock { Text = Lang._Dialogs.ShortcutCreationConfirmCheckBox, TextWrapping = TextWrapping.Wrap }, + Margin = new Thickness(0, 4, 0, -8), + HorizontalAlignment = HorizontalAlignment.Center + }; + panel.Children.Add(playOnLoad); + + ContentDialogResult result = await SpawnDialog( + Lang._Dialogs.ShortcutCreationConfirmTitle, + panel, + Content, + Lang._Misc.Cancel, + Lang._Misc.YesContinue, + dialogTheme: ContentDialogTheme.Warning + ); + + return new Tuple(result, playOnLoad.IsChecked ?? false); + } + + public static async Task Dialog_ShortcutCreationSuccess(UIElement Content, string path, bool play = false) + { + StackPanel panel = new StackPanel { Orientation = Orientation.Vertical, MaxWidth = 500 }; + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.ShortcutCreationSuccessSubtitle1, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 2, 0, 4) }); + TextBlock pathText = new TextBlock { HorizontalAlignment = HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 4) }; + pathText.Inlines.Add(new Run() { Text = Lang._Dialogs.ShortcutCreationSuccessSubtitle2 }); + pathText.Inlines.Add(new Run() { Text = path, FontWeight = FontWeights.Bold }); + panel.Children.Add(pathText); + + if (play) + { + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.ShortcutCreationSuccessSubtitle3, FontWeight = FontWeights.Bold, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 8, 0, 4) }); + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.ShortcutCreationSuccessSubtitle4, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 2) }); + } + + return await SpawnDialog( + Lang._Dialogs.ShortcutCreationSuccessTitle, + panel, + Content, + Lang._Misc.Close, + dialogTheme: ContentDialogTheme.Success + ); + } + + public static async Task> Dialog_SteamShortcutCreationConfirm(UIElement Content) + { + StackPanel panel = new StackPanel { Orientation = Orientation.Vertical, MaxWidth = 500 }; + + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationConfirmSubtitle1, HorizontalAlignment = HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 2) }); + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationConfirmSubtitle2, HorizontalAlignment = HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 4) }); + + CheckBox playOnLoad = new CheckBox() + { + Content = new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationConfirmCheckBox, TextWrapping = TextWrapping.Wrap }, + Margin = new Thickness(0, 4, 0, -8), + HorizontalAlignment = HorizontalAlignment.Center + }; + panel.Children.Add(playOnLoad); + + ContentDialogResult result = await SpawnDialog( + Lang._Dialogs.SteamShortcutCreationConfirmTitle, + panel, + Content, + Lang._Misc.Cancel, + Lang._Misc.YesContinue, + dialogTheme: ContentDialogTheme.Warning + ); + + return new Tuple(result, playOnLoad.IsChecked ?? false); + } + + public static async Task Dialog_SteamShortcutCreationSuccess(UIElement Content, bool play = false) + { + StackPanel panel = new StackPanel { Orientation = Orientation.Vertical, MaxWidth = 500 }; + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationSuccessSubtitle1, HorizontalAlignment = HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 4) }); + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationSuccessSubtitle2, FontWeight = FontWeights.Bold, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 8, 0, 4) }); + if (play) + { + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationSuccessSubtitle3, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 2) }); + } + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationSuccessSubtitle4, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 2) }); + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationSuccessSubtitle5, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 4) }); + + return await SpawnDialog( + Lang._Dialogs.SteamShortcutCreationSuccessTitle, + panel, + Content, + Lang._Misc.Close, + dialogTheme: ContentDialogTheme.Success + ); + } + + public static async Task Dialog_SteamShortcutCreationFailure(UIElement Content) + { + StackPanel panel = new StackPanel { Orientation = Orientation.Vertical, MaxWidth = 350 }; + panel.Children.Add(new TextBlock { Text = Lang._Dialogs.SteamShortcutCreationFailureSubtitle, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 4) }); + return await SpawnDialog( + Lang._Dialogs.SteamShortcutCreationFailureTitle, + panel, + Content, + Lang._Misc.Close, + dialogTheme: ContentDialogTheme.Error + ); + } + #endregion + public static async Task SpawnDialog( string title, object content, UIElement Content, string closeText = null, string primaryText = null, diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml index 95ff4fcb3..e4cb2790e 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml +++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml @@ -640,7 +640,8 @@ CornerRadius="20" PointerEntered="AnimateGameRegSettingIcon_Start" PointerExited="AnimateGameRegSettingIcon_End" - Shadow="{ThemeResource SharedShadow}"> + Shadow="{ThemeResource SharedShadow}" + Margin="0,0,8,0"> @@ -736,6 +737,22 @@ + + diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs index 2e8d399b5..cb77bcd8d 100644 --- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs +++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs @@ -1,7 +1,8 @@ using CollapseLauncher.Dialogs; +using CollapseLauncher.FileDialogCOM; using CollapseLauncher.Interfaces; using CollapseLauncher.Statics; -using CollapseLauncher.FileDialogCOM; +using CollapseLauncher.ShortcutUtils; using Hi3Helper; using Hi3Helper.Preset; using Hi3Helper.Screen; @@ -2028,5 +2029,45 @@ private void GenshinHDREnforcer() } } #endregion + + #region Shortcut Creation + private async void AddToSteamButton_Click(object sender, RoutedEventArgs e) + { + Tuple result = await Dialog_SteamShortcutCreationConfirm(this); + + if (result.Item1 != ContentDialogResult.Primary) + return; + + if (ShortcutCreator.AddToSteam(GamePropertyVault.GetCurrentGameProperty()._GamePreset, result.Item2)) + { + await Dialog_SteamShortcutCreationSuccess(this, result.Item2); + return; + } + + await Dialog_SteamShortcutCreationFailure(this); + } + + private async void ShortcutButton_Click(object sender, RoutedEventArgs e) + { + string folder = await FileDialogNative.GetFolderPicker(Lang._HomePage.CreateShortcut_FolderPicker); + + if (string.IsNullOrEmpty(folder)) + return; + + if (!IsUserHasPermission(folder)) + { + await Dialog_InsufficientWritePermission(sender as UIElement, folder); + return; + } + + Tuple result = await Dialog_ShortcutCreationConfirm(this, folder); + + if (result.Item1 != ContentDialogResult.Primary) + return; + + ShortcutCreator.CreateShortcut(folder, GamePropertyVault.GetCurrentGameProperty()._GamePreset, result.Item2); + await Dialog_ShortcutCreationSuccess(this, folder, result.Item2); + } + #endregion } } diff --git a/CollapseLauncher/packages.lock.json b/CollapseLauncher/packages.lock.json index f2df10093..5109021ff 100644 --- a/CollapseLauncher/packages.lock.json +++ b/CollapseLauncher/packages.lock.json @@ -214,6 +214,12 @@ "resolved": "6.0.0", "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==" }, + "System.Text.Encoding.CodePages": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==" + }, "System.Text.Json": { "type": "Direct", "requested": "[8.0.1, )", @@ -1384,6 +1390,12 @@ "resolved": "6.0.0", "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==" }, + "System.Text.Encoding.CodePages": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==" + }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", diff --git a/Hi3Helper.Core/Classes/Preset/Classes/PresetConfigV2.cs b/Hi3Helper.Core/Classes/Preset/Classes/PresetConfigV2.cs index 961eed3b3..50fc88cde 100644 --- a/Hi3Helper.Core/Classes/Preset/Classes/PresetConfigV2.cs +++ b/Hi3Helper.Core/Classes/Preset/Classes/PresetConfigV2.cs @@ -505,6 +505,7 @@ private bool CheckInnerGameConfig(in string GamePath) public string? ZoneURL { get; set; } public string? ZoneLogoURL { get; set; } public string? ZonePosterURL { get; set; } + #nullable disable public string InstallRegistryLocation { get => string.Format(PrefixRegInstallLocation, InternalGameNameInConfig); } public string DefaultGameLocation { get => string.Format(PrefixDefaultProgramFiles, InternalGameNameFolder, SystemDriveLetter); } @@ -557,6 +558,13 @@ private bool CheckInnerGameConfig(in string GamePath) public string? InternalGameNameFolder { get; set; } public string? InternalGameNameInConfig { get; set; } public Dictionary? GameDataTemplates { get; set; } + public Dictionary? ZoneSteamAssets { get; set; } = new Dictionary(); + } + + public class SteamGameProp + { + public string? URL { get; set; } + public string? MD5 { get; set; } } public class GameDataTemplate diff --git a/Hi3Helper.Core/Lang/Locale/LangDialogs.cs b/Hi3Helper.Core/Lang/Locale/LangDialogs.cs index ab14af26b..a691fcfd9 100644 --- a/Hi3Helper.Core/Lang/Locale/LangDialogs.cs +++ b/Hi3Helper.Core/Lang/Locale/LangDialogs.cs @@ -106,6 +106,27 @@ public sealed class LangDialogs public string MeteredConnectionWarningSubtitle { get; set; } = LangFallback?._Dialogs.MeteredConnectionWarningSubtitle; public string ResetKbShortcutsTitle { get; set; } = LangFallback?._Dialogs.ResetKbShortcutsTitle; public string ResetKbShortcutsSubtitle { get; set; } = LangFallback?._Dialogs.ResetKbShortcutsSubtitle; + public string ShortcutCreationConfirmTitle { get; set; } = LangFallback?._Dialogs.ShortcutCreationConfirmTitle; + public string ShortcutCreationConfirmSubtitle1 { get; set; } = LangFallback?._Dialogs.ShortcutCreationConfirmSubtitle1; + public string ShortcutCreationConfirmSubtitle2 { get; set; } = LangFallback?._Dialogs.ShortcutCreationConfirmSubtitle2; + public string ShortcutCreationConfirmCheckBox { get; set; } = LangFallback?._Dialogs.ShortcutCreationConfirmCheckBox; + public string ShortcutCreationSuccessTitle { get; set; } = LangFallback?._Dialogs.ShortcutCreationSuccessTitle; + public string ShortcutCreationSuccessSubtitle1 { get; set; } = LangFallback?._Dialogs.ShortcutCreationSuccessSubtitle1; + public string ShortcutCreationSuccessSubtitle2 { get; set; } = LangFallback?._Dialogs.ShortcutCreationSuccessSubtitle2; + public string ShortcutCreationSuccessSubtitle3 { get; set; } = LangFallback?._Dialogs.ShortcutCreationSuccessSubtitle3; + public string ShortcutCreationSuccessSubtitle4 { get; set; } = LangFallback?._Dialogs.ShortcutCreationSuccessSubtitle4; + public string SteamShortcutCreationConfirmTitle { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationConfirmTitle; + public string SteamShortcutCreationConfirmSubtitle1 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationConfirmSubtitle1; + public string SteamShortcutCreationConfirmSubtitle2 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationConfirmSubtitle2; + public string SteamShortcutCreationConfirmCheckBox { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationConfirmCheckBox; + public string SteamShortcutCreationSuccessTitle { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessTitle; + public string SteamShortcutCreationSuccessSubtitle1 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessSubtitle1; + public string SteamShortcutCreationSuccessSubtitle2 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessSubtitle2; + public string SteamShortcutCreationSuccessSubtitle3 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessSubtitle3; + public string SteamShortcutCreationSuccessSubtitle4 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessSubtitle4; + public string SteamShortcutCreationSuccessSubtitle5 { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationSuccessSubtitle5; + public string SteamShortcutCreationFailureTitle { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationFailureTitle; + public string SteamShortcutCreationFailureSubtitle { get; set; } = LangFallback?._Dialogs.SteamShortcutCreationFailureSubtitle; public string OperationErrorDiskSpaceInsufficientTitle { get; set; } = LangFallback?._Dialogs.OperationErrorDiskSpaceInsufficientTitle; public string OperationErrorDiskSpaceInsufficientMsg { get; set; } = LangFallback?._Dialogs.OperationErrorDiskSpaceInsufficientMsg; public string OperationWarningNotCancellableTitle { get; set; } = LangFallback?._Dialogs.OperationWarningNotCancellableTitle; diff --git a/Hi3Helper.Core/Lang/Locale/LangHomePage.cs b/Hi3Helper.Core/Lang/Locale/LangHomePage.cs index d097ab376..4a0c6f7b4 100644 --- a/Hi3Helper.Core/Lang/Locale/LangHomePage.cs +++ b/Hi3Helper.Core/Lang/Locale/LangHomePage.cs @@ -47,6 +47,9 @@ public sealed class LangHomePage public string GameSettings_Panel4 { get; set; } = LangFallback?._HomePage.GameSettings_Panel4; public string GameSettings_Panel4ShowEventsPanel { get; set; } = LangFallback?._HomePage.GameSettings_Panel4ShowEventsPanel; public string GameSettings_Panel4ShowSocialMediaPanel { get; set; } = LangFallback?._HomePage.GameSettings_Panel4ShowSocialMediaPanel; + public string GameSettings_Panel4CreateShortcutBtn { get; set; } = LangFallback?._HomePage.GameSettings_Panel4CreateShortcutBtn; + public string GameSettings_Panel4AddToSteamBtn { get; set; } = LangFallback?._HomePage.GameSettings_Panel4AddToSteamBtn; + public string CreateShortcut_FolderPicker { get; set; } = LangFallback?._HomePage.CreateShortcut_FolderPicker; public string GamePlaytime_Panel1 { get; set; } = LangFallback?._HomePage.GamePlaytime_Panel1; public string GamePlaytime_Idle_Panel1Hours { get; set; } = LangFallback?._HomePage.GamePlaytime_Idle_Panel1Hours; public string GamePlaytime_Idle_Panel1Minutes { get; set; } = LangFallback?._HomePage.GamePlaytime_Idle_Panel1Minutes; diff --git a/Hi3Helper.Core/Lang/Locale/LangMisc.cs b/Hi3Helper.Core/Lang/Locale/LangMisc.cs index da6e1417f..57e414b20 100644 --- a/Hi3Helper.Core/Lang/Locale/LangMisc.cs +++ b/Hi3Helper.Core/Lang/Locale/LangMisc.cs @@ -43,6 +43,7 @@ public sealed class LangMisc public string YesImReallySure { get; set; } = LangFallback?._Misc.YesImReallySure; public string YesIHaveBeefyPC { get; set; } = LangFallback?._Misc.YesIHaveBeefyPC; public string YesChangeLocation { get; set; } = LangFallback?._Misc.YesChangeLocation; + public string YesContinue { get; set; } = LangFallback?._Misc.YesContinue; public string No { get; set; } = LangFallback?._Misc.No; public string NoStartFromBeginning { get; set; } = LangFallback?._Misc.NoStartFromBeginning; public string NoCancel { get; set; } = LangFallback?._Misc.NoCancel; diff --git a/Hi3Helper.Core/Lang/en_US.json b/Hi3Helper.Core/Lang/en_US.json index 5cc097c9c..a20ebcdd9 100644 --- a/Hi3Helper.Core/Lang/en_US.json +++ b/Hi3Helper.Core/Lang/en_US.json @@ -136,6 +136,8 @@ "GameSettings_Panel4": "Miscellaneous", "GameSettings_Panel4ShowEventsPanel": "Show Events Panel", "GameSettings_Panel4ShowSocialMediaPanel": "Show Social Media Panel", + "GameSettings_Panel4CreateShortcutBtn": "Create shortcut", + "GameSettings_Panel4AddToSteamBtn": "Add to Steam", "GamePlaytime_Panel1": "Edit Playtime", "GamePlaytime_Idle_Panel1Hours": "Hours", "GamePlaytime_Idle_Panel1Minutes": "Minutes", @@ -152,7 +154,9 @@ "CommunityToolsBtn": "Community Tools", "CommunityToolsBtn_OfficialText": "Official Tools", "CommunityToolsBtn_CommunityText": "Community Tools", - "CommunityToolsBtn_OpenExecutableAppDialogTitle": "Select Program Executable: {0}" + "CommunityToolsBtn_OpenExecutableAppDialogTitle": "Select Program Executable: {0}", + + "CreateShortcut_FolderPicker": "Select where to place the shortcut" }, "_GameRepairPage": { @@ -468,6 +472,7 @@ "YesImReallySure": "Yes, I'm really sure!", "YesIHaveBeefyPC": "Yes, I have a beefy PC! ᕦ(ò_ó)ᕤ", "YesChangeLocation": "Yes, Change location", + "YesContinue": "Yes, Continue", "No": "No", "NoStartFromBeginning": "No, Start Over", @@ -660,7 +665,31 @@ "OperationWarningNotCancellableMsg2": "CANNOT CANCEL THIS PROCESS", "OperationWarningNotCancellableMsg3": " while it's running!\r\n\r\nClick \"", "OperationWarningNotCancellableMsg4": "\" to start the process or \"", - "OperationWarningNotCancellableMsg5": "\" to cancel the operation." + "OperationWarningNotCancellableMsg5": "\" to cancel the operation.", + + "ShortcutCreationConfirmTitle": "Are you sure you want to continue?", + "ShortcutCreationConfirmSubtitle1": "A shortcut will be created in the following path:", + "ShortcutCreationConfirmSubtitle2": "If there is already a shortcut with the same name in this folder, it will be replaced.", + "ShortcutCreationConfirmCheckBox": "Automatically start game after using this shortcut", + "ShortcutCreationSuccessTitle": "Success!", + "ShortcutCreationSuccessSubtitle1": "A shiny new shortcut was created!", + "ShortcutCreationSuccessSubtitle2": "Location: ", + "ShortcutCreationSuccessSubtitle3": "Notes:", + "ShortcutCreationSuccessSubtitle4": " • Using this shortcut will start the game after loading the region.", + + "SteamShortcutCreationConfirmTitle": "Are you sure you want to continue?", + "SteamShortcutCreationConfirmSubtitle1": "A shortcut will be added to every Steam profile in this computer.", + "SteamShortcutCreationConfirmSubtitle2": "If already added, the assets related to the shortcut will be verified.", + "SteamShortcutCreationConfirmCheckBox": "Automatically start game after using this shortcut", + "SteamShortcutCreationSuccessTitle": "Success!", + "SteamShortcutCreationSuccessSubtitle1": "A new shiny shortcut was added to every Steam profile in this computer!", + "SteamShortcutCreationSuccessSubtitle2": "Notes:", + "SteamShortcutCreationSuccessSubtitle3": " • Using this shortcut will start the game after loading the region.", + "SteamShortcutCreationSuccessSubtitle4": " • Running this process again will fix any corrupted/missing images belonging to the shortcut.", + "SteamShortcutCreationSuccessSubtitle5": " • New shortcuts will only be shown after Steam is reloaded.", + + "SteamShortcutCreationFailureTitle": "Invalid Steam data folder", + "SteamShortcutCreationFailureSubtitle": "It was not possible to find a valid userdata folder.\n\nPlease be sure to login at least once to the Steam client before trying to use this functionality." }, "_FileMigrationProcess": {