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 end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true
# 4 space indentation # 4 space indentation
indent_style = space indent_style = space

View file

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

9
.gitmodules vendored
View file

@ -10,3 +10,12 @@
[submodule "lib/TsudaKageyu-minhook"] [submodule "lib/TsudaKageyu-minhook"]
path = lib/TsudaKageyu-minhook path = lib/TsudaKageyu-minhook
url = https://github.com/TsudaKageyu/minhook.git 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": [ "enum": [
"Clean", "Clean",
"Compile", "Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud", "CompileDalamud",
"CompileDalamudBoot", "CompileDalamudBoot",
"CompileDalamudCrashHandler", "CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector", "CompileInjector",
"CompileInjectorBoot", "CompileInjectorBoot",
"Restore", "Restore",
@ -100,9 +104,13 @@
"enum": [ "enum": [
"Clean", "Clean",
"Compile", "Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud", "CompileDalamud",
"CompileDalamudBoot", "CompileDalamudBoot",
"CompileDalamudCrashHandler", "CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector", "CompileInjector",
"CompileInjectorBoot", "CompileInjectorBoot",
"Restore", "Restore",

View file

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

View file

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

View file

@ -41,10 +41,6 @@
<PropertyGroup Condition="'$(Configuration)'=='Debug'"> <PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\injector\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings"> <PropertyGroup Label="Warnings">
<NoWarn>IDE0003;IDE0044;IDE1006;CS1591;CS1701;CS1702</NoWarn> <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 ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
.gitignore = .gitignore .gitignore = .gitignore
tools\BannedSymbols.txt = tools\BannedSymbols.txt
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.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 tools\dalamud.ruleset = tools\dalamud.ruleset
Directory.Build.props = Directory.Build.props
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud.csproj", "{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}" 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 EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}"
EndProject EndProject
@ -25,7 +30,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Da
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject 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 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}" 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 EndProject
@ -49,6 +54,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Release|Any CPU.Build.0 = 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 {8430077C-F736-4246-A052-8EA1CECE844E}.Debug|Any CPU.ActiveCfg = Debug|x64
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {8430077C-F736-4246-A052-8EA1CECE844E}.Debug|Any CPU.Build.0 = Debug|x64
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {8430077C-F736-4246-A052-8EA1CECE844E}.Release|Any CPU.ActiveCfg = Release|x64
{E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.Build.0 = Release|Any CPU {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {0483026E-C6CE-4B1A-AA68-46544C08140B} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{3620414C-7DFC-423E-929F-310E19F5D930} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {DBE5345E-6594-4A59-B183-1C3D5592269D}
{A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
@ -11,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Profiles;
@ -45,6 +47,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
[JsonIgnore] [JsonIgnore]
private bool isSaveQueued; private bool isSaveQueued;
private Task? writeTask;
/// <summary> /// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved. /// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary> /// </summary>
@ -243,13 +247,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. /// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary> /// </summary>
public bool AssertsEnabledAtStartup { get; set; } public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
/// </summary> /// </summary>
public bool IsDocking { get; set; } public bool IsDocking { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
/// This setting is effected by the in-game "System Sounds" option and volume. /// This setting is effected by the in-game "System Sounds" option and volume.
@ -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 /// 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. /// on plugin title bars when using the Window System.
/// </summary> /// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")] public bool EnablePluginUiAdditionalOptions { get; set; } = true;
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled. /// Gets or sets a value indicating whether viewports should always be disabled.
@ -348,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false; public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets the default UI preset.
/// </summary>
public PresetModel DefaultUiPreset { get; set; } = new();
/// <summary> /// <summary>
/// Gets or sets the order of DTR elements, by title. /// Gets or sets the order of DTR elements, by title.
/// </summary> /// </summary>
@ -484,10 +492,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null; public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <summary> /// <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> /// </summary>
public bool CheckPeriodicallyForUpdates { get; set; } = true; 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> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>
@ -504,7 +517,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
deserialized = deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings); JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
// If this reads as null, the file was empty, that's no good // If this reads as null, the file was empty, that's no good
if (deserialized == null) if (deserialized == null)
throw new Exception("Read config was null."); throw new Exception("Read config was null.");
@ -530,7 +543,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
Log.Error(e, "Failed to set defaults for DalamudConfiguration"); Log.Error(e, "Failed to set defaults for DalamudConfiguration");
} }
return deserialized; return deserialized;
} }
@ -549,12 +562,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
this.Save(); this.Save();
} }
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
// Make sure that we save, if a save is queued while we are shutting down // Make sure that we save, if a save is queued while we are shutting down
this.Update(); this.Update();
// Wait for the write task to finish
this.writeTask?.Wait();
} }
/// <summary> /// <summary>
@ -595,22 +611,36 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
this.ReduceMotions = winAnimEnabled == 0; this.ReduceMotions = winAnimEnabled == 0;
} }
} }
// Migrate old auto-update setting to new auto-update behavior // Migrate old auto-update setting to new auto-update behavior
this.AutoUpdateBehavior ??= this.AutoUpdatePlugins this.AutoUpdateBehavior ??= this.AutoUpdatePlugins
? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll ? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll
: Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify; : Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify;
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
private void Save() private void Save()
{ {
ThreadSafety.AssertMainThread(); ThreadSafety.AssertMainThread();
if (this.configPath is null) if (this.configPath is null)
throw new InvalidOperationException("configPath is not set."); throw new InvalidOperationException("configPath is not set.");
Service<ReliableFileStorage>.Get().WriteAllText( // Wait for previous write to finish
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); this.writeTask?.Wait();
this.writeTask = Task.Run(() =>
{
Service<ReliableFileStorage>.Get().WriteAllText(
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); this.DalamudConfigurationSaved?.Invoke(this);
} }
} }

View file

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

View file

@ -178,6 +178,9 @@ public sealed class EntryPoint
throw new Exception("Working directory was invalid"); throw new Exception("Working directory was invalid");
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory); 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 // 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; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;

View file

@ -48,7 +48,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private bool lastConditionNone = true; private bool lastConditionNone = true;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool WarnMultithreadedUsage() private bool WarnMultithreadedUsage()
{ {

View file

@ -11,7 +11,7 @@ public interface IReadOnlyCommandInfo
/// <param name="command">The command itself.</param> /// <param name="command">The command itself.</param>
/// <param name="arguments">The arguments supplied to the command, ready for parsing.</param> /// <param name="arguments">The arguments supplied to the command, ready for parsing.</param>
public delegate void HandlerDelegate(string command, string arguments); public delegate void HandlerDelegate(string command, string arguments);
/// <summary> /// <summary>
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched. /// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
/// </summary> /// </summary>
@ -26,6 +26,11 @@ public interface IReadOnlyCommandInfo
/// Gets a value indicating whether if this command should be shown in the help output. /// Gets a value indicating whether if this command should be shown in the help output.
/// </summary> /// </summary>
bool ShowInHelp { get; } bool ShowInHelp { get; }
/// <summary>
/// Gets the display order of this command. Defaults to alphabetical ordering.
/// </summary>
int DisplayOrder { get; }
} }
/// <summary> /// <summary>
@ -51,4 +56,7 @@ public sealed class CommandInfo : IReadOnlyCommandInfo
/// <inheritdoc/> /// <inheritdoc/>
public bool ShowInHelp { get; set; } = true; public bool ShowInHelp { get; set; } = true;
/// <inheritdoc/>
public int DisplayOrder { get; set; } = -1;
} }

View file

@ -47,9 +47,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
} }
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/> /// <inheritdoc/>
@ -92,16 +92,22 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
var manager = RaptureAtkUnitManager.Instance(); var manager = RaptureAtkUnitManager.Instance();
if (manager == null)
return;
var menu = manager->GetAddonByName("ContextMenu"); var menu = manager->GetAddonByName("ContextMenu");
var submenu = manager->GetAddonByName("AddonContextSub"); var submenu = manager->GetAddonByName("AddonContextSub");
if (menu == null || submenu == null)
return;
if (menu->IsVisible) if (menu->IsVisible)
menu->FireCallbackInt(-1); menu->FireCallbackInt(-1);
if (submenu->IsVisible) if (submenu->IsVisible)
submenu->FireCallbackInt(-1); submenu->FireCallbackInt(-1);
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -145,7 +145,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <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; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <summary> /// <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.Game.Network.Structures;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using static Dalamud.Plugin.Services.IMarketBoard;
namespace Dalamud.Game.MarketBoard; namespace Dalamud.Game.MarketBoard;
/// <summary> /// <summary>
@ -29,19 +35,19 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
} }
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived; public event HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased; public event ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived; public event OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested; public event PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived; public event TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/> /// <inheritdoc/>
public void DisposeService() public void DisposeService()
@ -89,35 +95,42 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
{ {
private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped));
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get(); private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get();
private readonly string owningPluginName;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MarketBoardPluginScoped"/> class. /// Initializes a new instance of the <see cref="MarketBoardPluginScoped"/> class.
/// </summary> /// </summary>
internal MarketBoardPluginScoped() /// <param name="plugin">The plugin owning this service.</param>
internal MarketBoardPluginScoped(LocalPlugin? plugin)
{ {
this.marketBoardService.HistoryReceived += this.OnHistoryReceived; this.marketBoardService.HistoryReceived += this.OnHistoryReceived;
this.marketBoardService.ItemPurchased += this.OnItemPurchased; this.marketBoardService.ItemPurchased += this.OnItemPurchased;
this.marketBoardService.OfferingsReceived += this.OnOfferingsReceived; this.marketBoardService.OfferingsReceived += this.OnOfferingsReceived;
this.marketBoardService.PurchaseRequested += this.OnPurchaseRequested; this.marketBoardService.PurchaseRequested += this.OnPurchaseRequested;
this.marketBoardService.TaxRatesReceived += this.OnTaxRatesReceived; this.marketBoardService.TaxRatesReceived += this.OnTaxRatesReceived;
this.owningPluginName = plugin?.InternalName ?? "DalamudInternal";
} }
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived; public event HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased; public event ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived; public event OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested; public event PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/> /// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived; public event TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
@ -137,26 +150,96 @@ internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoar
private void OnHistoryReceived(IMarketBoardHistory history) 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) 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) 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) 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) 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. /// Upload data about an item.
/// </summary> /// </summary>
/// <param name="item">The item request data being uploaded.</param> /// <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> /// <returns>An async task.</returns>
Task Upload(MarketBoardItemRequest item); Task Upload(MarketBoardItemRequest item, ulong uploaderId, uint worldId);
/// <summary> /// <summary>
/// Upload tax rate data. /// Upload tax rate data.
/// </summary> /// </summary>
/// <param name="taxRates">The tax rate data being uploaded.</param> /// <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> /// <returns>An async task.</returns>
Task UploadTax(MarketTaxRates taxRates); Task UploadTax(MarketTaxRates taxRates, ulong uploaderId, uint worldId);
/// <summary> /// <summary>
/// Upload information about a purchase this client has made. /// Upload information about a purchase this client has made.
/// </summary> /// </summary>
/// <param name="purchaseHandler">The purchase handler data associated with the sale.</param> /// <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> /// <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.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -33,21 +32,16 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
this.httpClient = happyHttpClient.SharedHttpClient; this.httpClient = happyHttpClient.SharedHttpClient;
/// <inheritdoc/> /// <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"); Log.Verbose("Starting Universalis upload");
var uploader = clientState.LocalContentId;
// ==================================================================================== // ====================================================================================
var uploadObject = new UniversalisItemUploadRequest var uploadObject = new UniversalisItemUploadRequest
{ {
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0, WorldId = worldId,
UploaderId = uploader.ToString(), UploaderId = uploaderId.ToString(),
ItemId = request.CatalogId, ItemId = request.CatalogId,
Listings = [], Listings = [],
Sales = [], Sales = [],
@ -117,18 +111,12 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
} }
/// <inheritdoc/> /// <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 var taxUploadObject = new UniversalisTaxUploadRequest
{ {
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0, WorldId = worldId,
UploaderId = clientState.LocalContentId.ToString(), UploaderId = uploaderId.ToString(),
TaxData = new UniversalisTaxData TaxData = new UniversalisTaxData
{ {
LimsaLominsa = taxRates.LimsaLominsaTax, 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 /// 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. /// a listing, or delete it, when a purchase has been made.
/// </remarks> /// </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 itemId = purchaseHandler.CatalogId;
var worldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0;
// ==================================================================================== // ====================================================================================
@ -176,7 +159,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
Quantity = purchaseHandler.ItemQuantity, Quantity = purchaseHandler.ItemQuantity,
ListingId = purchaseHandler.ListingId.ToString(), ListingId = purchaseHandler.ListingId.ToString(),
RetainerId = purchaseHandler.RetainerId.ToString(), RetainerId = purchaseHandler.RetainerId.ToString(),
UploaderId = clientState.LocalContentId.ToString(), UploaderId = uploaderId.ToString(),
}; };
var deletePath = $"/api/{worldId}/{itemId}/delete"; var deletePath = $"/api/{worldId}/{itemId}/delete";

View file

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

View file

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

View file

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

View file

@ -182,7 +182,10 @@ public static partial class ImGuiComponents
/// </summary> /// </summary>
/// <param name="icon">Icon to show.</param> /// <param name="icon">Icon to show.</param>
/// <param name="text">Text 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> /// <returns>Indicator if button is clicked.</returns>
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector2 size) => IconButtonWithText(icon, text, null, null, null, size); 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="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</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="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> /// <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) 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> /// <returns>Width.</returns>
public static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text) public static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text)
{ {
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont)) 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;
} }
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; namespace Dalamud.Interface.Components;
/// <summary>
/// ImGui component used to create a radio-like input that uses icon buttons.
/// </summary>
public static partial class ImGuiComponents public static partial class ImGuiComponents
{ {
/// <summary> /// <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()) if (arguments.IsNullOrWhitespace())
{ {
chatGui.Print(Loc.Localize("DalamudCmdHelpAvailable", "Available commands:")); 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) if (!cmd.Value.ShowInHelp)
continue; continue;

View file

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

View file

@ -18,7 +18,6 @@ using Dalamud.Game.Internal;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.Data;
using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.PluginInstaller;
@ -58,7 +57,6 @@ internal class DalamudInterface : IInternalDisposableService
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private readonly DalamudConfiguration configuration; private readonly DalamudConfiguration configuration;
private readonly InterfaceManager interfaceManager; private readonly InterfaceManager interfaceManager;
private readonly DataManager dataManager;
private readonly ChangelogWindow changelogWindow; private readonly ChangelogWindow changelogWindow;
private readonly ColorDemoWindow colorDemoWindow; private readonly ColorDemoWindow colorDemoWindow;
@ -93,14 +91,13 @@ internal class DalamudInterface : IInternalDisposableService
private bool isImPlotDrawDemoWindow = false; private bool isImPlotDrawDemoWindow = false;
private bool isImGuiTestWindowsInMonospace = false; private bool isImGuiTestWindowsInMonospace = false;
private bool isImGuiDrawMetricsWindow = false; private bool isImGuiDrawMetricsWindow = false;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DalamudInterface( private DalamudInterface(
Dalamud dalamud, Dalamud dalamud,
DalamudConfiguration configuration, DalamudConfiguration configuration,
FontAtlasFactory fontAtlasFactory, FontAtlasFactory fontAtlasFactory,
InterfaceManager interfaceManager, InterfaceManager interfaceManager,
DataManager dataManager,
PluginImageCache pluginImageCache, PluginImageCache pluginImageCache,
DalamudAssetManager dalamudAssetManager, DalamudAssetManager dalamudAssetManager,
Game.Framework framework, Game.Framework framework,
@ -113,10 +110,9 @@ internal class DalamudInterface : IInternalDisposableService
this.dalamud = dalamud; this.dalamud = dalamud;
this.configuration = configuration; this.configuration = configuration;
this.interfaceManager = interfaceManager; this.interfaceManager = interfaceManager;
this.dataManager = dataManager;
this.WindowSystem = new WindowSystem("DalamudCore"); this.WindowSystem = new WindowSystem("DalamudCore");
this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false };
this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false };
this.dataWindow = new DataWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false };
@ -163,7 +159,7 @@ internal class DalamudInterface : IInternalDisposableService
this.WindowSystem.AddWindow(this.branchSwitcherWindow); this.WindowSystem.AddWindow(this.branchSwitcherWindow);
this.WindowSystem.AddWindow(this.hitchSettingsWindow); this.WindowSystem.AddWindow(this.hitchSettingsWindow);
ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; this.interfaceManager.ShowAsserts = configuration.ImGuiAssertsEnabledAtStartup ?? false;
this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup; this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup;
this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.Draw += this.OnDraw;
@ -197,7 +193,7 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
// This is temporary, until we know the repercussions of vtable hooking mode // This is temporary, until we know the repercussions of vtable hooking mode
consoleManager.AddCommand( consoleManager.AddCommand(
"dalamud.interface.swapchain_mode", "dalamud.interface.swapchain_mode",
@ -216,14 +212,14 @@ internal class DalamudInterface : IInternalDisposableService
Log.Error("Unknown swapchain mode: {Mode}", mode); Log.Error("Unknown swapchain mode: {Mode}", mode);
return false; return false;
} }
this.configuration.QueueSave(); this.configuration.QueueSave();
return true; return true;
}); });
} }
private delegate nint CrashDebugDelegate(nint self); private delegate nint CrashDebugDelegate(nint self);
/// <summary> /// <summary>
/// Gets the number of frames since Dalamud has loaded. /// Gets the number of frames since Dalamud has loaded.
/// </summary> /// </summary>
@ -323,7 +319,7 @@ internal class DalamudInterface : IInternalDisposableService
this.pluginStatWindow.IsOpen = true; this.pluginStatWindow.IsOpen = true;
this.pluginStatWindow.BringToFront(); this.pluginStatWindow.BringToFront();
} }
/// <summary> /// <summary>
/// Opens the <see cref="PluginInstallerWindow"/> on the plugin installed. /// Opens the <see cref="PluginInstallerWindow"/> on the plugin installed.
/// </summary> /// </summary>
@ -388,7 +384,7 @@ internal class DalamudInterface : IInternalDisposableService
this.profilerWindow.IsOpen = true; this.profilerWindow.IsOpen = true;
this.profilerWindow.BringToFront(); this.profilerWindow.BringToFront();
} }
/// <summary> /// <summary>
/// Opens the <see cref="HitchSettingsWindow"/>. /// Opens the <see cref="HitchSettingsWindow"/>.
/// </summary> /// </summary>
@ -700,7 +696,7 @@ internal class DalamudInterface : IInternalDisposableService
ImGui.EndMenu(); ImGui.EndMenu();
} }
var logSynchronously = this.configuration.LogSynchronously; var logSynchronously = this.configuration.LogSynchronously;
if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously))
{ {
@ -792,14 +788,14 @@ internal class DalamudInterface : IInternalDisposableService
} }
ImGui.Separator(); ImGui.Separator();
if (ImGui.BeginMenu("Crash game")) if (ImGui.BeginMenu("Crash game"))
{ {
if (ImGui.MenuItem("Access Violation")) if (ImGui.MenuItem("Access Violation"))
{ {
Marshal.ReadByte(IntPtr.Zero); Marshal.ReadByte(IntPtr.Zero);
} }
if (ImGui.MenuItem("Set UiModule to NULL")) if (ImGui.MenuItem("Set UiModule to NULL"))
{ {
unsafe unsafe
@ -808,7 +804,7 @@ internal class DalamudInterface : IInternalDisposableService
framework->UIModule = (UIModule*)0; framework->UIModule = (UIModule*)0;
} }
} }
if (ImGui.MenuItem("Set UiModule to invalid ptr")) if (ImGui.MenuItem("Set UiModule to invalid ptr"))
{ {
unsafe unsafe
@ -817,7 +813,7 @@ internal class DalamudInterface : IInternalDisposableService
framework->UIModule = (UIModule*)0x12345678; framework->UIModule = (UIModule*)0x12345678;
} }
} }
if (ImGui.MenuItem("Deref nullptr in Hook")) if (ImGui.MenuItem("Deref nullptr in Hook"))
{ {
unsafe unsafe
@ -832,7 +828,13 @@ internal class DalamudInterface : IInternalDisposableService
hook.Enable(); hook.Enable();
} }
} }
if (ImGui.MenuItem("Cause ImGui assert"))
{
ImGui.PopStyleVar();
ImGui.PopStyleVar();
}
ImGui.EndMenu(); ImGui.EndMenu();
} }
@ -848,7 +850,7 @@ internal class DalamudInterface : IInternalDisposableService
{ {
this.OpenBranchSwitcher(); this.OpenBranchSwitcher();
} }
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false);
ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
ImGui.MenuItem($"CLR: {Environment.Version}", false); ImGui.MenuItem($"CLR: {Environment.Version}", false);
@ -865,18 +867,27 @@ internal class DalamudInterface : IInternalDisposableService
ImGui.Separator(); ImGui.Separator();
var val = ImGuiManagedAsserts.AssertsEnabled; var showAsserts = this.interfaceManager.ShowAsserts;
if (ImGui.MenuItem("Enable Asserts", string.Empty, ref val)) 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(); this.configuration.QueueSave();
} }
ImGui.Separator();
if (ImGui.MenuItem("Clear focus")) if (ImGui.MenuItem("Clear focus"))
{ {
ImGui.SetWindowFocus(null); ImGui.SetWindowFocus(null);
@ -924,7 +935,7 @@ internal class DalamudInterface : IInternalDisposableService
{ {
this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo;
} }
ImGui.Separator(); ImGui.Separator();
if (ImGui.MenuItem("Show loading window")) if (ImGui.MenuItem("Show loading window"))
@ -1001,6 +1012,11 @@ internal class DalamudInterface : IInternalDisposableService
pluginManager.LoadBannedPlugins = !pluginManager.LoadBannedPlugins; pluginManager.LoadBannedPlugins = !pluginManager.LoadBannedPlugins;
} }
if (pluginManager.SafeMode && ImGui.MenuItem("Disable Safe Mode"))
{
pluginManager.SafeMode = false;
}
ImGui.Separator(); ImGui.Separator();
ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false); ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false);
ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count(), false); ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count(), false);

