merge from master

This commit is contained in:
goaaats 2025-03-08 16:10:25 +01:00
commit 6604678050
82 changed files with 2683 additions and 1371 deletions

View file

@ -6,6 +6,7 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# 4 space indentation
indent_style = space

View file

@ -55,6 +55,7 @@ jobs:
bin/Release/Dalamud.*.dll
bin/Release/Dalamud.*.exe
bin/Release/FFXIVClientStructs.dll
bin/Release/cim*.dll
- name: Upload artifact
uses: actions/upload-artifact@v4
with:

9
.gitmodules vendored
View file

@ -10,3 +10,12 @@
[submodule "lib/TsudaKageyu-minhook"]
path = lib/TsudaKageyu-minhook
url = https://github.com/TsudaKageyu/minhook.git
[submodule "lib/cimgui"]
path = lib/cimgui
url = https://github.com/goatcorp/gc-cimgui
[submodule "lib/cimplot"]
path = lib/cimplot
url = https://github.com/goatcorp/gc-cimplot
[submodule "lib/cimguizmo"]
path = lib/cimguizmo
url = https://github.com/goatcorp/gc-cimguizmo

View file

@ -78,9 +78,13 @@
"enum": [
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
@ -100,9 +104,13 @@
"enum": [
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",

View file

@ -28,7 +28,7 @@
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
@ -200,8 +200,10 @@
<ItemGroup>
<Manifest Include="themes.manifest" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</Target>
</Project>

View file

@ -25,9 +25,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="5.6.0" />
<PackageReference Include="Lumina.Excel" Version="7.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -41,10 +41,6 @@
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\injector\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings">
<NoWarn>IDE0003;IDE0044;IDE1006;CS1591;CS1701;CS1702</NoWarn>

View file

@ -6,16 +6,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
tools\BannedSymbols.txt = tools\BannedSymbols.txt
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
Directory.Build.props = Directory.Build.props
tools\BannedSymbols.txt = tools\BannedSymbols.txt
tools\dalamud.ruleset = tools\dalamud.ruleset
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud.csproj", "{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}"
ProjectSection(ProjectDependencies) = postProject
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16} = {76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}
{8430077C-F736-4246-A052-8EA1CECE844E} = {8430077C-F736-4246-A052-8EA1CECE844E}
{F258347D-31BE-4605-98CE-40E43BDF6F9D} = {F258347D-31BE-4605-98CE-40E43BDF6F9D}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}"
EndProject
@ -25,7 +30,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Da
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
EndProject
@ -49,6 +54,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imgui", "imgui", "{DBE5345E-6594-4A59-B183-1C3D5592269D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CS", "CS", "{8BBACF2D-7AB8-4610-A115-0E363D35C291}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimplot", "external\cimplot\cimplot.vcxproj", "{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimguizmo", "external\cimguizmo\cimguizmo.vcxproj", "{F258347D-31BE-4605-98CE-40E43BDF6F9D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -97,6 +116,10 @@ Global
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.Build.0 = Release|Any CPU
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
@ -117,21 +140,39 @@ Global
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Release|Any CPU.Build.0 = Release|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.Build.0 = Release|Any CPU
{8430077C-F736-4246-A052-8EA1CECE844E}.Debug|Any CPU.ActiveCfg = Debug|x64
{8430077C-F736-4246-A052-8EA1CECE844E}.Debug|Any CPU.Build.0 = Debug|x64
{8430077C-F736-4246-A052-8EA1CECE844E}.Release|Any CPU.ActiveCfg = Release|x64
{8430077C-F736-4246-A052-8EA1CECE844E}.Release|Any CPU.Build.0 = Release|x64
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}.Debug|Any CPU.ActiveCfg = Debug|x64
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}.Debug|Any CPU.Build.0 = Debug|x64
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}.Release|Any CPU.ActiveCfg = Release|x64
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16}.Release|Any CPU.Build.0 = Release|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Debug|Any CPU.ActiveCfg = Debug|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Debug|Any CPU.Build.0 = Debug|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.ActiveCfg = Release|x64
{F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{3620414C-7DFC-423E-929F-310E19F5D930} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{3620414C-7DFC-423E-929F-310E19F5D930} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{8430077C-F736-4246-A052-8EA1CECE844E} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{DBE5345E-6594-4A59-B183-1C3D5592269D} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{8BBACF2D-7AB8-4610-A115-0E363D35C291} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{F258347D-31BE-4605-98CE-40E43BDF6F9D} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
@ -11,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
@ -45,6 +47,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
[JsonIgnore]
private bool isSaveQueued;
private Task? writeTask;
/// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary>
@ -243,7 +247,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary>
public bool AssertsEnabledAtStartup { get; set; }
public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
@ -261,8 +265,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
/// on plugin title bars when using the Window System.
/// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
@ -348,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets the default UI preset.
/// </summary>
public PresetModel DefaultUiPreset { get; set; } = new();
/// <summary>
/// Gets or sets the order of DTR elements, by title.
/// </summary>
@ -484,10 +492,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating whether or not users should be notified regularly about pending updates.
/// Gets or sets a value indicating whether users should be notified regularly about pending updates.
/// </summary>
public bool CheckPeriodicallyForUpdates { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether users should be notified about updates in chat.
/// </summary>
public bool SendUpdateNotificationToChat { get; set; } = false;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
@ -555,6 +568,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
// Make sure that we save, if a save is queued while we are shutting down
this.Update();
// Wait for the write task to finish
this.writeTask?.Wait();
}
/// <summary>
@ -609,8 +625,22 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
if (this.configPath is null)
throw new InvalidOperationException("configPath is not set.");
// Wait for previous write to finish
this.writeTask?.Wait();
this.writeTask = Task.Run(() =>
{
Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
}
});
this.DalamudConfigurationSaved?.Invoke(this);
}
}

View file

@ -5,8 +5,8 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>11.0.2.0</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>11.0.8.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -43,10 +43,6 @@
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings">
<NoWarn>IDE0002;IDE0003;IDE1006;IDE0044;CA1822;CS1591;CS1701;CS1702</NoWarn>
@ -67,14 +63,14 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="5.6.0" />
<PackageReference Include="Lumina.Excel" Version="7.1.3" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />

View file

@ -179,6 +179,9 @@ public sealed class EntryPoint
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
// Apply common fixes for culture issues
CultureFixes.Apply();
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;

View file

@ -48,7 +48,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private bool lastConditionNone = true;
[ServiceManager.ServiceConstructor]
private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{

View file

@ -155,7 +155,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
};
}
[Api11ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
[Api12ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool WarnMultithreadedUsage()
{

View file

@ -26,6 +26,11 @@ public interface IReadOnlyCommandInfo
/// Gets a value indicating whether if this command should be shown in the help output.
/// </summary>
bool ShowInHelp { get; }
/// <summary>
/// Gets the display order of this command. Defaults to alphabetical ordering.
/// </summary>
int DisplayOrder { get; }
}
/// <summary>
@ -51,4 +56,7 @@ public sealed class CommandInfo : IReadOnlyCommandInfo
/// <inheritdoc/>
public bool ShowInHelp { get; set; } = true;
/// <inheritdoc/>
public int DisplayOrder { get; set; } = -1;
}

View file

@ -92,16 +92,22 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
var manager = RaptureAtkUnitManager.Instance();
if (manager == null)
return;
var menu = manager->GetAddonByName("ContextMenu");
var submenu = manager->GetAddonByName("AddonContextSub");
if (menu == null || submenu == null)
return;
if (menu->IsVisible)
menu->FireCallbackInt(-1);
if (submenu->IsVisible)
submenu->FireCallbackInt(-1);
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
}
/// <inheritdoc/>

View file

@ -145,7 +145,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
/// <inheritdoc/>
[Api11ToDo("Maybe make this config scoped to internalname?")]
[Api12ToDo("Maybe make this config scoped to internalname?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <summary>

View file

@ -1,9 +1,15 @@
using Dalamud.Game.Network.Internal;
using System.Linq;
using Dalamud.Game.Network.Internal;
using Dalamud.Game.Network.Structures;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using static Dalamud.Plugin.Services.IMarketBoard;
namespace Dalamud.Game.MarketBoard;
/// <summary>
@ -29,19 +35,19 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
}
/// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
public event HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
public event ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
public event OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
public event PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
public event TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/>
public void DisposeService()
@ -89,35 +95,42 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
#pragma warning restore SA1015
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
{
private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped));
[ServiceManager.ServiceDependency]
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get();
private readonly string owningPluginName;
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoardPluginScoped"/> class.
/// </summary>
internal MarketBoardPluginScoped()
/// <param name="plugin">The plugin owning this service.</param>
internal MarketBoardPluginScoped(LocalPlugin? plugin)
{
this.marketBoardService.HistoryReceived += this.OnHistoryReceived;
this.marketBoardService.ItemPurchased += this.OnItemPurchased;
this.marketBoardService.OfferingsReceived += this.OnOfferingsReceived;
this.marketBoardService.PurchaseRequested += this.OnPurchaseRequested;
this.marketBoardService.TaxRatesReceived += this.OnTaxRatesReceived;
this.owningPluginName = plugin?.InternalName ?? "DalamudInternal";
}
/// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
public event HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
public event ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
public event OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
public event PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
public event TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -137,26 +150,96 @@ internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoar
private void OnHistoryReceived(IMarketBoardHistory history)
{
this.HistoryReceived?.Invoke(history);
if (this.HistoryReceived == null) return;
foreach (var action in this.HistoryReceived.GetInvocationList().Cast<HistoryReceivedDelegate>())
{
try
{
action.Invoke(history);
}
catch (Exception ex)
{
this.LogInvocationError(ex, nameof(this.HistoryReceived));
}
}
}
private void OnItemPurchased(IMarketBoardPurchase purchase)
{
this.ItemPurchased?.Invoke(purchase);
if (this.ItemPurchased == null) return;
foreach (var action in this.ItemPurchased.GetInvocationList().Cast<ItemPurchasedDelegate>())
{
try
{
action.Invoke(purchase);
}
catch (Exception ex)
{
this.LogInvocationError(ex, nameof(this.ItemPurchased));
}
}
}
private void OnOfferingsReceived(IMarketBoardCurrentOfferings currentOfferings)
{
this.OfferingsReceived?.Invoke(currentOfferings);
if (this.OfferingsReceived == null) return;
foreach (var action in this.OfferingsReceived.GetInvocationList()
.Cast<OfferingsReceivedDelegate>())
{
try
{
action.Invoke(currentOfferings);
}
catch (Exception ex)
{
this.LogInvocationError(ex, nameof(this.OfferingsReceived));
}
}
}
private void OnPurchaseRequested(IMarketBoardPurchaseHandler purchaseHandler)
{
this.PurchaseRequested?.Invoke(purchaseHandler);
if (this.PurchaseRequested == null) return;
foreach (var action in this.PurchaseRequested.GetInvocationList().Cast<PurchaseRequestedDelegate>())
{
try
{
action.Invoke(purchaseHandler);
}
catch (Exception ex)
{
this.LogInvocationError(ex, nameof(this.PurchaseRequested));
}
}
}
private void OnTaxRatesReceived(IMarketTaxRates taxRates)
{
this.TaxRatesReceived?.Invoke(taxRates);
if (this.TaxRatesReceived == null) return;
foreach (var action in this.TaxRatesReceived.GetInvocationList().Cast<TaxRatesReceivedDelegate>())
{
try
{
action.Invoke(taxRates);
}
catch (Exception ex)
{
this.LogInvocationError(ex, nameof(this.TaxRatesReceived));
}
}
}
private void LogInvocationError(Exception ex, string delegateName)
{
Log.Error(
ex,
"An error occured while invoking event `{evName}` for {plugin}",
delegateName,
this.owningPluginName);
}
}

View file

