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

Implement dalamud-platform launch argument #1452

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Dalamud.Boot/DalamudStartInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
Expand Down
1 change: 1 addition & 0 deletions Dalamud.Boot/DalamudStartInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct DalamudStartInfo {
std::string DefaultPluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
std::string Platform;
std::string GameVersion;
int DelayInitializeMs = 0;
std::string TroubleshootingPackData;
Expand Down
16 changes: 2 additions & 14 deletions Dalamud.Boot/utils.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "pch.h"
#include "DalamudStartInfo.h"

#include "utils.h"

Expand Down Expand Up @@ -579,20 +580,7 @@ std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
}

bool utils::is_running_on_wine() {
if (get_env<bool>(L"XL_WINEONLINUX"))
return true;
HMODULE hntdll = GetModuleHandleW(L"ntdll.dll");
if (!hntdll)
return true;
if (GetProcAddress(hntdll, "wine_get_version"))
return true;
if (GetProcAddress(hntdll, "wine_get_host_version"))
return true;
if (GetProcAddress(hntdll, "wine_server_call"))
return true;
if (GetProcAddress(hntdll, "wine_unix_to_nt_file_name"))
return true;
return false;
return g_startInfo.Platform != "WINDOWS";
}

std::filesystem::path utils::get_module_path(HMODULE hModule) {
Expand Down
10 changes: 9 additions & 1 deletion Dalamud.Common/DalamudStartInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using Dalamud.Common.Game;
using Newtonsoft.Json;

Expand All @@ -14,7 +15,7 @@ public record DalamudStartInfo
/// </summary>
public DalamudStartInfo()
{
// ignored
this.Platform = OSPlatform.Create("UNKNOWN");
}

/// <summary>
Expand All @@ -30,6 +31,7 @@ public DalamudStartInfo(DalamudStartInfo other)
this.PluginDirectory = other.PluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.Platform = other.Platform;
this.GameVersion = other.GameVersion;
this.DelayInitializeMs = other.DelayInitializeMs;
this.TroubleshootingPackData = other.TroubleshootingPackData;
Expand Down Expand Up @@ -84,6 +86,12 @@ public DalamudStartInfo(DalamudStartInfo other)
/// </summary>
public ClientLanguage Language { get; set; } = ClientLanguage.English;

/// <summary>
/// Gets or sets the underlying platform�Dalamud runs on.
/// </summary>
[JsonConverter(typeof(OSPlatformConverter))]
public OSPlatform Platform { get; set; }

/// <summary>
/// Gets or sets the current game version code.
/// </summary>
Expand Down
78 changes: 78 additions & 0 deletions Dalamud.Common/OSPlatformConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Runtime.InteropServices;
using Newtonsoft.Json;

namespace Dalamud.Common;

/// <summary>
/// Converts a <see cref="OSPlatform"/> to and from a string (e.g. <c>"FreeBSD"</c>).
/// </summary>
public sealed class OSPlatformConverter : JsonConverter
{
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
}
else if (value is OSPlatform)
{
writer.WriteValue(value.ToString());
}
else
{
throw new JsonSerializationException("Expected OSPlatform object value");
}
}

/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing property value of the JSON that is being converted.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
if (reader.TokenType == JsonToken.String)
{
try
{
return OSPlatform.Create((string)reader.Value!);
}
catch (Exception ex)
{
throw new JsonSerializationException($"Error parsing OSPlatform string: {reader.Value}", ex);
}
}
else
{
throw new JsonSerializationException($"Unexpected token or value when parsing OSPlatform. Token: {reader.TokenType}, Value: {reader.Value}");
}
}
}

/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(OSPlatform);
}
}
113 changes: 92 additions & 21 deletions Dalamud.Injector/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,35 @@ private static void CullLogFile(string logPath, int cullingFileSize)
}
}

private static OSPlatform DetectPlatformHeuristic()
{
var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll");
var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version");
var winePlatform = GetWinePlatform(wineGetHostVersionPtr);
var isWine = wineServerCallPtr != nint.Zero;

static unsafe string? GetWinePlatform(nint wineGetHostVersionPtr)
{
if (wineGetHostVersionPtr == nint.Zero) return null;

var methodDelegate = (delegate* unmanaged[Fastcall]<out char*, out char*, void>)wineGetHostVersionPtr;
methodDelegate(out var platformPtr, out var _);

if (platformPtr == null) return null;

return Marshal.PtrToStringAnsi((nint)platformPtr);
}

if (!isWine)
return OSPlatform.Windows;

if (winePlatform == "Darwin")
return OSPlatform.OSX;

return OSPlatform.Linux;
}

private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List<string> args)
{
int len;
Expand All @@ -264,6 +293,7 @@ private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(Dalam
var logName = startInfo.LogName;
var logPath = startInfo.LogPath;
var languageStr = startInfo.Language.ToString().ToLowerInvariant();
var platformStr = startInfo.Platform.ToString().ToLowerInvariant();
var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}";

for (var i = 2; i < args.Count; i++)
Expand Down Expand Up @@ -292,6 +322,10 @@ private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(Dalam
{
languageStr = args[i][key.Length..].ToLowerInvariant();
}
else if (args[i].StartsWith(key = "--dalamud-platform="))
{
platformStr = args[i][key.Length..].ToLowerInvariant();
}
else if (args[i].StartsWith(key = "--dalamud-tspack-b64="))
{
troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..]));
Expand Down Expand Up @@ -359,11 +393,35 @@ private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(Dalam
throw new CommandLineException($"\"{languageStr}\" is not a valid supported language.");
}