View file

@ -32,19 +32,21 @@ internal static partial class DalamudComponents
var pm = Service<PluginManager>.GetNullable(); var pm = Service<PluginManager>.GetNullable();
if (pm == null) if (pm == null)
return 0; return 0;
var addPluginToProfilePopupId = ImGui.GetID(id); var addPluginToProfilePopupId = ImGui.GetID(id);
using var popup = ImRaii.Popup(id); using var popup = ImRaii.Popup(id);
if (popup.Success) if (popup.Success)
{ {
var width = ImGuiHelpers.GlobalScale * 300; var width = ImGuiHelpers.GlobalScale * 300;
ImGui.SetNextItemWidth(width); ImGui.SetNextItemWidth(width);
ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255); ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255);
var currentSearchString = pickerSearch; 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 // TODO: Plugin searching should be abstracted... installer and this should use the same search
var plugins = pm.InstalledPlugins.Where( var plugins = pm.InstalledPlugins.Where(
@ -53,19 +55,15 @@ internal static partial class DalamudComponents
currentSearchString, currentSearchString,
StringComparison.InvariantCultureIgnoreCase))) StringComparison.InvariantCultureIgnoreCase)))
.Where(pluginFiltered ?? (_ => true)); .Where(pluginFiltered ?? (_ => true));
foreach (var plugin in plugins) foreach (var plugin in plugins)
{ {
using var disabled2 = using var disabled2 = ImRaii.Disabled(pluginDisabled(plugin));
ImRaii.Disabled(pluginDisabled(plugin));
if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
{ {
onClicked(plugin); 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.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Asserts;
using Dalamud.Interface.Internal.DesignSystem; using Dalamud.Interface.Internal.DesignSystem;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Timing; using Dalamud.Utility.Timing;
@ -60,6 +62,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface. /// This class manages interaction with the ImGui interface.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
[InherentDependency<WindowSystemPersistence>] // Used by window system windows to restore state from the configuration
internal partial class InterfaceManager : IInternalDisposableService internal partial class InterfaceManager : IInternalDisposableService
{ {
/// <summary> /// <summary>
@ -73,7 +76,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private static readonly ModuleLog Log = new("INTERFACE"); private static readonly ModuleLog Log = new("INTERFACE");
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new(); private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new(); private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
@ -94,6 +97,8 @@ internal partial class InterfaceManager : IInternalDisposableService
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new(); private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new(); private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
private readonly AssertHandler assertHandler = new();
private RawDX11Scene? scene; private RawDX11Scene? scene;
private Hook<SetCursorDelegate>? setCursorHook; private Hook<SetCursorDelegate>? setCursorHook;
@ -267,11 +272,27 @@ internal partial class InterfaceManager : IInternalDisposableService
/// </remarks> /// </remarks>
public long CumulativePresentCalls { get; private set; } 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> /// <summary>
/// Dispose of managed and unmanaged resources. /// Dispose of managed and unmanaged resources.
/// </summary> /// </summary>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.assertHandler.Dispose();
// Unload hooks from the framework thread if possible. // Unload hooks from the framework thread if possible.
// We're currently off the framework thread, as this function can only be called from // We're currently off the framework thread, as this function can only be called from
// ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread. // ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread.
@ -565,6 +586,7 @@ internal partial class InterfaceManager : IInternalDisposableService
{ {
try try
{ {
this.assertHandler.Setup();
newScene = new RawDX11Scene((nint)swapChain); newScene = new RawDX11Scene((nint)swapChain);
} }
catch (DllNotFoundException ex) catch (DllNotFoundException ex)
@ -797,14 +819,14 @@ internal partial class InterfaceManager : IInternalDisposableService
}); });
}; };
} }
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
_ = this.dalamudAtlas.BuildFontsAsync(); _ = this.dalamudAtlas.BuildFontsAsync();
SwapChainHelper.BusyWaitForGameDeviceSwapChain(); SwapChainHelper.BusyWaitForGameDeviceSwapChain();
var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC); var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC);
if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED) if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED)
this.gameWindowHandle = swapChainDesc.OutputWindow; this.gameWindowHandle = swapChainDesc.OutputWindow;
try try
{ {
@ -947,7 +969,7 @@ internal partial class InterfaceManager : IInternalDisposableService
switch (this.dalamudConfiguration.SwapChainHookMode) switch (this.dalamudConfiguration.SwapChainHookMode)
{ {
case SwapChainHelper.HookMode.ByteCode: case SwapChainHelper.HookMode.ByteCode:
default: default:
{ {
Log.Information("Hooking using bytecode..."); Log.Information("Hooking using bytecode...");
@ -1128,15 +1150,22 @@ internal partial class InterfaceManager : IInternalDisposableService
WindowSystem.HasAnyWindowSystemFocus = false; WindowSystem.HasAnyWindowSystemFocus = false;
WindowSystem.FocusedWindowSystemNamespace = string.Empty; WindowSystem.FocusedWindowSystemNamespace = string.Empty;
var snap = ImGuiManagedAsserts.GetSnapshot();
if (this.IsDispatchingEvents) if (this.IsDispatchingEvents)
{ {
this.Draw?.Invoke(); try
{
this.Draw?.Invoke();
}
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");
}
Service<NotificationManager>.GetNullable()?.Draw(); Service<NotificationManager>.GetNullable()?.Draw();
} }
ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap);
} }
/// <summary> /// <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 ToastWidget(),
new UiColorWidget(), new UiColorWidget(),
new UldWidget(), new UldWidget(),
new VfsWidget(),
}; };
private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules; private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules;

View file

@ -18,7 +18,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
public class IconBrowserWidget : IDataWindowWidget public class IconBrowserWidget : IDataWindowWidget
{ {
private const int MaxIconId = 250_000; private const int MaxIconId = 250_000;
private Vector2 iconSize = new(64.0f, 64.0f); private Vector2 iconSize = new(64.0f, 64.0f);
private Vector2 editIconSize = new(64.0f, 64.0f); private Vector2 editIconSize = new(64.0f, 64.0f);
@ -126,7 +126,6 @@ public class IconBrowserWidget : IDataWindowWidget
this.valueRange = null; this.valueRange = null;
} }
ImGui.NextColumn(); ImGui.NextColumn();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0)) if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0))
@ -204,7 +203,7 @@ public class IconBrowserWidget : IDataWindowWidget
ImGui.GetColorU32(ImGuiColors.DalamudRed), ImGui.GetColorU32(ImGuiColors.DalamudRed),
iconText); iconText);
} }
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{iconId}\n{exc}".Replace("%", "%%")); ImGui.SetTooltip($"{iconId}\n{exc}".Replace("%", "%%"));
@ -224,7 +223,7 @@ public class IconBrowserWidget : IDataWindowWidget
cursor + ((this.iconSize - textSize) / 2), cursor + ((this.iconSize - textSize) / 2),
color, color,
text); text);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(iconId.ToString()); ImGui.SetTooltip(iconId.ToString());

View file

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

View file

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

View file

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