@ -13,20 +13,26 @@ internal interface IMarketBoardUploader
/// Upload data about an item.
/// </summary>
/// <param name="item">The item request data being uploaded.</param>
/// <param name="uploaderId">The uploaders ContentId.</param>
/// <param name="worldId">The uploaders WorldId.</param>
/// <returns>An async task.</returns>
Task Upload(MarketBoardItemRequest item);
Task Upload(MarketBoardItemRequest item, ulong uploaderId, uint worldId);
/// <summary>
/// Upload tax rate data.
/// </summary>
/// <param name="taxRates">The tax rate data being uploaded.</param>
/// <param name="uploaderId">The uploaders ContentId.</param>
/// <param name="worldId">The uploaders WorldId.</param>
/// <returns>An async task.</returns>
Task UploadTax(MarketTaxRates taxRates);
Task UploadTax(MarketTaxRates taxRates, ulong uploaderId, uint worldId);
/// <summary>
/// Upload information about a purchase this client has made.
/// </summary>
/// <param name="purchaseHandler">The purchase handler data associated with the sale.</param>
/// <param name="uploaderId">The uploaders ContentId.</param>
/// <param name="worldId">The uploaders WorldId.</param>
/// <returns>An async task.</returns>
Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler);
Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler, ulong uploaderId, uint worldId);
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@ -33,21 +32,16 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
this.httpClient = happyHttpClient.SharedHttpClient;
/// <inheritdoc/>
public async Task Upload(MarketBoardItemRequest request)
public async Task Upload(MarketBoardItemRequest request, ulong uploaderId, uint worldId)
{
var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
Log.Verbose("Starting Universalis upload");
var uploader = clientState.LocalContentId;
// ====================================================================================
var uploadObject = new UniversalisItemUploadRequest
{
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
UploaderId = uploader.ToString(),
WorldId = worldId,
UploaderId = uploaderId.ToString(),
ItemId = request.CatalogId,
Listings = [],
Sales = [],
@ -117,18 +111,12 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
}
/// <inheritdoc/>
public async Task UploadTax(MarketTaxRates taxRates)
public async Task UploadTax(MarketTaxRates taxRates, ulong uploaderId, uint worldId)
{
var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
// ====================================================================================
var taxUploadObject = new UniversalisTaxUploadRequest
{
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
UploaderId = clientState.LocalContentId.ToString(),
WorldId = worldId,
UploaderId = uploaderId.ToString(),
TaxData = new UniversalisTaxData
{
LimsaLominsa = taxRates.LimsaLominsaTax,
@ -159,14 +147,9 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
/// to track the available listings, that is done via the listings packet. All this does is remove
/// a listing, or delete it, when a purchase has been made.
/// </remarks>
public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler)
public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler, ulong uploaderId, uint worldId)
{
var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
var itemId = purchaseHandler.CatalogId;
var worldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0;
// ====================================================================================
@ -176,7 +159,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
Quantity = purchaseHandler.ItemQuantity,
ListingId = purchaseHandler.ListingId.ToString(),
RetainerId = purchaseHandler.RetainerId.ToString(),
UploaderId = clientState.LocalContentId.ToString(),
UploaderId = uploaderId.ToString(),
};
var deletePath = $"/api/{worldId}/{itemId}/delete";

View file

@ -16,8 +16,11 @@ using Dalamud.Hooking;
using Dalamud.Networking.Http;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Network;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.Sheets;
using Serilog;
@ -264,6 +267,33 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.cfPopHook.Dispose();
}
private static (ulong UploaderId, uint WorldId) GetUploaderInfo()
{
var agentLobby = AgentLobby.Instance();
var uploaderId = agentLobby->LobbyData.ContentId;
if (uploaderId == 0)
{
var playerState = PlayerState.Instance();
if (playerState->IsLoaded == 1)
{
uploaderId = playerState->ContentId;
}
}
var worldId = agentLobby->LobbyData.CurrentWorldId;
if (worldId == 0)
{
var localPlayer = Control.GetLocalPlayer();
if (localPlayer != null)
{
worldId = localPlayer->CurrentWorld;
}
}
return (uploaderId, worldId);
}
private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData)
{
var result = this.cfPopHook.OriginalDisposeSafe(packetData);
@ -424,14 +454,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
startObservable
.And(this.OnMarketBoardSalesBatch(startObservable))
.And(this.OnMarketBoardListingsBatch(startObservable))
.Then((request, sales, listings) => (request, sales, listings)))
.Then((request, sales, listings) => (request, sales, listings, GetUploaderInfo())))
.Where(this.ShouldUpload)
.SubscribeOn(ThreadPoolScheduler.Instance)
.Subscribe(
data =>
{
var (request, sales, listings) = data;
this.UploadMarketBoardData(request, sales, listings);
var (request, sales, listings, uploaderInfo) = data;
this.UploadMarketBoardData(request, sales, listings, uploaderInfo.UploaderId, uploaderInfo.WorldId);
},
ex => Log.Error(ex, "Failed to handle Market Board item request event"));
}
@ -439,7 +469,9 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private void UploadMarketBoardData(
MarketBoardItemRequest request,
(uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales,
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings)
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings,
ulong uploaderId,
uint worldId)
{
var catalogId = sales.CatalogId;
if (listings.Count != request.AmountToArrive)
@ -460,7 +492,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
request.Listings.AddRange(listings);
request.History.AddRange(sales.Sales);
Task.Run(() => this.uploader.Upload(request))
Task.Run(() => this.uploader.Upload(request, uploaderId, worldId))
.ContinueWith(
task => Log.Error(task.Exception, "Market Board offerings data upload failed"),
TaskContinuationOptions.OnlyOnFaulted);
@ -469,11 +501,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketTaxRates()
{
return this.MbTaxesObservable
.Select((taxes) => (taxes, GetUploaderInfo()))
.Where(this.ShouldUpload)
.SubscribeOn(ThreadPoolScheduler.Instance)
.Subscribe(
taxes =>
data =>
{
var (taxes, uploaderInfo) = data;
Log.Verbose(
"MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}",
taxes.LimsaLominsaTax,
@ -484,7 +519,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
taxes.CrystariumTax,
taxes.SharlayanTax);
Task.Run(() => this.uploader.UploadTax(taxes))
Task.Run(() => this.uploader.UploadTax(taxes, uploaderInfo.UploaderId, uploaderInfo.WorldId))
.ContinueWith(
task => Log.Error(task.Exception, "Market Board tax data upload failed"),
TaskContinuationOptions.OnlyOnFaulted);
@ -495,13 +530,13 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketBoardPurchaseHandler()
{
return this.MbPurchaseSentObservable
.Zip(this.MbPurchaseObservable)
.Zip(this.MbPurchaseObservable, (handler, purchase) => (handler, purchase, GetUploaderInfo()))
.Where(this.ShouldUpload)
.SubscribeOn(ThreadPoolScheduler.Instance)
.Subscribe(
data =>
{
var (handler, purchase) = data;
var (handler, purchase, uploaderInfo) = data;
var sameQty = purchase.ItemQuantity == handler.ItemQuantity;
var itemMatch = purchase.CatalogId == handler.CatalogId;
@ -516,7 +551,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
handler.CatalogId,
handler.PricePerUnit * purchase.ItemQuantity,
handler.ListingId);
Task.Run(() => this.uploader.UploadPurchase(handler))
Task.Run(() => this.uploader.UploadPurchase(handler, uploaderInfo.UploaderId, uploaderInfo.WorldId))
.ContinueWith(
task => Log.Error(task.Exception, "Market Board purchase data upload failed"),
TaskContinuationOptions.OnlyOnFaulted);

View file

@ -36,12 +36,12 @@ public sealed class XivChatEntry
}
/// <summary>
/// Gets or Sets the name payloads
/// Gets or sets the name payloads.
/// </summary>
public byte[] NameBytes { get; set; } = [];
/// <summary>
/// Gets or Sets the message payloads.
/// Gets or sets the message payloads.
/// </summary>
public byte[] MessageBytes { get; set; } = [];

View file

@ -1,11 +1,14 @@
using System.Diagnostics;
using System.Numerics;
using Dalamud.Utility;
namespace Dalamud.Interface.Animation;
/// <summary>
/// Base class facilitating the implementation of easing functions.
/// </summary>
[Api12ToDo("Re-apply https://github.com/goatcorp/Dalamud/commit/1aada983931d9e45a250eebbc17c8b782d07701b")]
public abstract class Easing
{
// TODO: Use game delta time here instead

View file

@ -182,7 +182,10 @@ public static partial class ImGuiComponents
/// </summary>
/// <param name="icon">Icon to show.</param>
/// <param name="text">Text to show.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
/// <param name="size">
/// Sets the size of the button. If either dimension is set to 0,
/// that dimension will conform to the size of the icon and text.
/// </param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector2 size) => IconButtonWithText(icon, text, null, null, null, size);
@ -194,7 +197,10 @@ public static partial class ImGuiComponents
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
/// <param name="size">
/// Sets the size of the button. If either dimension is set to 0,
/// that dimension will conform to the size of the icon and text.
/// </param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null)
{
@ -272,15 +278,14 @@ public static partial class ImGuiComponents
/// <returns>Width.</returns>
public static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text)
{
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var iconSize = ImGui.CalcTextSize(icon.ToIconString());
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var textSize = ImGui.CalcTextSize(text);
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
}
}
}

View file

@ -8,6 +8,9 @@ using ImGuiNET;
namespace Dalamud.Interface.Components;
/// <summary>
/// ImGui component used to create a radio-like input that uses icon buttons.
/// </summary>
public static partial class ImGuiComponents
{
/// <summary>

View file

@ -0,0 +1,228 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Interface.Internal.Asserts;
/// <summary>
/// Class responsible for registering and handling ImGui asserts.
/// </summary>
internal class AssertHandler : IDisposable
{
private const int HideThreshold = 20;
private const int HidePrintEvery = 500;
private readonly HashSet<string> ignoredAsserts = [];
private readonly Dictionary<string, uint> assertCounts = new();
// Store callback to avoid it from being GC'd
private readonly AssertCallbackDelegate callback;
private bool everShownAssertThisSession = false;
/// <summary>
/// Initializes a new instance of the <see cref="AssertHandler"/> class.
/// </summary>
public AssertHandler()
{
this.callback = (expr, file, line) => this.OnImGuiAssert(expr, file, line);
}
private delegate void AssertCallbackDelegate(
[MarshalAs(UnmanagedType.LPStr)] string expr,
[MarshalAs(UnmanagedType.LPStr)] string file,
int line);
/// <summary>
/// Gets or sets a value indicating whether ImGui asserts should be shown to the user.
/// </summary>
public bool ShowAsserts { get; set; }
/// <summary>
/// Gets or sets a value indicating whether we want to hide asserts that occur frequently (= every update)
/// and whether we want to log callstacks.
/// </summary>
public bool EnableVerboseLogging { get; set; }
/// <summary>
/// Register the cimgui assert handler with the native library.
/// </summary>
public void Setup()
{
CustomNativeFunctions.igCustom_SetAssertCallback(this.callback);
}
/// <summary>
/// Unregister the cimgui assert handler with the native library.
/// </summary>
public void Shutdown()
{
CustomNativeFunctions.igCustom_SetAssertCallback(null);
}
/// <inheritdoc/>
public void Dispose()
{
this.Shutdown();
}
private void OnImGuiAssert(string expr, string file, int line)
{
var key = $"{file}:{line}";
if (this.ignoredAsserts.Contains(key))
return;
// Don't log unless we've ever shown an assert this session
if (!this.ShowAsserts && !this.everShownAssertThisSession)
return;
Lazy<string> stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()).ToString());
if (!this.EnableVerboseLogging)
{
if (this.assertCounts.TryGetValue(key, out var count))
{
this.assertCounts[key] = count + 1;
if (count <= HideThreshold || count % HidePrintEvery == 0)
{
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
expr,
file,
line,
count);
}
}
else
{
this.assertCounts[key] = 1;
}
}
else
{
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
expr,
file,
line,
stackTrace.Value);
}
if (!this.ShowAsserts)
return;
this.everShownAssertThisSession = true;
string? GetRepoUrl()
{
// TODO: implot, imguizmo?
const string userName = "goatcorp";
const string repoName = "gc-imgui";
const string branch = "1.88-enhanced-abifix";
if (!file.Contains("imgui", StringComparison.OrdinalIgnoreCase))
return null;
var lastSlash = file.LastIndexOf('\\');
var fileName = file[(lastSlash + 1)..];
return $"https://github.com/{userName}/{repoName}/blob/{branch}/{fileName}#L{line}";
}
// grab the stack trace now that we've decided to show UI.
_ = stackTrace.Value;
var gitHubUrl = GetRepoUrl();
var showOnGitHubButton = new TaskDialogButton
{
Text = "Open GitHub",
AllowCloseDialog = false,
Enabled = !gitHubUrl.IsNullOrEmpty(),
};
showOnGitHubButton.Click += (_, _) =>
{
if (!gitHubUrl.IsNullOrEmpty())
Util.OpenLink(gitHubUrl);
};
var breakButton = new TaskDialogButton
{
Text = "Break",
AllowCloseDialog = true,
};
var disableButton = new TaskDialogButton
{
Text = "Disable for this session",
AllowCloseDialog = true,
};
var ignoreButton = TaskDialogButton.Ignore;
TaskDialogButton? result = null;
void DialogThreadStart()
{
// TODO(goat): This is probably not gonna work if we showed the loading dialog
// this session since it already loaded visual styles...
Application.EnableVisualStyles();
var page = new TaskDialogPage
{
Heading = "ImGui assertion failed",
Caption = "Dalamud",
Expander = new TaskDialogExpander
{
CollapsedButtonText = "Show stack trace",
ExpandedButtonText = "Hide stack trace",
Text = stackTrace.Value,
},
Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}",
Icon = TaskDialogIcon.Warning,
Buttons =
[
showOnGitHubButton,
breakButton,
disableButton,
ignoreButton,
],
DefaultButton = showOnGitHubButton,
};
result = TaskDialog.ShowDialog(page);
}
// Run in a separate thread because of STA and to not mess up other stuff
var thread = new Thread(DialogThreadStart)
{
Name = "Dalamud ImGui Assert Dialog",
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (result == breakButton)
{
Debugger.Break();
}
else if (result == disableButton)
{
this.ShowAsserts = false;
}
else if (result == ignoreButton)
{
this.ignoredAsserts.Add(key);
}
}
private static class CustomNativeFunctions
{
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
#pragma warning disable SA1300
public static extern void igCustom_SetAssertCallback(AssertCallbackDelegate? callback);
#pragma warning restore SA1300
}
}

View file

@ -178,7 +178,7 @@ internal class DalamudCommands : IServiceType
if (arguments.IsNullOrWhitespace())
{
chatGui.Print(Loc.Localize("DalamudCmdHelpAvailable", "Available commands:"));
foreach (var cmd in commandManager.Commands)
foreach (var cmd in commandManager.Commands.OrderBy(cInfo => cInfo.Key))
{
if (!cmd.Value.ShowInHelp)
continue;

View file

@ -16,7 +16,6 @@ using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.Colors;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
@ -38,9 +37,6 @@ namespace Dalamud.Interface.Internal;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudIme : IInternalDisposableService
{
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
private const int CImGuiStbTextUndoOffset = 0xB59C0;
private const int ImePageSize = 9;
private static readonly Dictionary<int, string> WmNames =
@ -70,11 +66,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
UnicodeRanges.HangulJamoExtendedB,
};
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>
StbTextMakeUndoReplace;
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void> StbTextUndo;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
@ -135,13 +126,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
{
return;
}
StbTextMakeUndoReplace =
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>)
(cimgui + CImGuiStbTextCreateUndoOffset);
StbTextUndo =
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void>)
(cimgui + CImGuiStbTextUndoOffset);
}
[ServiceManager.ServiceConstructor]
@ -185,7 +169,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
return true;
if (!ImGui.GetIO().ConfigInputTextCursorBlink)
return true;
var textState = TextState;
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0)
return true;
if (textState->CursorAnim <= 0)
@ -194,9 +178,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
}
}
private static ImGuiInputTextState* TextState =>
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
/// <summary>Gets a value indicating whether to display partial conversion status.</summary>
private bool ShowPartialConversion => this.partialConversionFrom != 0 ||
this.partialConversionTo != this.compositionString.Length;
@ -341,7 +322,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
try
{
var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
var invalidTarget = textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
#if IMEDEBUG
switch (args.Message)
@ -570,19 +552,20 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.ReflectCharacterEncounters(newString);
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
if (this.temporaryUndoSelection is not null)
{
TextState->Undo();
TextState->SelectionTuple = this.temporaryUndoSelection.Value;
textState->Undo();
textState->SelectionTuple = this.temporaryUndoSelection.Value;
this.temporaryUndoSelection = null;
}
TextState->SanitizeSelectionRange();
if (TextState->ReplaceSelectionAndPushUndo(newString))
this.temporaryUndoSelection = TextState->SelectionTuple;
textState->SanitizeSelectionRange();
if (textState->ReplaceSelectionAndPushUndo(newString))
this.temporaryUndoSelection = textState->SelectionTuple;
// Put the cursor at the beginning, so that the candidate window appears aligned with the text.
TextState->SetSelectionRange(TextState->SelectionTuple.Start, newString.Length, 0);
textState->SetSelectionRange(textState->SelectionTuple.Start, newString.Length, 0);
if (finalCommit)
{
@ -627,7 +610,10 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.partialConversionFrom = this.partialConversionTo = 0;
this.compositionCursorOffset = 0;
this.temporaryUndoSelection = null;
TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd;
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
textState->Stb.SelectStart = textState->Stb.Cursor = textState->Stb.SelectEnd;
this.candidateStrings.Clear();
this.immCandNative = default;
if (invokeCancel)
@ -1030,14 +1016,14 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
(s, e) = (e, s);
}
public void Undo() => StbTextUndo(this.ThisPtr, &this.ThisPtr->Stb);
public void Undo() => CustomNativeFunctions.igCustom_StbTextUndo(this.ThisPtr);
public bool MakeUndoReplace(int offset, int oldLength, int newLength)
{
if (oldLength == 0 && newLength == 0)
return false;
StbTextMakeUndoReplace(this.ThisPtr, &this.ThisPtr->Stb, offset, oldLength, newLength);
CustomNativeFunctions.igCustom_StbTextMakeUndoReplace(this.ThisPtr, offset, oldLength, newLength);
return true;
}
@ -1113,6 +1099,20 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
}
}
private static class CustomNativeFunctions
{
#pragma warning disable SA1300
[DllImport("cimgui")]
public static extern ImGuiInputTextState* igCustom_GetInputTextState();
[DllImport("cimgui")]
public static extern void igCustom_StbTextMakeUndoReplace(ImGuiInputTextState* str, int where, int old_length, int new_length);
[DllImport("cimgui")]
public static extern void igCustom_StbTextUndo(ImGuiInputTextState* str);
#pragma warning restore SA1300
}
#if IMEDEBUG
private static class ImeDebug
{

View file

@ -18,7 +18,6 @@ using Dalamud.Game.Internal;
using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Internal.Windows.Data;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
@ -58,7 +57,6 @@ internal class DalamudInterface : IInternalDisposableService
private readonly Dalamud dalamud;
private readonly DalamudConfiguration configuration;
private readonly InterfaceManager interfaceManager;
private readonly DataManager dataManager;
private readonly ChangelogWindow changelogWindow;
private readonly ColorDemoWindow colorDemoWindow;
@ -100,7 +98,6 @@ internal class DalamudInterface : IInternalDisposableService
DalamudConfiguration configuration,
FontAtlasFactory fontAtlasFactory,
InterfaceManager interfaceManager,
DataManager dataManager,
PluginImageCache pluginImageCache,
DalamudAssetManager dalamudAssetManager,
Game.Framework framework,
@ -113,7 +110,6 @@ internal class DalamudInterface : IInternalDisposableService
this.dalamud = dalamud;
this.configuration = configuration;
this.interfaceManager = interfaceManager;
this.dataManager = dataManager;
this.WindowSystem = new WindowSystem("DalamudCore");
@ -163,7 +159,7 @@ internal class DalamudInterface : IInternalDisposableService
this.WindowSystem.AddWindow(this.branchSwitcherWindow);
this.WindowSystem.AddWindow(this.hitchSettingsWindow);
ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup;
this.interfaceManager.ShowAsserts = configuration.ImGuiAssertsEnabledAtStartup ?? false;
this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup;
this.interfaceManager.Draw += this.OnDraw;
@ -833,6 +829,12 @@ internal class DalamudInterface : IInternalDisposableService
}
}
if (ImGui.MenuItem("Cause ImGui assert"))
{
ImGui.PopStyleVar();
ImGui.PopStyleVar();
}
ImGui.EndMenu();
}
@ -865,18 +867,27 @@ internal class DalamudInterface : IInternalDisposableService
ImGui.Separator();
var val = ImGuiManagedAsserts.AssertsEnabled;
if (ImGui.MenuItem("Enable Asserts", string.Empty, ref val))
var showAsserts = this.interfaceManager.ShowAsserts;
if (ImGui.MenuItem("Enable assert popups", string.Empty, ref showAsserts))
{
ImGuiManagedAsserts.AssertsEnabled = val;
this.interfaceManager.ShowAsserts = showAsserts;
}
if (ImGui.MenuItem("Enable asserts at startup", null, this.configuration.AssertsEnabledAtStartup))
var enableVerboseAsserts = this.interfaceManager.EnableVerboseAssertLogging;
if (ImGui.MenuItem("Enable verbose assert logging", string.Empty, ref enableVerboseAsserts))
{
this.configuration.AssertsEnabledAtStartup = !this.configuration.AssertsEnabledAtStartup;
this.interfaceManager.EnableVerboseAssertLogging = enableVerboseAsserts;
}
var assertsEnabled = this.configuration.ImGuiAssertsEnabledAtStartup ?? false;
if (ImGui.MenuItem("Enable asserts at startup", null, assertsEnabled))
{
this.configuration.ImGuiAssertsEnabledAtStartup = !assertsEnabled;
this.configuration.QueueSave();
}
ImGui.Separator();
if (ImGui.MenuItem("Clear focus"))
{
ImGui.SetWindowFocus(null);
@ -1001,6 +1012,11 @@ internal class DalamudInterface : IInternalDisposableService
pluginManager.LoadBannedPlugins = !pluginManager.LoadBannedPlugins;
}
if (pluginManager.SafeMode && ImGui.MenuItem("Disable Safe Mode"))
{
pluginManager.SafeMode = false;
}
ImGui.Separator();
ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false);
ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count(), false);

