Skip to content

Commit

Permalink
Merge pull request #188 from rankynbass/move-xl-common-unix
Browse files Browse the repository at this point in the history
Move XIVLauncher.Common.Unix to XIVLauncher.Core
  • Loading branch information
Blooym authored Nov 6, 2024
2 parents 12e550b + d8ac99f commit 980f3b9
Show file tree
Hide file tree
Showing 14 changed files with 757 additions and 2 deletions.
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

0 comments on commit 980f3b9

Please sign in to comment.