@ -120,7 +120,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private List<AvailablePluginUpdate> pluginListUpdatable = new(); private List<AvailablePluginUpdate> pluginListUpdatable = new();
private bool hasDevPlugins = false; private bool hasDevPlugins = false;
private bool hasHiddenPlugins = false; private bool hasHiddenPlugins = false;
private string searchText = string.Empty; private string searchText = string.Empty;
private bool isSearchTextPrefilled = false; private bool isSearchTextPrefilled = false;
@ -137,7 +137,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown; private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown;
private string verifiedCheckmarkHoveredPlugin = string.Empty; private string verifiedCheckmarkHoveredPlugin = string.Empty;
private string? staleDalamudNewVersion = null; private string? staleDalamudNewVersion = null;
/// <summary> /// <summary>
@ -215,18 +215,19 @@ internal class PluginInstallerWindow : Window, IDisposable
ProfileOrNot, ProfileOrNot,
SearchScore, SearchScore,
} }
[Flags] [Flags]
private enum PluginHeaderFlags private enum PluginHeaderFlags
{ {
None = 0, None = 0,
IsThirdParty = 1 << 0, IsThirdParty = 1 << 0,
HasTrouble = 1 << 1, HasTrouble = 1 << 1,
UpdateAvailable = 1 << 2, UpdateAvailable = 1 << 2,
IsNew = 1 << 3, MainRepoCrossUpdate = 1 << 3,
IsInstallableOutdated = 1 << 4, IsNew = 1 << 4,
IsOrphan = 1 << 5, IsInstallableOutdated = 1 << 5,
IsTesting = 1 << 6, IsOrphan = 1 << 6,
IsTesting = 1 << 7,
} }
private enum InstalledPluginListFilter private enum InstalledPluginListFilter
@ -236,7 +237,7 @@ internal class PluginInstallerWindow : Window, IDisposable
Updateable, Updateable,
Dev, Dev,
} }
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress || private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
this.updateStatus == OperationStatus.InProgress || this.updateStatus == OperationStatus.InProgress ||
this.enableDisableStatus == OperationStatus.InProgress; this.enableDisableStatus == OperationStatus.InProgress;
@ -282,6 +283,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync(); _ = pluginManager.ReloadPluginMastersAsync();
Service<PluginManager>.Get().ScanDevPlugins();
if (!this.isSearchTextPrefilled) this.searchText = string.Empty; if (!this.isSearchTextPrefilled) this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical; this.sortKind = PluginSortKind.Alphabetical;
@ -304,7 +306,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
if (!t.IsCompletedSuccessfully) if (!t.IsCompletedSuccessfully)
return; return;
var versionInfo = t.Result; var versionInfo = t.Result;
if (versionInfo.AssemblyVersion != Util.GetScmVersion() && if (versionInfo.AssemblyVersion != Util.GetScmVersion() &&
versionInfo.Track != "release" && versionInfo.Track != "release" &&
@ -413,7 +415,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
if (!task.IsFaulted && !task.IsCanceled) if (!task.IsFaulted && !task.IsCanceled)
return true; return true;
var newErrorMessage = state as string; var newErrorMessage = state as string;
if (task.Exception != null) if (task.Exception != null)
@ -438,7 +440,7 @@ internal class PluginInstallerWindow : Window, IDisposable
} }
} }
} }
if (task.IsCanceled) if (task.IsCanceled)
Log.Error("A task was cancelled"); Log.Error("A task was cancelled");
@ -446,14 +448,14 @@ internal class PluginInstallerWindow : Window, IDisposable
return false; return false;
} }
private static void EnsureHaveTestingOptIn(IPluginManifest manifest) private static void EnsureHaveTestingOptIn(IPluginManifest manifest)
{ {
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
if (configuration.PluginTestingOptIns.Any(x => x.InternalName == manifest.InternalName)) if (configuration.PluginTestingOptIns.Any(x => x.InternalName == manifest.InternalName))
return; return;
configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(manifest.InternalName)); configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(manifest.InternalName));
configuration.QueueSave(); configuration.QueueSave();
} }
@ -490,7 +492,7 @@ internal class PluginInstallerWindow : Window, IDisposable
throw new ArgumentOutOfRangeException(nameof(kind), kind, null); throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
} }
} }
private void DrawProgressOverlay() private void DrawProgressOverlay()
{ {
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
@ -733,7 +735,7 @@ internal class PluginInstallerWindow : Window, IDisposable
} }
} }
} }
private void DrawFooter() private void DrawFooter()
{ {
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
@ -754,8 +756,9 @@ internal class PluginInstallerWindow : Window, IDisposable
Service<DalamudInterface>.Get().OpenSettings(); Service<DalamudInterface>.Get().OpenSettings();
} }
// If any dev plugins are installed, allow a shortcut for the /xldev menu item // If any dev plugin locations exist, allow a shortcut for the /xldev menu item
if (this.hasDevPlugins) var hasDevPluginLocations = configuration.DevPluginLoadLocations.Count > 0;
if (hasDevPluginLocations)
{ {
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins)) if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
@ -802,7 +805,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
this.updateStatus = OperationStatus.InProgress; this.updateStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll; this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll;
var toUpdate = this.pluginListUpdatable var toUpdate = this.pluginListUpdatable
.Where(x => x.InstalledPlugin.IsWantedByAnyProfile) .Where(x => x.InstalledPlugin.IsWantedByAnyProfile)
.ToList(); .ToList();
@ -994,7 +997,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.Text(Locs.DeletePluginConfigWarningModal_ExplainTesting()); ImGui.Text(Locs.DeletePluginConfigWarningModal_ExplainTesting());
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
ImGui.Text(Locs.DeletePluginConfigWarningModal_Body(this.deletePluginConfigWarningModalPluginName)); ImGui.Text(Locs.DeletePluginConfigWarningModal_Body(this.deletePluginConfigWarningModalPluginName));
ImGui.Spacing(); ImGui.Spacing();
@ -1264,7 +1267,7 @@ internal class PluginInstallerWindow : Window, IDisposable
plugin.Manifest.RepoUrl == availableManifest.RepoUrl && plugin.Manifest.RepoUrl == availableManifest.RepoUrl &&
!plugin.IsDev); !plugin.IsDev);
// We "consumed" this plugin from the pile and remove it. // We "consumed" this plugin from the pile and remove it.
if (plugin != null) if (plugin != null)
{ {
installedPlugins.Remove(plugin); installedPlugins.Remove(plugin);
@ -1296,7 +1299,7 @@ internal class PluginInstallerWindow : Window, IDisposable
return isHidden; return isHidden;
return !isHidden; return !isHidden;
} }
// Filter out plugins that are not hidden // Filter out plugins that are not hidden
proxies = proxies.Where(IsProxyHidden).ToList(); proxies = proxies.Where(IsProxyHidden).ToList();
@ -1328,14 +1331,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopID(); ImGui.PopID();
} }
// Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one) // Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one)
if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden) if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
{ {
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
} }
} }
private void DrawInstalledPluginList(InstalledPluginListFilter filter) private void DrawInstalledPluginList(InstalledPluginListFilter filter)
{ {
var pluginList = this.pluginListInstalled; var pluginList = this.pluginListInstalled;
@ -1363,7 +1366,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest)) if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest))
continue; continue;
// Find applicable update and manifest, if we have them // Find applicable update and manifest, if we have them
AvailablePluginUpdate? update = null; AvailablePluginUpdate? update = null;
RemotePluginManifest? remoteManifest = null; RemotePluginManifest? remoteManifest = null;
@ -1383,11 +1386,11 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
continue; continue;
} }
this.DrawInstalledPlugin(plugin, i++, remoteManifest, update); this.DrawInstalledPlugin(plugin, i++, remoteManifest, update);
drewAny = true; drewAny = true;
} }
if (!drewAny) if (!drewAny)
{ {
var text = filter switch var text = filter switch
@ -1398,7 +1401,7 @@ internal class PluginInstallerWindow : Window, IDisposable
InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev, InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev,
_ => throw new ArgumentException(null, nameof(filter)), _ => throw new ArgumentException(null, nameof(filter)),
}; };
ImGuiHelpers.ScaledDummy(60); ImGuiHelpers.ScaledDummy(60);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
@ -1490,7 +1493,7 @@ internal class PluginInstallerWindow : Window, IDisposable
foreach (var categoryKind in groupInfo.Categories) foreach (var categoryKind in groupInfo.Categories)
{ {
var categoryInfo = this.categoryManager.CategoryList.First(x => x.CategoryKind == categoryKind); var categoryInfo = this.categoryManager.CategoryList.First(x => x.CategoryKind == categoryKind);
switch (categoryInfo.Condition) switch (categoryInfo.Condition)
{ {
case PluginCategoryManager.CategoryInfo.AppearCondition.None: case PluginCategoryManager.CategoryInfo.AppearCondition.None:
@ -1549,7 +1552,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopFont(); ImGui.PopFont();
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
void DrawLinesCentered(string text) void DrawLinesCentered(string text)
{ {
var lines = text.Split('\n'); var lines = text.Split('\n');
@ -1558,7 +1561,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.CenteredText(line); ImGuiHelpers.CenteredText(line);
} }
} }
var pm = Service<PluginManager>.Get(); var pm = Service<PluginManager>.Get();
if (pm.SafeMode) if (pm.SafeMode)
{ {
@ -1623,7 +1626,7 @@ internal class PluginInstallerWindow : Window, IDisposable
case PluginCategoryManager.CategoryKind.IsTesting: case PluginCategoryManager.CategoryKind.IsTesting:
this.DrawInstalledPluginList(InstalledPluginListFilter.Testing); this.DrawInstalledPluginList(InstalledPluginListFilter.Testing);
break; break;
case PluginCategoryManager.CategoryKind.UpdateablePlugins: case PluginCategoryManager.CategoryKind.UpdateablePlugins:
this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable); this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable);
break; break;
@ -1631,7 +1634,7 @@ internal class PluginInstallerWindow : Window, IDisposable
case PluginCategoryManager.CategoryKind.PluginProfiles: case PluginCategoryManager.CategoryKind.PluginProfiles:
this.profileManagerWidget.Draw(); this.profileManagerWidget.Draw();
break; break;
default: default:
ImGui.TextUnformatted("You found a secret category. Please feel a sense of pride and accomplishment."); ImGui.TextUnformatted("You found a secret category. Please feel a sense of pride and accomplishment.");
break; break;
@ -1652,7 +1655,7 @@ internal class PluginInstallerWindow : Window, IDisposable
case PluginCategoryManager.CategoryKind.PluginChangelogs: case PluginCategoryManager.CategoryKind.PluginChangelogs:
this.DrawChangelogList(false, true); this.DrawChangelogList(false, true);
break; break;
default: default:
ImGui.TextUnformatted("You found a quiet category. Please don't wake it up."); ImGui.TextUnformatted("You found a quiet category. Please don't wake it up.");
break; break;
@ -1979,9 +1982,9 @@ internal class PluginInstallerWindow : Window, IDisposable
var sectionSize = ImGuiHelpers.GlobalScale * 66; var sectionSize = ImGuiHelpers.GlobalScale * 66;
var tapeCursor = ImGui.GetCursorPos(); var tapeCursor = ImGui.GetCursorPos();
ImGui.Separator(); ImGui.Separator();
var startCursor = ImGui.GetCursorPos(); var startCursor = ImGui.GetCursorPos();
if (flags.HasFlag(PluginHeaderFlags.IsTesting)) if (flags.HasFlag(PluginHeaderFlags.IsTesting))
@ -1992,9 +1995,9 @@ internal class PluginInstallerWindow : Window, IDisposable
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
var scroll = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); var scroll = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY());
var adjustedPosition = windowPos + position - scroll; var adjustedPosition = windowPos + position - scroll;
var yellow = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.9f, 0.0f, 0.10f)); var yellow = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.9f, 0.0f, 0.10f));
var numStripes = (int)(size.X / stripeWidth) + (int)(size.Y / skewAmount) + 1; // +1 to cover partial stripe var numStripes = (int)(size.X / stripeWidth) + (int)(size.Y / skewAmount) + 1; // +1 to cover partial stripe
@ -2004,19 +2007,19 @@ internal class PluginInstallerWindow : Window, IDisposable
var x1 = x0 + stripeWidth; var x1 = x0 + stripeWidth;
var y0 = adjustedPosition.Y; var y0 = adjustedPosition.Y;
var y1 = y0 + size.Y; var y1 = y0 + size.Y;
var p0 = new Vector2(x0, y0); var p0 = new Vector2(x0, y0);
var p1 = new Vector2(x1, y0); var p1 = new Vector2(x1, y0);
var p2 = new Vector2(x1 - skewAmount, y1); var p2 = new Vector2(x1 - skewAmount, y1);
var p3 = new Vector2(x0 - skewAmount, y1); var p3 = new Vector2(x0 - skewAmount, y1);
if (i % 2 != 0) if (i % 2 != 0)
continue; continue;
wdl.AddQuadFilled(p0, p1, p2, p3, yellow); wdl.AddQuadFilled(p0, p1, p2, p3, yellow);
} }
} }
DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20); DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20);
} }
@ -2025,7 +2028,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.5f, 0.5f, 0.5f, 0.2f)); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.5f, 0.5f, 0.5f, 0.2f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f)); ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f));
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetCursorPos(tapeCursor); ImGui.SetCursorPos(tapeCursor);
if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y))) if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y)))
@ -2194,7 +2197,7 @@ internal class PluginInstallerWindow : Window, IDisposable
bodyText += " "; bodyText += " ";
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
bodyText += Locs.PluginBody_Outdated_CanNowUpdate; bodyText += "\n" + Locs.PluginBody_Outdated_CanNowUpdate;
else else
bodyText += Locs.PluginBody_Outdated_WaitForUpdate; bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
@ -2217,7 +2220,12 @@ internal class PluginInstallerWindow : Window, IDisposable
else if (plugin is { IsDecommissioned: true, IsThirdParty: true }) else if (plugin is { IsDecommissioned: true, IsThirdParty: true })
{ {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); 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(); ImGui.PopStyleColor();
} }
else if (plugin != null && !plugin.CheckPolicy()) else if (plugin != null && !plugin.CheckPolicy())
@ -2368,11 +2376,11 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
label += Locs.PluginTitleMod_TestingAvailable; label += Locs.PluginTitleMod_TestingAvailable;
} }
var isThirdParty = manifest.SourceRepo.IsThirdParty; var isThirdParty = manifest.SourceRepo.IsThirdParty;
ImGui.PushID($"available{index}{manifest.InternalName}"); ImGui.PushID($"available{index}{manifest.InternalName}");
var flags = PluginHeaderFlags.None; var flags = PluginHeaderFlags.None;
if (isThirdParty) if (isThirdParty)
flags |= PluginHeaderFlags.IsThirdParty; flags |= PluginHeaderFlags.IsThirdParty;
@ -2382,7 +2390,7 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsInstallableOutdated; flags |= PluginHeaderFlags.IsInstallableOutdated;
if (useTesting || manifest.IsTestingExclusive) if (useTesting || manifest.IsTestingExclusive)
flags |= PluginHeaderFlags.IsTesting; flags |= PluginHeaderFlags.IsTesting;
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index)) if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
{ {
if (!wasSeen) if (!wasSeen)
@ -2444,7 +2452,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
} }
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback) if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback && !isOutdated)
{ {
ImGui.SameLine(); ImGui.SameLine();
this.DrawSendFeedbackButton(manifest, false, true); this.DrawSendFeedbackButton(manifest, false, true);
@ -2478,7 +2486,7 @@ internal class PluginInstallerWindow : Window, IDisposable
EnsureHaveTestingOptIn(manifest); EnsureHaveTestingOptIn(manifest);
this.StartInstall(manifest, true); this.StartInstall(manifest, true);
} }
ImGui.Separator(); ImGui.Separator();
} }
@ -2602,7 +2610,10 @@ internal class PluginInstallerWindow : Window, IDisposable
availablePluginUpdate = null; availablePluginUpdate = null;
// Update available // 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; label += Locs.PluginTitleMod_HasUpdate;
} }
@ -2612,7 +2623,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (this.updatedPlugins != null && !plugin.IsDev) if (this.updatedPlugins != null && !plugin.IsDev)
{ {
var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName); var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName);
if (update != default) if (update != null)
{ {
if (update.Status == PluginUpdateStatus.StatusKind.Success) if (update.Status == PluginUpdateStatus.StatusKind.Success)
{ {
@ -2640,8 +2651,8 @@ internal class PluginInstallerWindow : Window, IDisposable
trouble = true; trouble = true;
} }
// Orphaned // Orphaned, if we don't have a cross-repo update
if (plugin.IsOrphaned) if (plugin.IsOrphaned && !isMainRepoCrossUpdate)
{ {
label += Locs.PluginTitleMod_OrphanedError; label += Locs.PluginTitleMod_OrphanedError;
trouble = true; trouble = true;
@ -2670,15 +2681,15 @@ internal class PluginInstallerWindow : Window, IDisposable
string? availableChangelog = null; string? availableChangelog = null;
var didDrawAvailableChangelogInsideCollapsible = false; var didDrawAvailableChangelogInsideCollapsible = false;
if (availablePluginUpdate != default) if (availablePluginUpdate != null)
{ {
availablePluginUpdateVersion = availablePluginUpdateVersion =
availablePluginUpdate.UseTesting ? availablePluginUpdate.UseTesting ?
availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.TestingAssemblyVersion :
availablePluginUpdate.UpdateManifest.AssemblyVersion; availablePluginUpdate.UpdateManifest.AssemblyVersion;
availableChangelog = availableChangelog =
availablePluginUpdate.UseTesting ? availablePluginUpdate.UseTesting ?
availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.TestingChangelog :
availablePluginUpdate.UpdateManifest.Changelog; availablePluginUpdate.UpdateManifest.Changelog;
} }
@ -2688,8 +2699,10 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsThirdParty; flags |= PluginHeaderFlags.IsThirdParty;
if (trouble) if (trouble)
flags |= PluginHeaderFlags.HasTrouble; flags |= PluginHeaderFlags.HasTrouble;
if (availablePluginUpdate != default) if (availablePluginUpdate != null)
flags |= PluginHeaderFlags.UpdateAvailable; flags |= PluginHeaderFlags.UpdateAvailable;
if (isMainRepoCrossUpdate)
flags |= PluginHeaderFlags.MainRepoCrossUpdate;
if (plugin.IsOrphaned) if (plugin.IsOrphaned)
flags |= PluginHeaderFlags.IsOrphan; flags |= PluginHeaderFlags.IsOrphan;
if (plugin.IsTesting) if (plugin.IsTesting)
@ -2724,8 +2737,8 @@ internal class PluginInstallerWindow : Window, IDisposable
var canFeedback = !isThirdParty && var canFeedback = !isThirdParty &&
!plugin.IsDev && !plugin.IsDev &&
!plugin.IsOrphaned && !plugin.IsOrphaned &&
(plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel (plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel ||
|| plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel) && (plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel && hasTestingAvailable)) &&
acceptsFeedback && acceptsFeedback &&
availablePluginUpdate == default; availablePluginUpdate == default;
@ -2762,13 +2775,14 @@ internal class PluginInstallerWindow : Window, IDisposable
var commands = commandManager.Commands var commands = commandManager.Commands
.Where(cInfo => .Where(cInfo =>
cInfo.Value is { ShowInHelp: true } && cInfo.Value is { ShowInHelp: true } &&
commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName) commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName);
.ToArray();
if (commands.Any()) if (commands.Any())
{ {
ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f)); 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}"); ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}");
} }
@ -2835,7 +2849,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
this.DrawInstalledPluginChangelog(applicableChangelog); this.DrawInstalledPluginChangelog(applicableChangelog);
} }
if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins && if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins &&
!availableChangelog.IsNullOrWhitespace() && !availableChangelog.IsNullOrWhitespace() &&
!didDrawAvailableChangelogInsideCollapsible) !didDrawAvailableChangelogInsideCollapsible)
@ -3689,7 +3703,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList(); this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
this.ResortPlugins(); this.ResortPlugins();
} }
this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName)); this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName));
this.UpdateCategoriesOnPluginsChange(); this.UpdateCategoriesOnPluginsChange();
@ -3943,16 +3957,16 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed."); public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed.");
public static string TabBody_SafeMode => Loc.Localize("InstallerSafeMode", "Dalamud is running in Plugin Safe Mode, restart to activate plugins."); public static string TabBody_SafeMode => Loc.Localize("InstallerSafeMode", "Dalamud is running in Plugin Safe Mode, restart to activate plugins.");
public static string TabBody_NoPluginsTesting => Loc.Localize("InstallerNoPluginsTesting", "You aren't testing any plugins at the moment!\nYou can opt in to testing versions in the plugin context menu."); public static string TabBody_NoPluginsTesting => Loc.Localize("InstallerNoPluginsTesting", "You aren't testing any plugins at the moment!\nYou can opt in to testing versions in the plugin context menu.");
public static string TabBody_NoPluginsInstalled => public static string TabBody_NoPluginsInstalled =>
string.Format(Loc.Localize("InstallerNoPluginsInstalled", "You don't have any plugins installed yet!\nYou can install them from the \"{0}\" tab."), PluginCategoryManager.Locs.Category_All); string.Format(Loc.Localize("InstallerNoPluginsInstalled", "You don't have any plugins installed yet!\nYou can install them from the \"{0}\" tab."), PluginCategoryManager.Locs.Category_All);
public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment."); public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings."); public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings.");
#endregion #endregion
#region Search text #region Search text
@ -4014,11 +4028,11 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginContext_TestingOptIn => Loc.Localize("InstallerTestingOptIn", "Receive plugin testing versions"); public static string PluginContext_TestingOptIn => Loc.Localize("InstallerTestingOptIn", "Receive plugin testing versions");
public static string PluginContext_InstallTestingVersion => Loc.Localize("InstallerInstallTestingVersion", "Install testing version"); public static string PluginContext_InstallTestingVersion => Loc.Localize("InstallerInstallTestingVersion", "Install testing version");
public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen"); public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen");
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer"); public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer"); public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer");
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data"); public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
@ -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_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_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."); 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:"); Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable => 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 => 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 => 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 => public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); 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 AutoUpdateBehavior behavior;
private bool checkPeriodically; private bool checkPeriodically;
private bool chatNotification;
private string pickerSearch = string.Empty; private string pickerSearch = string.Empty;
private List<AutoUpdatePreference> autoUpdatePreferences = []; 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"); public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates");
@ -36,15 +37,15 @@ public class SettingsTabAutoUpdates : SettingsTab
"Dalamud can update your plugins automatically, making sure that you always " + "Dalamud can update your plugins automatically, making sure that you always " +
"have the newest features and bug fixes. You can choose when and how auto-updates are run here.")); "have the newest features and bug fixes. You can choose when and how auto-updates are run here."));
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1",
"You can always update your plugins manually by clicking the update button in the plugin list. " + "You can always update your plugins manually by clicking the update button in the plugin list. " +
"You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\".")); "You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\"."));
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2",
"Dalamud will only notify you about updates while you are idle.")); "Dalamud will only notify you about updates while you are idle."));
ImGuiHelpers.ScaledDummy(8); ImGuiHelpers.ScaledDummy(8);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior",
"When the game starts...")); "When the game starts..."));
var behaviorInt = (int)this.behavior; var behaviorInt = (int)this.behavior;
@ -62,20 +63,21 @@ public class SettingsTabAutoUpdates : SettingsTab
"These updates are not reviewed by the Dalamud team and may contain malicious code."); "These updates are not reviewed by the Dalamud team and may contain malicious code.");
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudOrange, warning); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudOrange, warning);
} }
ImGuiHelpers.ScaledDummy(8); 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); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint", 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.")); "Plugins won't update automatically after startup, you will only receive a notification while you are not actively playing."));
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.Separator(); ImGui.Separator();
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOptedIn", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOptedIn",
"Per-plugin overrides")); "Per-plugin overrides"));
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOverrideHint", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOverrideHint",
"Here, you can choose to receive or not to receive updates for specific plugins. " + "Here, you can choose to receive or not to receive updates for specific plugins. " +
"This will override the settings above for the selected plugins.")); "This will override the settings above for the selected plugins."));
@ -83,25 +85,25 @@ public class SettingsTabAutoUpdates : SettingsTab
if (this.autoUpdatePreferences.Count == 0) if (this.autoUpdatePreferences.Count == 0)
{ {
ImGuiHelpers.ScaledDummy(20); ImGuiHelpers.ScaledDummy(20);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{ {
ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2", ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2",
"You don't have auto-update rules for any plugins.")); "You don't have auto-update rules for any plugins."));
} }
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);
} }
else else
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var pic = Service<PluginImageCache>.Get(); var pic = Service<PluginImageCache>.Get();
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
Guid? wantRemovePluginGuid = null; Guid? wantRemovePluginGuid = null;
foreach (var preference in this.autoUpdatePreferences) foreach (var preference in this.autoUpdatePreferences)
{ {
var pmPlugin = Service<PluginManager>.Get().InstalledPlugins var pmPlugin = Service<PluginManager>.Get().InstalledPlugins
@ -120,11 +122,12 @@ public class SettingsTabAutoUpdates : SettingsTab
if (pmPlugin.IsDev) if (pmPlugin.IsDev)
{ {
ImGui.SetCursorPos(cursorBeforeIcon); 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.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
}
} }
ImGui.SameLine(); ImGui.SameLine();
var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}";
@ -147,7 +150,7 @@ public class SettingsTabAutoUpdates : SettingsTab
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
ImGui.TextUnformatted(text); ImGui.TextUnformatted(text);
ImGui.SetCursorPos(before); ImGui.SetCursorPos(before);
} }
@ -166,19 +169,18 @@ public class SettingsTabAutoUpdates : SettingsTab
} }
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250); ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250);
if (ImGui.BeginCombo( using (var combo = ImRaii.Combo($"###autoUpdateBehavior{preference.WorkingPluginId}", OptKindToString(preference.Kind)))
$"###autoUpdateBehavior{preference.WorkingPluginId}",
OptKindToString(preference.Kind)))
{ {
foreach (var kind in Enum.GetValues<AutoUpdatePreference.OptKind>()) if (combo.Success)
{ {
if (ImGui.Selectable(OptKindToString(kind))) foreach (var kind in Enum.GetValues<AutoUpdatePreference.OptKind>())
{ {
preference.Kind = kind; if (ImGui.Selectable(OptKindToString(kind)))
{
preference.Kind = kind;
}
} }
} }
ImGui.EndCombo();
} }
ImGui.SameLine(); ImGui.SameLine();
@ -193,7 +195,7 @@ public class SettingsTabAutoUpdates : SettingsTab
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override")); ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override"));
} }
if (wantRemovePluginGuid != null) if (wantRemovePluginGuid != null)
{ {
this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid); this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid);
@ -205,19 +207,19 @@ public class SettingsTabAutoUpdates : SettingsTab
var id = plugin.EffectiveWorkingPluginId; var id = plugin.EffectiveWorkingPluginId;
if (id == Guid.Empty) if (id == Guid.Empty)
throw new InvalidOperationException("Plugin ID is empty."); throw new InvalidOperationException("Plugin ID is empty.");
this.autoUpdatePreferences.Add(new AutoUpdatePreference(id)); this.autoUpdatePreferences.Add(new AutoUpdatePreference(id));
} }
bool IsPluginDisabled(LocalPlugin plugin) bool IsPluginDisabled(LocalPlugin plugin)
=> this.autoUpdatePreferences.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId); => this.autoUpdatePreferences.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId);
bool IsPluginFiltered(LocalPlugin plugin) bool IsPluginFiltered(LocalPlugin plugin)
=> !plugin.IsDev; => !plugin.IsDev;
var pickerId = DalamudComponents.DrawPluginPicker( var pickerId = DalamudComponents.DrawPluginPicker(
"###autoUpdatePicker", ref this.pickerSearch, OnPluginPicked, IsPluginDisabled, IsPluginFiltered); "###autoUpdatePicker", ref this.pickerSearch, OnPluginPicked, IsPluginDisabled, IsPluginFiltered);
const FontAwesomeIcon addButtonIcon = FontAwesomeIcon.Plus; const FontAwesomeIcon addButtonIcon = FontAwesomeIcon.Plus;
var addButtonText = Loc.Localize("DalamudSettingsAutoUpdateOptInAdd", "Add new override"); var addButtonText = Loc.Localize("DalamudSettingsAutoUpdateOptInAdd", "Add new override");
ImGuiHelpers.CenterCursorFor(ImGuiComponents.GetIconButtonWithTextWidth(addButtonIcon, addButtonText)); ImGuiHelpers.CenterCursorFor(ImGuiComponents.GetIconButtonWithTextWidth(addButtonIcon, addButtonText));
@ -235,20 +237,22 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.chatNotification = configuration.SendUpdateNotificationToChat;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
base.Load(); base.Load();
} }
public override void Save() public override void Save()
{ {
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior; configuration.AutoUpdateBehavior = this.behavior;
configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
base.Save(); base.Save();
} }
} }