View file

@ -44,7 +44,9 @@ internal static partial class DalamudComponents
ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255);
var currentSearchString = pickerSearch;
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
using var listBox = ImRaii.ListBox("###pluginPicker", new Vector2(width, width - 80));
if (listBox.Success)
{
// TODO: Plugin searching should be abstracted... installer and this should use the same search
var plugins = pm.InstalledPlugins.Where(
@ -56,16 +58,12 @@ internal static partial class DalamudComponents
foreach (var plugin in plugins)
{
using var disabled2 =
ImRaii.Disabled(pluginDisabled(plugin));
using var disabled2 = ImRaii.Disabled(pluginDisabled(plugin));
if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
{
onClicked(plugin);
}
}
ImGui.EndListBox();
}
}

View file

@ -1,222 +0,0 @@
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Dalamud.Hooking;
using ImGuiNET;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global
/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected.
///
/// * The implementation for AddRectFilled is entirely replaced with the hook below.
/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID.
///
/// TODO:
/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled
/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block,
/// if _Data->TexIdCommon != _CmdHeader.TextureId.
/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine
/// The if block always needs to call PushTextureID if the abovementioned condition is not met.
/// Change push_texture_id to only have one condition.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService
{
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
private const int CImGuiImDrawListAddImageRounded = 0x58390;
private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0;
private readonly Hook<ImDrawListAddPolyLine> hookImDrawListAddPolyline;
private readonly Hook<ImDrawListAddRectFilled> hookImDrawListAddRectFilled;
private readonly Hook<ImDrawListAddImageRounded> hookImDrawListAddImageRounded;
[ServiceManager.ServiceConstructor]
private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws)
{
// Force cimgui.dll to be loaded.
_ = ImGui.GetCurrentContext();
var cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
.First(x => x.ModuleName == "cimgui.dll")
.BaseAddress;
this.hookImDrawListAddPolyline = Hook<ImDrawListAddPolyLine>.FromAddress(
cimgui + CImGuiImDrawListAddPolyLineOffset,
this.ImDrawListAddPolylineDetour);
this.hookImDrawListAddRectFilled = Hook<ImDrawListAddRectFilled>.FromAddress(
cimgui + CImGuiImDrawListAddRectFilled,
this.ImDrawListAddRectFilledDetour);
this.hookImDrawListAddImageRounded = Hook<ImDrawListAddImageRounded>.FromAddress(
cimgui + CImGuiImDrawListAddImageRounded,
this.ImDrawListAddImageRoundedDetour);
this.hookImDrawListAddPolyline.Enable();
this.hookImDrawListAddRectFilled.Enable();
this.hookImDrawListAddImageRounded.Enable();
}
private delegate void ImDrawListAddPolyLine(
ImDrawListPtr drawListPtr,
ref Vector2 points,
int pointsCount,
uint color,
ImDrawFlags flags,
float thickness);
private delegate void ImDrawListAddRectFilled(
ImDrawListPtr drawListPtr,
ref Vector2 min,
ref Vector2 max,
uint col,
float rounding,
ImDrawFlags flags);
private delegate void ImDrawListAddImageRounded(
ImDrawListPtr drawListPtr,
nint userTextureId, ref Vector2 xy0,
ref Vector2 xy1,
ref Vector2 uv0,
ref Vector2 uv1,
uint col,
float rounding,
ImDrawFlags flags);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.hookImDrawListAddPolyline.Dispose();
this.hookImDrawListAddRectFilled.Dispose();
this.hookImDrawListAddImageRounded.Dispose();
}
private static ImDrawFlags FixRectCornerFlags(ImDrawFlags flags)
{
#if !IMGUI_DISABLE_OBSOLETE_FUNCTIONS
// Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All)
// ~0 --> ImDrawFlags_RoundCornersAll or 0
if ((int)flags == ~0)
return ImDrawFlags.RoundCornersAll;
// Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations)
// 0x01 --> ImDrawFlags_RoundCornersTopLeft (VALUE 0x01 OVERLAPS ImDrawFlags_Closed but ImDrawFlags_Closed is never valid in this path!)
// 0x02 --> ImDrawFlags_RoundCornersTopRight
// 0x03 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight
// 0x04 --> ImDrawFlags_RoundCornersBotLeft
// 0x05 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBotLeft
// ...
// 0x0F --> ImDrawFlags_RoundCornersAll or 0
// (See all values in ImDrawCornerFlags_)
if ((int)flags >= 0x01 && (int)flags <= 0x0F)
return (ImDrawFlags)((int)flags << 4);
// We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f'
#endif
// If this triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values.
// Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc...
if (((int)flags & 0x0F) != 0)
throw new ArgumentException("Misuse of legacy hardcoded ImDrawCornerFlags values!");
if ((flags & ImDrawFlags.RoundCornersMask) == 0)
flags |= ImDrawFlags.RoundCornersAll;
return flags;
}
private void ImDrawListAddRectFilledDetour(
ImDrawListPtr drawListPtr,
ref Vector2 min,
ref Vector2 max,
uint col,
float rounding,
ImDrawFlags flags)
{
// Skip drawing if we're drawing something with alpha value of 0.
if ((col & 0xFF000000) == 0)
return;
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask)
{
// Take the fast path of drawing two triangles if no rounded corners are required.
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
if (pushTextureId)
drawListPtr.PushTextureID(texIdCommon);
drawListPtr.PrimReserve(6, 4);
drawListPtr.PrimRect(min, max, col);
if (pushTextureId)
drawListPtr.PopTextureID();
}
else
{
// Defer drawing rectangle with rounded corners to path drawing operations.
// Note that this may have a slightly different extent behaviors from the above if case.
// This is how it is in imgui_draw.cpp.
drawListPtr.PathRect(min, max, rounding, flags);
drawListPtr.PathFillConvex(col);
}
}
private void ImDrawListAddPolylineDetour(
ImDrawListPtr drawListPtr,
ref Vector2 points,
int pointsCount,
uint color,
ImDrawFlags flags,
float thickness)
{
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
if (pushTextureId)
drawListPtr.PushTextureID(texIdCommon);
this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness);
if (pushTextureId)
drawListPtr.PopTextureID();
}
private void ImDrawListAddImageRoundedDetour(ImDrawListPtr drawListPtr, nint userTextureId, ref Vector2 xy0, ref Vector2 xy1, ref Vector2 uv0, ref Vector2 uv1, uint col, float rounding, ImDrawFlags flags)
{
// Skip drawing if we're drawing something with alpha value of 0.
if ((col & 0xFF000000) == 0)
return;
// Handle non-rounded cases.
flags = FixRectCornerFlags(flags);
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersNone)
{
drawListPtr.AddImage(userTextureId, xy0, xy1, uv0, uv1, col);
return;
}
// Temporary provide the requested image as the common texture ID, so that the underlying
// ImDrawList::AddConvexPolyFilled does not create a separate draw command and then revert back.
// ImDrawList::AddImageRounded will temporarily push the texture ID provided by the user if the latest draw
// command does not point to the texture we're trying to draw. Once pushed, ImDrawList::AddConvexPolyFilled
// will leave the list of draw commands alone, so that ImGui::ShadeVertsLinearUV can safely work on the latest
// draw command.
ref var texIdCommon = ref *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
var texIdCommonPrev = texIdCommon;
texIdCommon = userTextureId;
this.hookImDrawListAddImageRounded.Original(
drawListPtr,
texIdCommon,
ref xy0,
ref xy1,
ref uv0,
ref uv1,
col,
rounding,
flags);
texIdCommon = texIdCommonPrev;
}
}

View file

