Skip to content
This repository has been archived by the owner on Feb 16, 2024. It is now read-only.

Adding XIVLauncher2.Common #5

Merged
merged 2 commits into from
Jan 17, 2023
Merged
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
281 changes: 281 additions & 0 deletions src/XIVLauncher2.Common.Unix/Compatibility/CompatibilityTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using XIVLauncher2.Common.Util;

#if FLATPAK
#warning THIS IS A FLATPAK BUILD!!!
#endif

namespace XIVLauncher2.Common.Unix.Compatibility;

public class CompatibilityTools
{
private DirectoryInfo toolDirectory;
private DirectoryInfo dxvkDirectory;

private StreamWriter logWriter;

#if WINE_XIV_ARCH_LINUX
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-arch-7.10.r3.g560db77d.tar.xz";
#elif WINE_XIV_FEDORA_LINUX
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-fedora-7.10.r3.g560db77d.tar.xz";
#else
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-ubuntu-7.10.r3.g560db77d.tar.xz";
#endif
private const string WINE_XIV_RELEASE_NAME = "wine-xiv-staging-fsync-git-7.10.r3.g560db77d";

public bool IsToolReady { get; private set; }

public WineSettings Settings { get; private set; }

private string WineBinPath => Settings.StartupType == WineStartupType.Managed ?
Path.Combine(toolDirectory.FullName, WINE_XIV_RELEASE_NAME, "bin") :
Settings.CustomBinPath;
private string Wine64Path => Path.Combine(WineBinPath, "wine64");
private string WineServerPath => Path.Combine(WineBinPath, "wineserver");

public bool IsToolDownloaded => File.Exists(Wine64Path) && Settings.Prefix.Exists;

public DxvkSettings DxvkSettings { get; private set; }

private readonly bool gamemodeOn;

public CompatibilityTools(WineSettings wineSettings, DxvkSettings dxvkSettings, bool? gamemodeOn, DirectoryInfo toolsFolder)
{
this.Settings = wineSettings;
this.DxvkSettings = dxvkSettings;
this.gamemodeOn = gamemodeOn ?? false;
this.toolDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "beta"));
this.dxvkDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "dxvk"));

this.logWriter = new StreamWriter(wineSettings.LogFile.FullName);

if (wineSettings.StartupType == WineStartupType.Managed)
{
if (!this.toolDirectory.Exists)
this.toolDirectory.Create();

if (!this.dxvkDirectory.Exists)
this.dxvkDirectory.Create();
}
}

public async Task EnsureTool(DirectoryInfo tempPath)
{
if (!File.Exists(Wine64Path))
{
Log.Information("Compatibility tool does not exist, downloading");
await DownloadTool(tempPath).ConfigureAwait(false);
}

EnsurePrefix();
await Dxvk.InstallDxvk(Settings.Prefix, dxvkDirectory, DxvkSettings).ConfigureAwait(false);

IsToolReady = true;
}

private async Task DownloadTool(DirectoryInfo tempPath)
{
using var client = new HttpClient();
var tempFilePath = Path.Combine(tempPath.FullName, $"{Guid.NewGuid()}");

await File.WriteAllBytesAsync(tempFilePath, await client.GetByteArrayAsync(WINE_XIV_RELEASE_URL).ConfigureAwait(false)).ConfigureAwait(false);

PlatformHelpers.Untar(tempFilePath, this.toolDirectory.FullName);

Log.Information("Compatibility tool successfully extracted to {Path}", this.toolDirectory.FullName);

File.Delete(tempFilePath);
}

private void ResetPrefix()
{
Settings.Prefix.Refresh();

if (Settings.Prefix.Exists)
Settings.Prefix.Delete(true);

Settings.Prefix.Create();
EnsurePrefix();
}

public void EnsurePrefix()
{
RunInPrefix("cmd /c dir %userprofile%/Documents > nul").WaitForExit();
}

public Process RunInPrefix(string command, string workingDirectory = "", IDictionary<string, string> environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false)
{
var psi = new ProcessStartInfo(Wine64Path);
psi.Arguments = command;

Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, command);
return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D);
}