View file

@ -39,18 +39,6 @@ public class SettingsTabExperimental : SettingsTab
new GapSettingsEntry(5), 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( new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"), Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
Loc.Localize( Loc.Localize(
@ -66,6 +54,26 @@ public class SettingsTabExperimental : SettingsTab
new DevPluginsSettingsEntry(), 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 GapSettingsEntry(5, true),
new ThirdRepoSettingsEntry(), new ThirdRepoSettingsEntry(),

View file

@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
public class SettingsTabLook : SettingsTab public class SettingsTabLook : SettingsTab
{ {
private static readonly (string, float)[] GlobalUiScalePresets = private static readonly (string, float)[] GlobalUiScalePresets =
{ {
("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f), ("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
("100%##DalamudSettingsGlobalUiScaleReset12", 1f), ("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
@ -107,7 +107,17 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."), Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."),
c => c.IsDocking, c => c.IsDocking,
(v, c) => c.IsDocking = v), (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>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"), 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."), 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."),
@ -125,19 +135,19 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."), Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."),
c => c.ShowTsm, c => c.ShowTsm,
(v, c) => c.ShowTsm = v), (v, c) => c.ShowTsm = v),
new SettingsEntry<bool>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"), Loc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"),
Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."),
c => c.PluginInstallerOpen == PluginInstallerOpenKind.InstalledPlugins, c => c.PluginInstallerOpen == PluginInstallerOpenKind.InstalledPlugins,
(v, c) => c.PluginInstallerOpen = v ? PluginInstallerOpenKind.InstalledPlugins : PluginInstallerOpenKind.AllPlugins), (v, c) => c.PluginInstallerOpen = v ? PluginInstallerOpenKind.InstalledPlugins : PluginInstallerOpenKind.AllPlugins),
new SettingsEntry<bool>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"), Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."), Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."),
c => c.ReduceMotions ?? false, c => c.ReduceMotions ?? false,
(v, c) => c.ReduceMotions = v), (v, c) => c.ReduceMotions = v),
new SettingsEntry<float>( new SettingsEntry<float>(
Loc.Localize("DalamudSettingImeStateIndicatorOpacity", "IME State Indicator Opacity (CJK only)"), Loc.Localize("DalamudSettingImeStateIndicatorOpacity", "IME State Indicator Opacity (CJK only)"),
Loc.Localize("DalamudSettingImeStateIndicatorOpacityHint", "When any of CJK IMEs is in use, the state of IME will be shown with the opacity specified here."), Loc.Localize("DalamudSettingImeStateIndicatorOpacityHint", "When any of CJK IMEs is in use, the state of IME will be shown with the opacity specified here."),

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() private void AddDevPlugin()
{ {
this.devPluginTempLocation = this.devPluginTempLocation.Trim('"');
if (this.devPluginLocations.Any( if (this.devPluginLocations.Any(
r => string.Equals(r.Path, this.devPluginTempLocation, StringComparison.InvariantCultureIgnoreCase))) r => string.Equals(r.Path, this.devPluginTempLocation, StringComparison.InvariantCultureIgnoreCase)))
{ {
@ -210,25 +219,21 @@ public class DevPluginsSettingsEntry : SettingsEntry
"DalamudDevPluginInvalid", "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?"); "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); Task.Delay(5000).ContinueWith(t => this.devPluginLocationAddError = string.Empty);
return;
} }
else else
{ {
this.devPluginLocations.Add( this.devPluginLocations.Add(
new DevPluginLocationSettings new DevPluginLocationSettings
{ {
Path = this.devPluginTempLocation.Replace("\"", string.Empty), Path = this.devPluginTempLocation,
IsEnabled = true, IsEnabled = true,
}); });
this.devPluginLocationsChanged = true; this.devPluginLocationsChanged = true;
this.devPluginTempLocation = string.Empty; this.devPluginTempLocation = string.Empty;
} }
}
public override void PostDraw() // Enable ImGui asserts if a dev plugin is added, if no choice was made prior
{ Service<DalamudConfiguration>.Get().ImGuiAssertsEnabledAtStartup ??= true;
this.fileDialogManager.Draw();
} }
private static bool ValidDevPluginPath(string path)
=> Path.IsPathRooted(path) && Path.GetExtension(path) == ".dll";
} }

View file

