diff --git a/.editorconfig b/.editorconfig index 72d2313af..109c9f406 100644 --- a/.editorconfig +++ b/.editorconfig @@ -118,7 +118,11 @@ resharper_arrange_type_member_modifiers_highlighting = hint resharper_arrange_type_modifiers_highlighting = hint resharper_built_in_type_reference_style_for_member_access_highlighting = hint resharper_built_in_type_reference_style_highlighting = none +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = none +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none resharper_invert_if_highlighting = none +resharper_loop_can_be_converted_to_query_highlighting = none +resharper_method_has_async_overload_highlighting = none resharper_redundant_base_qualifier_highlighting = none resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 5955eac56..0629d4466 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -117,10 +117,6 @@ - - - - diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index 0106f0589..ad673c2d4 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -1,10 +1,6 @@  - - {18be40ac-9367-46ff-b848-4c528aa97a8d} - lib - {dc468303-865e-43bd-908f-a3542c4bb669} @@ -56,12 +52,4 @@ Dalamud.Boot DLL - - - Library Files - - - Library Files - - \ No newline at end of file diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index fec175972..cb2fa9226 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -68,6 +68,7 @@ DllExport DWORD WINAPI Initialize(LPVOID lpParam) void* entrypoint_vfn; int result = InitializeClrAndGetEntryPoint( + g_hModule, runtimeconfig_path, module_path, L"Dalamud.EntryPoint, Dalamud", diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 9ef0bb4f8..a396d4f42 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,8 +27,8 @@ - - + + all diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs index 6a45941f2..27be82f41 100644 --- a/Dalamud.CorePlugin/PluginWindow.cs +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -1,8 +1,6 @@ using System; using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using ImGuiNET; diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj index 66b55feb2..ecd572daf 100644 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj +++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj @@ -45,7 +45,7 @@ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - Windows + Console true false ..\lib\CoreCLR;%(AdditionalLibraryDirectories) @@ -99,10 +99,6 @@ - - - - diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters index 45bf1b970..75b1bf84e 100644 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters +++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters @@ -13,9 +13,6 @@ {4faac519-3a73-4b2b-96e7-fb597f02c0be} ico;rc - - {6aff1bed-6979-4bc9-94e8-ddafb626e6bf} - @@ -55,12 +52,4 @@ Header Files - - - Library Files - - - Library Files - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp index d3deb051b..cf5a95b5c 100644 --- a/Dalamud.Injector.Boot/main.cpp +++ b/Dalamud.Injector.Boot/main.cpp @@ -6,16 +6,8 @@ #include "..\lib\CoreCLR\CoreCLR.h" #include "..\lib\CoreCLR\boot.h" -int wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) +int wmain(int argc, wchar_t** argv) { - #ifndef NDEBUG - ConsoleSetup(L"Dalamud.Injector"); - #endif - - int argc = 0; - LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); - //ShowWindow(GetConsoleWindow(), SW_HIDE); - printf("Dalamud.Injector, (c) 2021 XIVLauncher Contributors\nBuilt at: %s@%s\n\n", __DATE__, __TIME__); wchar_t _module_path[MAX_PATH]; @@ -29,6 +21,7 @@ int wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LP void* entrypoint_vfn; int result = InitializeClrAndGetEntryPoint( + GetModuleHandleW(nullptr), runtimeconfig_path, module_path, L"Dalamud.Injector.EntryPoint, Dalamud.Injector", @@ -42,18 +35,9 @@ int wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LP typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**); custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); - printf("Running Dalamud Injector... "); + printf("Running Dalamud Injector...\n"); entrypoint_fn(argc, argv); printf("Done!\n"); - // =========================================================================== // - - #ifndef NDEBUG - fclose(stdin); - fclose(stdout); - fclose(stderr); - FreeConsole(); - #endif - return 0; } diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index ee417d955..868d212e5 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -67,6 +67,7 @@ + all diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 829a15429..8896dc293 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; -using System.Threading; using Dalamud.Game; using Newtonsoft.Json; @@ -37,45 +37,78 @@ namespace Dalamud.Injector /// byte** string arguments. public static void Main(int argc, IntPtr argvPtr) { - InitUnhandledException(); - InitLogging(); - - var args = new string[argc]; + Init(); + List args = new(argc); unsafe { var argv = (IntPtr*)argvPtr; for (var i = 0; i < argc; i++) + args.Add(Marshal.PtrToStringUni(argv[i])); + } + + if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test") + { + Environment.Exit(ProcessLaunchTestCommand(args)); + return; + } + + DalamudStartInfo startInfo = null; + if (args.Count == 1) + { + // No command defaults to inject + args.Add("inject"); + args.Add("--all"); + args.Add("--warn"); + } + else if (int.TryParse(args[1], out var _)) + { + // Assume that PID has been passed. + args.Insert(1, "inject"); + + // If originally second parameter exists, then assume that it's a base64 encoded start info. + // Dalamud.Injector.exe inject [pid] [base64] + if (args.Count == 4) { - args[i] = Marshal.PtrToStringUni(argv[i]); + startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[3]))); + args.RemoveAt(3); } } + startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); + + var mainCommand = args[1].ToLowerInvariant(); + if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessInjectCommand(args, startInfo)); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessLaunchCommand(args, startInfo)); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null)); + } + else + { + Console.WriteLine("Invalid command: {0}", mainCommand); + ProcessHelpCommand(args); + Environment.Exit(-1); + } + } + + private static void Init() + { + InitUnhandledException(); + InitLogging(); + var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; if (cwd.FullName != Directory.GetCurrentDirectory()) { Log.Debug($"Changing cwd to {cwd}"); Directory.SetCurrentDirectory(cwd.FullName); } - - var process = GetProcess(args.ElementAtOrDefault(1)); - if (process == null) - { - Log.Error("Could not open process"); - return; - } - - var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process); - - // TODO: XL does not set this!!! we need to keep this line here for now, otherwise we crash in the Dalamud entrypoint - startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); - - // This seems to help with the STATUS_INTERNAL_ERROR condition - Thread.Sleep(1000); - - Inject(process, startInfo); - - Thread.Sleep(1000); } private static void InitUnhandledException() @@ -138,6 +171,7 @@ namespace Dalamud.Injector CullLogFile(logPath, 1 * 1024 * 1024); Log.Logger = new LoggerConfiguration() + .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Verbose) .WriteTo.Async(a => a.File(logPath)) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); @@ -189,133 +223,423 @@ namespace Dalamud.Injector } } - private static Process? GetProcess(string? arg) + private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List args) { - Process process = null; + if (startInfo == null) + startInfo = new(); - var pid = -1; - if (arg != default) + var workingDirectory = startInfo.WorkingDirectory; + var configurationPath = startInfo.ConfigurationPath; + var pluginDirectory = startInfo.PluginDirectory; + var defaultPluginDirectory = startInfo.DefaultPluginDirectory; + var assetDirectory = startInfo.AssetDirectory; + var delayInitializeMs = startInfo.DelayInitializeMs; + + for (var i = 2; i < args.Count; i++) { - pid = int.Parse(arg); + string key; + if (args[i].StartsWith(key = "--dalamud-working-directory=")) + workingDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-configuration-path=")) + configurationPath = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-plugin-directory=")) + pluginDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-dev-plugin-directory=")) + defaultPluginDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-asset-directory=")) + assetDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) + delayInitializeMs = int.Parse(args[i][key.Length..]); + else + continue; + + args.RemoveAt(i); + i--; } - switch (pid) + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); + + workingDirectory ??= Directory.GetCurrentDirectory(); + configurationPath ??= Path.Combine(xivlauncherDir, "dalamudConfig.json"); + pluginDirectory ??= Path.Combine(xivlauncherDir, "installedPlugins"); + defaultPluginDirectory ??= Path.Combine(xivlauncherDir, "devPlugins"); + assetDirectory ??= Path.Combine(xivlauncherDir, "dalamudAssets", "dev"); + + return new() { - case -1: - process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault(); + WorkingDirectory = workingDirectory, + ConfigurationPath = configurationPath, + PluginDirectory = pluginDirectory, + DefaultPluginDirectory = defaultPluginDirectory, + AssetDirectory = assetDirectory, + Language = ClientLanguage.English, + GameVersion = null, + DelayInitializeMs = delayInitializeMs, + }; + } - if (process == default) - { - throw new Exception("Could not find process"); - } + private static int ProcessHelpCommand(List args, string? particularCommand = default) + { + var exeName = Path.GetFileName(args[0]); - #if !DEBUG - var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({process.Id}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel); + var exeSpaces = string.Empty; + for (var i = exeName.Length; i > 0; i--) + exeSpaces += " "; - // IDCANCEL - if (result == 2) - { - Log.Information("User cancelled injection"); - return null; - } - #endif + if (particularCommand is null or "help") + Console.WriteLine("{0} help [command]", exeName); - break; + if (particularCommand is null or "inject") + Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [pid1] [pid2] [pid3] ...", exeName); - case -2: - var exePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe"; - var exeArgs = new StringBuilder() - .Append("DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 ") - .Append("DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 ") - .Append("DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 ") - .Append("DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 ") - .Append("DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 ") - .Append("DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 ") - .Append("DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 ") - .Append("DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 ") - .Append("DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 ") - .Append("SYS.Region=0 language=1 version=1.0.0.0 ") - .Append("DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0").ToString(); - process = Process.Start(exePath, exeArgs); - Thread.Sleep(1000); - break; - default: + if (particularCommand is null or "launch") + { + Console.WriteLine("{0} launch [-h/--help] [-f/--fake-arguments]", exeName); + Console.WriteLine("{0} [-g path/to/ffxiv_dx11.exe] [--game=path/to/ffxiv_dx11.exe]", exeSpaces); + Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces); + Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces); + Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces); + } + + Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory path] [--dalamud-configuration-path path]"); + Console.WriteLine(" [--dalamud-plugin-directory path] [--dalamud-dev-plugin-directory path]"); + Console.WriteLine(" [--dalamud-asset-directory path] [--dalamud-delay-initialize 0(ms)]"); + + return 0; + } + + private static int ProcessInjectCommand(List args, DalamudStartInfo dalamudStartInfo) + { + List processes = new(); + + var targetProcessSpecified = false; + var warnManualInjection = false; + var showHelp = args.Count <= 2; + + for (var i = 2; i < args.Count; i++) + { + if (int.TryParse(args[i], out int pid)) + { + targetProcessSpecified = true; try { - process = Process.GetProcessById(pid); + processes.Add(Process.GetProcessById(pid)); } catch (ArgumentException) { Log.Error("Could not find process with PID: {Pid}", pid); } - break; + continue; + } + + if (args[i] == "-h" || args[i] == "--help") + { + showHelp = true; + } + else if (args[i] == "-a" || args[i] == "--all") + { + targetProcessSpecified = true; + processes.AddRange(Process.GetProcessesByName("ffxiv_dx11")); + } + else if (args[i] == "--warn") + { + warnManualInjection = true; + } + else + { + Log.Error("\"{0}\" is not a valid argument.", args[i]); + return -1; + } } - return process; + if (showHelp) + { + ProcessHelpCommand(args, "inject"); + return args.Count <= 2 ? -1 : 0; + } + + if (!targetProcessSpecified) + { + Log.Error("No target process has been specified."); + return -1; + } + else if (!processes.Any()) + { + Log.Error("No suitable target process has been found."); + return -1; + } + + if (warnManualInjection) + { + var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel); + + // IDCANCEL + if (result == 2) + { + Log.Information("User cancelled injection"); + return -2; + } + } + + foreach (var process in processes) + Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName)); + + return 0; } - private static DalamudStartInfo GetStartInfo(string? arg, Process process) + private static int ProcessLaunchCommand(List args, DalamudStartInfo dalamudStartInfo) { - DalamudStartInfo startInfo; + string? gamePath = null; + List gameArguments = new(); + string? mode = null; + var useFakeArguments = false; + var showHelp = args.Count <= 2; + var handleOwner = IntPtr.Zero; - if (arg != default) + var parsingGameArgument = false; + for (var i = 2; i < args.Count; i++) { - startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg))); + if (parsingGameArgument) + { + gameArguments.Add(args[i]); + continue; + } + + if (args[i] == "-h" || args[i] == "--help") + { + showHelp = true; + } + else if (args[i] == "-f" || args[i] == "--fake-arguments") + { + useFakeArguments = true; + } + else if (args[i] == "-g") + { + gamePath = args[++i]; + } + else if (args[i].StartsWith("--game=")) + { + gamePath = args[i].Split('=', 2)[1]; + } + else if (args[i] == "-m") + { + mode = args[++i]; + } + else if (args[i].StartsWith("--mode=")) + { + gamePath = args[i].Split('=', 2)[1]; + } + else if (args[i].StartsWith("--handle-owner=")) + { + handleOwner = IntPtr.Parse(args[i].Split('=', 2)[1]); + } + else if (args[i] == "--") + { + parsingGameArgument = true; + } + else + { + Log.Error("No such command found: {0}", args[i]); + return -1; + } + } + + if (showHelp) + { + ProcessHelpCommand(args, "launch"); + return args.Count <= 2 ? -1 : 0; + } + + mode = mode == null ? "entrypoint" : mode.ToLowerInvariant(); + if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode) + { + mode = "entrypoint"; + } + else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode) + { + mode = "inject"; } else { - var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName); - var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); - - var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); - var gameVer = GameVersion.Parse(gameVerStr); - - startInfo = new DalamudStartInfo - { - WorkingDirectory = null, - ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"), - PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"), - DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"), - AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets", "dev"), - GameVersion = gameVer, - Language = ClientLanguage.English, - }; - - Log.Debug( - "Creating a new StartInfo with:\n" + - $" WorkingDirectory: {startInfo.WorkingDirectory}\n" + - $" ConfigurationPath: {startInfo.ConfigurationPath}\n" + - $" PluginDirectory: {startInfo.PluginDirectory}\n" + - $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + - $" AssetDirectory: {startInfo.AssetDirectory}\n" + - $" GameVersion: {startInfo.GameVersion}\n" + - $" Language: {startInfo.Language}\n"); - - Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you."); - Log.Information("Copy the following contents into the program arguments:"); - - var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))); - Log.Information(startInfoJson); + Log.Error("Invalid mode: {0}", mode); + return -1; } - return startInfo; + if (gamePath == null) + { + 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>(new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], "game", "ffxiv_dx11.exe"); + Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath); + } + catch (Exception) + { + gamePath = @"C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\ffxiv_dx11.exe"; + Log.Warning("Failed to read launcherConfigV3.json. Using default game installation path: {0}", gamePath); + } + + if (!File.Exists(gamePath)) + { + Log.Error("File not found: {0}", gamePath); + return -1; + } + } + + if (useFakeArguments) + { + var gameVersion = File.ReadAllText(Path.Combine(Directory.GetParent(gamePath).FullName, "ffxivgame.ver")); + var sqpackPath = Path.Combine(Directory.GetParent(gamePath).FullName, "sqpack"); + var maxEntitledExpansionId = 0; + while (File.Exists(Path.Combine(sqpackPath, $"ex{maxEntitledExpansionId + 1}", $"ex{maxEntitledExpansionId + 1}.ver"))) + maxEntitledExpansionId++; + + gameArguments.InsertRange(0, new string[] + { + "DEV.TestSID=0", + "DEV.UseSqPack=1", + "DEV.DataPathType=1", + "DEV.LobbyHost01=127.0.0.1", + "DEV.LobbyPort01=54994", + "DEV.LobbyHost02=127.0.0.2", + "DEV.LobbyPort02=54994", + "DEV.LobbyHost03=127.0.0.3", + "DEV.LobbyPort03=54994", + "DEV.LobbyHost04=127.0.0.4", + "DEV.LobbyPort04=54994", + "DEV.LobbyHost05=127.0.0.5", + "DEV.LobbyPort05=54994", + "DEV.LobbyHost06=127.0.0.6", + "DEV.LobbyPort06=54994", + "DEV.LobbyHost07=127.0.0.7", + "DEV.LobbyPort07=54994", + "DEV.LobbyHost08=127.0.0.8", + "DEV.LobbyPort08=54994", + "DEV.LobbyHost09=127.0.0.9", + "DEV.LobbyPort09=54994", + "SYS.Region=0", + "language=1", + $"ver={gameVersion}", + $"DEV.MaxEntitledExpansionID={maxEntitledExpansionId}", + "DEV.GMServerHost=127.0.0.100", + "DEV.GameQuitMessageBox=0", + }); + } + + var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); + var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) => + { + if (mode == "entrypoint") + { + var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); + Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); + if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0) + { + Log.Error("[HOOKS] RewriteRemoteEntryPointW failed"); + throw new Exception("RewriteRemoteEntryPointW failed"); + } + } + }); + + if (mode == "inject") + { + var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); + Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); + Inject(process, startInfo); + } + + var processHandleForOwner = IntPtr.Zero; + if (handleOwner != IntPtr.Zero) + { + if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess)) + Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + } + + Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}"); + + return 0; + } + + private static Process GetInheritableCurrentProcessHandle() + { + if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) + { + Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + return null; + } + + return new ExistingProcess(inheritableCurrentProcessHandle); + } + + private static int ProcessLaunchTestCommand(List args) + { + Console.WriteLine("Testing launch command."); + args[0] = Process.GetCurrentProcess().MainModule.FileName; + args[1] = "launch"; + + var inheritableCurrentProcess = GetInheritableCurrentProcessHandle(); // so that it closes the handle when it's done + args.Insert(2, $"--handle-owner={inheritableCurrentProcess.Handle}"); + + for (var i = 0; i < args.Count; i++) + Console.WriteLine("Argument {0}: {1}", i, args[i]); + + Process helperProcess = new(); + helperProcess.StartInfo.FileName = args[0]; + for (var i = 1; i < args.Count; i++) + helperProcess.StartInfo.ArgumentList.Add(args[i]); + helperProcess.StartInfo.RedirectStandardOutput = true; + helperProcess.StartInfo.RedirectStandardError = true; + helperProcess.StartInfo.UseShellExecute = false; + helperProcess.ErrorDataReceived += new DataReceivedEventHandler((sendingProcess, errLine) => Console.WriteLine($"stderr: \"{errLine.Data}\"")); + helperProcess.Start(); + helperProcess.BeginErrorReadLine(); + helperProcess.WaitForExit(); + if (helperProcess.ExitCode != 0) + return -1; + + var result = JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(helperProcess.StandardOutput)); + var pid = result["pid"]; + var handle = (IntPtr)result["handle"]; + var resultProcess = new ExistingProcess(handle); + Console.WriteLine("PID: {0}, Handle: {1}", pid, handle); + Console.WriteLine("Press Enter to force quit"); + Console.ReadLine(); + resultProcess.Kill(); + return 0; + } + + private static DalamudStartInfo AdjustStartInfo(DalamudStartInfo startInfo, string gamePath) + { + var ffxivDir = Path.GetDirectoryName(gamePath); + var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); + var gameVer = GameVersion.Parse(gameVerStr); + + return new() + { + WorkingDirectory = startInfo.WorkingDirectory, + ConfigurationPath = startInfo.ConfigurationPath, + PluginDirectory = startInfo.PluginDirectory, + DefaultPluginDirectory = startInfo.DefaultPluginDirectory, + AssetDirectory = startInfo.AssetDirectory, + Language = ClientLanguage.English, + GameVersion = gameVer, + DelayInitializeMs = startInfo.DelayInitializeMs, + }; } private static void Inject(Process process, DalamudStartInfo startInfo) { - var nethostName = "nethost.dll"; var bootName = "Dalamud.Boot.dll"; - - var nethostPath = Path.GetFullPath(nethostName); var bootPath = Path.GetFullPath(bootName); // ====================================================== - using var injector = new Injector(process); + using var injector = new Injector(process, false); - injector.LoadLibrary(nethostPath, out _); injector.LoadLibrary(bootPath, out var bootModule); // ====================================================== @@ -342,5 +666,73 @@ namespace Dalamud.Injector Log.Information("Done"); } + + [DllImport("Dalamud.Boot.dll")] + private static extern int RewriteRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath, [MarshalAs(UnmanagedType.LPWStr)] string loadInfoJson); + + /// + /// This routine appends the given argument to a command line such that + /// CommandLineToArgvW will return the argument string unchanged. Arguments + /// in a command line should be separated by spaces; this function does + /// not add these spaces. + /// + /// Taken from https://stackoverflow.com/questions/5510343/escape-command-line-arguments-in-c-sharp + /// and https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/. + /// + /// Supplies the argument to encode. + /// + /// Supplies an indication of whether we should quote the argument even if it + /// does not contain any characters that would ordinarily require quoting. + /// + private static string EncodeParameterArgument(string argument, bool force = false) + { + if (argument == null) throw new ArgumentNullException(nameof(argument)); + + // Unless we're told otherwise, don't quote unless we actually + // need to do so --- hopefully avoid problems if programs won't + // parse quotes properly + if (force == false + && argument.Length > 0 + && argument.IndexOfAny(" \t\n\v\"".ToCharArray()) == -1) + { + return argument; + } + + var quoted = new StringBuilder(); + quoted.Append('"'); + + var numberBackslashes = 0; + + foreach (var chr in argument) + { + switch (chr) + { + case '\\': + numberBackslashes++; + continue; + case '"': + // Escape all backslashes and the following + // double quotation mark. + quoted.Append('\\', (numberBackslashes * 2) + 1); + quoted.Append(chr); + break; + default: + // Backslashes aren't special here. + quoted.Append('\\', numberBackslashes); + quoted.Append(chr); + break; + } + + numberBackslashes = 0; + } + + // Escape all backslashes, but let the terminating + // double quotation mark we add below be interpreted + // as a metacharacter. + quoted.Append('\\', numberBackslashes * 2); + quoted.Append('"'); + + return quoted.ToString(); + } } } diff --git a/Dalamud.Injector/ExistingProcess.cs b/Dalamud.Injector/ExistingProcess.cs new file mode 100644 index 000000000..24ee3bc64 --- /dev/null +++ b/Dalamud.Injector/ExistingProcess.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.Reflection; + +using Microsoft.Win32.SafeHandles; + +namespace Dalamud.Injector; + +/// +/// Class representing an already held process handle. +/// +internal class ExistingProcess : Process +{ + /// + /// Initializes a new instance of the class. + /// + /// The existing held process handle. + public ExistingProcess(IntPtr handle) + { + this.SetHandle(handle); + } + + private void SetHandle(IntPtr handle) + { + var baseType = this.GetType().BaseType; + if (baseType == null) + return; + + var setProcessHandleMethod = baseType.GetMethod( + "SetProcessHandle", + BindingFlags.NonPublic | BindingFlags.Instance); + setProcessHandleMethod?.Invoke(this, new object[] { new SafeProcessHandle(handle, true) }); + } +} diff --git a/Dalamud.Injector/Injector.cs b/Dalamud.Injector/Injector.cs index 20a668047..e5664388e 100644 --- a/Dalamud.Injector/Injector.cs +++ b/Dalamud.Injector/Injector.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; @@ -26,6 +27,7 @@ namespace Dalamud.Injector internal sealed class Injector : IDisposable { private readonly Process targetProcess; + private readonly bool disposeTargetProcess; private readonly ExternalMemory extMemory; private readonly CircularBuffer circularBuffer; private readonly PrivateMemoryBuffer memoryBuffer; @@ -40,9 +42,11 @@ namespace Dalamud.Injector /// Initializes a new instance of the class. /// /// Process to inject. - public Injector(Process targetProcess) + /// Dispose given process on disposing self. + public Injector(Process targetProcess, bool disposeTargetProcess = true) { this.targetProcess = targetProcess; + this.disposeTargetProcess = disposeTargetProcess; this.extMemory = new ExternalMemory(targetProcess); this.circularBuffer = new CircularBuffer(4096, this.extMemory); @@ -66,7 +70,8 @@ namespace Dalamud.Injector { GC.SuppressFinalize(this); - this.targetProcess?.Dispose(); + if (this.disposeTargetProcess) + this.targetProcess?.Dispose(); this.circularBuffer?.Dispose(); this.memoryBuffer?.Dispose(); } @@ -83,23 +88,10 @@ namespace Dalamud.Injector if (lpParameter == IntPtr.Zero) throw new Exception("Unable to allocate LoadLibraryW parameter"); - Log.Verbose($"CreateRemoteThread: call 0x{this.loadLibraryShellPtr.ToInt64():X} with 0x{lpParameter.ToInt64():X}"); - - var threadHandle = CreateRemoteThread( - this.targetProcess.Handle, - IntPtr.Zero, - UIntPtr.Zero, - this.loadLibraryShellPtr, - lpParameter, - CreateThreadFlags.RunImmediately, - out _); - - _ = WaitForSingleObject(threadHandle, uint.MaxValue); - + this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err); address = this.extMemory.Read(this.loadLibraryRetPtr); - if (address == IntPtr.Zero) - throw new Exception($"Error calling LoadLibraryW with {modulePath}"); + throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})"); } /// @@ -113,25 +105,13 @@ namespace Dalamud.Injector var functionNamePtr = this.WriteNullTerminatedASCIIString(functionName); var getProcAddressParams = new GetProcAddressParams(module, functionNamePtr); var lpParameter = this.circularBuffer.Add(ref getProcAddressParams); - if (lpParameter == IntPtr.Zero) throw new Exception("Unable to allocate GetProcAddress parameter ptr"); - var threadHandle = CreateRemoteThread( - this.targetProcess.Handle, - IntPtr.Zero, - UIntPtr.Zero, - this.getProcAddressShellPtr, - lpParameter, - CreateThreadFlags.RunImmediately, - out _); - - _ = WaitForSingleObject(threadHandle, uint.MaxValue); - - this.extMemory.Read(this.getProcAddressRetPtr, out address); - + this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err); + address = this.extMemory.Read(this.getProcAddressRetPtr); if (address == IntPtr.Zero) - throw new Exception($"Error calling GetProcAddress with {functionName}"); + throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})"); } /// @@ -152,6 +132,9 @@ namespace Dalamud.Injector CreateThreadFlags.RunImmediately, out _); + if (threadHandle == IntPtr.Zero) + throw new Exception($"CreateRemoteThread failure: {Marshal.GetLastWin32Error()}"); + _ = WaitForSingleObject(threadHandle, uint.MaxValue); GetExitCodeThread(threadHandle, out exitCode); @@ -161,8 +144,10 @@ namespace Dalamud.Injector private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports) { - var offset = this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW"); - var functionAddr = kernel32Module.BaseAddress + (int)offset; + var getLastErrorAddr = kernel32Module.BaseAddress + (int)this.GetExportedFunctionOffset(kernel32Exports, "GetLastError"); + Log.Verbose($"GetLastError: 0x{getLastErrorAddr.ToInt64():X}"); + + var functionAddr = kernel32Module.BaseAddress + (int)this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW"); Log.Verbose($"LoadLibraryW: 0x{functionAddr.ToInt64():X}"); var functionPtr = this.memoryBuffer.Add(ref functionAddr); @@ -187,7 +172,9 @@ namespace Dalamud.Injector asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX. asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal], rax // asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space. - asm.ret(); // ret // Restore stack ptr. (Callee cleanup) + asm.mov(rax, (ulong)getLastErrorAddr); // mov rax, pfnGetLastError // Change return address to GetLastError. + asm.push(rax); // push rax // + asm.ret(); // ret // Jump to GetLastError. var bytes = this.Assemble(asm); this.loadLibraryShellPtr = this.memoryBuffer.Add(bytes); @@ -212,6 +199,9 @@ namespace Dalamud.Injector private void SetupGetProcAddress(ProcessModule kernel32Module, ExportFunction[] kernel32Exports) { + var getLastErrorAddr = kernel32Module.BaseAddress + (int)this.GetExportedFunctionOffset(kernel32Exports, "GetLastError"); + Log.Verbose($"GetLastError: 0x{getLastErrorAddr.ToInt64():X}"); + var offset = this.GetExportedFunctionOffset(kernel32Exports, "GetProcAddress"); var functionAddr = kernel32Module.BaseAddress + (int)offset; Log.Verbose($"GetProcAddress: 0x{functionAddr.ToInt64():X}"); @@ -240,7 +230,9 @@ namespace Dalamud.Injector asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal] // asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space. - asm.ret(); // ret // Restore stack ptr. (Callee cleanup) + asm.mov(rax, (ulong)getLastErrorAddr); // mov rax, pfnGetLastError // Change return address to GetLastError. + asm.push(rax); // push rax // + asm.ret(); // ret // Jump to GetLastError. var bytes = this.Assemble(asm); this.getProcAddressShellPtr = this.memoryBuffer.Add(bytes); diff --git a/Dalamud.Injector/NativeAclFix.cs b/Dalamud.Injector/NativeAclFix.cs new file mode 100644 index 000000000..d3798cfde --- /dev/null +++ b/Dalamud.Injector/NativeAclFix.cs @@ -0,0 +1,563 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading; + +using Serilog; + +// ReSharper disable InconsistentNaming + +namespace Dalamud.Injector +{ + /// + /// Class responsible for stripping ACL protections from processes. + /// + public static class NativeAclFix + { + /// + /// Start a process without ACL protections. + /// + /// The working directory. + /// The path to the executable file. + /// Arguments to pass to the executable file. + /// Action to execute before the process is started. + /// The started process. + /// Thrown when a win32 error occurs. + /// Thrown when the process did not start correctly. + public static Process LaunchGame(string workingDir, string exePath, string arguments, Action beforeResume) + { + Process process = null; + + var userName = Environment.UserName; + + var pExplicitAccess = default(PInvoke.EXPLICIT_ACCESS); + PInvoke.BuildExplicitAccessWithName( + ref pExplicitAccess, + userName, + PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE, + PInvoke.GRANT_ACCESS, + 0); + + if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!PInvoke.InitializeSecurityDescriptor(out var secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(secDesc, psecDesc, true); + + var lpProcessInformation = default(PInvoke.PROCESS_INFORMATION); + try + { + var lpProcessAttributes = new PInvoke.SECURITY_ATTRIBUTES + { + nLength = Marshal.SizeOf(), + lpSecurityDescriptor = psecDesc, + bInheritHandle = false, + }; + + var lpStartupInfo = new PInvoke.STARTUPINFO + { + cb = Marshal.SizeOf(), + }; + + var compatLayerPrev = Environment.GetEnvironmentVariable("__COMPAT_LAYER"); + + Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RunAsInvoker"); + try + { + if (!PInvoke.CreateProcess( + null, + $"\"{exePath}\" {arguments}", + ref lpProcessAttributes, + IntPtr.Zero, + false, + PInvoke.CREATE_SUSPENDED, + IntPtr.Zero, + workingDir, + ref lpStartupInfo, + out lpProcessInformation)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + finally + { + Environment.SetEnvironmentVariable("__COMPAT_LAYER", compatLayerPrev); + } + + DisableSeDebug(lpProcessInformation.hProcess); + + process = new ExistingProcess(lpProcessInformation.hProcess); + + beforeResume?.Invoke(process); + + PInvoke.ResumeThread(lpProcessInformation.hThread); + + // Ensure that the game main window is prepared + try + { + do + { + process.WaitForInputIdle(); + + Thread.Sleep(100); + } + while (TryFindGameWindow(process) == IntPtr.Zero); + } + catch (InvalidOperationException) + { + throw new GameExitedException(); + } + + if (PInvoke.GetSecurityInfo( + PInvoke.GetCurrentProcess(), + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, + IntPtr.Zero, + IntPtr.Zero, + out var pACL, + IntPtr.Zero, + IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (PInvoke.SetSecurityInfo( + lpProcessInformation.hProcess, + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION, + IntPtr.Zero, + IntPtr.Zero, + pACL, + IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + catch (Exception ex) + { + Log.Error(ex, "[NativeAclFix] Uncaught error during initialization, trying to kill process"); + + try + { + process?.Kill(); + } + catch (Exception killEx) + { + Log.Error(killEx, "[NativeAclFix] Could not kill process"); + } + + throw; + } + finally + { + Marshal.FreeHGlobal(psecDesc); + PInvoke.CloseHandle(lpProcessInformation.hThread); + } + + return process; + } + + private static void DisableSeDebug(IntPtr processHandle) + { + if (!PInvoke.OpenProcessToken(processHandle, PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, out var tokenHandle)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var luidDebugPrivilege = default(PInvoke.LUID); + if (!PInvoke.LookupPrivilegeValue(null, "SeDebugPrivilege", ref luidDebugPrivilege)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var requiredPrivileges = new PInvoke.PRIVILEGE_SET + { + PrivilegeCount = 1, + Control = PInvoke.PRIVILEGE_SET_ALL_NECESSARY, + Privilege = new PInvoke.LUID_AND_ATTRIBUTES[1], + }; + + requiredPrivileges.Privilege[0].Luid = luidDebugPrivilege; + requiredPrivileges.Privilege[0].Attributes = PInvoke.SE_PRIVILEGE_ENABLED; + + if (!PInvoke.PrivilegeCheck(tokenHandle, ref requiredPrivileges, out bool bResult)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // SeDebugPrivilege is enabled; try disabling it + if (bResult) + { + var tokenPrivileges = new PInvoke.TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1], + }; + + tokenPrivileges.Privileges[0].Luid = luidDebugPrivilege; + tokenPrivileges.Privileges[0].Attributes = PInvoke.SE_PRIVILEGE_REMOVED; + + if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + PInvoke.CloseHandle(tokenHandle); + } + + private static IntPtr TryFindGameWindow(Process process) + { + IntPtr hwnd = IntPtr.Zero; + while ((hwnd = PInvoke.FindWindowEx(IntPtr.Zero, hwnd, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) + { + PInvoke.GetWindowThreadProcessId(hwnd, out uint pid); + + if (pid == process.Id && PInvoke.IsWindowVisible(hwnd)) + { + break; + } + } + + return hwnd; + } + + /// + /// Exception thrown when the process has exited before a window could be found. + /// + public class GameExitedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public GameExitedException() + : base("Game exited prematurely.") + { + } + } + + // Definitions taken from PInvoke.net (with some changes) + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "WINAPI conventions")] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1121:Use built-in type alias", Justification = "WINAPI conventions")] + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1400:Access modifier should be declared", Justification = "WINAPI conventions")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "WINAPI conventions")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "WINAPI conventions")] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "WINAPI conventions")] + private static class PInvoke + { + #region Constants + public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000; + public const UInt32 SPECIFIC_RIGHTS_ALL = 0x0000FFFF; + public const UInt32 PROCESS_VM_WRITE = 0x0020; + + public const UInt32 GRANT_ACCESS = 1; + + public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1; + + public const UInt32 CREATE_SUSPENDED = 0x00000004; + + public const UInt32 TOKEN_QUERY = 0x0008; + public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020; + + public const UInt32 PRIVILEGE_SET_ALL_NECESSARY = 1; + + public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; + public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004; + + public enum MULTIPLE_TRUSTEE_OPERATION + { + NO_MULTIPLE_TRUSTEE, + TRUSTEE_IS_IMPERSONATE, + } + + public enum TRUSTEE_FORM + { + TRUSTEE_IS_SID, + TRUSTEE_IS_NAME, + TRUSTEE_BAD_FORM, + TRUSTEE_IS_OBJECTS_AND_SID, + TRUSTEE_IS_OBJECTS_AND_NAME, + } + + public enum TRUSTEE_TYPE + { + TRUSTEE_IS_UNKNOWN, + TRUSTEE_IS_USER, + TRUSTEE_IS_GROUP, + TRUSTEE_IS_DOMAIN, + TRUSTEE_IS_ALIAS, + TRUSTEE_IS_WELL_KNOWN_GROUP, + TRUSTEE_IS_DELETED, + TRUSTEE_IS_INVALID, + TRUSTEE_IS_COMPUTER, + } + + public enum SE_OBJECT_TYPE + { + SE_UNKNOWN_OBJECT_TYPE, + SE_FILE_OBJECT, + SE_SERVICE, + SE_PRINTER, + SE_REGISTRY_KEY, + SE_LMSHARE, + SE_KERNEL_OBJECT, + SE_WINDOW_OBJECT, + SE_DS_OBJECT, + SE_DS_OBJECT_ALL, + SE_PROVIDER_DEFINED_OBJECT, + SE_WMIGUID_OBJECT, + SE_REGISTRY_WOW64_32KEY, + } + + [Flags] + public enum SECURITY_INFORMATION + { + OWNER_SECURITY_INFORMATION = 1, + GROUP_SECURITY_INFORMATION = 2, + DACL_SECURITY_INFORMATION = 4, + SACL_SECURITY_INFORMATION = 8, + UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000, + UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000, + PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000, + } + #endregion + + #region Methods + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern void BuildExplicitAccessWithName( + ref EXPLICIT_ACCESS pExplicitAccess, + string pTrusteeName, + uint accessPermissions, + uint accessMode, + uint inheritance); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern int SetEntriesInAcl( + int cCountOfExplicitEntries, + ref EXPLICIT_ACCESS pListOfExplicitEntries, + IntPtr oldAcl, + out IntPtr newAcl); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool InitializeSecurityDescriptor( + out SECURITY_DESCRIPTOR pSecurityDescriptor, + uint dwRevision); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool SetSecurityDescriptorDacl( + ref SECURITY_DESCRIPTOR pSecurityDescriptor, + bool bDaclPresent, + IntPtr pDacl, + bool bDaclDefaulted); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + UInt32 dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint ResumeThread(IntPtr hThread); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken( + IntPtr processHandle, + UInt32 desiredAccess, + out IntPtr tokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool PrivilegeCheck( + IntPtr clientToken, + ref PRIVILEGE_SET requiredPrivileges, + out bool pfResult); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AdjustTokenPrivileges( + IntPtr tokenHandle, + bool disableAllPrivileges, + ref TOKEN_PRIVILEGES newState, + UInt32 bufferLengthInBytes, + IntPtr previousState, + UInt32 returnLengthInBytes); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern uint GetSecurityInfo( + IntPtr handle, + SE_OBJECT_TYPE objectType, + SECURITY_INFORMATION securityInfo, + IntPtr pSidOwner, + IntPtr pSidGroup, + out IntPtr pDacl, + IntPtr pSacl, + IntPtr pSecurityDescriptor); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern uint SetSecurityInfo( + IntPtr handle, + SE_OBJECT_TYPE objectType, + SECURITY_INFORMATION securityInfo, + IntPtr psidOwner, + IntPtr psidGroup, + IntPtr pDacl, + IntPtr pSacl); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetCurrentProcess(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, IntPtr windowTitle); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindowVisible(IntPtr hWnd); + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] + public struct TRUSTEE : IDisposable + { + public IntPtr pMultipleTrustee; + public MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation; + public TRUSTEE_FORM TrusteeForm; + public TRUSTEE_TYPE TrusteeType; + private IntPtr ptstrName; + + public string Name => Marshal.PtrToStringAuto(this.ptstrName) ?? string.Empty; + +#pragma warning disable CA1416 + + void IDisposable.Dispose() + { + if (this.ptstrName != IntPtr.Zero) Marshal.Release(this.ptstrName); + } + +#pragma warning restore CA1416 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] + public struct EXPLICIT_ACCESS + { + uint grfAccessPermissions; + uint grfAccessMode; + uint grfInheritance; + TRUSTEE Trustee; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_DESCRIPTOR + { + public byte Revision; + public byte Sbz1; + public UInt16 Control; + public IntPtr Owner; + public IntPtr Group; + public IntPtr Sacl; + public IntPtr Dacl; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwYSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public UInt32 dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public int nLength; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PRIVILEGE_SET + { + public UInt32 PrivilegeCount; + public UInt32 Control; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privilege; + } + + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public UInt32 PrivilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + #endregion + } + } +} diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index b9ed9f33d..f1749ae0a 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -364,6 +364,23 @@ namespace Dalamud.Injector StackSizeParamIsReservation = 0x10000, } + /// + /// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess. + /// + [Flags] + public enum DuplicateOptions : uint + { + /// + /// Closes the source handle. This occurs regardless of any error status returned. + /// + CloseSource = 0x00000001, + + /// + /// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle. + /// + SameAccess = 0x00000002, + } + /// /// PAGE_* from memoryapi. /// @@ -833,5 +850,65 @@ namespace Dalamud.Injector byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesWritten); + + /// + /// Duplicates an object handle. + /// + /// + /// A handle to the process with the handle to be duplicated. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// The handle to be duplicated. This is an open object handle that is valid in the context of the source process. + /// For a list of objects whose handles can be duplicated, see the following Remarks section. + /// + /// + /// A handle to the process that is to receive the duplicated handle. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. + /// + /// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively. + /// + /// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. + /// + /// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// Optional actions. + /// + /// + /// If the function succeeds, the return value is nonzero. + /// + /// If the function fails, the return value is zero. To get extended error information, call GetLastError. + /// + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle. + /// + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DuplicateHandle( + IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + out IntPtr lpTargetHandle, + uint dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, + DuplicateOptions dwOptions); } } diff --git a/Dalamud.sln.DotSettings b/Dalamud.sln.DotSettings index 4653085d4..690f1a7ac 100644 --- a/Dalamud.sln.DotSettings +++ b/Dalamud.sln.DotSettings @@ -47,12 +47,15 @@ True True True + True True True True True True True + True + True True True True diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 3232b7d24..55e831b42 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Linq; using Dalamud.Game.Text; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Style; using Newtonsoft.Json; using Serilog; @@ -149,6 +150,11 @@ namespace Dalamud.Configuration.Internal /// public int FontResolutionLevel { get; set; } = 2; + /// + /// Gets or sets a value indicating whether to disable font fallback notice. + /// + public bool DisableFontFallbackNotice { get; set; } = false; + /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. /// @@ -194,6 +200,11 @@ namespace Dalamud.Configuration.Internal /// public bool LogOpenAtStartup { get; set; } + /// + /// Gets or sets a value indicating whether or not the dev bar should open at startup. + /// + public bool DevBarOpenAtStartup { get; set; } + /// /// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. /// @@ -301,6 +312,42 @@ namespace Dalamud.Configuration.Internal /// public bool IsMbCollect { get; set; } = true; + /// + /// Gets the ISO 639-1 two-letter code for the language of the effective Dalamud display language. + /// + public string EffectiveLanguage + { + get + { + var languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); + try + { + if (string.IsNullOrEmpty(this.LanguageOverride)) + { + var currentUiLang = CultureInfo.CurrentUICulture; + + if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) + return currentUiLang.TwoLetterISOLanguageName; + else + return languages[0]; + } + else + { + return this.LanguageOverride; + } + } + catch (Exception) + { + return languages[0]; + } + } + } + + /// + /// Gets or sets a value indicating whether or not to show info on dev bar. + /// + public bool ShowDevBarInfo { get; set; } = true; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs index 2cb89915c..01ca64bdd 100644 --- a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs +++ b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs @@ -32,6 +32,11 @@ namespace Dalamud.Configuration.Internal /// public static bool DalamudWaitForDebugger { get; } = GetEnvironmentVariable("DALAMUD_WAIT_DEBUGGER"); + /// + /// Gets a value indicating whether or not Dalamud context menus should be disabled. + /// + public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU"); + private static bool GetEnvironmentVariable(string name) => bool.Parse(Environment.GetEnvironmentVariable(name) ?? "false"); } diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5d4a65cb5..711081a5f 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 6.3.0.18 + 6.4.0.6 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) @@ -65,15 +65,15 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 37cbcb473..f8e952d7b 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,15 +1,12 @@ using System; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; -using Dalamud.Game; using Dalamud.Logging.Internal; using Dalamud.Support; using Dalamud.Utility; diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 931ff8949..66c366734 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -243,11 +243,11 @@ namespace Dalamud.Game var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion) - + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count)); + + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); if (configuration.PrintPluginsWelcomeMsg) { - foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name)) + foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) { chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion)); } @@ -302,7 +302,7 @@ namespace Dalamud.Game { if (configuration.AutoUpdatePlugins) { - pluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); + PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info); } else diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 0d50c2002..44d81735b 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -1,9 +1,9 @@ using System; using System.Runtime.InteropServices; +using Dalamud.Data; using Dalamud.Game.ClientState.Aetherytes; using Dalamud.Game.ClientState.Buddy; -using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Fates; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.JobGauge; @@ -17,6 +17,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; using Serilog; namespace Dalamud.Game.ClientState @@ -32,6 +33,7 @@ namespace Dalamud.Game.ClientState private readonly Hook setupTerritoryTypeHook; private bool lastConditionNone = true; + private bool lastFramePvP = false; /// /// Initializes a new instance of the class. @@ -60,7 +62,7 @@ namespace Dalamud.Game.ClientState Service.Set(this.address); - Service.Set(this.address); + Service.Set(this.address); Service.Set(this.address); @@ -95,6 +97,16 @@ namespace Dalamud.Game.ClientState /// public event EventHandler Logout; + /// + /// Event that fires when a character is entering PvP. + /// + public event System.Action EnterPvP; + + /// + /// Event that fires when a character is leaving PvP. + /// + public event System.Action LeavePvP; + /// /// Event that gets fired when a duty is ready. /// @@ -125,12 +137,17 @@ namespace Dalamud.Game.ClientState /// public bool IsLoggedIn { get; private set; } + /// + /// Gets a value indicating whether or not the user is playing PvP. + /// + public bool IsPvP { get; private set; } + /// /// Enable this module. /// public void Enable() { - Service.Get().Enable(); + Service.Get().Enable(); Service.Get().Enable(); this.setupTerritoryTypeHook.Enable(); } @@ -141,7 +158,7 @@ namespace Dalamud.Game.ClientState void IDisposable.Dispose() { this.setupTerritoryTypeHook.Dispose(); - Service.Get().ExplicitDispose(); + Service.Get().ExplicitDispose(); Service.Get().ExplicitDispose(); Service.Get().Update -= this.FrameworkOnOnUpdateEvent; Service.Get().CfPop -= this.NetworkHandlersOnCfPop; @@ -164,8 +181,10 @@ namespace Dalamud.Game.ClientState private void FrameworkOnOnUpdateEvent(Framework framework) { - var condition = Service.Get(); + var condition = Service.Get(); var gameGui = Service.Get(); + var data = Service.Get(); + if (condition.Any() && this.lastConditionNone == true) { Log.Debug("Is login"); @@ -183,6 +202,26 @@ namespace Dalamud.Game.ClientState this.Logout?.Invoke(this, null); gameGui.ResetUiHideState(); } + + if (this.TerritoryType != 0) + { + var terriRow = data.GetExcelSheet()!.GetRow(this.TerritoryType); + this.IsPvP = terriRow?.Bg.RawString.StartsWith("ffxiv/pvp") ?? false; + } + + if (this.IsPvP != this.lastFramePvP) + { + this.lastFramePvP = this.IsPvP; + + if (this.IsPvP) + { + this.EnterPvP?.Invoke(); + } + else + { + this.LeavePvP?.Invoke(); + } + } } } } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/DRGGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/DRGGauge.cs index 26b9137c2..1003d2cd5 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/DRGGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/DRGGauge.cs @@ -1,7 +1,5 @@ using System; -using Dalamud.Game.ClientState.JobGauge.Enums; - namespace Dalamud.Game.ClientState.JobGauge.Types { /// diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 9d70592f7..64e6fda64 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -7,7 +7,6 @@ using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory; -using JetBrains.Annotations; namespace Dalamud.Game.ClientState.Party { diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs index 7bcae5045..87ebb6aa7 100644 --- a/Dalamud/Game/FrameworkAddressResolver.cs +++ b/Dalamud/Game/FrameworkAddressResolver.cs @@ -1,8 +1,6 @@ using System; using System.Runtime.InteropServices; -using Dalamud.Game.Internal; - namespace Dalamud.Game { /// diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index 07c154f1f..f11f8b2f0 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -1,7 +1,5 @@ using System; -using Dalamud.Game.Internal; - namespace Dalamud.Game.Gui { /// diff --git a/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs index 44307b48a..90917fd0d 100644 --- a/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Dalamud.Game.Gui.ContextMenus.OldStructs; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging; using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -38,7 +40,7 @@ namespace Dalamud.Game.Gui.ContextMenus #endregion - private unsafe AgentContextInterface* currentAgentContextInterface; + private unsafe OldAgentContextInterface* currentAgentContextInterface; private IntPtr currentSubContextMenuTitle; @@ -67,15 +69,15 @@ namespace Dalamud.Game.Gui.ContextMenus #region Delegates - private unsafe delegate bool OpenSubContextMenuDelegate(AgentContext* agentContext); + private unsafe delegate bool OpenSubContextMenuDelegate(OldAgentContext* agentContext); - private unsafe delegate IntPtr ContextMenuOpeningDelegate(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, AgentContextInterface* agentContextInterface, IntPtr a7, ushort a8); + private unsafe delegate IntPtr ContextMenuOpeningDelegate(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, OldAgentContextInterface* agentContextInterface, IntPtr a7, ushort a8); private unsafe delegate bool ContextMenuOpenedDelegate(AddonContextMenu* addonContextMenu, int menuSize, AtkValue* atkValueArgs); private unsafe delegate bool ContextMenuItemSelectedDelegate(AddonContextMenu* addonContextMenu, int selectedIndex, byte a3); - private unsafe delegate bool SubContextMenuOpeningDelegate(AgentContext* agentContext); + private unsafe delegate bool SubContextMenuOpeningDelegate(OldAgentContext* agentContext); #endregion @@ -108,7 +110,7 @@ namespace Dalamud.Game.Gui.ContextMenus this.subContextMenuOpenedHook.Enable(); } - private static unsafe bool IsInventoryContext(AgentContextInterface* agentContextInterface) + private static unsafe bool IsInventoryContext(OldAgentContextInterface* agentContextInterface) { return agentContextInterface == AgentInventoryContext.Instance(); } @@ -121,7 +123,7 @@ namespace Dalamud.Game.Gui.ContextMenus } } - private unsafe IntPtr ContextMenuOpeningDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, AgentContextInterface* agentContextInterface, IntPtr a7, ushort a8) + private unsafe IntPtr ContextMenuOpeningDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, OldAgentContextInterface* agentContextInterface, IntPtr a7, ushort a8) { this.currentAgentContextInterface = agentContextInterface; return this.contextMenuOpeningHook!.Original(a1, a2, a3, a4, a5, agentContextInterface, a7, a8); @@ -182,6 +184,9 @@ namespace Dalamud.Game.Gui.ContextMenus // TODO: For inventory sub context menus, we take only the last item -- the return item. // This is because we're doing a hack to spawn a Second Tier sub context menu and then appropriating it. var contextMenuItems = contextMenuReaderWriter.Read(); + if (contextMenuItems == null) + return; + if (IsInventoryContext(this.currentAgentContextInterface) && this.selectedOpenSubContextMenuItem != null) { contextMenuItems = contextMenuItems.TakeLast(1).ToArray(); @@ -212,12 +217,12 @@ namespace Dalamud.Game.Gui.ContextMenus } } - private unsafe bool SubContextMenuOpeningDetour(AgentContext* agentContext) + private unsafe bool SubContextMenuOpeningDetour(OldAgentContext* agentContext) { return this.SubContextMenuOpeningImplementation(agentContext) || this.subContextMenuOpeningHook.Original(agentContext); } - private unsafe bool SubContextMenuOpeningImplementation(AgentContext* agentContext) + private unsafe bool SubContextMenuOpeningImplementation(OldAgentContext* agentContext) { if (this.openSubContextMenu == null || this.selectedOpenSubContextMenuItem == null) { @@ -274,7 +279,7 @@ namespace Dalamud.Game.Gui.ContextMenus this.ContextMenuOpenedImplementation(addonContextMenu, ref atkValueCount, ref atkValues); } - private unsafe ContextMenuOpenedArgs? NotifyContextMenuOpened(AddonContextMenu* addonContextMenu, AgentContextInterface* agentContextInterface, string? title, ContextMenus.ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable initialContextMenuItems) + private unsafe ContextMenuOpenedArgs? NotifyContextMenuOpened(AddonContextMenu* addonContextMenu, OldAgentContextInterface* agentContextInterface, string? title, ContextMenus.ContextMenuOpenedDelegate contextMenuOpenedDelegate, IEnumerable initialContextMenuItems) { var parentAddonName = this.GetParentAddonName(&addonContextMenu->AtkUnitBase); @@ -285,11 +290,11 @@ namespace Dalamud.Game.Gui.ContextMenus if (IsInventoryContext(agentContextInterface)) { var agentInventoryContext = (AgentInventoryContext*)agentContextInterface; - inventoryItemContext = new InventoryItemContext(agentInventoryContext->InventoryItemId, agentInventoryContext->InventoryItemCount, agentInventoryContext->InventoryItemIsHighQuality); + inventoryItemContext = new InventoryItemContext(agentInventoryContext->TargetDummyItem.ItemID, agentInventoryContext->TargetDummyItem.Quantity, agentInventoryContext->TargetDummyItem.Flags.HasFlag(InventoryItem.ItemFlags.HQ)); } else { - var agentContext = (AgentContext*)agentContextInterface; + var agentContext = (OldAgentContext*)agentContextInterface; uint? id = agentContext->GameObjectId; if (id == 0) @@ -399,6 +404,9 @@ namespace Dalamud.Game.Gui.ContextMenus // Read the selected item directly from the game ContextMenuReaderWriter contextMenuReaderWriter = new ContextMenuReaderWriter(this.currentAgentContextInterface, addonContextMenu->AtkValuesCount, addonContextMenu->AtkValues); var gameContextMenuItems = contextMenuReaderWriter.Read(); + if (gameContextMenuItems == null) + return; + var gameSelectedItem = gameContextMenuItems.ElementAtOrDefault(selectedIndex); // This should be impossible diff --git a/Dalamud/Game/Gui/ContextMenus/ContextMenuOpenedArgs.cs b/Dalamud/Game/Gui/ContextMenus/ContextMenuOpenedArgs.cs index 05293bacd..0924dcf54 100644 --- a/Dalamud/Game/Gui/ContextMenus/ContextMenuOpenedArgs.cs +++ b/Dalamud/Game/Gui/ContextMenus/ContextMenuOpenedArgs.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Dalamud.Game.Gui.ContextMenus.OldStructs; using Dalamud.Game.Text.SeStringHandling; using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; namespace Dalamud.Game.Gui.ContextMenus { @@ -18,7 +19,7 @@ namespace Dalamud.Game.Gui.ContextMenus /// The agent associated with the context menu. /// The the name of the parent addon associated with the context menu. /// The items in the context menu. - public ContextMenuOpenedArgs(AddonContextMenu* addon, AgentContextInterface* agent, string? parentAddonName, IEnumerable items) + public ContextMenuOpenedArgs(AddonContextMenu* addon, OldAgentContextInterface* agent, string? parentAddonName, IEnumerable items) { this.Addon = addon; this.Agent = agent; @@ -34,7 +35,7 @@ namespace Dalamud.Game.Gui.ContextMenus /// /// Gets the agent associated with the context menu. /// - public AgentContextInterface* Agent { get; } + public OldAgentContextInterface* Agent { get; } /// /// Gets the name of the parent addon associated with the context menu. @@ -46,11 +47,6 @@ namespace Dalamud.Game.Gui.ContextMenus /// public string? Title { get; init; } - /// - /// Gets the items in the context menu. - /// - public List Items { get; } - /// /// Gets the game object context associated with the context menu. /// @@ -61,6 +57,11 @@ namespace Dalamud.Game.Gui.ContextMenus /// public InventoryItemContext? InventoryItemContext { get; init; } + /// + /// Gets the items in the context menu. + /// + internal List Items { get; } + /// /// Append a custom context menu item to this context menu. /// @@ -75,7 +76,12 @@ namespace Dalamud.Game.Gui.ContextMenus /// /// The name of the submenu. /// The action to be executed once opened. - public void AddCustomSubMenu(SeString name, ContextMenuOpenedDelegate opened) => + public void AddCustomSubMenu(SeString name, ContextMenuOpenedDelegate opened) + { + if (this.GameObjectContext != null) + throw new Exception("Submenus on GameObjects are not supported yet."); + this.Items.Add(new OpenSubContextMenuItem(name, opened)); + } } } diff --git a/Dalamud/Game/Gui/ContextMenus/ContextMenuReaderWriter.cs b/Dalamud/Game/Gui/ContextMenus/ContextMenuReaderWriter.cs index 16f24365d..633c6560b 100644 --- a/Dalamud/Game/Gui/ContextMenus/ContextMenuReaderWriter.cs +++ b/Dalamud/Game/Gui/ContextMenus/ContextMenuReaderWriter.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using Dalamud.Game.Gui.ContextMenus.OldStructs; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Logging; using Dalamud.Memory; @@ -19,7 +20,7 @@ namespace Dalamud.Game.Gui.ContextMenus /// internal unsafe class ContextMenuReaderWriter { - private readonly AgentContextInterface* agentContextInterface; + private readonly OldAgentContextInterface* agentContextInterface; private int atkValueCount; private AtkValue* atkValues; @@ -30,7 +31,7 @@ namespace Dalamud.Game.Gui.ContextMenus /// The AgentContextInterface to act upon. /// The number of ATK values to consider. /// Pointer to the array of ATK values. - public ContextMenuReaderWriter(AgentContextInterface* agentContextInterface, int atkValueCount, AtkValue* atkValues) + public ContextMenuReaderWriter(OldAgentContextInterface* agentContextInterface, int atkValueCount, AtkValue* atkValues) { PluginLog.Warning($"{(IntPtr)atkValues:X}"); @@ -277,7 +278,7 @@ namespace Dalamud.Game.Gui.ContextMenus /// Read the context menu from the agent. /// /// Read menu items. - public GameContextMenuItem[] Read() + public GameContextMenuItem[]? Read() { var gameContextMenuItems = new List(); for (var contextMenuItemIndex = 0; contextMenuItemIndex < this.ContextMenuItemCount; contextMenuItemIndex++) @@ -306,18 +307,23 @@ namespace Dalamud.Game.Gui.ContextMenus byte action; if (this.IsInventoryContext) { - var actions = &((AgentInventoryContext*)this.agentContextInterface)->Actions; + var actions = &((OldAgentInventoryContext*)this.agentContextInterface)->Actions; action = *(actions + contextMenuItemAtkValueBaseIndex); } else if (this.StructLayout is SubContextMenuStructLayout.Alternate) { - var redButtonActions = &((AgentContext*)this.agentContextInterface)->Items->RedButtonActions; + var redButtonActions = &((OldAgentContext*)this.agentContextInterface)->Items->RedButtonActions; action = (byte)*(redButtonActions + contextMenuItemIndex); } + else if (((OldAgentContext*)this.agentContextInterface)->Items != null) + { + var actions = &((OldAgentContext*)this.agentContextInterface)->Items->Actions; + action = *(actions + contextMenuItemAtkValueBaseIndex); + } else { - var actions = &((AgentContext*)this.agentContextInterface)->Items->Actions; - action = *(actions + contextMenuItemAtkValueBaseIndex); + PluginLog.Warning("Context Menu action failed, Items pointer was unexpectedly null."); + return null; } // Get the has previous indicator flag @@ -438,28 +444,33 @@ namespace Dalamud.Game.Gui.ContextMenus if (this.IsInventoryContext) { - var actions = &((AgentInventoryContext*)this.agentContextInterface)->Actions; + var actions = &((OldAgentInventoryContext*)this.agentContextInterface)->Actions; *(actions + this.FirstContextMenuItemIndex + contextMenuItemIndex) = action; } else if (this.StructLayout is SubContextMenuStructLayout.Alternate && this.FirstUnhandledAction != null) { // Some weird placeholder goes here - var actions = &((AgentContext*)this.agentContextInterface)->Items->Actions; + var actions = &((OldAgentContext*)this.agentContextInterface)->Items->Actions; *(actions + this.FirstContextMenuItemIndex + contextMenuItemIndex) = (byte)(this.FirstUnhandledAction.Value + contextMenuItemIndex); // Make sure there's one of these function pointers for every item. // The function needs to be the same, so we just copy the first one into every index. - var unkFunctionPointers = &((AgentContext*)this.agentContextInterface)->Items->UnkFunctionPointers; + var unkFunctionPointers = &((OldAgentContext*)this.agentContextInterface)->Items->UnkFunctionPointers; *(unkFunctionPointers + this.FirstContextMenuItemIndex + contextMenuItemIndex) = *(unkFunctionPointers + this.FirstContextMenuItemIndex); // The real action goes here - var redButtonActions = &((AgentContext*)this.agentContextInterface)->Items->RedButtonActions; + var redButtonActions = &((OldAgentContext*)this.agentContextInterface)->Items->RedButtonActions; *(redButtonActions + contextMenuItemIndex) = action; } + else if (((OldAgentContext*)this.agentContextInterface)->Items != null) + { + // TODO: figure out why this branch is reached on inventory contexts and why Items is sometimes null. + var actions = &((OldAgentContext*)this.agentContextInterface)->Items->Actions; + *(actions + this.FirstContextMenuItemIndex + contextMenuItemIndex) = action; + } else { - var actions = &((AgentContext*)this.agentContextInterface)->Items->Actions; - *(actions + this.FirstContextMenuItemIndex + contextMenuItemIndex) = action; + PluginLog.Warning("Context Menu action failed, Items pointer was unexpectedly null."); } if (contextMenuItem.Indicator == ContextMenuItemIndicator.Previous) diff --git a/Dalamud/Game/Gui/ContextMenus/GameObjectContext.cs b/Dalamud/Game/Gui/ContextMenus/GameObjectContext.cs index a6280dde5..076888f6a 100644 --- a/Dalamud/Game/Gui/ContextMenus/GameObjectContext.cs +++ b/Dalamud/Game/Gui/ContextMenus/GameObjectContext.cs @@ -1,6 +1,4 @@ -using Dalamud.Game.Text.SeStringHandling; - -namespace Dalamud.Game.Gui.ContextMenus +namespace Dalamud.Game.Gui.ContextMenus { /// /// Provides game object context to a context menu. diff --git a/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContext.cs b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContext.cs new file mode 100644 index 000000000..d0c59b616 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContext.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenus.OldStructs; + +// TODO: This is transplanted from client structs before the rework. Need to take some time to sort all of this out soon. + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct OldAgentContext +{ + public static OldAgentContext* Instance() => (OldAgentContext*)FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Context); + + [FieldOffset(0x0)] public AgentInterface AgentInterface; + [FieldOffset(0x0)] public OldAgentContextInterface AgentContextInterface; + [FieldOffset(0xD18)] public unsafe OldAgentContextMenuItems* Items; + [FieldOffset(0xE08)] public Utf8String GameObjectName; + [FieldOffset(0xEE0)] public ulong GameObjectContentId; + [FieldOffset(0xEF0)] public uint GameObjectId; + [FieldOffset(0xF00)] public ushort GameObjectWorldId; +} + +[StructLayout(LayoutKind.Explicit)] +public struct OldAgentContextMenuItems +{ + [FieldOffset(0x0)] public ushort AtkValueCount; + [FieldOffset(0x8)] public AtkValue AtkValues; + [FieldOffset(0x428)] public byte Actions; + [FieldOffset(0x450)] public ulong UnkFunctionPointers; + [FieldOffset(0x598)] public ulong RedButtonActions; +} diff --git a/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContextInterface.cs b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContextInterface.cs new file mode 100644 index 000000000..2dde22dfb --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentContextInterface.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenus.OldStructs; + +// TODO: This is transplanted from client structs before the rework. Need to take some time to sort all of this out soon. + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct OldAgentContextInterface +{ + [FieldOffset(0x0)] public AgentInterface AgentInterface; + [FieldOffset(0x670)] public unsafe byte SelectedIndex; + [FieldOffset(0x690)] public byte* Unk1; + [FieldOffset(0xD08)] public byte* SubContextMenuTitle; + [FieldOffset(0x1740)] public bool IsSubContextMenu; +} diff --git a/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentInventoryContext.cs b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentInventoryContext.cs new file mode 100644 index 000000000..56fbbac97 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenus/OldStructs/OldAgentInventoryContext.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenus.OldStructs; + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct OldAgentInventoryContext +{ + public static OldAgentInventoryContext* Instance() => (OldAgentInventoryContext*) FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.InventoryContext); + + [FieldOffset(0x0)] public AgentInterface AgentInterface; + [FieldOffset(0x0)] public OldAgentContextInterface AgentContextInterface; + [FieldOffset(0x2C)] public uint FirstContextMenuItemAtkValueIndex; + [FieldOffset(0x30)] public uint ContextMenuItemCount; + [FieldOffset(0x38)] public AtkValue AtkValues; + [FieldOffset(0x558)] public unsafe byte Actions; + [FieldOffset(0x5A8)] public uint UnkFlags; + [FieldOffset(0x5B0)] public uint PositionX; + [FieldOffset(0x5B4)] public uint PositionY; + [FieldOffset(0x5F8)] public uint InventoryItemId; + [FieldOffset(0x5FC)] public uint InventoryItemCount; + [FieldOffset(0x604)] public bool InventoryItemIsHighQuality; +} diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 94e195ef0..6ed963bf1 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -15,6 +15,8 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using Serilog; @@ -31,7 +33,6 @@ namespace Dalamud.Game.Gui private readonly GetMatrixSingletonDelegate getMatrixSingleton; private readonly ScreenToWorldNativeDelegate screenToWorldNative; - private readonly GetAgentModuleDelegate getAgentModule; private readonly Hook setGlobalBgmHook; private readonly Hook handleItemHoverHook; @@ -60,7 +61,6 @@ namespace Dalamud.Game.Gui Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}"); Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}"); Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}"); - Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}"); Service.Set(new ChatGui(this.address.ChatManager)); Service.Set(); @@ -85,8 +85,6 @@ namespace Dalamud.Game.Gui this.toggleUiHideHook = new Hook(this.address.ToggleUiHide, this.ToggleUiHideDetour); - this.getAgentModule = Marshal.GetDelegateForFunctionPointer(this.address.GetAgentModule); - this.utf8StringFromSequenceHook = new Hook(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); } @@ -98,8 +96,6 @@ namespace Dalamud.Game.Gui [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown); - private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule); - // Hooked delegates [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -390,40 +386,36 @@ namespace Dalamud.Game.Gui /// /// Find the agent associated with an addon, if possible. /// - /// The addon address. + /// The addon address. /// A pointer to the agent interface. - public unsafe IntPtr FindAgentInterface(IntPtr addon) + public unsafe IntPtr FindAgentInterface(IntPtr addonPtr) { - if (addon == IntPtr.Zero) + if (addonPtr == IntPtr.Zero) return IntPtr.Zero; - var uiModule = Service.Get().GetUIModule(); - if (uiModule == IntPtr.Zero) + var uiModule = (UIModule*)this.GetUIModule(); + if (uiModule == null) + return IntPtr.Zero; + + var agentModule = uiModule->GetAgentModule(); + if (agentModule == null) + return IntPtr.Zero; + + var addon = (AtkUnitBase*)addonPtr; + var addonId = addon->ParentID == 0 ? addon->ID : addon->ParentID; + + if (addonId == 0) + return IntPtr.Zero; + + var index = 0; + while (true) { - return IntPtr.Zero; - } + var agent = agentModule->GetAgentByInternalID((uint)index++); + if (agent == uiModule || agent == null) + break; - var agentModule = this.getAgentModule(uiModule); - if (agentModule == IntPtr.Zero) - { - return IntPtr.Zero; - } - - var unitBase = (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)addon; - var id = unitBase->ParentID; - if (id == 0) - id = unitBase->IDu; - - if (id == 0) - return IntPtr.Zero; - - for (var i = 0; i < 380; i++) - { - var agent = Marshal.ReadIntPtr(agentModule, 0x20 + (i * 8)); - if (agent == IntPtr.Zero) - continue; - if (Marshal.ReadInt32(agent, 0x20) == id) - return agent; + if (agent->AddonId == addonId) + return new IntPtr(agent); } return IntPtr.Zero; @@ -445,13 +437,8 @@ namespace Dalamud.Game.Gui Service.Get().Enable(); Service.Get().Enable(); - // TODO(goat): Remove when stable - var config = Service.Get(); - if (config.DalamudBetaKey == DalamudConfiguration.DalamudCurrentBetaKey) - { - Log.Warning("TAKE CARE!!! You are using Dalamud Testing, so the new context menu feature is enabled.\nThis may cause crashes with unupdated plugins."); + if (EnvironmentConfiguration.DalamudDoContextMenu) Service.Get().Enable(); - } this.setGlobalBgmHook.Enable(); this.handleItemHoverHook.Enable(); diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index adeaab1af..c81f9fd5c 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace Dalamud.Game.Gui { @@ -71,11 +70,6 @@ namespace Dalamud.Game.Gui /// public IntPtr ToggleUiHide { get; private set; } - /// - /// Gets the address of the native GetAgentModule method. - /// - public IntPtr GetAgentModule { get; private set; } - /// /// Gets the address of the native Utf8StringFromSequence method. /// @@ -94,9 +88,6 @@ namespace Dalamud.Game.Gui this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1"); this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??"); this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8"); - - var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28"); - this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size); } /// diff --git a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs index c39822eb7..67469f6aa 100644 --- a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs +++ b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Data; using Lumina.Excel.GeneratedSheets; diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index a39cbe998..111d1364c 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -138,18 +138,18 @@ namespace Dalamud.Game.Internal return; } - // the max size (hardcoded) is 0xE/15, but the system menu currently uses 0xC/12 + // the max size (hardcoded) is 0x11/17, but the system menu currently uses 0xC/12 // this is a just in case that doesnt really matter // see if we can add 2 entries - if (menuSize >= 0xD) + if (menuSize >= 0x11) { this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize); return; } // atkValueArgs is actually an array of AtkValues used as args. all their UI code works like this. - // in this case, menu size is stored in atkValueArgs[4], and the next 15 slots are the MainCommand - // the 15 slots after that, if they exist, are the entry names, but they are otherwise pulled from MainCommand EXD + // in this case, menu size is stored in atkValueArgs[4], and the next 17 slots are the MainCommand + // the 17 slots after that, if they exist, are the entry names, but they are otherwise pulled from MainCommand EXD // reference the original function for more details :) // step 1) move all the current menu items down so we can put Dalamud at the top like it deserves @@ -173,9 +173,9 @@ namespace Dalamud.Game.Internal // step 3) create strings for them // since the game first checks for strings in the AtkValue argument before pulling them from the exd, if we create strings we dont have to worry // about hooking the exd reader, thank god - var firstStringEntry = &atkValueArgs[5 + 15]; + var firstStringEntry = &atkValueArgs[5 + 17]; this.atkValueChangeType(firstStringEntry, ValueType.String); - var secondStringEntry = &atkValueArgs[6 + 15]; + var secondStringEntry = &atkValueArgs[6 + 17]; this.atkValueChangeType(secondStringEntry, ValueType.String); const int color = 539; diff --git a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs index 4d30a1e74..02b031b44 100644 --- a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs +++ b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs @@ -1,7 +1,5 @@ using System; -using Dalamud.Game.Internal; - namespace Dalamud.Game.Libc { /// diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index 130986197..565a1e2b9 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,7 +1,5 @@ using System; -using Dalamud.Game.Internal; - namespace Dalamud.Game.Network { /// diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index 82f2f0d91..2abe4a326 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -3,7 +3,6 @@ using System.Net.Sockets; using System.Runtime.InteropServices; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; namespace Dalamud.Game.Network.Internal { diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/EmphasisItalicPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/EmphasisItalicPayload.cs index b6c3bbd76..0a61f5ef3 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/EmphasisItalicPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/EmphasisItalicPayload.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index 8ab3d2484..06e4dfc76 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs index 16f2b4209..68403b39e 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Data; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.IoC; using Dalamud.IoC.Internal; using Lumina.Excel.GeneratedSheets; diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index a61e366f8..9c1cbaa06 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using Dalamud.Configuration.Internal; diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index d9237c156..9cc73da3b 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -4,11 +4,9 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; using Dalamud.Memory; using Iced.Intel; -using Microsoft.Win32; namespace Dalamud.Hooking.Internal { diff --git a/Dalamud/Interface/Colors/ImGuiColors.cs b/Dalamud/Interface/Colors/ImGuiColors.cs index a8cb2f6fb..e1a3356fa 100644 --- a/Dalamud/Interface/Colors/ImGuiColors.cs +++ b/Dalamud/Interface/Colors/ImGuiColors.cs @@ -45,12 +45,12 @@ namespace Dalamud.Interface.Colors /// /// Gets yellow used in dalamud. /// - public static Vector4 DalamudYellow { get; } = new(1f, 1f, .4f, 1f); + public static Vector4 DalamudYellow { get; internal set; } = new(1f, 1f, .4f, 1f); /// /// Gets violet used in dalamud. /// - public static Vector4 DalamudViolet { get; } = new(0.770f, 0.700f, 0.965f, 1.000f); + public static Vector4 DalamudViolet { get; internal set; } = new(0.770f, 0.700f, 0.965f, 1.000f); /// /// Gets tank blue (UIColor37). @@ -70,36 +70,36 @@ namespace Dalamud.Interface.Colors /// /// Gets parsed grey. /// - public static Vector4 ParsedGrey { get; } = new(0.4f, 0.4f, 0.4f, 1f); + public static Vector4 ParsedGrey { get; internal set; } = new(0.4f, 0.4f, 0.4f, 1f); /// /// Gets parsed green. /// - public static Vector4 ParsedGreen { get; } = new(0.117f, 1f, 0f, 1f); + public static Vector4 ParsedGreen { get; internal set; } = new(0.117f, 1f, 0f, 1f); /// /// Gets parsed blue. /// - public static Vector4 ParsedBlue { get; } = new(0f, 0.439f, 1f, 1f); + public static Vector4 ParsedBlue { get; internal set; } = new(0f, 0.439f, 1f, 1f); /// /// Gets parsed purple. /// - public static Vector4 ParsedPurple { get; } = new(0.639f, 0.207f, 0.933f, 1f); + public static Vector4 ParsedPurple { get; internal set; } = new(0.639f, 0.207f, 0.933f, 1f); /// /// Gets parsed orange. /// - public static Vector4 ParsedOrange { get; } = new(1f, 0.501f, 0f, 1f); + public static Vector4 ParsedOrange { get; internal set; } = new(1f, 0.501f, 0f, 1f); /// /// Gets parsed pink. /// - public static Vector4 ParsedPink { get; } = new(0.886f, 0.407f, 0.658f, 1f); + public static Vector4 ParsedPink { get; internal set; } = new(0.886f, 0.407f, 0.658f, 1f); /// /// Gets parsed gold. /// - public static Vector4 ParsedGold { get; } = new(0.898f, 0.8f, 0.501f, 1f); + public static Vector4 ParsedGold { get; internal set; } = new(0.898f, 0.8f, 0.501f, 1f); } } diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs index 5d28041b9..155766ded 100644 --- a/Dalamud/Interface/GameFonts/FdtReader.cs +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; namespace Dalamud.Interface.GameFonts { diff --git a/Dalamud/Interface/GameFonts/GameFontFamily.cs b/Dalamud/Interface/GameFonts/GameFontFamily.cs index 2aa836927..cb3e84a59 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamily.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamily.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Dalamud.Interface.GameFonts { /// diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs index 482ef22e2..93fe5ab87 100644 --- a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs +++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; -using System.Text; -using System.Threading.Tasks; using ImGuiNET; diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index a376f508b..ae45b7226 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using System.Text; -using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Interface.Internal; using ImGuiNET; @@ -189,6 +189,8 @@ namespace Dalamud.Interface.GameFonts font->FontSize /= fontScale; font->Ascent /= fontScale; font->Descent /= fontScale; + if (font->ConfigData != null) + font->ConfigData->SizePixels /= fontScale; var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) { @@ -227,7 +229,10 @@ namespace Dalamud.Interface.GameFonts } if (needRebuild) + { + Log.Information("[GameFontManager] Calling RebuildFonts because {0} has been requested.", style.ToString()); this.interfaceManager.RebuildFonts(); + } return new(this, style); } @@ -285,7 +290,8 @@ namespace Dalamud.Interface.GameFonts /// /// Build fonts before plugins do something more. To be called from InterfaceManager. /// - public void BuildFonts() + /// Whether to load fonts in minimum sizes. + public void BuildFonts(bool forceMinSize) { unsafe { @@ -303,7 +309,7 @@ namespace Dalamud.Interface.GameFonts { var rectIds = this.glyphRectIds[style] = new(); - var fdt = this.fdts[(int)style.FamilyAndSize]; + var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; if (fdt == null) continue; @@ -316,7 +322,7 @@ namespace Dalamud.Interface.GameFonts if (c < 32 || c >= 0xFFFF) continue; - var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); rectIds[c] = Tuple.Create( io.Fonts.AddCustomRectFontGlyph( font, @@ -336,7 +342,8 @@ namespace Dalamud.Interface.GameFonts /// /// Post-build fonts before plugins do something more. To be called from InterfaceManager. /// - public unsafe void AfterBuildFonts() + /// Whether to load fonts in minimum sizes. + public unsafe void AfterBuildFonts(bool forceMinSize) { var ioFonts = ImGui.GetIO().Fonts; ioFonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); @@ -345,9 +352,12 @@ namespace Dalamud.Interface.GameFonts foreach (var (style, font) in this.fonts) { - var fdt = this.fdts[(int)style.FamilyAndSize]; + var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)]; + var scale = style.SizePt / fdt.FontHeader.Size; var fontPtr = font.NativePtr; - fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; + fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; + if (fontPtr->ConfigData != null) + fontPtr->ConfigData->SizePixels = fontPtr->FontSize; fontPtr->Ascent = fdt.FontHeader.Ascent; fontPtr->Descent = fdt.FontHeader.Descent; fontPtr->EllipsisChar = '…'; @@ -361,11 +371,18 @@ namespace Dalamud.Interface.GameFonts } } - fixed (char* c = FontNames[(int)style.FamilyAndSize]) + // I have no idea what's causing NPE, so just to be safe + try { - for (var j = 0; j < 40; j++) - fontPtr->ConfigData->Name[j] = 0; - Encoding.UTF8.GetBytes(c, FontNames[(int)style.FamilyAndSize].Length, fontPtr->ConfigData->Name, 40); + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing } foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) @@ -373,7 +390,7 @@ namespace Dalamud.Interface.GameFonts var rc = ioFonts.GetCustomRectByIndex(rectId); var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); if (widthAdjustment == 0) { for (var y = 0; y < glyph.BoundingHeight; y++) @@ -399,10 +416,10 @@ namespace Dalamud.Interface.GameFonts for (var y = 0; y < glyph.BoundingHeight; y++) { float xDelta = xbold; - if (style.SkewStrength > 0) - xDelta += style.SkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.SkewStrength < 0) - xDelta -= style.SkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; + if (style.BaseSkewStrength > 0) + xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; + else if (style.BaseSkewStrength < 0) + xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; var xDeltaInt = (int)Math.Floor(xDelta); var xness = xDelta - xDeltaInt; for (var x = 0; x < glyph.BoundingWidth; x++) @@ -431,11 +448,9 @@ namespace Dalamud.Interface.GameFonts } } } - } - foreach (var font in this.fonts.Values) - { CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); + UnscaleFont(font, 1 / scale, false); font.BuildLookupTable(); } } diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 8636f128e..6ec078d69 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dalamud.Interface.GameFonts { @@ -16,6 +12,11 @@ namespace Dalamud.Interface.GameFonts /// public GameFontFamilyAndSize FamilyAndSize; + /// + /// Size of the font in pixels unit. + /// + public float SizePx; + /// /// Weight of the font. /// @@ -37,11 +38,12 @@ namespace Dalamud.Interface.GameFonts /// Initializes a new instance of the struct. /// /// Font family. - /// Size in points. - public GameFontStyle(GameFontFamily family, float size) + /// Size in pixels. + public GameFontStyle(GameFontFamily family, float sizePx) { - this.FamilyAndSize = GetRecommendedFamilyAndSize(family, size); + this.FamilyAndSize = GetRecommendedFamilyAndSize(family, sizePx * 3 / 4); this.Weight = this.SkewStrength = 0f; + this.SizePx = sizePx; } /// @@ -52,6 +54,29 @@ namespace Dalamud.Interface.GameFonts { this.FamilyAndSize = familyAndSize; this.Weight = this.SkewStrength = 0f; + + // Dummy assignment to satisfy requirements + this.SizePx = 0; + + this.SizePx = this.BaseSizePx; + } + + /// + /// Gets or sets the size of the font in points unit. + /// + public float SizePt + { + get => this.SizePx * 3 / 4; + set => this.SizePx = value * 4 / 3; + } + + /// + /// Gets or sets the base skew strength. + /// + public float BaseSkewStrength + { + get => this.SkewStrength * this.BaseSizePx / this.SizePx; + set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// @@ -87,9 +112,23 @@ namespace Dalamud.Interface.GameFonts }; /// - /// Gets the font size in point unit. + /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public float SizePt => this.FamilyAndSize switch + public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + { + GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, + GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, + GameFontFamily.JupiterNumeric => GameFontFamilyAndSize.Jupiter45, + GameFontFamily.Meidinger => GameFontFamilyAndSize.Meidinger16, + GameFontFamily.MiedingerMid => GameFontFamilyAndSize.MiedingerMid10, + GameFontFamily.TrumpGothic => GameFontFamilyAndSize.TrumpGothic184, + _ => GameFontFamilyAndSize.Undefined, + }; + + /// + /// Gets the base font size in point unit. + /// + public float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -119,9 +158,9 @@ namespace Dalamud.Interface.GameFonts }; /// - /// Gets the font size in pixel unit. + /// Gets the base font size in pixel unit. /// - public float SizePx => this.SizePt * 4 / 3; + public float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. @@ -138,7 +177,7 @@ namespace Dalamud.Interface.GameFonts public bool Italic { get => this.SkewStrength != 0; - set => this.SkewStrength = value ? 4 : 0; + set => this.SkewStrength = value ? this.SizePx / 7 : 0; } /// @@ -226,15 +265,21 @@ namespace Dalamud.Interface.GameFonts /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; - if (this.SkewStrength > 0) - widthDelta += 1f * this.SkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; - else if (this.SkewStrength < 0) - widthDelta -= 1f * this.SkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; + if (this.BaseSkewStrength > 0) + widthDelta += 1f * this.BaseSkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; + else if (this.BaseSkewStrength < 0) + widthDelta -= 1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; return (int)Math.Ceiling(widthDelta); } + + /// + public override string ToString() + { + return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; + } } } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index 6630c0439..05e69b4f5 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Interface; - namespace Dalamud.Interface.ImGuiFileDialog { /// diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs index 475147518..e6dd5e3a4 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Numerics; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index 9e2a77f0d..924dc68c3 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -116,20 +116,30 @@ namespace Dalamud.Interface.ImGuiFileDialog /// Gets the result of the selection. /// /// The result of the selection (file or folder path). If multiple entries were selected, they are separated with commas. + [Obsolete("Use GetResults() instead.", true)] public string GetResult() + { + return string.Join(',', this.GetResults()); + } + + /// + /// Gets the result of the selection. + /// + /// The list of selected paths. + public List GetResults() { if (!this.flags.HasFlag(ImGuiFileDialogFlags.SelectOnly)) { - return this.GetFilePathName(); + return new List { this.GetFilePathName() }; } if (this.IsDirectoryMode() && this.selectedFileNames.Count == 0) { - return this.GetFilePathName(); // current directory + return new List { this.GetFilePathName() }; // current directory } var fullPaths = this.selectedFileNames.Where(x => !string.IsNullOrEmpty(x)).Select(x => Path.Combine(this.currentPath, x)); - return string.Join(",", fullPaths.ToArray()); + return fullPaths.ToList(); } /// diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index 18bd9dc14..e5c5854db 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Dalamud.Interface.ImGuiFileDialog { @@ -7,9 +8,10 @@ namespace Dalamud.Interface.ImGuiFileDialog /// public class FileDialogManager { - private FileDialog dialog; + private FileDialog? dialog; + private Action? callback; + private Action>? multiCallback; private string savedPath = "."; - private Action callback; /// /// Create a dialog which selects an already existing folder. @@ -18,7 +20,21 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The action to execute when the dialog is finished. public void OpenFolderDialog(string title, Action callback) { - this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback); + this.SetCallback(callback); + this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly); + } + + /// + /// Create a dialog which selects an already existing folder. + /// + /// The header title of the dialog. + /// The action to execute when the dialog is finished. + /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. + /// Whether the dialog should be a modal popup. + public void OpenFolderDialog(string title, Action callback, string? startPath, bool isModal = false) + { + this.SetCallback(callback); + this.SetDialog("OpenFolderDialog", title, string.Empty, startPath ?? this.savedPath, ".", string.Empty, 1, isModal, ImGuiFileDialogFlags.SelectOnly); } /// @@ -29,18 +45,55 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The action to execute when the dialog is finished. public void SaveFolderDialog(string title, string defaultFolderName, Action callback) { - this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None, callback); + this.SetCallback(callback); + this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None); } /// - /// Create a dialog which selects an already existing file. + /// Create a dialog which selects an already existing folder or new folder. + /// + /// The header title of the dialog. + /// The default name to use when creating a new folder. + /// The action to execute when the dialog is finished. + /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. + /// Whether the dialog should be a modal popup. + public void SaveFolderDialog(string title, string defaultFolderName, Action callback, string? startPath, bool isModal = false) + { + this.SetCallback(callback); + this.SetDialog("SaveFolderDialog", title, string.Empty, startPath ?? this.savedPath, defaultFolderName, string.Empty, 1, isModal, ImGuiFileDialogFlags.None); + } + + /// + /// Create a dialog which selects a single, already existing file. /// /// The header title of the dialog. /// Which files to show in the dialog. /// The action to execute when the dialog is finished. public void OpenFileDialog(string title, string filters, Action callback) { - this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback); + this.SetCallback(callback); + this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly); + } + + /// + /// Create a dialog which selects already existing files. + /// + /// The header title of the dialog. + /// Which files to show in the dialog. + /// The action to execute when the dialog is finished. + /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. + /// The maximum amount of files or directories which can be selected. Set to 0 for an infinite number. + /// Whether the dialog should be a modal popup. + public void OpenFileDialog( + string title, + string filters, + Action> callback, + string? startPath = null, + int selectionCountMax = 1, + bool isModal = false) + { + this.SetCallback(callback); + this.SetDialog("OpenFileDialog", title, filters, startPath ?? this.savedPath, ".", string.Empty, selectionCountMax, isModal, ImGuiFileDialogFlags.SelectOnly); } /// @@ -51,9 +104,38 @@ namespace Dalamud.Interface.ImGuiFileDialog /// The default name to use when creating a new file. /// The extension to use when creating a new file. /// The action to execute when the dialog is finished. - public void SaveFileDialog(string title, string filters, string defaultFileName, string defaultExtension, Action callback) + public void SaveFileDialog( + string title, + string filters, + string defaultFileName, + string defaultExtension, + Action callback) { - this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None, callback); + this.SetCallback(callback); + this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None); + } + + /// + /// Create a dialog which selects an already existing folder or new file. + /// + /// The header title of the dialog. + /// Which files to show in the dialog. + /// The default name to use when creating a new file. + /// The extension to use when creating a new file. + /// The action to execute when the dialog is finished. + /// The directory which the dialog should start inside of. The last path this manager was in is used if this is null. + /// Whether the dialog should be a modal popup. + public void SaveFileDialog( + string title, + string filters, + string defaultFileName, + string defaultExtension, + Action callback, + string? startPath, + bool isModal = false) + { + this.SetCallback(callback); + this.SetDialog("SaveFileDialog", title, filters, startPath ?? this.savedPath, defaultFileName, defaultExtension, 1, isModal, ImGuiFileDialogFlags.None); } /// @@ -64,7 +146,10 @@ namespace Dalamud.Interface.ImGuiFileDialog if (this.dialog == null) return; if (this.dialog.Draw()) { - this.callback(this.dialog.GetIsOk(), this.dialog.GetResult()); + var isOk = this.dialog.GetIsOk(); + var results = this.dialog.GetResults(); + this.callback?.Invoke(isOk, results.Count > 0 ? results[0] : string.Empty); + this.multiCallback?.Invoke(isOk, results); this.savedPath = this.dialog.GetCurrentPath(); this.Reset(); } @@ -78,6 +163,19 @@ namespace Dalamud.Interface.ImGuiFileDialog this.dialog?.Hide(); this.dialog = null; this.callback = null; + this.multiCallback = null; + } + + private void SetCallback(Action action) + { + this.callback = action; + this.multiCallback = null; + } + + private void SetCallback(Action> action) + { + this.multiCallback = action; + this.callback = null; } private void SetDialog( @@ -89,11 +187,9 @@ namespace Dalamud.Interface.ImGuiFileDialog string defaultExtension, int selectionCountMax, bool isModal, - ImGuiFileDialogFlags flags, - Action callback) + ImGuiFileDialogFlags flags) { this.Reset(); - this.callback = callback; this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); this.dialog.Show(); } diff --git a/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs b/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs index fe189c77c..7ed959796 100644 --- a/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs +++ b/Dalamud/Interface/ImGuiFileDialog/ImGuiFileDialogFlags.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dalamud.Interface.ImGuiFileDialog { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index ee56e749f..fda86ac43 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -53,6 +53,7 @@ namespace Dalamud.Interface.Internal private readonly SelfTestWindow selfTestWindow; private readonly StyleEditorWindow styleEditorWindow; private readonly TitleScreenMenuWindow titleScreenMenuWindow; + private readonly FallbackFontNoticeWindow fallbackFontNoticeWindow; private readonly TextureWrap logoTexture; private readonly TextureWrap tsmLogoTexture; @@ -74,6 +75,7 @@ namespace Dalamud.Interface.Internal public DalamudInterface() { var configuration = Service.Get(); + var interfaceManager = Service.Get(); this.WindowSystem = new WindowSystem("DalamudCore"); @@ -91,6 +93,7 @@ namespace Dalamud.Interface.Internal this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; + this.fallbackFontNoticeWindow = new FallbackFontNoticeWindow() { IsOpen = interfaceManager.IsFallbackFontMode && !configuration.DisableFontFallbackNotice }; this.WindowSystem.AddWindow(this.changelogWindow); this.WindowSystem.AddWindow(this.colorDemoWindow); @@ -106,10 +109,11 @@ namespace Dalamud.Interface.Internal this.WindowSystem.AddWindow(this.selfTestWindow); this.WindowSystem.AddWindow(this.styleEditorWindow); this.WindowSystem.AddWindow(this.titleScreenMenuWindow); + this.WindowSystem.AddWindow(this.fallbackFontNoticeWindow); ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; + this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup; - var interfaceManager = Service.Get(); interfaceManager.Draw += this.OnDraw; var dalamud = Service.Get(); @@ -207,6 +211,11 @@ namespace Dalamud.Interface.Internal /// public void OpenDevMenu() => this.isImGuiDrawDevMenu = true; + /// + /// Opens the fallback font notice window. + /// + public void OpenFallbackFontNoticeWindow() => this.fallbackFontNoticeWindow.IsOpen = true; + /// /// Opens the . /// @@ -425,7 +434,13 @@ namespace Dalamud.Interface.Internal if (ImGui.BeginMenu("Dalamud")) { - ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImGuiDrawDevMenu); + ImGui.MenuItem("Draw dev menu", string.Empty, ref this.isImGuiDrawDevMenu); + var devBarAtStartup = configuration.DevBarOpenAtStartup; + if (ImGui.MenuItem("Draw dev menu at startup", string.Empty, ref devBarAtStartup)) + { + configuration.DevBarOpenAtStartup ^= true; + configuration.Save(); + } ImGui.Separator(); @@ -607,6 +622,11 @@ namespace Dalamud.Interface.Internal Log.Information(info); } + if (ImGui.MenuItem("Show dev bar info", null, configuration.ShowDevBarInfo)) + { + configuration.ShowDevBarInfo = !configuration.ShowDevBarInfo; + } + ImGui.EndMenu(); } @@ -674,7 +694,7 @@ namespace Dalamud.Interface.Internal configuration.Save(); } - if (ImGui.MenuItem("Load banned plugins", null, configuration.LoadBannedPlugins)) + if (ImGui.MenuItem("Load blacklisted plugins", null, configuration.LoadBannedPlugins)) { configuration.LoadBannedPlugins = !configuration.LoadBannedPlugins; configuration.Save(); @@ -724,14 +744,17 @@ namespace Dalamud.Interface.Internal if (Service.Get().GameUiHidden) ImGui.BeginMenu("UI is hidden...", false); - ImGui.PushFont(InterfaceManager.MonoFont); + if (configuration.ShowDevBarInfo) + { + ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu(Util.GetGitHash(), false); - ImGui.BeginMenu(this.frameCount.ToString("000000"), false); - ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); - ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false); + ImGui.BeginMenu(Util.GetGitHash(), false); + ImGui.BeginMenu(this.frameCount.ToString("000000"), false); + ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); + ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false); - ImGui.PopFont(); + ImGui.PopFont(); + } ImGui.EndMainMenuBar(); } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 21b5de793..c2ee00fdc 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,11 +15,9 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.Style; using Dalamud.Interface.Windowing; using Dalamud.Utility; @@ -47,6 +45,8 @@ namespace Dalamud.Interface.Internal /// internal class InterfaceManager : IDisposable { + private const float MinimumFallbackFontSizePt = 9.6f; // Game's minimum AXIS font size + private const float MinimumFallbackFontSizePx = MinimumFallbackFontSizePt * 4.0f / 3.0f; private const float DefaultFontSizePt = 12.0f; private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f; private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. @@ -69,6 +69,8 @@ namespace Dalamud.Interface.Internal private bool lastWantCapture = false; private bool isRebuildingFonts = false; + private bool isFallbackFontMode = false; + /// /// Initializes a new instance of the class. /// @@ -146,6 +148,11 @@ namespace Dalamud.Interface.Internal /// public event Action AfterBuildFonts; + /// + /// Gets or sets an action that is executed right after font fallback mode has been changed. + /// + public event Action OnFallbackFontModeChange; + /// /// Gets the default ImGui font. /// @@ -195,6 +202,22 @@ namespace Dalamud.Interface.Internal /// public bool IsReady => this.scene != null; + /// + /// Gets or sets a value indicating whether the font has been loaded in fallback mode. + /// + public bool IsFallbackFontMode + { + get => this.isFallbackFontMode; + internal set + { + if (value == this.isFallbackFontMode) + return; + + this.isFallbackFontMode = value; + this.OnFallbackFontModeChange?.Invoke(value); + } + } + /// /// Gets or sets a value indicating whether to override configuration for UseAxis. /// @@ -594,43 +617,11 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; } - private unsafe class TargetFontModification : IDisposable - { - internal TargetFontModification(string name, AxisMode axis, float sizePx, float scale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = scale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, sizePx * scale * 3 / 4)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } - - private unsafe void SetupFonts(int scaler = 0) + /// + /// Loads font for use in ImGui text functions. + /// + /// If set, then glyphs will be loaded in smaller resolution to make all glyphs fit into given constraints. + private unsafe void SetupFonts(bool disableBigFonts = false) { var gameFontManager = Service.Get(); var dalamud = Service.Get(); @@ -638,8 +629,6 @@ namespace Dalamud.Interface.Internal var ioFonts = io.Fonts; var maxTexDimension = 1 << (10 + Math.Max(0, Math.Min(4, this.FontResolutionLevel))); - var disableBigFonts = scaler != 0; - var fontLoadScale = disableBigFonts ? Math.Min(io.FontGlobalScale, 1.0f / scaler) : io.FontGlobalScale; var fontGamma = this.FontGamma; this.fontBuildSignal.Reset(); @@ -667,9 +656,19 @@ namespace Dalamud.Interface.Internal fontConfig.OversampleH = 1; fontConfig.OversampleV = 1; - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); + if (!File.Exists(fontPathJp)) + fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); if (!File.Exists(fontPathJp)) ShowFontError(fontPathJp); + Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); + + var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = null; + Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); // Default font Log.Verbose("[FONT] SetupFonts - Default font"); @@ -677,12 +676,13 @@ namespace Dalamud.Interface.Internal "Default", this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - fontLoadScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.SizePt, fontInfo.SourceAxis.Style.SizePx); + io.FontGlobalScale, + disableBigFonts); + Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); + fontConfig.SizePixels = disableBigFonts ? Math.Min(MinimumFallbackFontSizePx, fontInfo.TargetSizePx) : fontInfo.TargetSizePx * io.FontGlobalScale; if (this.UseAxis) { fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.TargetSizePx; fontConfig.PixelSnapH = false; DefaultFont = ioFonts.AddFontDefault(fontConfig); this.loadedFontInfo[DefaultFont] = fontInfo; @@ -694,10 +694,19 @@ namespace Dalamud.Interface.Internal fontConfig.GlyphRanges = japaneseRangeHandle.AddrOfPinnedObject(); fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontInfo.TargetSizePx * fontLoadScale, fontConfig); + DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); this.loadedFontInfo[DefaultFont] = fontInfo; } + if (fontPathKr != null && Service.Get().EffectiveLanguage == "ko") + { + fontConfig.MergeMode = true; + fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + // FontAwesome icon font Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); { @@ -710,8 +719,8 @@ namespace Dalamud.Interface.Internal fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * fontLoadScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, fontLoadScale); + IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, disableBigFonts ? Math.Min(MinimumFallbackFontSizePx, DefaultFontSizePx) : DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale, disableBigFonts); } // Monospace font @@ -723,8 +732,8 @@ namespace Dalamud.Interface.Internal fontConfig.GlyphRanges = IntPtr.Zero; fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * fontLoadScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, fontLoadScale); + MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, disableBigFonts ? Math.Min(MinimumFallbackFontSizePx, DefaultFontSizePx) : DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale, disableBigFonts); } // Default font but in requested size for requested glyphs @@ -776,11 +785,12 @@ namespace Dalamud.Interface.Internal $"Requested({fontSize}px)", this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, fontSize, - fontLoadScale); + io.FontGlobalScale, + disableBigFonts); if (this.UseAxis) { fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.SizePx; + fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; fontConfig.PixelSnapH = false; var sizedFont = ioFonts.AddFontDefault(fontConfig); @@ -794,7 +804,7 @@ namespace Dalamud.Interface.Internal garbageList.Add(rangeHandle); fontConfig.PixelSnapH = true; - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, (disableBigFonts ? DefaultFontSizePx + 1 : fontSize) * fontLoadScale, fontConfig, rangeHandle.AddrOfPinnedObject()); + var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, disableBigFonts ? Math.Min(MinimumFallbackFontSizePx, fontSize) : fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); this.loadedFontInfo[sizedFont] = fontInfo; foreach (var request in requests) request.FontInternal = sizedFont; @@ -802,7 +812,7 @@ namespace Dalamud.Interface.Internal } } - gameFontManager.BuildFonts(); + gameFontManager.BuildFonts(disableBigFonts); var customFontFirstConfigIndex = ioFonts.ConfigData.Size; @@ -817,26 +827,71 @@ namespace Dalamud.Interface.Internal config.OversampleV = 1; var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PluginRequest({name})", TargetFontModification.AxisMode.Suppress, config.SizePixels, 1); - config.SizePixels = (disableBigFonts ? DefaultFontSizePx : config.SizePixels) * fontLoadScale; + + // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. + if (config.MergeMode) + { + if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); + continue; + } + } + else + { + if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); + continue; + } + + // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. + this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); + } + + if (disableBigFonts) + { + // If a plugin has requested a font size that is bigger than current restrictions, load it scaled down. + // After loading glyphs onto font atlas, font information will be modified to make it look like the font of original size has been loaded. + if (config.SizePixels > MinimumFallbackFontSizePx) + config.SizePixels = MinimumFallbackFontSizePx; + } + else + { + config.SizePixels = config.SizePixels * io.FontGlobalScale; + } } + Log.Verbose("[FONT] ImGui.IO.Build will be called."); ioFonts.Build(); + Log.Verbose("[FONT] ImGui.IO.Build OK!"); if (ioFonts.TexHeight > maxTexDimension) { - if (scaler < 4) + var possibilityForScaling = false; + foreach (var x in this.loadedFontInfo.Values) { - Log.Information("[FONT] Atlas size is {0}x{1} which is bigger than allowed {2}x{3}. Retrying with scale {4}.", ioFonts.TexWidth, ioFonts.TexHeight, maxTexDimension, maxTexDimension, scaler + 1); - this.SetupFonts(scaler + 1); + if (x.TargetSizePx * x.Scale > MinimumFallbackFontSizePx) + { + possibilityForScaling = true; + break; + } + } + + if (possibilityForScaling && !disableBigFonts) + { + Log.Information("[FONT] Atlas size is {0}x{1} which is bigger than allowed {2}x{3}. Retrying with minimized font sizes.", ioFonts.TexWidth, ioFonts.TexHeight, maxTexDimension, maxTexDimension); + this.SetupFonts(true); return; } else { - Log.Warning("[FONT] Atlas size is {0}x{1} which is bigger than allowed {2}x{3}, but giving up trying to scale smaller.", ioFonts.TexWidth, ioFonts.TexHeight, maxTexDimension, maxTexDimension); + Log.Warning("[FONT] Atlas size is {0}x{1} which is bigger than allowed {2}x{3} even when font sizes are minimized up to {4}px. This may result in crash.", ioFonts.TexWidth, ioFonts.TexHeight, maxTexDimension, maxTexDimension, MinimumFallbackFontSizePx); } } + this.IsFallbackFontMode = disableBigFonts; + if (Math.Abs(fontGamma - 1.0f) >= 0.001) { // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) @@ -845,12 +900,23 @@ namespace Dalamud.Interface.Internal texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f); } - gameFontManager.AfterBuildFonts(); + gameFontManager.AfterBuildFonts(disableBigFonts); foreach (var (font, mod) in this.loadedFontInfo) { - var nameBytes = Encoding.UTF8.GetBytes(mod.Name + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + // I have no idea what's causing NPE, so just to be safe + try + { + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing + } Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); GameFontManager.UnscaleFont(font, mod.Scale, false); @@ -1103,5 +1169,65 @@ namespace Dalamud.Interface.Internal this.Manager.glyphRequests.Remove(this); } } + + private unsafe class TargetFontModification : IDisposable + { + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. + /// + /// Name of the font to write to ImGui font information. + /// Target font size in pixels, which will not be considered for further scaling. + internal TargetFontModification(string name, float sizePx) + { + this.Name = name; + this.Axis = AxisMode.Suppress; + this.TargetSizePx = sizePx; + this.Scale = 1; + this.SourceAxis = null; + } + + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information. + /// + /// Name of the font to write to ImGui font information. + /// Whether and how to use AXIS fonts. + /// Target font size in pixels, which will not be considered for further scaling. + /// Font scale to be referred for loading AXIS font of appropriate size. + /// Whether to enable loading big AXIS fonts. + internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale, bool disableBigFonts) + { + this.Name = name; + this.Axis = axis; + this.TargetSizePx = sizePx; + this.Scale = disableBigFonts ? MinimumFallbackFontSizePx / sizePx : globalFontScale; + this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); + } + + internal enum AxisMode + { + Suppress, + GameGlyphsOnly, + Overwrite, + } + + internal string Name { get; private init; } + + internal AxisMode Axis { get; private init; } + + internal float TargetSizePx { get; private init; } + + internal float Scale { get; private init; } + + internal GameFontHandle? SourceAxis { get; private init; } + + internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; + + public void Dispose() + { + this.SourceAxis?.Dispose(); + } + } } } diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 3b02a1b8d..51e2b6b42 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Numerics; -using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Game; @@ -19,7 +15,7 @@ namespace Dalamud.Interface.Internal /// /// This class displays a debug window to inspect native addons. /// - internal unsafe class UIDebug + internal unsafe class UiDebug { private const int UnitListCount = 18; @@ -52,9 +48,9 @@ namespace Dalamud.Interface.Internal private AtkUnitBase* selectedUnitBase = null; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public UIDebug() + public UiDebug() { var sigScanner = Service.Get(); var getSingletonAddr = sigScanner.ScanText("E8 ?? ?? ?? ?? 41 B8 01 00 00 00 48 8D 15 ?? ?? ?? ?? 48 8B 48 20 E8 ?? ?? ?? ?? 48 8B CF"); diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index e6f32b551..468bdb3f3 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Numerics; @@ -19,31 +18,19 @@ namespace Dalamud.Interface.Internal.Windows /// /// Whether the latest update warrants a changelog window. /// - public const string WarrantsChangelogForMajorMinor = "6.3."; + public const string WarrantsChangelogForMajorMinor = "6.4."; private const string ChangeLog = - @"• Added a new menu to the title screen which allows you to access the plugin installer and various other plugins before logging in. - => You can disable this menu in the settings under ""Look & Feel"". -• Added a way for plugins to add information to the game's server info bar (e.g. current song, ping, etc). - => You can disable and reorder this information in the settings, if any plugin provides it. -• Switched the plugin download server to a self-hosted solution instead of GitHub, to circumvent API limits, country blocks and bad ISP routing. - => Please see the ""Are plugins safe to use"" part of the XIVLauncher FAQ(goatcorp.github.io/faq) or reach out on Discord if you have concerns about security or want details on how this is set up and ran. - => Changelogs in-game/the plugin installer should now also be more common, as the new service takes changelogs from the developer pull request descriptions. -• The ""Available Plugins"" list in the plugin installer now also shows installed plugins to make the split less confusing. A new filter mode that filters installed plugins has been added. -• A ""Changelog"" category has been added in the plugin installer which will list all recent changes to your plugins, and recent changes to Dalamud. + @"• Updated Dalamud for compatibility with Patch 6.1. -If you note any issues or need help, please make sure to ask on our discord server. +If you note any issues or need help, please check the FAQ, and reach out on our Discord if you need help Thanks and have fun!"; private const string UpdatePluginsInfo = @"• All of your plugins were disabled automatically, due to this update. This is normal. • Open the plugin installer, then click 'update plugins'. Updated plugins should update and then re-enable themselves. => Please keep in mind that not all of your plugins may already be updated for the new version. - => If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available. - -While we tested the released plugins considerably with a smaller set of people and believe that they are stable, we cannot guarantee to you that you will not run into crashes. - -Considering current queue times, this is why we recommend that for now, you only use a set of plugins that are most essential to you, so you can go on playing the game instead of waiting endlessly."; + => If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."; private readonly string assemblyVersion = Util.AssemblyVersion; @@ -53,7 +40,7 @@ Considering current queue times, this is why we recommend that for now, you only /// Initializes a new instance of the class. /// public ChangelogWindow() - : base("What's new in XIVLauncher?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize) + : base("What's new in Dalamud?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize) { this.Namespace = "DalamudChangelogWindow"; @@ -82,19 +69,17 @@ Considering current queue times, this is why we recommend that for now, you only ImGui.TextWrapped(ChangeLog); - /* ImGuiHelpers.ScaledDummy(5); ImGui.TextColored(ImGuiColors.DalamudRed, " !!! ATTENTION !!!"); ImGui.TextWrapped(UpdatePluginsInfo); - */ ImGuiHelpers.ScaledDummy(10); - ImGui.Text("Thank you for using our tools!"); + // ImGui.Text("Thank you for using our tools!"); - ImGuiHelpers.ScaledDummy(10); + // ImGuiHelpers.ScaledDummy(10); ImGui.PushFont(UiBuilder.IconFont); diff --git a/Dalamud/Interface/Internal/Windows/CreditsWindow.cs b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs index 4e2a8d847..9dd7fa4cb 100644 --- a/Dalamud/Interface/Internal/Windows/CreditsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Game; using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index 1cacc7f38..2783d00ce 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -72,7 +72,7 @@ namespace Dalamud.Interface.Internal.Windows private bool resolveGameData = false; private bool resolveObjects = false; - private UIDebug addonInspector = null; + private UiDebug addonInspector = null; private Hook? messageBoxMinHook; private bool hookUseMinHook = false; @@ -877,7 +877,7 @@ namespace Dalamud.Interface.Internal.Windows private void DrawAddonInspector() { - this.addonInspector ??= new UIDebug(); + this.addonInspector ??= new UiDebug(); this.addonInspector.Draw(); } diff --git a/Dalamud/Interface/Internal/Windows/FallbackFontNoticeWindow.cs b/Dalamud/Interface/Internal/Windows/FallbackFontNoticeWindow.cs new file mode 100644 index 000000000..c0a035d52 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/FallbackFontNoticeWindow.cs @@ -0,0 +1,95 @@ +using System; +using System.Numerics; + +using CheapLoc; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// For major updates, an in-game Changelog window. + /// + internal sealed class FallbackFontNoticeWindow : Window, IDisposable + { + /// + /// Initializes a new instance of the class. + /// + public FallbackFontNoticeWindow() + : base(Title, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) + { + this.Namespace = "FallbackFontNoticeWindow"; + this.RespectCloseHotkey = false; + + this.Size = new Vector2(885, 463); + this.SizeCondition = ImGuiCond.Appearing; + + var interfaceManager = Service.Get(); + var dalamud = Service.Get(); + + Service.Get().OnFallbackFontModeChange += this.OnFallbackFontModeChange; + } + + private static string Title => Loc.Localize("FallbackFontNoticeWindowTitle", "Fallback Font Mode Active"); + + /// + public override void Draw() + { + ImGui.Text(Title); + ImGuiHelpers.ScaledDummy(10); + + ImGui.Text(Loc.Localize("FallbackFontNoticeWindowBody", "The text used by Dalamud and plugins has been made blurry in order to prevent possible crash.")); + ImGuiHelpers.ScaledDummy(10); + + ImGui.Text(Loc.Localize("FallbackFontNoticeWindowSolution1", "* You may attempt to increase the limits on text quality. This may result in a crash.")); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("FallbackFontNoticeWindowOpenDalamudSettings", "Open Dalamud Settings"))) + Service.Get().OpenSettings(); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + ImGui.Text(string.Format( + Loc.Localize( + "FallbackFontNoticeWindowSolution1Instructions", + "In \"{0}\" tab, choose a better option for \"{1}\"."), + Loc.Localize("DalamudSettingsVisual", "Look & Feel"), + Loc.Localize("DalamudSettingsFontResolutionLevel", "Font resolution level"))); + + ImGuiHelpers.ScaledDummy(10); + + ImGui.Text(Loc.Localize("FallbackFontNoticeWindowSolution2", "* You may disable custom fonts, or make fonts smaller, from individual plugin settings.")); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("FallbackFontNoticeWindowOpenDalamudPlugins", "Open Plugin Installer"))) + Service.Get().OpenPluginInstaller(); + + ImGuiHelpers.ScaledDummy(10); + + if (ImGui.Button(Loc.Localize("FallbackFontNoticeWindowDoNotShowAgain", "Do not show again"))) + { + this.IsOpen = false; + Service.Get().DisableFontFallbackNotice = true; + Service.Get().Save(); + } + } + + /// + /// Dispose this window. + /// + public void Dispose() + { + Service.Get().OnFallbackFontModeChange -= this.OnFallbackFontModeChange; + } + + private void OnFallbackFontModeChange(bool mode) + { + Log.Verbose("[{0}] OnFallbackFontModeChange called: {1} (disable={2})", this.Namespace, mode, Service.Get().DisableFontFallbackNotice); + if (!mode) + this.IsOpen = false; + else if (!Service.Get().DisableFontFallbackNotice) + this.IsOpen = true; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 684ffc439..2e714a8c8 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -42,7 +42,7 @@ namespace Dalamud.Interface.Internal.Windows /// public const int PluginIconHeight = 512; - private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api5/{0}/{1}/images/{2}"; + private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}"; private BlockingCollection> downloadQueue = new(); private BlockingCollection loadQueue = new(); @@ -320,7 +320,7 @@ namespace Dalamud.Interface.Internal.Windows isThirdParty = true; } - var useTesting = pluginManager.UseTesting(manifest); + var useTesting = PluginManager.UseTesting(manifest); var url = this.GetPluginIconUrl(manifest, isThirdParty, useTesting); if (!url.IsNullOrEmpty()) @@ -441,7 +441,7 @@ namespace Dalamud.Interface.Internal.Windows isThirdParty = true; } - var useTesting = pluginManager.UseTesting(manifest); + var useTesting = PluginManager.UseTesting(manifest); var urls = this.GetPluginImageUrls(manifest, isThirdParty, useTesting); if (urls != null) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogEntry.cs index 7d9f4b5ab..7a060d34f 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogEntry.cs @@ -1,7 +1,5 @@ using System; -using ImGuiScene; - namespace Dalamud.Interface.Internal.Windows.PluginInstaller { /// diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/IChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/IChangelogEntry.cs index 9e5c6b2ab..21143e5c0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/IChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/IChangelogEntry.cs @@ -1,7 +1,5 @@ using System; -using ImGuiScene; - namespace Dalamud.Interface.Internal.Windows.PluginInstaller { /// diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs index 4008ed4b4..ce2353ec8 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs @@ -2,7 +2,6 @@ using Dalamud.Plugin.Internal; using Dalamud.Utility; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.PluginInstaller { diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 265dcb58d..838f03345 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net.Http; using System.Numerics; using System.Threading.Tasks; @@ -348,7 +347,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller if (this.updatePluginCount > 0) { - pluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox); + PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox); notifications.AddNotification(Locs.Notifications_UpdatesInstalled(this.updatePluginCount), Locs.Notifications_UpdatesInstalledTitle, NotificationType.Success); var installedGroupIdx = this.categoryManager.GroupList.TakeWhile( @@ -1251,7 +1250,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller var notifications = Service.Get(); var pluginManager = Service.Get(); - var useTesting = pluginManager.UseTesting(manifest); + var useTesting = PluginManager.UseTesting(manifest); var wasSeen = this.WasPluginSeen(manifest.InternalName); // Check for valid versions diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index 9bceef857..0fcedc48e 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -4,8 +4,6 @@ using System.Linq; using System.Reflection; using Dalamud.Game; -using Dalamud.Game.Internal; -using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index a26368d47..0af8bf1bb 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Numerics; diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 4dffd0148..8678b26bb 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -12,7 +12,6 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Utility; @@ -134,26 +133,9 @@ namespace Dalamud.Interface.Internal.Windows }; this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); - try - { - if (string.IsNullOrEmpty(configuration.LanguageOverride)) - { - var currentUiLang = CultureInfo.CurrentUICulture; - - if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) - this.langIndex = Array.IndexOf(this.languages, currentUiLang.TwoLetterISOLanguageName); - else - this.langIndex = 0; - } - else - { - this.langIndex = Array.IndexOf(this.languages, configuration.LanguageOverride); - } - } - catch (Exception) - { + this.langIndex = Array.IndexOf(this.languages, configuration.EffectiveLanguage); + if (this.langIndex == -1) this.langIndex = 0; - } try { @@ -197,7 +179,9 @@ namespace Dalamud.Interface.Internal.Windows var configuration = Service.Get(); var interfaceManager = Service.Get(); - var rebuildFont = interfaceManager.FontGamma != configuration.FontGammaLevel; + var rebuildFont = interfaceManager.FontGamma != configuration.FontGammaLevel + || interfaceManager.FontResolutionLevel != configuration.FontResolutionLevel + || interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; interfaceManager.FontGammaOverride = null; @@ -353,7 +337,7 @@ namespace Dalamud.Interface.Internal.Windows interfaceManager.RebuildFonts(); } - if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f")) + if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp)) { ImGui.GetIO().FontGlobalScale = this.globalUiScale; interfaceManager.RebuildFonts(); @@ -396,6 +380,18 @@ namespace Dalamud.Interface.Internal.Windows ImGui.GetIO().Fonts.TexHeight)); ImGui.PopStyleColor(); + if (Service.Get().DisableFontFallbackNotice) + { + ImGui.Text(Loc.Localize("DalamudSettingsFontResolutionLevelWarningDisabled", "Warning will not be displayed even when the limits are enforced and fonts become blurry.")); + if (ImGui.Button(Loc.Localize("DalamudSettingsFontResolutionLevelWarningReset", "Show warnings") + "##DalamudSettingsFontResolutionLevelWarningReset")) + { + Service.Get().DisableFontFallbackNotice = false; + Service.Get().Save(); + if (Service.Get().IsFallbackFontMode) + Service.Get().OpenFallbackFontNoticeWindow(); + } + } + ImGuiHelpers.ScaledDummy(10); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")); @@ -440,7 +436,7 @@ namespace Dalamud.Interface.Internal.Windows interfaceManager.RebuildFonts(); } - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, MinScale, MaxScale, "%.2f")) + if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp)) { interfaceManager.FontGammaOverride = this.fontGamma; interfaceManager.RebuildFonts(); @@ -603,7 +599,9 @@ namespace Dalamud.Interface.Internal.Windows } ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories.")); - ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.\nTake care when installing third-party plugins from untrusted sources.")); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.")); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install third-party plugins from developers you trust.")); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 40bb29672..628f52f2f 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -9,7 +9,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using ImGuiNET; using ImGuiScene; diff --git a/Dalamud/Interface/Style/DalamudColors.cs b/Dalamud/Interface/Style/DalamudColors.cs index a674ee4b2..e74097936 100644 --- a/Dalamud/Interface/Style/DalamudColors.cs +++ b/Dalamud/Interface/Style/DalamudColors.cs @@ -39,6 +39,33 @@ namespace Dalamud.Interface.Style [JsonProperty("j")] public Vector4? DPSRed { get; set; } + [JsonProperty("k")] + public Vector4? DalamudYellow { get; set; } + + [JsonProperty("l")] + public Vector4? DalamudViolet { get; set; } + + [JsonProperty("m")] + public Vector4? ParsedGrey { get; set; } + + [JsonProperty("n")] + public Vector4? ParsedGreen { get; set; } + + [JsonProperty("o")] + public Vector4? ParsedBlue { get; set; } + + [JsonProperty("p")] + public Vector4? ParsedPurple { get; set; } + + [JsonProperty("q")] + public Vector4? ParsedOrange { get; set; } + + [JsonProperty("r")] + public Vector4? ParsedPink { get; set; } + + [JsonProperty("s")] + public Vector4? ParsedGold { get; set; } + public void Apply() { if (this.DalamudRed.HasValue) @@ -90,6 +117,51 @@ namespace Dalamud.Interface.Style { ImGuiColors.DPSRed = this.DPSRed.Value; } + + if (this.DalamudYellow.HasValue) + { + ImGuiColors.DalamudYellow = this.DalamudYellow.Value; + } + + if (this.DalamudViolet.HasValue) + { + ImGuiColors.DalamudViolet = this.DalamudViolet.Value; + } + + if (this.ParsedGrey.HasValue) + { + ImGuiColors.ParsedGrey = this.ParsedGrey.Value; + } + + if (this.ParsedGreen.HasValue) + { + ImGuiColors.ParsedGreen = this.ParsedGreen.Value; + } + + if (this.ParsedBlue.HasValue) + { + ImGuiColors.ParsedBlue = this.ParsedBlue.Value; + } + + if (this.ParsedPurple.HasValue) + { + ImGuiColors.ParsedPurple = this.ParsedPurple.Value; + } + + if (this.ParsedOrange.HasValue) + { + ImGuiColors.ParsedOrange = this.ParsedOrange.Value; + } + + if (this.ParsedPink.HasValue) + { + ImGuiColors.ParsedPink = this.ParsedPink.Value; + } + + if (this.ParsedGold.HasValue) + { + ImGuiColors.ParsedGold = this.ParsedGold.Value; + } } } diff --git a/Dalamud/Interface/Style/StyleModelV1.cs b/Dalamud/Interface/Style/StyleModelV1.cs index c08c3460c..0d10387c4 100644 --- a/Dalamud/Interface/Style/StyleModelV1.cs +++ b/Dalamud/Interface/Style/StyleModelV1.cs @@ -126,9 +126,18 @@ namespace Dalamud.Interface.Style DalamudWhite = new Vector4(1f, 1f, 1f, 1f), DalamudWhite2 = new Vector4(0.878f, 0.878f, 0.878f, 1f), DalamudOrange = new Vector4(1f, 0.709f, 0f, 1f), + DalamudYellow = new Vector4(1f, 1f, .4f, 1f), + DalamudViolet = new Vector4(0.770f, 0.700f, 0.965f, 1.000f), TankBlue = new Vector4(0f, 0.6f, 1f, 1f), HealerGreen = new Vector4(0f, 0.8f, 0.1333333f, 1f), DPSRed = new Vector4(0.7058824f, 0f, 0f, 1f), + ParsedGrey = new Vector4(0.4f, 0.4f, 0.4f, 1f), + ParsedGreen = new Vector4(0.117f, 1f, 0f, 1f), + ParsedBlue = new Vector4(0f, 0.439f, 1f, 1f), + ParsedPurple = new Vector4(0.639f, 0.207f, 0.933f, 1f), + ParsedOrange = new Vector4(1f, 0.501f, 0f, 1f), + ParsedPink = new Vector4(0.886f, 0.407f, 0.658f, 1f), + ParsedGold = new Vector4(0.898f, 0.8f, 0.501f, 1f), }, }; @@ -236,9 +245,18 @@ namespace Dalamud.Interface.Style DalamudWhite = new Vector4(1f, 1f, 1f, 1f), DalamudWhite2 = new Vector4(0.878f, 0.878f, 0.878f, 1f), DalamudOrange = new Vector4(1f, 0.709f, 0f, 1f), + DalamudYellow = new Vector4(1f, 1f, .4f, 1f), + DalamudViolet = new Vector4(0.770f, 0.700f, 0.965f, 1.000f), TankBlue = new Vector4(0f, 0.6f, 1f, 1f), HealerGreen = new Vector4(0f, 0.8f, 0.1333333f, 1f), DPSRed = new Vector4(0.7058824f, 0f, 0f, 1f), + ParsedGrey = new Vector4(0.4f, 0.4f, 0.4f, 1f), + ParsedGreen = new Vector4(0.117f, 1f, 0f, 1f), + ParsedBlue = new Vector4(0f, 0.439f, 1f, 1f), + ParsedPurple = new Vector4(0.639f, 0.207f, 0.933f, 1f), + ParsedOrange = new Vector4(1f, 0.501f, 0f, 1f), + ParsedPink = new Vector4(0.886f, 0.407f, 0.658f, 1f), + ParsedGold = new Vector4(0.898f, 0.8f, 0.501f, 1f), }, }; @@ -400,9 +418,18 @@ namespace Dalamud.Interface.Style DalamudWhite = ImGuiColors.DalamudWhite, DalamudWhite2 = ImGuiColors.DalamudWhite2, DalamudOrange = ImGuiColors.DalamudOrange, + DalamudYellow = ImGuiColors.DalamudYellow, + DalamudViolet = ImGuiColors.DalamudViolet, TankBlue = ImGuiColors.TankBlue, HealerGreen = ImGuiColors.HealerGreen, DPSRed = ImGuiColors.DPSRed, + ParsedGrey = ImGuiColors.ParsedGrey, + ParsedGreen = ImGuiColors.ParsedGreen, + ParsedBlue = ImGuiColors.ParsedBlue, + ParsedPurple = ImGuiColors.ParsedPurple, + ParsedOrange = ImGuiColors.ParsedOrange, + ParsedPink = ImGuiColors.ParsedPink, + ParsedGold = ImGuiColors.ParsedGold, }; return model; diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index 0e3e2d5eb..b43194b22 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -106,7 +106,8 @@ namespace Dalamud.Interface.Windowing if (hasNamespace) ImGui.PushID(this.Namespace); - foreach (var window in this.windows) + // Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating + foreach (var window in this.windows.ToArray()) { #if DEBUG // Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}"); diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index 3888d55db..1f465b4a5 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Threading.Tasks; using Dalamud.Game; -using Serilog; namespace Dalamud.Logging.Internal { diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 8bff19d7b..2be81eaf5 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -14,7 +14,6 @@ using Dalamud.Game.Text.Sanitizer; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; -using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs index 6b5c8920a..0488f5539 100644 --- a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs +++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs @@ -1,4 +1,3 @@ -using System; using System.IO; namespace Dalamud.Plugin.Internal.Exceptions diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs index a80d6d51d..a2d8e7361 100644 --- a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs +++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Plugin.Internal.Exceptions { /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index e1ac35d11..f98e122f5 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -31,7 +31,7 @@ namespace Dalamud.Plugin.Internal /// /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. /// - public const int DalamudApiLevel = 5; + public const int DalamudApiLevel = 6; private static readonly ModuleLog Log = new("PLUGINM"); @@ -104,7 +104,7 @@ namespace Dalamud.Plugin.Internal /// /// Gets a value indicating whether plugins are not still loading from boot. /// - public bool PluginsReady { get; private set; } = false; + public bool PluginsReady { get; private set; } /// /// Gets a value indicating whether all added repos are not in progress. @@ -121,6 +121,82 @@ namespace Dalamud.Plugin.Internal /// public PluginConfigurations PluginConfigs { get; } + /// + /// Print to chat any plugin updates and whether they were successful. + /// + /// The list of updated plugin metadata. + /// The header text to send to chat prior to any update info. + public static void PrintUpdatedPlugins(List? updateMetadata, string header) + { + var chatGui = Service.Get(); + + if (updateMetadata is { Count: > 0 }) + { + chatGui.Print(header); + + foreach (var metadata in updateMetadata) + { + if (metadata.WasUpdated) + { + chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + } + else + { + chatGui.PrintChat(new XivChatEntry + { + Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), + Type = XivChatType.Urgent, + }); + } + } + } + } + + /// + /// For a given manifest, determine if the testing version should be used over the normal version. + /// The higher of the two versions is calculated after checking other settings. + /// + /// Manifest to check. + /// A value indicating whether testing should be used. + public static bool UseTesting(PluginManifest manifest) + { + var configuration = Service.Get(); + + if (!configuration.DoPluginTest) + return false; + + if (manifest.IsTestingExclusive) + return true; + + var av = manifest.AssemblyVersion; + var tv = manifest.TestingAssemblyVersion; + var hasTv = tv != null; + + if (hasTv) + { + return tv > av; + } + + return false; + } + + /// + /// Gets a value indicating whether the given repo manifest should be visible to the user. + /// + /// Repo manifest. + /// If the manifest is visible. + public static bool IsManifestVisible(RemotePluginManifest manifest) + { + var configuration = Service.Get(); + + // Hidden by user + if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) + return false; + + // Hidden by manifest + return !manifest.IsHide; + } + /// public void Dispose() { @@ -144,7 +220,7 @@ namespace Dalamud.Plugin.Internal /// Set the list of repositories to use and downloads their contents. /// Should be called when the Settings window has been updated or at instantiation. /// - /// Whether the available plugins changed should be evented after. + /// Whether the available plugins changed event should be sent after. /// A representing the asynchronous operation. public async Task SetPluginReposFromConfigAsync(bool notify) { @@ -198,7 +274,7 @@ namespace Dalamud.Plugin.Internal var manifest = LocalPluginManifest.Load(manifestFile); - pluginDefs.Add(new(dllFile, manifest, false)); + pluginDefs.Add(new PluginDef(dllFile, manifest, false)); } } @@ -225,7 +301,7 @@ namespace Dalamud.Plugin.Internal // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; - devPluginDefs.Add(new(dllFile, manifest, true)); + devPluginDefs.Add(new PluginDef(dllFile, manifest, true)); } // Sort for load order - unloaded definitions have default priority of 0 @@ -235,9 +311,9 @@ namespace Dalamud.Plugin.Internal // Dev plugins should load first. pluginDefs.InsertRange(0, devPluginDefs); - void LoadPlugins(IEnumerable pluginDefs) + void LoadPlugins(IEnumerable pluginDefsList) { - foreach (var pluginDef in pluginDefs) + foreach (var pluginDef in pluginDefsList) { try { @@ -260,7 +336,7 @@ namespace Dalamud.Plugin.Internal var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); Task.Run(() => LoadPlugins(asyncPlugins)) - .ContinueWith(task => + .ContinueWith(_ => { this.PluginsReady = true; this.NotifyInstalledPluginsChanged(); @@ -318,7 +394,7 @@ namespace Dalamud.Plugin.Internal this.AvailablePlugins = this.Repos .SelectMany(repo => repo.PluginMaster) .Where(this.IsManifestEligible) - .Where(this.IsManifestVisible) + .Where(IsManifestVisible) .ToImmutableList(); if (notify) @@ -412,7 +488,7 @@ namespace Dalamud.Plugin.Internal var response = await Util.HttpClient.GetAsync(downloadUrl); response.EnsureSuccessStatusCode(); - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version.ToString())); + var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); try { @@ -568,7 +644,6 @@ namespace Dalamud.Plugin.Internal { // Out of date plugins get added so they can be updated. Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); - // plugin.Disable(); // Don't disable, or it gets deleted next boot. } else { @@ -625,14 +700,12 @@ namespace Dalamud.Plugin.Internal return version; }) - .Where(version => version != null) .ToArray(); if (versionDirs.Length == 0) { Log.Information($"No versions: cleaning up {pluginDir.FullName}"); pluginDir.Delete(true); - continue; } else { @@ -675,7 +748,6 @@ namespace Dalamud.Plugin.Internal { Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}"); versionDir.Delete(true); - continue; } } catch (Exception ex) @@ -737,13 +809,12 @@ namespace Dalamud.Plugin.Internal { InternalName = plugin.Manifest.InternalName, Name = plugin.Manifest.Name, - Version = metadata.UseTesting - ? metadata.UpdateManifest.TestingAssemblyVersion - : metadata.UpdateManifest.AssemblyVersion, + Version = (metadata.UseTesting + ? metadata.UpdateManifest.TestingAssemblyVersion + : metadata.UpdateManifest.AssemblyVersion)!, + WasUpdated = true, }; - updateStatus.WasUpdated = true; - if (!dryRun) { // Unload if loaded @@ -836,88 +907,6 @@ namespace Dalamud.Plugin.Internal plugin.Load(PluginLoadReason.Installer); } - /// - /// Print to chat any plugin updates and whether they were successful. - /// - /// The list of updated plugin metadata. - /// The header text to send to chat prior to any update info. - public void PrintUpdatedPlugins(List updateMetadata, string header) - { - var chatGui = Service.Get(); - - if (updateMetadata != null && updateMetadata.Count > 0) - { - chatGui.Print(header); - - foreach (var metadata in updateMetadata) - { - if (metadata.WasUpdated) - { - chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); - } - else - { - chatGui.PrintChat(new XivChatEntry - { - Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), - Type = XivChatType.Urgent, - }); - } - } - } - } - - /// - /// For a given manifest, determine if the testing version should be used over the normal version. - /// The higher of the two versions is calculated after checking other settings. - /// - /// Manifest to check. - /// A value indicating whether testing should be used. - public bool UseTesting(PluginManifest manifest) - { - var configuration = Service.Get(); - - if (!configuration.DoPluginTest) - return false; - - if (manifest.IsTestingExclusive) - return true; - - var av = manifest.AssemblyVersion; - var tv = manifest.TestingAssemblyVersion; - var hasAv = av != null; - var hasTv = tv != null; - - if (hasAv && hasTv) - { - return tv > av; - } - else - { - return hasTv; - } - } - - /// - /// Gets a value indicating whether the given repo manifest should be visible to the user. - /// - /// Repo manifest. - /// If the manifest is visible. - public bool IsManifestVisible(RemotePluginManifest manifest) - { - var configuration = Service.Get(); - - // Hidden by user - if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) - return false; - - // Hidden by manifest - if (manifest.IsHide) - return false; - - return true; - } - /// /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard /// checks that should not allow installation or loading. @@ -942,10 +931,7 @@ namespace Dalamud.Plugin.Internal return false; // Banned - if (this.IsManifestBanned(manifest)) - return false; - - return true; + return !this.IsManifestBanned(manifest); } /// @@ -984,7 +970,7 @@ namespace Dalamud.Plugin.Internal .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) .Select(remoteManifest => { - var useTesting = this.UseTesting(remoteManifest); + var useTesting = UseTesting(remoteManifest); var candidateVersion = useTesting ? remoteManifest.TestingAssemblyVersion : remoteManifest.AssemblyVersion; @@ -998,7 +984,7 @@ namespace Dalamud.Plugin.Internal if (updates.Count > 0) { var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); - updatablePlugins.Add(new(plugin, update.remoteManifest, update.useTesting)); + updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting)); } } @@ -1033,41 +1019,6 @@ namespace Dalamud.Plugin.Internal } } - private struct BannedPlugin - { - [JsonProperty] - public string Name { get; private set; } - - [JsonProperty] - public Version AssemblyVersion { get; private set; } - - [JsonProperty] - public string Reason { get; private set; } - } - - private struct PluginDef - { - public PluginDef(FileInfo dllFile, LocalPluginManifest? manifest, bool isDev) - { - this.DllFile = dllFile; - this.Manifest = manifest; - this.IsDev = isDev; - } - - public FileInfo DllFile { get; init; } - - public LocalPluginManifest? Manifest { get; init; } - - public bool IsDev { get; init; } - - public static int Sorter(PluginDef def1, PluginDef def2) - { - var prio1 = def1.Manifest?.LoadPriority ?? 0; - var prio2 = def2.Manifest?.LoadPriority ?? 0; - return prio2.CompareTo(prio1); - } - } - private static class Locs { public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); @@ -1162,7 +1113,7 @@ namespace Dalamud.Plugin.Internal if (methodBase == null) continue; - yield return methodBase.Module.Assembly.FullName; + yield return methodBase.Module.Assembly.FullName!; } } @@ -1171,37 +1122,16 @@ namespace Dalamud.Plugin.Internal var targetType = typeof(PluginManager).Assembly.GetType(); var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod(); - var locationPatch = typeof(PluginManager).GetMethod(nameof(PluginManager.AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static); + var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static); this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch); -#pragma warning disable SYSLIB0012 // Type or member is obsolete - var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase)).GetGetMethod(); - var codebasePatch = typeof(PluginManager).GetMethod(nameof(PluginManager.AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); + #pragma warning disable CS0618 + #pragma warning disable SYSLIB0012 + var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod(); + #pragma warning restore SYSLIB0012 + #pragma warning restore CS0618 + var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch); -#pragma warning restore SYSLIB0012 // Type or member is obsolete - } - - internal record PluginPatchData - { - /// - /// Initializes a new instance of the class. - /// - /// DLL file being loaded. - public PluginPatchData(FileInfo dllFile) - { - this.Location = dllFile.FullName; - this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri; - } - - /// - /// Gets simulated Assembly.Location output. - /// - public string Location { get; } - - /// - /// Gets simulated Assembly.CodeBase output. - /// - public string CodeBase { get; } } } } diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs index 55b77ce49..c00755ec2 100644 --- a/Dalamud/Plugin/Internal/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/PluginRepository.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; -using Dalamud.Utility; using Newtonsoft.Json; namespace Dalamud.Plugin.Internal @@ -19,6 +20,17 @@ namespace Dalamud.Plugin.Internal private static readonly ModuleLog Log = new("PLUGINR"); + private static readonly HttpClient HttpClient = new() + { + DefaultRequestHeaders = + { + CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }, + }, + }; + /// /// Initializes a new instance of the class. /// @@ -74,8 +86,7 @@ namespace Dalamud.Plugin.Internal { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - // ?ticks causes a cache invalidation. Get a fresh repo every time. - using var response = await Util.HttpClient.GetAsync(this.PluginMasterUrl + "?" + DateTime.Now.Ticks); + using var response = await HttpClient.GetAsync(this.PluginMasterUrl); response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); diff --git a/Dalamud/Plugin/Internal/Types/BannedPlugin.cs b/Dalamud/Plugin/Internal/Types/BannedPlugin.cs new file mode 100644 index 000000000..1d5f956b3 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/BannedPlugin.cs @@ -0,0 +1,29 @@ +using System; + +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Banned plugin version that is blocked from installation. +/// +internal struct BannedPlugin +{ + /// + /// Gets plugin name. + /// + [JsonProperty] + public string Name { get; private set; } + + /// + /// Gets plugin assembly version. + /// + [JsonProperty] + public Version AssemblyVersion { get; private set; } + + /// + /// Gets reason for the ban. + /// + [JsonProperty] + public string Reason { get; private set; } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginDef.cs b/Dalamud/Plugin/Internal/Types/PluginDef.cs new file mode 100644 index 000000000..c5dbede0d --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginDef.cs @@ -0,0 +1,50 @@ +using System.IO; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Plugin Definition. +/// +internal struct PluginDef +{ + /// + /// Initializes a new instance of the struct. + /// + /// plugin dll file. + /// plugin manifest. + /// plugin dev indicator. + public PluginDef(FileInfo dllFile, LocalPluginManifest? manifest, bool isDev) + { + this.DllFile = dllFile; + this.Manifest = manifest; + this.IsDev = isDev; + } + + /// + /// Gets plugin DLL File. + /// + public FileInfo DllFile { get; init; } + + /// + /// Gets plugin manifest. + /// + public LocalPluginManifest? Manifest { get; init; } + + /// + /// Gets a value indicating whether plugin is a dev plugin. + /// + public bool IsDev { get; init; } + + /// + /// Sort plugin definitions by priority. + /// + /// plugin definition 1 to compare. + /// plugin definition 2 to compare. + /// sort order. + public static int Sorter(PluginDef def1, PluginDef def2) + { + var priority1 = def1.Manifest?.LoadPriority ?? 0; + var priority2 = def2.Manifest?.LoadPriority ?? 0; + return priority2.CompareTo(priority1); + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginPatchData.cs b/Dalamud/Plugin/Internal/Types/PluginPatchData.cs new file mode 100644 index 000000000..d95c1e62c --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginPatchData.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; + +namespace Dalamud.Plugin.Internal.Types; + +internal record PluginPatchData +{ + /// + /// Initializes a new instance of the class. + /// + /// DLL file being loaded. + public PluginPatchData(FileSystemInfo dllFile) + { + this.Location = dllFile.FullName; + this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri; + } + + /// + /// Gets simulated Assembly.Location output. + /// + public string Location { get; } + + /// + /// Gets simulated Assembly.CodeBase output. + /// + public string CodeBase { get; } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 4cc1f8cee..9c02efe2c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -8,7 +8,6 @@ using System.Net.Http; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using Dalamud.Configuration.Internal; diff --git a/README.md b/README.md index c1ac46dc3..22e77f0c6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,19 @@ If you need any support regarding the API or usage of Dalamud, please [join our Thanks to Mino, whose work has made this possible! +## Components & Pipeline + +These components are used in order to load Dalamud into a target process. +Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint. + +| Name | Purpose | +|---|---| +| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector | +| *Dalamud.Injector* (C#) | Performs DLL injection on the target process | +| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so | +| *Dalamud* (C#) | Core API, game bindings, plugin framework | +| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features | + ## Branches We are currently working from the following branches. diff --git a/lib/CoreCLR/CoreCLR.cpp b/lib/CoreCLR/CoreCLR.cpp index 265f1869e..53288f94f 100644 --- a/lib/CoreCLR/CoreCLR.cpp +++ b/lib/CoreCLR/CoreCLR.cpp @@ -2,12 +2,14 @@ #include "CoreCLR.h" #include +#include #include #include "nethost/nethost.h" -#pragma comment(lib, "nethost/libnethost.lib") - -CoreCLR::CoreCLR() {} +CoreCLR::CoreCLR(void* calling_module) + : m_calling_module(calling_module) +{ +} /* Core public functions */ int CoreCLR::load_hostfxr() @@ -18,19 +20,43 @@ int CoreCLR::load_hostfxr() int CoreCLR::load_hostfxr(const struct get_hostfxr_parameters* parameters) { // Get the path to CoreCLR's hostfxr + std::wstring calling_module_path(MAX_PATH, L'\0'); + + do + { + calling_module_path.resize(GetModuleFileNameW(static_cast(m_calling_module), &calling_module_path[0], static_cast(calling_module_path.size()))); + } + while (!calling_module_path.empty() && GetLastError() == ERROR_INSUFFICIENT_BUFFER); + if (calling_module_path.empty()) + return -1; + + calling_module_path = (std::filesystem::path(calling_module_path).parent_path() / L"nethost.dll").wstring(); + + auto lib_nethost = reinterpret_cast(load_library(calling_module_path.c_str())); + if (!lib_nethost) + return -1; + + auto get_hostfxr_path = reinterpret_cast( + get_export(lib_nethost, "get_hostfxr_path")); + if (!get_hostfxr_path) + return -1; + wchar_t buffer[MAX_PATH]{}; size_t buffer_size = sizeof buffer / sizeof(wchar_t); if (int rc = get_hostfxr_path(buffer, &buffer_size, parameters); rc != 0) return rc; // Load hostfxr and get desired exports - auto lib = reinterpret_cast(load_library(buffer)); + auto lib_hostfxr = reinterpret_cast(load_library(buffer)); + if (!lib_hostfxr) + return -1; + m_hostfxr_initialize_for_runtime_config_fptr = reinterpret_cast( - get_export(lib, "hostfxr_initialize_for_runtime_config")); + get_export(lib_hostfxr, "hostfxr_initialize_for_runtime_config")); m_hostfxr_get_runtime_delegate_fptr = reinterpret_cast( - get_export(lib, "hostfxr_get_runtime_delegate")); + get_export(lib_hostfxr, "hostfxr_get_runtime_delegate")); m_hostfxr_close_fptr = reinterpret_cast( - get_export(lib, "hostfxr_close")); + get_export(lib_hostfxr, "hostfxr_close")); return m_hostfxr_initialize_for_runtime_config_fptr && m_hostfxr_get_runtime_delegate_fptr diff --git a/lib/CoreCLR/CoreCLR.h b/lib/CoreCLR/CoreCLR.h index 71c62d90e..fcce811ef 100644 --- a/lib/CoreCLR/CoreCLR.h +++ b/lib/CoreCLR/CoreCLR.h @@ -5,8 +5,10 @@ #include "nethost/nethost.h" class CoreCLR { - public: - explicit CoreCLR(); + void* const m_calling_module; + +public: + explicit CoreCLR(void* calling_module); ~CoreCLR() = default; int load_hostfxr(); @@ -32,7 +34,7 @@ class CoreCLR { void* reserved, void** delegate) const; - private: +private: /* HostFXR delegates. */ hostfxr_initialize_for_runtime_config_fn m_hostfxr_initialize_for_runtime_config_fptr{}; hostfxr_get_runtime_delegate_fn m_hostfxr_get_runtime_delegate_fptr{}; diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index 63636e709..acdba1909 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -26,6 +26,7 @@ void ConsoleTeardown() std::optional g_clr; int InitializeClrAndGetEntryPoint( + void* calling_module, std::wstring runtimeconfig_path, std::wstring module_path, std::wstring entrypoint_assembly_name, @@ -33,7 +34,7 @@ int InitializeClrAndGetEntryPoint( std::wstring entrypoint_delegate_type_name, void** entrypoint_fn) { - g_clr = CoreCLR(); + g_clr.emplace(calling_module); int result; SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0"); diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h index 8f58042a1..f306563ad 100644 --- a/lib/CoreCLR/boot.h +++ b/lib/CoreCLR/boot.h @@ -2,6 +2,7 @@ void ConsoleSetup(const std::wstring console_name); void ConsoleTeardown(); int InitializeClrAndGetEntryPoint( + void* calling_module, std::wstring runtimeconfig_path, std::wstring module_path, std::wstring entrypoint_assembly_name, diff --git a/lib/CoreCLR/nethost/libnethost.lib b/lib/CoreCLR/nethost/libnethost.lib deleted file mode 100644 index 4291a5387..000000000 Binary files a/lib/CoreCLR/nethost/libnethost.lib and /dev/null differ diff --git a/lib/CoreCLR/nethost/nethost.h b/lib/CoreCLR/nethost/nethost.h index 31adde5e8..aff09b286 100644 --- a/lib/CoreCLR/nethost/nethost.h +++ b/lib/CoreCLR/nethost/nethost.h @@ -6,36 +6,6 @@ #include -#ifdef _WIN32 - #ifdef NETHOST_EXPORT - #define NETHOST_API __declspec(dllexport) - #else - // Consuming the nethost as a static library - // Shouldn't export attempt to dllimport. - #ifdef NETHOST_USE_AS_STATIC - #define NETHOST_API - #else - #define NETHOST_API __declspec(dllimport) - #endif - #endif - - #define NETHOST_CALLTYPE __stdcall - #ifdef _WCHAR_T_DEFINED - typedef wchar_t char_t; - #else - typedef unsigned short char_t; - #endif -#else - #ifdef NETHOST_EXPORT - #define NETHOST_API __attribute__((__visibility__("default"))) - #else - #define NETHOST_API - #endif - - #define NETHOST_CALLTYPE - typedef char char_t; -#endif - #ifdef __cplusplus extern "C" { #endif @@ -87,10 +57,10 @@ struct get_hostfxr_parameters { // The full search for the hostfxr library is done on every call. To minimize the need // to call this function multiple times, pass a large buffer (e.g. PATH_MAX). // -NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path( - char_t * buffer, - size_t * buffer_size, - const struct get_hostfxr_parameters *parameters); +using get_hostfxr_path_type = int(__stdcall *)( + char_t* buffer, + size_t* buffer_size, + const struct get_hostfxr_parameters* parameters); #ifdef __cplusplus } // extern "C" diff --git a/lib/CoreCLR/nethost/nethost.lib b/lib/CoreCLR/nethost/nethost.lib deleted file mode 100644 index 4cd6f0382..000000000 Binary files a/lib/CoreCLR/nethost/nethost.lib and /dev/null differ diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index db6e7c4a7..7d95dce09 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit db6e7c4a7b2638bedc2888c546009c7292c5de45 +Subproject commit 7d95dce097dce7aa6712e4ef499a9d4ad8fafed7