public Process RunInPrefix(string[] args, string workingDirectory = "", IDictionary<string, string> environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false)
{
var psi = new ProcessStartInfo(Wine64Path);
foreach (var arg in args)
psi.ArgumentList.Add(arg);

Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, psi.ArgumentList.Aggregate(string.Empty, (a, b) => a + " " + b));
return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D);
}

private void MergeDictionaries(StringDictionary a, IDictionary<string, string> b)
{
if (b is null)
return;

foreach (var keyValuePair in b)
{
if (a.ContainsKey(keyValuePair.Key))
a[keyValuePair.Key] = keyValuePair.Value;
else
a.Add(keyValuePair.Key, keyValuePair.Value);
}
}

private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDictionary<string, string> environment, bool redirectOutput, bool writeLog, bool wineD3D)
{
psi.RedirectStandardOutput = redirectOutput;
psi.RedirectStandardError = writeLog;
psi.UseShellExecute = false;
psi.WorkingDirectory = workingDirectory;

var wineEnviromentVariables = new Dictionary<string, string>();
wineEnviromentVariables.Add("WINEPREFIX", Settings.Prefix.FullName);
wineEnviromentVariables.Add("WINEDLLOVERRIDES", $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(DxvkSettings.Enabled && !wineD3D ? "n" : "b")}");

if (!string.IsNullOrEmpty(Settings.DebugVars))
{
wineEnviromentVariables.Add("WINEDEBUG", Settings.DebugVars);
}

wineEnviromentVariables.Add("XL_WINEONLINUX", "true");
string ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD") ?? "";

if (gamemodeOn && !ldPreload.Contains("libgamemodeauto.so.0"))
{
ldPreload = ldPreload.Equals("") ? "libgamemodeauto.so.0" : ldPreload + ":libgamemodeauto.so.0";
}

foreach (KeyValuePair<string, string> dxvkVar in DxvkSettings.DxvkVars)
wineEnviromentVariables.Add(dxvkVar.Key, dxvkVar.Value);

wineEnviromentVariables.Add("WINEESYNC", Settings.EsyncOn);
wineEnviromentVariables.Add("WINEFSYNC", Settings.FsyncOn);

wineEnviromentVariables.Add("LD_PRELOAD", ldPreload);

MergeDictionaries(psi.EnvironmentVariables, wineEnviromentVariables);
MergeDictionaries(psi.EnvironmentVariables, environment);

#if FLATPAK_NOTRIGHTNOW
psi.FileName = "flatpak-spawn";

psi.ArgumentList.Insert(0, "--host");
psi.ArgumentList.Insert(1, Wine64Path);

foreach (KeyValuePair<string, string> envVar in wineEnviromentVariables)
{
psi.ArgumentList.Insert(1, $"--env={envVar.Key}={envVar.Value}");
}

if (environment != null)
{
foreach (KeyValuePair<string, string> envVar in environment)
{
psi.ArgumentList.Insert(1, $"--env=\"{envVar.Key}\"=\"{envVar.Value}\"");
}
}
#endif

Process helperProcess = new();
helperProcess.StartInfo = psi;
helperProcess.ErrorDataReceived += new DataReceivedEventHandler((_, errLine) =>
{
if (String.IsNullOrEmpty(errLine.Data))
return;

try
{
logWriter.WriteLine(errLine.Data);
Console.Error.WriteLine(errLine.Data);
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException ||
ex is OverflowException ||
ex is IndexOutOfRangeException)
{
// very long wine log lines get chopped off after a (seemingly) arbitrary limit resulting in strings that are not null terminated
//logWriter.WriteLine("Error writing Wine log line:");
//logWriter.WriteLine(ex.Message);
}
});

helperProcess.Start();
if (writeLog)
helperProcess.BeginErrorReadLine();

return helperProcess;
}

public Int32[] GetProcessIds(string executableName)
{
var wineDbg = RunInPrefix("winedbg --command \"info proc\"", redirectOutput: true);
var output = wineDbg.StandardOutput.ReadToEnd();
var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Where(l => l.Contains(executableName));
return matchingLines.Select(l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber)).ToArray();
}