@ -19,14 +19,16 @@ using Dalamud.Hooking.Internal;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Asserts;
using Dalamud.Interface.Internal.DesignSystem;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@ -60,6 +62,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface.
/// </summary>
[ServiceManager.EarlyLoadedService]
[InherentDependency<WindowSystemPersistence>] // Used by window system windows to restore state from the configuration
internal partial class InterfaceManager : IInternalDisposableService
{
/// <summary>
@ -94,6 +97,8 @@ internal partial class InterfaceManager : IInternalDisposableService
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
private readonly AssertHandler assertHandler = new();
private RawDX11Scene? scene;
private Hook<SetCursorDelegate>? setCursorHook;
@ -267,11 +272,27 @@ internal partial class InterfaceManager : IInternalDisposableService
/// </remarks>
public long CumulativePresentCalls { get; private set; }
/// <inheritdoc cref="AssertHandler.ShowAsserts"/>
public bool ShowAsserts
{
get => this.assertHandler.ShowAsserts;
set => this.assertHandler.ShowAsserts = value;
}
/// <inheritdoc cref="AssertHandler.EnableVerboseLogging"/>
public bool EnableVerboseAssertLogging
{
get => this.assertHandler.EnableVerboseLogging;
set => this.assertHandler.EnableVerboseLogging = value;
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IInternalDisposableService.DisposeService()
{
this.assertHandler.Dispose();
// Unload hooks from the framework thread if possible.
// We're currently off the framework thread, as this function can only be called from
// ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread.
@ -565,6 +586,7 @@ internal partial class InterfaceManager : IInternalDisposableService
{
try
{
this.assertHandler.Setup();
newScene = new RawDX11Scene((nint)swapChain);
}
catch (DllNotFoundException ex)
@ -1128,15 +1150,22 @@ internal partial class InterfaceManager : IInternalDisposableService
WindowSystem.HasAnyWindowSystemFocus = false;
WindowSystem.FocusedWindowSystemNamespace = string.Empty;
var snap = ImGuiManagedAsserts.GetSnapshot();
if (this.IsDispatchingEvents)
{
try
{
this.Draw?.Invoke();
Service<NotificationManager>.GetNullable()?.Draw();
}
catch (Exception ex)
{
Log.Error(ex, "Error when invoking global Draw");
// We should always handle this in the callbacks.
Util.Fatal("An internal error occurred while drawing the Dalamud UI and the game must close.\nPlease report this error.", "Dalamud");
}
ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap);
Service<NotificationManager>.GetNullable()?.Draw();
}
}
/// <summary>

View file

@ -1,23 +0,0 @@
namespace Dalamud.Interface.Internal.ManagedAsserts;
/// <summary>
/// Offsets to various data in ImGui context.
/// </summary>
/// <remarks>
/// Last updated for ImGui 1.83.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the unsage instead.")]
internal static class ImGuiContextOffsets
{
public const int CurrentWindowStackOffset = 0x73A;
public const int ColorStackOffset = 0x79C;
public const int StyleVarStackOffset = 0x7A0;
public const int FontStackOffset = 0x7A4;
public const int BeginPopupStackOffset = 0x7B8;
public const int TextStateOffset = 0x4588;
}

View file

@ -1,140 +0,0 @@
using System.Diagnostics;
using ImGuiNET;
using static Dalamud.NativeFunctions;
namespace Dalamud.Interface.Internal.ManagedAsserts;
/// <summary>
/// Report ImGui problems with a MessageBox dialog.
/// </summary>
internal static class ImGuiManagedAsserts
{
/// <summary>
/// Gets or sets a value indicating whether asserts are enabled for ImGui.
/// </summary>
public static bool AssertsEnabled { get; set; }
/// <summary>
/// Create a snapshot of the current ImGui context.
/// Should be called before rendering an ImGui frame.
/// </summary>
/// <returns>A snapshot of the current context.</returns>
public static unsafe ImGuiContextSnapshot GetSnapshot()
{
var contextPtr = ImGui.GetCurrentContext();
var styleVarStack = *((int*)contextPtr + ImGuiContextOffsets.StyleVarStackOffset); // ImVector.Size
var colorStack = *((int*)contextPtr + ImGuiContextOffsets.ColorStackOffset); // ImVector.Size
var fontStack = *((int*)contextPtr + ImGuiContextOffsets.FontStackOffset); // ImVector.Size
var popupStack = *((int*)contextPtr + ImGuiContextOffsets.BeginPopupStackOffset); // ImVector.Size
var windowStack = *((int*)contextPtr + ImGuiContextOffsets.CurrentWindowStackOffset); // ImVector.Size
return new ImGuiContextSnapshot
{
StyleVarStackSize = styleVarStack,
ColorStackSize = colorStack,
FontStackSize = fontStack,
BeginPopupStackSize = popupStack,
WindowStackSize = windowStack,
};
}
/// <summary>
/// Compare a snapshot to the current post-draw state and report any errors in a MessageBox dialog.
/// </summary>
/// <param name="source">The source of any problems, something to blame.</param>
/// <param name="before">ImGui context snapshot.</param>
public static void ReportProblems(string source, ImGuiContextSnapshot before)
{
// TODO: Needs to be updated for ImGui 1.88
return;
#pragma warning disable CS0162
if (!AssertsEnabled)
{
return;
}
var cSnap = GetSnapshot();
if (before.StyleVarStackSize != cSnap.StyleVarStackSize)
{
ShowAssert(source, $"You forgot to pop a style var!\n\nBefore: {before.StyleVarStackSize}, after: {cSnap.StyleVarStackSize}");
return;
}
if (before.ColorStackSize != cSnap.ColorStackSize)
{
ShowAssert(source, $"You forgot to pop a color!\n\nBefore: {before.ColorStackSize}, after: {cSnap.ColorStackSize}");
return;
}
if (before.FontStackSize != cSnap.FontStackSize)
{
ShowAssert(source, $"You forgot to pop a font!\n\nBefore: {before.FontStackSize}, after: {cSnap.FontStackSize}");
return;
}
if (before.BeginPopupStackSize != cSnap.BeginPopupStackSize)
{
ShowAssert(source, $"You forgot to end a popup!\n\nBefore: {before.BeginPopupStackSize}, after: {cSnap.BeginPopupStackSize}");
return;
}
if (cSnap.WindowStackSize != 1)
{
if (cSnap.WindowStackSize > 1)
{
ShowAssert(source, $"Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?\n\ncSnap.WindowStackSize = {cSnap.WindowStackSize}");
}
else
{
ShowAssert(source, $"Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?\n\ncSnap.WindowStackSize = {cSnap.WindowStackSize}");
}
}
#pragma warning restore CS0162
}
private static void ShowAssert(string source, string message)
{
var caption = $"You fucked up";
message = $"{message}\n\nSource: {source}\n\nAsserts are now disabled. You may re-enable them.";
var flags = MessageBoxType.Ok | MessageBoxType.IconError;
_ = MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
AssertsEnabled = false;
}
/// <summary>
/// A snapshot of various ImGui context properties.
/// </summary>
public class ImGuiContextSnapshot
{
/// <summary>
/// Gets the ImGui style var stack size.
/// </summary>
public int StyleVarStackSize { get; init; }
/// <summary>
/// Gets the ImGui color stack size.
/// </summary>
public int ColorStackSize { get; init; }
/// <summary>
/// Gets the ImGui font stack size.
/// </summary>
public int FontStackSize { get; init; }
/// <summary>
/// Gets the ImGui begin popup stack size.
/// </summary>
public int BeginPopupStackSize { get; init; }
/// <summary>
/// Gets the ImGui window stack size.
/// </summary>
public int WindowStackSize { get; init; }
}
}

View file

@ -60,6 +60,7 @@ internal class DataWindow : Window, IDisposable
new ToastWidget(),
new UiColorWidget(),
new UldWidget(),
new VfsWidget(),
};
private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules;

View file

@ -126,7 +126,6 @@ public class IconBrowserWidget : IDataWindowWidget
this.valueRange = null;
}
ImGui.NextColumn();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0))

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Text;
using ImGuiNET;
using System.Linq;
using System.Linq;
using Dalamud.Game.Text;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;

View file

@ -15,7 +15,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.Payloads;
@ -31,7 +30,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue"];
private ImVectorWrapper<byte> testStringBuffer;
private string testString = string.Empty;
private ExcelSheet<Addon> addons = null!;
private ReadOnlySeString? logkind;
private SeStringDrawParams style;
private bool interactable;
@ -51,7 +49,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
public void Load()
{
this.style = new() { GetEntity = this.GetEntity };
this.addons = Service<DataManager>.Get().GetExcelSheet<Addon>();
this.logkind = null;
this.testString = string.Empty;
this.interactable = this.useEntity = true;
@ -193,13 +190,16 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA").X);
ImGui.TableHeadersRow();
var addon = Service<DataManager>.GetNullable()?.GetExcelSheet<Addon>() ??
throw new InvalidOperationException("Addon sheet not loaded.");
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
clipper.Begin(this.addons.Count);
clipper.Begin(addon.Count);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var row = this.addons.GetRowAt(i);
var row = addon.GetRowAt(i);
ImGui.TableNextRow();
ImGui.PushID(i);

View file

@ -1,5 +1,4 @@
using System.Buffers.Binary;
using System.Linq;
using System.Numerics;
using System.Text;
@ -7,12 +6,9 @@ using Dalamud.Data;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using ImGuiNET;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -22,8 +18,6 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class UiColorWidget : IDataWindowWidget
{
private ExcelSheet<UIColor> colors;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["uicolor"];
@ -37,12 +31,14 @@ internal class UiColorWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
this.colors = Service<DataManager>.Get().GetExcelSheet<UIColor>();
}
/// <inheritdoc/>
public unsafe void Draw()
{
var colors = Service<DataManager>.GetNullable()?.GetExcelSheet<UIColor>()
?? throw new InvalidOperationException("UIColor sheet not loaded.");
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(
"· Color notation is #" +
"<edgecolor(0xFFEEEE)><color(0xFF0000)>RR<color(stackcolor)><edgecolor(stackcolor)>" +
@ -71,16 +67,16 @@ internal class UiColorWidget : IDataWindowWidget
ImGui.TableHeadersRow();
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
clipper.Begin(this.colors.Count, ImGui.GetFrameHeightWithSpacing());
clipper.Begin(colors.Count, ImGui.GetFrameHeightWithSpacing());
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var row = this.colors.GetRowAt(i);
var row = colors.GetRowAt(i);
UIColor? adjacentRow = null;
if (i + 1 < this.colors.Count)
if (i + 1 < colors.Count)
{
var adjRow = this.colors.GetRowAt(i + 1);
var adjRow = colors.GetRowAt(i + 1);
if (adjRow.RowId == row.RowId + 1)
{
adjacentRow = adjRow;

View file

@ -0,0 +1,102 @@
using System.Diagnostics;
using System.IO;
using Dalamud.Configuration.Internal;
using Dalamud.Storage;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying configuration info.
/// </summary>
internal class VfsWidget : IDataWindowWidget
{
private int numBytes = 1024;
private int reps = 1;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "vfs" };
/// <inheritdoc/>
public string DisplayName { get; init; } = "VFS Performance";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load()
{
this.Ready = true;
}
/// <inheritdoc/>
public void Draw()
{
var service = Service<ReliableFileStorage>.Get();
var dalamud = Service<Dalamud>.Get();
ImGui.InputInt("Num bytes", ref this.numBytes);
ImGui.InputInt("Reps", ref this.reps);
var path = Path.Combine(dalamud.StartInfo.WorkingDirectory!, "test.bin");
if (ImGui.Button("Write"))
{
Log.Information("=== WRITING ===");
var data = new byte[this.numBytes];
var stopwatch = new Stopwatch();
var acc = 0L;
for (var i = 0; i < this.reps; i++)
{
stopwatch.Restart();
service.WriteAllBytes(path, data);
stopwatch.Stop();
acc += stopwatch.ElapsedMilliseconds;
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
}
Log.Information("Took {Ms}ms in total", acc);
}
if (ImGui.Button("Read"))
{
Log.Information("=== READING ===");
var stopwatch = new Stopwatch();
var acc = 0L;
for (var i = 0; i < this.reps; i++)
{
stopwatch.Restart();
service.ReadAllBytes(path);
stopwatch.Stop();
acc += stopwatch.ElapsedMilliseconds;
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
}
Log.Information("Took {Ms}ms in total", acc);
}
if (ImGui.Button("Test Config"))
{
var config = Service<DalamudConfiguration>.Get();
Log.Information("=== READING ===");
var stopwatch = new Stopwatch();
var acc = 0L;
for (var i = 0; i < this.reps; i++)
{
stopwatch.Restart();
config.ForceSave();
stopwatch.Stop();
acc += stopwatch.ElapsedMilliseconds;
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
}
Log.Information("Took {Ms}ms in total", acc);
}
}
}

View file

@ -223,10 +223,11 @@ internal class PluginInstallerWindow : Window, IDisposable
IsThirdParty = 1 << 0,
HasTrouble = 1 << 1,
UpdateAvailable = 1 << 2,
IsNew = 1 << 3,
IsInstallableOutdated = 1 << 4,
IsOrphan = 1 << 5,
IsTesting = 1 << 6,
MainRepoCrossUpdate = 1 << 3,
IsNew = 1 << 4,
IsInstallableOutdated = 1 << 5,
IsOrphan = 1 << 6,
IsTesting = 1 << 7,
}
private enum InstalledPluginListFilter
@ -282,6 +283,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync();
Service<PluginManager>.Get().ScanDevPlugins();
if (!this.isSearchTextPrefilled) this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical;
@ -754,8 +756,9 @@ internal class PluginInstallerWindow : Window, IDisposable
Service<DalamudInterface>.Get().OpenSettings();
}
// If any dev plugins are installed, allow a shortcut for the /xldev menu item
if (this.hasDevPlugins)
// If any dev plugin locations exist, allow a shortcut for the /xldev menu item
var hasDevPluginLocations = configuration.DevPluginLoadLocations.Count > 0;
if (hasDevPluginLocations)
{
ImGui.SameLine();
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
@ -2194,7 +2197,7 @@ internal class PluginInstallerWindow : Window, IDisposable
bodyText += " ";
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
bodyText += "\n" + Locs.PluginBody_Outdated_CanNowUpdate;
else
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
@ -2217,7 +2220,12 @@ internal class PluginInstallerWindow : Window, IDisposable
else if (plugin is { IsDecommissioned: true, IsThirdParty: true })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_NoServiceThird);
ImGui.TextWrapped(
flags.HasFlag(PluginHeaderFlags.MainRepoCrossUpdate)
? Locs.PluginBody_NoServiceThirdCrossUpdate
: Locs.PluginBody_NoServiceThird);
ImGui.PopStyleColor();
}
else if (plugin != null && !plugin.CheckPolicy())
@ -2444,7 +2452,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(3);
}
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback)
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback && !isOutdated)
{
ImGui.SameLine();
this.DrawSendFeedbackButton(manifest, false, true);
@ -2602,7 +2610,10 @@ internal class PluginInstallerWindow : Window, IDisposable
availablePluginUpdate = null;
// Update available
if (availablePluginUpdate != default)
var isMainRepoCrossUpdate = availablePluginUpdate != null &&
availablePluginUpdate.UpdateManifest.RepoUrl != plugin.Manifest.RepoUrl &&
availablePluginUpdate.UpdateManifest.RepoUrl == PluginRepository.MainRepoUrl;
if (availablePluginUpdate != null)
{
label += Locs.PluginTitleMod_HasUpdate;
}
@ -2612,7 +2623,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (this.updatedPlugins != null && !plugin.IsDev)
{
var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName);
if (update != default)
if (update != null)
{
if (update.Status == PluginUpdateStatus.StatusKind.Success)
{
@ -2640,8 +2651,8 @@ internal class PluginInstallerWindow : Window, IDisposable
trouble = true;
}
// Orphaned
if (plugin.IsOrphaned)
// Orphaned, if we don't have a cross-repo update
if (plugin.IsOrphaned && !isMainRepoCrossUpdate)
{
label += Locs.PluginTitleMod_OrphanedError;
trouble = true;
@ -2670,7 +2681,7 @@ internal class PluginInstallerWindow : Window, IDisposable
string? availableChangelog = null;
var didDrawAvailableChangelogInsideCollapsible = false;
if (availablePluginUpdate != default)
if (availablePluginUpdate != null)
{
availablePluginUpdateVersion =
availablePluginUpdate.UseTesting ?
@ -2688,8 +2699,10 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsThirdParty;
if (trouble)
flags |= PluginHeaderFlags.HasTrouble;
if (availablePluginUpdate != default)
if (availablePluginUpdate != null)
flags |= PluginHeaderFlags.UpdateAvailable;
if (isMainRepoCrossUpdate)
flags |= PluginHeaderFlags.MainRepoCrossUpdate;
if (plugin.IsOrphaned)
flags |= PluginHeaderFlags.IsOrphan;
if (plugin.IsTesting)
@ -2724,8 +2737,8 @@ internal class PluginInstallerWindow : Window, IDisposable
var canFeedback = !isThirdParty &&
!plugin.IsDev &&
!plugin.IsOrphaned &&
(plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel
|| plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel) &&
(plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel ||
(plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel && hasTestingAvailable)) &&
acceptsFeedback &&
availablePluginUpdate == default;
@ -2762,13 +2775,14 @@ internal class PluginInstallerWindow : Window, IDisposable
var commands = commandManager.Commands
.Where(cInfo =>
cInfo.Value is { ShowInHelp: true } &&
commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName)
.ToArray();
commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName);
if (commands.Any())
{
ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f));
foreach (var command in commands)
foreach (var command in commands
.OrderBy(cInfo => cInfo.Value.DisplayOrder)
.ThenBy(cInfo => cInfo.Key))
{
ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}");
}
@ -4055,6 +4069,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginBody_NoServiceThird => Loc.Localize("InstallerNoServiceThirdPluginBody", "This plugin is no longer being serviced by its source repo. You may have to look for an updated version in another repo.");
public static string PluginBody_NoServiceThirdCrossUpdate => Loc.Localize("InstallerNoServiceThirdCrossUpdatePluginBody", "This plugin is no longer being serviced by its source repo. An update is available and will update it to a version from the official repository.");
public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information.");
public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available.");

View file

@ -625,13 +625,13 @@ internal class ProfileManagerWidget
Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable =>
Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(ProfileCommandHandler.CommandEnable);
Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(PluginManagementCommandHandler.CommandEnableProfile);
public static string TutorialCommandsDisable =>
Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(ProfileCommandHandler.CommandDisable);
Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(PluginManagementCommandHandler.CommandDisableProfile);
public static string TutorialCommandsToggle =>
Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(ProfileCommandHandler.CommandToggle);
Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(PluginManagementCommandHandler.CommandToggleProfile);
public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");

View file

@ -23,10 +23,11 @@ public class SettingsTabAutoUpdates : SettingsTab
{
private AutoUpdateBehavior behavior;
private bool checkPeriodically;
private bool chatNotification;
private string pickerSearch = string.Empty;
private List<AutoUpdatePreference> autoUpdatePreferences = [];
public override SettingsEntry[] Entries { get; } = Array.Empty<SettingsEntry>();
public override SettingsEntry[] Entries { get; } = [];
public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates");
@ -65,6 +66,7 @@ public class SettingsTabAutoUpdates : SettingsTab
ImGuiHelpers.ScaledDummy(8);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
"Plugins won't update automatically after startup, you will only receive a notification while you are not actively playing."));
@ -120,9 +122,10 @@ public class SettingsTabAutoUpdates : SettingsTab
if (pmPlugin.IsDev)
{
ImGui.SetCursorPos(cursorBeforeIcon);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f);
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.7f))
{
ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
ImGui.PopStyleVar();
}
}
ImGui.SameLine();
@ -166,9 +169,9 @@ public class SettingsTabAutoUpdates : SettingsTab
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250);
if (ImGui.BeginCombo(
$"###autoUpdateBehavior{preference.WorkingPluginId}",
OptKindToString(preference.Kind)))
using (var combo = ImRaii.Combo($"###autoUpdateBehavior{preference.WorkingPluginId}", OptKindToString(preference.Kind)))
{
if (combo.Success)
{
foreach (var kind in Enum.GetValues<AutoUpdatePreference.OptKind>())
{
@ -177,8 +180,7 @@ public class SettingsTabAutoUpdates : SettingsTab
preference.Kind = kind;
}
}
ImGui.EndCombo();
}
}
ImGui.SameLine();
@ -235,6 +237,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.chatNotification = configuration.SendUpdateNotificationToChat;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
@ -246,6 +249,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior;
configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;

