mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-02 22:03:41 +01:00
commit
8544f389fc
42 changed files with 1835 additions and 358 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "lib/ImGuiScene"]
|
||||
path = lib/ImGuiScene
|
||||
url = https://github.com/ff-meli/ImGuiScene
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup Label="Feature">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyVersion>4.3.1.0</AssemblyVersion>
|
||||
<FileVersion>4.3.1.0</FileVersion>
|
||||
<AssemblyVersion>4.7.3.0</AssemblyVersion>
|
||||
<FileVersion>4.7.3.0</FileVersion>
|
||||
<Description>XIVLauncher addon injection</Description>
|
||||
<Version>4.3.1.0</Version>
|
||||
<Version>4.7.3.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DocumentationFile></DocumentationFile>
|
||||
|
|
|
|||
|
|
@ -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<DalamudStartInfo>(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);
|
||||
}
|
||||
|
|
|
|||
52
Dalamud.sln
52
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
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ namespace Dalamud
|
|||
|
||||
public string LastVersion { get; set; }
|
||||
|
||||
public Dictionary<string, IPluginConfiguration> PluginConfigurations { get; set; }
|
||||
public Dictionary<string, object> PluginConfigurations { get; set; }
|
||||
|
||||
public bool WelcomeGuideDismissed;
|
||||
|
||||
public static DalamudConfiguration Load(string path) {
|
||||
return JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path));
|
||||
|
|
|
|||
|
|
@ -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 <roulette name> <role name>"
|
||||
});
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Target">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<TargetFramework>net471</TargetFramework>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup Label="Feature">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyVersion>4.3.1.0</AssemblyVersion>
|
||||
<Version>4.3.1.0</Version>
|
||||
<FileVersion>4.3.1.0</FileVersion>
|
||||
<AssemblyVersion>4.7.3.0</AssemblyVersion>
|
||||
<Version>4.7.3.0</Version>
|
||||
<FileVersion>4.7.3.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<None Include="$(SolutionDir)/Resources/**/*" CopyToOutputDirectory="PreserveNewest" Visible="false" />
|
||||
|
|
@ -41,13 +41,14 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="2.1.0" />
|
||||
<PackageReference Include="EasyHook" Version="2.7.6270" />
|
||||
<PackageReference Include="Google.Cloud.Translation.V2" Version="1.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="2.6.1" />
|
||||
<PackageReference Include="Serilog" Version="2.6.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="EasyHook" Version="2.7.6270" />
|
||||
<PackageReference Include="SharpDX.Desktop" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Net.Http" />
|
||||
|
|
@ -65,4 +66,23 @@
|
|||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Configuration\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
|
||||
<ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj" />
|
||||
<ProjectReference Include="..\lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="NotoSansCJKjp-Medium.otf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="UIRes\logo.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="UIRes\NotoSansCJKjp-Medium.otf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
57
Dalamud/Data/DataManager.cs
Normal file
57
Dalamud/Data/DataManager.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
|
||||
/// </summary>
|
||||
public class DataManager {
|
||||
private const string DataBaseUrl = "https://goaaats.github.io/ffxiv/tools/launcher/addons/Hooks/Data/";
|
||||
|
||||
public ReadOnlyDictionary<string, ushort> ServerOpCodes;
|
||||
public ReadOnlyDictionary<uint, JObject> 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<string, ushort>(new Dictionary<string, ushort>());
|
||||
this.ContentFinderCondition = new ReadOnlyDictionary<uint, JObject>(new Dictionary<uint, JObject>());
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
try {
|
||||
Log.Verbose("Starting data download...");
|
||||
|
||||
using var client = new HttpClient() {
|
||||
BaseAddress = new Uri(DataBaseUrl)
|
||||
};
|
||||
|
||||
var opCodeDict =
|
||||
JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
|
||||
await client.GetStringAsync(DataBaseUrl + "serveropcode.json"));
|
||||
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(opCodeDict);
|
||||
|
||||
Log.Verbose("Loaded {0} ServerOpCodes.", opCodeDict.Count);
|
||||
|
||||
var cfcs = JsonConvert.DeserializeObject<Dictionary<uint, JObject>>(
|
||||
await client.GetStringAsync(DataBaseUrl + "contentfindercondition.json"));
|
||||
this.ContentFinderCondition = new ReadOnlyDictionary<uint, JObject>(cfcs);
|
||||
|
||||
Log.Verbose("Loaded {0} ContentFinderCondition.", cfcs.Count);
|
||||
|
||||
IsDataReady = true;
|
||||
} catch (Exception ex) {
|
||||
Log.Error(ex, "Could not download data.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<SeStringPayloadContainer> Payloads) Parse(byte[] bytes)
|
||||
{
|
||||
var output = new List<byte>();
|
||||
var payloads = new List<SeStringPayloadContainer>();
|
||||
|
||||
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<SeStringPayloadContainer> Payloads) Parse(string input) {
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return Parse(bytes);
|
||||
}
|
||||
|
||||
private static void ProcessPacket(BinaryReader reader, List<byte> output,
|
||||
List<SeStringPayloadContainer> 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
|
||||
}
|
||||
}
|
||||
221
Dalamud/Game/Chat/SeStringHandling/Payload.cs
Normal file
221
Dalamud/Game/Chat/SeStringHandling/Payload.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a parsed SeString payload.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
31
Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
Normal file
31
Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// All parsed types of SeString payloads.
|
||||
/// </summary>
|
||||
public enum PayloadType
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString payload representing a player link.
|
||||
/// </summary>
|
||||
Player,
|
||||
/// <summary>
|
||||
/// An SeString payload representing an Item link.
|
||||
/// </summary>
|
||||
Item,
|
||||
/// <summary>
|
||||
/// An SeString payload representing an Status Effect link.
|
||||
/// </summary>
|
||||
Status,
|
||||
/// <summary>
|
||||
/// An SeString payload representing raw, typed text.
|
||||
/// </summary>
|
||||
RawText
|
||||
}
|
||||
}
|
||||
81
Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
Normal file
81
Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
85
Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs
Normal file
64
Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs
Normal file
58
Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs
Normal file
|
|
@ -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<byte>();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
Dalamud/Game/Chat/SeStringHandling/SeString.cs
Normal file
107
Dalamud/Game/Chat/SeStringHandling/SeString.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a parsed SeString.
|
||||
/// </summary>
|
||||
public class SeString
|
||||
{
|
||||
private Dictionary<PayloadType, List<Payload>> mappedPayloads_ = null;
|
||||
|
||||
public List<Payload> Payloads { get; }
|
||||
|
||||
public Dictionary<PayloadType, List<Payload>> MappedPayloads
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mappedPayloads_ == null)
|
||||
{
|
||||
mappedPayloads_ = new Dictionary<PayloadType, List<Payload>>();
|
||||
foreach (var p in Payloads)
|
||||
{
|
||||
if (!mappedPayloads_.ContainsKey(p.Type))
|
||||
{
|
||||
mappedPayloads_[p.Type] = new List<Payload>();
|
||||
}
|
||||
mappedPayloads_[p.Type].Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
return mappedPayloads_;
|
||||
}
|
||||
}
|
||||
|
||||
public SeString(List<Payload> payloads)
|
||||
{
|
||||
Payloads = payloads;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to get all raw text from a message as a single joined string
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// All the raw text from the contained payloads, joined into a single string
|
||||
/// </returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an array of bytes to a SeString.
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public static SeString Parse(byte[] bytes)
|
||||
{
|
||||
var payloads = new List<Payload>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode a parsed/created SeString to an array of bytes, to be used for injection.
|
||||
/// </summary>
|
||||
/// <param name="payloads"></param>
|
||||
/// <returns>The bytes of the message.</returns>
|
||||
public static byte[] Encode(List<Payload> payloads)
|
||||
{
|
||||
var messageBytes = new List<byte>();
|
||||
foreach (var p in payloads)
|
||||
{
|
||||
messageBytes.AddRange(p.Encode());
|
||||
}
|
||||
|
||||
return messageBytes.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int, bool>)(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("*");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Dalamud/Game/Internal/DXGI/ISwapChainAddressResolver.cs
Normal file
7
Dalamud/Game/Internal/DXGI/ISwapChainAddressResolver.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Game.Internal.DXGI {
|
||||
public interface ISwapChainAddressResolver {
|
||||
IntPtr Present { get; set; }
|
||||
}
|
||||
}
|
||||
32
Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
Normal file
32
Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
Normal file
|
|
@ -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<ProcessModule>().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");
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
Normal file
98
Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
Normal file
|
|
@ -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<IntPtr> vtblAddresses = new List<IntPtr>();
|
||||
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<IntPtr> d3d11VTblAddresses = null;
|
||||
private List<IntPtr> 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<IntPtr>();
|
||||
this.dxgiSwapChainVTblAddresses = new List<IntPtr>();
|
||||
|
||||
#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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
83
Dalamud/Interface/DalamudDataWindow.cs
Normal file
83
Dalamud/Interface/DalamudDataWindow.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Dalamud/Interface/DalamudLogWindow.cs
Normal file
84
Dalamud/Interface/DalamudLogWindow.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
210
Dalamud/Interface/InterfaceManager.cs
Normal file
210
Dalamud/Interface/InterfaceManager.cs
Normal file
|
|
@ -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<PresentDelegate> presentHook;
|
||||
|
||||
private readonly Hook<SetCursorDelegate> setCursorHook;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
private delegate IntPtr SetCursorDelegate(IntPtr hCursor);
|
||||
|
||||
private ISwapChainAddressResolver Address { get; }
|
||||
|
||||
private Dalamud dalamud;
|
||||
private RawDX11Scene scene;
|
||||
|
||||
/// <summary>
|
||||
/// This event gets called by a plugin UiBuilder when read
|
||||
/// </summary>
|
||||
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<SetCursorDelegate>(setCursorAddr, new SetCursorDelegate(SetCursorDetour), this);
|
||||
|
||||
this.presentHook =
|
||||
new Hook<PresentDelegate>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Dalamud/Interface/SerilogEventSink.cs
Normal file
48
Dalamud/Interface/SerilogEventSink.cs
Normal file
|
|
@ -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<string> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Dalamud/Interface/UiBuilder.cs
Normal file
51
Dalamud/Interface/UiBuilder.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an image from the specified file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The full filepath to the image.</param>
|
||||
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image()</returns>
|
||||
public TextureWrap LoadImage(string filePath) =>
|
||||
this.interfaceManager.LoadImage(filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Loads an image from a byte stream, such as a png downloaded into memory.
|
||||
/// </summary>
|
||||
/// <param name="imageData">A byte array containing the raw image data.</param>
|
||||
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image()</returns>
|
||||
public TextureWrap LoadImage(byte[] imageData) =>
|
||||
this.interfaceManager.LoadImage(imageData);
|
||||
|
||||
private void OnDraw() {
|
||||
ImGui.PushID(this.namespaceName);
|
||||
OnBuildUi?.Invoke();
|
||||
ImGui.PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class acts as an interface to various objects needed to interact with Dalamud and the game.
|
||||
/// </summary>
|
||||
public class DalamudPluginInterface {
|
||||
public class DalamudPluginInterface : IDisposable {
|
||||
/// <summary>
|
||||
/// The CommandManager object that allows you to add and remove custom chat commands.
|
||||
/// </summary>
|
||||
|
|
@ -33,11 +35,21 @@ namespace Dalamud.Plugin
|
|||
/// </summary>
|
||||
public readonly Framework Framework;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="UiBuilder">UiBuilder</see> instance which allows you to draw UI into the game via ImGui draw calls.
|
||||
/// </summary>
|
||||
public readonly UiBuilder UiBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="SigScanner">SigScanner</see> instance targeting the main module of the FFXIV process.
|
||||
/// </summary>
|
||||
public readonly SigScanner TargetModuleScanner;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="DataManager">DataManager</see> instance which allows you to access game data needed by the main dalamud features.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a plugin configuration(inheriting IPluginConfiguration).
|
||||
/// </summary>
|
||||
/// <param name="currentConfig">The current configuration.</param>
|
||||
public void SavePluginConfig(IPluginConfiguration currentConfig) {
|
||||
if (this.dalamud.Configuration.PluginConfigurations == null)
|
||||
this.dalamud.Configuration.PluginConfigurations = new Dictionary<string, IPluginConfiguration>();
|
||||
this.dalamud.Configuration.PluginConfigurations = new Dictionary<string, object>();
|
||||
|
||||
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
|
|||
/// <returns>A previously saved config or null if none was saved before.</returns>
|
||||
public IPluginConfiguration GetPluginConfig() {
|
||||
if (this.dalamud.Configuration.PluginConfigurations == null)
|
||||
this.dalamud.Configuration.PluginConfigurations = new Dictionary<string, IPluginConfiguration>();
|
||||
this.dalamud.Configuration.PluginConfigurations = new Dictionary<string, object>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Assembly>(pluginFileNames.Length);
|
||||
foreach (var dllFile in pluginFileNames)
|
||||
var assemblies = new List<Assembly>(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);
|
||||
}
|
||||
|
|
|
|||
BIN
Dalamud/UIRes/NotoSansCJKjp-Medium.otf
Normal file
BIN
Dalamud/UIRes/NotoSansCJKjp-Medium.otf
Normal file
Binary file not shown.
BIN
Dalamud/UIRes/logo.png
Normal file
BIN
Dalamud/UIRes/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
76
Dalamud/Util.cs
Normal file
76
Dalamud/Util.cs
Normal file
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, JObject> cachedResponses = new Dictionary<string, JObject>();
|
||||
private static readonly ConcurrentDictionary<string, JObject> cachedResponses = new ConcurrentDictionary<string, JObject>();
|
||||
|
||||
public static async Task<JObject> 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;
|
||||
}
|
||||
|
|
|
|||
1
lib/ImGuiScene
Submodule
1
lib/ImGuiScene
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit cd24a6108c05e52b0dde80e85ff1f9fa812f631b
|
||||
Loading…
Add table
Add a link
Reference in a new issue