public Int32 GetProcessId(string executableName)
{
return GetProcessIds(executableName).FirstOrDefault();
}

public Int32 GetUnixProcessId(Int32 winePid)
{
var wineDbg = RunInPrefix("winedbg --command \"info procmap\"", redirectOutput: true);
var output = wineDbg.StandardOutput.ReadToEnd();
if (output.Contains("syntax error\n"))
return 0;
var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where(
l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid);
var unixPids = matchingLines.Select(l => int.Parse(l.Substring(10, 8), System.Globalization.NumberStyles.HexNumber)).ToArray();
return unixPids.FirstOrDefault();
}

public string UnixToWinePath(string unixPath)
{
var launchArguments = new string[] { "winepath", "--windows", unixPath };
var winePath = RunInPrefix(launchArguments, redirectOutput: true);
var output = winePath.StandardOutput.ReadToEnd();
return output.Split('\n', StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
}

public void AddRegistryKey(string key, string value, string data)
{
var args = new string[] { "reg", "add", key, "/v", value, "/d", data, "/f" };
var wineProcess = RunInPrefix(args);
wineProcess.WaitForExit();
}

public void Kill()
{
var psi = new ProcessStartInfo(WineServerPath)
{
Arguments = "-k"
};
psi.EnvironmentVariables.Add("WINEPREFIX", Settings.Prefix.FullName);

Process.Start(psi);
}
}
79 changes: 79 additions & 0 deletions src/XIVLauncher2.Common.Unix/Compatibility/Dxvk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using XIVLauncher2.Common.Util;

namespace XIVLauncher2.Common.Unix.Compatibility;

public static class Dxvk
{
public static async Task InstallDxvk(DirectoryInfo prefix, DirectoryInfo installDirectory, DxvkSettings dxvkSettings)
{
var dxvkPath = Path.Combine(installDirectory.FullName, dxvkSettings.FolderName, "x64");

if (!Directory.Exists(dxvkPath))
{
Log.Information("DXVK does not exist, downloading");
await DownloadDxvk(installDirectory, dxvkSettings.DownloadURL).ConfigureAwait(false);
}

var system32 = Path.Combine(prefix.FullName, "drive_c", "windows", "system32");
var files = Directory.GetFiles(dxvkPath);

foreach (string fileName in files)
{
File.Copy(fileName, Path.Combine(system32, Path.GetFileName(fileName)), true);
}
}

private static async Task DownloadDxvk(DirectoryInfo installDirectory, string downloadURL)
{
using var client = new HttpClient();
var tempPath = Path.GetTempFileName();

File.WriteAllBytes(tempPath, await client.GetByteArrayAsync(downloadURL));
PlatformHelpers.Untar(tempPath, installDirectory.FullName);

File.Delete(tempPath);
}

public enum DxvkHudType
{
[SettingsDescription("None", "Show nothing")]
None,

[SettingsDescription("FPS", "Only show FPS")]
Fps,

[SettingsDescription("DXVK Hud Custom", "Use a custom DXVK_HUD string")]
Custom,

[SettingsDescription("Full", "Show everything")]
Full,

[SettingsDescription("MangoHud Default", "Uses no config file.")]
MangoHud,

[SettingsDescription("MangoHud Custom", "Specify a custom config file")]
MangoHudCustom,

[SettingsDescription("MangoHud Full", "Show (almost) everything")]
MangoHudFull,
}

public enum DxvkVersion
{
[SettingsDescription("1.10.1", "The version of DXVK used with XIVLauncher.Core 1.0.2. Safe to use.")]
v1_10_1,

[SettingsDescription("1.10.2", "Older version of 1.10 branch of DXVK. Safe to use.")]
v1_10_2,

[SettingsDescription("1.10.3 (default)", "Current version of 1.10 branch of DXVK.")]
v1_10_3,

[SettingsDescription("2.0 (might break Dalamud, GShade)", "Newest version of DXVK. May be faster, but not stable yet.")]
v2_0,
}
}
Loading