-
-
Notifications
You must be signed in to change notification settings - Fork 87
/
Copy pathMainWindowViewModel.cs
377 lines (330 loc) · 15.7 KB
/
MainWindowViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
using System.IO.Compression;
using System.Net.Http;
using Windows.Win32.Security;
using Windows.Win32.System.Threading;
using Microsoft.Win32;
using Reloaded.Mod.Installer.Lib.Utilities;
using static Windows.Win32.PInvoke;
using static Reloaded.Mod.Installer.DependencyInstaller.DependencyInstaller;
using Path = System.IO.Path;
namespace Reloaded.Mod.Installer.Lib;
public class MainWindowViewModel : ObservableObject
{
/// <summary>
/// The current step of the download process.
/// </summary>
public int CurrentStepNo { get; set; }
/// <summary>
/// Current production step.
/// </summary>
public string CurrentStepDescription { get; set; } = "";
/// <summary>
/// The current setup progress.
/// Range 0 - 100.0f.
/// </summary>
public double Progress { get; set; }
/// <summary>
/// A cancellation token allowing you to cancel the download operation.
/// </summary>
public CancellationTokenSource CancellationToken { get; } = new();
public async Task InstallReloadedAsync(Settings settings)
{
// Note: All of this code is terrible, I don't have the time to make it good.
// ReSharper disable InconsistentNaming
const uint MB_OK = 0x0;
const uint MB_ICONINFORMATION = 0x40;
// ReSharper restore InconsistentNaming
// Handle Proton specific customizations.
var protonTricksSuffix = GetProtontricksSuffix();
var isProton = !string.IsNullOrEmpty(protonTricksSuffix);
OverrideInstallLocationForProton(settings, protonTricksSuffix, out var nativeInstallFolder, out var userName);
// Check for existing installation
if (Directory.Exists(settings.InstallLocation) && Directory.GetFiles(settings.InstallLocation).Length > 0)
{
Native.MessageBox(IntPtr.Zero, $"An existing installation has been detected at:\n{settings.InstallLocation}\n\n" +
$"To prevent data loss, installation will be aborted.\n" +
$"If you wish to reinstall, delete or move the existing installation.", "Existing Installation", MB_OK | MB_ICONINFORMATION);
return;
}
// Step
Directory.CreateDirectory(settings.InstallLocation);
using var tempDownloadDir = new TemporaryFolderAllocation();
var progressSlicer = new ProgressSlicer(new Progress<double>(d =>
{
Progress = d * 100.0;
}));
try
{
var downloadLocation = Path.Combine(tempDownloadDir.FolderPath, $"Reloaded-II.zip");
// 0.15
CurrentStepNo = 0;
await DownloadReloadedAsync(downloadLocation, progressSlicer.Slice(0.15));
if (CancellationToken.IsCancellationRequested)
throw new TaskCanceledException();
// 0.20
CurrentStepNo = 1;
ExtractReloaded(settings.InstallLocation, downloadLocation, progressSlicer.Slice(0.05));
if (CancellationToken.IsCancellationRequested)
throw new TaskCanceledException();
// 1.00
CurrentStepNo = 2;
await CheckAndInstallMissingRuntimesAsync(settings.InstallLocation, tempDownloadDir.FolderPath,
progressSlicer.Slice(0.8),
s => { CurrentStepDescription = s; }, CancellationToken.Token);
var executableName = IntPtr.Size == 8 ? "Reloaded-II.exe" : "Reloaded-II32.exe";
var executablePath = Path.Combine(settings.InstallLocation, executableName);
var nativeExecutablePath = Path.Combine(nativeInstallFolder, executableName);
var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "Reloaded-II.lnk");
// For Proton/Wine, create a shortcut up one folder above install location.
if (!string.IsNullOrEmpty(protonTricksSuffix))
shortcutPath = Path.Combine(Path.GetDirectoryName(settings.InstallLocation)!, "Reloaded-II - " + protonTricksSuffix + ".lnk");
if (settings.CreateShortcut)
{
CurrentStepDescription = "Creating Shortcut";
if (!isProton)
NativeShellLink.MakeShortcut(shortcutPath, executablePath);
else
MakeProtonShortcut(userName, protonTricksSuffix, shortcutPath, nativeExecutablePath);
}
CurrentStepDescription = "All Set";
// On WINE, overwrite environment variables that may be inherited
// from host permanently.
if (WineDetector.IsWine())
{
ShowDotFilesInWine();
SetEnvironmentVariable("DOTNET_ROOT", "%ProgramFiles%\\dotnet");
SetEnvironmentVariable("DOTNET_BUNDLE_EXTRACT_BASE_DIR", "%TEMP%\\.net");
}
if (!settings.HideNonErrorGuiMessages && isProton)
Native.MessageBox(IntPtr.Zero, $"Reloaded was installed via Proton to your Desktop.\nYou can find it at: {nativeInstallFolder}", "Installation Complete", MB_OK | MB_ICONINFORMATION);
if (settings.StartReloaded)
{
// We're in an admin process; but we want to de-escalate as Reloaded-II is not
// meant to be used in admin mode.
StartProcessWithReducedPrivileges(executablePath);
}
}
catch (TaskCanceledException)
{
IOEx.TryDeleteDirectory(settings.InstallLocation);
}
catch (Exception e)
{
IOEx.TryDeleteDirectory(settings.InstallLocation);
Native.MessageBox(IntPtr.Zero, "There was an error in installing Reloaded.\n" +
$"Feel free to open an issue on github.com/Reloaded-Project/Reloaded-II if you require support.\n" +
$"Message: {e.Message}\n" +
$"Stack Trace: {e.StackTrace}", "Error in Installing Reloaded", MB_OK);
}
}
private void MakeProtonShortcut(string? userName, string protonTricksSuffix, string shortcutPath, string nativeExecutablePath)
{
nativeExecutablePath = nativeExecutablePath.Replace('\\', '/');
var desktopFile =
"""
[Desktop Entry]
Name=Reloaded-II ({SUFFIX})
Exec=bash -ic 'protontricks-launch --appid {APPID} "{NATIVEPATH}"'
Type=Application
StartupNotify=true
Comment=Reloaded II installation for {SUFFIX}
Path={RELOADEDFOLDER}
Icon={RELOADEDFOLDER}/Mods/reloaded.sharedlib.hooks/Preview.png
StartupWMClass=reloaded-ii.exe
""";
// reloaded.sharedlib.hooks is present in all Reloaded installs after boot, so we can use that... for now.
desktopFile = desktopFile.Replace("{USER}", userName);
desktopFile = desktopFile.Replace("{APPID}", Environment.GetEnvironmentVariable("STEAM_APPID"));
desktopFile = desktopFile.Replace("{SUFFIX}", protonTricksSuffix);
desktopFile = desktopFile.Replace("{RELOADEDFOLDER}", Path.GetDirectoryName(nativeExecutablePath)!.Replace('\\', '/'));
desktopFile = desktopFile.Replace("{NATIVEPATH}", nativeExecutablePath);
shortcutPath = shortcutPath.Replace(".lnk", ".desktop");
File.WriteAllText(shortcutPath, desktopFile);
// Write `.desktop` file that integrates into shell.
var shellShortcutPath = $@"Z:\home\{userName}\.local\share\applications\{Path.GetFileName(shortcutPath)}";
File.WriteAllText(shellShortcutPath, desktopFile);
// Mark as executable.
LinuxTryMarkAsExecutable(shortcutPath);
LinuxTryMarkAsExecutable(shellShortcutPath);
}
private static void LinuxTryMarkAsExecutable(string windowsPath)
{
windowsPath = windowsPath.Replace('\\', '/');
windowsPath = windowsPath.Replace("Z:", "");
var processInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c start Z:/bin/chmod +x \"{windowsPath}\"",
UseShellExecute = true,
CreateNoWindow = true
};
try
{
Process.Start(processInfo);
}
catch (Exception)
{
// If the first attempt fails, try with the alternative path
processInfo.Arguments = $"/c start Z:/usr/bin/chmod +x \"{windowsPath}\"";
try
{
Process.Start(processInfo);
}
catch (Exception)
{
// Both attempts failed
}
}
}
private static void OverrideInstallLocationForProton(Settings settings, string protonTricksSuffix, out string nativeInstallFolder, out string? userName)
{
nativeInstallFolder = "";
userName = "";
if (settings.IsManuallyOverwrittenLocation) return;
if (string.IsNullOrEmpty(protonTricksSuffix)) return;
var desktopDir = GetHomeDesktopDirectoryOnProton(out nativeInstallFolder, out userName);
var folderName = $"Reloaded-II - {protonTricksSuffix}";
settings.InstallLocation = Path.Combine(desktopDir, folderName);
nativeInstallFolder = Path.Combine(nativeInstallFolder, folderName);
}
private static async Task DownloadReloadedAsync(string downloadLocation, IProgress<double> downloadProgress)
{
using var client = new HttpClient();
using var response = await client.GetAsync("https://github.com/Reloaded-Project/Reloaded-II/releases/latest/download/Release.zip", HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? 0L;
var totalReadBytes = 0L;
int readBytes;
var buffer = new byte[128 * 1024];
using var contentStream = await response.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(downloadLocation, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, true);
while ((readBytes = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, readBytes);
totalReadBytes += readBytes;
downloadProgress?.Report(totalReadBytes * 1d / totalBytes);
}
if (!File.Exists(downloadLocation))
throw new Exception("Reloaded failed to download (no file was written to disk).");
}
private static void ExtractReloaded(string extractFolderPath, string downloadedPackagePath, IProgress<double> slice)
{
ZipFile.ExtractToDirectory(downloadedPackagePath, extractFolderPath);
if (Directory.GetFiles(extractFolderPath).Length == 0)
throw new Exception($"Reloaded failed to download (downloaded archive was not properly extracted).");
File.Delete(downloadedPackagePath);
slice.Report(1);
}
private static unsafe void StartProcessWithReducedPrivileges(string executablePath)
{
SAFER_LEVEL_HANDLE saferHandle = default;
try
{
// 1. Create a new new access token
if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER,
SAFER_LEVEL_OPEN, &saferHandle, null))
throw new Win32Exception(Marshal.GetLastWin32Error());
if (!SaferComputeTokenFromLevel(saferHandle, null, out var newAccessToken, 0, null))
throw new Win32Exception(Marshal.GetLastWin32Error());
// Set the token to medium integrity because SaferCreateLevel doesn't reduce the
// integrity level of the token and keep it as high.
if (!ConvertStringSidToSid("S-1-16-8192", out var psid))
throw new Win32Exception(Marshal.GetLastWin32Error());
TOKEN_MANDATORY_LABEL tml = default;
tml.Label.Attributes = SE_GROUP_INTEGRITY;
tml.Label.Sid = (PSID)psid.DangerousGetHandle();
var length = (uint)Marshal.SizeOf(tml);
if (!SetTokenInformation(newAccessToken, TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, &tml, length))
throw new Win32Exception(Marshal.GetLastWin32Error());
// 2. Start process using the new access token
// Cannot use Process.Start as there is no way to set the access token to use
fixed (char* commandLinePtr = executablePath)
{
STARTUPINFOW si = default;
Span<char> span = new Span<char>(commandLinePtr, executablePath.Length);
if (CreateProcessAsUser(newAccessToken, null, ref span, null, null, bInheritHandles: false, default,
null, null, in si, out var pi))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
}
catch
{
// In case of WINE, or some other unexpected event.
Process.Start(executablePath);
}
finally
{
if (saferHandle != default)
{
SaferCloseLevel(saferHandle);
}
}
}
private static void SetEnvironmentVariable(string name, string value)
{
Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.Process);
using var key = Registry.CurrentUser.OpenSubKey("Environment", true);
if (key != null)
{
key.SetValue(name, value);
Console.WriteLine($"Environment variable '{name}' set to '{value}'");
}
else
{
Console.WriteLine("Failed to open Environment key in registry");
}
}
/// <summary>
/// This suffix is appended to shortcut name and install folder.
/// </summary>
private static string GetProtontricksSuffix()
{
try
{
// Note: Steam games are usually installed in a folder which is a friendly name
// for the game. If the user is running in Protontricks, there's a high
// chance that the folder will be named just right, e.g. 'Persona 5 Royal'.
return Path.GetFileName(Environment.GetEnvironmentVariable("STEAM_APP_PATH")) ?? string.Empty;
}
catch (Exception)
{
return "";
}
}
/// <summary>
/// This suffix is appended to shortcut name and install folder.
/// </summary>
private static string GetHomeDesktopDirectoryOnProton(out string linuxPath, out string? userName)
{
userName = Environment.GetEnvironmentVariable("LOGNAME");
if (userName != null)
{
// TODO: This is a terrible hack.
linuxPath = $"/home/{userName}/Desktop";
return @$"Z:\home\{userName}\Desktop";
}
Native.MessageBox(IntPtr.Zero, "Cannot determine username for proton installation.\n" +
"Please make sure that 'LOGNAME' environment variable is set.",
"Error in Installing Reloaded", 0x0);
throw new Exception("Terminated because cannot find username.");
}
private static void ShowDotFilesInWine()
{
try
{
using RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Wine", true)!;
// Set the ShowDotFiles value to "Y"
key.SetValue("ShowDotFiles", "Y", RegistryValueKind.String);
Console.WriteLine("Successfully set ShowDotFiles to Y in the Wine registry.");
}
catch (Exception)
{
Native.MessageBox(IntPtr.Zero, "Failed to auto-unhide dot files in Wine.\n" +
"You'll need to enter `winecfg` and check `show dot files` manually yourself.",
"Error in Configuring WINE", 0x0);
}
}
}