@ -50,11 +50,11 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private readonly Lazy<IFontHandle> myFontHandle; private readonly Lazy<IFontHandle> myFontHandle;
private readonly Lazy<IDalamudTextureWrap> shadeTexture; private readonly Lazy<IDalamudTextureWrap> shadeTexture;
private readonly AddonLifecycleEventListener versionStringListener; private readonly AddonLifecycleEventListener versionStringListener;
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new(); private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
private readonly Dictionary<Guid, InOutQuint> moveEasings = new(); private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
private readonly Dictionary<Guid, InOutCubic> logoEasings = new(); private readonly Dictionary<Guid, InOutCubic> logoEasings = new();
private readonly IConsoleVariable<bool> showTsm; private readonly IConsoleVariable<bool> showTsm;
private InOutCubic? fadeOutEasing; private InOutCubic? fadeOutEasing;
@ -62,7 +62,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private State state = State.Hide; private State state = State.Hide;
private int lastLoadedPluginCount = -1; private int lastLoadedPluginCount = -1;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class. /// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class.
/// </summary> /// </summary>
@ -91,7 +91,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
{ {
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true); this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);
this.clientState = clientState; this.clientState = clientState;
this.configuration = configuration; this.configuration = configuration;
this.gameGui = gameGui; this.gameGui = gameGui;
@ -124,7 +124,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
framework.Update += this.FrameworkOnUpdate; framework.Update += this.FrameworkOnUpdate;
this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate);
this.versionStringListener = new AddonLifecycleEventListener(AddonEvent.PreDraw, "_TitleRevision", this.OnVersionStringDraw); this.versionStringListener = new AddonLifecycleEventListener(AddonEvent.PreDraw, "_TitleRevision", this.OnVersionStringDraw);
addonLifecycle.RegisterListener(this.versionStringListener); addonLifecycle.RegisterListener(this.versionStringListener);
this.scopedFinalizer.Add(() => addonLifecycle.UnregisterListener(this.versionStringListener)); this.scopedFinalizer.Add(() => addonLifecycle.UnregisterListener(this.versionStringListener));
@ -136,7 +136,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
Show, Show,
FadeOut, FadeOut,
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether drawing is allowed. /// Gets or sets a value indicating whether drawing is allowed.
/// </summary> /// </summary>
@ -165,7 +165,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
{ {
if (!this.AllowDrawing || !this.showTsm.Value) if (!this.AllowDrawing || !this.showTsm.Value)
return; return;
var scale = ImGui.GetIO().FontGlobalScale; var scale = ImGui.GetIO().FontGlobalScale;
var entries = this.titleScreenMenu.PluginEntries; var entries = this.titleScreenMenu.PluginEntries;
@ -174,7 +174,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
Service<InterfaceManager>.Get().OverrideGameCursor = !hovered; Service<InterfaceManager>.Get().OverrideGameCursor = !hovered;
switch (this.state) switch (this.state)
{ {
case State.Show: case State.Show:
@ -251,7 +251,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.fadeOutEasing.Update(); 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; var i = 0;
foreach (var entry in entries) foreach (var entry in entries)
@ -392,21 +392,19 @@ internal class TitleScreenMenuWindow : Window, IDisposable
if (overrideAlpha) 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 // 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.SetCursorPos(cursor);
ImGui.Text(entry.Name); ImGuiHelpers.SeStringWrapped(
ReadOnlySeString.FromText(entry.Name),
new()
{
FontSize = TargetFontSizePx * ImGui.GetIO().FontGlobalScale,
Edge = true,
Shadow = true,
});
if (overrideAlpha) if (overrideAlpha)
{ {
@ -439,7 +437,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
var addon = (AtkUnitBase*)drawArgs.Addon; var addon = (AtkUnitBase*)drawArgs.Addon;
var textNode = addon->GetTextNodeById(3); var textNode = addon->GetTextNodeById(3);
// look and feel init. should be harmless to set. // look and feel init. should be harmless to set.
textNode->TextFlags |= (byte)TextFlags.MultiLine; textNode->TextFlags |= (byte)TextFlags.MultiLine;
textNode->AlignmentType = AlignmentType.TopLeft; textNode->AlignmentType = AlignmentType.TopLeft;

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 /// <summary>A texture with a backing instance of <see cref="IDalamudTextureWrap"/> that is shared across multiple
/// requesters.</summary> /// requesters.</summary>
/// <remarks> /// <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. /// <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> /// Use <see cref="RentAsync"/> to lock the texture for use in any thread for any duration.</para>
/// </remarks> /// </remarks>

View file

@ -9,7 +9,6 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
@ -713,8 +712,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
ImGui.End(); ImGui.End();
} }
var snapshot = this.Draw is null ? null : ImGuiManagedAsserts.GetSnapshot();
try try
{ {
this.Draw?.InvokeSafely(); this.Draw?.InvokeSafely();
@ -728,10 +725,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
this.hasErrorWindow = true; 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++; this.FrameCount++;
if (DoStats) 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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using CheapLoc; using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET; using ImGuiNET;
using PInvoke; using PInvoke;
namespace Dalamud.Interface.Windowing; namespace Dalamud.Interface.Windowing;
@ -26,7 +31,7 @@ public abstract class Window
private static readonly ModuleLog Log = new("WindowSystem"); private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false; private static bool wasEscPressedLastFrame = false;
private bool internalLastIsOpen = false; private bool internalLastIsOpen = false;
private bool internalIsOpen = false; private bool internalIsOpen = false;
private bool internalIsPinned = false; private bool internalIsPinned = false;
@ -35,15 +40,19 @@ public abstract class Window
private float? internalAlpha = null; private float? internalAlpha = null;
private bool nextFrameBringToFront = false; private bool nextFrameBringToFront = false;
private bool hasInitializedFromPreset = false;
private PresetModel.PresetWindow? presetWindow;
private bool presetDirty = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Window"/> class. /// Initializes a new instance of the <see cref="Window"/> class.
/// </summary> /// </summary>
/// <param name="name">The name/ID of this window. /// <param name="name">The name/ID of this window.
/// If you have multiple windows with the same name, you will need to /// 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>
/// <param name="flags">The <see cref="ImGuiWindowFlags"/> of this window.</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) protected Window(string name, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
{ {
this.WindowName = name; this.WindowName = name;
@ -51,6 +60,33 @@ public abstract class Window
this.ForceMainWindow = forceMainWindow; 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> /// <summary>
/// Gets or sets the namespace of the window. /// Gets or sets the namespace of the window.
/// </summary> /// </summary>
@ -87,7 +123,7 @@ public abstract class Window
/// Gets or sets a value representing the sound effect id to be played when the window is closed. /// Gets or sets a value representing the sound effect id to be played when the window is closed.
/// </summary> /// </summary>
public uint OnCloseSfxId { get; set; } = 24u; public uint OnCloseSfxId { get; set; } = 24u;
/// <summary> /// <summary>
/// Gets or sets the position of this window. /// Gets or sets the position of this window.
/// </summary> /// </summary>
@ -155,7 +191,7 @@ public abstract class Window
/// <summary> /// <summary>
/// Gets or sets a list of available title bar buttons. /// Gets or sets a list of available title bar buttons.
/// ///
/// If <see cref="AllowPinning"/> or <see cref="AllowClickthrough"/> are set to true, and this features is not /// If <see cref="AllowPinning"/> or <see cref="AllowClickthrough"/> are set to true, and this features is not
/// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will /// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will
/// not appear in this collection. If you wish to remove this button, set both of these values to false. /// not appear in this collection. If you wish to remove this button, set both of these values to false.
@ -170,7 +206,7 @@ public abstract class Window
get => this.internalIsOpen; get => this.internalIsOpen;
set => this.internalIsOpen = value; set => this.internalIsOpen = value;
} }
private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough; private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough;
/// <summary> /// <summary>
@ -267,17 +303,16 @@ public abstract class Window
public virtual void Update() public virtual void Update()
{ {
} }
/// <summary> /// <summary>
/// Draw the window via ImGui. /// Draw the window via ImGui.
/// </summary> /// </summary>
/// <param name="configuration">Configuration instance used to check if certain window management features should be enabled.</param> /// <param name="internalDrawFlags">Flags controlling window behavior.</param>
internal void DrawInternal(DalamudConfiguration? configuration) /// <param name="persistence">Handler for window persistence data.</param>
internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence)
{ {
this.PreOpenCheck(); this.PreOpenCheck();
var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
if (!this.IsOpen) if (!this.IsOpen)
{ {
if (this.internalIsOpen != this.internalLastIsOpen) if (this.internalIsOpen != this.internalLastIsOpen)
@ -286,8 +321,9 @@ public abstract class Window
this.OnClose(); this.OnClose();
this.IsFocused = false; this.IsFocused = false;
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnCloseSfxId); if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
} }
return; return;
@ -301,13 +337,16 @@ public abstract class Window
if (hasNamespace) if (hasNamespace)
ImGui.PushID(this.Namespace); ImGui.PushID(this.Namespace);
this.PreHandlePreset(persistence);
if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen) if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen)
{ {
this.internalLastIsOpen = this.internalIsOpen; this.internalLastIsOpen = this.internalIsOpen;
this.OnOpen(); this.OnOpen();
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnOpenSfxId); if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
} }
this.PreDraw(); 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)) 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 // Draw the actual window contents
try try
{ {
@ -355,7 +406,7 @@ public abstract class Window
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) && var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar); !flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) && var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
(configuration?.EnablePluginUiAdditionalOptions ?? true) && internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
flagsApplicableForTitleBarIcons; flagsApplicableForTitleBarIcons;
if (showAdditions) if (showAdditions)
{ {
@ -364,10 +415,10 @@ public abstract class Window
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
{ {
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport(); var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
if (!isAvailable) if (!isAvailable)
ImGui.BeginDisabled(); ImGui.BeginDisabled();
if (this.internalIsClickthrough) if (this.internalIsClickthrough)
ImGui.BeginDisabled(); ImGui.BeginDisabled();
@ -375,36 +426,51 @@ public abstract class Window
{ {
var showAsPinned = this.internalIsPinned || this.internalIsClickthrough; var showAsPinned = this.internalIsPinned || this.internalIsClickthrough;
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned)) if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
{
this.internalIsPinned = 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) if (this.internalIsClickthrough)
ImGui.EndDisabled(); ImGui.EndDisabled();
if (this.AllowClickthrough) 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; var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f)) 100f))
{ {
this.internalAlpha = alpha / 100f; this.internalAlpha = alpha / 100f;
this.presetDirty = true;
} }
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset"))) if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
{ {
this.internalAlpha = null; this.internalAlpha = null;
this.presetDirty = true;
} }
if (isAvailable) if (isAvailable)
{ {
ImGui.TextColored(ImGuiColors.DalamudGrey, ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again to disable clickthrough.")); "Open this menu again by clicking the three dashes to disable clickthrough."));
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionDisclaimer",
"These options may not work for all plugins at the moment."));
} }
else else
{ {
@ -415,7 +481,7 @@ public abstract class Window
if (!isAvailable) if (!isAvailable)
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.EndPopup(); ImGui.EndPopup();
} }
@ -435,6 +501,7 @@ public abstract class Window
Click = _ => Click = _ =>
{ {
this.internalIsClickthrough = false; this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName); ImGui.OpenPopup(additionsPopupName);
}, },
Priority = int.MinValue, Priority = int.MinValue,
@ -457,8 +524,7 @@ public abstract class Window
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var isAllowed = configuration?.IsFocusManagementEnabled ?? false; if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement) && !this.internalIsPinned)
if (isAllowed)
{ {
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE]; var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey) if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
@ -476,6 +542,8 @@ public abstract class Window
this.PostDraw(); this.PostDraw();
this.PostHandlePreset(persistence);
if (hasNamespace) if (hasNamespace)
ImGui.PopID(); ImGui.PopID();
} }
@ -511,7 +579,7 @@ public abstract class Window
{ {
ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value); ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value);
} }
// Manually set alpha takes precedence, if devs don't want that, they should turn it off // Manually set alpha takes precedence, if devs don't want that, they should turn it off
if (this.internalAlpha.HasValue) if (this.internalAlpha.HasValue)
{ {
@ -519,21 +587,65 @@ 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) private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable<TitleBarButton> buttons)
{ {
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false); ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
var fontSize = ImGui.GetFontSize(); var fontSize = ImGui.GetFontSize();
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var padR = 0f; var padR = 0f;
var buttonSize = ImGui.GetFontSize(); var buttonSize = ImGui.GetFontSize();
var numNativeButtons = 0; var numNativeButtons = 0;
if (this.CanShowCloseButton) if (this.CanShowCloseButton)
numNativeButtons++; numNativeButtons++;
if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right) if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right)
numNativeButtons++; numNativeButtons++;
@ -543,15 +655,15 @@ public abstract class Window
// Pad to the left, to get out of the way of the native buttons // Pad to the left, to get out of the way of the native buttons
padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X); padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X);
Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f); Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f);
var numButtons = 0; var numButtons = 0;
bool DrawButton(TitleBarButton button, Vector2 pos) bool DrawButton(TitleBarButton button, Vector2 pos)
{ {
var id = ImGui.GetID($"###CustomTbButton{numButtons}"); var id = ImGui.GetID($"###CustomTbButton{numButtons}");
numButtons++; numButtons++;
var min = pos; var min = pos;
var max = pos + new Vector2(fontSize, fontSize); var max = pos + new Vector2(fontSize, fontSize);
Vector4 bb = new(min.X, min.Y, max.X, max.Y); Vector4 bb = new(min.X, min.Y, max.X, max.Y);
@ -563,12 +675,12 @@ public abstract class Window
{ {
hovered = false; hovered = false;
held = false; held = false;
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves // ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
if (ImGui.IsMouseHoveringRect(min, max)) if (ImGui.IsMouseHoveringRect(min, max))
{ {
hovered = true; hovered = true;
// We can't use ImGui native functions here, because they don't work with clickthrough // We can't use ImGui native functions here, because they don't work with clickthrough
if ((User32.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) if ((User32.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
{ {
@ -581,7 +693,7 @@ public abstract class Window
{ {
pressed = ImGuiNativeAdditions.igButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None); pressed = ImGuiNativeAdditions.igButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None);
} }
if (isClipped) if (isClipped)
return pressed; return pressed;
@ -590,10 +702,10 @@ public abstract class Window
var textCol = ImGui.GetColorU32(ImGuiCol.Text); var textCol = ImGui.GetColorU32(ImGuiCol.Text);
if (hovered || held) if (hovered || held)
drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol); drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol);
var offset = button.IconOffset * ImGuiHelpers.GlobalScale; var offset = button.IconOffset * ImGuiHelpers.GlobalScale;
drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString()); drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString());
if (hovered) if (hovered)
button.ShowTooltip?.Invoke(); button.ShowTooltip?.Invoke();
@ -608,14 +720,14 @@ public abstract class Window
{ {
if (this.internalIsClickthrough && !button.AvailableClickthrough) if (this.internalIsClickthrough && !button.AvailableClickthrough)
return; return;
Vector2 position = new(titleBarRect.Z - padR - buttonSize, titleBarRect.Y + style.FramePadding.Y); Vector2 position = new(titleBarRect.Z - padR - buttonSize, titleBarRect.Y + style.FramePadding.Y);
padR += buttonSize + style.ItemInnerSpacing.X; padR += buttonSize + style.ItemInnerSpacing.X;
if (DrawButton(button, position)) if (DrawButton(button, position))
button.Click?.Invoke(ImGuiMouseButton.Left); button.Click?.Invoke(ImGuiMouseButton.Left);
} }
ImGui.PopClipRect(); ImGui.PopClipRect();
} }
@ -625,7 +737,7 @@ public abstract class Window
public struct WindowSizeConstraints public struct WindowSizeConstraints
{ {
private Vector2 internalMaxSize = new(float.MaxValue); private Vector2 internalMaxSize = new(float.MaxValue);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct. /// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
/// </summary> /// </summary>
@ -637,7 +749,7 @@ public abstract class Window
/// Gets or sets the minimum size of the window. /// Gets or sets the minimum size of the window.
/// </summary> /// </summary>
public Vector2 MinimumSize { get; set; } = new(0); public Vector2 MinimumSize { get; set; } = new(0);
/// <summary> /// <summary>
/// Gets or sets the maximum size of the window. /// Gets or sets the maximum size of the window.
/// </summary> /// </summary>
@ -646,12 +758,12 @@ public abstract class Window
get => this.GetSafeMaxSize(); get => this.GetSafeMaxSize();
set => this.internalMaxSize = value; set => this.internalMaxSize = value;
} }
private Vector2 GetSafeMaxSize() private Vector2 GetSafeMaxSize()
{ {
var currentMin = this.MinimumSize; var currentMin = this.MinimumSize;
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y) if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
return new Vector2(float.MaxValue); return new Vector2(float.MaxValue);
return this.internalMaxSize; return this.internalMaxSize;
@ -667,53 +779,56 @@ public abstract class Window
/// Gets or sets the icon of the button. /// Gets or sets the icon of the button.
/// </summary> /// </summary>
public FontAwesomeIcon Icon { get; set; } public FontAwesomeIcon Icon { get; set; }
/// <summary> /// <summary>
/// Gets or sets a vector by which the position of the icon within the button shall be offset. /// Gets or sets a vector by which the position of the icon within the button shall be offset.
/// Automatically scaled by the global font scale for you. /// Automatically scaled by the global font scale for you.
/// </summary> /// </summary>
public Vector2 IconOffset { get; set; } public Vector2 IconOffset { get; set; }
/// <summary> /// <summary>
/// Gets or sets an action that is called when a tooltip shall be drawn. /// Gets or sets an action that is called when a tooltip shall be drawn.
/// May be null if no tooltip shall be drawn. /// May be null if no tooltip shall be drawn.
/// </summary> /// </summary>
public Action? ShowTooltip { get; set; } public Action? ShowTooltip { get; set; }
/// <summary> /// <summary>
/// Gets or sets an action that is called when the button is clicked. /// Gets or sets an action that is called when the button is clicked.
/// </summary> /// </summary>
public Action<ImGuiMouseButton> Click { get; set; } public Action<ImGuiMouseButton> Click { get; set; }
/// <summary> /// <summary>
/// Gets or sets the priority the button shall be shown in. /// Gets or sets the priority the button shall be shown in.
/// Lower = closer to ImGui default buttons. /// Lower = closer to ImGui default buttons.
/// </summary> /// </summary>
public int Priority { get; set; } public int Priority { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the button shall be clickable /// Gets or sets a value indicating whether or not the button shall be clickable
/// when the respective window is set to clickthrough. /// when the respective window is set to clickthrough.
/// </summary> /// </summary>
public bool AvailableClickthrough { get; set; } public bool AvailableClickthrough { get; set; }
} }
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")]
private static unsafe class ImGuiNativeAdditions private static unsafe class ImGuiNativeAdditions
{ {
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags); public static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags); public static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void* igGetCurrentWindow(); public static extern void* igGetCurrentWindow();
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void igStartMouseMovingWindow(void* window); public static extern void igStartMouseMovingWindow(void* window);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window); 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 System.Linq;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Windowing.Persistence;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
@ -104,20 +104,28 @@ public class WindowSystem
if (hasNamespace) if (hasNamespace)
ImGui.PushID(this.Namespace); 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 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 // 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()) foreach (var window in this.windows.ToArray())
{ {
#if DEBUG #if DEBUG
// Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}"); // Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}");
#endif #endif
var snapshot = ImGuiManagedAsserts.GetSnapshot(); window.DrawInternal(flags, persistence);
window.DrawInternal(config);
var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName;
ImGuiManagedAsserts.ReportProblems(source, snapshot);
} }
var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey); var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey);

