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