diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..8afb5dcb0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/ImGuiScene"] + path = lib/ImGuiScene + url = https://github.com/ff-meli/ImGuiScene diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index d0901d863..7e377c0f9 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -14,10 +14,10 @@ true - 4.3.1.0 - 4.3.1.0 + 4.7.3.0 + 4.7.3.0 XIVLauncher addon injection - 4.3.1.0 + 4.7.3.0 diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs index db9ceb692..5db47c2dd 100644 --- a/Dalamud.Injector/Program.cs +++ b/Dalamud.Injector/Program.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Windows.Forms; using Dalamud.DiscordBot; using Dalamud.Game.Chat; @@ -17,7 +18,7 @@ namespace Dalamud.Injector { { File.WriteAllText("InjectorException.txt", eventArgs.ExceptionObject.ToString()); - MessageBox.Show("Failed to inject the XIVLauncher in-game addon. Please report this error:\n\n" + eventArgs.ExceptionObject, "XIVLauncher Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show("Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\nIf this keeps happening, please report this error.", "XIVLauncher Error", MessageBoxButtons.OK, MessageBoxIcon.Error); Environment.Exit(0); }; @@ -34,6 +35,7 @@ namespace Dalamud.Injector { process = Process.Start( "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe", "DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 SYS.Region=0 language=1 version=1.0.0.0 DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0"); + Thread.Sleep(1000); break; default: process = Process.GetProcessById(pid); @@ -43,6 +45,9 @@ namespace Dalamud.Injector { var startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1]))); startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); + // Seems to help with the STATUS_INTERNAL_ERROR condition + Thread.Sleep(1000); + // Inject to process Inject(process, startInfo); } diff --git a/Dalamud.sln b/Dalamud.sln index ac5182893..c76935bb0 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -7,30 +7,82 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{85480198-8711-4355-830E-72FD794AD3F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64 + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64 + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64 + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64 + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|Any CPU + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|Any CPU + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.ActiveCfg = Debug|x64 + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.Build.0 = Debug|x64 + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.ActiveCfg = Debug|x86 + {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.Build.0 = Debug|x86 + {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.Build.0 = Release|Any CPU + {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.ActiveCfg = Release|x64 + {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.Build.0 = Release|x64 + {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.ActiveCfg = Release|x86 + {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.Build.0 = Release|x86 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|Any CPU + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/DalamudConfiguration.cs index 752865f51..802645cec 100644 --- a/Dalamud/Configuration/DalamudConfiguration.cs +++ b/Dalamud/Configuration/DalamudConfiguration.cs @@ -29,7 +29,9 @@ namespace Dalamud public string LastVersion { get; set; } - public Dictionary PluginConfigurations { get; set; } + public Dictionary PluginConfigurations { get; set; } + + public bool WelcomeGuideDismissed; public static DalamudConfiguration Load(string path) { return JsonConvert.DeserializeObject(File.ReadAllText(path)); diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 94a8e5892..acc599411 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.DiscordBot; using Dalamud.Game; using Dalamud.Game.Chat; @@ -16,7 +18,9 @@ using Dalamud.Game.Command; using Dalamud.Game.Internal; using Dalamud.Game.Internal.Gui; using Dalamud.Game.Network; +using Dalamud.Interface; using Dalamud.Plugin; +using ImGuiNET; using Serilog; namespace Dalamud { @@ -25,9 +29,9 @@ namespace Dalamud { private readonly ManualResetEvent unloadSignal; - public readonly ProcessModule TargetModule; + private readonly ProcessModule targetModule; - private readonly SigScanner sigScanner; + public readonly SigScanner SigScanner; public Framework Framework { get; } @@ -45,7 +49,13 @@ namespace Dalamud { public readonly DalamudConfiguration Configuration; - internal readonly WinSockHandlers WinSock2; + private readonly WinSockHandlers WinSock2; + + public readonly InterfaceManager InterfaceManager; + + public readonly DataManager Data; + + private readonly string assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); public Dalamud(DalamudStartInfo info) { this.StartInfo = info; @@ -56,11 +66,11 @@ namespace Dalamud { this.unloadSignal = new ManualResetEvent(false); // Initialize the process information. - this.TargetModule = Process.GetCurrentProcess().MainModule; - this.sigScanner = new SigScanner(this.TargetModule); + this.targetModule = Process.GetCurrentProcess().MainModule; + SigScanner = new SigScanner(this.targetModule, true); // Initialize game subsystem - Framework = new Framework(this.sigScanner, this); + Framework = new Framework(this.SigScanner, this); // Initialize managers. Basically handlers for the logic CommandManager = new CommandManager(this, info.Language); @@ -69,7 +79,11 @@ namespace Dalamud { ChatHandlers = new ChatHandlers(this); NetworkHandlers = new NetworkHandlers(this, this.Configuration.OptOutMbCollection); - this.ClientState = new ClientState(this, info, this.sigScanner, this.TargetModule); + this.Data = new DataManager(); + //Task.Run(() => ); + this.Data.Initialize(); + + this.ClientState = new ClientState(this, info, this.SigScanner, this.targetModule); this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig); @@ -78,18 +92,34 @@ namespace Dalamud { this.WinSock2 = new WinSockHandlers(); try { - this.PluginManager.LoadPlugins(); - } catch (Exception ex) { - Framework.Gui.Chat.PrintError( - "[XIVLAUNCHER] There was an error loading additional plugins. Please check the log for more details."); - Log.Error(ex, "Plugin load failed."); + this.InterfaceManager = new InterfaceManager(this, this.SigScanner); + this.InterfaceManager.OnDraw += BuildDalamudUi; + } catch (Exception e) { + Log.Information(e, "Could not init interface."); } } public void Start() { + try { + this.InterfaceManager?.Enable(); + } catch (Exception e) { + Log.Information("Could not enable interface."); + } + Framework.Enable(); this.BotManager.Start(); + + try + { + this.PluginManager.LoadPlugins(); + } + catch (Exception ex) + { + Framework.Gui.Chat.PrintError( + "[XIVLAUNCHER] There was an error loading additional plugins. Please check the log for more details."); + Log.Error(ex, "Plugin load failed."); + } } public void Unload() { @@ -101,6 +131,19 @@ namespace Dalamud { } public void Dispose() { + try + { + this.PluginManager.UnloadPlugins(); + } + catch (Exception ex) + { + Framework.Gui.Chat.PrintError( + "[XIVLAUNCHER] There was an error unloading additional plugins. Please check the log for more details."); + Log.Error(ex, "Plugin unload failed."); + } + + this.InterfaceManager.Dispose(); + Framework.Dispose(); this.BotManager.Dispose(); @@ -108,8 +151,120 @@ namespace Dalamud { this.unloadSignal.Dispose(); this.WinSock2.Dispose(); + + this.SigScanner.Dispose(); } + #region Interface + + private bool isImguiDrawDemoWindow = false; + private bool isImguiDrawWelcome = true; + +#if DEBUG + private bool isImguiDrawDevMenu = true; +#else + private bool isImguiDrawDevMenu = false; +#endif + + private bool isImguiDrawLogWindow = false; + private bool isImguiDrawDataWindow = false; + + private bool neverDrawWelcome = false; + + private DalamudLogWindow logWindow; + private DalamudDataWindow dataWindow; + + private void BuildDalamudUi() + { + if (this.isImguiDrawDevMenu) + { + if (ImGui.BeginMainMenuBar()) + { + if (ImGui.BeginMenu("Dalamud")) + { + ImGui.MenuItem("Draw Dalamud dev menu", "", ref this.isImguiDrawDevMenu); + ImGui.Separator(); + if (ImGui.MenuItem("Open Log window")) + { + this.logWindow = new DalamudLogWindow(); + this.isImguiDrawLogWindow = true; + } + if (ImGui.MenuItem("Open Data window")) + { + this.dataWindow = new DalamudDataWindow(this.Data); + this.isImguiDrawDataWindow = true; + } + ImGui.MenuItem("Draw ImGui demo", "", ref this.isImguiDrawDemoWindow); + ImGui.Separator(); + if (ImGui.MenuItem("Unload Dalamud")) + { + Unload(); + } + if (ImGui.MenuItem("Kill game")) + { + Process.GetCurrentProcess().Kill(); + } + + ImGui.EndMenu(); + } + + if (ImGui.BeginMenu("Plugins")) + { + if (ImGui.MenuItem("Reload plugins")) + { + OnPluginReloadCommand(string.Empty, string.Empty); + } + ImGui.EndMenu(); + } + + //ImGui.EndMainMenuBar(); + } + } + + if (this.isImguiDrawLogWindow) + { + this.isImguiDrawLogWindow = this.logWindow != null && this.logWindow.Draw(); + + if (this.isImguiDrawLogWindow == false) + { + this.logWindow?.Dispose(); + } + } + + if (this.isImguiDrawDataWindow) + { + this.isImguiDrawDataWindow = this.dataWindow != null && this.dataWindow.Draw(); + } + + if (this.isImguiDrawDemoWindow) + ImGui.ShowDemoWindow(); + + if (!this.Configuration.WelcomeGuideDismissed) + { + if (!ImGui.Begin("Welcome to XIVLauncher", ImGuiWindowFlags.Modal | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) + { + // Early out if the window is collapsed, as an optimization. + ImGui.End(); + } + else + { + ImGui.Text($"dalamud says hello. ({this.assemblyVersion})"); + ImGui.Spacing(); + ImGui.Spacing(); + + if (ImGui.Button("Close")) + { + this.Configuration.WelcomeGuideDismissed = true; + this.Configuration.Save(this.StartInfo.ConfigurationPath); + } + + ImGui.End(); + } + } + } + + #endregion + private void SetupCommands() { CommandManager.AddHandler("/xldclose", new CommandInfo(OnUnloadCommand) { HelpMessage = "Unloads XIVLauncher in-game addon.", @@ -177,6 +332,11 @@ namespace Dalamud { { HelpMessage = "Notify when a roulette has a bonus you specified. Run without parameters for more info. Usage: /xlbonus " }); + + CommandManager.AddHandler("/xldev", new CommandInfo(OnDebugDrawDevMenu) { + HelpMessage = "Draw dev menu DEBUG", + ShowInHelp = false + }); } private void OnUnloadCommand(string command, string arguments) { @@ -394,6 +554,10 @@ namespace Dalamud { "Possible values for role: tank, dps, healer, all, none/reset"); } + private void OnDebugDrawDevMenu(string command, string arguments) { + this.isImguiDrawDevMenu = true; + } + private int RouletteSlugToKey(string slug) => slug.ToLower() switch { "leveling" => 1, "506070" => 2, diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6a9a5ee73..b807f608e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -1,7 +1,7 @@ - + AnyCPU - net471 + net472 8.0 AnyCPU;x64 @@ -14,9 +14,9 @@ true - 4.3.1.0 - 4.3.1.0 - 4.3.1.0 + 4.7.3.0 + 4.7.3.0 + 4.7.3.0 @@ -41,13 +41,14 @@ - + + @@ -65,4 +66,23 @@ Resources.Designer.cs + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs new file mode 100644 index 000000000..2cf8202f7 --- /dev/null +++ b/Dalamud/Data/DataManager.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Serilog; + +namespace Dalamud.Data +{ + /// + /// This class provides data for Dalamud-internal features, but can also be used by plugins if needed. + /// + public class DataManager { + private const string DataBaseUrl = "https://goaaats.github.io/ffxiv/tools/launcher/addons/Hooks/Data/"; + + public ReadOnlyDictionary ServerOpCodes; + public ReadOnlyDictionary ContentFinderCondition; + + public bool IsDataReady { get; private set; } + + public DataManager() { + // Set up default values so plugins do not null-reference when data is being loaded. + this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); + this.ContentFinderCondition = new ReadOnlyDictionary(new Dictionary()); + } + + public async Task Initialize() { + try { + Log.Verbose("Starting data download..."); + + using var client = new HttpClient() { + BaseAddress = new Uri(DataBaseUrl) + }; + + var opCodeDict = + JsonConvert.DeserializeObject>( + await client.GetStringAsync(DataBaseUrl + "serveropcode.json")); + this.ServerOpCodes = new ReadOnlyDictionary(opCodeDict); + + Log.Verbose("Loaded {0} ServerOpCodes.", opCodeDict.Count); + + var cfcs = JsonConvert.DeserializeObject>( + await client.GetStringAsync(DataBaseUrl + "contentfindercondition.json")); + this.ContentFinderCondition = new ReadOnlyDictionary(cfcs); + + Log.Verbose("Loaded {0} ContentFinderCondition.", cfcs.Count); + + IsDataReady = true; + } catch (Exception ex) { + Log.Error(ex, "Could not download data."); + } + } + } +} diff --git a/Dalamud/DiscordBot/DiscordBotManager.cs b/Dalamud/DiscordBot/DiscordBotManager.cs index fdf1a945f..10b56663c 100644 --- a/Dalamud/DiscordBot/DiscordBotManager.cs +++ b/Dalamud/DiscordBot/DiscordBotManager.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; +using System.IO.Ports; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.Chat; +using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; +using Dalamud.Game.Internal.Libc; using Discord; using Discord.WebSocket; using Newtonsoft.Json.Linq; @@ -38,6 +42,7 @@ namespace Dalamud.DiscordBot { this.socketClient = new DiscordSocketClient(); this.socketClient.Ready += SocketClientOnReady; + this.dalamud.NetworkHandlers.ProcessCfPop += ProcessCfPop; } private XivChatType GetChatTypeBySlug(string slug) { @@ -80,29 +85,10 @@ namespace Dalamud.DiscordBot { return Task.CompletedTask; } - public async Task ProcessFate(int id) { - if (this.config.FateNotificationChannel == null) + public async Task ProcessCfPop(JObject contentFinderCondition) { + if (!this.IsConnected) return; - var channel = await GetChannel(this.config.FateNotificationChannel); - - dynamic fateInfo = XivApi.GetFate(id).GetAwaiter().GetResult(); - - this.dalamud.Framework.Gui.Chat.Print("Watched Fate spawned: " + (string) fateInfo.Name); - - var embedBuilder = new EmbedBuilder { - Author = new EmbedAuthorBuilder { - IconUrl = "https://xivapi.com" + (string) fateInfo.Icon, - Name = "Fate spawned: " + (string) fateInfo.Name - }, - Color = new Color(0xa73ed1), - Timestamp = DateTimeOffset.Now - }; - - await channel.SendMessageAsync(embed: embedBuilder.Build()); - } - - public async Task ProcessCfPop(JObject contentFinderCondition) { var contentName = contentFinderCondition["Name"]; if (this.config.CfNotificationChannel == null) @@ -175,12 +161,11 @@ namespace Dalamud.DiscordBot { await channel.SendMessageAsync(embed: embedBuilder.Build()); } - public async Task ProcessChatMessage(XivChatType type, string message, string sender) { + public async Task ProcessChatMessage(XivChatType type, StdString message, StdString sender) { // Special case for outgoing tells, these should be sent under Incoming tells var wasOutgoingTell = false; if (type == XivChatType.TellOutgoing) { type = XivChatType.TellIncoming; - sender = this.dalamud.ClientState.LocalPlayer.Name; wasOutgoingTell = true; } @@ -191,32 +176,34 @@ namespace Dalamud.DiscordBot { return; var chatTypeDetail = type.GetDetails(); - var channels = chatTypeConfigs.Select(c => GetChannel(c.Channel).GetAwaiter().GetResult()); - var senderSplit = sender.Split(new[] {this.worldIcon}, StringSplitOptions.None); + var parsedSender = SeString.Parse(sender.RawData); + var playerLink = parsedSender.Payloads.FirstOrDefault(x => x.Type == PayloadType.Player) as PlayerPayload; - var world = string.Empty; + var senderName = string.Empty; + var senderWorld = string.Empty; - if (this.dalamud.ClientState.Actors.Length > 0) - world = this.dalamud.ClientState.LocalPlayer.CurrentWorld.Name; + if (playerLink == null) { + Log.Error("playerLink was null. Sender: {0}", BitConverter.ToString(sender.RawData)); - if (senderSplit.Length == 2) { - world = senderSplit[1]; - sender = senderSplit[0]; + senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : parsedSender.TextValue; + senderWorld = this.dalamud.ClientState.LocalPlayer.HomeWorld.Name; + } else { + playerLink.Resolve(); + + senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : playerLink.PlayerName; + senderWorld = playerLink.ServerName; } - sender = SeString.Parse(sender).Output; - message = SeString.Parse(message).Output; + var rawMessage = SeString.Parse(message.RawData).TextValue; - sender = RemoveAllNonLanguageCharacters(sender); - - var avatarUrl = ""; - var lodestoneId = ""; + var avatarUrl = string.Empty; + var lodestoneId = string.Empty; if (!this.config.DisableEmbeds) { - var searchResult = await GetCharacterInfo(sender, world); + var searchResult = await GetCharacterInfo(senderName, senderWorld); lodestoneId = searchResult.LodestoneId; avatarUrl = searchResult.AvatarUrl; @@ -226,9 +213,9 @@ namespace Dalamud.DiscordBot { var name = wasOutgoingTell ? "You" - : sender + (string.IsNullOrEmpty(world) || string.IsNullOrEmpty(sender) + : senderName + (string.IsNullOrEmpty(senderWorld) || string.IsNullOrEmpty(senderName) ? "" - : $" on {world}"); + : $" on {senderWorld}"); for (var chatTypeIndex = 0; chatTypeIndex < chatTypeConfigs.Count(); chatTypeIndex++) { if (!this.config.DisableEmbeds) { @@ -240,7 +227,7 @@ namespace Dalamud.DiscordBot { Name = name, Url = !string.IsNullOrEmpty(lodestoneId) ? "https://eu.finalfantasyxiv.com/lodestone/character/" + lodestoneId : null }, - Description = message, + Description = rawMessage, Timestamp = DateTimeOffset.Now, Footer = new EmbedFooterBuilder { Text = type.GetDetails().FancyName }, Color = new Color((uint)(chatTypeConfigs.ElementAt(chatTypeIndex).Color & 0xFFFFFF)) @@ -271,7 +258,7 @@ namespace Dalamud.DiscordBot { await channels.ElementAt(chatTypeIndex).SendMessageAsync(embed: embedBuilder.Build()); } else { - var simpleMessage = $"{name}: {message}"; + var simpleMessage = $"{name}: {rawMessage}"; if (this.config.CheckForDuplicateMessages) { var recentMsg = this.recentMessages.FirstOrDefault( @@ -285,7 +272,7 @@ namespace Dalamud.DiscordBot { } } - await channels.ElementAt(chatTypeIndex).SendMessageAsync($"**[{chatTypeDetail.Slug}]{name}**: {message}"); + await channels.ElementAt(chatTypeIndex).SendMessageAsync($"**[{chatTypeDetail.Slug}]{name}**: {rawMessage}"); } } } @@ -317,10 +304,6 @@ namespace Dalamud.DiscordBot { return await this.socketClient.GetUser(channelConfig.ChannelId).GetOrCreateDMChannelAsync(); } - private string RemoveAllNonLanguageCharacters(string input) { - return Regex.Replace(input, @"[^\p{L} ']", ""); - } - public void Dispose() { this.socketClient.LogoutAsync().GetAwaiter().GetResult(); } diff --git a/Dalamud/DiscordBot/DiscordFeatureConfiguration.cs b/Dalamud/DiscordBot/DiscordFeatureConfiguration.cs index dcf86dd22..551f3d8ad 100644 --- a/Dalamud/DiscordBot/DiscordFeatureConfiguration.cs +++ b/Dalamud/DiscordBot/DiscordFeatureConfiguration.cs @@ -44,7 +44,6 @@ namespace Dalamud.DiscordBot public ChannelConfiguration CfNotificationChannel { get; set; } public ChannelConfiguration CfPreferredRoleChannel { get; set; } - public ChannelConfiguration FateNotificationChannel { get; set; } public ChannelConfiguration RetainerNotificationChannel { get; set; } } } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 680de45e0..232640f5e 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Net; +using Dalamud.Interface; using EasyHook; using Serilog; using Serilog.Core; @@ -17,6 +19,10 @@ namespace Dalamud { try { Log.Information("Initializing a session.."); + // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally + System.Net.ServicePointManager.SecurityProtocol = + SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; + // Log any unhandled exception. AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; @@ -42,6 +48,7 @@ namespace Dalamud { return new LoggerConfiguration() .WriteTo.Async(a => a.File(logPath)) + .WriteTo.EventSink() #if DEBUG .MinimumLevel.Verbose() #else diff --git a/Dalamud/Game/Chat/SeString.cs b/Dalamud/Game/Chat/SeString.cs deleted file mode 100644 index 21203baa0..000000000 --- a/Dalamud/Game/Chat/SeString.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Dalamud.Game.Chat { - // TODO: This class does not work - it's a hack, needs a revamp and better handling for payloads used in player chat - public class SeString { - public enum PlayerLinkType { - ItemLink = 0x03 - } - - public enum SeStringPayloadType { - PlayerLink = 0x27 - } - - // in all likelihood these are flags of some kind, but these are the only 2 values I've noticed - public enum ItemQuality { - NormalQuality = 0xF2, - HighQuality = 0xF6 - } - - private const int START_BYTE = 0x02; - private const int END_BYTE = 0x03; - - public static (string Output, List Payloads) Parse(byte[] bytes) - { - var output = new List(); - var payloads = new List(); - - using (var stream = new MemoryStream(bytes)) - using (var reader = new BinaryReader(stream)) - { - while (stream.Position < bytes.Length) - { - var b = stream.ReadByte(); - - if (b == START_BYTE) - ProcessPacket(reader, output, payloads); - else - output.Add((byte)b); - } - } - - return (Encoding.UTF8.GetString(output.ToArray()), payloads); - } - - public static (string Output, List Payloads) Parse(string input) { - var bytes = Encoding.UTF8.GetBytes(input); - return Parse(bytes); - } - - private static void ProcessPacket(BinaryReader reader, List output, - List payloads) { - var type = reader.ReadByte(); - var payloadSize = GetInteger(reader); - - var payload = new byte[payloadSize]; - - reader.Read(payload, 0, payloadSize); - - var orphanByte = reader.Read(); - // If the end of the tag isn't what we predicted, let's ignore it for now - while (orphanByte != END_BYTE) orphanByte = reader.Read(); - - //output.AddRange(Encoding.UTF8.GetBytes($"<{type.ToString("X")}:{BitConverter.ToString(payload)}>")); - - switch ((SeStringPayloadType) type) { - case SeStringPayloadType.PlayerLink: - if (payload[0] == (byte)PlayerLinkType.ItemLink) - { - int itemId; - bool isHQ = payload[1] == (byte)ItemQuality.HighQuality; - if (isHQ) - { - // hq items have an extra 0x0F byte before the ID, and the ID is 0x4240 above the actual item ID - // This _seems_ consistent but I really don't know - itemId = (payload[3] << 8 | payload[4]) - 0x4240; - } - else - { - itemId = (payload[2] << 8 | payload[3]); - } - - payloads.Add(new SeStringPayloadContainer - { - Type = SeStringPayloadType.PlayerLink, - Param1 = (itemId, isHQ) - }); - } - - break; - } - } - - public class SeStringPayloadContainer { - public SeStringPayloadType Type { get; set; } - public object Param1 { get; set; } - } - - #region Shared - - public enum IntegerType { - Byte = 0xF0, - ByteTimes256 = 0xF1, - Int16 = 0xF2, - Int24 = 0xFA, - Int32 = 0xFE - } - - protected static int GetInteger(BinaryReader input) { - var t = input.ReadByte(); - var type = (IntegerType) t; - return GetInteger(input, type); - } - - protected static int GetInteger(BinaryReader input, IntegerType type) { - const byte ByteLengthCutoff = 0xF0; - - var t = (byte) type; - if (t < ByteLengthCutoff) - return t - 1; - - switch (type) { - case IntegerType.Byte: - return input.ReadByte(); - case IntegerType.ByteTimes256: - return input.ReadByte() * 256; - case IntegerType.Int16: { - var v = 0; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - case IntegerType.Int24: { - var v = 0; - v |= input.ReadByte() << 16; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - case IntegerType.Int32: { - var v = 0; - v |= input.ReadByte() << 24; - v |= input.ReadByte() << 16; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - default: - throw new NotSupportedException(); - } - } - - #endregion - } -} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs new file mode 100644 index 000000000..f5ff7aed0 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Game.Chat.SeStringHandling.Payloads; +using Serilog; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// This class represents a parsed SeString payload. + /// + public abstract class Payload + { + public abstract PayloadType Type { get; } + + public abstract void Resolve(); + + public abstract byte[] Encode(); + + protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream); + + public static Payload Process(BinaryReader reader) + { + if ((byte)reader.PeekChar() != START_BYTE) + { + return ProcessText(reader); + } + else + { + return ProcessChunk(reader); + } + } + + private static Payload ProcessChunk(BinaryReader reader) + { + Payload payload = null; + + reader.ReadByte(); // START_BYTE + var chunkType = (SeStringChunkType)reader.ReadByte(); + var chunkLen = GetInteger(reader); + + var packetStart = reader.BaseStream.Position; + + switch (chunkType) + { + case SeStringChunkType.Interactable: + { + var subType = (EmbeddedInfoType)reader.ReadByte(); + switch (subType) + { + case EmbeddedInfoType.PlayerName: + payload = new PlayerPayload(); + break; + + case EmbeddedInfoType.ItemLink: + payload = new ItemPayload(); + break; + + case EmbeddedInfoType.Status: + payload = new StatusPayload(); + break; + case EmbeddedInfoType.LinkTerminator: + // Does not need to be handled + break; + default: + Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType); + break; + } + } + break; + default: + Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); + break; + } + + payload?.ProcessChunkImpl(reader, reader.BaseStream.Position + chunkLen - 1); + + // read through the rest of the packet + var readBytes = (int)(reader.BaseStream.Position - packetStart); + reader.ReadBytes(chunkLen - readBytes + 1); // +1 for the END_BYTE marker + + return payload; + } + + private static Payload ProcessText(BinaryReader reader) + { + var payload = new TextPayload(); + payload.ProcessChunkImpl(reader, reader.BaseStream.Length); + + return payload; + } + + #region parse constants and helpers + + protected const byte START_BYTE = 0x02; + protected const byte END_BYTE = 0x03; + + protected enum SeStringChunkType + { + Interactable = 0x27 + } + + protected enum EmbeddedInfoType + { + PlayerName = 0x01, + ItemLink = 0x03, + Status = 0x09, + + LinkTerminator = 0xCF // not clear but seems to always follow a link + } + + protected enum IntegerType + { + Byte = 0xF0, + ByteTimes256 = 0xF1, + Int16 = 0xF2, + Int16Plus1Million = 0xF6, + Int24 = 0xFA, + Int32 = 0xFE + } + + // made protected, unless we actually want to use it externally + // in which case it should probably go live somewhere else + protected static int GetInteger(BinaryReader input) + { + var t = input.ReadByte(); + var type = (IntegerType)t; + return GetInteger(input, type); + } + + private static int GetInteger(BinaryReader input, IntegerType type) + { + const byte ByteLengthCutoff = 0xF0; + + var t = (byte)type; + if (t < ByteLengthCutoff) + return t - 1; + + switch (type) + { + case IntegerType.Byte: + return input.ReadByte(); + case IntegerType.ByteTimes256: + return input.ReadByte() * 256; + case IntegerType.Int16: + { + var v = 0; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + case IntegerType.Int16Plus1Million: + { + var v = 0; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + // need the actual value since it's used as a flag + // v -= 1000000; + return v; + } + case IntegerType.Int24: + { + var v = 0; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + case IntegerType.Int32: + { + var v = 0; + v |= input.ReadByte() << 24; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + default: + throw new NotSupportedException(); + } + } + + protected static byte[] MakeInteger(int value) + { + // clearly the epitome of efficiency + + var bytesPadded = BitConverter.GetBytes(value); + Array.Reverse(bytesPadded); + return bytesPadded.SkipWhile(b => b == 0x00).ToArray(); + } + + protected static IntegerType GetTypeForIntegerBytes(byte[] bytes) + { + // not the most scientific, exists mainly for laziness + + if (bytes.Length == 1) + { + return IntegerType.Byte; + } + else if (bytes.Length == 2) + { + return IntegerType.Int16; + } + else if (bytes.Length == 3) + { + return IntegerType.Int24; + } + else if (bytes.Length == 4) + { + return IntegerType.Int32; + } + + throw new NotSupportedException(); + } + #endregion + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs new file mode 100644 index 000000000..87a6a0461 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// All parsed types of SeString payloads. + /// + public enum PayloadType + { + /// + /// An SeString payload representing a player link. + /// + Player, + /// + /// An SeString payload representing an Item link. + /// + Item, + /// + /// An SeString payload representing an Status Effect link. + /// + Status, + /// + /// An SeString payload representing raw, typed text. + /// + RawText + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs new file mode 100644 index 000000000..d8bad2c0b --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class ItemPayload : Payload + { + public override PayloadType Type => PayloadType.Item; + + public int ItemId { get; private set; } + public string ItemName { get; private set; } = string.Empty; + public bool IsHQ { get; private set; } = false; + + public ItemPayload() { } + + public ItemPayload(int itemId, bool isHQ) + { + ItemId = itemId; + IsHQ = isHQ; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(ItemName)) + { + dynamic item = XivApi.GetItem(ItemId).GetAwaiter().GetResult(); + ItemName = item.Name; + } + } + + public override byte[] Encode() + { + var actualItemId = IsHQ ? ItemId + 1000000 : ItemId; + var idBytes = MakeInteger(actualItemId); + + var itemIdFlag = IsHQ ? IntegerType.Int16Plus1Million : IntegerType.Int16; + + var chunkLen = idBytes.Length + 5; + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink, + (byte)itemIdFlag + }; + bytes.AddRange(idBytes); + // unk + bytes.AddRange(new byte[] { 0x02, 0x01, END_BYTE }); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - ItemId: {ItemId}, ItemName: {ItemName}, IsHQ: {IsHQ}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + ItemId = GetInteger(reader); + + if (ItemId > 1000000) + { + ItemId -= 1000000; + IsHQ = true; + } + + if (reader.BaseStream.Position + 3 < endOfStream) + { + // unk + reader.ReadBytes(3); + + var itemNameLen = GetInteger(reader); + ItemName = Encoding.UTF8.GetString(reader.ReadBytes(itemNameLen)); + } + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs new file mode 100644 index 000000000..15f555445 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class PlayerPayload : Payload + { + public override PayloadType Type => PayloadType.Player; + + public string PlayerName { get; private set; } + public int ServerId { get; private set; } + public string ServerName { get; private set; } = String.Empty; + + public PlayerPayload() { } + + public PlayerPayload(string playerName, int serverId) + { + PlayerName = playerName; + ServerId = serverId; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(ServerName)) + { + dynamic server = XivApi.Get($"World/{ServerId}").GetAwaiter().GetResult(); + ServerName = server.Name; + } + } + + public override byte[] Encode() + { + var chunkLen = PlayerName.Length + 7; + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.PlayerName, + /* unk */ 0x01, + (byte)(ServerId+1), // I didn't want to deal with single-byte values in MakeInteger, so we have to do the +1 manually + /* unk */0x01, /* unk */0xFF, // these sometimes vary but are frequently this + (byte)(PlayerName.Length+1) + }; + + bytes.AddRange(Encoding.UTF8.GetBytes(PlayerName)); + bytes.Add(END_BYTE); + + // encoded names are followed by the name in plain text again + // use the payload parsing for consistency, as this is technically a new chunk + bytes.AddRange(new TextPayload(PlayerName).Encode()); + + // unsure about this entire packet, but it seems to always follow a name + bytes.AddRange(new byte[] + { + START_BYTE, (byte)SeStringChunkType.Interactable, 0x07, (byte)EmbeddedInfoType.LinkTerminator, + 0x01, 0x01, 0x01, 0xFF, 0x01, + END_BYTE + }); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - PlayerName: {PlayerName}, ServerId: {ServerId}, ServerName: {ServerName}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + // unk + reader.ReadByte(); + + ServerId = GetInteger(reader); + + // unk + reader.ReadBytes(2); + + var nameLen = GetInteger(reader); + PlayerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen)); + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs new file mode 100644 index 000000000..4169ac42a --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class StatusPayload : Payload + { + public override PayloadType Type => PayloadType.Status; + + public int StatusId { get; private set; } + + public string StatusName { get; private set; } = string.Empty; + + public StatusPayload() { } + + public StatusPayload(int statusId) + { + StatusId = statusId; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(StatusName)) + { + dynamic status = XivApi.Get($"Status/{StatusId}").GetAwaiter().GetResult(); + //Console.WriteLine($"Resolved status {StatusId} to {status.Name}"); + StatusName = status.Name; + } + } + + public override byte[] Encode() + { + var idBytes = MakeInteger(StatusId); + var idPrefix = GetTypeForIntegerBytes(idBytes); + + var chunkLen = idBytes.Length + 8; + var bytes = new List() + { + START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status, + (byte)idPrefix + }; + + bytes.AddRange(idBytes); + // unk + bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE }); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - StatusId: {StatusId}, StatusName: {StatusName}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + StatusId = GetInteger(reader); + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs new file mode 100644 index 000000000..c2fad1ea9 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class TextPayload : Payload + { + public override PayloadType Type => PayloadType.RawText; + + public string Text { get; private set; } + + public TextPayload() { } + + public TextPayload(string text) + { + Text = text; + } + + public override void Resolve() + { + // nothing to do + } + + public override byte[] Encode() + { + return Encoding.UTF8.GetBytes(Text); + } + + public override string ToString() + { + return $"{Type} - Text: {Text}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + var text = new List(); + + while (reader.BaseStream.Position < endOfStream) + { + if ((byte)reader.PeekChar() == START_BYTE) + break; + + // not the most efficient, but the easiest + text.Add(reader.ReadByte()); + } + + if (text.Count > 0) + { + // TODO: handling of the game's assorted special unicode characters + Text = Encoding.UTF8.GetString(text.ToArray()); + } + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/SeString.cs b/Dalamud/Game/Chat/SeStringHandling/SeString.cs new file mode 100644 index 000000000..f0ab270b7 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/SeString.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Game.Chat.SeStringHandling.Payloads; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// This class represents a parsed SeString. + /// + public class SeString + { + private Dictionary> mappedPayloads_ = null; + + public List Payloads { get; } + + public Dictionary> MappedPayloads + { + get + { + if (mappedPayloads_ == null) + { + mappedPayloads_ = new Dictionary>(); + foreach (var p in Payloads) + { + if (!mappedPayloads_.ContainsKey(p.Type)) + { + mappedPayloads_[p.Type] = new List(); + } + mappedPayloads_[p.Type].Add(p); + } + } + + return mappedPayloads_; + } + } + + public SeString(List payloads) + { + Payloads = payloads; + } + + /// + /// Helper function to get all raw text from a message as a single joined string + /// + /// + /// All the raw text from the contained payloads, joined into a single string + /// + public string TextValue + { + get { + var sb = new StringBuilder(); + foreach (var p in Payloads) + { + if (p.Type == PayloadType.RawText) + { + sb.Append(((TextPayload)p).Text); + } + } + + return sb.ToString(); + } + } + + /// + /// Parse an array of bytes to a SeString. + /// + /// + /// + public static SeString Parse(byte[] bytes) + { + var payloads = new List(); + + using (var stream = new MemoryStream(bytes)) { + using var reader = new BinaryReader(stream); + + while (stream.Position < bytes.Length) + { + var payload = Payload.Process(reader); + if (payload != null) + payloads.Add(payload); + } + } + + return new SeString(payloads); + } + + /// + /// Encode a parsed/created SeString to an array of bytes, to be used for injection. + /// + /// + /// The bytes of the message. + public static byte[] Encode(List payloads) + { + var messageBytes = new List(); + foreach (var p in payloads) + { + messageBytes.AddRange(p.Encode()); + } + + return messageBytes.ToArray(); + } + } +} diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 1bdfd9552..5420bfebc 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -7,6 +7,8 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Dalamud.Game.Chat; +using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.Internal.Libc; using Serilog; @@ -149,10 +151,16 @@ namespace Dalamud.Game { var itemInfo = matchInfo.Groups["item"]; if (!itemInfo.Success) continue; - //var itemName = SeString.Parse(itemInfo.Value).Output; - var (itemId, isHQ) = (ValueTuple)(SeString.Parse(message.RawData).Payloads[0].Param1); - Log.Debug($"Probable retainer sale: {message}, decoded item {itemId}, HQ {isHQ}"); + var itemLink = + SeString.Parse(message.RawData).Payloads.First(x => x.Type == PayloadType.Item) as ItemPayload; + + if (itemLink == null) { + Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.RawData)); + break; + } + + Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.ItemId}, HQ {itemLink.IsHQ}"); int itemValue = 0; var valueInfo = matchInfo.Groups["value"]; @@ -160,16 +168,16 @@ namespace Dalamud.Game { if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out itemValue)) continue; - Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemId, itemValue, isHQ)); + Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.ItemId, itemValue, itemLink.IsHQ)); break; } } + var messageCopy = message; + var senderCopy = sender; + this.dalamud.BotManager.ProcessChatMessage(type, messageCopy, senderCopy); - Task.Run(() => this.dalamud.BotManager.ProcessChatMessage(type, messageVal, senderVal).GetAwaiter() - .GetResult()); - - + // Handle all of this with SeString some day if ((this.HandledChatTypeColors.ContainsKey(type) || type == XivChatType.Say || type == XivChatType.Shout || type == XivChatType.Alliance || type == XivChatType.TellOutgoing || type == XivChatType.Yell) && !message.Value.Contains((char)0x02)) { var italicsStart = message.Value.IndexOf("*"); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 2bb445adb..5f883a0d6 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -108,7 +108,7 @@ namespace Dalamud.Game.Command { this.commandMap.Add(command, info); return true; } catch (ArgumentException) { - Log.Warning("Command {CommandName} is already registered.", command); + Log.Error("Command {CommandName} is already registered.", command); return false; } } diff --git a/Dalamud/Game/Internal/DXGI/ISwapChainAddressResolver.cs b/Dalamud/Game/Internal/DXGI/ISwapChainAddressResolver.cs new file mode 100644 index 000000000..ad2e003f6 --- /dev/null +++ b/Dalamud/Game/Internal/DXGI/ISwapChainAddressResolver.cs @@ -0,0 +1,7 @@ +using System; + +namespace Dalamud.Game.Internal.DXGI { + public interface ISwapChainAddressResolver { + IntPtr Present { get; set; } + } +} diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs new file mode 100644 index 000000000..37c28197e --- /dev/null +++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog; + +namespace Dalamud.Game.Internal.DXGI +{ + public sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver + { + public IntPtr Present { get; set; } + //public IntPtr ResizeBuffers { get; private set; } + + protected override void Setup64Bit(SigScanner sig) + { + var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "dxgi.dll"); + + Log.Debug($"Found DXGI: {module.BaseAddress.ToInt64():X}"); + + var scanner = new SigScanner(module); + + // This(code after the function head - offset of it) was picked to avoid running into issues with other hooks being installed into this function. + Present = scanner.ScanModule("41 8B F0 8B FA 89 54 24 ?? 48 8B D9 48 89 4D ?? C6 44 24 ?? 00") - 0x37; + + + // seems unnecessary for now, but we may need to handle it + //ResizeBuffers = scanner.ScanModule("48 8B C4 55 41 54 41 55 41 56 41 57 48 8D 68 ?? 48 81 EC C0 00 00 00"); + } + } +} diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs new file mode 100644 index 000000000..a9981b8ed --- /dev/null +++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs @@ -0,0 +1,98 @@ +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using SharpDX.Windows; +using Device = SharpDX.Direct3D11.Device; + +namespace Dalamud.Game.Internal.DXGI +{ + /* + * This method of getting the SwapChain Addresses is currently not used. + * If the normal AddressResolver(SigScanner) fails, we should use it as a fallback.(Linux?) + */ + public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver + { + private const int DxgiSwapchainMethodCount = 18; + private const int D3D11DeviceMethodCount = 43; + + private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm) { + return new SwapChainDescription { + BufferCount = 1, + Flags = SwapChainFlags.None, + IsWindowed = true, + ModeDescription = new ModeDescription(100, 100, new Rational(60, 1), Format.R8G8B8A8_UNorm), + OutputHandle = renderForm, + SampleDescription = new SampleDescription(1, 0), + SwapEffect = SwapEffect.Discard, + Usage = Usage.RenderTargetOutput + }; + } + + private IntPtr[] GetVTblAddresses(IntPtr pointer, int numberOfMethods) + { + return GetVTblAddresses(pointer, 0, numberOfMethods); + } + + private IntPtr[] GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods) + { + List vtblAddresses = new List(); + IntPtr vTable = Marshal.ReadIntPtr(pointer); + for (int i = startIndex; i < startIndex + numberOfMethods; i++) + vtblAddresses.Add(Marshal.ReadIntPtr(vTable, i * IntPtr.Size)); // using IntPtr.Size allows us to support both 32 and 64-bit processes + + return vtblAddresses.ToArray(); + } + + private List d3d11VTblAddresses = null; + private List dxgiSwapChainVTblAddresses = null; + + #region Internal device resources + + private Device device; + private SwapChain swapChain; + private RenderForm renderForm; + #endregion + + #region Addresses + + public IntPtr Present { get; set; } + + #endregion + + protected override void Setup64Bit(SigScanner sig) { + if (this.d3d11VTblAddresses == null) { + this.d3d11VTblAddresses = new List(); + this.dxgiSwapChainVTblAddresses = new List(); + + #region Get Device and SwapChain method addresses + + // Create temporary device + swapchain and determine method addresses + this.renderForm = new RenderForm(); + Device.CreateWithSwapChain( + DriverType.Hardware, + DeviceCreationFlags.BgraSupport, + CreateSwapChainDescription(this.renderForm.Handle), + out this.device, + out this.swapChain + ); + + if (this.device != null && this.swapChain != null) { + this.d3d11VTblAddresses.AddRange( + GetVTblAddresses(this.device.NativePointer, D3D11DeviceMethodCount)); + this.dxgiSwapChainVTblAddresses.AddRange( + GetVTblAddresses(this.swapChain.NativePointer, DxgiSwapchainMethodCount)); + } + + this.device?.Dispose(); + this.swapChain?.Dispose(); + + #endregion + } + + Present = this.dxgiSwapChainVTblAddresses[8]; + } + } +} diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs index ccfaca00a..03705db5b 100644 --- a/Dalamud/Game/Internal/Gui/ChatGui.cs +++ b/Dalamud/Game/Internal/Gui/ChatGui.cs @@ -88,8 +88,7 @@ namespace Dalamud.Game.Internal.Gui { var senderName = StdString.ReadFromPointer(pSenderName); var message = StdString.ReadFromPointer(pMessage); - Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}"); - // Log.Debug($"Got message bytes {BitConverter.ToString(messageBytes.Bytes).Replace("-", " ")}"); + //Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}"); var originalMessageData = (byte[]) message.RawData.Clone(); diff --git a/Dalamud/Game/Internal/Libc/LibcFunction.cs b/Dalamud/Game/Internal/Libc/LibcFunction.cs index 88261c3f7..9ea4dc6a8 100644 --- a/Dalamud/Game/Internal/Libc/LibcFunction.cs +++ b/Dalamud/Game/Internal/Libc/LibcFunction.cs @@ -26,8 +26,6 @@ namespace Dalamud.Game.Internal.Libc { } public OwnedStdString NewString(byte[] content) { - Log.Verbose("Allocating"); - // While 0x70 bytes in the memory should be enough in DX11 version, // I don't trust my analysis so we're just going to allocate almost two times more than that. var pString = Marshal.AllocHGlobal(256); diff --git a/Dalamud/Game/Internal/Libc/OwnedStdString.cs b/Dalamud/Game/Internal/Libc/OwnedStdString.cs index 6e3b822dc..ea68d8f91 100644 --- a/Dalamud/Game/Internal/Libc/OwnedStdString.cs +++ b/Dalamud/Game/Internal/Libc/OwnedStdString.cs @@ -39,8 +39,6 @@ namespace Dalamud.Game.Internal.Libc { // Something got seriously fucked. throw new AccessViolationException(); } - - Log.Verbose("Deallocting {Addr}", Address); // Deallocate inner string first this.dealloc(Address); diff --git a/Dalamud/Game/Internal/Resource/ResourceManager.cs b/Dalamud/Game/Internal/Resource/ResourceManager.cs index 91196fcfd..b12e1b8a6 100644 --- a/Dalamud/Game/Internal/Resource/ResourceManager.cs +++ b/Dalamud/Game/Internal/Resource/ResourceManager.cs @@ -90,7 +90,7 @@ namespace Dalamud.Game.Internal.File var data = new byte[len]; Marshal.Copy(address, data, 0, len); - Log.Verbose($"MEMDMP at {address.ToInt64():X} for {len:X}\n{ByteArrayToHex(data)}"); + Log.Verbose($"MEMDMP at {address.ToInt64():X} for {len:X}\n{Util.ByteArrayToHex(data)}"); } private IntPtr GetResourceSyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr a5, IntPtr a6) { @@ -142,70 +142,5 @@ namespace Dalamud.Game.Internal.File return (!string.IsNullOrEmpty(path) && path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0); } - - public static string ByteArrayToHex(byte[] bytes, int offset = 0, int bytesPerLine = 16) - { - if (bytes == null) - { - return string.Empty; - } - - var hexChars = "0123456789ABCDEF".ToCharArray(); - - var offsetBlock = 8 + 3; - var byteBlock = offsetBlock + bytesPerLine * 3 + (bytesPerLine - 1) / 8 + 2; - var lineLength = byteBlock + bytesPerLine + Environment.NewLine.Length; - - var line = (new string(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); - var numLines = (bytes.Length + bytesPerLine - 1) / bytesPerLine; - - var sb = new StringBuilder(numLines * lineLength); - - for (var i = 0; i < bytes.Length; i += bytesPerLine) - { - var h = i + offset; - - line[0] = hexChars[(h >> 28) & 0xF]; - line[1] = hexChars[(h >> 24) & 0xF]; - line[2] = hexChars[(h >> 20) & 0xF]; - line[3] = hexChars[(h >> 16) & 0xF]; - line[4] = hexChars[(h >> 12) & 0xF]; - line[5] = hexChars[(h >> 8) & 0xF]; - line[6] = hexChars[(h >> 4) & 0xF]; - line[7] = hexChars[(h >> 0) & 0xF]; - - var hexColumn = offsetBlock; - var charColumn = byteBlock; - - for (var j = 0; j < bytesPerLine; j++) - { - if (j > 0 && (j & 7) == 0) - { - hexColumn++; - } - - if (i + j >= bytes.Length) - { - line[hexColumn] = ' '; - line[hexColumn + 1] = ' '; - line[charColumn] = ' '; - } - else - { - var by = bytes[i + j]; - line[hexColumn] = hexChars[(by >> 4) & 0xF]; - line[hexColumn + 1] = hexChars[by & 0xF]; - line[charColumn] = by < 32 ? '.' : (char)by; - } - - hexColumn += 3; - charColumn++; - } - - sb.Append(line); - } - - return sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()); - } } } diff --git a/Dalamud/Game/Network/NetworkHandlers.cs b/Dalamud/Game/Network/NetworkHandlers.cs index 4613e03de..a3da8c57c 100644 --- a/Dalamud/Game/Network/NetworkHandlers.cs +++ b/Dalamud/Game/Network/NetworkHandlers.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.Game.Network.MarketBoardUploaders; using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Universalis.MarketBoardUploaders; +using Newtonsoft.Json.Linq; using Serilog; namespace Dalamud.Game.Network { @@ -19,6 +20,9 @@ namespace Dalamud.Game.Network { private byte[] lastPreferredRole; + public delegate Task CfPop(JObject contentFinderCondition); + public event CfPop ProcessCfPop; + public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads) { this.dalamud = dalamud; this.optOutMbUploads = optOutMbUploads; @@ -26,36 +30,39 @@ namespace Dalamud.Game.Network { this.uploader = new UniversalisMarketBoardUploader(dalamud); dalamud.Framework.Network.OnZonePacket += OnZonePacket; + } private void OnZonePacket(IntPtr dataPtr) { - var opCode = (ZoneOpCode) Marshal.ReadInt16(dataPtr, 2); + if (!this.dalamud.Data.IsDataReady) + return; - if (opCode == ZoneOpCode.CfNotifyPop) { + var opCode = (ushort) Marshal.ReadInt16(dataPtr, 2); + + if (opCode == this.dalamud.Data.ServerOpCodes["CfNotifyPop"]) { var data = new byte[64]; Marshal.Copy(dataPtr, data, 0, 64); var notifyType = data[16]; - var contentFinderConditionId = BitConverter.ToInt16(data, 36); + var contentFinderConditionId = BitConverter.ToUInt16(data, 36); Task.Run(async () => { if (notifyType != 3 || contentFinderConditionId == 0) return; - var contentFinderCondition = - await XivApi.GetContentFinderCondition(contentFinderConditionId); + var contentFinderCondition = this.dalamud.Data.ContentFinderCondition[contentFinderConditionId]; this.dalamud.Framework.Gui.Chat.Print($"Duty pop: " + contentFinderCondition["Name"]); - if (this.dalamud.BotManager.IsConnected) - await this.dalamud.BotManager.ProcessCfPop(contentFinderCondition); + await this.ProcessCfPop?.Invoke(contentFinderCondition); + }); return; } - if (opCode == ZoneOpCode.CfPreferredRole) { + if (opCode == this.dalamud.Data.ServerOpCodes["CfPreferredRole"]) { if (this.dalamud.Configuration.PreferredRoleReminders == null) return; @@ -110,7 +117,7 @@ namespace Dalamud.Game.Network { } if (!this.optOutMbUploads) { - if (opCode == ZoneOpCode.MarketBoardItemRequestStart) { + if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardItemRequestStart"]) { var catalogId = (uint) Marshal.ReadInt32(dataPtr + 0x10); var amount = Marshal.ReadByte(dataPtr + 0x1B); @@ -125,7 +132,7 @@ namespace Dalamud.Game.Network { return; } - if (opCode == ZoneOpCode.MarketBoardOfferings) { + if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardOfferings"]) { var listing = MarketBoardCurrentOfferings.Read(dataPtr + 0x10); var request = @@ -180,7 +187,7 @@ namespace Dalamud.Game.Network { return; } - if (opCode == ZoneOpCode.MarketBoardHistory) { + if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardHistory"]) { var listing = MarketBoardHistory.Read(dataPtr + 0x10); var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId); @@ -202,7 +209,7 @@ namespace Dalamud.Game.Network { Log.Verbose("Added history for item#{0}", listing.CatalogId); } - if (opCode == ZoneOpCode.MarketTaxRates) + if (opCode == this.dalamud.Data.ServerOpCodes["MarketTaxRates"]) { var taxes = MarketTaxRates.Read(dataPtr + 0x10); @@ -220,15 +227,6 @@ namespace Dalamud.Game.Network { } } - private enum ZoneOpCode { - CfNotifyPop = 0x1F8, - CfPreferredRole = 0x32A, - MarketTaxRates = 0x25E, - MarketBoardItemRequestStart = 0x328, - MarketBoardOfferings = 0x15F, - MarketBoardHistory = 0x113 - } - private DalamudConfiguration.PreferredRole RoleKeyToPreferredRole(int key) => key switch { 1 => DalamudConfiguration.PreferredRole.Tank, diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 823d016ba..86eba33ee 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -7,24 +7,34 @@ using System.Runtime.InteropServices; using Serilog; namespace Dalamud.Game { - public sealed class SigScanner { - public SigScanner(ProcessModule module) { + public sealed class SigScanner : IDisposable { + public SigScanner(ProcessModule module, bool doCopy = false) { Module = module; Is32BitProcess = !Environment.Is64BitProcess; + IsCopy = doCopy; // Limit the search space to .text section. SetupSearchSpace(module); + if (IsCopy) + SetupCopiedSegments(); + Log.Verbose("Module base: {Address}", TextSectionBase); - Log.Verbose("Moudle size: {Size}", TextSectionSize); + Log.Verbose("Module size: {Size}", TextSectionSize); } + public bool IsCopy { get; private set; } + public bool Is32BitProcess { get; } - public IntPtr TextSectionBase { get; private set; } + public IntPtr SearchBase => IsCopy ? this.moduleCopyPtr : Module.BaseAddress; + + public IntPtr TextSectionBase => new IntPtr(SearchBase.ToInt64() + TextSectionOffset); + public long TextSectionOffset { get; private set; } public int TextSectionSize { get; private set; } - public IntPtr DataSectionBase { get; private set; } + public IntPtr DataSectionBase => new IntPtr(SearchBase.ToInt64() + DataSectionOffset); + public long DataSectionOffset { get; private set; } public int DataSectionSize { get; private set; } public ProcessModule Module { get; } @@ -59,11 +69,11 @@ namespace Dalamud.Game { // .text switch (sectionName) { case 0x747865742E: // .text - TextSectionBase = baseAddress + Marshal.ReadInt32(sectionCursor, 12); + TextSectionOffset = Marshal.ReadInt32(sectionCursor, 12); TextSectionSize = Marshal.ReadInt32(sectionCursor, 8); break; case 0x617461642E: // .data - DataSectionBase = baseAddress + Marshal.ReadInt32(sectionCursor, 12); + DataSectionOffset = Marshal.ReadInt32(sectionCursor, 12); DataSectionSize = Marshal.ReadInt32(sectionCursor, 8); break; } @@ -72,16 +82,53 @@ namespace Dalamud.Game { } } + private IntPtr moduleCopyPtr; + private long moduleCopyOffset; + + private unsafe void SetupCopiedSegments() { + Log.Verbose("module copy START"); + // .text + this.moduleCopyPtr = Marshal.AllocHGlobal(Module.ModuleMemorySize); + Log.Verbose($"Alloc: {this.moduleCopyPtr.ToInt64():x}"); + Buffer.MemoryCopy(Module.BaseAddress.ToPointer(), this.moduleCopyPtr.ToPointer(), Module.ModuleMemorySize, + Module.ModuleMemorySize); + + this.moduleCopyOffset = this.moduleCopyPtr.ToInt64() - Module.BaseAddress.ToInt64(); + + Log.Verbose("copy OK!"); + } + + public void Dispose() { + Marshal.FreeHGlobal(this.moduleCopyPtr); + } + public IntPtr ScanText(string signature) { - return Scan(TextSectionBase, TextSectionSize, signature); + var mBase = IsCopy ? this.moduleCopyPtr : TextSectionBase; + + var scanRet = Scan(mBase, TextSectionSize, signature); + + if (IsCopy) + scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); + + return scanRet; } public IntPtr ScanData(string signature) { - return Scan(DataSectionBase, DataSectionSize, signature); + var scanRet = Scan(DataSectionBase, DataSectionSize, signature); + + if (IsCopy) + scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); + + return scanRet; } public IntPtr ScanModule(string signature) { - return Scan(Module.BaseAddress, Module.ModuleMemorySize, signature); + var scanRet = Scan(SearchBase, Module.ModuleMemorySize, signature); + + if (IsCopy) + scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); + + return scanRet; } public IntPtr Scan(IntPtr baseAddress, int size, string signature) { diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs new file mode 100644 index 000000000..cdb7f19ec --- /dev/null +++ b/Dalamud/Interface/DalamudDataWindow.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Data; +using ImGuiNET; +using Newtonsoft.Json; + +namespace Dalamud.Interface +{ + class DalamudDataWindow { + private DataManager dataMgr; + + private bool wasReady; + private string serverOpString; + private string cfcString; + + private int currentKind; + + public DalamudDataWindow(DataManager dataMgr) { + this.dataMgr = dataMgr; + + Load(); + } + + private void Load() { + if (this.dataMgr.IsDataReady) + { + this.serverOpString = JsonConvert.SerializeObject(this.dataMgr.ServerOpCodes, Formatting.Indented); + this.cfcString = JsonConvert.SerializeObject(this.dataMgr.ContentFinderCondition, Formatting.Indented); + this.wasReady = true; + } + } + + public bool Draw() + { + ImGui.SetNextWindowSize(new Vector2(500, 500), ImGuiCond.Always); + + var isOpen = true; + + if (!ImGui.Begin("Dalamud Data", ref isOpen, ImGuiWindowFlags.NoCollapse)) + { + ImGui.End(); + return false; + } + + // Main window + if (ImGui.Button("Force Reload")) + Load(); + ImGui.SameLine(); + var copy = ImGui.Button("Copy all"); + ImGui.SameLine(); + ImGui.Combo("Data kind", ref currentKind, new[] {"ServerOpCode", "ContentFinderCondition"}, 2); + + ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); + + if (copy) + ImGui.LogToClipboard(); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + + if (this.wasReady) { + switch (currentKind) { + case 0: ImGui.TextUnformatted(this.serverOpString); + break; + case 1: ImGui.TextUnformatted(this.cfcString); + break; + } + } else { + ImGui.TextUnformatted("Data not ready."); + } + + ImGui.PopStyleVar(); + + ImGui.EndChild(); + ImGui.End(); + + return isOpen; + } + } +} diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/DalamudLogWindow.cs new file mode 100644 index 000000000..5d49c961a --- /dev/null +++ b/Dalamud/Interface/DalamudLogWindow.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using ImGuiNET; + +namespace Dalamud.Interface +{ + class DalamudLogWindow : IDisposable { + private bool autoScroll = true; + private string logText = string.Empty; + + public DalamudLogWindow() { + SerilogEventSink.Instance.OnLogLine += Serilog_OnLogLine; + } + + public void Dispose() { + SerilogEventSink.Instance.OnLogLine -= Serilog_OnLogLine; + } + + private void Serilog_OnLogLine(object sender, string e) + { + AddLog(e + "\n"); + } + + public void Clear() { + this.logText = string.Empty; + } + + public void AddLog(string line) { + this.logText += line; + } + + public bool Draw() { + ImGui.SetNextWindowSize(new Vector2(500, 400), ImGuiCond.FirstUseEver); + + var isOpen = true; + + if (!ImGui.Begin("Dalamud LOG", ref isOpen, ImGuiWindowFlags.NoCollapse)) + { + ImGui.End(); + return false; + } + + // Options menu + if (ImGui.BeginPopup("Options")) + { + ImGui.Checkbox("Auto-scroll", ref this.autoScroll); + ImGui.EndPopup(); + } + + // Main window + if (ImGui.Button("Options")) + ImGui.OpenPopup("Options"); + ImGui.SameLine(); + var clear = ImGui.Button("Clear"); + ImGui.SameLine(); + var copy = ImGui.Button("Copy"); + + ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); + + if (clear) + Clear(); + if (copy) + ImGui.LogToClipboard(); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + + ImGui.TextUnformatted(this.logText); + + ImGui.PopStyleVar(); + + if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) + ImGui.SetScrollHereY(1.0f); + + ImGui.EndChild(); + ImGui.End(); + + return isOpen; + } + } +} diff --git a/Dalamud/Interface/InterfaceManager.cs b/Dalamud/Interface/InterfaceManager.cs new file mode 100644 index 000000000..7c0c25bd7 --- /dev/null +++ b/Dalamud/Interface/InterfaceManager.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Game; +using Dalamud.Game.Internal.DXGI; +using Dalamud.Hooking; +using EasyHook; +using ImGuiNET; +using ImGuiScene; +using Serilog; + +// general dev notes, here because it's easiest +/* + * - Hooking ResizeBuffers seemed to be unnecessary, though I'm not sure why. Left out for now since it seems to work without it. + * - We may want to build our ImGui command list in a thread to keep it divorced from present. We'd still have to block in present to + * synchronize on the list and render it, but ideally the overall delay we add to present would then be shorter. This may cause minor + * timing issues with anything animated inside ImGui, but that is probably rare and may not even be noticeable. + * - Our hook is too low level to really work well with debugging, as we only have access to the 'real' dx objects and not any + * that have been hooked/wrapped by tools. + * - Might eventually want to render to a separate target and composite, especially with reshade etc in the mix. + */ + +namespace Dalamud.Interface +{ + public class InterfaceManager : IDisposable + { + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate IntPtr PresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags); + + private readonly Hook presentHook; + + private readonly Hook setCursorHook; + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate IntPtr SetCursorDelegate(IntPtr hCursor); + + private ISwapChainAddressResolver Address { get; } + + private Dalamud dalamud; + private RawDX11Scene scene; + + /// + /// This event gets called by a plugin UiBuilder when read + /// + public event RawDX11Scene.BuildUIDelegate OnDraw; + + public InterfaceManager(Dalamud dalamud, SigScanner scanner) + { + this.dalamud = dalamud; + + try { + var sigResolver = new SwapChainSigResolver(); + sigResolver.Setup(scanner); + + Log.Verbose("Found SwapChain via signatures."); + + Address = sigResolver; + } catch (Exception ex) { + // The SigScanner method fails on wine/proton since DXGI is not a real DLL. We fall back to vtable to detect our Present function address. + Log.Error(ex, "Could not get SwapChain address via sig method, falling back to vtable..."); + + var vtableResolver = new SwapChainVtableResolver(); + vtableResolver.Setup(scanner); + + Log.Verbose("Found SwapChain via vtable."); + + Address = vtableResolver; + } + + var setCursorAddr = LocalHook.GetProcAddress("user32.dll", "SetCursor"); + + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose("SetCursor address {SetCursor}", setCursorAddr); + Log.Verbose("Present address {Present}", Address.Present); + + this.setCursorHook = new Hook(setCursorAddr, new SetCursorDelegate(SetCursorDetour), this); + + this.presentHook = + new Hook(Address.Present, + new PresentDelegate(PresentDetour), + this); + } + + public void Enable() + { + this.setCursorHook.Enable(); + this.presentHook.Enable(); + } + + private void Disable() + { + this.setCursorHook.Disable(); + this.presentHook.Disable(); + } + + public void Dispose() + { + // HACK: this is usually called on a separate thread from PresentDetour (likely on a dedicated render thread) + // and if we aren't already disabled, disposing of the scene and hook can frequently crash due to the hook + // being disposed of in this thread while it is actively in use in the render thread. + // This is a terrible way to prevent issues, but should basically always work to ensure that all outstanding + // calls to PresentDetour have finished (and Disable means no new ones will start), before we try to cleanup + // So... not great, but much better than constantly crashing on unload + this.Disable(); + System.Threading.Thread.Sleep(100); + + this.scene.Dispose(); + this.presentHook.Dispose(); + } + + public TextureWrap LoadImage(string filePath) + { + try + { + return this.scene?.LoadImage(filePath) ?? null; + } + catch (Exception ex) + { + Log.Error(ex, $"Failed to load image from {filePath}"); + } + return null; + } + + public TextureWrap LoadImage(byte[] imageData) + { + try + { + return this.scene?.LoadImage(imageData) ?? null; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load image from memory"); + } + return null; + } + + private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) + { + if (this.scene == null) + { + this.scene = new RawDX11Scene(swapChain); + this.scene.ImGuiIniPath = Path.Combine(Path.GetDirectoryName(this.dalamud.StartInfo.ConfigurationPath), "dalamudUI.ini"); + this.scene.OnBuildUI += Display; + + var fontPathJp = Path.Combine(Path.GetDirectoryName(typeof(InterfaceManager).Assembly.Location), "UIRes", "NotoSansCJKjp-Medium.otf"); + ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathJp, 17.0f, null, ImGui.GetIO().Fonts.GetGlyphRangesJapanese()); + + ImGui.GetIO().Fonts.Build(); + + ImGui.GetStyle().GrabRounding = 3f; + ImGui.GetStyle().FrameRounding = 4f; + ImGui.GetStyle().WindowRounding = 4f; + ImGui.GetStyle().WindowBorderSize = 0f; + ImGui.GetStyle().WindowMenuButtonPosition = ImGuiDir.Right; + ImGui.GetStyle().ScrollbarSize = 16f; + + ImGui.GetStyle().Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.06f, 0.06f, 0.06f, 0.87f); + ImGui.GetStyle().Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.29f, 0.29f, 0.29f, 0.54f); + ImGui.GetStyle().Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.54f, 0.54f, 0.54f, 0.40f); + ImGui.GetStyle().Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.64f, 0.64f, 0.64f, 0.67f); + ImGui.GetStyle().Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.29f, 0.29f, 0.29f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.86f, 0.86f, 0.86f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.54f, 0.54f, 0.54f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.67f, 0.67f, 0.67f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.Button] = new Vector4(0.71f, 0.71f, 0.71f, 0.40f); + ImGui.GetStyle().Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.47f, 0.47f, 0.47f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.74f, 0.74f, 0.74f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.Header] = new Vector4(0.59f, 0.59f, 0.59f, 0.31f); + ImGui.GetStyle().Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.50f, 0.50f, 0.50f, 0.80f); + ImGui.GetStyle().Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.60f, 0.60f, 0.60f, 1.00f); + ImGui.GetStyle().Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.79f, 0.79f, 0.79f, 0.25f); + ImGui.GetStyle().Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.78f, 0.78f, 0.78f, 0.67f); + ImGui.GetStyle().Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.88f, 0.88f, 0.88f, 0.95f); + ImGui.GetStyle().Colors[(int) ImGuiCol.Tab] = new Vector4(0.23f, 0.23f, 0.23f, 0.86f); + ImGui.GetStyle().Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.71f, 0.71f, 0.71f, 0.80f); + ImGui.GetStyle().Colors[(int) ImGuiCol.TabActive] = new Vector4(0.36f, 0.36f, 0.36f, 1.00f); + } + + this.scene.Render(); + + return this.presentHook.Original(swapChain, syncInterval, presentFlags); + } + + // can't access imgui IO before first present call + private bool lastWantCapture = false; + + private IntPtr SetCursorDetour(IntPtr hCursor) { + if (this.lastWantCapture == true && (!scene?.IsImGuiCursor(hCursor) ?? false)) + return IntPtr.Zero; + + return this.setCursorHook.Original(hCursor); + } + + private void Display() + { + // this is more or less part of what reshade/etc do to avoid having to manually + // set the cursor inside the ui + // This will just tell ImGui to draw its own software cursor instead of using the hardware cursor + // The scene internally will handle hiding and showing the hardware (game) cursor + // If the player has the game software cursor enabled, we can't really do anything about that and + // they will see both cursors. + // Doing this here because it's somewhat application-specific behavior + //ImGui.GetIO().MouseDrawCursor = ImGui.GetIO().WantCaptureMouse; + this.lastWantCapture = ImGui.GetIO().WantCaptureMouse; + + OnDraw?.Invoke(); + } + } +} diff --git a/Dalamud/Interface/SerilogEventSink.cs b/Dalamud/Interface/SerilogEventSink.cs new file mode 100644 index 000000000..3e0f7d2d2 --- /dev/null +++ b/Dalamud/Interface/SerilogEventSink.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; + +namespace Dalamud.Interface +{ + public class SerilogEventSink : ILogEventSink + { + private readonly IFormatProvider _formatProvider; + + public static SerilogEventSink Instance; + + public event EventHandler OnLogLine; + + public SerilogEventSink(IFormatProvider formatProvider) + { + _formatProvider = formatProvider; + + Instance = this; + } + + public void Emit(LogEvent logEvent) + { + var message = $"[{DateTimeOffset.Now.ToString()}][{logEvent.Level}] {logEvent.RenderMessage(_formatProvider)}"; + + if (logEvent.Exception != null) + message += "\n" + logEvent.Exception; + + OnLogLine?.Invoke(this, message); + } + } + + public static class MySinkExtensions + { + public static LoggerConfiguration EventSink( + this LoggerSinkConfiguration loggerConfiguration, + IFormatProvider formatProvider = null) + { + return loggerConfiguration.Sink(new SerilogEventSink(formatProvider)); + } + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs new file mode 100644 index 000000000..9c1a76016 --- /dev/null +++ b/Dalamud/Interface/UiBuilder.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ImGuiNET; +using ImGuiScene; + +namespace Dalamud.Interface +{ + public class UiBuilder : IDisposable { + private readonly string namespaceName; + + public event RawDX11Scene.BuildUIDelegate OnBuildUi; + + private InterfaceManager interfaceManager; + + public UiBuilder(InterfaceManager interfaceManager, string namespaceName) { + this.namespaceName = namespaceName; + + this.interfaceManager = interfaceManager; + this.interfaceManager.OnDraw += OnDraw; + } + + public void Dispose() { + this.interfaceManager.OnDraw -= OnDraw; + } + + /// + /// Loads an image from the specified file. + /// + /// The full filepath to the image. + /// A object wrapping the created image. Use inside ImGui.Image() + public TextureWrap LoadImage(string filePath) => + this.interfaceManager.LoadImage(filePath); + + /// + /// Loads an image from a byte stream, such as a png downloaded into memory. + /// + /// A byte array containing the raw image data. + /// A object wrapping the created image. Use inside ImGui.Image() + public TextureWrap LoadImage(byte[] imageData) => + this.interfaceManager.LoadImage(imageData); + + private void OnDraw() { + ImGui.PushID(this.namespaceName); + OnBuildUi?.Invoke(); + ImGui.PopID(); + } + } +} diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 23253c8e2..c897e14d2 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -6,18 +6,20 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Dalamud.Configuration; +using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Command; using Dalamud.Game.Internal; using Dalamud.Game.Internal.Gui; +using Dalamud.Interface; namespace Dalamud.Plugin { /// /// This class acts as an interface to various objects needed to interact with Dalamud and the game. /// - public class DalamudPluginInterface { + public class DalamudPluginInterface : IDisposable { /// /// The CommandManager object that allows you to add and remove custom chat commands. /// @@ -33,11 +35,21 @@ namespace Dalamud.Plugin /// public readonly Framework Framework; + /// + /// A UiBuilder instance which allows you to draw UI into the game via ImGui draw calls. + /// + public readonly UiBuilder UiBuilder; + /// /// A SigScanner instance targeting the main module of the FFXIV process. /// public readonly SigScanner TargetModuleScanner; + /// + /// A DataManager instance which allows you to access game data needed by the main dalamud features. + /// + public readonly DataManager Data; + private readonly Dalamud dalamud; private readonly string pluginName; @@ -49,25 +61,34 @@ namespace Dalamud.Plugin this.CommandManager = dalamud.CommandManager; this.Framework = dalamud.Framework; this.ClientState = dalamud.ClientState; - this.TargetModuleScanner = new SigScanner(dalamud.TargetModule); + this.UiBuilder = new UiBuilder(dalamud.InterfaceManager, pluginName); + this.TargetModuleScanner = dalamud.SigScanner; + this.Data = dalamud.Data; this.dalamud = dalamud; this.pluginName = pluginName; } + public void Dispose() { + this.UiBuilder.Dispose(); + } + /// /// Save a plugin configuration(inheriting IPluginConfiguration). /// /// The current configuration. public void SavePluginConfig(IPluginConfiguration currentConfig) { if (this.dalamud.Configuration.PluginConfigurations == null) - this.dalamud.Configuration.PluginConfigurations = new Dictionary(); + this.dalamud.Configuration.PluginConfigurations = new Dictionary(); if (this.dalamud.Configuration.PluginConfigurations.ContainsKey(this.pluginName)) { this.dalamud.Configuration.PluginConfigurations[this.pluginName] = currentConfig; return; } + if (currentConfig == null) + return; + this.dalamud.Configuration.PluginConfigurations.Add(this.pluginName, currentConfig); this.dalamud.Configuration.Save(this.dalamud.StartInfo.ConfigurationPath); } @@ -78,12 +99,12 @@ namespace Dalamud.Plugin /// A previously saved config or null if none was saved before. public IPluginConfiguration GetPluginConfig() { if (this.dalamud.Configuration.PluginConfigurations == null) - this.dalamud.Configuration.PluginConfigurations = new Dictionary(); + this.dalamud.Configuration.PluginConfigurations = new Dictionary(); if (!this.dalamud.Configuration.PluginConfigurations.ContainsKey(this.pluginName)) return null; - return this.dalamud.Configuration.PluginConfigurations[this.pluginName]; + return this.dalamud.Configuration.PluginConfigurations[this.pluginName] as IPluginConfiguration; } } } diff --git a/Dalamud/Plugin/PluginManager.cs b/Dalamud/Plugin/PluginManager.cs index 204931dfe..a9de4b645 100644 --- a/Dalamud/Plugin/PluginManager.cs +++ b/Dalamud/Plugin/PluginManager.cs @@ -33,22 +33,22 @@ namespace Dalamud.Plugin } public void LoadPlugins() { - LoadPluginsAt(this.defaultPluginDirectory); - LoadPluginsAt(this.pluginDirectory); + LoadPluginsAt(new DirectoryInfo(this.defaultPluginDirectory)); + LoadPluginsAt(new DirectoryInfo(this.pluginDirectory)); } - private void LoadPluginsAt(string folder) { - if (Directory.Exists(folder)) + private void LoadPluginsAt(DirectoryInfo folder) { + if (folder.Exists) { Log.Debug("Loading plugins at {0}", folder); - var pluginFileNames = Directory.GetFiles(folder, "*.dll"); + var pluginDlls = folder.GetFiles("*.dll", SearchOption.AllDirectories); - var assemblies = new List(pluginFileNames.Length); - foreach (var dllFile in pluginFileNames) + var assemblies = new List(pluginDlls.Length); + foreach (var dllFile in pluginDlls) { Log.Debug("Loading assembly at {0}", dllFile); - var assemblyName = AssemblyName.GetAssemblyName(dllFile); + var assemblyName = AssemblyName.GetAssemblyName(dllFile.FullName); var pluginAssembly = Assembly.Load(assemblyName); assemblies.Add(pluginAssembly); } diff --git a/Dalamud/UIRes/NotoSansCJKjp-Medium.otf b/Dalamud/UIRes/NotoSansCJKjp-Medium.otf new file mode 100644 index 000000000..ba41937ae Binary files /dev/null and b/Dalamud/UIRes/NotoSansCJKjp-Medium.otf differ diff --git a/Dalamud/UIRes/logo.png b/Dalamud/UIRes/logo.png new file mode 100644 index 000000000..34b9f0b4f Binary files /dev/null and b/Dalamud/UIRes/logo.png differ diff --git a/Dalamud/Util.cs b/Dalamud/Util.cs new file mode 100644 index 000000000..8c1642c0f --- /dev/null +++ b/Dalamud/Util.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud +{ + static class Util + { + public static string ByteArrayToHex(byte[] bytes, int offset = 0, int bytesPerLine = 16) + { + if (bytes == null) + { + return String.Empty; + } + + var hexChars = "0123456789ABCDEF".ToCharArray(); + + var offsetBlock = 8 + 3; + var byteBlock = offsetBlock + bytesPerLine * 3 + (bytesPerLine - 1) / 8 + 2; + var lineLength = byteBlock + bytesPerLine + Environment.NewLine.Length; + + var line = (new string(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); + var numLines = (bytes.Length + bytesPerLine - 1) / bytesPerLine; + + var sb = new StringBuilder(numLines * lineLength); + + for (var i = 0; i < bytes.Length; i += bytesPerLine) + { + var h = i + offset; + + line[0] = hexChars[(h >> 28) & 0xF]; + line[1] = hexChars[(h >> 24) & 0xF]; + line[2] = hexChars[(h >> 20) & 0xF]; + line[3] = hexChars[(h >> 16) & 0xF]; + line[4] = hexChars[(h >> 12) & 0xF]; + line[5] = hexChars[(h >> 8) & 0xF]; + line[6] = hexChars[(h >> 4) & 0xF]; + line[7] = hexChars[(h >> 0) & 0xF]; + + var hexColumn = offsetBlock; + var charColumn = byteBlock; + + for (var j = 0; j < bytesPerLine; j++) + { + if (j > 0 && (j & 7) == 0) + { + hexColumn++; + } + + if (i + j >= bytes.Length) + { + line[hexColumn] = ' '; + line[hexColumn + 1] = ' '; + line[charColumn] = ' '; + } + else + { + var by = bytes[i + j]; + line[hexColumn] = hexChars[(@by >> 4) & 0xF]; + line[hexColumn + 1] = hexChars[@by & 0xF]; + line[charColumn] = @by < 32 ? '.' : (char)@by; + } + + hexColumn += 3; + charColumn++; + } + + sb.Append(line); + } + + return sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()); + } + } +} diff --git a/Dalamud/XivApi.cs b/Dalamud/XivApi.cs index 9d2374281..5f06a98fd 100644 --- a/Dalamud/XivApi.cs +++ b/Dalamud/XivApi.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -13,9 +14,9 @@ namespace Dalamud { class XivApi { - private const string URL = "http://xivapi.com/"; + private const string URL = "https://xivapi.com/"; - private static readonly Dictionary cachedResponses = new Dictionary(); + private static readonly ConcurrentDictionary cachedResponses = new ConcurrentDictionary(); public static async Task GetWorld(int world) { @@ -77,8 +78,8 @@ namespace Dalamud { Log.Verbose("XIVAPI FETCH: {0}", endpoint); - if (cachedResponses.ContainsKey(endpoint) && !noCache) - return cachedResponses[endpoint]; + if (cachedResponses.TryGetValue(endpoint, out var val) && !noCache) + return val; var client = new HttpClient(); var response = await client.GetAsync(URL + endpoint); @@ -87,7 +88,7 @@ namespace Dalamud var obj = JObject.Parse(result); if (!noCache) - cachedResponses.Add(endpoint, obj); + cachedResponses.TryAdd(endpoint, obj); return obj; } diff --git a/lib/ImGuiScene b/lib/ImGuiScene new file mode 160000 index 000000000..cd24a6108 --- /dev/null +++ b/lib/ImGuiScene @@ -0,0 +1 @@ +Subproject commit cd24a6108c05e52b0dde80e85ff1f9fa812f631b