View file

@ -1,5 +1,6 @@
using Serilog; using Serilog;
using Serilog.Core; using Serilog.Core;
using Serilog.Core.Enrichers;
using Serilog.Events; using Serilog.Events;
namespace Dalamud.Logging.Internal; namespace Dalamud.Logging.Internal;
@ -11,7 +12,7 @@ public class ModuleLog
{ {
private readonly string moduleName; private readonly string moduleName;
private readonly ILogger moduleLogger; private readonly ILogger moduleLogger;
// FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting. // FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting.
// We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned // We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned
// type, instead returning a (prepared) ILogger appropriately. // type, instead returning a (prepared) ILogger appropriately.
@ -27,6 +28,21 @@ public class ModuleLog
this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName); 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> /// <summary>
/// Log a templated verbose message to the in-game debug log. /// Log a templated verbose message to the in-game debug log.
/// </summary> /// </summary>
@ -160,4 +176,11 @@ public class ModuleLog
messageTemplate: $"[{this.moduleName}] {messageTemplate}", messageTemplate: $"[{this.moduleName}] {messageTemplate}",
values); 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; namespace Dalamud.Plugin;
@ -22,6 +23,47 @@ public interface IExposedPlugin
/// </summary> /// </summary>
bool IsLoaded { get; } 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> /// <summary>
/// Gets the version of the plugin. /// Gets the version of the plugin.
/// </summary> /// </summary>
@ -74,6 +116,30 @@ internal sealed class ExposedPlugin(LocalPlugin plugin) : IExposedPlugin
/// <inheritdoc/> /// <inheritdoc/>
public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasConfigUi ?? false; 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/> /// <inheritdoc/>
public void OpenMainUi() public void OpenMainUi()
{ {

View file

@ -9,6 +9,10 @@ using Dalamud.Console;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions; 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;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.ImGuiNotification.EventArgs;
@ -31,17 +35,17 @@ namespace Dalamud.Plugin.Internal.AutoUpdate;
internal class AutoUpdateManager : IServiceType internal class AutoUpdateManager : IServiceType
{ {
private static readonly ModuleLog Log = new("AUTOUPDATE"); private static readonly ModuleLog Log = new("AUTOUPDATE");
/// <summary> /// <summary>
/// Time we should wait after login to update. /// Time we should wait after login to update.
/// </summary> /// </summary>
private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20); private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
/// <summary> /// <summary>
/// Time we should wait between scheduled update checks. /// Time we should wait between scheduled update checks.
/// </summary> /// </summary>
private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2); private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2);
/// <summary> /// <summary>
/// Time we should wait between scheduled update checks if the user has dismissed the notification, /// Time we should wait between scheduled update checks if the user has dismissed the notification,
/// instead of updating. We don't want to spam the user with notifications. /// instead of updating. We don't want to spam the user with notifications.
@ -56,28 +60,30 @@ internal class AutoUpdateManager : IServiceType
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly PluginManager pluginManager = Service<PluginManager>.Get(); private readonly PluginManager pluginManager = Service<PluginManager>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly NotificationManager notificationManager = Service<NotificationManager>.Get(); private readonly NotificationManager notificationManager = Service<NotificationManager>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudInterface dalamudInterface = Service<DalamudInterface>.Get(); private readonly DalamudInterface dalamudInterface = Service<DalamudInterface>.Get();
private readonly IConsoleVariable<bool> isDryRun; private readonly IConsoleVariable<bool> isDryRun;
private readonly Task<DalamudLinkPayload> openInstallerWindowLinkTask;
private DateTime? loginTime; private DateTime? loginTime;
private DateTime? nextUpdateCheckTime; private DateTime? nextUpdateCheckTime;
private DateTime? unblockedSince; private DateTime? unblockedSince;
private bool hasStartedInitialUpdateThisSession; private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification; private IActiveNotification? updateNotification;
private Task? autoUpdateTask; private Task? autoUpdateTask;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AutoUpdateManager"/> class. /// Initializes a new instance of the <see cref="AutoUpdateManager"/> class.
/// </summary> /// </summary>
@ -92,7 +98,17 @@ internal class AutoUpdateManager : IServiceType
t.Result.Logout += (int type, int code) => this.OnLogout(); t.Result.Logout += (int type, int code) => this.OnLogout();
}); });
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; }); 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); this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false);
console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () => console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () =>
{ {
@ -106,36 +122,36 @@ internal class AutoUpdateManager : IServiceType
return true; return true;
}); });
} }
private enum UpdateListingRestriction private enum UpdateListingRestriction
{ {
Unrestricted, Unrestricted,
AllowNone, AllowNone,
AllowMainRepo, AllowMainRepo,
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session. /// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary> /// </summary>
public bool IsAutoUpdateComplete { get; private set; } public bool IsAutoUpdateComplete { get; private set; }
/// <summary> /// <summary>
/// Gets the time of the next scheduled update check. /// Gets the time of the next scheduled update check.
/// </summary> /// </summary>
public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime; public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime;
/// <summary> /// <summary>
/// Gets the time the auto-update was unblocked. /// Gets the time the auto-update was unblocked.
/// </summary> /// </summary>
public DateTime? UnblockedSince => this.unblockedSince; public DateTime? UnblockedSince => this.unblockedSince;
private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdateBehavior behavior) private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdateBehavior behavior)
{ {
return behavior switch return behavior switch
{ {
// We don't generally allow any updates in this mode, but specific opt-ins. // We don't generally allow any updates in this mode, but specific opt-ins.
AutoUpdateBehavior.None => UpdateListingRestriction.AllowNone, AutoUpdateBehavior.None => UpdateListingRestriction.AllowNone,
// If we're only notifying, I guess it's fine to list all plugins. // If we're only notifying, I guess it's fine to list all plugins.
AutoUpdateBehavior.OnlyNotify => UpdateListingRestriction.Unrestricted, AutoUpdateBehavior.OnlyNotify => UpdateListingRestriction.Unrestricted,
@ -144,7 +160,7 @@ internal class AutoUpdateManager : IServiceType
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null), _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
}; };
} }
private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification) private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
{ {
if (primary ? if (primary ?
@ -179,7 +195,7 @@ internal class AutoUpdateManager : IServiceType
this.updateNotification = null; this.updateNotification = null;
} }
} }
// If we're blocked, we don't do anything. // If we're blocked, we don't do anything.
if (!isUnblocked) if (!isUnblocked)
return; return;
@ -199,16 +215,16 @@ internal class AutoUpdateManager : IServiceType
if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin)) if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin))
{ {
this.hasStartedInitialUpdateThisSession = true; this.hasStartedInitialUpdateThisSession = true;
var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior)); var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior));
if (currentlyUpdatablePlugins.Count == 0) if (currentlyUpdatablePlugins.Count == 0)
{ {
this.IsAutoUpdateComplete = true; this.IsAutoUpdateComplete = true;
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
return; return;
} }
// TODO: This is not 100% what we want... Plugins that are opted-in should be updated regardless of the behavior, // TODO: This is not 100% what we want... Plugins that are opted-in should be updated regardless of the behavior,
// and we should show a notification for the others afterwards. // and we should show a notification for the others afterwards.
if (behavior == AutoUpdateBehavior.OnlyNotify) if (behavior == AutoUpdateBehavior.OnlyNotify)
@ -241,6 +257,7 @@ internal class AutoUpdateManager : IServiceType
Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update"); 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( var updatable = this.GetAvailablePluginUpdates(
DecideUpdateListingRestriction(behavior)); DecideUpdateListingRestriction(behavior));
@ -252,7 +269,7 @@ internal class AutoUpdateManager : IServiceType
{ {
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
Log.Verbose( Log.Verbose(
"Auto update found nothing to do, next update at {Time}", "Auto update found nothing to do, next update at {Time}",
this.nextUpdateCheckTime); this.nextUpdateCheckTime);
} }
}); });
@ -263,13 +280,13 @@ internal class AutoUpdateManager : IServiceType
{ {
if (this.updateNotification != null) if (this.updateNotification != null)
throw new InvalidOperationException("Already showing a notification"); throw new InvalidOperationException("Already showing a notification");
this.updateNotification = this.notificationManager.AddNotification(notification); this.updateNotification = this.notificationManager.AddNotification(notification);
this.updateNotification.Dismiss += _ => this.updateNotification.Dismiss += _ =>
{ {
this.updateNotification = null; this.updateNotification = null;
// Schedule the next update opportunistically for when this closes. // Schedule the next update opportunistically for when this closes.
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
}; };
@ -291,7 +308,7 @@ internal class AutoUpdateManager : IServiceType
{ {
Log.Warning("Auto-update task was canceled"); Log.Warning("Auto-update task was canceled");
} }
this.autoUpdateTask = null; this.autoUpdateTask = null;
this.IsAutoUpdateComplete = true; this.IsAutoUpdateComplete = true;
}); });
@ -321,20 +338,20 @@ internal class AutoUpdateManager : IServiceType
notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name); notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name);
notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins; notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins;
}; };
var pluginStates = (await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress)).ToList(); var pluginStates = (await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress)).ToList();
this.pluginManager.PrintUpdatedPlugins(pluginStates, Loc.Localize("DalamudPluginAutoUpdate", "The following plugins were auto-updated:")); this.pluginManager.PrintUpdatedPlugins(pluginStates, Loc.Localize("DalamudPluginAutoUpdate", "The following plugins were auto-updated:"));
notification.Progress = 1; notification.Progress = 1;
notification.UserDismissable = true; notification.UserDismissable = true;
notification.HardExpiry = DateTime.Now.AddSeconds(30); notification.HardExpiry = DateTime.Now.AddSeconds(30);
notification.DrawActions += _ => notification.DrawActions += _ =>
{ {
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);
DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification); DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification);
}; };
// Update the notification to show the final state // Update the notification to show the final state
if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success))
{ {
@ -342,7 +359,7 @@ internal class AutoUpdateManager : IServiceType
// Janky way to make sure the notification does not change before it's minimized... // Janky way to make sure the notification does not change before it's minimized...
await Task.Delay(500); await Task.Delay(500);
notification.Title = Locs.NotificationTitleUpdatesSuccessful; notification.Title = Locs.NotificationTitleUpdatesSuccessful;
notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized; notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized;
notification.Type = NotificationType.Success; notification.Type = NotificationType.Success;
@ -354,11 +371,11 @@ internal class AutoUpdateManager : IServiceType
notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized; notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized;
notification.Type = NotificationType.Error; notification.Type = NotificationType.Error;
notification.Content = Locs.NotificationContentUpdatesFailed; notification.Content = Locs.NotificationContentUpdatesFailed;
var failedPlugins = pluginStates var failedPlugins = pluginStates
.Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
.Select(x => x.Name).ToList(); .Select(x => x.Name).ToList();
notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins); notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins);
} }
} }
@ -367,7 +384,7 @@ internal class AutoUpdateManager : IServiceType
{ {
if (updatablePlugins.Count == 0) if (updatablePlugins.Count == 0)
return; return;
var notification = this.GetBaseNotification(new Notification var notification = this.GetBaseNotification(new Notification
{ {
Title = Locs.NotificationTitleUpdatesAvailable, Title = Locs.NotificationTitleUpdatesAvailable,
@ -400,16 +417,44 @@ internal class AutoUpdateManager : IServiceType
notification.Dismiss += args => notification.Dismiss += args =>
{ {
if (args.Reason != NotificationDismissReason.Manual) return; if (args.Reason != NotificationDismissReason.Manual) return;
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed; this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed;
Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime); 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) private List<AvailablePluginUpdate> GetAvailablePluginUpdates(UpdateListingRestriction restriction)
{ {
var optIns = this.config.PluginAutoUpdatePreferences.ToArray(); var optIns = this.config.PluginAutoUpdatePreferences.ToArray();
// Get all of our updatable plugins and do some initial filtering that must apply to all plugins. // Get all of our updatable plugins and do some initial filtering that must apply to all plugins.
var updateablePlugins = this.pluginManager.UpdatablePlugins var updateablePlugins = this.pluginManager.UpdatablePlugins
.Where( .Where(
@ -423,14 +468,14 @@ internal class AutoUpdateManager : IServiceType
bool FilterPlugin(AvailablePluginUpdate availablePluginUpdate) bool FilterPlugin(AvailablePluginUpdate availablePluginUpdate)
{ {
var optIn = optIns.FirstOrDefault(x => x.WorkingPluginId == availablePluginUpdate.InstalledPlugin.EffectiveWorkingPluginId); var optIn = optIns.FirstOrDefault(x => x.WorkingPluginId == availablePluginUpdate.InstalledPlugin.EffectiveWorkingPluginId);
// If this is an opt-out, we don't update. // If this is an opt-out, we don't update.
if (optIn is { Kind: AutoUpdatePreference.OptKind.NeverUpdate }) if (optIn is { Kind: AutoUpdatePreference.OptKind.NeverUpdate })
return false; return false;
if (restriction == UpdateListingRestriction.AllowNone && optIn is not { Kind: AutoUpdatePreference.OptKind.AlwaysUpdate }) if (restriction == UpdateListingRestriction.AllowNone && optIn is not { Kind: AutoUpdatePreference.OptKind.AlwaysUpdate })
return false; return false;
if (restriction == UpdateListingRestriction.AllowMainRepo && availablePluginUpdate.InstalledPlugin.IsThirdParty) if (restriction == UpdateListingRestriction.AllowMainRepo && availablePluginUpdate.InstalledPlugin.IsThirdParty)
return false; return false;
@ -442,7 +487,7 @@ internal class AutoUpdateManager : IServiceType
{ {
this.loginTime = DateTime.Now; this.loginTime = DateTime.Now;
} }
private void OnLogout() private void OnLogout()
{ {
this.loginTime = null; this.loginTime = null;
@ -452,7 +497,7 @@ internal class AutoUpdateManager : IServiceType
{ {
var condition = Service<Condition>.Get(); var condition = Service<Condition>.Get();
return this.IsPluginManagerReady() && return this.IsPluginManagerReady() &&
!this.dalamudInterface.IsPluginInstallerOpen && !this.dalamudInterface.IsPluginInstallerOpen &&
condition.OnlyAny(ConditionFlag.NormalConditions, condition.OnlyAny(ConditionFlag.NormalConditions,
ConditionFlag.Jumping, ConditionFlag.Jumping,
ConditionFlag.Mounted, ConditionFlag.Mounted,
@ -469,21 +514,21 @@ internal class AutoUpdateManager : IServiceType
public static string NotificationButtonOpenPluginInstaller => Loc.Localize("AutoUpdateOpenPluginInstaller", "Open installer"); public static string NotificationButtonOpenPluginInstaller => Loc.Localize("AutoUpdateOpenPluginInstaller", "Open installer");
public static string NotificationButtonUpdate => Loc.Localize("AutoUpdateUpdate", "Update"); public static string NotificationButtonUpdate => Loc.Localize("AutoUpdateUpdate", "Update");
public static string NotificationTitleUpdatesAvailable => Loc.Localize("AutoUpdateUpdatesAvailable", "Updates available!"); public static string NotificationTitleUpdatesAvailable => Loc.Localize("AutoUpdateUpdatesAvailable", "Updates available!");
public static string NotificationTitleUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessful", "Updates successful!"); public static string NotificationTitleUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessful", "Updates successful!");
public static string NotificationTitleUpdatingPlugins => Loc.Localize("AutoUpdateUpdatingPlugins", "Updating plugins..."); public static string NotificationTitleUpdatingPlugins => Loc.Localize("AutoUpdateUpdatingPlugins", "Updating plugins...");
public static string NotificationTitleUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailed", "Updates failed!"); public static string NotificationTitleUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailed", "Updates failed!");
public static string NotificationContentUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessfulContent", "All plugins have been updated successfully."); public static string NotificationContentUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessfulContent", "All plugins have been updated successfully.");
public static string NotificationContentUpdatesSuccessfulMinimized => Loc.Localize("AutoUpdateUpdatesSuccessfulContentMinimized", "Plugins updated successfully."); public static string NotificationContentUpdatesSuccessfulMinimized => Loc.Localize("AutoUpdateUpdatesSuccessfulContentMinimized", "Plugins updated successfully.");
public static string NotificationContentUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailedContent", "Some plugins failed to update. Please check the plugin installer for more information."); public static string NotificationContentUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailedContent", "Some plugins failed to update. Please check the plugin installer for more information.");
public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update."); public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update.");
public static string NotificationContentUpdatesAvailable(ICollection<AvailablePluginUpdate> updatablePlugins) public static string NotificationContentUpdatesAvailable(ICollection<AvailablePluginUpdate> updatablePlugins)
@ -497,20 +542,20 @@ internal class AutoUpdateManager : IServiceType
"There are {0} plugins that can be updated:"), "There are {0} plugins that can be updated:"),
updatablePlugins.Count)) updatablePlugins.Count))
+ "\n\n" + string.Join(", ", updatablePlugins.Select(x => x.InstalledPlugin.Manifest.Name)); + "\n\n" + string.Join(", ", updatablePlugins.Select(x => x.InstalledPlugin.Manifest.Name));
public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) public static string NotificationContentUpdatesAvailableMinimized(int numUpdates)
=> numUpdates == 1 ? => numUpdates == 1 ?
Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") : Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates); string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates);
public static string NotificationContentPreparingToUpdate(int numPlugins) public static string NotificationContentPreparingToUpdate(int numPlugins)
=> numPlugins == 1 ? => numPlugins == 1 ?
Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") : Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins); string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins);
public static string NotificationContentUpdating(string name) public static string NotificationContentUpdating(string name)
=> string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name); => string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name);
public static string NotificationContentFailedPlugins(IEnumerable<string> failedPlugins) public static string NotificationContentFailedPlugins(IEnumerable<string> failedPlugins)
=> string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins)); => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins));
} }

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,12 +48,12 @@ internal class PluginManager : IInternalDisposableService
/// </summary> /// </summary>
public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable 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 object pluginListLock = new();
private readonly DirectoryInfo pluginDirectory; private readonly DirectoryInfo pluginDirectory;
private readonly BannedPlugin[]? bannedPlugins; private readonly BannedPlugin[]? bannedPlugins;
private readonly List<LocalPlugin> installedPluginsList = new(); private readonly List<LocalPlugin> installedPluginsList = new();
private readonly List<RemotePluginManifest> availablePluginsList = new(); private readonly List<RemotePluginManifest> availablePluginsList = new();
private readonly List<AvailablePluginUpdate> updatablePluginsList = new(); private readonly List<AvailablePluginUpdate> updatablePluginsList = new();
@ -134,9 +134,6 @@ internal class PluginManager : IInternalDisposableService
this.configuration.PluginTestingOptIns ??= new(); this.configuration.PluginTestingOptIns ??= new();
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
// NET8 CHORE
// this.ApplyPatches();
registerStartupBlocker( registerStartupBlocker(
Task.Run(this.LoadAndStartLoadSyncPlugins), Task.Run(this.LoadAndStartLoadSyncPlugins),
"Waiting for plugins that asked to be loaded before the game."); "Waiting for plugins that asked to be loaded before the game.");
@ -210,7 +207,7 @@ internal class PluginManager : IInternalDisposableService
} }
} }
} }
/// <summary> /// <summary>
/// Gets a copy of the list of all plugins with an available update. /// Gets a copy of the list of all plugins with an available update.
/// </summary> /// </summary>
@ -246,9 +243,9 @@ internal class PluginManager : IInternalDisposableService
public bool ReposReady { get; private set; } public bool ReposReady { get; private set; }
/// <summary> /// <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> /// </summary>
public bool SafeMode { get; init; } public bool SafeMode { get; set; }
/// <summary> /// <summary>
/// Gets the <see cref="PluginConfigurations"/> object used when initializing plugins. /// Gets the <see cref="PluginConfigurations"/> object used when initializing plugins.
@ -264,7 +261,7 @@ internal class PluginManager : IInternalDisposableService
/// Gets or sets a value indicating whether banned plugins will be loaded. /// Gets or sets a value indicating whether banned plugins will be loaded.
/// </summary> /// </summary>
public bool LoadBannedPlugins { get; set; } public bool LoadBannedPlugins { get; set; }
/// <summary> /// <summary>
/// Gets a tracker for plugins that are loading at startup, used to display information to the user. /// Gets a tracker for plugins that are loading at startup, used to display information to the user.
/// </summary> /// </summary>
@ -433,10 +430,6 @@ internal class PluginManager : IInternalDisposableService
await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask())) await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask()))
.SuppressException(); .SuppressException();
} }
// NET8 CHORE
// this.assemblyLocationMonoHook?.Dispose();
// this.assemblyCodeBaseMonoHook?.Dispose();
} }
/// <summary> /// <summary>
@ -486,7 +479,7 @@ internal class PluginManager : IInternalDisposableService
Log.Error("No DLL found for plugin at {Path}", versionDir.FullName); Log.Error("No DLL found for plugin at {Path}", versionDir.FullName);
continue; continue;
} }
var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
if (!manifestFile.Exists) if (!manifestFile.Exists)
{ {
@ -513,7 +506,7 @@ internal class PluginManager : IInternalDisposableService
} }
this.configuration.QueueSave(); this.configuration.QueueSave();
if (versionsDefs.Count == 0) if (versionsDefs.Count == 0)
{ {
Log.Verbose("No versions found for plugin: {Name}", pluginDir.Name); Log.Verbose("No versions found for plugin: {Name}", pluginDir.Name);
@ -559,7 +552,7 @@ internal class PluginManager : IInternalDisposableService
Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName); Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName);
continue; continue;
} }
var manifest = LocalPluginManifest.Load(manifestFile); var manifest = LocalPluginManifest.Load(manifestFile);
if (manifest == null) if (manifest == null)
{ {
@ -768,7 +761,7 @@ internal class PluginManager : IInternalDisposableService
.SelectMany(repo => repo.PluginMaster) .SelectMany(repo => repo.PluginMaster)
.Where(this.IsManifestEligible) .Where(this.IsManifestEligible)
.Where(IsManifestVisible)); .Where(IsManifestVisible));
if (notify) if (notify)
{ {
this.NotifyAvailablePluginsChanged(); this.NotifyAvailablePluginsChanged();
@ -790,7 +783,7 @@ internal class PluginManager : IInternalDisposableService
{ {
if (!setting.IsEnabled) if (!setting.IsEnabled)
continue; continue;
Log.Verbose("Scanning dev plugins at {Path}", setting.Path); Log.Verbose("Scanning dev plugins at {Path}", setting.Path);
if (File.Exists(setting.Path)) if (File.Exists(setting.Path))
@ -817,7 +810,7 @@ internal class PluginManager : IInternalDisposableService
Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName); Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName);
continue; continue;
} }
var manifest = LocalPluginManifest.Load(manifestFile); var manifest = LocalPluginManifest.Load(manifestFile);
if (manifest == null) if (manifest == null)
{ {
@ -861,7 +854,7 @@ internal class PluginManager : IInternalDisposableService
var stream = await this.DownloadPluginAsync(repoManifest, useTesting); var stream = await this.DownloadPluginAsync(repoManifest, useTesting);
return await this.InstallPluginInternalAsync(repoManifest, useTesting, reason, stream, inheritedWorkingPluginId); return await this.InstallPluginInternalAsync(repoManifest, useTesting, reason, stream, inheritedWorkingPluginId);
} }
/// <summary> /// <summary>
/// Remove a plugin. /// Remove a plugin.
/// </summary> /// </summary>
@ -876,9 +869,6 @@ internal class PluginManager : IInternalDisposableService
this.installedPluginsList.Remove(plugin); this.installedPluginsList.Remove(plugin);
} }
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
this.NotifyinstalledPluginsListChanged(); this.NotifyinstalledPluginsListChanged();
this.NotifyAvailablePluginsChanged(); this.NotifyAvailablePluginsChanged();
} }
@ -1058,7 +1048,7 @@ internal class PluginManager : IInternalDisposableService
Status = PluginUpdateStatus.StatusKind.Success, Status = PluginUpdateStatus.StatusKind.Success,
HasChangelog = !metadata.UpdateManifest.Changelog.IsNullOrWhitespace(), HasChangelog = !metadata.UpdateManifest.Changelog.IsNullOrWhitespace(),
}; };
// Check if this plugin is already up to date (=> AvailablePluginUpdate was stale) // Check if this plugin is already up to date (=> AvailablePluginUpdate was stale)
lock (this.installedPluginsList) lock (this.installedPluginsList)
{ {
@ -1085,7 +1075,7 @@ internal class PluginManager : IInternalDisposableService
updateStatus.Status = PluginUpdateStatus.StatusKind.FailedDownload; updateStatus.Status = PluginUpdateStatus.StatusKind.FailedDownload;
return updateStatus; return updateStatus;
} }
// Unload if loaded // Unload if loaded
if (plugin.State is PluginState.Loaded or PluginState.LoadError or PluginState.DependencyResolutionFailed) if (plugin.State is PluginState.Loaded or PluginState.LoadError or PluginState.DependencyResolutionFailed)
{ {
@ -1315,7 +1305,7 @@ internal class PluginManager : IInternalDisposableService
{ {
if (serviceType == typeof(PluginManager)) if (serviceType == typeof(PluginManager))
continue; continue;
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
// Nonetheless, their direct dependencies must be considered. // Nonetheless, their direct dependencies must be considered.
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
@ -1323,19 +1313,19 @@ internal class PluginManager : IInternalDisposableService
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false); var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false);
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
foreach (var scopedDep in dependencies) foreach (var scopedDep in dependencies)
{ {
if (scopedDep == typeof(PluginManager)) if (scopedDep == typeof(PluginManager))
throw new Exception("Scoped plugin services cannot depend on PluginManager."); throw new Exception("Scoped plugin services cannot depend on PluginManager.");
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
yield return scopedDep; yield return scopedDep;
} }
continue; continue;
} }
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true); var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
if (pluginInterfaceAttribute == null) if (pluginInterfaceAttribute == null)
continue; continue;
@ -1346,12 +1336,12 @@ internal class PluginManager : IInternalDisposableService
} }
/// <summary> /// <summary>
/// Check if there are any inconsistencies with our plugins, their IDs, and our profiles. /// Check if there are any inconsistencies with our plugins, their IDs, and our profiles.
/// </summary> /// </summary>
private void ParanoiaValidatePluginsAndProfiles() private void ParanoiaValidatePluginsAndProfiles()
{ {
var seenIds = new List<Guid>(); var seenIds = new List<Guid>();
foreach (var installedPlugin in this.InstalledPlugins) foreach (var installedPlugin in this.InstalledPlugins)
{ {
if (installedPlugin.EffectiveWorkingPluginId == Guid.Empty) if (installedPlugin.EffectiveWorkingPluginId == Guid.Empty)
@ -1362,13 +1352,13 @@ internal class PluginManager : IInternalDisposableService
throw new Exception( throw new Exception(
$"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.EffectiveWorkingPluginId}'"); $"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.EffectiveWorkingPluginId}'");
} }
seenIds.Add(installedPlugin.EffectiveWorkingPluginId); seenIds.Add(installedPlugin.EffectiveWorkingPluginId);
} }
this.profileManager.ParanoiaValidateProfiles(); this.profileManager.ParanoiaValidateProfiles();
} }
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
{ {
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
@ -1401,7 +1391,7 @@ internal class PluginManager : IInternalDisposableService
{ {
var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting}, version={version}, reason={reason})"); Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting}, version={version}, reason={reason})");
// If this plugin is in the default profile for whatever reason, delete the state // If this plugin is in the default profile for whatever reason, delete the state
// If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there,
// or the user removed the plugin manually in which case we don't care // or the user removed the plugin manually in which case we don't care
@ -1435,7 +1425,7 @@ internal class PluginManager : IInternalDisposableService
// If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed // If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed
Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null"); Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null");
} }
// Ensure that we have a testing opt-in for this plugin if we are installing a testing version // Ensure that we have a testing opt-in for this plugin if we are installing a testing version
if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName))
{ {
@ -1544,7 +1534,7 @@ internal class PluginManager : IInternalDisposableService
this.NotifyinstalledPluginsListChanged(); this.NotifyinstalledPluginsListChanged();
return plugin; return plugin;
} }
/// <summary> /// <summary>
/// Load a plugin. /// Load a plugin.
/// </summary> /// </summary>
@ -1572,13 +1562,17 @@ internal class PluginManager : IInternalDisposableService
{ {
Log.Information($"Loading dev plugin {name}"); Log.Information($"Loading dev plugin {name}");
plugin = new LocalDevPlugin(dllFile, manifest); 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 else
{ {
Log.Information($"Loading plugin {name}"); Log.Information($"Loading plugin {name}");
plugin = new LocalPlugin(dllFile, manifest); plugin = new LocalPlugin(dllFile, manifest);
} }
// Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here.
// This will also happen if you are installing a plugin with the installer, and that's intended! // This will also happen if you are installing a plugin with the installer, and that's intended!
// It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will // It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will
@ -1586,7 +1580,7 @@ internal class PluginManager : IInternalDisposableService
if (plugin.EffectiveWorkingPluginId == Guid.Empty) if (plugin.EffectiveWorkingPluginId == Guid.Empty)
throw new Exception("Plugin should have a WorkingPluginId at this point"); throw new Exception("Plugin should have a WorkingPluginId at this point");
this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.EffectiveWorkingPluginId); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.EffectiveWorkingPluginId);
var wantedByAnyProfile = false; var wantedByAnyProfile = false;
// Now, if this is a devPlugin, figure out if we want to load it // Now, if this is a devPlugin, figure out if we want to load it
@ -1602,11 +1596,11 @@ internal class PluginManager : IInternalDisposableService
// We don't know about this plugin, so we don't want to do anything here. // We don't know about this plugin, so we don't want to do anything here.
// The code below will take care of it and add it with the default value. // The code below will take care of it and add it with the default value.
Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName);
// Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active. // Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active.
// Note that this will not add the plugin to the default profile. That's done below in any other case. // Note that this will not add the plugin to the default profile. That's done below in any other case.
wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, false, false); wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, false, false);
// If it is wanted by any other profile, we do want to load it. // If it is wanted by any other profile, we do want to load it.
if (wantedByAnyProfile) if (wantedByAnyProfile)
loadPlugin = true; loadPlugin = true;
@ -1653,12 +1647,12 @@ internal class PluginManager : IInternalDisposableService
#pragma warning disable CS0618 #pragma warning disable CS0618
var defaultState = manifest?.Disabled != true && loadPlugin; var defaultState = manifest?.Disabled != true && loadPlugin;
#pragma warning restore CS0618 #pragma warning restore CS0618
// Plugins that aren't in any profile will be added to the default profile with this call. // Plugins that aren't in any profile will be added to the default profile with this call.
// We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above. // We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above.
wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, defaultState); wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, defaultState);
Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin);
if (loadPlugin) if (loadPlugin)
{ {
try try
@ -1674,8 +1668,6 @@ internal class PluginManager : IInternalDisposableService
} }
catch (InvalidPluginException) catch (InvalidPluginException)
{ {
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw; throw;
} }
catch (BannedPluginException) catch (BannedPluginException)
@ -1721,8 +1713,6 @@ internal class PluginManager : IInternalDisposableService
} }
else else
{ {
// NET8 CHORE
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw; throw;
} }
} }
@ -1746,11 +1736,11 @@ internal class PluginManager : IInternalDisposableService
private void DetectAvailablePluginUpdates() private void DetectAvailablePluginUpdates()
{ {
Log.Debug("Starting plugin update check..."); Log.Debug("Starting plugin update check...");
lock (this.pluginListLock) lock (this.pluginListLock)
{ {
this.updatablePluginsList.Clear(); this.updatablePluginsList.Clear();
foreach (var plugin in this.installedPluginsList) foreach (var plugin in this.installedPluginsList)
{ {
var installedVersion = plugin.IsTesting var installedVersion = plugin.IsTesting
@ -1789,12 +1779,12 @@ internal class PluginManager : IInternalDisposableService
} }
} }
} }
Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count); Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count);
} }
private void NotifyAvailablePluginsChanged() private void NotifyAvailablePluginsChanged()
{ {
this.DetectAvailablePluginUpdates(); this.DetectAvailablePluginUpdates();
this.OnAvailablePluginsChanged?.InvokeSafely(); this.OnAvailablePluginsChanged?.InvokeSafely();
@ -1842,7 +1832,7 @@ internal class PluginManager : IInternalDisposableService
using (Timings.Start("PM Load Sync Plugins")) using (Timings.Start("PM Load Sync Plugins"))
{ {
var loadAllPlugins = Task.Run(this.LoadAllPlugins); var loadAllPlugins = Task.Run(this.LoadAllPlugins);
// We wait for all blocking services and tasks to finish before kicking off the main thread in any mode. // We wait for all blocking services and tasks to finish before kicking off the main thread in any mode.
// This means that we don't want to block here if this stupid thing isn't enabled. // This means that we don't want to block here if this stupid thing isn't enabled.
if (this.configuration.IsResumeGameAfterPluginLoad) if (this.configuration.IsResumeGameAfterPluginLoad)
@ -1861,12 +1851,12 @@ internal class PluginManager : IInternalDisposableService
Log.Error(ex, "Plugin load failed"); Log.Error(ex, "Plugin load failed");
} }
} }
/// <summary> /// <summary>
/// Class representing progress of an update operation. /// Class representing progress of an update operation.
/// </summary> /// </summary>
public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest); public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest);
/// <summary> /// <summary>
/// Simple class that tracks the internal names and public names of plugins that we are planning to load at startup, /// Simple class that tracks the internal names and public names of plugins that we are planning to load at startup,
/// and are still actively loading. /// and are still actively loading.
@ -1876,12 +1866,12 @@ internal class PluginManager : IInternalDisposableService
private readonly Dictionary<string, string> internalToPublic = new(); private readonly Dictionary<string, string> internalToPublic = new();
private readonly ConcurrentBag<string> allInternalNames = new(); private readonly ConcurrentBag<string> allInternalNames = new();
private readonly ConcurrentBag<string> finishedInternalNames = new(); private readonly ConcurrentBag<string> finishedInternalNames = new();
/// <summary> /// <summary>
/// Gets a value indicating the total load progress. /// Gets a value indicating the total load progress.
/// </summary> /// </summary>
public float Progress => (float)this.finishedInternalNames.Count / this.allInternalNames.Count; public float Progress => (float)this.finishedInternalNames.Count / this.allInternalNames.Count;
/// <summary> /// <summary>
/// Calculate a set of internal names that are still pending. /// Calculate a set of internal names that are still pending.
/// </summary> /// </summary>
@ -1892,7 +1882,7 @@ internal class PluginManager : IInternalDisposableService
pending.ExceptWith(this.finishedInternalNames); pending.ExceptWith(this.finishedInternalNames);
return pending; return pending;
} }
/// <summary> /// <summary>
/// Track a new plugin. /// Track a new plugin.
/// </summary> /// </summary>
@ -1903,7 +1893,7 @@ internal class PluginManager : IInternalDisposableService
this.internalToPublic[internalName] = publicName; this.internalToPublic[internalName] = publicName;
this.allInternalNames.Add(internalName); this.allInternalNames.Add(internalName);
} }
/// <summary> /// <summary>
/// Mark a plugin as finished loading. /// Mark a plugin as finished loading.
/// </summary> /// </summary>
@ -1912,7 +1902,7 @@ internal class PluginManager : IInternalDisposableService
{ {
this.finishedInternalNames.Add(internalName); this.finishedInternalNames.Add(internalName);
} }
/// <summary> /// <summary>
/// Get the public name for a given internal name. /// Get the public name for a given internal name.
/// </summary> /// </summary>
@ -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); 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