View file

@ -39,18 +39,6 @@ public class SettingsTabExperimental : SettingsTab
new GapSettingsEntry(5),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
new GapSettingsEntry(5),
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
Loc.Localize(
@ -66,6 +54,26 @@ public class SettingsTabExperimental : SettingsTab
new DevPluginsSettingsEntry(),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnableImGuiAsserts",
"Enable ImGui asserts"),
Loc.Localize(
"DalamudSettingEnableImGuiAssertsHint",
"If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."),
c => Service<InterfaceManager>.Get().ShowAsserts,
(v, _) => Service<InterfaceManager>.Get().ShowAsserts = v),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnableImGuiAssertsAtStartup",
"Always enable ImGui asserts at startup"),
Loc.Localize(
"DalamudSettingEnableImGuiAssertsAtStartupHint",
"This will enable ImGui asserts every time the game starts."),
c => c.ImGuiAssertsEnabledAtStartup ?? false,
(v, c) => c.ImGuiAssertsEnabledAtStartup = v),
new GapSettingsEntry(5, true),
new ThirdRepoSettingsEntry(),

View file

@ -108,6 +108,16 @@ public class SettingsTabLook : SettingsTab
c => c.IsDocking,
(v, c) => c.IsDocking = v),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"),
Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."),

View file

@ -196,8 +196,17 @@ public class DevPluginsSettingsEntry : SettingsEntry
}
}
public override void PostDraw()
{
this.fileDialogManager.Draw();
}
private static bool ValidDevPluginPath(string path)
=> Path.IsPathRooted(path) && Path.GetExtension(path) == ".dll";
private void AddDevPlugin()
{
this.devPluginTempLocation = this.devPluginTempLocation.Trim('"');
if (this.devPluginLocations.Any(
r => string.Equals(r.Path, this.devPluginTempLocation, StringComparison.InvariantCultureIgnoreCase)))
{
@ -210,25 +219,21 @@ public class DevPluginsSettingsEntry : SettingsEntry
"DalamudDevPluginInvalid",
"The entered value is not a valid path to a potential Dev Plugin.\nDid you mean to enter it as a custom plugin repository in the fields below instead?");
Task.Delay(5000).ContinueWith(t => this.devPluginLocationAddError = string.Empty);
return;
}
else
{
this.devPluginLocations.Add(
new DevPluginLocationSettings
{
Path = this.devPluginTempLocation.Replace("\"", string.Empty),
Path = this.devPluginTempLocation,
IsEnabled = true,
});
this.devPluginLocationsChanged = true;
this.devPluginTempLocation = string.Empty;
}
}
public override void PostDraw()
{
this.fileDialogManager.Draw();
// Enable ImGui asserts if a dev plugin is added, if no choice was made prior
Service<DalamudConfiguration>.Get().ImGuiAssertsEnabledAtStartup ??= true;
}
private static bool ValidDevPluginPath(string path)
=> Path.IsPathRooted(path) && Path.GetExtension(path) == ".dll";
}

View file

@ -251,7 +251,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.fadeOutEasing.Update();
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value))
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)Math.Max(this.fadeOutEasing.Value, 0)))
{
var i = 0;
foreach (var entry in entries)
@ -392,21 +392,19 @@ internal class TitleScreenMenuWindow : Window, IDisposable
if (overrideAlpha)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)Math.Min(logoEasing.Value, 1) : 0f);
}
// Drop shadow
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF000000))
{
for (int i = 0, to = (int)Math.Ceiling(1 * scale); i < to; i++)
{
ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i));
ImGui.Text(entry.Name);
}
}
ImGui.SetCursorPos(cursor);
ImGui.Text(entry.Name);
ImGuiHelpers.SeStringWrapped(
ReadOnlySeString.FromText(entry.Name),
new()
{
FontSize = TargetFontSizePx * ImGui.GetIO().FontGlobalScale,
Edge = true,
Shadow = true,
});
if (overrideAlpha)
{

View file

@ -11,7 +11,6 @@ namespace Dalamud.Interface.Textures;
/// <summary>A texture with a backing instance of <see cref="IDalamudTextureWrap"/> that is shared across multiple
/// requesters.</summary>
/// <remarks>
/// <para>Calling <see cref="IDisposable.Dispose"/> on this interface is a no-op.</para>
/// <para><see cref="GetWrapOrEmpty"/> and <see cref="TryGetWrap"/> may stop returning the intended texture at any point.
/// Use <see cref="RentAsync"/> to lock the texture for use in any thread for any duration.</para>
/// </remarks>

View file

@ -9,7 +9,6 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin.Internal.Types;
@ -713,8 +712,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
ImGui.End();
}
var snapshot = this.Draw is null ? null : ImGuiManagedAsserts.GetSnapshot();
try
{
this.Draw?.InvokeSafely();
@ -728,10 +725,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
this.hasErrorWindow = true;
}
// Only if Draw was successful
if (this.Draw is not null && snapshot is not null)
ImGuiManagedAsserts.ReportProblems(this.namespaceName, snapshot);
this.FrameCount++;
if (DoStats)

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Dalamud.Interface.Windowing.Persistence;
/// <summary>
/// Class representing a Window System preset.
/// </summary>
internal class PresetModel
{
/// <summary>
/// Gets or sets the ID of this preset.
/// </summary>
[JsonProperty("id")]
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the name of this preset.
/// </summary>
[JsonProperty("n")]
public string Name { get; set; } = "New Preset";
/// <summary>
/// Gets or sets a dictionary containing the windows in the preset, mapping their ID to the preset.
/// </summary>
[JsonProperty("w")]
public Dictionary<uint, PresetWindow> Windows { get; set; } = new();
/// <summary>
/// Class representing a window in a preset.
/// </summary>
internal class PresetWindow
{
/// <summary>
/// Gets or sets a value indicating whether the window is pinned.
/// </summary>
[JsonProperty("p")]
public bool IsPinned { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the window is clickthrough.
/// </summary>
[JsonProperty("ct")]
public bool IsClickThrough { get; set; }
/// <summary>
/// Gets or sets the window's opacity override.
/// </summary>
[JsonProperty("a")]
public float? Alpha { get; set; }
/// <summary>
/// Gets a value indicating whether this preset is in the default state.
/// </summary>
public bool IsDefault =>
!this.IsPinned &&
!this.IsClickThrough &&
!this.Alpha.HasValue;
}
}

View file

@ -0,0 +1,57 @@
using Dalamud.Configuration.Internal;
namespace Dalamud.Interface.Windowing.Persistence;
/// <summary>
/// Class handling persistence for window system windows.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class WindowSystemPersistence : IServiceType
{
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="WindowSystemPersistence"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
public WindowSystemPersistence()
{
}
/// <summary>
/// Gets the active window system preset.
/// </summary>
public PresetModel ActivePreset => this.config.DefaultUiPreset;
/// <summary>
/// Get or add a window to the active preset.
/// </summary>
/// <param name="id">The ID of the window.</param>
/// <returns>The preset window instance, or null if the preset does not contain this window.</returns>
public PresetModel.PresetWindow? GetWindow(uint id)
{
return this.ActivePreset.Windows.TryGetValue(id, out var window) ? window : null;
}
/// <summary>
/// Persist the state of a window to the active preset.
/// </summary>
/// <param name="id">The ID of the window.</param>
/// <param name="window">The preset window instance.</param>
public void SaveWindow(uint id, PresetModel.PresetWindow window)
{
// If the window is in the default state, don't save it to avoid saving every possible window
// if the user has not customized anything.
if (window.IsDefault)
{
this.ActivePreset.Windows.Remove(id);
}
else
{
this.ActivePreset.Windows[id] = window;
}
this.config.QueueSave();
}
}

View file

@ -1,19 +1,24 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
using PInvoke;
namespace Dalamud.Interface.Windowing;
@ -35,15 +40,19 @@ public abstract class Window
private float? internalAlpha = null;
private bool nextFrameBringToFront = false;
private bool hasInitializedFromPreset = false;
private PresetModel.PresetWindow? presetWindow;
private bool presetDirty = false;
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
/// <param name="name">The name/ID of this window.
/// If you have multiple windows with the same name, you will need to
/// append an unique ID to it by specifying it after "###" behind the window title.
/// append a unique ID to it by specifying it after "###" behind the window title.
/// </param>
/// <param name="flags">The <see cref="ImGuiWindowFlags"/> of this window.</param>
/// <param name="forceMainWindow">Whether or not this window should be limited to the main game window.</param>
/// <param name="forceMainWindow">Whether this window should be limited to the main game window.</param>
protected Window(string name, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
{
this.WindowName = name;
@ -51,6 +60,33 @@ public abstract class Window
this.ForceMainWindow = forceMainWindow;
}
/// <summary>
/// Flags to control window behavior.
/// </summary>
[Flags]
internal enum WindowDrawFlags
{
/// <summary>
/// Nothing.
/// </summary>
None = 0,
/// <summary>
/// Enable window opening/closing sound effects.
/// </summary>
UseSoundEffects = 1 << 0,
/// <summary>
/// Hook into the game's focus management.
/// </summary>
UseFocusManagement = 1 << 1,
/// <summary>
/// Enable the built-in "additional options" menu on the title bar.
/// </summary>
UseAdditionalOptions = 1 << 2,
}
/// <summary>
/// Gets or sets the namespace of the window.
/// </summary>
@ -271,13 +307,12 @@ public abstract class Window
/// <summary>
/// Draw the window via ImGui.
/// </summary>
/// <param name="configuration">Configuration instance used to check if certain window management features should be enabled.</param>
internal void DrawInternal(DalamudConfiguration? configuration)
/// <param name="internalDrawFlags">Flags controlling window behavior.</param>
/// <param name="persistence">Handler for window persistence data.</param>
internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence)
{
this.PreOpenCheck();
var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
@ -287,7 +322,8 @@ public abstract class Window
this.IsFocused = false;
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
}
return;
@ -302,12 +338,15 @@ public abstract class Window
if (hasNamespace)
ImGui.PushID(this.Namespace);
this.PreHandlePreset(persistence);
if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen)
{
this.internalLastIsOpen = this.internalIsOpen;
this.OnOpen();
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
this.PreDraw();
@ -340,6 +379,18 @@ public abstract class Window
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{
ImGuiNativeAdditions.igCustom_WindowSetInheritNoInputs(this.internalIsClickthrough);
// Not supported yet on non-main viewports
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
this.internalAlpha = null;
this.internalIsPinned = false;
this.internalIsClickthrough = false;
this.presetDirty = true;
}
// Draw the actual window contents
try
{
@ -355,7 +406,7 @@ public abstract class Window
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
(configuration?.EnablePluginUiAdditionalOptions ?? true) &&
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
flagsApplicableForTitleBarIcons;
if (showAdditions)
{
@ -375,36 +426,51 @@ public abstract class Window
{
var showAsPinned = this.internalIsPinned || this.internalIsClickthrough;
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
{
this.internalIsPinned = showAsPinned;
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionPinHint", "Pinned windows will not move or resize when you click and drag them, nor will they close when escape is pressed."));
}
if (this.internalIsClickthrough)
ImGui.EndDisabled();
if (this.AllowClickthrough)
ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), ref this.internalIsClickthrough);
{
if (ImGui.Checkbox(
Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"),
ref this.internalIsClickthrough))
{
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
{
this.internalAlpha = alpha / 100f;
this.presetDirty = true;
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
{
this.internalAlpha = null;
this.presetDirty = true;
}
if (isAvailable)
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again to disable clickthrough."));
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionDisclaimer",
"These options may not work for all plugins at the moment."));
"Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
@ -435,6 +501,7 @@ public abstract class Window
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName);
},
Priority = int.MinValue,
@ -457,8 +524,7 @@ public abstract class Window
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var isAllowed = configuration?.IsFocusManagementEnabled ?? false;
if (isAllowed)
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement) && !this.internalIsPinned)
{
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
@ -476,6 +542,8 @@ public abstract class Window
this.PostDraw();
this.PostHandlePreset(persistence);
if (hasNamespace)
ImGui.PopID();
}
@ -519,6 +587,50 @@ public abstract class Window
}
}
private void PreHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null || this.hasInitializedFromPreset)
return;
var id = ImGui.GetID(this.WindowName);
this.presetWindow = persistence.GetWindow(id);
this.hasInitializedFromPreset = true;
// Fresh preset - don't apply anything
if (this.presetWindow == null)
{
this.presetWindow = new PresetModel.PresetWindow();
this.presetDirty = true;
return;
}
this.internalIsPinned = this.presetWindow.IsPinned;
this.internalIsClickthrough = this.presetWindow.IsClickThrough;
this.internalAlpha = this.presetWindow.Alpha;
}
private void PostHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null)
return;
Debug.Assert(this.presetWindow != null, "this.presetWindow != null");
if (this.presetDirty)
{
this.presetWindow.IsPinned = this.internalIsPinned;
this.presetWindow.IsClickThrough = this.internalIsClickthrough;
this.presetWindow.Alpha = this.internalAlpha;
var id = ImGui.GetID(this.WindowName);
persistence.SaveWindow(id, this.presetWindow!);
this.presetDirty = false;
Log.Verbose("Saved preset for {WindowName}", this.WindowName);
}
}
private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable<TitleBarButton> buttons)
{
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
@ -715,5 +827,8 @@ public abstract class Window
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void igCustom_WindowSetInheritNoInputs(bool inherit);
}
}

