Merge pull request #31 from goaaats/interface

Interface master merge
This commit is contained in:
goaaats 2020-02-08 08:57:35 +09:00 committed by GitHub
commit 8544f389fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1835 additions and 358 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "lib/ImGuiScene"]
path = lib/ImGuiScene
url = https://github.com/ff-meli/ImGuiScene

View file

@ -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>

View file

@ -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);
}

View file

@ -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

View file

@ -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));

View file

@ -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,

View file

@ -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>

View 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.");
}
}
}
}

View file

@ -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();
}

View file

@ -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; }
}
}

View file

@ -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

View file

@ -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
}
}

View 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
}
}

View 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
}
}

View 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));
}
}
}
}

View 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));
}
}
}

View 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);
}
}
}

View 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());
}
}
}
}

View 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();
}
}
}

View file

@ -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("*");

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
using System;
namespace Dalamud.Game.Internal.DXGI {
public interface ISwapChainAddressResolver {
IntPtr Present { get; set; }
}
}

View 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");
}
}
}

View 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];
}
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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());
}
}
}

View file

@ -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,

View file

@ -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) {

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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));
}
}
}

View 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();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}

Binary file not shown.

BIN
Dalamud/UIRes/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

76
Dalamud/Util.cs Normal file
View 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());
}
}
}

View file

@ -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

@ -0,0 +1 @@
Subproject commit cd24a6108c05e52b0dde80e85ff1f9fa812f631b