Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Add to Steam" and creation of shortcuts #378

Merged
merged 72 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
b6fb2ec
Add parsing for a play option
gablm Jan 8, 2024
3843d35
launching arguments for regions
gablm Jan 8, 2024
ab718cc
No redirection way
gablm Jan 8, 2024
c2efc3f
Handle redirection
gablm Jan 8, 2024
834c66a
Update MainPage.xaml.cs
gablm Jan 8, 2024
f1db3b5
Change the check for starting the game
gablm Jan 8, 2024
6360a97
Fix out of bounds exception
gablm Jan 8, 2024
6dde8ef
Separate activation handling from mainPage
gablm Jan 8, 2024
3b8a6f7
Remove unused "using" statements && partial keyword
gablm Jan 8, 2024
a697a99
Fix arguments not being cleared before parsing them on activation
gablm Jan 8, 2024
9725ef6
Alterations based on code review + Fix parsing of arguments between q…
gablm Jan 9, 2024
87ef616
Help message changes
gablm Jan 9, 2024
43059bc
Basic UI
gablm Jan 9, 2024
00ba023
Add icons + new class
gablm Jan 9, 2024
06587a0
Start of steam support
gablm Jan 9, 2024
1645e60
Shortcut struct
gablm Jan 9, 2024
49915fd
Update ShortcutCreator.cs
gablm Jan 9, 2024
13fa9e9
Why does valve need a proprietary data format :sku
gablm Jan 10, 2024
d86554c
Update ShortcutCreator.cs
gablm Jan 10, 2024
e48ebc4
The parser is real!
gablm Jan 11, 2024
a9698ea
Update ShortcutCreator.cs
gablm Jan 11, 2024
a6a7dfe
It works now
gablm Jan 11, 2024
b05bf4c
Actually fully works now
gablm Jan 11, 2024
cb77d7d
Add to steam
gablm Jan 11, 2024
118e4f8
Steam id generation
gablm Jan 11, 2024
16fc4d1
Update ShortcutCreator.cs
gablm Jan 11, 2024
5091783
Add Game Logos
gablm Jan 11, 2024
68e57d7
Merge branch 'main' into shortcuts
gablm Jan 11, 2024
c0fac29
Add other ids
gablm Jan 11, 2024
6cd3e23
Shortcut assets + fix appid generation
gablm Jan 11, 2024
0e51c91
Add banners to the shortcut creation
gablm Jan 11, 2024
2ee1ede
Change usage of GamePresetProperty to PresetConfigV2
gablm Jan 11, 2024
c838d41
Separate code into two classes
gablm Jan 12, 2024
219fb30
Merge branch 'CollapseLauncher:main' into shortcuts
gablm Jan 12, 2024
4c72f95
Desktop shortcut using protocols
gablm Jan 12, 2024
1ec5728
URL Protocol creation and parsing
gablm Jan 12, 2024
a6c7890
Merge pull request #11 from gablm/url-prot
gablm Jan 12, 2024
7bf6c62
Fix empty protocol and broken non-public commands
gablm Jan 12, 2024
33a5baf
Merge branch 'url-prot' into shortcuts
gablm Jan 12, 2024
2cde4af
Fix creation of shortcuts
gablm Jan 13, 2024
546dec1
Limit commands allowed using protocols
gablm Jan 13, 2024
f6b168a
Better way to save the allowed commands
gablm Jan 13, 2024
06bc220
Logging protocol activation
gablm Jan 13, 2024
c0a32a6
Reorder imports and renaming variables (Code Review)
gablm Jan 13, 2024
f678036
Merge branch 'url-prot' into shortcuts
gablm Jan 13, 2024
9c1d7db
Streamline asset copy
gablm Jan 13, 2024
421e402
Remove duplicated icons
gablm Jan 13, 2024
00693db
Experiment downloading logos from metadata
gablm Jan 14, 2024
4aabf95
try at getting username from steamid3
gablm Jan 15, 2024
143d76c
Improve logging
gablm Jan 15, 2024
726d202
Remove assets in favour of download via metadata repo
gablm Jan 15, 2024
e07bce8
Fix names
gablm Jan 15, 2024
b657c96
Update SteamShortcutParser.cs
gablm Jan 15, 2024
2e919b2
Remove old copy file section
gablm Jan 15, 2024
717f3e3
Remove creation of prop file, for now
gablm Jan 15, 2024
af8cb5c
Reuse downloader from MainPage
gablm Jan 15, 2024
d21b15e
Asset download + verification
gablm Jan 16, 2024
9fb6280
UI changes
gablm Jan 16, 2024
a373d15
ui changes
gablm Jan 16, 2024
eda43ab
Update HomePage.xaml.cs
gablm Jan 16, 2024
af992bb
Dialogs for creating .url file
gablm Jan 17, 2024
0042e18
Fix dialogs + Always allow adding to Steam
gablm Jan 17, 2024
9824d3e
UI tweaks
gablm Jan 17, 2024
e6c0b36
Change SHA1 in favour or md5
gablm Jan 17, 2024
e38f8c9
Merge branch 'main' into shortcuts
gablm Jan 17, 2024
719e541
Localization support
gablm Jan 17, 2024
9e21a70
Merge branch 'main' into shortcuts
gablm Jan 17, 2024
018d4ca
Verify is folder exists + new failure dialog
gablm Jan 18, 2024
acf56c9
Fix typo in namespace name
gablm Jan 18, 2024
9c65230
Change metadata storage format
gablm Jan 20, 2024
23320a8
Merge branch 'main' into shortcuts
gablm Jan 21, 2024
3b7fc54
Revert changes in the CDNList
gablm Jan 21, 2024
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 2 additions & 1 deletion CollapseLauncher/Classes/GamePropertyVault.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ private static List<T> CopyReturn<T>(IEnumerable<T> Source)
SubChannelID = GamePreset.SubChannelID,
LauncherID = GamePreset.LauncherID,
LauncherPluginURL = GamePreset.LauncherPluginURL,
GameDataTemplates = new Dictionary<string, GameDataTemplate>(GamePreset.GameDataTemplates)
GameDataTemplates = new Dictionary<string, GameDataTemplate>(GamePreset.GameDataTemplates),
ZoneSteamAssets = new Dictionary<string, SteamGameProp>(GamePreset.ZoneSteamAssets),
};
#endregion

Expand Down
108 changes: 108 additions & 0 deletions CollapseLauncher/Classes/ShortcutCreator/ShortcutCreator.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
216 changes: 216 additions & 0 deletions CollapseLauncher/Classes/ShortcutCreator/SteamShortcut.cs
Original file line number Diff line number Diff line change
@@ -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<string, SteamGameProp> 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<byte>.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<byte>.Shared.Return(buffer);
}
}
}
}
Loading
Loading