OSPlatform platform;
if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len]) // covers both win32 and Windows
{
platform = OSPlatform.Windows;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "linux").Length))] == key[0..len])
{
platform = OSPlatform.Linux;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "macos").Length))] == key[0..len])
{
platform = OSPlatform.OSX;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "osx").Length))] == key[0..len])
{
platform = OSPlatform.OSX;
}
else
{
platform = DetectPlatformHeuristic();
Log.Warning("Heuristically determined host system platform as {platform}", platform);
}

startInfo.WorkingDirectory = workingDirectory;
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs;
startInfo.GameVersion = null;
startInfo.TroubleshootingPackData = troubleshootingData;
Expand Down Expand Up @@ -427,7 +485,7 @@ private static int ProcessHelpCommand(List<string> args, string? particularComma
}

Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]");
Console.WriteLine(" [--dalamud-plugin-directory=path]");
Console.WriteLine(" [--dalamud-plugin-directory=path] [--dalamud-platform=win32|linux|macOS]");
Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]");
Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]");

Expand Down Expand Up @@ -693,15 +751,42 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
{
try
{
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json");
gamePath = Path.Combine(JsonSerializer.CreateDefault().Deserialize<Dictionary<string, string>>(new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], "game", "ffxiv_dx11.exe");
Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath);
if (dalamudStartInfo.Platform == OSPlatform.Windows)
{
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json");
gamePath = Path.Combine(
JsonSerializer.CreateDefault()
.Deserialize<Dictionary<string, string>>(
new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"],
"game",
"ffxiv_dx11.exe");
Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath);
}
else if (dalamudStartInfo.Platform == OSPlatform.Linux)
{
var homeDir = $"Z:\\home\\{Environment.UserName}";
var xivlauncherDir = Path.Combine(homeDir, ".xlcore");
var launcherConfigPath = Path.Combine(xivlauncherDir, "launcher.ini");
var config = File.ReadAllLines(launcherConfigPath)
.Where(line => line.Contains('='))
.ToDictionary(line => line.Split('=')[0], line => line.Split('=')[1]);
gamePath = Path.Combine("Z:" + config["GamePath"].Replace('/', '\\'), "game", "ffxiv_dx11.exe");
Log.Information("Using game installation path configuration from from XIVLauncher Core: {0}", gamePath);
}
else
{
var homeDir = $"Z:\\Users\\{Environment.UserName}";
var xomlauncherDir = Path.Combine(homeDir, "Library", "Application Support", "XIV on Mac");
// we could try to parse the binary plist file here if we really wanted to...
gamePath = Path.Combine(xomlauncherDir, "ffxiv", "game", "ffxiv_dx11.exe");
Log.Information("Using default game installation path from XOM: {0}", gamePath);
}
}
catch (Exception)
{
Log.Error("Failed to read launcherConfigV3.json to get the set-up game path, please specify one using -g");
Log.Error("Failed to read launcher config to get the set-up game path, please specify one using -g");
return -1;
}

Expand Down Expand Up @@ -756,20 +841,6 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
if (encryptArguments)
{
var rawTickCount = (uint)Environment.TickCount;

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
#pragma warning disable SA1300
static extern ulong clock_gettime_nsec_np(int clockId);
#pragma warning restore SA1300

const int CLOCK_MONOTONIC_RAW = 4;
var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000;
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}

var ticks = rawTickCount & 0xFFFF_FFFFu;
var key = ticks & 0xFFFF_0000u;
gameArguments.Insert(0, $"T={ticks}");
Expand Down
42 changes: 42 additions & 0 deletions Dalamud.Injector/NativeFunctions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;

namespace Dalamud.Injector
Expand Down Expand Up @@ -910,5 +911,46 @@ public static extern bool DuplicateHandle(
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
DuplicateOptions dwOptions);

/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
/// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
/// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
/// </summary>
/// <param name="lpModuleName">
/// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default
/// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate
/// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure
/// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules
/// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns
/// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve
/// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetModuleHandleW(string lpModuleName);

/// <summary>
/// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
/// </summary>
/// <param name="hModule">
/// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
/// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
/// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <param name="procName">
/// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
/// in the low-order word; the high-order word must be zero.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the address of the exported function or variable. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
}
}
5 changes: 0 additions & 5 deletions Dalamud/Configuration/Internal/EnvironmentConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ namespace Dalamud.Configuration.Internal;
/// </summary>
internal class EnvironmentConfiguration
{
/// <summary>
/// Gets a value indicating whether the XL_WINEONLINUX setting has been enabled.
/// </summary>
public static bool XlWineOnLinux { get; } = GetEnvironmentVariable("XL_WINEONLINUX");

/// <summary>
/// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled.
/// </summary>
Expand Down
Loading
Loading