@ -30,7 +30,7 @@ internal class LocalPlugin : IAsyncDisposable
#pragma warning disable SA1401 #pragma warning disable SA1401
protected LocalPluginManifest manifest; protected LocalPluginManifest manifest;
#pragma warning restore SA1401 #pragma warning restore SA1401
private static readonly ModuleLog Log = new("LOCALPLUGIN"); private static readonly ModuleLog Log = new("LOCALPLUGIN");
private readonly FileInfo manifestFile; private readonly FileInfo manifestFile;
@ -281,7 +281,7 @@ internal class LocalPlugin : IAsyncDisposable
case PluginState.Unloaded: case PluginState.Unloaded:
if (this.instance is not null) if (this.instance is not null)
{ {
throw new InvalidPluginOperationException( throw new InternalPluginStateException(
"Plugin should have been unloaded but instance is not cleared"); "Plugin should have been unloaded but instance is not cleared");
} }
@ -314,7 +314,7 @@ internal class LocalPlugin : IAsyncDisposable
this.State = PluginState.Loading; this.State = PluginState.Loading;
Log.Information($"Loading {this.DllFile.Name}"); Log.Information($"Loading {this.DllFile.Name}");
this.EnsureLoader(); this.EnsureLoader();
if (this.DllFile.DirectoryName != null && if (this.DllFile.DirectoryName != null &&
@ -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.dalamudInterface = new(this, reason);
this.serviceScope = ioc.GetScope(); this.serviceScope = ioc.GetScope();
@ -413,9 +409,11 @@ internal class LocalPlugin : IAsyncDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
this.State = PluginState.LoadError; // 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. // If a precondition fails, don't record it as an error, as it isn't really.
if (ex is PluginPreconditionFailedException) if (ex is PluginPreconditionFailedException)
Log.Warning(ex.Message); Log.Warning(ex.Message);
else else
@ -476,7 +474,10 @@ internal class LocalPlugin : IAsyncDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
this.State = PluginState.UnloadError; // 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); Log.Error(ex, "Error while unloading {PluginName}", this.InternalName);
throw; throw;
@ -509,9 +510,6 @@ internal class LocalPlugin : IAsyncDisposable
var startInfo = Service<Dalamud>.Get().StartInfo; var startInfo = Service<Dalamud>.Get().StartInfo;
var manager = Service<PluginManager>.Get(); var manager = Service<PluginManager>.Get();
if (startInfo.NoLoadPlugins)
return false;
if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty) if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty)
return false; return false;
@ -555,7 +553,7 @@ internal class LocalPlugin : IAsyncDisposable
/// </summary> /// </summary>
/// <param name="reason">Why it should be saved.</param> /// <param name="reason">Why it should be saved.</param>
protected void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); protected void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason);
/// <summary> /// <summary>
/// Called before a plugin is reloaded. /// Called before a plugin is reloaded.
/// </summary> /// </summary>
@ -594,7 +592,7 @@ internal class LocalPlugin : IAsyncDisposable
// but plugins may load other versions of assemblies that Dalamud depends on. // but plugins may load other versions of assemblies that Dalamud depends on.
config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false)); config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false));
config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false)); config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false));
// Pin Lumina since we expose it as an API surface. Before anyone removes this again, please see #1598. // Pin Lumina since we expose it as an API surface. Before anyone removes this again, please see #1598.
// Changes to Lumina should be upstreamed if feasible, and if there is a desire to re-add unpinned Lumina we // Changes to Lumina should be upstreamed if feasible, and if there is a desire to re-add unpinned Lumina we
// will need to put this behind some kind of feature flag somewhere. // will need to put this behind some kind of feature flag somewhere.
@ -606,7 +604,7 @@ internal class LocalPlugin : IAsyncDisposable
{ {
if (this.loader != null) if (this.loader != null)
return; return;
try try
{ {
this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);

View file

@ -4,7 +4,7 @@ namespace Dalamud.Utility;
/// Utility class for marking something to be changed for API 11, for ease of lookup. /// Utility class for marking something to be changed for API 11, for ease of lookup.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.All, Inherited = false)] [AttributeUsage(AttributeTargets.All, Inherited = false)]
internal sealed class Api11ToDoAttribute : Attribute internal sealed class Api12ToDoAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Marks that this should be made internal. /// Marks that this should be made internal.
@ -12,11 +12,11 @@ internal sealed class Api11ToDoAttribute : Attribute
public const string MakeInternal = "Make internal."; public const string MakeInternal = "Make internal.";
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Api11ToDoAttribute"/> class. /// Initializes a new instance of the <see cref="Api12ToDoAttribute"/> class.
/// </summary> /// </summary>
/// <param name="what">The explanation.</param> /// <param name="what">The explanation.</param>
/// <param name="what2">The explanation 2.</param> /// <param name="what2">The explanation 2.</param>
public Api11ToDoAttribute(string what, string what2 = "") public Api12ToDoAttribute(string what, string what2 = "")
{ {
_ = what; _ = what;
_ = what2; _ = 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> /// <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> /// <returns><c>this</c> for method chaining.</returns>
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)] [Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
[Api11ToDo("Remove")] [Api12ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString) => public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); 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> /// <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> /// <returns><c>this</c> for method chaining.</returns>
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)] [Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
[Api11ToDo("Remove")] [Api12ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString) => public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); 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.Game.ClientState.Objects.Types;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Support; using Dalamud.Support;
using ImGuiNET; using ImGuiNET;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -28,8 +29,6 @@ using Windows.Win32.Storage.FileSystem;
using Windows.Win32.System.Memory; using Windows.Win32.System.Memory;
using Windows.Win32.System.Ole; using Windows.Win32.System.Ole;
using Dalamud.Interface.Utility.Raii;
using static TerraFX.Interop.Windows.Windows; using static TerraFX.Interop.Windows.Windows;
using Win32_PInvoke = Windows.Win32.PInvoke; using Win32_PInvoke = Windows.Win32.PInvoke;
@ -66,7 +65,6 @@ public static class Util
private static readonly Type GenericSpanType = typeof(Span<>); private static readonly Type GenericSpanType = typeof(Span<>);
private static string? scmVersionInternal; private static string? scmVersionInternal;
private static string? gitHashInternal; private static string? gitHashInternal;
private static int? gitCommitCountInternal;
private static string? gitHashClientStructsInternal; private static string? gitHashClientStructsInternal;
private static ulong moduleStartAddr; private static ulong moduleStartAddr;
@ -78,58 +76,6 @@ public static class Util
public static string AssemblyVersion { get; } = public static string AssemblyVersion { get; } =
Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); 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> /// <summary>
/// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return /// 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 /// the <c>git describe</c> output for this build, which will be a raw version if this is a stable build or an
@ -139,11 +85,11 @@ public static class Util
public static string GetScmVersion() public static string GetScmVersion()
{ {
if (scmVersionInternal != null) return scmVersionInternal; if (scmVersionInternal != null) return scmVersionInternal;
var asm = typeof(Util).Assembly; var asm = typeof(Util).Assembly;
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>(); var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value
?? asm.GetName().Version!.ToString(); ?? asm.GetName().Version!.ToString();
} }
@ -853,7 +799,7 @@ public static class Util
// ignore // ignore
} }
} }
/// <summary> /// <summary>
/// Print formatted IGameObject Information to ImGui. /// Print formatted IGameObject Information to ImGui.
/// </summary> /// </summary>
@ -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; const int batchSize = 20;
if (spanobj.Length > batchSize) if (spanobj.Length > batchSize)
{ {
@ -1221,6 +1168,7 @@ public static class Util
ImGui.TextDisabled($"[0x{offset.Value:X}]"); ImGui.TextDisabled($"[0x{offset.Value:X}]");
ImGui.SameLine(); ImGui.SameLine();
} }
ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{f.FieldType.Name}"); 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; log << std::format(L"Dump at: {}", dumpPath.wstring()) << std::endl;
else else
log << std::format(L"Dump error: {}", dumpError) << std::endl; 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; log << L"\n" << stackTrace << std::endl;
if (pProgressDialog) if (pProgressDialog)