View file

@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Windowing.Persistence;
using ImGuiNET;
using Serilog;
@ -104,7 +104,20 @@ public class WindowSystem
if (hasNamespace)
ImGui.PushID(this.Namespace);
// These must be nullable, people are using stock WindowSystems and Windows without Dalamud for tests
var config = Service<DalamudConfiguration>.GetNullable();
var persistence = Service<WindowSystemPersistence>.GetNullable();
var flags = Window.WindowDrawFlags.None;
if (config?.EnablePluginUISoundEffects ?? false)
flags |= Window.WindowDrawFlags.UseSoundEffects;
if (config?.EnablePluginUiAdditionalOptions ?? false)
flags |= Window.WindowDrawFlags.UseAdditionalOptions;
if (config?.IsFocusManagementEnabled ?? false)
flags |= Window.WindowDrawFlags.UseFocusManagement;
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
foreach (var window in this.windows.ToArray())
@ -112,12 +125,7 @@ public class WindowSystem
#if DEBUG
// Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}");
#endif
var snapshot = ImGuiManagedAsserts.GetSnapshot();
window.DrawInternal(config);
var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName;
ImGuiManagedAsserts.ReportProblems(source, snapshot);
window.DrawInternal(flags, persistence);
}
var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey);

View file

@ -1,5 +1,6 @@
using Serilog;
using Serilog.Core;
using Serilog.Core.Enrichers;
using Serilog.Events;
namespace Dalamud.Logging.Internal;
@ -27,6 +28,21 @@ public class ModuleLog
this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName);
}
/// <summary>
/// Initializes a new instance of the <see cref="ModuleLog"/> class.
/// This class will properly attach SourceContext and other attributes per Serilog standards.
/// </summary>
/// <param name="type">The type of the class this logger is for.</param>
public ModuleLog(Type type)
{
this.moduleName = type.Name;
this.moduleLogger = Log.ForContext(
[
new PropertyEnricher(Constants.SourceContextPropertyName, type.FullName),
new PropertyEnricher("Dalamud.ModuleName", this.moduleName)
]);
}
/// <summary>
/// Log a templated verbose message to the in-game debug log.
/// </summary>
@ -160,4 +176,11 @@ public class ModuleLog
messageTemplate: $"[{this.moduleName}] {messageTemplate}",
values);
}
/// <summary>
/// Helper method to create a new <see cref="ModuleLog"/> instance based on a type.
/// </summary>
/// <typeparam name="T">The class to create this ModuleLog for.</typeparam>
/// <returns>Returns a ModuleLog with name set.</returns>
internal static ModuleLog Create<T>() => new(typeof(T));
}

View file

@ -1,4 +1,5 @@
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
namespace Dalamud.Plugin;
@ -22,6 +23,47 @@ public interface IExposedPlugin
/// </summary>
bool IsLoaded { get; }
/// <summary>
/// Gets a value indicating whether this plugin's API level is out of date.
/// </summary>
bool IsOutdated { get; }
/// <summary>
/// Gets a value indicating whether the plugin is for testing use only.
/// </summary>
bool IsTesting { get; }
/// <summary>
/// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not.
/// </summary>
bool IsOrphaned { get; }
/// <summary>
/// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does).
/// </summary>
bool IsDecommissioned { get; }
/// <summary>
/// Gets a value indicating whether this plugin has been banned.
/// </summary>
bool IsBanned { get; }
/// <summary>
/// Gets a value indicating whether this plugin is dev plugin.
/// </summary>
bool IsDev { get; }
/// <summary>
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo.
/// </summary>
bool IsThirdParty { get; }
/// <summary>
/// Gets the plugin manifest.
/// </summary>
ILocalPluginManifest Manifest { get; }
/// <summary>
/// Gets the version of the plugin.
/// </summary>
@ -74,6 +116,30 @@ internal sealed class ExposedPlugin(LocalPlugin plugin) : IExposedPlugin
/// <inheritdoc/>
public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasConfigUi ?? false;
/// <inheritdoc/>
public bool IsOutdated => plugin.IsOutdated;
/// <inheritdoc/>
public bool IsTesting => plugin.IsTesting;
/// <inheritdoc/>
public bool IsOrphaned => plugin.IsOrphaned;
/// <inheritdoc/>
public bool IsDecommissioned => plugin.IsDecommissioned;
/// <inheritdoc/>
public bool IsBanned => plugin.IsBanned;
/// <inheritdoc/>
public bool IsDev => plugin.IsDev;
/// <inheritdoc/>
public bool IsThirdParty => plugin.IsThirdParty;
/// <inheritdoc/>
public ILocalPluginManifest Manifest => plugin.Manifest;
/// <inheritdoc/>
public void OpenMainUi()
{

View file

@ -9,6 +9,10 @@ using Dalamud.Console;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.EventArgs;
@ -68,6 +72,8 @@ internal class AutoUpdateManager : IServiceType
private readonly IConsoleVariable<bool> isDryRun;
private readonly Task<DalamudLinkPayload> openInstallerWindowLinkTask;
private DateTime? loginTime;
private DateTime? nextUpdateCheckTime;
private DateTime? unblockedSince;
@ -93,6 +99,16 @@ internal class AutoUpdateManager : IServiceType
});
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
this.openInstallerWindowLinkTask =
Service<ChatGui>.GetAsync().ContinueWith(
chatGuiTask => chatGuiTask.Result.AddChatLinkHandler(
"Dalamud",
1001,
(_, _) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
}));
this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false);
console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () =>
{
@ -241,6 +257,7 @@ internal class AutoUpdateManager : IServiceType
Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update");
}
Log.Verbose($"Available Updates: {string.Join(", ", this.pluginManager.UpdatablePlugins.Select(s => s.UpdateManifest.InternalName))}");
var updatable = this.GetAvailablePluginUpdates(
DecideUpdateListingRestriction(behavior));
@ -404,6 +421,34 @@ internal class AutoUpdateManager : IServiceType
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed;
Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime);
};
// Send out a chat message only if the user requested so
if (!this.config.SendUpdateNotificationToChat)
return;
var chatGui = Service<ChatGui>.GetNullable();
if (chatGui == null)
{
Log.Verbose("Unable to get chat gui, discard notification for chat.");
return;
}
chatGui.Print(new XivChatEntry
{
Message = new SeString(new List<Payload>
{
new TextPayload(Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count)),
new TextPayload(" ["),
new UIForegroundPayload(500),
this.openInstallerWindowLinkTask.Result,
new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
Type = XivChatType.Urgent,
});
}
private List<AvailablePluginUpdate> GetAvailablePluginUpdates(UpdateListingRestriction restriction)

View file

@ -0,0 +1,16 @@
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// An exception to be thrown when policy blocks a plugin from loading.
/// </summary>
internal class InternalPluginStateException : InvalidPluginOperationException
{
/// <summary>
/// Initializes a new instance of the <see cref="InternalPluginStateException"/> class.
/// </summary>
/// <param name="message">The message to associate with this exception.</param>
public InternalPluginStateException(string message)
: base(message)
{
}
}

View file

@ -48,7 +48,7 @@ internal class PluginManager : IInternalDisposableService
/// </summary>
public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable
private static readonly ModuleLog Log = new("PLUGINM");
private static readonly ModuleLog Log = ModuleLog.Create<PluginManager>();
private readonly object pluginListLock = new();
private readonly DirectoryInfo pluginDirectory;
@ -134,9 +134,6 @@ internal class PluginManager : IInternalDisposableService
this.configuration.PluginTestingOptIns ??= new();
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
// NET8 CHORE
// this.ApplyPatches();
registerStartupBlocker(
Task.Run(this.LoadAndStartLoadSyncPlugins),
"Waiting for plugins that asked to be loaded before the game.");
@ -246,9 +243,9 @@ internal class PluginManager : IInternalDisposableService
public bool ReposReady { get; private set; }
/// <summary>
/// Gets a value indicating whether the plugin manager started in safe mode.
/// Gets or sets a value indicating whether the plugin manager started in safe mode.
/// </summary>
public bool SafeMode { get; init; }
public bool SafeMode { get; set; }
/// <summary>
/// Gets the <see cref="PluginConfigurations"/> object used when initializing plugins.
@ -433,10 +430,6 @@ internal class PluginManager : IInternalDisposableService
await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask()))
.SuppressException();
}
// NET8 CHORE
// this.assemblyLocationMonoHook?.Dispose();
// this.assemblyCodeBaseMonoHook?.Dispose();
}
/// <summary>
@ -876,9 +869,6 @@ internal class PluginManager : IInternalDisposableService
this.installedPluginsList.Remove(plugin);
}
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
this.NotifyinstalledPluginsListChanged();
this.NotifyAvailablePluginsChanged();
}
@ -1572,6 +1562,10 @@ internal class PluginManager : IInternalDisposableService
{
Log.Information($"Loading dev plugin {name}");
plugin = new LocalDevPlugin(dllFile, manifest);
// This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet
// TODO(goat): Re-enable this when we have better tracing for what was rendering when
// this.configuration.ImGuiAssertsEnabledAtStartup ??= true;
}
else
{
@ -1674,8 +1668,6 @@ internal class PluginManager : IInternalDisposableService
}
catch (InvalidPluginException)
{
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
catch (BannedPluginException)
@ -1721,8 +1713,6 @@ internal class PluginManager : IInternalDisposableService
}
else
{
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
}
@ -1931,114 +1921,3 @@ internal class PluginManager : IInternalDisposableService
public static string DalamudPluginUpdateFailed(string name, Version version, string why) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed ({2}).").Format(name, version, why);
}
}
// NET8 CHORE
/*
/// <summary>
/// Class responsible for loading and unloading plugins.
/// This contains the assembly patching functionality to resolve assembly locations.
/// </summary>
internal partial class PluginManager
{
/// <summary>
/// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
/// plugins via byte[].
/// </summary>
internal static readonly ConcurrentDictionary<string, PluginPatchData> PluginLocations = new();
private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook;
private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook;
/// <summary>
/// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
/// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
/// It should never be called manually.
/// </summary>
/// <param name="orig">A delegate that acts as the original method.</param>
/// <param name="self">The equivalent of `this`.</param>
/// <returns>The plugin location, or the result from the original method.</returns>
private static string AssemblyLocationPatch(Func<Assembly, string?> orig, Assembly self)
{
var result = orig(self);
if (string.IsNullOrEmpty(result))
{
foreach (var assemblyName in GetStackFrameAssemblyNames())
{
if (PluginLocations.TryGetValue(assemblyName, out var data))
{
result = data.Location;
break;
}
}
}
result ??= string.Empty;
Log.Verbose($"Assembly.Location // {self.FullName} // {result}");
return result;
}
/// <summary>
/// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
/// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
/// It should never be called manually.
/// </summary>
/// <param name="orig">A delegate that acts as the original method.</param>
/// <param name="self">The equivalent of `this`.</param>
/// <returns>The plugin code base, or the result from the original method.</returns>
private static string AssemblyCodeBasePatch(Func<Assembly, string?> orig, Assembly self)
{
var result = orig(self);
if (string.IsNullOrEmpty(result))
{
foreach (var assemblyName in GetStackFrameAssemblyNames())
{
if (PluginLocations.TryGetValue(assemblyName, out var data))
{
result = data.CodeBase;
break;
}
}
}
result ??= string.Empty;
Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}");
return result;
}
private static IEnumerable<string> GetStackFrameAssemblyNames()
{
var stackTrace = new StackTrace();
var stackFrames = stackTrace.GetFrames();
foreach (var stackFrame in stackFrames)
{
var methodBase = stackFrame.GetMethod();
if (methodBase == null)
continue;
yield return methodBase.Module.Assembly.FullName!;
}
}
private void ApplyPatches()
{
var targetType = typeof(PluginManager).Assembly.GetType();
var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod();
var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static);
this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch);
#pragma warning disable CS0618
#pragma warning disable SYSLIB0012
var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod();
#pragma warning restore SYSLIB0012
#pragma warning restore CS0618
var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static);
this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
}
}
*/

View file

