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

Move XIVLauncher.Common.Unix to XIVLauncher.Core #188

Merged
merged 2 commits into from
Nov 6, 2024
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
293 changes: 293 additions & 0 deletions src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
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 XIVLauncher.Common.Util;

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

namespace XIVLauncher.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/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-arch-8.5.r4.g4211bac7.tar.xz";
#elif WINE_XIV_FEDORA_LINUX
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-fedora-8.5.r4.g4211bac7.tar.xz";
#else
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-ubuntu-8.5.r4.g4211bac7.tar.xz";
#endif
private const string WINE_XIV_RELEASE_NAME = "wine-xiv-staging-fsync-git-8.5.r4.g4211bac7";

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;

private readonly Dxvk.DxvkHudType hudType;
private readonly bool gamemodeOn;
private readonly string dxvkAsyncOn;

public CompatibilityTools(WineSettings wineSettings, Dxvk.DxvkHudType hudType, bool? gamemodeOn, bool? dxvkAsyncOn, DirectoryInfo toolsFolder)
{
this.Settings = wineSettings;
this.hudType = hudType;
this.gamemodeOn = gamemodeOn ?? false;
this.dxvkAsyncOn = (dxvkAsyncOn ?? false) ? "1" : "0";

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();
}

if (!wineSettings.Prefix.Exists)
wineSettings.Prefix.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).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={(wineD3D ? "b" : "n")}");

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

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

string dxvkHud = hudType switch
{
Dxvk.DxvkHudType.None => "0",
Dxvk.DxvkHudType.Fps => "fps",
Dxvk.DxvkHudType.Full => "full",
_ => throw new ArgumentOutOfRangeException()
};

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

wineEnviromentVariables.Add("DXVK_HUD", dxvkHud);
wineEnviromentVariables.Add("DXVK_ASYNC", dxvkAsyncOn);
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);
}
}
55 changes: 55 additions & 0 deletions src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using XIVLauncher.Common.Util;

namespace XIVLauncher.Common.Unix.Compatibility;

public static class Dxvk
{
private const string DXVK_DOWNLOAD = "https://github.com/Sporif/dxvk-async/releases/download/1.10.1/dxvk-async-1.10.1.tar.gz";
private const string DXVK_NAME = "dxvk-async-1.10.1";

public static async Task InstallDxvk(DirectoryInfo prefix, DirectoryInfo installDirectory)
{
var dxvkPath = Path.Combine(installDirectory.FullName, DXVK_NAME, "x64");

if (!Directory.Exists(dxvkPath))
{
Log.Information("DXVK does not exist, downloading");
await DownloadDxvk(installDirectory).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)
{
using var client = new HttpClient();
var tempPath = PlatformHelpers.GetTempFileName();

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

File.Delete(tempPath);
}

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

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

[SettingsDescription("Full", "Show everything")]
Full,
}
}
28 changes: 28 additions & 0 deletions src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.IO;

namespace XIVLauncher.Common.Unix.Compatibility.GameFixes;

public abstract class GameFix
{
public GameFix(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory)
{
GameDir = gameDirectory;
ConfigDir = configDirectory;
WinePrefixDir = winePrefixDirectory;
TempDir = tempDirectory;
}

public abstract string LoadingTitle { get; }

public GameFixApply.UpdateProgressDelegate UpdateProgress;

public DirectoryInfo WinePrefixDir { get; private set; }

public DirectoryInfo ConfigDir { get; private set; }

public DirectoryInfo GameDir { get; private set; }

public DirectoryInfo TempDir { get; private set; }

public abstract void Apply();
}
Loading
Loading