View file

@ -1,13 +1,21 @@
<!-- Shared settings for all Dalamud projects. --> <!-- Shared settings for all Dalamud projects. -->
<Project> <Project>
<PropertyGroup Label="Target"> <PropertyGroup Label="Target">
<TargetFramework>net9.0-windows</TargetFramework> <TargetFramework>net9.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms> <Platforms>x64</Platforms>
<LangVersion>13.0</LangVersion> <LangVersion>13.0</LangVersion>
</PropertyGroup> </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"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)tools\BannedSymbols.txt" /> <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"/> <img src="https://raw.githubusercontent.com/goatcorp/DalamudAssets/master/UIRes/logo.png" alt="Dalamud" width="200"/>
</p> </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! ## 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. 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 ## Plugin development
Dalamud features a growing API for in-game plugin development with game data and chat access and overlays. 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. 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* (C#) | Core API, game bindings, plugin framework |
| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features | | *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> <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. ##### 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 TestProjectDir => RootDirectory / "Dalamud.Test";
AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj"; 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; AbsolutePath ArtifactsDirectory => RootDirectory / "bin" / Configuration;
private static AbsolutePath LibraryDirectory => RootDirectory / "lib"; private static AbsolutePath LibraryDirectory => RootDirectory / "lib";
@ -59,9 +67,54 @@ public class DalamudBuild : NukeBuild
DotNetTasks.DotNetRestore(s => s DotNetTasks.DotNetRestore(s => s
.SetProjectFile(Solution)); .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 => _ => _ Target CompileDalamud => _ => _
.DependsOn(Restore) .DependsOn(Restore)
.DependsOn(CompileImGuiNatives)
.Executes(() => .Executes(() =>
{ {
DotNetTasks.DotNetBuild(s => 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 // 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... // TODO: This fails every build after this because of redefinitions...
if (IsDocsBuild) // if (IsDocsBuild)
{ // {
Log.Warning("Building for documentation, emitting compiler generated files. This can cause issues on Windows due to path-length limitations"); // Log.Warning("Building for documentation, emitting compiler generated files. This can cause issues on Windows due to path-length limitations");
s = s // s = s
.SetProperty("IsDocsBuild", "true"); // .SetProperty("IsDocsBuild", "true");
} // }
return s; return s;
}); });
@ -138,6 +191,21 @@ public class DalamudBuild : NukeBuild
Target Clean => _ => _ Target Clean => _ => _
.Executes(() => .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 DotNetTasks.DotNetClean(s => s
.SetProject(DalamudProjectFile) .SetProject(DalamudProjectFile)
.SetConfiguration(Configuration)); .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