@ -0,0 +1,388 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Service responsible for profile-related chat commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class PluginManagementCommandHandler : IInternalDisposableService
{
#pragma warning disable SA1600
public const string CommandEnableProfile = "/xlenablecollection";
public const string CommandDisableProfile = "/xldisablecollection";
public const string CommandToggleProfile = "/xltogglecollection";
public const string CommandEnablePlugin = "/xlenableplugin";
public const string CommandDisablePlugin = "/xldisableplugin";
public const string CommandTogglePlugin = "/xltoggleplugin";
#pragma warning restore SA1600
private static readonly string LegacyCommandEnable = CommandEnableProfile.Replace("collection", "profile");
private static readonly string LegacyCommandDisable = CommandDisableProfile.Replace("collection", "profile");
private static readonly string LegacyCommandToggle = CommandToggleProfile.Replace("collection", "profile");
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
private readonly PluginManager pluginManager;
private readonly ChatGui chat;
private readonly Framework framework;
private List<(Target Target, PluginCommandOperation Operation)> commandQueue = new();
/// <summary>
/// Initializes a new instance of the <see cref="PluginManagementCommandHandler"/> class.
/// </summary>
/// <param name="cmd">Command handler.</param>
/// <param name="profileManager">Profile manager.</param>
/// <param name="pluginManager">Plugin manager.</param>
/// <param name="chat">Chat handler.</param>
/// <param name="framework">Framework.</param>
[ServiceManager.ServiceConstructor]
public PluginManagementCommandHandler(
CommandManager cmd,
ProfileManager profileManager,
PluginManager pluginManager,
ChatGui chat,
Framework framework)
{
this.cmd = cmd;
this.profileManager = profileManager;
this.pluginManager = pluginManager;
this.chat = chat;
this.framework = framework;
this.cmd.AddHandler(CommandEnableProfile, new CommandInfo(this.OnEnableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandDisableProfile, new CommandInfo(this.OnDisableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandToggleProfile, new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
{
ShowInHelp = false,
});
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
{
ShowInHelp = false,
});
this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
{
ShowInHelp = false,
});
this.cmd.AddHandler(CommandEnablePlugin, new CommandInfo(this.OnEnablePlugin)
{
HelpMessage = Loc.Localize("PluginCommandsEnableHint", "Enable a plugin. Usage: /xlenableplugin \"Plugin Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandDisablePlugin, new CommandInfo(this.OnDisablePlugin)
{
HelpMessage = Loc.Localize("PluginCommandsDisableHint", "Disable a plugin. Usage: /xldisableplugin \"Plugin Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandTogglePlugin, new CommandInfo(this.OnTogglePlugin)
{
HelpMessage = Loc.Localize("PluginCommandsToggleHint", "Toggle a plugin. Usage: /xltoggleplugin \"Plugin Name\""),
ShowInHelp = true,
});
this.framework.Update += this.FrameworkOnUpdate;
}
private enum PluginCommandOperation
{
Enable,
Disable,
Toggle,
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cmd.RemoveHandler(CommandEnableProfile);
this.cmd.RemoveHandler(CommandDisableProfile);
this.cmd.RemoveHandler(CommandToggleProfile);
this.cmd.RemoveHandler(LegacyCommandEnable);
this.cmd.RemoveHandler(LegacyCommandDisable);
this.cmd.RemoveHandler(LegacyCommandToggle);
this.framework.Update += this.FrameworkOnUpdate;
}
private void HandleProfileOperation(string profileName, PluginCommandOperation operation)
{
var profile = this.profileManager.Profiles.FirstOrDefault(
x => x.Name == profileName);
if (profile == null || profile.IsDefaultProfile)
return;
switch (operation)
{
case PluginCommandOperation.Enable:
if (!profile.IsEnabled)
Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult();
break;
case PluginCommandOperation.Disable:
if (profile.IsEnabled)
Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult();
break;
case PluginCommandOperation.Toggle:
Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult();
break;
default:
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
}
this.chat.Print(
profile.IsEnabled
? Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name)
: Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
{
if (!t.IsCompletedSuccessfully && t.Exception != null)
{
Log.Error(t.Exception, "Could not apply profiles through commands");
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
}
else
{
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
}
});
}
private bool HandlePluginOperation(Guid workingPluginId, PluginCommandOperation operation)
{
var plugin = this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.EffectiveWorkingPluginId == workingPluginId);
if (plugin == null)
return true;
switch (plugin.State)
{
// Ignore if the plugin is in a fail state
case PluginState.LoadError or PluginState.UnloadError:
this.chat.Print(Loc.Localize("PluginCommandsFailed", "Plugin \"{0}\" has previously failed to load/unload, not continuing.").Format(plugin.Name));
return true;
case PluginState.Loaded when operation == PluginCommandOperation.Enable:
this.chat.Print(Loc.Localize("PluginCommandsAlreadyEnabled", "Plugin \"{0}\" is already enabled.").Format(plugin.Name));
return true;
case PluginState.Unloaded when operation == PluginCommandOperation.Disable:
this.chat.Print(Loc.Localize("PluginCommandsAlreadyDisabled", "Plugin \"{0}\" is already disabled.").Format(plugin.Name));
return true;
// Defer if this plugin is busy right now
case PluginState.Loading or PluginState.Unloading:
return false;
}
void Continuation(Task t, string onSuccess, string onError)
{
if (!t.IsCompletedSuccessfully && t.Exception != null)
{
Log.Error(t.Exception, "Plugin command operation failed for plugin {PluginName}", plugin.Name);
this.chat.PrintError(onError);
return;
}
this.chat.Print(onSuccess);
}
switch (operation)
{
case PluginCommandOperation.Enable:
this.chat.Print(Loc.Localize("PluginCommandsEnabling", "Enabling plugin \"{0}\"...").Format(plugin.Name));
Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(t => Continuation(t,
Loc.Localize("PluginCommandsEnableSuccess", "Plugin \"{0}\" enabled.").Format(plugin.Name),
Loc.Localize("PluginCommandsEnableFailed", "Failed to enable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
.ConfigureAwait(false);
break;
case PluginCommandOperation.Disable:
this.chat.Print(Loc.Localize("PluginCommandsDisabling", "Disabling plugin \"{0}\"...").Format(plugin.Name));
Task.Run(() => plugin.UnloadAsync())
.ContinueWith(t => Continuation(t,
Loc.Localize("PluginCommandsDisableSuccess", "Plugin \"{0}\" disabled.").Format(plugin.Name),
Loc.Localize("PluginCommandsDisableFailed", "Failed to disable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
.ConfigureAwait(false);
break;
case PluginCommandOperation.Toggle:
this.chat.Print(Loc.Localize("PluginCommandsToggling", "Toggling plugin \"{0}\"...").Format(plugin.Name));
Task.Run(() => plugin.State == PluginState.Loaded ? plugin.UnloadAsync() : plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(t => Continuation(t,
Loc.Localize("PluginCommandsToggleSuccess", "Plugin \"{0}\" toggled.").Format(plugin.Name),
Loc.Localize("PluginCommandsToggleFailed", "Failed to toggle plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
.ConfigureAwait(false);
break;
default:
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
}
return true;
}
private void FrameworkOnUpdate(IFramework framework1)
{
if (this.profileManager.IsBusy)
{
return;
}
if (this.commandQueue.Count > 0)
{
var op = this.commandQueue[0];
var remove = true;
switch (op.Target)
{
case PluginTarget pluginTarget:
remove = this.HandlePluginOperation(pluginTarget.WorkingPluginId, op.Operation);
break;
case ProfileTarget profileTarget:
this.HandleProfileOperation(profileTarget.ProfileName, op.Operation);
break;
}
if (remove)
{
this.commandQueue.RemoveAt(0);
}
}
}
private void OnEnableProfile(string command, string arguments)
{
var name = this.ValidateProfileName(arguments);
if (name == null)
return;
var target = new ProfileTarget(name);
this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList();
this.commandQueue.Add((target, PluginCommandOperation.Enable));
}
private void OnDisableProfile(string command, string arguments)
{
var name = this.ValidateProfileName(arguments);
if (name == null)
return;
var target = new ProfileTarget(name);
this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList();
this.commandQueue.Add((target, PluginCommandOperation.Disable));
}
private void OnToggleProfile(string command, string arguments)
{
var name = this.ValidateProfileName(arguments);
if (name == null)
return;
var target = new ProfileTarget(name);
this.commandQueue.Add((target, PluginCommandOperation.Toggle));
}
private void OnEnablePlugin(string command, string arguments)
{
var plugin = this.ValidatePluginName(arguments);
if (plugin == null)
return;
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
this.commandQueue
.RemoveAll(x => x.Target == target);
this.commandQueue.Add((target, PluginCommandOperation.Enable));
}
private void OnDisablePlugin(string command, string arguments)
{
var plugin = this.ValidatePluginName(arguments);
if (plugin == null)
return;
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
this.commandQueue
.RemoveAll(x => x.Target == target);
this.commandQueue.Add((target, PluginCommandOperation.Disable));
}
private void OnTogglePlugin(string command, string arguments)
{
var plugin = this.ValidatePluginName(arguments);
if (plugin == null)
return;
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
this.commandQueue
.RemoveAll(x => x.Target == target);
this.commandQueue.Add((target, PluginCommandOperation.Toggle));
}
private string? ValidateProfileName(string arguments)
{
var name = arguments.Replace("\"", string.Empty);
if (this.profileManager.Profiles.All(x => x.Name != name))
{
this.chat.PrintError(Loc.Localize("ProfileCommandsNotFound", "Collection \"{0}\" not found.").Format(name));
return null;
}
return name;
}
private LocalPlugin? ValidatePluginName(string arguments)
{
var name = arguments.Replace("\"", string.Empty);
var targetPlugin =
this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.InternalName == name || x.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase));
if (targetPlugin == null)
{
this.chat.PrintError(Loc.Localize("PluginCommandsNotFound", "Plugin \"{0}\" not found.").Format(name));
return null;
}
if (!this.profileManager.IsInDefaultProfile(targetPlugin.EffectiveWorkingPluginId))
{
this.chat.PrintError(Loc.Localize("PluginCommandsNotInDefaultProfile", "Plugin \"{0}\" is in a collection and can't be managed through commands. Manage the collection instead.")
.Format(targetPlugin.Name));
}
return targetPlugin;
}
private abstract record Target;
private record PluginTarget(Guid WorkingPluginId) : Target;
private record ProfileTarget(string ProfileName) : Target;
}

View file

@ -1,204 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Service responsible for profile-related chat commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IInternalDisposableService
{
#pragma warning disable SA1600
public const string CommandEnable = "/xlenablecollection";
public const string CommandDisable = "/xldisablecollection";
public const string CommandToggle = "/xltogglecollection";
#pragma warning restore SA1600
private static readonly string LegacyCommandEnable = CommandEnable.Replace("collection", "profile");
private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile");
private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile");
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
private readonly ChatGui chat;
private readonly Framework framework;
private List<(string, ProfileOp)> queue = new();
/// <summary>
/// Initializes a new instance of the <see cref="ProfileCommandHandler"/> class.
/// </summary>
/// <param name="cmd">Command handler.</param>
/// <param name="profileManager">Profile manager.</param>
/// <param name="chat">Chat handler.</param>
/// <param name="framework">Framework.</param>
[ServiceManager.ServiceConstructor]
public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
{
this.cmd = cmd;
this.profileManager = profileManager;
this.chat = chat;
this.framework = framework;
this.cmd.AddHandler(CommandEnable, new CommandInfo(this.OnEnableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandDisable, new CommandInfo(this.OnDisableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
{
ShowInHelp = false,
});
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
{
ShowInHelp = true,
});
this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
{
ShowInHelp = true,
});
this.framework.Update += this.FrameworkOnUpdate;
}
private enum ProfileOp
{
Enable,
Disable,
Toggle,
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cmd.RemoveHandler(CommandEnable);
this.cmd.RemoveHandler(CommandDisable);
this.cmd.RemoveHandler(CommandToggle);
this.cmd.RemoveHandler(LegacyCommandEnable);
this.cmd.RemoveHandler(LegacyCommandDisable);
this.cmd.RemoveHandler(LegacyCommandToggle);
this.framework.Update += this.FrameworkOnUpdate;
}
private void FrameworkOnUpdate(IFramework framework1)
{
if (this.profileManager.IsBusy)
return;
if (this.queue.Count > 0)
{
var op = this.queue[0];
this.queue.RemoveAt(0);
var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1);
if (profile == null || profile.IsDefaultProfile)
return;
switch (op.Item2)
{
case ProfileOp.Enable:
if (!profile.IsEnabled)
Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult();
break;
case ProfileOp.Disable:
if (profile.IsEnabled)
Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult();
break;
case ProfileOp.Toggle:
Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult();
break;
default:
throw new ArgumentOutOfRangeException();
}
if (profile.IsEnabled)
{
this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name));
}
else
{
this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
}
Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
{
if (!t.IsCompletedSuccessfully && t.Exception != null)
{
Log.Error(t.Exception, "Could not apply profiles through commands");
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
}
else
{
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
}
});
}
}
private void OnEnableProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
this.queue.Add((name, ProfileOp.Enable));
}
private void OnDisableProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
this.queue.Add((name, ProfileOp.Disable));
}
private void OnToggleProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue.Add((name, ProfileOp.Toggle));
}
private string? ValidateName(string arguments)
{
var name = arguments.Replace("\"", string.Empty);
if (this.profileManager.Profiles.All(x => x.Name != name))
{
this.chat.PrintError($"No collection like \"{name}\".");
return null;
}
return name;
}
}

View file

@ -281,7 +281,7 @@ internal class LocalPlugin : IAsyncDisposable
case PluginState.Unloaded:
if (this.instance is not null)
{
throw new InvalidPluginOperationException(
throw new InternalPluginStateException(
"Plugin should have been unloaded but instance is not cleared");
}
@ -382,10 +382,6 @@ internal class LocalPlugin : IAsyncDisposable
}
}
// Update the location for the Location and CodeBase patches
// NET8 CHORE
// PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
this.dalamudInterface = new(this, reason);
this.serviceScope = ioc.GetScope();
@ -413,6 +409,8 @@ internal class LocalPlugin : IAsyncDisposable
}
catch (Exception ex)
{
// These are "user errors", we don't want to mark the plugin as failed
if (ex is not InvalidPluginOperationException)
this.State = PluginState.LoadError;
// If a precondition fails, don't record it as an error, as it isn't really.
@ -476,7 +474,10 @@ internal class LocalPlugin : IAsyncDisposable
}
catch (Exception ex)
{
// These are "user errors", we don't want to mark the plugin as failed
if (ex is not InvalidPluginOperationException)
this.State = PluginState.UnloadError;
Log.Error(ex, "Error while unloading {PluginName}", this.InternalName);
throw;
@ -509,9 +510,6 @@ internal class LocalPlugin : IAsyncDisposable
var startInfo = Service<Dalamud>.Get().StartInfo;
var manager = Service<PluginManager>.Get();
if (startInfo.NoLoadPlugins)
return false;
if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty)
return false;

View file

@ -4,7 +4,7 @@ namespace Dalamud.Utility;
/// Utility class for marking something to be changed for API 11, for ease of lookup.
/// </summary>
[AttributeUsage(AttributeTargets.All, Inherited = false)]
internal sealed class Api11ToDoAttribute : Attribute
internal sealed class Api12ToDoAttribute : Attribute
{
/// <summary>
/// Marks that this should be made internal.
@ -12,11 +12,11 @@ internal sealed class Api11ToDoAttribute : Attribute
public const string MakeInternal = "Make internal.";
/// <summary>
/// Initializes a new instance of the <see cref="Api11ToDoAttribute"/> class.
/// Initializes a new instance of the <see cref="Api12ToDoAttribute"/> class.
/// </summary>
/// <param name="what">The explanation.</param>
/// <param name="what2">The explanation 2.</param>
public Api11ToDoAttribute(string what, string what2 = "")
public Api12ToDoAttribute(string what, string what2 = "")
{
_ = what;
_ = what2;

View file

@ -0,0 +1,52 @@
using System.Globalization;
namespace Dalamud.Utility;
/// <summary>
/// Class containing fixes for culture-specific issues.
/// </summary>
internal static class CultureFixes
{
/// <summary>
/// Apply all fixes.
/// </summary>
public static void Apply()
{
PatchFrenchNumberSeparator();
}
private static void PatchFrenchNumberSeparator()
{
// Reset formatting specifier for the "digit grouping symbol" to an empty string
// for cultures that use a narrow no-break space (U+202F).
// This glyph is not present in any game fonts and not in the range for our Noto
// so it will be rendered as a geta (=) instead. That's a hack, but it works and
// doesn't look as weird.
CultureInfo PatchCulture(CultureInfo info)
{
var newCulture = (CultureInfo)info.Clone();
const string invalidGroupSeparator = "\u202F";
const string replacedGroupSeparator = " ";
if (info.NumberFormat.NumberGroupSeparator == invalidGroupSeparator)
newCulture.NumberFormat.NumberGroupSeparator = replacedGroupSeparator;
if (info.NumberFormat.NumberDecimalSeparator == invalidGroupSeparator)
newCulture.NumberFormat.NumberDecimalSeparator = replacedGroupSeparator;
if (info.NumberFormat.CurrencyGroupSeparator == invalidGroupSeparator)
newCulture.NumberFormat.CurrencyGroupSeparator = replacedGroupSeparator;
if (info.NumberFormat.CurrencyDecimalSeparator == invalidGroupSeparator)
newCulture.NumberFormat.CurrencyDecimalSeparator = replacedGroupSeparator;
return newCulture;
}
CultureInfo.CurrentCulture = PatchCulture(CultureInfo.CurrentCulture);
CultureInfo.CurrentUICulture = PatchCulture(CultureInfo.CurrentUICulture);
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CurrentCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CurrentUICulture;
}
}

View file

@ -0,0 +1,32 @@
using System.Diagnostics;
using System.Linq;
namespace Dalamud.Utility;
/// <summary>
/// A set of utilities for diagnostics.
/// </summary>
public static class DiagnosticUtil
{
private static readonly string[] IgnoredNamespaces = [
nameof(System),
nameof(ImGuiNET.ImGuiNative)
];
/// <summary>
/// Gets a stack trace that filters out irrelevant frames.
/// </summary>
/// <param name="source">The source stacktrace to filter.</param>
/// <returns>Returns a stack trace with "extra" frames removed.</returns>
public static StackTrace GetUsefulTrace(StackTrace source)
{
var frames = source.GetFrames().SkipWhile(
f =>
{
var frameNs = f.GetMethod()?.DeclaringType?.Namespace;
return frameNs == null || IgnoredNamespaces.Any(i => frameNs.StartsWith(i, true, null));
});
return new StackTrace(frames);
}
}

View file

@ -43,7 +43,7 @@ public static class SeStringExtensions
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
[Api11ToDo("Remove")]
[Api12ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });
@ -52,7 +52,7 @@ public static class SeStringExtensions
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
[Api11ToDo("Remove")]
[Api12ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });

View file

@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Support;
using ImGuiNET;
using Lumina.Excel.Sheets;
@ -28,8 +29,6 @@ using Windows.Win32.Storage.FileSystem;
using Windows.Win32.System.Memory;
using Windows.Win32.System.Ole;
using Dalamud.Interface.Utility.Raii;
using static TerraFX.Interop.Windows.Windows;
using Win32_PInvoke = Windows.Win32.PInvoke;
@ -66,7 +65,6 @@ public static class Util
private static readonly Type GenericSpanType = typeof(Span<>);
private static string? scmVersionInternal;
private static string? gitHashInternal;
private static int? gitCommitCountInternal;
private static string? gitHashClientStructsInternal;
private static ulong moduleStartAddr;
@ -78,58 +76,6 @@ public static class Util
public static string AssemblyVersion { get; } =
Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
/// <summary>
/// Check two byte arrays for equality.
/// </summary>
/// <param name="a1">The first byte array.</param>
/// <param name="a2">The second byte array.</param>
/// <returns>Whether or not the byte arrays are equal.</returns>
public static unsafe bool FastByteArrayCompare(byte[]? a1, byte[]? a2)
{
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
{
if (*((long*)x1) != *((long*)x2))
return false;
}
if ((l & 4) != 0)
{
if (*((int*)x1) != *((int*)x2))
return false;
x1 += 4;
x2 += 4;
}
if ((l & 2) != 0)
{
if (*((short*)x1) != *((short*)x2))
return false;
x1 += 2;
x2 += 2;
}
if ((l & 1) != 0)
{
if (*((byte*)x1) != *((byte*)x2))
return false;
}
return true;
}
}
/// <summary>
/// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return
/// the <c>git describe</c> output for this build, which will be a raw version if this is a stable build or an
@ -1051,7 +997,8 @@ public static class Util
}
}
private static unsafe void ShowSpanEntryPrivate<T>(ulong addr, IList<string> path, int offset, Span<T> spanobj) {
private static unsafe void ShowSpanEntryPrivate<T>(ulong addr, IList<string> path, int offset, Span<T> spanobj)
{
const int batchSize = 20;
if (spanobj.Length > batchSize)
{
@ -1221,6 +1168,7 @@ public static class Util
ImGui.TextDisabled($"[0x{offset.Value:X}]");
ImGui.SameLine();
}
ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{f.FieldType.Name}");
}

View file

@ -941,7 +941,7 @@ int main() {
log << std::format(L"Dump at: {}", dumpPath.wstring()) << std::endl;
else
log << std::format(L"Dump error: {}", dumpError) << std::endl;
log << L"System Time: " << std::chrono::system_clock::now() << std::endl;
log << std::format(L"System Time: {0:%F} {0:%T} {0:%Ez}", std::chrono::system_clock::now()) << std::endl;
log << L"\n" << stackTrace << std::endl;
if (pProgressDialog)

View file

@ -4,10 +4,18 @@
<PropertyGroup Label="Target">
<TargetFramework>net9.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<Platforms>x64</Platforms>
<LangVersion>13.0</LangVersion>
</PropertyGroup>
<!-- Dependency versions -->
<PropertyGroup Label="Dependency Versions">
<LuminaVersion>5.6.1</LuminaVersion>
<LuminaExcelVersion>7.1.3</LuminaExcelVersion>
<NewtonsoftJsonVersion>13.0.3</NewtonsoftJsonVersion>
</PropertyGroup>
<!-- Code analysis settings for all Dalamud projects. -->
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)tools\BannedSymbols.txt" />

View file

@ -4,13 +4,18 @@
<img src="https://raw.githubusercontent.com/goatcorp/DalamudAssets/master/UIRes/logo.png" alt="Dalamud" width="200"/>
</p>
Dalamud is a plugin development framework for FINAL FANTASY XIV that provides access to game data and native interoperability with the game itself to add functionality and quality-of-life.
Dalamud is a plugin development framework for FFXIV that provides access to game data and native interoperability with the game itself to add functionality and quality-of-life.
It is meant to be used in conjunction with [FFXIVQuickLauncher](https://github.com/goatcorp/FFXIVQuickLauncher), which manages and launches Dalamud for you. __It is generally not recommended for users to try to run Dalamud manually as there are multiple dependencies and assumed folder paths.__
It is meant to be used in conjunction with [XIVLauncher](https://github.com/goatcorp/FFXIVQuickLauncher), which manages and launches Dalamud for you. __It is generally not recommended for end users to try to run Dalamud manually as XIVLauncher manages multiple required dependencies.__
## Hold Up!
If you are just trying to **use** Dalamud, you don't need to do anything on this page - please [download XIVLauncher](https://goatcorp.github.io/) from its official page and follow the setup instructions.
## Building and testing locally
Please check the [docs page on building Dalamud](https://dalamud.dev/building) for more information and required dependencies.
## Plugin development
Dalamud features a growing API for in-game plugin development with game data and chat access and overlays.
Please see our [Developer FAQ](https://goatcorp.github.io/faq/development) and the [API documentation](https://dalamud.dev) for more details.
@ -34,15 +39,6 @@ Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint.
| *Dalamud* (C#) | Core API, game bindings, plugin framework |
| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
## Branches
We are currently working from the following branches.
| Name | API Level | Purpose | .NET Version | Track |
|----------|-----------|------------------------------------------------------------|----------------------------|-------------------|
| *master* | **9** | Current release branch | .NET 8.0.0 (November 2023) | Release & Staging |
| *api10* | **10** | Next major version, slated for release alongside Patch 7.0 | .NET 8.0.0 (November 2023) | api10 |
<br>
##### Final Fantasy XIV © 2010-2021 SQUARE ENIX CO., LTD. All Rights Reserved. We are not affiliated with SQUARE ENIX CO., LTD. in any way.

View file

@ -47,6 +47,14 @@ public class DalamudBuild : NukeBuild
AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test";
AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj";
AbsolutePath ExternalsDir => RootDirectory / "external";
AbsolutePath CImGuiDir => ExternalsDir / "cimgui";
AbsolutePath CImGuiProjectFile => CImGuiDir / "cimgui.vcxproj";
AbsolutePath CImPlotDir => ExternalsDir / "cimplot";
AbsolutePath CImPlotProjectFile => CImPlotDir / "cimplot.vcxproj";
AbsolutePath CImGuizmoDir => ExternalsDir / "cimguizmo";
AbsolutePath CImGuizmoProjectFile => CImGuizmoDir / "cimguizmo.vcxproj";
AbsolutePath ArtifactsDirectory => RootDirectory / "bin" / Configuration;
private static AbsolutePath LibraryDirectory => RootDirectory / "lib";
@ -60,8 +68,53 @@ public class DalamudBuild : NukeBuild
.SetProjectFile(Solution));
});
Target CompileCImGui => _ => _
.Executes(() =>
{
// Not necessary, and does not build on Linux
if (IsDocsBuild)
return;
MSBuildTasks.MSBuild(s => s
.SetTargetPath(CImGuiProjectFile)
.SetConfiguration(Configuration)
.SetTargetPlatform(MSBuildTargetPlatform.x64));
});
Target CompileCImPlot => _ => _
.Executes(() =>
{
// Not necessary, and does not build on Linux
if (IsDocsBuild)
return;
MSBuildTasks.MSBuild(s => s
.SetTargetPath(CImPlotProjectFile)
.SetConfiguration(Configuration)
.SetTargetPlatform(MSBuildTargetPlatform.x64));
});
Target CompileCImGuizmo => _ => _
.Executes(() =>
{
// Not necessary, and does not build on Linux
if (IsDocsBuild)
return;
MSBuildTasks.MSBuild(s => s
.SetTargetPath(CImGuizmoProjectFile)
.SetConfiguration(Configuration)
.SetTargetPlatform(MSBuildTargetPlatform.x64));
});
Target CompileImGuiNatives => _ => _
.DependsOn(CompileCImGui)
.DependsOn(CompileCImPlot)
.DependsOn(CompileCImGuizmo);
Target CompileDalamud => _ => _
.DependsOn(Restore)
.DependsOn(CompileImGuiNatives)
.Executes(() =>
{
DotNetTasks.DotNetBuild(s =>
@ -73,12 +126,12 @@ public class DalamudBuild : NukeBuild
// We need to emit compiler generated files for the docs build, since docfx can't run generators directly
// TODO: This fails every build after this because of redefinitions...
if (IsDocsBuild)
{
Log.Warning("Building for documentation, emitting compiler generated files. This can cause issues on Windows due to path-length limitations");
s = s
.SetProperty("IsDocsBuild", "true");
}
// if (IsDocsBuild)
// {
// Log.Warning("Building for documentation, emitting compiler generated files. This can cause issues on Windows due to path-length limitations");
// s = s
// .SetProperty("IsDocsBuild", "true");
// }
return s;
});
@ -138,6 +191,21 @@ public class DalamudBuild : NukeBuild
Target Clean => _ => _
.Executes(() =>
{
MSBuildTasks.MSBuild(s => s
.SetProjectFile(CImGuiProjectFile)
.SetConfiguration(Configuration)
.SetTargets("Clean"));
MSBuildTasks.MSBuild(s => s
.SetProjectFile(CImPlotProjectFile)
.SetConfiguration(Configuration)
.SetTargets("Clean"));
MSBuildTasks.MSBuild(s => s
.SetProjectFile(CImGuizmoProjectFile)
.SetConfiguration(Configuration)
.SetTargets("Clean"));
DotNetTasks.DotNetClean(s => s
.SetProject(DalamudProjectFile)
.SetConfiguration(Configuration));

3
external/Directory.Build.props vendored Normal file
View file

@ -0,0 +1,3 @@
<!-- This is left empty on purpose to avoid having Directory.Build.props in the root of the repository leak into dependencies. -->
<Project>
</Project>

107
external/cimgui/cimgui.vcxproj vendored Normal file
View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimgui\cimgui.cpp" />
<ClCompile Include="..\..\lib\cimgui\cimgui_impl.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimgui\cimgui.h" />
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8430077c-f736-4246-a052-8ea1cece844e}</ProjectGuid>
<RootNamespace>cimgui</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\lib\cimgui\imgui;..\..\lib\cimgui;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;CIMGUI_EXPORTS;_WINDOWS;_USRDLL;IMGUI_DISABLE_OBSOLETE_FUNCTIONS=1;IMGUI_USER_CONFIG="cimgui_user.h";%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;CIMGUI_EXPORTS;_WINDOWS;_USRDLL;IMGUI_DISABLE_OBSOLETE_FUNCTIONS=1;IMGUI_USER_CONFIG="cimgui_user.h";%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\..\bin\$(Configuration)\" />
</Target>
</Project>

45
external/cimgui/cimgui.vcxproj.filters vendored Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimgui\cimgui.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\cimgui_impl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimgui\cimgui.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

113
external/cimguizmo/cimguizmo.vcxproj vendored Normal file
View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimguizmo\cimguizmo.cpp" />
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\GraphEditor.cpp" />
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImCurveEdit.cpp" />
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImGradient.cpp" />
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImGuizmo.cpp" />
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImSequencer.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimguizmo\cimguizmo.h" />
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{F258347D-31BE-4605-98CE-40E43BDF6F9D}</ProjectGuid>
<RootNamespace>cimplot</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\lib\cimgui\imgui;..\..\lib\cimguizmo\ImGuizmo;..\..\lib\cimgui;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;CIMGUIZMO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;CIMGUIZMO_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\..\bin\$(Configuration)\" />
</Target>
</Project>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\cimguizmo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\GraphEditor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImCurveEdit.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImGradient.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImGuizmo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimguizmo\ImGuizmo\ImSequencer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimguizmo\cimguizmo.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

111
external/cimplot/cimplot.vcxproj vendored Normal file
View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp" />
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp" />
<ClCompile Include="..\..\lib\cimplot\cimplot.cpp" />
<ClCompile Include="..\..\lib\cimplot\implot\implot.cpp" />
<ClCompile Include="..\..\lib\cimplot\implot\implot_demo.cpp" />
<ClCompile Include="..\..\lib\cimplot\implot\implot_items.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimplot\cimplot.h" />
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{76caa246-c405-4a8c-b0ae-f4a0ef3d4e16}</ProjectGuid>
<RootNamespace>cimplot</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\lib\cimgui\imgui;..\..\lib\cimplot\implot;..\..\lib\cimgui;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;CIMPLOT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;CIMPLOT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\..\bin\$(Configuration)\" />
</Target>
</Project>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\lib\cimplot\cimplot.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimplot\implot\implot.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimplot\implot\implot_demo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimplot\implot\implot_items.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_demo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_draw.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_tables.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\lib\cimgui\imgui\imgui_widgets.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\lib\cimplot\cimplot.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

@ -1 +1 @@
Subproject commit df03181ccbbbfead3db116b59359dae4a31cb07d
Subproject commit 484b1b588be5e685855392f090ec9e3eea7b3a71

@ -1 +1 @@
Subproject commit b0d41471b7ef3d69daaf6d862eb74e7e00a25651
Subproject commit d336b20a85ea48723a98681b18bdfe14a56a3403

1
lib/cimgui Submodule

@ -0,0 +1 @@
Subproject commit 122ee16819437eea7eefe0c04398b44174106d86

1
lib/cimguizmo Submodule

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

1
lib/cimplot Submodule

@ -0,0 +1 @@
Subproject commit 939f8f36deebd895f6cda522ee4bb2b798920935

31
release.ps1 Normal file
View file

@ -0,0 +1,31 @@
param(
[string]$VersionString
)
if (-not $VersionString) {
Write-Error "Version string is required as the first argument."
exit 1
}
$csprojPath = "Dalamud/Dalamud.csproj"
if (-not (Test-Path $csprojPath)) {
Write-Error "Cannot find Dalamud.csproj at the specified path."
exit 1
}
# Update the version in the csproj file
(Get-Content $csprojPath) -replace '<DalamudVersion>.*?</DalamudVersion>', "<DalamudVersion>$VersionString</DalamudVersion>" | Set-Content $csprojPath
# Commit the change
git add $csprojPath
git commit -m "build: $VersionString"
# Get the current branch
$currentBranch = git rev-parse --abbrev-ref HEAD
# Create a tag
git tag -a -m "v$VersionString" $VersionString
# Push atomically
git push origin $currentBranch $VersionString