diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 821f20b..feecf19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.x.x' + dotnet-version: | + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud @@ -37,7 +39,7 @@ jobs: - name: Archive run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Glamourer/bin/Release/* diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 4435e39..5639c7b 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,13 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.x.x' + dotnet-version: | + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud @@ -37,7 +39,7 @@ jobs: - name: Archive run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Glamourer/bin/Debug/* diff --git a/Glamourer.Api b/Glamourer.Api index a20d4ab..5b6730d 160000 --- a/Glamourer.Api +++ b/Glamourer.Api @@ -1 +1 @@ -Subproject commit a20d4ab1811a7f66918afab9b41ec723f75054f5 +Subproject commit 5b6730d46f17bdd02a441e23e2141576cf7acf53 diff --git a/Glamourer.sln b/Glamourer.sln index 254f8e4..e2915d5 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -6,7 +6,10 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .github\workflows\release.yml = .github\workflows\release.yml + Glamourer\Glamourer.json = Glamourer\Glamourer.json repo.json = repo.json + .github\workflows\test_release.yml = .github\workflows\test_release.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}" @@ -27,30 +30,30 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU - {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Glamourer/Api/ApiHelpers.cs b/Glamourer/Api/ApiHelpers.cs index cf67912..14cff3b 100644 --- a/Glamourer/Api/ApiHelpers.cs +++ b/Glamourer/Api/ApiHelpers.cs @@ -1,48 +1,58 @@ using Glamourer.Api.Enums; using Glamourer.Designs; -using Glamourer.GameData; using Glamourer.State; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; using Penumbra.String; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.Api; -public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService +public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService { [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - internal IEnumerable FindExistingStates(string actorName) + internal IEnumerable FindExistingStates(string actorName, ushort worldId = ushort.MaxValue) { if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString)) yield break; - foreach (var state in stateManager.Values.Where(state - => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)) - yield return state; + if (worldId == WorldId.AnyWorld.Id) + { + foreach (var state in stateManager.Values.Where(state + => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)) + yield return state; + } + else + { + var identifier = actors.CreatePlayer(byteString, worldId); + if (stateManager.TryGetValue(identifier, out var state)) + yield return state; + } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] internal GlamourerApiEc FindExistingState(int objectIndex, out ActorState? state) { - var actor = objects[objectIndex]; + var actor = objects.Objects[objectIndex]; var identifier = actor.GetIdentifier(actors); if (!identifier.IsValid) { state = null; return GlamourerApiEc.ActorNotFound; } - stateManager.TryGetValue(identifier, out state); + + stateManager.TryGetValue(identifier, out state); return GlamourerApiEc.Success; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] internal ActorState? FindState(int objectIndex) { - var actor = objects[objectIndex]; + var actor = objects.Objects[objectIndex]; var identifier = actor.GetIdentifier(actors); if (identifier.IsValid && stateManager.GetOrCreate(identifier, actor, out var state)) return state; @@ -54,12 +64,10 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags) => (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch { - ApplyFlag.Equipment => design.TemporarilyRestrictApplication(EquipFlagExtensions.All, 0, CrestExtensions.All, 0), - ApplyFlag.Customization => design.TemporarilyRestrictApplication(0, CustomizeFlagExtensions.All, 0, - CustomizeParameterExtensions.All), - ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(EquipFlagExtensions.All, - CustomizeFlagExtensions.All, CrestExtensions.All, CustomizeParameterExtensions.All), - _ => design.TemporarilyRestrictApplication(0, 0, 0, 0), + ApplyFlag.Equipment => design.TemporarilyRestrictApplication(ApplicationCollection.Equipment), + ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.Customizations), + ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.All), + _ => design.TemporarilyRestrictApplication(ApplicationCollection.None), }; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -75,10 +83,8 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM if (objectName.Length == 0 || !ByteString.FromString(objectName, out var byteString)) return []; - objects.Update(); - return stateManager.Values.Where(state => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString) - .Concat(objects.Identifiers + .Concat(objects .Where(kvp => kvp.Key is { IsValid: true, Type: IdentifierType.Player } && kvp.Key.PlayerName == byteString) .SelectWhere(kvp => { @@ -114,7 +120,10 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM { sb.Append(arguments[2 * i]); sb.Append(" = "); - sb.Append(arguments[2 * i + 1]); + if (arguments[2 * i + 1] is IEnumerable e) + sb.Append($"[{string.Join(',', e)}]"); + else + sb.Append(arguments[2 * i + 1]); sb.Append(", "); } diff --git a/Glamourer/Api/DesignsApi.cs b/Glamourer/Api/DesignsApi.cs index ee49bd5..9b48ade 100644 --- a/Glamourer/Api/DesignsApi.cs +++ b/Glamourer/Api/DesignsApi.cs @@ -2,15 +2,32 @@ using Glamourer.Api.Enums; using Glamourer.Designs; using Glamourer.State; +using Newtonsoft.Json.Linq; using OtterGui.Services; namespace Glamourer.Api; -public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager stateManager) : IGlamourerApiDesigns, IApiService +public class DesignsApi( + ApiHelpers helpers, + DesignManager designs, + StateManager stateManager, + DesignFileSystem fileSystem, + DesignColors color, + DesignConverter converter) + : IGlamourerApiDesigns, IApiService { public Dictionary GetDesignList() => designs.Designs.ToDictionary(d => d.Identifier, d => d.Name.Text); + public Dictionary GetDesignListExtended() + => fileSystem.ToDictionary(kvp => kvp.Key.Identifier, + kvp => (kvp.Key.Name.Text, kvp.Value.FullName(), color.GetColor(kvp.Key), kvp.Key.QuickDesign)); + + public (string DisplayName, string FullPath, uint DisplayColor, bool ShowInQdb) GetExtendedDesignData(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } d + ? (d.Name.Text, fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : d.Name.Text, color.GetColor(d), d.QuickDesign) + : (string.Empty, string.Empty, 0, false); + public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags) { var args = ApiHelpers.Args("Design", designId, "Index", objectIndex, "Key", key, "Flags", flags); @@ -33,7 +50,7 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0); + ResetMaterials: !once && key != 0, IsFinal: true); using var restrict = ApiHelpers.Restrict(design, flags); stateManager.ApplyDesign(state, design, settings); @@ -66,4 +83,56 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager return ApiHelpers.Return(GlamourerApiEc.Success, args); } + + public (GlamourerApiEc, Guid) AddDesign(string designInput, string name) + { + var args = ApiHelpers.Args("DesignData", designInput, "Name", name); + + if (converter.FromBase64(designInput, true, true, out _) is not { } designBase) + try + { + var jObj = JObject.Parse(designInput); + designBase = converter.FromJObject(jObj, true, true); + if (designBase is null) + return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Failure parsing data for AddDesign due to\n{ex}"); + return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty); + } + + try + { + var design = designBase is Design d + ? designs.CreateClone(d, name, true) + : designs.CreateClone(designBase, name, true); + return (ApiHelpers.Return(GlamourerApiEc.Success, args), design.Identifier); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Unknown error creating design via IPC:\n{ex}"); + return (ApiHelpers.Return(GlamourerApiEc.UnknownError, args), Guid.Empty); + } + } + + public GlamourerApiEc DeleteDesign(Guid designId) + { + var args = ApiHelpers.Args("DesignId", designId); + if (designs.Designs.ByIdentifier(designId) is not { } design) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + designs.Delete(design); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public string? GetDesignBase64(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } design + ? converter.ShareBase64(design) + : null; + + public JObject? GetDesignJObject(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } design + ? converter.ShareJObject(design) + : null; } diff --git a/Glamourer/Api/GlamourerApi.cs b/Glamourer/Api/GlamourerApi.cs index b9e21fd..85f873a 100644 --- a/Glamourer/Api/GlamourerApi.cs +++ b/Glamourer/Api/GlamourerApi.cs @@ -3,14 +3,17 @@ using OtterGui.Services; namespace Glamourer.Api; -public class GlamourerApi(DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService +public class GlamourerApi(Configuration config, DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService { public const int CurrentApiVersionMajor = 1; - public const int CurrentApiVersionMinor = 2; + public const int CurrentApiVersionMinor = 7; public (int Major, int Minor) ApiVersion => (CurrentApiVersionMajor, CurrentApiVersionMinor); + public bool AutoReloadGearEnabled + => config.AutoRedrawEquipOnChanges; + public IGlamourerApiDesigns Designs => designs; diff --git a/Glamourer/Api/IpcProviders.cs b/Glamourer/Api/IpcProviders.cs index 638d228..f120db3 100644 --- a/Glamourer/Api/IpcProviders.cs +++ b/Glamourer/Api/IpcProviders.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Glamourer.Api.Api; using Glamourer.Api.Helpers; using OtterGui.Services; +using Glamourer.Api.Enums; namespace Glamourer.Api; @@ -12,7 +13,7 @@ public sealed class IpcProviders : IDisposable, IApiService private readonly EventProvider _disposedProvider; private readonly EventProvider _initializedProvider; - public IpcProviders(DalamudPluginInterface pi, IGlamourerApi api) + public IpcProviders(IDalamudPluginInterface pi, IGlamourerApi api) { _disposedProvider = IpcSubscribers.Disposed.Provider(pi); _initializedProvider = IpcSubscribers.Initialized.Provider(pi); @@ -21,29 +22,50 @@ public sealed class IpcProviders : IDisposable, IApiService new FuncProvider<(int Major, int Minor)>(pi, "Glamourer.ApiVersions", () => api.ApiVersion), // backward compatibility new FuncProvider(pi, "Glamourer.ApiVersion", () => api.ApiVersion.Major), // backward compatibility IpcSubscribers.ApiVersion.Provider(pi, api), + IpcSubscribers.AutoReloadGearEnabled.Provider(pi, api), IpcSubscribers.GetDesignList.Provider(pi, api.Designs), + IpcSubscribers.GetDesignListExtended.Provider(pi, api.Designs), + IpcSubscribers.GetExtendedDesignData.Provider(pi, api.Designs), IpcSubscribers.ApplyDesign.Provider(pi, api.Designs), IpcSubscribers.ApplyDesignName.Provider(pi, api.Designs), + IpcSubscribers.AddDesign.Provider(pi, api.Designs), + IpcSubscribers.DeleteDesign.Provider(pi, api.Designs), + IpcSubscribers.GetDesignBase64.Provider(pi, api.Designs), + IpcSubscribers.GetDesignJObject.Provider(pi, api.Designs), IpcSubscribers.SetItem.Provider(pi, api.Items), IpcSubscribers.SetItemName.Provider(pi, api.Items), - + // backward compatibility + new FuncProvider(pi, IpcSubscribers.Legacy.SetItemV2.Label, + (a, b, c, d, e, f) => (int)api.Items.SetItem(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)), + new FuncProvider(pi, IpcSubscribers.Legacy.SetItemName.Label, + (a, b, c, d, e, f) => (int)api.Items.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)), + IpcSubscribers.SetBonusItem.Provider(pi, api.Items), + IpcSubscribers.SetBonusItemName.Provider(pi, api.Items), + IpcSubscribers.SetMetaState.Provider(pi, api.Items), + IpcSubscribers.SetMetaStateName.Provider(pi, api.Items), IpcSubscribers.GetState.Provider(pi, api.State), IpcSubscribers.GetStateName.Provider(pi, api.State), IpcSubscribers.GetStateBase64.Provider(pi, api.State), IpcSubscribers.GetStateBase64Name.Provider(pi, api.State), IpcSubscribers.ApplyState.Provider(pi, api.State), IpcSubscribers.ApplyStateName.Provider(pi, api.State), + IpcSubscribers.ReapplyState.Provider(pi, api.State), + IpcSubscribers.ReapplyStateName.Provider(pi, api.State), IpcSubscribers.RevertState.Provider(pi, api.State), IpcSubscribers.RevertStateName.Provider(pi, api.State), IpcSubscribers.UnlockState.Provider(pi, api.State), + IpcSubscribers.CanUnlock.Provider(pi, api.State), IpcSubscribers.UnlockStateName.Provider(pi, api.State), + IpcSubscribers.DeletePlayerState.Provider(pi, api.State), IpcSubscribers.UnlockAll.Provider(pi, api.State), IpcSubscribers.RevertToAutomation.Provider(pi, api.State), IpcSubscribers.RevertToAutomationName.Provider(pi, api.State), + IpcSubscribers.AutoReloadGearChanged.Provider(pi, api.State), IpcSubscribers.StateChanged.Provider(pi, api.State), IpcSubscribers.StateChangedWithType.Provider(pi, api.State), + IpcSubscribers.StateFinalized.Provider(pi, api.State), IpcSubscribers.GPoseChanged.Provider(pi, api.State), ]; _initializedProvider.Invoke(); @@ -59,3 +81,5 @@ public sealed class IpcProviders : IDisposable, IApiService _disposedProvider.Dispose(); } } + + diff --git a/Glamourer/Api/ItemsApi.cs b/Glamourer/Api/ItemsApi.cs index cda1980..ac971c9 100644 --- a/Glamourer/Api/ItemsApi.cs +++ b/Glamourer/Api/ItemsApi.cs @@ -11,9 +11,9 @@ namespace Glamourer.Api; public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager stateManager) : IGlamourerApiItems, IApiService { - public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, byte stain, uint key, ApplyFlag flags) + public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, ApplyFlag flags) { - var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", itemId, "Stain", stain, "Key", key, "Flags", flags); + var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags); if (!ResolveItem(slot, itemId, out var item)) return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); @@ -27,14 +27,14 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); - stateManager.ChangeEquip(state, (EquipSlot)slot, item, stain, settings); + stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings); ApiHelpers.Lock(state, key, flags); return GlamourerApiEc.Success; } - public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, byte stain, uint key, ApplyFlag flags) + public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, ApplyFlag flags) { - var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", itemId, "Stain", stain, "Key", key, "Flags", flags); + var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags); if (!ResolveItem(slot, itemId, out var item)) return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); @@ -53,10 +53,134 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager continue; anyUnlocked = true; - stateManager.ChangeEquip(state, (EquipSlot)slot, item, stain, settings); + stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings); ApiHelpers.Lock(state, key, flags); } + if (!anyFound) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags); + if (!ResolveBonusItem(slot, bonusItemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.ModelData.IsHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings); + ApiHelpers.Lock(state, key, flags); + return GlamourerApiEc.Success; + } + + public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags); + if (!ResolveBonusItem(slot, bonusItemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + var anyHuman = false; + var anyFound = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + anyFound = true; + if (!state.ModelData.IsHuman) + continue; + + anyHuman = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings); + ApiHelpers.Lock(state, key, flags); + } + + if (!anyFound) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags); + if (types == 0) + return ApiHelpers.Return(GlamourerApiEc.InvalidState, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.ModelData.IsHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + // Grab MetaIndices from attached flags, and update the states. + var indices = types.ToIndices(); + foreach (var index in indices) + { + stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual); + ApiHelpers.Lock(state, key, flags); + } + + return GlamourerApiEc.Success; + } + + public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags); + if (types == 0) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + var anyHuman = false; + var anyFound = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + anyFound = true; + if (!state.ModelData.IsHuman) + continue; + + anyHuman = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + // update all MetaStates for this ActorState + foreach (var index in types.ToIndices()) + { + stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual); + ApiHelpers.Lock(state, key, flags); + } + } + if (!anyFound) return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); @@ -79,4 +203,15 @@ public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager item = itemManager.Resolve(slot, id); return item.Valid; } + + private bool ResolveBonusItem(ApiBonusSlot apiSlot, ulong itemId, out EquipItem item) + { + var slot = apiSlot switch + { + ApiBonusSlot.Glasses => BonusItemFlag.Glasses, + _ => BonusItemFlag.Unknown, + }; + + return itemManager.IsBonusItemValid(slot, (BonusItemId)itemId, out item); + } } diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 203f1b0..4ce9c01 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -2,52 +2,59 @@ using Glamourer.Api.Enums; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Events; -using Glamourer.Interop.Structs; using Glamourer.State; using Newtonsoft.Json.Linq; using OtterGui.Services; using Penumbra.GameData.Interop; -using ObjectManager = Glamourer.Interop.ObjectManager; +using Penumbra.GameData.Structs; using StateChanged = Glamourer.Events.StateChanged; namespace Glamourer.Api; public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable { - private readonly ApiHelpers _helpers; - private readonly StateManager _stateManager; - private readonly DesignConverter _converter; - private readonly Configuration _config; - private readonly AutoDesignApplier _autoDesigns; - private readonly ObjectManager _objects; - private readonly StateChanged _stateChanged; - private readonly GPoseService _gPose; + private readonly ApiHelpers _helpers; + private readonly StateManager _stateManager; + private readonly DesignConverter _converter; + private readonly AutoDesignApplier _autoDesigns; + private readonly ActorObjectManager _objects; + private readonly AutoRedrawChanged _autoRedraw; + private readonly StateChanged _stateChanged; + private readonly StateFinalized _stateFinalized; + private readonly GPoseService _gPose; public StateApi(ApiHelpers helpers, StateManager stateManager, DesignConverter converter, - Configuration config, AutoDesignApplier autoDesigns, - ObjectManager objects, + ActorObjectManager objects, + AutoRedrawChanged autoRedraw, StateChanged stateChanged, + StateFinalized stateFinalized, GPoseService gPose) { - _helpers = helpers; - _stateManager = stateManager; - _converter = converter; - _config = config; - _autoDesigns = autoDesigns; - _objects = objects; - _stateChanged = stateChanged; - _gPose = gPose; - _stateChanged.Subscribe(OnStateChange, Events.StateChanged.Priority.GlamourerIpc); - _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc); + _helpers = helpers; + _stateManager = stateManager; + _converter = converter; + _autoDesigns = autoDesigns; + _objects = objects; + _autoRedraw = autoRedraw; + _stateChanged = stateChanged; + _stateFinalized = stateFinalized; + _gPose = gPose; + _autoRedraw.Subscribe(OnAutoRedrawChange, AutoRedrawChanged.Priority.StateApi); + _stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); + _stateFinalized.Subscribe(OnStateFinalized, Events.StateFinalized.Priority.StateApi); + _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.StateApi); } public void Dispose() { - _stateChanged.Unsubscribe(OnStateChange); + _autoRedraw.Unsubscribe(OnAutoRedrawChange); + _stateChanged.Unsubscribe(OnStateChanged); + _stateFinalized.Unsubscribe(OnStateFinalized); _gPose.Unsubscribe(OnGPoseChange); } @@ -119,6 +126,48 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable return ApiHelpers.Return(GlamourerApiEc.Success, args); } + public GlamourerApiEc ReapplyState(int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); + if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (state is null) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + Reapply(_objects.Objects[objectIndex], state, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc ReapplyStateName(string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags); + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyReapplied = false; + foreach (var state in states) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyReapplied = true; + anyReapplied |= Reapply(state, key, flags) is GlamourerApiEc.Success; + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyReapplied) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags) { var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); @@ -176,6 +225,20 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable return ApiHelpers.Return(GlamourerApiEc.Success, args); } + public GlamourerApiEc CanUnlock(int objectIndex, uint key, out bool isLocked, out bool canUnlock) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key); + isLocked = false; + canUnlock = true; + if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + if (state is null) + return ApiHelpers.Return(GlamourerApiEc.Success, args); + isLocked = state.IsLocked; + canUnlock = state.CanUnlock(key); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + public GlamourerApiEc UnlockStateName(string playerName, uint key) { var args = ApiHelpers.Args("Name", playerName, "Key", key); @@ -198,6 +261,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable return ApiHelpers.Return(GlamourerApiEc.Success, args); } + public GlamourerApiEc DeletePlayerState(string playerName, ushort worldId, uint key) + { + var args = ApiHelpers.Args("Name", playerName, "World", worldId, "Key", key); + var states = _helpers.FindExistingStates(playerName).ToList(); + if (states.Count is 0) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + var anyLocked = false; + foreach (var state in states) + { + if (state.CanUnlock(key)) + _stateManager.DeleteState(state.Identifier); + else + anyLocked = true; + } + + return ApiHelpers.Return(anyLocked + ? GlamourerApiEc.InvalidKey + : GlamourerApiEc.Success, args); + } + public int UnlockAll(uint key) => _stateManager.Values.Count(state => state.Unlock(key)); @@ -213,7 +297,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable if (!state.CanUnlock(key)) return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); - RevertToAutomation(_objects[objectIndex], state, key, flags); + RevertToAutomation(_objects.Objects[objectIndex], state, key, flags); return ApiHelpers.Return(GlamourerApiEc.Success, args); } @@ -247,33 +331,47 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable return ApiHelpers.Return(GlamourerApiEc.Success, args); } - public event Action? StateChanged; - public event Action? StateChangedWithType; - public event Action? GPoseChanged; + public event Action? AutoReloadGearChanged; + public event Action? StateChanged; + public event Action? StateChangedWithType; + public event Action? StateFinalized; + public event Action? GPoseChanged; private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0); + ResetMaterials: !once && key != 0, IsFinal: true); _stateManager.ApplyDesign(state, design, settings); ApiHelpers.Lock(state, key, flags); } + private GlamourerApiEc Reapply(ActorState state, uint key, ApplyFlag flags) + { + if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid) + return GlamourerApiEc.ActorNotFound; + + foreach (var actor in actors.Objects) + Reapply(actor, state, key, flags); + + return GlamourerApiEc.Success; + } + + private void Reapply(Actor actor, ActorState state, uint key, ApplyFlag flags) + { + var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual; + _stateManager.ReapplyState(actor, state, false, source, true); + ApiHelpers.Lock(state, key, flags); + } + private void Revert(ActorState state, uint key, ApplyFlag flags) { - var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed; + var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual; switch (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) { - case ApplyFlag.Equipment: - _stateManager.ResetEquip(state, source, key); - break; - case ApplyFlag.Customization: - _stateManager.ResetCustomize(state, source, key); - break; - case ApplyFlag.Equipment | ApplyFlag.Customization: - _stateManager.ResetState(state, source, key); - break; + case ApplyFlag.Equipment: _stateManager.ResetEquip(state, source, key); break; + case ApplyFlag.Customization: _stateManager.ResetCustomize(state, source, key); break; + case ApplyFlag.Equipment | ApplyFlag.Customization: _stateManager.ResetState(state, source, key, true); break; } ApiHelpers.Lock(state, key, flags); @@ -281,7 +379,6 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private GlamourerApiEc RevertToAutomation(ActorState state, uint key, ApplyFlag flags) { - _objects.Update(); if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid) return GlamourerApiEc.ActorNotFound; @@ -294,8 +391,8 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void RevertToAutomation(Actor actor, ActorState state, uint key, ApplyFlag flags) { var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed; - _autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, out var forcedRedraw); - _stateManager.ReapplyState(actor, state, forcedRedraw, source); + _autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source); ApiHelpers.Lock(state, key, flags); } @@ -307,7 +404,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable if (!state.CanUnlock(key)) return (GlamourerApiEc.InvalidKey, null); - return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.AllWithConfig(_config))); + return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.All)); } private (GlamourerApiEc, string?) ConvertBase64(ActorState? state, uint key) @@ -321,17 +418,18 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable version = DesignConverter.Version; return state switch { - string s => _converter.FromBase64(s, (flags & ApplyFlag.Equipment) != 0, (flags & ApplyFlag.Customization) != 0, out version), - JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Equipment) != 0, (flags & ApplyFlag.Customization) != 0), + string s => _converter.FromBase64(s, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0, out version), + JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0), _ => null, }; } - private void OnGPoseChange(bool gPose) - => GPoseChanged?.Invoke(gPose); + private void OnAutoRedrawChange(bool autoReload) + => AutoReloadGearChanged?.Invoke(autoReload); - private void OnStateChange(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, object? _5) + private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { + Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]"); if (StateChanged != null) foreach (var actor in actors.Objects) StateChanged.Invoke(actor.Address); @@ -340,4 +438,15 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable foreach (var actor in actors.Objects) StateChangedWithType.Invoke(actor.Address, type); } + + private void OnStateFinalized(StateFinalizationType type, ActorData actors) + { + Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (StateFinalized != null) + foreach (var actor in actors.Objects) + StateFinalized.Invoke(actor.Address, type); + } + + private void OnGPoseChange(bool gPose) + => GPoseChanged?.Invoke(gPose); } diff --git a/Glamourer/Automation/ApplicationType.cs b/Glamourer/Automation/ApplicationType.cs index 12dac50..f72c93f 100644 --- a/Glamourer/Automation/ApplicationType.cs +++ b/Glamourer/Automation/ApplicationType.cs @@ -1,4 +1,5 @@ -using Glamourer.Designs; +using Glamourer.Api.Enums; +using Glamourer.Designs; using Glamourer.GameData; using Penumbra.GameData.Enums; @@ -28,8 +29,7 @@ public static class ApplicationTypeExtensions (ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), ]; - public static (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat( - this ApplicationType type, IDesignStandIn designStandIn) + public static ApplicationCollection Collection(this ApplicationType type) { var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0) | (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0) @@ -37,16 +37,22 @@ public static class ApplicationTypeExtensions | (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0); var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0; var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0; - var crestFlag = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0; - var metaFlag = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0) + var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0; + var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0) | (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0) | (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0); + var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0; - if (designStandIn is not DesignBase design) - return (equipFlags, customizeFlags, crestFlag, parameterFlags, metaFlag); + return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags); + } - return (equipFlags & design!.ApplyEquip, customizeFlags & design.ApplyCustomize, crestFlag & design.ApplyCrest, - parameterFlags & design.ApplyParameters, metaFlag & design.ApplyMeta); + public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn) + { + if(designStandIn is not DesignBase design) + return type.Collection(); + var ret = type.Collection().Restrict(design.Application); + ret.CustomizeRaw = ret.CustomizeRaw.FixApplication(design.CustomizeSet); + return ret; } public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 9fc8ca7..e31fb16 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -61,6 +61,6 @@ public class AutoDesign return ret; } - public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat() + public ApplicationCollection ApplyWhat() => Type.ApplyWhat(Design); } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index f2ac1b3..a61a004 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -11,29 +11,28 @@ using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.Automation; public sealed class AutoDesignApplier : IDisposable { - private readonly Configuration _config; - private readonly AutoDesignManager _manager; - private readonly StateManager _state; - private readonly JobService _jobs; - private readonly EquippedGearset _equippedGearset; - private readonly ActorManager _actors; - private readonly AutomationChanged _event; - private readonly ObjectManager _objects; - private readonly WeaponLoading _weapons; - private readonly HumanModelList _humans; - private readonly DesignMerger _designMerger; - private readonly IClientState _clientState; + private readonly Configuration _config; + private readonly AutoDesignManager _manager; + private readonly StateManager _state; + private readonly JobService _jobs; + private readonly EquippedGearset _equippedGearset; + private readonly ActorManager _actors; + private readonly AutomationChanged _event; + private readonly ActorObjectManager _objects; + private readonly WeaponLoading _weapons; + private readonly HumanModelList _humans; + private readonly DesignMerger _designMerger; + private readonly IClientState _clientState; private readonly JobChangeState _jobChangeState; public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors, - AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, + AutomationChanged @event, ActorObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState) { _config = config; @@ -55,6 +54,15 @@ public sealed class AutoDesignApplier : IDisposable _equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier); } + public void OnEnableAutoDesignsChanged(bool value) + { + if (value) + return; + + foreach (var state in _state.Values) + state.Sources.RemoveFixedDesignSources(); + } + public void Dispose() { _weapons.Unsubscribe(OnWeaponLoading); @@ -77,7 +85,7 @@ public sealed class AutoDesignApplier : IDisposable { case EquipSlot.MainHand: { - if (_jobChangeState.TryGetValue(current.Type, actor.Job, out var data)) + if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data)) { Glamourer.Log.Verbose( $"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); @@ -89,7 +97,7 @@ public sealed class AutoDesignApplier : IDisposable } case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand(): { - if (_jobChangeState.TryGetValue(current.Type, actor.Job, out var data)) + if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data)) { Glamourer.Log.Verbose( $"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); @@ -145,16 +153,15 @@ public sealed class AutoDesignApplier : IDisposable if (newSet is not { Enabled: true }) return; - _objects.Update(); foreach (var id in newSet.Identifiers) { if (_objects.TryGetValue(id, out var data)) { if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, out var forcedRedraw); + Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); foreach (var actor in data.Objects) - _state.ReapplyState(actor, forcedRedraw,StateSource.Fixed); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); } } else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) @@ -164,8 +171,8 @@ public sealed class AutoDesignApplier : IDisposable var specificId = actor.GetIdentifier(_actors); if (_state.GetOrCreate(specificId, actor, out var state)) { - Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, out var forcedRedraw); - _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); + Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); } } } @@ -212,22 +219,21 @@ public sealed class AutoDesignApplier : IDisposable var respectManual = state.LastJob == newJob.Id; state.LastJob = actor.Job; - Reduce(actor, state, set, respectManual, true, out var forcedRedraw); + Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw); _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); } - public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, out bool forcedRedraw) + public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw) { forcedRedraw = false; if (!_config.EnableAutoDesigns) return; - if (!GetPlayerSet(identifier, out var set)) - return; - if (reset) _state.ResetState(state, StateSource.Game); - Reduce(actor, state, set, false, false, out forcedRedraw); + + if (GetPlayerSet(identifier, out var set)) + Reduce(actor, state, set, false, false, forcedNew, out forcedRedraw); } public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state) @@ -235,9 +241,6 @@ public sealed class AutoDesignApplier : IDisposable AutoDesignSet set; if (!_state.TryGetValue(identifier, out state)) { - if (!_config.EnableAutoDesigns) - return false; - if (!GetPlayerSet(identifier, out set!)) return false; @@ -254,11 +257,12 @@ public sealed class AutoDesignApplier : IDisposable var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange; if (!respectManual) _state.ResetState(state, StateSource.Game); - Reduce(actor, state, set, respectManual, false, out _); + Reduce(actor, state, set, respectManual, false, false, out _); return true; } - private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, out bool forcedRedraw) + private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, bool newApplication, + out bool forcedRedraw) { if (set.BaseState is AutoDesignSet.Base.Game) { @@ -277,12 +281,27 @@ public sealed class AutoDesignApplier : IDisposable } forcedRedraw = false; - if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) + if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) + return; + + if (actor.IsTransformed) return; var mergedDesign = _designMerger.Merge( - set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design.AllLinks.Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))), + set.Designs.Where(d => d.IsActive(actor)) + .SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))), state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods); + + if (_objects.IsInGPose && actor.IsGPoseOrCutscene) + { + mergedDesign.ResetTemporarySettings = false; + mergedDesign.AssociatedMods.Clear(); + } + else if (set.ResetTemporarySettings) + { + mergedDesign.ResetTemporarySettings = true; + } + _state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false)); forcedRedraw = mergedDesign.ForcedRedraw; } @@ -290,18 +309,31 @@ public sealed class AutoDesignApplier : IDisposable /// Get world-specific first and all-world afterward. private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) { + if (!_config.EnableAutoDesigns) + { + set = null; + return false; + } + switch (identifier.Type) { case IdentifierType.Player: if (_manager.EnabledSets.TryGetValue(identifier, out set)) return true; - identifier = _actors.CreatePlayer(identifier.PlayerName, ushort.MaxValue); + identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld); return _manager.EnabledSets.TryGetValue(identifier, out set); case IdentifierType.Retainer: case IdentifierType.Npc: return _manager.EnabledSets.TryGetValue(identifier, out set); case IdentifierType.Owned: + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + + identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId); + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId); return _manager.EnabledSets.TryGetValue(identifier, out set); default: @@ -326,7 +358,7 @@ public sealed class AutoDesignApplier : IDisposable var respectManual = prior == id; NewGearsetId = id; - Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, out var forcedRedraw); + Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw); NewGearsetId = -1; foreach (var actor in data.Objects) _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 1b95f73..7a4511b 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Special; using Glamourer.Events; using Glamourer.Interop; @@ -9,6 +10,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; @@ -233,6 +235,22 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase)); } + public void ChangeResetSettings(int whichSet, bool newValue) + { + if (whichSet >= _data.Count || whichSet < 0) + return; + + var set = _data[whichSet]; + if (newValue == set.ResetTemporarySettings) + return; + + var old = set.ResetTemporarySettings; + set.ResetTemporarySettings = newValue; + Save(); + Glamourer.Log.Debug($"Changed resetting of temporary settings of set {whichSet + 1} from {old} to {newValue}."); + _event.Invoke(AutomationChanged.Type.ChangedTemporarySettingsReset, set, newValue); + } + public void AddDesign(AutoDesignSet set, IDesignStandIn design) { var newDesign = new AutoDesign() @@ -443,8 +461,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos var set = new AutoDesignSet(name, group) { - Enabled = obj["Enabled"]?.ToObject() ?? false, - BaseState = obj["BaseState"]?.ToObject() ?? AutoDesignSet.Base.Current, + Enabled = obj["Enabled"]?.ToObject() ?? false, + ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject() ?? false, + BaseState = obj["BaseState"]?.ToObject() ?? AutoDesignSet.Base.Current, }; if (set.Enabled) @@ -569,12 +588,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos IdentifierType.Player => true, IdentifierType.Retainer => true, IdentifierType.Npc => true, + IdentifierType.Owned => true, _ => false, }; if (!validType) { - group = Array.Empty(); + group = []; return false; } @@ -600,8 +620,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos ? ActorIdentifier.RetainerType.Mannequin : ActorIdentifier.RetainerType.Bell).CreatePermanent(), ], - IdentifierType.Npc => CreateNpcs(_actors, identifier), - _ => [], + IdentifierType.Npc => CreateNpcs(_actors, identifier), + IdentifierType.Owned => CreateNpcs(_actors, identifier), + _ => [], }; static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) @@ -615,12 +636,11 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos }; return table.Where(kvp => kvp.Value == name) .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, - identifier.Kind, - kvp.Key)).ToArray(); + identifier.Kind, kvp.Key)).ToArray(); } } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) return; diff --git a/Glamourer/Automation/AutoDesignSet.cs b/Glamourer/Automation/AutoDesignSet.cs index adaa355..f8987af 100644 --- a/Glamourer/Automation/AutoDesignSet.cs +++ b/Glamourer/Automation/AutoDesignSet.cs @@ -10,7 +10,8 @@ public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List[] ValidSortModes = - { + [ ISortMode.FoldersFirst, ISortMode.Lexicographical, new DesignFileSystem.CreationDate(), @@ -157,7 +178,7 @@ public class Configuration : IPluginConfiguration, ISavable ISortMode.InverseFoldersLast, ISortMode.InternalOrder, ISortMode.InverseInternalOrder, - }; + ]; } /// Convert SortMode Types to their name. diff --git a/Glamourer/DesignPanelFlag.cs b/Glamourer/DesignPanelFlag.cs new file mode 100644 index 0000000..f9465d9 --- /dev/null +++ b/Glamourer/DesignPanelFlag.cs @@ -0,0 +1,96 @@ +using Glamourer.Designs; +using Dalamud.Bindings.ImGui; +using OtterGui.Text; +using OtterGui.Text.EndObjects; + +namespace Glamourer; + +[Flags] +public enum DesignPanelFlag : uint +{ + Customization = 0x0001, + Equipment = 0x0002, + AdvancedCustomizations = 0x0004, + AdvancedDyes = 0x0008, + AppearanceDetails = 0x0010, + DesignDetails = 0x0020, + ModAssociations = 0x0040, + DesignLinks = 0x0080, + ApplicationRules = 0x0100, + DebugData = 0x0200, +} + +public static class DesignPanelFlagExtensions +{ + public static ReadOnlySpan ToName(this DesignPanelFlag flag) + => flag switch + { + DesignPanelFlag.Customization => "Customization"u8, + DesignPanelFlag.Equipment => "Equipment"u8, + DesignPanelFlag.AdvancedCustomizations => "Advanced Customization"u8, + DesignPanelFlag.AdvancedDyes => "Advanced Dyes"u8, + DesignPanelFlag.DesignDetails => "Design Details"u8, + DesignPanelFlag.ApplicationRules => "Application Rules"u8, + DesignPanelFlag.ModAssociations => "Mod Associations"u8, + DesignPanelFlag.DesignLinks => "Design Links"u8, + DesignPanelFlag.DebugData => "Debug Data"u8, + DesignPanelFlag.AppearanceDetails => "Appearance Details"u8, + _ => ""u8, + }; + + public static CollapsingHeader Header(this DesignPanelFlag flag, Configuration config) + { + if (config.HideDesignPanel.HasFlag(flag)) + return new CollapsingHeader() + { + Disposed = true, + }; + + var expand = config.AutoExpandDesignPanel.HasFlag(flag); + return ImUtf8.CollapsingHeaderId(flag.ToName(), expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); + } + + public static void DrawTable(ReadOnlySpan label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action setterHide, + Action setterExpand) + { + var checkBoxWidth = Math.Max(ImGui.GetFrameHeight(), ImUtf8.CalcTextSize("Expand"u8).X); + var textWidth = ImUtf8.CalcTextSize(DesignPanelFlag.AdvancedCustomizations.ToName()).X; + var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * ImGui.GetStyle().CellPadding.X + 2 * ImGui.GetStyle().WindowPadding.X + 2 * ImGui.GetStyle().FrameBorderSize; + using var table = ImUtf8.Table(label, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders, new Vector2(tableSize, 6 * ImGui.GetFrameHeight())); + if (!table) + return; + + var headerColor = ImGui.GetColorU32(ImGuiCol.TableHeaderBg); + var checkBoxOffset = (checkBoxWidth - ImGui.GetFrameHeight()) / 2; + ImUtf8.TableSetupColumn("Panel##1"u8, ImGuiTableColumnFlags.WidthFixed, textWidth); + ImUtf8.TableSetupColumn("Show##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Expand##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Panel##2"u8, ImGuiTableColumnFlags.WidthFixed, textWidth); + ImUtf8.TableSetupColumn("Show##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Expand##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + + ImGui.TableHeadersRow(); + foreach (var panel in Enum.GetValues()) + { + using var id = ImUtf8.PushId((int)panel); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, headerColor); + ImUtf8.TextFrameAligned(panel.ToName()); + var isShown = !hidden.HasFlag(panel); + var isExpanded = expanded.HasFlag(panel); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset); + if (ImUtf8.Checkbox("##show"u8, ref isShown)) + setterHide.Invoke(isShown ? hidden & ~panel : hidden | panel); + ImUtf8.HoverTooltip( + "Show this panel and associated functionality in all relevant tabs.\n\nToggling this off does NOT disable any functionality, just the display of it, so hide panels at your own risk."u8); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset); + if (ImUtf8.Checkbox("##expand"u8, ref isExpanded)) + setterExpand.Invoke(isExpanded ? expanded | panel : expanded & ~panel); + ImUtf8.HoverTooltip("Expand this panel by default in all relevant tabs."u8); + } + } +} diff --git a/Glamourer/Designs/ApplicationCollection.cs b/Glamourer/Designs/ApplicationCollection.cs new file mode 100644 index 0000000..c03d4b4 --- /dev/null +++ b/Glamourer/Designs/ApplicationCollection.cs @@ -0,0 +1,68 @@ +using Glamourer.Api.Enums; +using Glamourer.GameData; +using Dalamud.Bindings.ImGui; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +public record struct ApplicationCollection( + EquipFlag Equip, + BonusItemFlag BonusItem, + CustomizeFlag CustomizeRaw, + CrestFlag Crest, + CustomizeParameterFlag Parameters, + MetaFlag Meta) +{ + public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All); + + public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0); + + public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState); + + public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0, + CustomizeParameterExtensions.All, MetaFlag.Wetness); + + public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState); + + public static ApplicationCollection FromKeys() + => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch + { + (false, false) => All, + (true, true) => All, + (true, false) => Equipment, + (false, true) => Customizations, + }; + + public CustomizeFlag Customize + { + get => CustomizeRaw; + set => CustomizeRaw = value | CustomizeFlag.BodyType; + } + + public void RemoveEquip() + { + Equip = 0; + BonusItem = 0; + Crest = 0; + Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState); + } + + public void RemoveCustomize() + { + Customize = 0; + Parameters = 0; + Meta &= MetaFlag.Wetness; + } + + public ApplicationCollection Restrict(ApplicationCollection old) + => new(old.Equip & Equip, old.BonusItem & BonusItem, (old.Customize & Customize) | CustomizeFlag.BodyType, old.Crest & Crest, + old.Parameters & Parameters, old.Meta & Meta); + + public ApplicationCollection CloneSecure() + => new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All, + (Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant, + Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All); +} diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs index c15b26a..281a940 100644 --- a/Glamourer/Designs/ApplicationRules.cs +++ b/Glamourer/Designs/ApplicationRules.cs @@ -1,20 +1,14 @@ -using Glamourer.GameData; +using Glamourer.Api.Enums; +using Glamourer.GameData; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.GameData.Enums; namespace Glamourer.Designs; -public readonly struct ApplicationRules( - EquipFlag equip, - CustomizeFlag customize, - CrestFlag crest, - CustomizeParameterFlag parameters, - MetaFlag meta, - bool materials) +public readonly struct ApplicationRules(ApplicationCollection application, bool materials) { - public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, - CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All, true); + public static readonly ApplicationRules All = new(ApplicationCollection.All, true); public static ApplicationRules FromModifiers(ActorState state) => FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); @@ -23,54 +17,40 @@ public readonly struct ApplicationRules( => NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); public static ApplicationRules AllButParameters(ActorState state) - => new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta, true); - - public static ApplicationRules AllWithConfig(Configuration config) - => new(All.Equip, All.Customize, All.Crest, config.UseAdvancedParameters ? All.Parameters : 0, All.Meta, config.UseAdvancedDyes); + => new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true); public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift) - => new(ctrl || !shift ? EquipFlagExtensions.All : 0, - !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0, - 0, - 0, - ctrl || !shift ? MetaFlag.VisorState : 0, false); + { + var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; + var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; + var visor = equip != 0 ? MetaFlag.VisorState : 0; + return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false); + } public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift) { var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; + var bonus = equip == 0 ? 0 : BonusExtensions.All; var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant; var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All; var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0; if (equip != 0) meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState; - return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta, equip != 0); + var collection = new ApplicationCollection(equip, bonus, customize, crest, + ComputeParameters(state.ModelData, state.BaseData, parameters), meta); + return new ApplicationRules(collection, equip != 0); } public void Apply(DesignBase design) - { - design.ApplyEquip = Equip; - design.ApplyCustomize = Customize; - design.ApplyCrest = Crest; - design.ApplyParameters = Parameters; - design.ApplyMeta = Meta; - } + => design.Application = application; public EquipFlag Equip - => equip & EquipFlagExtensions.All; - - public CustomizeFlag Customize - => customize & CustomizeFlagExtensions.AllRelevant; - - public CrestFlag Crest - => crest & CrestExtensions.AllRelevant; + => application.Equip & EquipFlagExtensions.All; public CustomizeParameterFlag Parameters - => parameters & CustomizeParameterExtensions.All; - - public MetaFlag Meta - => meta & MetaExtensions.All; + => application.Parameters & CustomizeParameterExtensions.All; public bool Materials => materials; diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index f09e7bc..848e7d6 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Automation; using Glamourer.Designs.Links; using Glamourer.Interop.Material; @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Structs; +using Notification = OtterGui.Classes.Notification; namespace Glamourer.Designs; @@ -27,32 +28,39 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn internal Design(Design other) : base(other) { - Tags = [.. other.Tags]; - Description = other.Description; - QuickDesign = other.QuickDesign; - AssociatedMods = new SortedList(other.AssociatedMods); + Tags = [.. other.Tags]; + Description = other.Description; + QuickDesign = other.QuickDesign; + ForcedRedraw = other.ForcedRedraw; + ResetAdvancedDyes = other.ResetAdvancedDyes; + ResetTemporarySettings = other.ResetTemporarySettings; + Color = other.Color; + AssociatedMods = new SortedList(other.AssociatedMods); + Links = Links.Clone(); } // Metadata - public new const int FileVersion = 1; + public new const int FileVersion = 2; - public Guid Identifier { get; internal init; } - public DateTimeOffset CreationDate { get; internal init; } - public DateTimeOffset LastEdit { get; internal set; } - public LowerString Name { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string[] Tags { get; internal set; } = []; - public int Index { get; internal set; } - public bool ForcedRedraw { get; internal set; } - public bool QuickDesign { get; internal set; } = true; - public string Color { get; internal set; } = string.Empty; - public SortedList AssociatedMods { get; private set; } = []; - public LinkContainer Links { get; private set; } = []; + public Guid Identifier { get; internal init; } + public DateTimeOffset CreationDate { get; internal init; } + public DateTimeOffset LastEdit { get; internal set; } + public LowerString Name { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string[] Tags { get; internal set; } = []; + public int Index { get; internal set; } + public bool ForcedRedraw { get; internal set; } + public bool ResetAdvancedDyes { get; internal set; } + public bool ResetTemporarySettings { get; internal set; } + public bool QuickDesign { get; internal set; } = true; + public string Color { get; internal set; } = string.Empty; + public SortedList AssociatedMods { get; private set; } = []; + public LinkContainer Links { get; private set; } = []; public string Incognito => Identifier.ToString()[..8]; - public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) => LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All)); #endregion @@ -92,25 +100,28 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn public new JObject JsonSerialize() { - var ret = new JObject() + var ret = new JObject { - ["FileVersion"] = FileVersion, - ["Identifier"] = Identifier, - ["CreationDate"] = CreationDate, - ["LastEdit"] = LastEdit, - ["Name"] = Name.Text, - ["Description"] = Description, - ["ForcedRedraw"] = ForcedRedraw, - ["Color"] = Color, - ["QuickDesign"] = QuickDesign, - ["Tags"] = JArray.FromObject(Tags), - ["WriteProtected"] = WriteProtected(), - ["Equipment"] = SerializeEquipment(), - ["Customize"] = SerializeCustomize(), - ["Parameters"] = SerializeParameters(), - ["Materials"] = SerializeMaterials(), - ["Mods"] = SerializeMods(), - ["Links"] = Links.Serialize(), + ["FileVersion"] = FileVersion, + ["Identifier"] = Identifier, + ["CreationDate"] = CreationDate, + ["LastEdit"] = LastEdit, + ["Name"] = Name.Text, + ["Description"] = Description, + ["ForcedRedraw"] = ForcedRedraw, + ["ResetAdvancedDyes"] = ResetAdvancedDyes, + ["ResetTemporarySettings"] = ResetTemporarySettings, + ["Color"] = Color, + ["QuickDesign"] = QuickDesign, + ["Tags"] = JArray.FromObject(Tags), + ["WriteProtected"] = WriteProtected(), + ["Equipment"] = SerializeEquipment(), + ["Bonus"] = SerializeBonusItems(), + ["Customize"] = SerializeCustomize(), + ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), + ["Mods"] = SerializeMods(), + ["Links"] = Links.Serialize(), }; return ret; } @@ -120,12 +131,17 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn var ret = new JArray(); foreach (var (mod, settings) in AssociatedMods) { - var obj = new JObject() + var obj = new JObject { ["Name"] = mod.Name, ["Directory"] = mod.DirectoryName, - ["Enabled"] = settings.Enabled, }; + if (settings.Remove) + obj["Remove"] = true; + else if (settings.ForceInherit) + obj["Inherit"] = true; + else + obj["Enabled"] = settings.Enabled; if (settings.Enabled) { obj["Priority"] = settings.Priority; @@ -142,17 +158,83 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn #region Deserialization - public static Design LoadDesign(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) + public static Design LoadDesign(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, + JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch { - FileVersion => LoadDesignV1(customizations, items, linkLoader, json), + 1 => LoadDesignV1(saveService, customizations, items, linkLoader, json), + FileVersion => LoadDesignV2(customizations, items, linkLoader, json), _ => throw new Exception("The design to be loaded has no valid Version."), }; } - private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) + /// The values for gloss and specular strength were switched. Swap them for all appropriate designs. + private static Design LoadDesignV1(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, + JObject json) + { + var design = LoadDesignV2(customizations, items, linkLoader, json); + var materialDesignData = design.GetMaterialDataRef(); + if (materialDesignData.Values.Count == 0) + return design; + + var materialData = materialDesignData.Clone(); + // Guesstimate whether to migrate material rows: + // Update 1.3.0.10 released at that time, so any design last updated before that can be migrated. + if (design.LastEdit <= new DateTime(2024, 8, 7, 16, 0, 0, DateTimeKind.Utc)) + { + Migrate("because it was saved the wrong way around before 1.3.0.10, and this design was not changed since that release."); + } + else + { + var hasNegativeGloss = false; + var hasNonPositiveGloss = false; + var specularLarger = 0; + foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max())) + { + hasNegativeGloss |= value.Value.GlossStrength < 0; + hasNonPositiveGloss |= value.Value.GlossStrength <= 0; + if (value.Value.SpecularStrength > value.Value.GlossStrength) + ++specularLarger; + } + + // If there is any negative gloss, this is wrong and can be migrated. + if (hasNegativeGloss) + Migrate("because it had a negative Gloss value, which is not supported and thus probably outdated."); + // If there is any non-positive Gloss and some specular values that are larger, it is probably wrong and can be migrated. + else if (hasNonPositiveGloss && specularLarger > 0) + Migrate("because it had a zero Gloss value, and at least one Specular Strength larger than the Gloss, which is unusual."); + // If most of the specular strengths are larger, it is probably wrong and can be migrated. + else if (specularLarger > materialData.Values.Count / 2) + Migrate("because most of its Specular Strength values were larger than the Gloss values, which is unusual."); + } + + return design; + + void Migrate(string reason) + { + materialDesignData.Clear(); + foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max())) + { + var gloss = Math.Clamp(value.Value.SpecularStrength, 0, (float)Half.MaxValue); + var specularStrength = Math.Clamp(value.Value.GlossStrength, 0, (float)Half.MaxValue); + var colorRow = value.Value with + { + GlossStrength = gloss, + SpecularStrength = specularStrength, + }; + materialDesignData.AddOrUpdateValue(MaterialValueIndex.FromKey(key), value with { Value = colorRow }); + } + + Glamourer.Messager.AddMessage(new Notification( + $"Swapped Gloss and Specular Strength in {materialDesignData.Values.Count} Rows in design {design.Incognito} {reason}", + NotificationType.Info)); + saveService.Save(SaveType.ImmediateSync, design); + } + } + + private static Design LoadDesignV2(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) { var creationDate = json["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate"); @@ -171,17 +253,20 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn design.SetWriteProtected(json["WriteProtected"]?.ToObject() ?? false); LoadCustomize(customizations, json["Customize"], design, design.Name, true, false); LoadEquip(items, json["Equipment"], design, design.Name, true); + LoadBonus(items, design, json["Bonus"]); LoadMods(json["Mods"], design); LoadParameters(json["Parameters"], design, design.Name); LoadMaterials(json["Materials"], design, design.Name); LoadLinks(linkLoader, json["Links"], design); - design.Color = json["Color"]?.ToObject() ?? string.Empty; - design.ForcedRedraw = json["ForcedRedraw"]?.ToObject() ?? false; + design.Color = json["Color"]?.ToObject() ?? string.Empty; + design.ForcedRedraw = json["ForcedRedraw"]?.ToObject() ?? false; + design.ResetAdvancedDyes = json["ResetAdvancedDyes"]?.ToObject() ?? false; + design.ResetTemporarySettings = json["ResetTemporarySettings"]?.ToObject() ?? false; return design; static string[] ParseTags(JObject json) { - var tags = json["Tags"]?.ToObject() ?? Array.Empty(); + var tags = json["Tags"]?.ToObject() ?? []; return tags.OrderBy(t => t).Distinct().ToArray(); } } @@ -202,12 +287,15 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn continue; } - var settingsDict = tok["Settings"]?.ToObject>>() ?? []; - var settings = new Dictionary>(settingsDict.Count); + var forceInherit = tok["Inherit"]?.ToObject() ?? false; + var removeSetting = tok["Remove"]?.ToObject() ?? false; + var settingsDict = tok["Settings"]?.ToObject>>() ?? []; + var settings = new Dictionary>(settingsDict.Count); foreach (var (key, value) in settingsDict) settings.Add(key, value); var priority = tok["Priority"]?.ToObject() ?? 0; - if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value))) + if (!design.AssociatedMods.TryAdd(new Mod(name, directory), + new ModSettings(settings, priority, enabled.Value, forceInherit, removeSetting))) Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning); } } @@ -226,10 +314,10 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn if (array == null) return; - foreach (var obj in array.OfType()) + foreach (var jObj in array.OfType()) { - var identifier = obj["Design"]?.ToObject() ?? throw new ArgumentNullException("Design"); - var type = (ApplicationType)(obj["Type"]?.ToObject() ?? 0); + var identifier = jObj["Design"]?.ToObject() ?? throw new ArgumentNullException(nameof(design)); + var type = (ApplicationType)(jObj["Type"]?.ToObject() ?? 0); linkLoader.AddObject(design, new LinkData(identifier, type, order)); } } diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 4910793..f87d75a 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.GameData; using Glamourer.Interop.Material; using Glamourer.Services; @@ -40,25 +40,23 @@ public class DesignBase } /// Used when importing .cma or .chara files. - internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) + internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags, + BonusItemFlag bonusFlags) { - _designData = designData; - ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; - ApplyEquip = equipFlags & EquipFlagExtensions.All; - ApplyMeta = 0; - CustomizeSet = SetCustomizationSet(customize); + _designData = designData; + ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; + Application.Equip = equipFlags & EquipFlagExtensions.All; + Application.BonusItem = bonusFlags & BonusExtensions.All; + Application.Meta = 0; + CustomizeSet = SetCustomizationSet(customize); } internal DesignBase(DesignBase clone) { - _designData = clone._designData; - _materials = clone._materials.Clone(); - CustomizeSet = clone.CustomizeSet; - ApplyCustomize = clone.ApplyCustomizeRaw; - ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; - ApplyParameters = clone.ApplyParameters & CustomizeParameterExtensions.All; - ApplyCrest = clone.ApplyCrest & CrestExtensions.All; - ApplyMeta = clone.ApplyMeta & MetaExtensions.All; + _designData = clone._designData; + _materials = clone._materials.Clone(); + CustomizeSet = clone.CustomizeSet; + Application = clone.Application.CloneSecure(); } /// Ensure that the customization set is updated when the design data changes. @@ -70,27 +68,20 @@ public class DesignBase #region Application Data - private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; - public CustomizeSet CustomizeSet { get; private set; } + public CustomizeSet CustomizeSet { get; private set; } - public CustomizeParameterFlag ApplyParameters { get; internal set; } + public ApplicationCollection Application = ApplicationCollection.Default; internal CustomizeFlag ApplyCustomize { - get => _applyCustomize.FixApplication(CustomizeSet); - set => _applyCustomize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType; + get => Application.Customize.FixApplication(CustomizeSet); + set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType; } internal CustomizeFlag ApplyCustomizeExcludingBodyType - => _applyCustomize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType; + => Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType; - internal CustomizeFlag ApplyCustomizeRaw - => _applyCustomize; - - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; - internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; - private bool _writeProtected; + private bool _writeProtected; public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) { @@ -103,18 +94,18 @@ public class DesignBase } public bool DoApplyMeta(MetaIndex index) - => ApplyMeta.HasFlag(index.ToFlag()); + => Application.Meta.HasFlag(index.ToFlag()); public bool WriteProtected() => _writeProtected; public bool SetApplyMeta(MetaIndex index, bool value) { - var newFlag = value ? ApplyMeta | index.ToFlag() : ApplyMeta & ~index.ToFlag(); - if (newFlag == ApplyMeta) + var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag(); + if (newFlag == Application.Meta) return false; - ApplyMeta = newFlag; + Application.Meta = newFlag; return true; } @@ -128,103 +119,103 @@ public class DesignBase } public bool DoApplyEquip(EquipSlot slot) - => ApplyEquip.HasFlag(slot.ToFlag()); + => Application.Equip.HasFlag(slot.ToFlag()); public bool DoApplyStain(EquipSlot slot) - => ApplyEquip.HasFlag(slot.ToStainFlag()); + => Application.Equip.HasFlag(slot.ToStainFlag()); public bool DoApplyCustomize(CustomizeIndex idx) - => ApplyCustomize.HasFlag(idx.ToFlag()); + => Application.Customize.HasFlag(idx.ToFlag()); public bool DoApplyCrest(CrestFlag slot) - => ApplyCrest.HasFlag(slot); + => Application.Crest.HasFlag(slot); public bool DoApplyParameter(CustomizeParameterFlag flag) - => ApplyParameters.HasFlag(flag); + => Application.Parameters.HasFlag(flag); + + public bool DoApplyBonusItem(BonusItemFlag slot) + => Application.BonusItem.HasFlag(slot); internal bool SetApplyEquip(EquipSlot slot, bool value) { - var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); - if (newValue == ApplyEquip) + var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag(); + if (newValue == Application.Equip) return false; - ApplyEquip = newValue; + Application.Equip = newValue; + return true; + } + + internal bool SetApplyBonusItem(BonusItemFlag slot, bool value) + { + var newValue = value ? Application.BonusItem | slot : Application.BonusItem & ~slot; + if (newValue == Application.BonusItem) + return false; + + Application.BonusItem = newValue; return true; } internal bool SetApplyStain(EquipSlot slot, bool value) { - var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); - if (newValue == ApplyEquip) + var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag(); + if (newValue == Application.Equip) return false; - ApplyEquip = newValue; + Application.Equip = newValue; return true; } internal bool SetApplyCustomize(CustomizeIndex idx, bool value) { - var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); - if (newValue == _applyCustomize) + var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag(); + if (newValue == Application.Customize) return false; - _applyCustomize = newValue; + Application.Customize = newValue; return true; } internal bool SetApplyCrest(CrestFlag slot, bool value) { - var newValue = value ? ApplyCrest | slot : ApplyCrest & ~slot; - if (newValue == ApplyCrest) + var newValue = value ? Application.Crest | slot : Application.Crest & ~slot; + if (newValue == Application.Crest) return false; - ApplyCrest = newValue; + Application.Crest = newValue; return true; } internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value) { - var newValue = value ? ApplyParameters | flag : ApplyParameters & ~flag; - if (newValue == ApplyParameters) + var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag; + if (newValue == Application.Parameters) return false; - ApplyParameters = newValue; + Application.Parameters = newValue; return true; } - internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, - CustomizeParameterFlag parameterFlags) - => new(this, equipFlags, customizeFlags, crestFlags, parameterFlags); + public IEnumerable FilteredItemNames + => _designData.FilteredItemNames(Application.Equip, Application.BonusItem); + + internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions) + => new(this, restrictions); internal readonly struct FlagRestrictionResetter : IDisposable { - private readonly DesignBase _design; - private readonly EquipFlag _oldEquipFlags; - private readonly CustomizeFlag _oldCustomizeFlags; - private readonly CrestFlag _oldCrestFlags; - private readonly CustomizeParameterFlag _oldParameterFlags; + private readonly DesignBase _design; + private readonly ApplicationCollection _oldFlags; - public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, - CustomizeParameterFlag parameterFlags) + public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions) { - _design = d; - _oldEquipFlags = d.ApplyEquip; - _oldCustomizeFlags = d.ApplyCustomizeRaw; - _oldCrestFlags = d.ApplyCrest; - _oldParameterFlags = d.ApplyParameters; - d.ApplyEquip &= equipFlags; - d.ApplyCustomize &= customizeFlags; - d.ApplyCrest &= crestFlags; - d.ApplyParameters &= parameterFlags; + _design = d; + _oldFlags = d.Application; + _design.Application = restrictions.Restrict(_oldFlags); } public void Dispose() - { - _design.ApplyEquip = _oldEquipFlags; - _design.ApplyCustomize = _oldCustomizeFlags; - _design.ApplyCrest = _oldCrestFlags; - _design.ApplyParameters = _oldParameterFlags; - } + => _design.Application = _oldFlags; } private CustomizeSet SetCustomizationSet(CustomizeService customize) @@ -242,6 +233,7 @@ public class DesignBase { ["FileVersion"] = FileVersion, ["Equipment"] = SerializeEquipment(), + ["Bonus"] = SerializeBonusItems(), ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), ["Materials"] = SerializeMaterials(), @@ -257,15 +249,16 @@ public class DesignBase foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { var item = _designData.Item(slot); - var stain = _designData.Stain(slot); + var stains = _designData.Stain(slot); var crestSlot = slot.ToCrestFlag(); var crest = _designData.Crest(crestSlot); - ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); + ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); } - ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply"); - ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply"); - ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply"); + ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply"); + ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply"); + ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply"); + ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply"); } else { @@ -274,16 +267,31 @@ public class DesignBase return ret; - static JObject Serialize(CustomItemId id, StainId stain, bool crest, bool apply, bool applyStain, bool applyCrest) - => new() + static JObject Serialize(CustomItemId id, StainIds stains, bool crest, bool apply, bool applyStain, bool applyCrest) + => stains.AddToObject(new JObject { ["ItemId"] = id.Id, - ["Stain"] = stain.Id, ["Crest"] = crest, ["Apply"] = apply, ["ApplyStain"] = applyStain, ["ApplyCrest"] = applyCrest, + }); + } + + protected JObject SerializeBonusItems() + { + var ret = new JObject(); + foreach (var slot in BonusExtensions.AllFlags) + { + var item = _designData.BonusItem(slot); + ret[slot.ToString()] = new JObject() + { + ["BonusId"] = item.Id.Id, + ["Apply"] = DoApplyBonusItem(slot), }; + } + + return ret; } protected JObject SerializeCustomize() @@ -300,7 +308,7 @@ public class DesignBase ret[idx.ToString()] = new JObject() { ["Value"] = customize[idx].Value, - ["Apply"] = ApplyCustomizeRaw.HasFlag(idx.ToFlag()), + ["Apply"] = Application.Customize.HasFlag(idx.ToFlag()), }; } else @@ -383,7 +391,7 @@ public class DesignBase { var k = uint.Parse(key.Name, NumberStyles.HexNumber); var v = value.ToObject(); - if (!MaterialValueIndex.FromKey(k, out var idx)) + if (!MaterialValueIndex.FromKey(k, out _)) { Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.", NotificationType.Warning); @@ -423,19 +431,43 @@ public class DesignBase LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); LoadParameters(json["Parameters"], ret, "Temporary Design"); LoadMaterials(json["Materials"], ret, "Temporary Design"); + LoadBonus(items, ret, json["Bonus"]); return ret; } + protected static void LoadBonus(ItemManager items, DesignBase design, JToken? json) + { + if (json is not JObject) + { + design.Application.BonusItem = 0; + return; + } + + foreach (var slot in BonusExtensions.AllFlags) + { + if (json[slot.ToString()] is not JObject itemJson) + { + design.Application.BonusItem &= ~slot; + design.GetDesignDataRef().SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); + continue; + } + + design.SetApplyBonusItem(slot, itemJson["Apply"]?.ToObject() ?? false); + var id = itemJson["BonusId"]?.ToObject() ?? 0; + var item = items.Resolve(slot, id); + design.GetDesignDataRef().SetBonusItem(slot, item); + } + } + protected static void LoadParameters(JToken? parameters, DesignBase design, string name) { if (parameters == null) { - design.ApplyParameters = 0; + design.Application.Parameters = 0; design.GetDesignDataRef().Parameters = default; return; } - foreach (var flag in CustomizeParameterExtensions.ValueFlags) { if (!TryGetToken(flag, out var token)) @@ -491,7 +523,7 @@ public class DesignBase return true; } - design.ApplyParameters &= ~flag; + design.Application.Parameters &= ~flag; design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero; return false; } @@ -522,32 +554,15 @@ public class DesignBase return; } - static (CustomItemId, StainId, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item) - { - var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot).Id; - var stain = (StainId)(item?["Stain"]?.ToObject() ?? 0); - var crest = item?["Crest"]?.ToObject() ?? false; - var apply = item?["Apply"]?.ToObject() ?? false; - var applyStain = item?["ApplyStain"]?.ToObject() ?? false; - var applyCrest = item?["ApplyCrest"]?.ToObject() ?? false; - return (id, stain, crest, apply, applyStain, applyCrest); - } - - void PrintWarning(string msg) - { - if (msg.Length > 0 && name != "Temporary Design") - Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning); - } - foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]); + var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]); PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown)); - PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); + PrintWarning(items.ValidateStain(stains, out stains, allowUnknown)); var crestSlot = slot.ToCrestFlag(); design._designData.SetItem(slot, item); - design._designData.SetStain(slot, stain); + design._designData.SetStain(slot, stains); design._designData.SetCrest(crestSlot, crest); design.SetApplyEquip(slot, apply); design.SetApplyStain(slot, applyStain); @@ -555,21 +570,21 @@ public class DesignBase } { - var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); + var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); if (id == ItemManager.NothingId(EquipSlot.MainHand)) id = items.DefaultSword.ItemId; - var (idOff, stainOff, crestOff, applyOff, applyStainOff, applyCrestOff) = + var (idOff, stainsOff, crestOff, applyOff, applyStainOff, applyCrestOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); if (id == ItemManager.NothingId(EquipSlot.OffHand)) id = ItemManager.NothingId(FullEquipType.Shield); PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown)); - PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); - PrintWarning(items.ValidateStain(stainOff, out stainOff, allowUnknown)); + PrintWarning(items.ValidateStain(stains, out stains, allowUnknown)); + PrintWarning(items.ValidateStain(stainsOff, out stainsOff, allowUnknown)); design._designData.SetItem(EquipSlot.MainHand, main); design._designData.SetItem(EquipSlot.OffHand, off); - design._designData.SetStain(EquipSlot.MainHand, stain); - design._designData.SetStain(EquipSlot.OffHand, stainOff); + design._designData.SetStain(EquipSlot.MainHand, stains); + design._designData.SetStain(EquipSlot.OffHand, stainsOff); design._designData.SetCrest(CrestFlag.MainHand, crest); design._designData.SetCrest(CrestFlag.OffHand, crestOff); design.SetApplyEquip(EquipSlot.MainHand, apply); @@ -590,6 +605,28 @@ public class DesignBase metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled); design._designData.SetVisor(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["VieraEars"], "Show", "Apply", QuadBool.NullTrue); + design.SetApplyMeta(MetaIndex.EarState, metaValue.Enabled); + design._designData.SetEarsVisible(metaValue.ForcedValue); + return; + + void PrintWarning(string msg) + { + if (msg.Length > 0 && name != "Temporary Design") + Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning); + } + + static (CustomItemId, StainIds, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item) + { + var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot).Id; + var stains = StainIds.ParseFromObject(item as JObject); + var crest = item?["Crest"]?.ToObject() ?? false; + var apply = item?["Apply"]?.ToObject() ?? false; + var applyStain = item?["ApplyStain"]?.ToObject() ?? false; + var applyCrest = item?["ApplyCrest"]?.ToObject() ?? false; + return (id, stains, crest, apply, applyStain, applyCrest); + } } protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman, @@ -670,11 +707,12 @@ public class DesignBase { _designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags, out var writeProtected, out var applyMeta); - ApplyEquip = equipFlags; - ApplyCustomize = customizeFlags; - ApplyParameters = 0; - ApplyCrest = 0; - ApplyMeta = applyMeta; + Application.Equip = equipFlags; + ApplyCustomize = customizeFlags; + Application.Parameters = 0; + Application.Crest = 0; + Application.Meta = applyMeta; + Application.BonusItem = 0; SetWriteProtected(writeProtected); CustomizeSet = SetCustomizationSet(customize); } diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs index a8b2f7b..8cd137f 100644 --- a/Glamourer/Designs/DesignBase64Migration.cs +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -1,6 +1,7 @@ -using Glamourer.Services; -using Glamourer.State; +using Glamourer.Api.Enums; +using Glamourer.Services; using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -96,8 +97,8 @@ public class DesignBase64Migration fixed (byte* ptr = bytes) { - var cur = (CharacterWeapon*)(ptr + 30); - var eq = (CharacterArmor*)(cur + 2); + var cur = (LegacyCharacterWeapon*)(ptr + 30); + var eq = (LegacyCharacterArmor*)(cur + 2); if (!humans.IsHuman(data.ModelId)) { @@ -163,7 +164,8 @@ public class DesignBase64Migration } } - public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta, bool writeProtected, float alpha = 1.0f) + public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta, + bool writeProtected, float alpha = 1.0f) { var data = stackalloc byte[Base64SizeV4]; data[0] = 5; @@ -186,10 +188,12 @@ public class DesignBase64Migration | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); save.Customize.Write(data + 4); - ((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)); - ((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)); + ((LegacyCharacterWeapon*)(data + 30))[0] = + new LegacyCharacterWeapon(save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand))); + ((LegacyCharacterWeapon*)(data + 30))[1] = + new LegacyCharacterWeapon(save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand))); foreach (var slot in EquipSlotExtensions.EqdpSlots) - ((CharacterArmor*)(data + 44))[slot.ToIndex()] = save.Item(slot).Armor(save.Stain(slot)); + ((LegacyCharacterArmor*)(data + 44))[slot.ToIndex()] = new LegacyCharacterArmor(save.Item(slot).Armor(save.Stain(slot))); *(ushort*)(data + 84) = 1; // IsSet. *(float*)(data + 86) = 1f; data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01) diff --git a/Glamourer/Designs/DesignColors.cs b/Glamourer/Designs/DesignColors.cs index 8bc5539..a8f3178 100644 --- a/Glamourer/Designs/DesignColors.cs +++ b/Glamourer/Designs/DesignColors.cs @@ -1,13 +1,14 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; using Glamourer.Gui; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; namespace Glamourer.Designs; @@ -270,7 +271,7 @@ public class DesignColors : ISavable, IReadOnlyDictionary public static uint AutoColor(DesignBase design) { var customize = design.ApplyCustomizeExcludingBodyType == 0; - var equip = design.ApplyEquip == 0; + var equip = design.Application.Equip == 0; return (customize, equip) switch { (true, true) => ColorId.StateDesign.Value(), diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index f3955c5..058b023 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Designs; public class DesignConverter( + SaveService saveService, ItemManager _items, DesignManager _designs, CustomizeService _customize, @@ -69,19 +70,14 @@ public class DesignConverter( try { var ret = jObject["Identifier"] != null - ? Design.LoadDesign(_customize, _items, _linkLoader, jObject) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObject) : DesignBase.LoadDesignBase(_customize, _items, jObject); - ret.SetApplyMeta(MetaIndex.Wetness, customize); if (!customize) - ret.ApplyCustomize = 0; + ret.Application.RemoveCustomize(); if (!equip) - { - ret.ApplyEquip = 0; - ret.ApplyCrest = 0; - ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState); - } + ret.Application.RemoveEquip(); return ret; } @@ -105,7 +101,7 @@ public class DesignConverter( case (byte)'{': var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes)); ret = jObj1["Identifier"] != null - ? Design.LoadDesign(_customize, _items, _linkLoader, jObj1) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1) : DesignBase.LoadDesignBase(_customize, _items, jObj1); break; case 1: @@ -120,7 +116,7 @@ public class DesignConverter( var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 3); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -131,7 +127,7 @@ public class DesignConverter( var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 5); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -141,7 +137,7 @@ public class DesignConverter( var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 6); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, _linkLoader, jObj2) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } @@ -155,16 +151,11 @@ public class DesignConverter( return null; } - ret.SetApplyMeta(MetaIndex.Wetness, customize); if (!customize) - ret.ApplyCustomize = 0; + ret.Application.RemoveCustomize(); if (!equip) - { - ret.ApplyEquip = 0; - ret.ApplyCrest = 0; - ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState); - } + ret.Application.RemoveEquip(); return ret; } @@ -176,7 +167,7 @@ public class DesignConverter( return System.Convert.ToBase64String(compressed); } - public IEnumerable<(EquipSlot Slot, EquipItem Item, StainId Stain)> FromDrawData(IReadOnlyList armors, + public IEnumerable<(EquipSlot Slot, EquipItem Item, StainIds Stains)> FromDrawData(IReadOnlyList armors, CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings) { if (armors.Count != 10) @@ -194,7 +185,7 @@ public class DesignConverter( item = ItemManager.NothingItem(slot); } - yield return (slot, item, armor.Stain); + yield return (slot, item, armor.Stains); } var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant); @@ -204,7 +195,7 @@ public class DesignConverter( mh = _items.DefaultSword; } - yield return (EquipSlot.MainHand, mh, mainhand.Stain); + yield return (EquipSlot.MainHand, mh, mainhand.Stains); var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type); if (!skipWarnings && !oh.Valid) @@ -215,29 +206,47 @@ public class DesignConverter( oh = ItemManager.NothingItem(FullEquipType.Shield); } - yield return (EquipSlot.OffHand, oh, offhand.Stain); + yield return (EquipSlot.OffHand, oh, offhand.Stains); } private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials, - EquipFlag equipFlags = EquipFlagExtensions.All) + EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All) { foreach (var (key, value) in materials.Values) { var idx = MaterialValueIndex.FromKey(key); - if (idx.RowIndex >= LegacyColorTable.NumUsedRows) + if (idx.RowIndex >= ColorTable.NumRows) continue; if (idx.MaterialIndex >= MaterialService.MaterialsPerModel) continue; - var slot = idx.DrawObject switch + switch (idx.DrawObject) { - MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown, - MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand, - MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand, - _ => EquipSlot.Unknown, - }; - if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0) - continue; + case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0: + if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0) + continue; + + break; + case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0: + if ((equipFlags & (EquipFlag.Offhand | EquipFlag.OffhandStain)) == 0) + continue; + + break; + case MaterialValueIndex.DrawObjectType.Human: + if (idx.SlotIndex < 10) + { + if ((((uint)idx.SlotIndex).ToEquipSlot().ToBothFlags() & equipFlags) == 0) + continue; + } + else if (idx.SlotIndex >= 16) + { + if (((idx.SlotIndex - 16u).ToBonusSlot() & bonusFlags) == 0) + continue; + } + + break; + default: continue; + } manager.AddOrUpdateValue(idx, value.Convert()); } diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 6b84768..c7ca8e5 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -9,27 +9,35 @@ namespace Glamourer.Designs; public unsafe struct DesignData { - private string _nameHead = string.Empty; - private string _nameBody = string.Empty; - private string _nameHands = string.Empty; - private string _nameLegs = string.Empty; - private string _nameFeet = string.Empty; - private string _nameEars = string.Empty; - private string _nameNeck = string.Empty; - private string _nameWrists = string.Empty; - private string _nameRFinger = string.Empty; - private string _nameLFinger = string.Empty; - private string _nameMainhand = string.Empty; - private string _nameOffhand = string.Empty; - private fixed uint _itemIds[12]; - private fixed ushort _iconIds[12]; - private fixed byte _equipmentBytes[48]; + public const int NumEquipment = 10; + public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size; + public const int NumBonusItems = 1; + public const int NumWeapons = 2; + + private string _nameHead = string.Empty; + private string _nameBody = string.Empty; + private string _nameHands = string.Empty; + private string _nameLegs = string.Empty; + private string _nameFeet = string.Empty; + private string _nameEars = string.Empty; + private string _nameNeck = string.Empty; + private string _nameWrists = string.Empty; + private string _nameRFinger = string.Empty; + private string _nameLFinger = string.Empty; + private string _nameMainhand = string.Empty; + private string _nameOffhand = string.Empty; + private string _nameGlasses = string.Empty; + + private fixed uint _itemIds[NumEquipment + NumWeapons]; + private fixed uint _iconIds[NumEquipment + NumWeapons + NumBonusItems]; + private fixed byte _equipmentBytes[EquipmentByteSize + NumWeapons * CharacterWeapon.Size]; + private fixed ushort _bonusIds[NumBonusItems]; + private fixed ushort _bonusModelIds[NumBonusItems]; + private fixed byte _bonusVariants[NumBonusItems]; public CustomizeParameterData Parameters; public CustomizeArray Customize = CustomizeArray.Default; public uint ModelId; public CrestFlag CrestVisibility; - private SecondaryId _secondaryMainhand; - private SecondaryId _secondaryOffhand; private FullEquipType _typeMainhand; private FullEquipType _typeOffhand; private byte _states; @@ -38,29 +46,76 @@ public unsafe struct DesignData public DesignData() { } + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public readonly bool ContainsName(LowerString name) - => name.IsContained(_nameHead) - || name.IsContained(_nameBody) - || name.IsContained(_nameHands) - || name.IsContained(_nameLegs) - || name.IsContained(_nameFeet) - || name.IsContained(_nameEars) - || name.IsContained(_nameNeck) - || name.IsContained(_nameWrists) - || name.IsContained(_nameRFinger) - || name.IsContained(_nameLFinger) - || name.IsContained(_nameMainhand) - || name.IsContained(_nameOffhand); + => ItemNames.Any(name.IsContained); - public readonly StainId Stain(EquipSlot slot) + public readonly StainIds Stain(EquipSlot slot) { var index = slot.ToIndex(); - return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3]; + return index switch + { + < 10 => new StainIds(_equipmentBytes[CharacterArmor.Size * index + 3], _equipmentBytes[CharacterArmor.Size * index + 4]), + 10 => new StainIds(_equipmentBytes[EquipmentByteSize + 6], _equipmentBytes[EquipmentByteSize + 7]), + 11 => new StainIds(_equipmentBytes[EquipmentByteSize + 14], _equipmentBytes[EquipmentByteSize + 15]), + _ => StainIds.None, + }; } public readonly bool Crest(CrestFlag slot) => CrestVisibility.HasFlag(slot); + public readonly IEnumerable ItemNames + { + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + get + { + yield return _nameHead; + yield return _nameBody; + yield return _nameHands; + yield return _nameLegs; + yield return _nameFeet; + yield return _nameEars; + yield return _nameNeck; + yield return _nameWrists; + yield return _nameRFinger; + yield return _nameLFinger; + yield return _nameMainhand; + yield return _nameOffhand; + yield return _nameGlasses; + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public readonly IEnumerable FilteredItemNames(EquipFlag item, BonusItemFlag bonusItem) + { + if (item.HasFlag(EquipFlag.Head)) + yield return _nameHead; + if (item.HasFlag(EquipFlag.Body)) + yield return _nameBody; + if (item.HasFlag(EquipFlag.Hands)) + yield return _nameHands; + if (item.HasFlag(EquipFlag.Legs)) + yield return _nameLegs; + if (item.HasFlag(EquipFlag.Feet)) + yield return _nameFeet; + if (item.HasFlag(EquipFlag.Ears)) + yield return _nameEars; + if (item.HasFlag(EquipFlag.Neck)) + yield return _nameNeck; + if (item.HasFlag(EquipFlag.Wrist)) + yield return _nameWrists; + if (item.HasFlag(EquipFlag.RFinger)) + yield return _nameRFinger; + if (item.HasFlag(EquipFlag.LFinger)) + yield return _nameLFinger; + if (item.HasFlag(EquipFlag.Mainhand)) + yield return _nameMainhand; + if (item.HasFlag(EquipFlag.Offhand)) + yield return _nameOffhand; + if (bonusItem.HasFlag(BonusItemFlag.Glasses)) + yield return _nameGlasses; + } public readonly FullEquipType MainhandType => _typeMainhand; @@ -69,22 +124,36 @@ public unsafe struct DesignData => _typeOffhand; public readonly EquipItem Item(EquipSlot slot) - => slot.ToIndex() switch + { + fixed (byte* ptr = _equipmentBytes) + { + return slot.ToIndex() switch + { + // @formatter:off + 0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], ((CharacterArmor*)ptr)[0].Set, 0, ((CharacterArmor*)ptr)[0].Variant, FullEquipType.Head, name: _nameHead ), + 1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], ((CharacterArmor*)ptr)[1].Set, 0, ((CharacterArmor*)ptr)[1].Variant, FullEquipType.Body, name: _nameBody ), + 2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], ((CharacterArmor*)ptr)[2].Set, 0, ((CharacterArmor*)ptr)[2].Variant, FullEquipType.Hands, name: _nameHands ), + 3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], ((CharacterArmor*)ptr)[3].Set, 0, ((CharacterArmor*)ptr)[3].Variant, FullEquipType.Legs, name: _nameLegs ), + 4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], ((CharacterArmor*)ptr)[4].Set, 0, ((CharacterArmor*)ptr)[4].Variant, FullEquipType.Feet, name: _nameFeet ), + 5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], ((CharacterArmor*)ptr)[5].Set, 0, ((CharacterArmor*)ptr)[5].Variant, FullEquipType.Ears, name: _nameEars ), + 6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], ((CharacterArmor*)ptr)[6].Set, 0, ((CharacterArmor*)ptr)[6].Variant, FullEquipType.Neck, name: _nameNeck ), + 7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], ((CharacterArmor*)ptr)[7].Set, 0, ((CharacterArmor*)ptr)[7].Variant, FullEquipType.Wrists, name: _nameWrists ), + 8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], ((CharacterArmor*)ptr)[8].Set, 0, ((CharacterArmor*)ptr)[8].Variant, FullEquipType.Finger, name: _nameRFinger ), + 9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], ((CharacterArmor*)ptr)[9].Set, 0, ((CharacterArmor*)ptr)[9].Variant, FullEquipType.Finger, name: _nameLFinger ), + 10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], *(PrimaryId*)(ptr + EquipmentByteSize + 0), *(SecondaryId*)(ptr + EquipmentByteSize + 2), *(Variant*)(ptr + EquipmentByteSize + 4), _typeMainhand, name: _nameMainhand), + 11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], *(PrimaryId*)(ptr + EquipmentByteSize + 8), *(SecondaryId*)(ptr + EquipmentByteSize + 10), *(Variant*)(ptr + EquipmentByteSize + 12), _typeOffhand, name: _nameOffhand ), + _ => new EquipItem(), + // @formatter:on + }; + } + } + + public readonly EquipItem BonusItem(BonusItemFlag slot) + => slot switch { // @formatter:off - 0 => EquipItem.FromIds((ItemId)_itemIds[ 0], (IconId)_iconIds[ 0], (PrimaryId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ), - 1 => EquipItem.FromIds((ItemId)_itemIds[ 1], (IconId)_iconIds[ 1], (PrimaryId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ), - 2 => EquipItem.FromIds((ItemId)_itemIds[ 2], (IconId)_iconIds[ 2], (PrimaryId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[10], FullEquipType.Hands, name: _nameHands ), - 3 => EquipItem.FromIds((ItemId)_itemIds[ 3], (IconId)_iconIds[ 3], (PrimaryId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ), - 4 => EquipItem.FromIds((ItemId)_itemIds[ 4], (IconId)_iconIds[ 4], (PrimaryId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ), - 5 => EquipItem.FromIds((ItemId)_itemIds[ 5], (IconId)_iconIds[ 5], (PrimaryId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[22], FullEquipType.Ears, name: _nameEars ), - 6 => EquipItem.FromIds((ItemId)_itemIds[ 6], (IconId)_iconIds[ 6], (PrimaryId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ), - 7 => EquipItem.FromIds((ItemId)_itemIds[ 7], (IconId)_iconIds[ 7], (PrimaryId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ), - 8 => EquipItem.FromIds((ItemId)_itemIds[ 8], (IconId)_iconIds[ 8], (PrimaryId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ), - 9 => EquipItem.FromIds((ItemId)_itemIds[ 9], (IconId)_iconIds[ 9], (PrimaryId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ), - 10 => EquipItem.FromIds((ItemId)_itemIds[10], (IconId)_iconIds[10], (PrimaryId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, (Variant)_equipmentBytes[42], _typeMainhand, name: _nameMainhand), - 11 => EquipItem.FromIds((ItemId)_itemIds[11], (IconId)_iconIds[11], (PrimaryId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, (Variant)_equipmentBytes[46], _typeOffhand, name: _nameOffhand ), - _ => new EquipItem(), + BonusItemFlag.Glasses => EquipItem.FromBonusIds(_bonusIds[0], _iconIds[12], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses, _nameGlasses), + _ => EquipItem.BonusItemNothing(slot), // @formatter:on }; @@ -113,22 +182,22 @@ public unsafe struct DesignData { fixed (byte* ptr = _equipmentBytes) { - var armorPtr = (CharacterArmor*)ptr; - return slot is EquipSlot.MainHand ? armorPtr[10].ToWeapon(_secondaryMainhand) : armorPtr[11].ToWeapon(_secondaryOffhand); + var weaponPtr = (CharacterWeapon*)(ptr + EquipmentByteSize); + return weaponPtr[slot is EquipSlot.MainHand ? 0 : 1]; } } public bool SetItem(EquipSlot slot, EquipItem item) { var index = slot.ToIndex(); - if (index > 11) + if (index > NumEquipment + NumWeapons) return false; - _itemIds[index] = item.ItemId.Id; - _iconIds[index] = item.IconId.Id; - _equipmentBytes[4 * index + 0] = (byte)item.PrimaryId.Id; - _equipmentBytes[4 * index + 1] = (byte)(item.PrimaryId.Id >> 8); - _equipmentBytes[4 * index + 2] = item.Variant.Id; + _itemIds[index] = item.ItemId.Id; + _iconIds[index] = item.IconId.Id; + _equipmentBytes[CharacterArmor.Size * index + 0] = (byte)item.PrimaryId.Id; + _equipmentBytes[CharacterArmor.Size * index + 1] = (byte)(item.PrimaryId.Id >> 8); + _equipmentBytes[CharacterArmor.Size * index + 2] = item.Variant.Id; switch (index) { // @formatter:off @@ -144,36 +213,61 @@ public unsafe struct DesignData case 9: _nameLFinger = item.Name; return true; // @formatter:on case 10: - _nameMainhand = item.Name; - _secondaryMainhand = item.SecondaryId; - _typeMainhand = item.Type; + _nameMainhand = item.Name; + _equipmentBytes[EquipmentByteSize + 2] = (byte)item.SecondaryId.Id; + _equipmentBytes[EquipmentByteSize + 3] = (byte)(item.SecondaryId.Id >> 8); + _equipmentBytes[EquipmentByteSize + 4] = item.Variant.Id; + _typeMainhand = item.Type; return true; case 11: - _nameOffhand = item.Name; - _secondaryOffhand = item.SecondaryId; - _typeOffhand = item.Type; + _nameOffhand = item.Name; + _equipmentBytes[EquipmentByteSize + 10] = (byte)item.SecondaryId.Id; + _equipmentBytes[EquipmentByteSize + 11] = (byte)(item.SecondaryId.Id >> 8); + _equipmentBytes[EquipmentByteSize + 12] = item.Variant.Id; + _typeOffhand = item.Type; return true; } return true; } - public bool SetStain(EquipSlot slot, StainId stain) + public bool SetBonusItem(BonusItemFlag slot, EquipItem item) + { + var index = slot.ToIndex(); + if (index > NumBonusItems) + return false; + + _iconIds[NumEquipment + NumWeapons + index] = item.IconId.Id; + _bonusIds[index] = item.Id.BonusItem.Id; + _bonusModelIds[index] = item.PrimaryId.Id; + _bonusVariants[index] = item.Variant.Id; + switch (index) + { + case 0: + _nameGlasses = item.Name; + return true; + default: return false; + } + } + + public bool SetStain(EquipSlot slot, StainIds stains) => slot.ToIndex() switch { - 0 => SetIfDifferent(ref _equipmentBytes[3], stain.Id), - 1 => SetIfDifferent(ref _equipmentBytes[7], stain.Id), - 2 => SetIfDifferent(ref _equipmentBytes[11], stain.Id), - 3 => SetIfDifferent(ref _equipmentBytes[15], stain.Id), - 4 => SetIfDifferent(ref _equipmentBytes[19], stain.Id), - 5 => SetIfDifferent(ref _equipmentBytes[23], stain.Id), - 6 => SetIfDifferent(ref _equipmentBytes[27], stain.Id), - 7 => SetIfDifferent(ref _equipmentBytes[31], stain.Id), - 8 => SetIfDifferent(ref _equipmentBytes[35], stain.Id), - 9 => SetIfDifferent(ref _equipmentBytes[39], stain.Id), - 10 => SetIfDifferent(ref _equipmentBytes[43], stain.Id), - 11 => SetIfDifferent(ref _equipmentBytes[47], stain.Id), - _ => false, + // @formatter:off + 0 => SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 4], stains.Stain2.Id), + 1 => SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 4], stains.Stain2.Id), + 2 => SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 4], stains.Stain2.Id), + 3 => SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 4], stains.Stain2.Id), + 4 => SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 4], stains.Stain2.Id), + 5 => SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 4], stains.Stain2.Id), + 6 => SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 4], stains.Stain2.Id), + 7 => SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 4], stains.Stain2.Id), + 8 => SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 4], stains.Stain2.Id), + 9 => SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 4], stains.Stain2.Id), + 10 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 6], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 7], stains.Stain2.Id), + 11 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 14], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 15], stains.Stain2.Id), + _ => false, + // @formatter:on }; public bool SetCrest(CrestFlag slot, bool visible) @@ -193,6 +287,7 @@ public unsafe struct DesignData MetaIndex.HatState => IsHatVisible(), MetaIndex.VisorState => IsVisorToggled(), MetaIndex.WeaponState => IsWeaponVisible(), + MetaIndex.EarState => AreEarsVisible(), _ => false, }; @@ -203,6 +298,7 @@ public unsafe struct DesignData MetaIndex.HatState => SetHatVisible(value), MetaIndex.VisorState => SetVisor(value), MetaIndex.WeaponState => SetWeaponVisible(value), + MetaIndex.EarState => SetEarsVisible(value), _ => false, }; @@ -246,6 +342,9 @@ public unsafe struct DesignData public readonly bool IsWeaponVisible() => (_states & 0x08) == 0x08; + public readonly bool AreEarsVisible() + => (_states & 0x10) == 0x00; + public bool SetWeaponVisible(bool value) { if (value == IsWeaponVisible()) @@ -255,21 +354,37 @@ public unsafe struct DesignData return true; } + public bool SetEarsVisible(bool value) + { + if (value == AreEarsVisible()) + return false; + + _states = (byte)(value ? _states & ~0x10 : _states | 0x10); + return true; + } + public void SetDefaultEquipment(ItemManager items) { foreach (var slot in EquipSlotExtensions.EqdpSlots) { SetItem(slot, ItemManager.NothingItem(slot)); - SetStain(slot, 0); + SetStain(slot, StainIds.None); SetCrest(slot.ToCrestFlag(), false); } SetItem(EquipSlot.MainHand, items.DefaultSword); - SetStain(EquipSlot.MainHand, 0); + SetStain(EquipSlot.MainHand, StainIds.None); SetCrest(CrestFlag.MainHand, false); SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); - SetStain(EquipSlot.OffHand, 0); + SetStain(EquipSlot.OffHand, StainIds.None); SetCrest(CrestFlag.OffHand, false); + SetDefaultBonusItems(); + } + + public void SetDefaultBonusItems() + { + foreach (var slot in BonusExtensions.AllFlags) + SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); } @@ -285,13 +400,14 @@ public unsafe struct DesignData SetHatVisible(true); SetWeaponVisible(true); + SetEarsVisible(true); SetVisor(false); fixed (uint* ptr = _itemIds) { MemoryUtility.MemSet(ptr, 0, 10 * 4); } - fixed (ushort* ptr = _iconIds) + fixed (uint* ptr = _iconIds) { MemoryUtility.MemSet(ptr, 0, 10 * 2); } @@ -306,6 +422,7 @@ public unsafe struct DesignData _nameWrists = string.Empty; _nameRFinger = string.Empty; _nameLFinger = string.Empty; + _nameGlasses = string.Empty; return true; } @@ -322,7 +439,7 @@ public unsafe struct DesignData public readonly byte[] GetEquipmentBytes() { - var ret = new byte[40]; + var ret = new byte[80]; fixed (byte* retPtr = ret, inPtr = _equipmentBytes) { MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length); @@ -343,8 +460,8 @@ public unsafe struct DesignData { fixed (byte* dataPtr = _equipmentBytes) { - var data = new Span(dataPtr, 40); - return Convert.TryFromBase64String(base64, data, out var written) && written == 40; + var data = new Span(dataPtr, 80); + return Convert.TryFromBase64String(base64, data, out var written) && written == 80; } } diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs index 91050ec..448e373 100644 --- a/Glamourer/Designs/DesignEditor.cs +++ b/Glamourer/Designs/DesignEditor.cs @@ -1,9 +1,9 @@ +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; using Glamourer.Interop.Material; using Glamourer.Services; -using Glamourer.State; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -73,7 +73,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); + DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value)); } /// @@ -89,7 +89,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, (oldCustomize, applied, changed)); + DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize)); } /// @@ -104,7 +104,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag)); + DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new)); } /// @@ -123,11 +123,14 @@ public class DesignEditor( if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets)) return; + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); - DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff, newGauntlets)); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff, + newGauntlets ?? currentGauntlets)); return; } case EquipSlot.OffHand: @@ -140,11 +143,13 @@ public class DesignEditor( if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) return; + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); - DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets)); return; } default: @@ -160,36 +165,53 @@ public class DesignEditor( Glamourer.Log.Debug( $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); + DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item)); return; } } } /// - public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings _ = default) + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default) { var design = (Design)data; - if (Items.ValidateStain(stain, out var _, false).Length > 0) + if (item.Type.ToBonus() != slot) return; - var oldStain = design.DesignData.Stain(slot); - if (!design.GetDesignDataRef().SetStain(slot, stain)) + var oldItem = design.DesignData.BonusItem(slot); + if (!design.GetDesignDataRef().SetBonusItem(slot, item)) return; design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}."); - DesignChanged.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + Glamourer.Log.Debug($"Set {slot} bonus item to {item}."); + DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item)); } /// - public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings _ = default) + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings _ = default) + { + var design = (Design)data; + if (Items.ValidateStain(stains, out var _, false).Length > 0) + return; + + var oldStain = design.DesignData.Stain(slot); + if (!design.GetDesignDataRef().SetStain(slot, stains)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stains}."); + DesignChanged.Invoke(DesignChanged.Type.Stains, design, new StainTransaction(slot, oldStain, stains)); + } + + /// + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings _ = default) { if (item.HasValue) ChangeItem(data, slot, item.Value, _); - if (stain.HasValue) - ChangeStain(data, slot, stain.Value, _); + if (stains.HasValue) + ChangeStains(data, slot, stains.Value, _); } /// @@ -203,7 +225,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); - DesignChanged.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); + DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest)); } /// @@ -216,7 +238,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value)); } public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert) @@ -229,7 +251,7 @@ public class DesignEditor( Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}"); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, index); + DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert)); } public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row) @@ -264,7 +286,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.DelaySave(design); - DesignChanged.Invoke(DesignChanged.Type.Material, design, (oldValue, row, index)); + DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row)); } public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value) @@ -277,13 +299,13 @@ public class DesignEditor( Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}."); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, index); + DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value)); } /// - public void ApplyDesign(object data, MergedDesign other, ApplySettings _ = default) - => ApplyDesign(data, other.Design); + public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default) + => ApplyDesign(data, other.Design, settings); /// public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default) @@ -308,6 +330,12 @@ public class DesignEditor( _forceFullItemOff = false; + foreach (var slot in BonusExtensions.AllFlags) + { + if (other.DoApplyBonusItem(slot)) + ChangeBonusItem(design, slot, other.DesignData.BonusItem(slot)); + } + foreach (var slot in Enum.GetValues().Where(other.DoApplyCrest)) ChangeCrest(design, slot, other.DesignData.Crest(slot)); @@ -338,7 +366,7 @@ public class DesignEditor( newOff = o; } - else if (!_forceFullItemOff && Config.ChangeEntireItem) + else if (!_forceFullItemOff && Config.ChangeEntireItem && newMain.Type is not FullEquipType.Sword) // Skip applying shields. { var defaultOffhand = Items.GetDefaultOffhand(newMain); if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index 00277c2..fd47793 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -1,4 +1,5 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; using Newtonsoft.Json; @@ -40,11 +41,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct CreationDate : ISortMode { - public string Name - => "Creation Date (Older First)"; + public ReadOnlySpan Name + => "Creation Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate)); @@ -52,11 +53,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct UpdateDate : ISortMode { - public string Name - => "Update Date (Older First)"; + public ReadOnlySpan Name + => "Update Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit)); @@ -64,11 +65,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct InverseCreationDate : ISortMode { - public string Name - => "Creation Date (Newer First)"; + public ReadOnlySpan Name + => "Creation Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate)); @@ -76,11 +77,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct InverseUpdateDate : ISortMode { - public string Name - => "Update Date (Newer First)"; + public ReadOnlySpan Name + => "Update Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit)); @@ -92,13 +93,13 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _saveService.QueueSave(this); } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data) { switch (type) { case DesignChanged.Type.Created: var parent = Root; - if (data is string path) + if ((data as CreationTransaction?)?.Path is { } path) try { parent = FindOrCreateAllFolders(path); @@ -113,14 +114,14 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable return; case DesignChanged.Type.Deleted: - if (FindLeaf(design, out var leaf1)) + if (TryGetValue(design, out var leaf1)) Delete(leaf1); return; case DesignChanged.Type.ReloadedAll: Reload(); return; - case DesignChanged.Type.Renamed when data is string oldName: - if (!FindLeaf(design, out var leaf2)) + case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName: + if (!TryGetValue(design, out var leaf2)) return; var old = oldName.FixName(); @@ -149,15 +150,6 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable ? (string.Empty, false) : (DesignToIdentifier(design), true); - // Search the entire filesystem for the leaf corresponding to a design. - public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf) - { - leaf = Root.GetAllDescendants(ISortMode.Lexicographical) - .OfType() - .FirstOrDefault(l => l.Value == design); - return leaf != null; - } - internal static void MigrateOldPaths(SaveService saveService, Dictionary oldPaths) { if (oldPaths.Count == 0) diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 7bd949c..92f8398 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -1,15 +1,18 @@ using Dalamud.Utility; +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Services; +using OtterGui.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; + namespace Glamourer.Designs; public sealed class DesignManager : DesignEditor @@ -52,7 +55,7 @@ public sealed class DesignManager : DesignEditor { var text = File.ReadAllText(f.FullName); var data = JObject.Parse(text); - var design = Design.LoadDesign(Customizations, Items, linkLoader, data); + var design = Design.LoadDesign(SaveService, Customizations, Items, linkLoader, data); designs.Value!.Add((design, f.FullName)); } catch (Exception ex) @@ -98,16 +101,21 @@ public sealed class DesignManager : DesignEditor var (actualName, path) = ParseName(name, handlePath); var design = new Design(Customizations, Items) { - CreationDate = DateTimeOffset.UtcNow, - LastEdit = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Name = actualName, - Index = Designs.Count, + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + Index = Designs.Count, + ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing, + ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes, + QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar, + ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings, }; + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -117,17 +125,22 @@ public sealed class DesignManager : DesignEditor var (actualName, path) = ParseName(name, handlePath); var design = new Design(clone) { - CreationDate = DateTimeOffset.UtcNow, - LastEdit = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Name = actualName, - Index = Designs.Count, + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + Index = Designs.Count, + ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing, + ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes, + QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar, + ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings, }; + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -143,11 +156,12 @@ public sealed class DesignManager : DesignEditor Name = actualName, Index = Designs.Count, }; + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); Designs.Add(design); Glamourer.Log.Debug( $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -176,7 +190,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.Renamed, design, oldName); + DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName)); } /// Change the description of a design. @@ -190,7 +204,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); + DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, new DescriptionTransaction(oldDescription, description)); } /// Change the associated color of a design. @@ -204,7 +218,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, oldColor); + DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor)); } /// Add a new tag to a design. The tags remain sorted. @@ -215,10 +229,10 @@ public sealed class DesignManager : DesignEditor design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); design.LastEdit = DateTimeOffset.UtcNow; - var idx = design.Tags.IndexOf(tag); + var idx = design.Tags.AsEnumerable().IndexOf(tag); SaveService.QueueSave(design); Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); + DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx)); } /// Remove a tag from a design by its index. @@ -232,7 +246,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, new TagRemovedTransaction(oldTag, tagIdx)); } /// Rename a tag from a design by its index. The tags stay sorted. @@ -247,7 +261,8 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); - DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, + new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.AsEnumerable().IndexOf(newTag))); } /// Add an associated mod to a design. @@ -259,7 +274,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings)); } /// Remove an associated mod from a design. @@ -271,17 +286,26 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings)); } /// Add or update an associated mod to a design. public void UpdateMod(Design design, Mod mod, ModSettings settings) { + var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings); design.AssociatedMods[mod] = settings; design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - Glamourer.Log.Debug($"Updated (or added) associated mod {mod.DirectoryName} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + if (hasOldSettings) + { + Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}."); + DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings)); + } + else + { + Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} from design {design.Identifier}."); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings)); + } } /// Set the write protection status of a design. @@ -292,7 +316,7 @@ public sealed class DesignManager : DesignEditor SaveService.QueueSave(design); Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); - DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value); + DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null); } /// Set the quick design bar display status of a design. @@ -305,7 +329,7 @@ public sealed class DesignManager : DesignEditor SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar."); - DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value); + DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null); } #endregion @@ -319,10 +343,32 @@ public sealed class DesignManager : DesignEditor design.ForcedRedraw = forcedRedraw; SaveService.QueueSave(design); - Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? "not" : string.Empty)} force redraws."); + Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? string.Empty : "not")} force redraws."); DesignChanged.Invoke(DesignChanged.Type.ForceRedraw, design, null); } + public void ChangeResetAdvancedDyes(Design design, bool resetAdvancedDyes) + { + if (design.ResetAdvancedDyes == resetAdvancedDyes) + return; + + design.ResetAdvancedDyes = resetAdvancedDyes; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {design.Identifier} to {(resetAdvancedDyes ? string.Empty : "not")} reset advanced dyes."); + DesignChanged.Invoke(DesignChanged.Type.ResetAdvancedDyes, design, null); + } + + public void ChangeResetTemporarySettings(Design design, bool resetTemporarySettings) + { + if (design.ResetTemporarySettings == resetTemporarySettings) + return; + + design.ResetTemporarySettings = resetTemporarySettings; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {design.Identifier} to {(resetTemporarySettings ? string.Empty : "not")} reset temporary settings."); + DesignChanged.Invoke(DesignChanged.Type.ResetTemporarySettings, design, null); + } + /// Change whether to apply a specific customize value. public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) { @@ -332,7 +378,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); + DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value)); } /// Change whether to apply a specific equipment piece. @@ -344,11 +390,23 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value)); + } + + /// Change whether to apply a specific equipment piece. + public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value) + { + if (!design.SetApplyBonusItem(slot, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}."); + DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, new ApplicationTransaction(slot, !value, value)); } /// Change whether to apply a specific stain. - public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + public void ChangeApplyStains(Design design, EquipSlot slot, bool value) { if (!design.SetApplyStain(slot, value)) return; @@ -356,7 +414,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value)); } /// Change whether to apply a specific crest visibility. @@ -368,7 +426,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value)); } /// Change the application value of one of the meta flags. @@ -380,7 +438,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value)); } /// Change the application value of a customize parameter. @@ -392,7 +450,40 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, flag); + DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value)); + } + + /// Change multiple application values at once. + public void ChangeApplyMulti(Design design, bool? equipment, bool? customization, bool? bonus, bool? parameters, bool? meta, bool? stains, + bool? materials, bool? crest) + { + if (equipment is { } e) + foreach (var f in EquipSlotExtensions.FullSlots) + ChangeApplyItem(design, f, e); + if (stains is { } s) + foreach (var f in EquipSlotExtensions.FullSlots) + ChangeApplyStains(design, f, s); + if (customization is { } c) + foreach (var f in CustomizationExtensions.All.Where(design.CustomizeSet.IsAvailable).Prepend(CustomizeIndex.Clan) + .Prepend(CustomizeIndex.Gender)) + ChangeApplyCustomize(design, f, c); + if (bonus is { } b) + foreach (var f in BonusExtensions.AllFlags) + ChangeApplyBonusItem(design, f, b); + if (meta is { } m) + foreach (var f in MetaExtensions.AllRelevant) + ChangeApplyMeta(design, f, m); + if (crest is { } cr) + foreach (var f in CrestExtensions.AllRelevantSet) + ChangeApplyCrest(design, f, cr); + + if (parameters is { } p) + foreach (var f in CustomizeParameterExtensions.AllFlags) + ChangeApplyParameter(design, f, p); + + if (materials is { } ma) + foreach (var (key, _) in design.GetMaterialData().ToArray()) + ChangeApplyMaterialValue(design, MaterialValueIndex.FromKey(key), ma); } #endregion @@ -466,7 +557,7 @@ public sealed class DesignManager : DesignEditor try { File.Move(SaveService.FileNames.MigrationDesignFile, - Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak")); + Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true); Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file."); } catch (Exception ex) diff --git a/Glamourer/Designs/History/DesignTransaction.cs b/Glamourer/Designs/History/DesignTransaction.cs new file mode 100644 index 0000000..65086db --- /dev/null +++ b/Glamourer/Designs/History/DesignTransaction.cs @@ -0,0 +1,185 @@ +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs.History; + +/// Only Designs. Can not be reverted. +public readonly record struct CreationTransaction(string Name, string? Path) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { } +} + +/// Only Designs. +public readonly record struct RenameTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is RenameTransaction other ? new RenameTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).Rename((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DescriptionTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DescriptionTransaction other ? new DescriptionTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeDescription((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DesignColorTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DesignColorTransaction other ? new DesignColorTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeColor((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagAddedTransaction(string New, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveTag((Design)data, Index); +} + +/// Only Designs. +public readonly record struct TagRemovedTransaction(string Old, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddTag((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagChangedTransaction(string Old, string New, int IndexOld, int IndexNew) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is TagChangedTransaction other && other.IndexNew == IndexOld + ? new TagChangedTransaction(other.Old, New, other.IndexOld, IndexNew) + : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RenameTag((Design)data, IndexNew, Old); +} + +/// Only Designs. +public readonly record struct ModAddedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveMod((Design)data, Mod); +} + +/// Only Designs. +public readonly record struct ModRemovedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddMod((Design)data, Mod, Settings); +} + +/// Only Designs. +public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, ModSettings New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ModUpdatedTransaction other && Mod == other.Mod ? new ModUpdatedTransaction(Mod, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).UpdateMod((Design)data, Mod, Old); +} + +/// Only Designs. +public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + { + if (editor is DesignManager e) + e.ChangeMaterialValue((Design)data, Index, Old); + } +} + +/// Only Designs. +public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old); +} + +/// Only Designs. +public readonly record struct ApplicationTransaction(object Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { + var manager = (DesignManager)editor; + var design = (Design)data; + switch (Index) + { + case CustomizeIndex idx: + manager.ChangeApplyCustomize(design, idx, Old); + break; + case (EquipSlot slot, true): + manager.ChangeApplyStains(design, slot, Old); + break; + case (EquipSlot slot, _): + manager.ChangeApplyItem(design, slot, Old); + break; + case BonusItemFlag slot: + manager.ChangeApplyBonusItem(design, slot, Old); + break; + case CrestFlag slot: + manager.ChangeApplyCrest(design, slot, Old); + break; + case MetaIndex slot: + manager.ChangeApplyMeta(design, slot, Old); + break; + case CustomizeParameterFlag slot: + manager.ChangeApplyParameter(design, slot, Old); + break; + case MaterialValueIndex slot: + manager.ChangeApplyMaterialValue(design, slot, Old); + break; + } + } +} diff --git a/Glamourer/Designs/History/EditorHistory.cs b/Glamourer/Designs/History/EditorHistory.cs new file mode 100644 index 0000000..caec151 --- /dev/null +++ b/Glamourer/Designs/History/EditorHistory.cs @@ -0,0 +1,191 @@ +using Glamourer.Api.Enums; +using Glamourer.Events; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Interop; + +namespace Glamourer.Designs.History; + +public class EditorHistory : IDisposable, IService +{ + public const int MaxUndo = 16; + + private sealed class Queue : IReadOnlyList + { + private DateTime _lastAdd = DateTime.UtcNow; + + private readonly ITransaction[] _data = new ITransaction[MaxUndo]; + public int Offset { get; private set; } + public int Count { get; private set; } + + public void Add(ITransaction transaction) + { + if (!TryMerge(transaction)) + { + if (Count == MaxUndo) + { + _data[Offset] = transaction; + Offset = (Offset + 1) % MaxUndo; + } + else + { + if (Offset > 0) + { + _data[(Count + Offset) % MaxUndo] = transaction; + ++Count; + } + else + { + _data[Count] = transaction; + ++Count; + } + } + } + + _lastAdd = DateTime.UtcNow; + } + + private bool TryMerge(ITransaction newTransaction) + { + if (Count == 0) + return false; + + var time = DateTime.UtcNow; + if (time - _lastAdd > TimeSpan.FromMilliseconds(250)) + return false; + + var lastIdx = (Offset + Count - 1) % MaxUndo; + if (newTransaction.Merge(_data[lastIdx]) is not { } transaction) + return false; + + _data[lastIdx] = transaction; + return true; + } + + public ITransaction? RemoveLast() + { + if (Count == 0) + return null; + + --Count; + var idx = (Offset + Count) % MaxUndo; + return _data[idx]; + } + + public IEnumerator GetEnumerator() + { + var end = Offset + (Offset + Count) % MaxUndo; + for (var i = Offset; i < end; ++i) + yield return _data[i]; + + end = Count - end; + for (var i = 0; i < end; ++i) + yield return _data[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ITransaction this[int index] + => index < 0 || index >= Count + ? throw new IndexOutOfRangeException() + : _data[(Offset + index) % MaxUndo]; + } + + private readonly DesignEditor _designEditor; + private readonly StateEditor _stateEditor; + private readonly DesignChanged _designChanged; + private readonly StateChanged _stateChanged; + + private readonly Dictionary _stateEntries = []; + private readonly Dictionary _designEntries = []; + + private bool _undoMode; + + public EditorHistory(DesignManager designEditor, StateManager stateEditor, DesignChanged designChanged, StateChanged stateChanged) + { + _designEditor = designEditor; + _stateEditor = stateEditor; + _designChanged = designChanged; + _stateChanged = stateChanged; + + _designChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.EditorHistory); + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.EditorHistory); + } + + public void Dispose() + { + _designChanged.Unsubscribe(OnDesignChanged); + _stateChanged.Unsubscribe(OnStateChanged); + } + + public bool CanUndo(ActorState state) + => _stateEntries.TryGetValue(state, out var list) && list.Count > 0; + + public bool CanUndo(Design design) + => _designEntries.TryGetValue(design, out var list) && list.Count > 0; + + public bool Undo(ActorState state) + { + if (!_stateEntries.TryGetValue(state, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_stateEditor, state); + _undoMode = false; + return true; + } + + public bool Undo(Design design) + { + if (!_designEntries.TryGetValue(design, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_designEditor, design); + _undoMode = false; + return true; + } + + + private void AddStateTransaction(ActorState state, ITransaction transaction) + { + if (!_stateEntries.TryGetValue(state, out var list)) + { + list = []; + _stateEntries.Add(state, list); + } + + list.Add(transaction); + } + + private void AddDesignTransaction(Design design, ITransaction transaction) + { + if (!_designEntries.TryGetValue(design, out var list)) + { + list = []; + _designEntries.Add(design, list); + } + + list.Add(transaction); + } + + + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData actors, ITransaction? data) + { + if (_undoMode || source is not StateSource.Manual) + return; + + if (data is not null) + AddStateTransaction(state, data); + } + + private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? data) + { + if (_undoMode) + return; + + if (data is not null) + AddDesignTransaction(design, data); + } +} diff --git a/Glamourer/Designs/History/Transaction.cs b/Glamourer/Designs/History/Transaction.cs new file mode 100644 index 0000000..47b10bf --- /dev/null +++ b/Glamourer/Designs/History/Transaction.cs @@ -0,0 +1,113 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Glamourer.GameData; + +namespace Glamourer.Designs.History; + +public interface ITransaction +{ + public ITransaction? Merge(ITransaction other); + public void Revert(IDesignEditor editor, object data); +} + +public readonly record struct CustomizeTransaction(CustomizeIndex Slot, CustomizeValue Old, CustomizeValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CustomizeTransaction other && Slot == other.Slot ? new CustomizeTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomize(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct EntireCustomizeTransaction(CustomizeFlag Apply, CustomizeArray Old, CustomizeArray New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EntireCustomizeTransaction other ? new EntireCustomizeTransaction(Apply | other.Apply, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeEntireCustomize(data, Old, Apply, ApplySettings.Manual); +} + +public readonly record struct EquipTransaction(EquipSlot Slot, EquipItem Old, EquipItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EquipTransaction other && Slot == other.Slot ? new EquipTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct BonusItemTransaction(BonusItemFlag Slot, EquipItem Old, EquipItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is BonusItemTransaction other && Slot == other.Slot ? new BonusItemTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeBonusItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct WeaponTransaction( + EquipItem OldMain, + EquipItem OldOff, + EquipItem OldGauntlets, + EquipItem NewMain, + EquipItem NewOff, + EquipItem NewGauntlets) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is WeaponTransaction other + ? new WeaponTransaction(other.OldMain, other.OldOff, other.OldGauntlets, NewMain, NewOff, NewGauntlets) + : null; + + public void Revert(IDesignEditor editor, object data) + { + editor.ChangeItem(data, EquipSlot.MainHand, OldMain, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.OffHand, OldOff, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.Hands, OldGauntlets, ApplySettings.Manual); + } +} + +public readonly record struct StainTransaction(EquipSlot Slot, StainIds Old, StainIds New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is StainTransaction other && Slot == other.Slot ? new StainTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeStains(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct CrestTransaction(CrestFlag Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CrestTransaction other && Slot == other.Slot ? new CrestTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCrest(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct ParameterTransaction(CustomizeParameterFlag Slot, CustomizeParameterValue Old, CustomizeParameterValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ParameterTransaction other && Slot == other.Slot ? new ParameterTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomizeParameter(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct MetaTransaction(MetaIndex Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeMetaState(data, Slot, Old, ApplySettings.Manual); +} diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs index a0aab84..c18c98b 100644 --- a/Glamourer/Designs/IDesignEditor.cs +++ b/Glamourer/Designs/IDesignEditor.cs @@ -13,7 +13,8 @@ public readonly record struct ApplySettings( bool FromJobChange = false, bool UseSingleSource = false, bool MergeLinks = false, - bool ResetMaterials = false) + bool ResetMaterials = false, + bool IsFinal = false) { public static readonly ApplySettings Manual = new() { @@ -24,6 +25,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = false, + IsFinal = false, }; public static readonly ApplySettings ManualWithLinks = new() @@ -35,6 +37,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = true, ResetMaterials = false, + IsFinal = false, }; public static readonly ApplySettings Game = new() @@ -46,6 +49,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = true, + IsFinal = false, }; } @@ -64,12 +68,15 @@ public interface IDesignEditor public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) => ChangeEquip(data, slot, item, null, settings); + /// Change a bonus item. + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default); + /// Change the stain for any equipment piece. - public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings = default) - => ChangeEquip(data, slot, null, stain, settings); + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default) + => ChangeEquip(data, slot, null, stains, settings); /// Change an equipment piece and its stain at the same time. - public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings settings = default); + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings = default); /// Change the crest visibility for any equipment piece. public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default); diff --git a/Glamourer/Designs/IDesignStandIn.cs b/Glamourer/Designs/IDesignStandIn.cs index 492bc6b..d07acb9 100644 --- a/Glamourer/Designs/IDesignStandIn.cs +++ b/Glamourer/Designs/IDesignStandIn.cs @@ -16,7 +16,7 @@ public interface IDesignStandIn : IEquatable public string SerializeName(); public StateSource AssociatedSource(); - public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks { get; } + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication); public void AddData(JObject jObj); @@ -25,4 +25,7 @@ public interface IDesignStandIn : IEquatable public bool ChangeData(object data); public bool ForcedRedraw { get; } + + public bool ResetAdvancedDyes { get; } + public bool ResetTemporarySettings { get; } } diff --git a/Glamourer/Designs/Links/DesignLinkLoader.cs b/Glamourer/Designs/Links/DesignLinkLoader.cs index 4d438bd..24138a8 100644 --- a/Glamourer/Designs/Links/DesignLinkLoader.cs +++ b/Glamourer/Designs/Links/DesignLinkLoader.cs @@ -1,7 +1,8 @@ -using Dalamud.Interface.Internal.Notifications; -using OtterGui; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; +using Notification = OtterGui.Classes.Notification; namespace Glamourer.Designs.Links; diff --git a/Glamourer/Designs/Links/DesignLinkManager.cs b/Glamourer/Designs/Links/DesignLinkManager.cs index 76d9c9a..df1f147 100644 --- a/Glamourer/Designs/Links/DesignLinkManager.cs +++ b/Glamourer/Designs/Links/DesignLinkManager.cs @@ -1,4 +1,5 @@ using Glamourer.Automation; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; using OtterGui.Services; @@ -67,7 +68,7 @@ public sealed class DesignLinkManager : IService, IDisposable _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); } - private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, object? _) + private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) return; diff --git a/Glamourer/Designs/Links/DesignMerger.cs b/Glamourer/Designs/Links/DesignMerger.cs index 558377a..847d5f1 100644 --- a/Glamourer/Designs/Links/DesignMerger.cs +++ b/Glamourer/Designs/Links/DesignMerger.cs @@ -1,4 +1,6 @@ -using Glamourer.Automation; +using Glamourer.Api.Enums; +using Glamourer.Automation; +using Glamourer.Designs.Special; using Glamourer.GameData; using Glamourer.Interop.Material; using Glamourer.Services; @@ -23,8 +25,7 @@ public class DesignMerger( modAssociations); public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize, - in DesignData baseRef, - bool respectOwnership, bool modAssociations) + in DesignData baseRef, bool respectOwnership, bool modAssociations) { var ret = new MergedDesign(designManager); ret.Design.SetCustomize(_customize, currentCustomize); @@ -42,19 +43,24 @@ public class DesignMerger( if (!data.IsHuman) continue; - var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyMeta) = type.ApplyWhat(design); - ReduceMeta(data, applyMeta, ret, source); - ReduceCustomize(data, customizeFlags, ref fixFlags, ret, source, respectOwnership, startBodyType); - ReduceEquip(data, equipFlags, ret, source, respectOwnership); - ReduceMainhands(data, jobs, equipFlags, ret, source, respectOwnership); - ReduceOffhands(data, jobs, equipFlags, ret, source, respectOwnership); - ReduceCrests(data, crestFlags, ret, source); - ReduceParameters(data, parameterFlags, ret, source); + var collection = type.ApplyWhat(design); + ReduceMeta(data, collection.Meta, ret, source); + ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType); + ReduceEquip(data, collection.Equip, ret, source, respectOwnership); + ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership); + ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership); + ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership); + ReduceCrests(data, collection.Crest, ret, source); + ReduceParameters(data, collection.Parameters, ret, source); ReduceMods(design as Design, ret, modAssociations); if (type.HasFlag(ApplicationType.GearCustomization)) ReduceMaterials(design, ret); if (design.ForcedRedraw) ret.ForcedRedraw = true; + if (design.ResetAdvancedDyes) + ret.ResetAdvancedDyes = true; + if (design.ResetTemporarySettings) + ret.ResetTemporarySettings = true; } ApplyFixFlags(ret, fixFlags); @@ -83,7 +89,7 @@ public class DesignMerger( private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source) { - applyMeta &= ~ret.Design.ApplyMeta; + applyMeta &= ~ret.Design.Application.Meta; if (applyMeta == 0) return; @@ -100,7 +106,7 @@ public class DesignMerger( private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source) { - crestFlags &= ~ret.Design.ApplyCrest; + crestFlags &= ~ret.Design.Application.Crest; if (crestFlags == 0) return; @@ -118,7 +124,7 @@ public class DesignMerger( private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret, StateSource source) { - parameterFlags &= ~ret.Design.ApplyParameters; + parameterFlags &= ~ret.Design.Application.Parameters; if (parameterFlags == 0) return; @@ -136,7 +142,7 @@ public class DesignMerger( private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, bool respectOwnership) { - equipFlags &= ~ret.Design.ApplyEquip; + equipFlags &= ~ret.Design.Application.Equip; if (equipFlags == 0) return; @@ -174,6 +180,22 @@ public class DesignMerger( } } + private void ReduceBonusItems(in DesignData design, BonusItemFlag bonusItems, MergedDesign ret, StateSource source, bool respectOwnership) + { + bonusItems &= ~ret.Design.Application.BonusItem; + if (bonusItems == 0) + return; + + foreach (var slot in BonusExtensions.AllFlags.Where(b => bonusItems.HasFlag(b))) + { + var item = design.BonusItem(slot); + if (!respectOwnership || true) // TODO: maybe check unlocks + ret.Design.GetDesignDataRef().SetBonusItem(slot, item); + ret.Design.SetApplyBonusItem(slot, true); + ret.Sources[slot] = source; + } + } + private void ReduceMainhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source, bool respectOwnership) { diff --git a/Glamourer/Designs/Links/LinkContainer.cs b/Glamourer/Designs/Links/LinkContainer.cs index ef67688..6cfc121 100644 --- a/Glamourer/Designs/Links/LinkContainer.cs +++ b/Glamourer/Designs/Links/LinkContainer.cs @@ -14,6 +14,16 @@ public sealed class LinkContainer : List public new int Count => base.Count + After.Count; + public LinkContainer Clone() + { + var ret = new LinkContainer(); + ret.EnsureCapacity(base.Count); + ret.After.EnsureCapacity(After.Count); + ret.AddRange(this); + ret.After.AddRange(After); + return ret; + } + public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder) { var fromList = fromOrder switch @@ -89,13 +99,15 @@ public sealed class LinkContainer : List if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order)) { - error = $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction."; + error = + $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction."; return false; } if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order)) { - error = $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction."; + error = + $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction."; return false; } diff --git a/Glamourer/Designs/Links/MergedDesign.cs b/Glamourer/Designs/Links/MergedDesign.cs index d3c7664..3d81cda 100644 --- a/Glamourer/Designs/Links/MergedDesign.cs +++ b/Glamourer/Designs/Links/MergedDesign.cs @@ -23,8 +23,8 @@ public readonly struct WeaponList _list.Add(type, list); } - var remainingFlags = list.Select(t => t.Item3) - .Aggregate(flags, (current, existingFlags) => current & ~existingFlags); + var existingFlags = list.Count == 0 ? 0 : list.Select(t => t.Item3).Aggregate((t, existing) => t | existing); + var remainingFlags = flags & ~existingFlags; if (remainingFlags == 0) return false; @@ -33,7 +33,7 @@ public readonly struct WeaponList return true; } - public bool TryGet(FullEquipType type, JobId id, out (EquipItem, StateSource) ret) + public bool TryGet(FullEquipType type, JobId id, bool gameStateAllowed, out (EquipItem, StateSource) ret) { if (!_list.TryGetValue(type, out var list)) { @@ -45,7 +45,7 @@ public readonly struct WeaponList foreach (var (item, source, flags) in list) { - if (flags.HasFlag(flag)) + if (flags.HasFlag(flag) && (gameStateAllowed || source is not StateSource.Game)) { ret = (item, source); return true; @@ -64,12 +64,8 @@ public sealed class MergedDesign { public MergedDesign(DesignManager designManager) { - Design = designManager.CreateTemporary(); - Design.ApplyEquip = 0; - Design.ApplyCustomize = 0; - Design.ApplyCrest = 0; - Design.ApplyParameters = 0; - Design.ApplyMeta = 0; + Design = designManager.CreateTemporary(); + Design.Application = ApplicationCollection.None; } public MergedDesign(DesignBase design) @@ -104,4 +100,6 @@ public sealed class MergedDesign public readonly SortedList AssociatedMods = []; public StateSources Sources = new(); public bool ForcedRedraw; + public bool ResetAdvancedDyes; + public bool ResetTemporarySettings; } diff --git a/Glamourer/Designs/MetaIndex.cs b/Glamourer/Designs/MetaIndex.cs index edbf7b6..1842ae3 100644 --- a/Glamourer/Designs/MetaIndex.cs +++ b/Glamourer/Designs/MetaIndex.cs @@ -1,4 +1,5 @@ -using Glamourer.State; +using Glamourer.Api.Enums; +using Glamourer.State; namespace Glamourer.Designs; @@ -9,23 +10,15 @@ public enum MetaIndex VisorState = StateIndex.MetaVisorState, WeaponState = StateIndex.MetaWeaponState, ModelId = StateIndex.MetaModelId, -} - -[Flags] -public enum MetaFlag : byte -{ - Wetness = 0x01, - HatState = 0x02, - VisorState = 0x04, - WeaponState = 0x08, + EarState = StateIndex.MetaEarState, } public static class MetaExtensions { public static readonly IReadOnlyList AllRelevant = - [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState]; + [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState]; - public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; + public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState; public static MetaFlag ToFlag(this MetaIndex index) => index switch @@ -34,6 +27,7 @@ public static class MetaExtensions MetaIndex.HatState => MetaFlag.HatState, MetaIndex.VisorState => MetaFlag.VisorState, MetaIndex.WeaponState => MetaFlag.WeaponState, + MetaIndex.EarState => MetaFlag.EarState, _ => (MetaFlag)byte.MaxValue, }; @@ -44,9 +38,24 @@ public static class MetaExtensions MetaFlag.HatState => MetaIndex.HatState, MetaFlag.VisorState => MetaIndex.VisorState, MetaFlag.WeaponState => MetaIndex.WeaponState, + MetaFlag.EarState => MetaIndex.EarState, _ => (MetaIndex)byte.MaxValue, }; + public static IEnumerable ToIndices(this MetaFlag index) + { + if (index.HasFlag(MetaFlag.Wetness)) + yield return MetaIndex.Wetness; + if (index.HasFlag(MetaFlag.HatState)) + yield return MetaIndex.HatState; + if (index.HasFlag(MetaFlag.VisorState)) + yield return MetaIndex.VisorState; + if (index.HasFlag(MetaFlag.WeaponState)) + yield return MetaIndex.WeaponState; + if (index.HasFlag(MetaFlag.EarState)) + yield return MetaIndex.EarState; + } + public static string ToName(this MetaIndex index) => index switch { @@ -54,6 +63,7 @@ public static class MetaExtensions MetaIndex.VisorState => "Visor Toggled", MetaIndex.WeaponState => "Weapon Visible", MetaIndex.Wetness => "Force Wetness", + MetaIndex.EarState => "Ears Visible", _ => "Unknown Meta", }; @@ -64,6 +74,7 @@ public static class MetaExtensions MetaIndex.VisorState => "Toggle the visor state of the characters head gear.", MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.", MetaIndex.Wetness => "Force the character to be wet or not.", + MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)", _ => string.Empty, }; } diff --git a/Glamourer/Designs/Special/QuickSelectedDesign.cs b/Glamourer/Designs/Special/QuickSelectedDesign.cs index c506f0a..740bb7f 100644 --- a/Glamourer/Designs/Special/QuickSelectedDesign.cs +++ b/Glamourer/Designs/Special/QuickSelectedDesign.cs @@ -39,8 +39,8 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ public StateSource AssociatedSource() => StateSource.Manual; - public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks - => combo.Design?.AllLinks ?? []; + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) + => combo.Design?.AllLinks(newApplication) ?? []; public void AddData(JObject jObj) { } @@ -53,4 +53,10 @@ public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IServ public bool ForcedRedraw => combo.Design?.ForcedRedraw ?? false; + + public bool ResetAdvancedDyes + => combo.Design?.ResetAdvancedDyes ?? false; + + public bool ResetTemporarySettings + => combo.Design?.ResetTemporarySettings ?? false; } diff --git a/Glamourer/Designs/Special/RandomDesign.cs b/Glamourer/Designs/Special/RandomDesign.cs index 5fac61b..844f203 100644 --- a/Glamourer/Designs/Special/RandomDesign.cs +++ b/Glamourer/Designs/Special/RandomDesign.cs @@ -12,7 +12,8 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn public const string ResolvedName = "Random"; private Design? _currentDesign; - public IReadOnlyList Predicates { get; private set; } = []; + public IReadOnlyList Predicates { get; private set; } = []; + public bool ResetOnRedraw { get; set; } = false; public string ResolveName(bool _) => ResolvedName; @@ -40,45 +41,62 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn public bool Equals(IDesignStandIn? other) => other is RandomDesign r + && r.ResetOnRedraw == ResetOnRedraw && string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates), StringComparison.OrdinalIgnoreCase); public StateSource AssociatedSource() => StateSource.Manual; - public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) { - get - { + if (newApplication || ResetOnRedraw) _currentDesign = rng.Design(Predicates); - if (_currentDesign == null) - yield break; + else + _currentDesign ??= rng.Design(Predicates); + if (_currentDesign == null) + yield break; - foreach (var (link, type, jobs) in _currentDesign.AllLinks) - yield return (link, type, jobs); - } + foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication)) + yield return (link, type, jobs); } public void AddData(JObject jObj) { - jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates); + jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates); + jObj["ResetOnRedraw"] = ResetOnRedraw; } public void ParseData(JObject jObj) { var restrictions = jObj["Restrictions"]?.ToObject() ?? string.Empty; - Predicates = RandomPredicate.GeneratePredicates(restrictions); + Predicates = RandomPredicate.GeneratePredicates(restrictions); + ResetOnRedraw = jObj["ResetOnRedraw"]?.ToObject() ?? false; } public bool ChangeData(object data) { - if (data is not List predicates) - return false; + if (data is List predicates) + { + Predicates = predicates; + return true; + } - Predicates = predicates; - return true; + if (data is bool resetOnRedraw) + { + ResetOnRedraw = resetOnRedraw; + return true; + } + + return false; } public bool ForcedRedraw - => false; + => _currentDesign?.ForcedRedraw ?? false; + + public bool ResetAdvancedDyes + => _currentDesign?.ResetAdvancedDyes ?? false; + + public bool ResetTemporarySettings + => _currentDesign?.ResetTemporarySettings ?? false; } diff --git a/Glamourer/Designs/Special/RandomDesignGenerator.cs b/Glamourer/Designs/Special/RandomDesignGenerator.cs index 7ed4452..b1e1e7c 100644 --- a/Glamourer/Designs/Special/RandomDesignGenerator.cs +++ b/Glamourer/Designs/Special/RandomDesignGenerator.cs @@ -1,19 +1,33 @@ -using OtterGui.Services; +using OtterGui; +using OtterGui.Services; namespace Glamourer.Designs.Special; -public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem) : IService +public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem, Configuration config) : IService { - private readonly Random _rng = new(); + private readonly Random _rng = new(); + private readonly WeakReference _lastDesign = new(null!, false); public Design? Design(IReadOnlyList localDesigns) { - if (localDesigns.Count == 0) + if (localDesigns.Count is 0) return null; var idx = _rng.Next(0, localDesigns.Count); - Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {localDesigns[idx].Incognito}."); - return localDesigns[idx]; + if (localDesigns.Count is 1) + { + _lastDesign.SetTarget(localDesigns[idx]); + return localDesigns[idx]; + } + + if (config.PreventRandomRepeats && _lastDesign.TryGetTarget(out var lastDesign)) + while (lastDesign == localDesigns[idx]) + idx = _rng.Next(0, localDesigns.Count); + + var design = localDesigns[idx]; + Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {design.Incognito}."); + _lastDesign.SetTarget(design); + return design; } public Design? Design() @@ -24,12 +38,12 @@ public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileS public Design? Design(IReadOnlyList predicates) { - if (predicates.Count == 0) - return Design(); - if (predicates.Count == 1) - return Design(predicates[0]); - - return Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()); + return predicates.Count switch + { + 0 => Design(), + 1 => Design(predicates[0]), + _ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()), + }; } public Design? Design(string restrictions) diff --git a/Glamourer/Designs/Special/RandomPredicate.cs b/Glamourer/Designs/Special/RandomPredicate.cs index efb3233..ae05f8f 100644 --- a/Glamourer/Designs/Special/RandomPredicate.cs +++ b/Glamourer/Designs/Special/RandomPredicate.cs @@ -22,7 +22,7 @@ public interface IDesignPredicate : designs; private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs) - => (d, d.Name.Lower, d.Identifier.ToString(), fs.FindLeaf(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty); + => (d, d.Name.Lower, d.Identifier.ToString(), fs.TryGetValue(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty); } public static class RandomPredicate diff --git a/Glamourer/Designs/Special/RevertDesign.cs b/Glamourer/Designs/Special/RevertDesign.cs index 023d5eb..4caf7b6 100644 --- a/Glamourer/Designs/Special/RevertDesign.cs +++ b/Glamourer/Designs/Special/RevertDesign.cs @@ -29,9 +29,9 @@ public class RevertDesign : IDesignStandIn public StateSource AssociatedSource() => StateSource.Game; - public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _) { - get { yield return (this, ApplicationType.All, JobFlag.All); } + yield return (this, ApplicationType.All, JobFlag.All); } public void AddData(JObject jObj) @@ -45,4 +45,10 @@ public class RevertDesign : IDesignStandIn public bool ForcedRedraw => false; + + public bool ResetAdvancedDyes + => true; + + public bool ResetTemporarySettings + => true; } diff --git a/Glamourer/EphemeralConfig.cs b/Glamourer/EphemeralConfig.cs index 027685f..98dabec 100644 --- a/Glamourer/EphemeralConfig.cs +++ b/Glamourer/EphemeralConfig.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Gui; using Glamourer.Services; using Newtonsoft.Json; @@ -20,6 +20,10 @@ public class EphemeralConfig : ISavable public Guid SelectedQuickDesign { get; set; } = Guid.Empty; public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion; + public float CurrentDesignSelectorWidth { get; set; } = 200f; + public float DesignSelectorMinimumScale { get; set; } = 0.1f; + public float DesignSelectorMaximumScale { get; set; } = 0.5f; + [JsonIgnore] private readonly SaveService _saveService; diff --git a/Glamourer/Events/AutoRedrawChanged.cs b/Glamourer/Events/AutoRedrawChanged.cs new file mode 100644 index 0000000..a8dd03a --- /dev/null +++ b/Glamourer/Events/AutoRedrawChanged.cs @@ -0,0 +1,16 @@ +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when the auto-reload gear setting is changed in glamourer configuration. +/// +public sealed class AutoRedrawChanged() + : EventWrapper(nameof(AutoRedrawChanged)) +{ + public enum Priority + { + /// + StateApi = int.MinValue, + } +} \ No newline at end of file diff --git a/Glamourer/Events/AutomationChanged.cs b/Glamourer/Events/AutomationChanged.cs index 26f799a..c368899 100644 --- a/Glamourer/Events/AutomationChanged.cs +++ b/Glamourer/Events/AutomationChanged.cs @@ -37,6 +37,9 @@ public sealed class AutomationChanged() /// Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. ChangedBase, + /// Change the resetting of temporary settings for a given set. Additional data is the new value. + ChangedTemporarySettingsReset, + /// Add a new associated design to a given set. Additional data is the index it got added at [int]. AddedDesign, diff --git a/Glamourer/Events/BonusSlotUpdating.cs b/Glamourer/Events/BonusSlotUpdating.cs new file mode 100644 index 0000000..3f6e761 --- /dev/null +++ b/Glamourer/Events/BonusSlotUpdating.cs @@ -0,0 +1,25 @@ +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a model flags a bonus slot for an update. +/// +/// Parameter is the model with a flagged slot. +/// Parameter is the bonus slot changed. +/// Parameter is the model values to change the bonus piece to. +/// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. +/// +/// +public sealed class BonusSlotUpdating() + : EventWrapperRef34(nameof(BonusSlotUpdating)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 1837aad..04bb46a 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -1,4 +1,5 @@ using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Gui; using OtterGui.Classes; @@ -13,107 +14,122 @@ namespace Glamourer.Events; /// /// public sealed class DesignChanged() - : EventWrapper(nameof(DesignChanged)) + : EventWrapper(nameof(DesignChanged)) { public enum Type { - /// A new design was created. Data is a potential path to move it to [string?]. + /// A new design was created. Created, - /// An existing design was deleted. Data is null. + /// An existing design was deleted. Deleted, - /// Invoked on full reload. Design and Data are null. + /// Invoked on full reload. ReloadedAll, - /// An existing design was renamed. Data is the prior name [string]. + /// An existing design was renamed. Renamed, - /// An existing design had its description changed. Data is the prior description [string]. + /// An existing design had its description changed. ChangedDescription, - /// An existing design had its associated color changed. Data is the prior color [string]. + /// An existing design had its associated color changed. ChangedColor, - /// An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. + /// An existing design had a new tag added. AddedTag, - /// An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. + /// An existing design had an existing tag removed. RemovedTag, - /// An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. + /// An existing design had an existing tag renamed. ChangedTag, - /// An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had a new associated mod added. AddedMod, - /// An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had an existing associated mod removed. RemovedMod, - /// An existing design had a link to a different design added, removed or moved. Data is null. + /// An existing design had an existing associated mod updated. + UpdatedMod, + + /// An existing design had a link to a different design added, removed or moved. ChangedLink, - /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + /// An existing design had a customization changed. Customize, - /// An existing design had its entire customize array changed. Data is the old array, the applied flags and the changed flags. [(CustomizeArray, CustomizeFlag, CustomizeFlag)]. + /// An existing design had its entire customize array changed. EntireCustomize, - /// An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + /// An existing design had an equipment piece changed. Equip, - /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand, the new offhand (if any) and the new gauntlets (if any). [(EquipItem, EquipItem, EquipItem, EquipItem?, EquipItem?)]. + /// An existing design had a bonus item changed. + BonusItem, + + /// An existing design had its weapons changed. Weapon, - /// An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. - Stain, + /// An existing design had a stain changed. + Stains, - /// An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. + /// An existing design had a crest visibility changed. Crest, - /// An existing design had a customize parameter changed. Data is the old value, the new value and the flag [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. + /// An existing design had a customize parameter changed. Parameter, - /// An existing design had an advanced dye row added, changed, or deleted. Data is the old value, the new value and the index [(ColorRow?, ColorRow?, MaterialValueIndex)]. + /// An existing design had an advanced dye row added, changed, or deleted. Material, - /// An existing design had an advanced dye rows Revert state changed. Data is the index [MaterialValueIndex]. + /// An existing design had an advanced dye rows Revert state changed. MaterialRevert, /// An existing design had changed whether it always forces a redraw or not. ForceRedraw, - /// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. + /// An existing design had changed whether it always resets advanced dyes or not. + ResetAdvancedDyes, + + /// An existing design had changed whether it always resets all prior temporary settings or not. + ResetTemporarySettings, + + /// An existing design changed whether a specific customization is applied. ApplyCustomize, - /// An existing design changed whether a specific equipment piece is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific equipment piece is applied. ApplyEquip, - /// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific bonus item is applied. + ApplyBonusItem, + + /// An existing design changed whether a specific stain is applied. ApplyStain, - /// An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific crest visibility is applied. ApplyCrest, - /// An existing design changed whether a specific customize parameter is applied. Data is the flag for the parameter [CustomizeParameterFlag]. + /// An existing design changed whether a specific customize parameter is applied. ApplyParameter, - /// An existing design changed whether an advanced dye row is applied. Data is the index [MaterialValueIndex]. + /// An existing design changed whether an advanced dye row is applied. ApplyMaterial, - /// An existing design changed its write protection status. Data is the new value [bool]. + /// An existing design changed its write protection status. WriteProtection, - /// An existing design changed its display status for the quick design bar. Data is the new value [bool]. + /// An existing design changed its display status for the quick design bar. QuickDesignBar, - /// An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. + /// An existing design changed one of the meta flags. Other, } public enum Priority { - /// + /// DesignLinkManager = 1, /// @@ -125,7 +141,10 @@ public sealed class DesignChanged() /// DesignFileSystemSelector = -1, - /// + /// DesignCombo = -2, + + /// + EditorHistory = -1000, } } diff --git a/Glamourer/Events/SlotUpdating.cs b/Glamourer/Events/EquipSlotUpdating.cs similarity index 80% rename from Glamourer/Events/SlotUpdating.cs rename to Glamourer/Events/EquipSlotUpdating.cs index 8a766fb..a2daf85 100644 --- a/Glamourer/Events/SlotUpdating.cs +++ b/Glamourer/Events/EquipSlotUpdating.cs @@ -14,12 +14,12 @@ namespace Glamourer.Events; /// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. /// /// -public sealed class SlotUpdating() - : EventWrapperRef34(nameof(SlotUpdating)) +public sealed class EquipSlotUpdating() + : EventWrapperRef34(nameof(EquipSlotUpdating)) { public enum Priority { - /// + /// StateListener = 0, } -} +} \ No newline at end of file diff --git a/Glamourer/Events/GPoseService.cs b/Glamourer/Events/GPoseService.cs index a84f1d6..44421a0 100644 --- a/Glamourer/Events/GPoseService.cs +++ b/Glamourer/Events/GPoseService.cs @@ -13,8 +13,8 @@ public sealed class GPoseService : EventWrapper public enum Priority { - /// - GlamourerIpc = int.MinValue, + /// + StateApi = int.MinValue, } public bool InGPose { get; private set; } diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs new file mode 100644 index 0000000..620bdab --- /dev/null +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -0,0 +1,21 @@ +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData) +/// This defines an endpoint for when the gameState is updated. +/// +/// The model draw object associated with the finished load (Also fired by other players on render) +/// +/// +public sealed class GearsetDataLoaded() + : EventWrapper(nameof(GearsetDataLoaded)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/MovedEquipment.cs b/Glamourer/Events/MovedEquipment.cs index 53491f1..9d24a03 100644 --- a/Glamourer/Events/MovedEquipment.cs +++ b/Glamourer/Events/MovedEquipment.cs @@ -11,7 +11,7 @@ namespace Glamourer.Events; /// /// public sealed class MovedEquipment() - : EventWrapper<(EquipSlot, uint, StainId)[], MovedEquipment.Priority>(nameof(MovedEquipment)) + : EventWrapper<(EquipSlot, uint, StainIds)[], MovedEquipment.Priority>(nameof(MovedEquipment)) { public enum Priority { diff --git a/Glamourer/Events/PenumbraReloaded.cs b/Glamourer/Events/PenumbraReloaded.cs index 7df8b3f..0975670 100644 --- a/Glamourer/Events/PenumbraReloaded.cs +++ b/Glamourer/Events/PenumbraReloaded.cs @@ -12,5 +12,11 @@ public sealed class PenumbraReloaded() { /// ChangeCustomizeService = 0, + + /// + VisorService = 0, + + /// + VieraEarService = 0, } } diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 641665c..2bcc6fe 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -1,7 +1,9 @@ using Glamourer.Api.Enums; +using Glamourer.Designs.History; using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Classes; +using Penumbra.GameData.Interop; namespace Glamourer.Events; @@ -15,11 +17,17 @@ namespace Glamourer.Events; /// /// public sealed class StateChanged() - : EventWrapper(nameof(StateChanged)) + : EventWrapper(nameof(StateChanged)) { public enum Priority { - GlamourerIpc = int.MinValue, + /// + GlamourerIpc = int.MinValue, + + /// PenumbraAutoRedraw = 0, + + /// + EditorHistory = -1000, } -} \ No newline at end of file +} diff --git a/Glamourer/Events/StateFinalized.cs b/Glamourer/Events/StateFinalized.cs new file mode 100644 index 0000000..0ccaa8b --- /dev/null +++ b/Glamourer/Events/StateFinalized.cs @@ -0,0 +1,24 @@ +using Glamourer.Api; +using Glamourer.Api.Enums; +using Glamourer.Interop.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggered when a set of grouped changes finishes being applied to a Glamourer state. +/// +/// Parameter is the operation that finished updating the saved state. +/// Parameter is the existing actors using this saved state. +/// +/// +public sealed class StateFinalized() + : EventWrapper(nameof(StateFinalized)) +{ + public enum Priority + { + /// + StateApi = int.MinValue, + } +} diff --git a/Glamourer/Events/VieraEarStateChanged.cs b/Glamourer/Events/VieraEarStateChanged.cs new file mode 100644 index 0000000..65730b8 --- /dev/null +++ b/Glamourer/Events/VieraEarStateChanged.cs @@ -0,0 +1,22 @@ +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggered when the state of viera ear visibility for any draw object is changed. +/// +/// Parameter is the model with a changed viera ear visibility state. +/// Parameter is the new state. +/// Parameter is whether to call the original function. +/// +/// +public sealed class VieraEarStateChanged() + : EventWrapperRef2(nameof(VieraEarStateChanged)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/VisorStateChanged.cs b/Glamourer/Events/VisorStateChanged.cs index d2d3a6c..03b7336 100644 --- a/Glamourer/Events/VisorStateChanged.cs +++ b/Glamourer/Events/VisorStateChanged.cs @@ -19,4 +19,4 @@ public sealed class VisorStateChanged() /// StateListener = 0, } -} +} \ No newline at end of file diff --git a/Glamourer/GameData/CharaMakeParams.cs b/Glamourer/GameData/CharaMakeParams.cs deleted file mode 100644 index 4db5825..0000000 --- a/Glamourer/GameData/CharaMakeParams.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Lumina.Data; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; - -namespace Glamourer.GameData; - -/// A custom version of CharaMakeParams that is easier to parse. -[Sheet("CharaMakeParams")] -public class CharaMakeParams : ExcelRow -{ - public const int NumMenus = 28; - public const int NumVoices = 12; - public const int NumGraphics = 10; - public const int MaxNumValues = 100; - public const int NumFaces = 8; - public const int NumFeatures = 7; - public const int NumEquip = 3; - - public enum MenuType - { - ListSelector = 0, - IconSelector = 1, - ColorPicker = 2, - DoubleColorPicker = 3, - IconCheckmark = 4, - Percentage = 5, - Checkmark = 6, // custom - Nothing = 7, // custom - List1Selector = 8, // custom, 1-indexed lists - } - - public struct Menu - { - public uint Id; - public byte InitVal; - public MenuType Type; - public byte Size; - public byte LookAt; - public uint Mask; - public uint Customize; - public uint[] Values; - public byte[] Graphic; - } - - public struct FacialFeatures - { - public uint[] Icons; - } - - public LazyRow Race { get; set; } = null!; - public LazyRow Tribe { get; set; } = null!; - public sbyte Gender { get; set; } - - public Menu[] Menus { get; set; } = new Menu[NumMenus]; - public byte[] Voices { get; set; } = new byte[NumVoices]; - public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces]; - - public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip]; - - public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language) - { - RowId = parser.RowId; - SubRowId = parser.SubRowId; - Race = new LazyRow(gameData, parser.ReadColumn(0), language); - Tribe = new LazyRow(gameData, parser.ReadColumn(1), language); - Gender = parser.ReadColumn(2); - int currentOffset; - for (var i = 0; i < NumMenus; ++i) - { - currentOffset = 3 + i; - Menus[i].Id = parser.ReadColumn(0 * NumMenus + currentOffset); - Menus[i].InitVal = parser.ReadColumn(1 * NumMenus + currentOffset); - Menus[i].Type = (MenuType)parser.ReadColumn(2 * NumMenus + currentOffset); - Menus[i].Size = parser.ReadColumn(3 * NumMenus + currentOffset); - Menus[i].LookAt = parser.ReadColumn(4 * NumMenus + currentOffset); - Menus[i].Mask = parser.ReadColumn(5 * NumMenus + currentOffset); - Menus[i].Customize = parser.ReadColumn(6 * NumMenus + currentOffset); - Menus[i].Values = new uint[Menus[i].Size]; - - switch (Menus[i].Type) - { - case MenuType.ColorPicker: - case MenuType.DoubleColorPicker: - case MenuType.Percentage: - break; - default: - currentOffset += 7 * NumMenus; - for (var j = 0; j < Menus[i].Size; ++j) - Menus[i].Values[j] = parser.ReadColumn(j * NumMenus + currentOffset); - break; - } - - Menus[i].Graphic = new byte[NumGraphics]; - currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i; - for (var j = 0; j < NumGraphics; ++j) - Menus[i].Graphic[j] = parser.ReadColumn(j * NumMenus + currentOffset); - } - - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus; - for (var i = 0; i < NumVoices; ++i) - Voices[i] = parser.ReadColumn(currentOffset++); - - for (var i = 0; i < NumFaces; ++i) - { - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i; - FacialFeatureByFace[i].Icons = new uint[NumFeatures]; - for (var j = 0; j < NumFeatures; ++j) - FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn(j * NumFaces + currentOffset); - } - - for (var i = 0; i < NumEquip; ++i) - { - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7; - Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj() - { - Helmet = parser.ReadColumn(currentOffset + 0), - Top = parser.ReadColumn(currentOffset + 1), - Gloves = parser.ReadColumn(currentOffset + 2), - Legs = parser.ReadColumn(currentOffset + 3), - Shoes = parser.ReadColumn(currentOffset + 4), - Weapon = parser.ReadColumn(currentOffset + 5), - SubWeapon = parser.ReadColumn(currentOffset + 6), - }; - } - } -} diff --git a/Glamourer/GameData/CustomizeManager.cs b/Glamourer/GameData/CustomizeManager.cs index 57df104..9e065b4 100644 --- a/Glamourer/GameData/CustomizeManager.cs +++ b/Glamourer/GameData/CustomizeManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; using Dalamud.Plugin.Services; using OtterGui.Classes; using OtterGui.Services; @@ -32,8 +32,8 @@ public class CustomizeManager : IAsyncDataContainer } /// Get specific icons. - public IDalamudTextureWrap GetIcon(uint id) - => _icons.LoadIcon(id)!; + public ISharedImmediateTexture GetIcon(uint id) + => _icons.TextureProvider.GetFromGameIcon(id); /// Iterate over all supported genders and clans. public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets() @@ -47,8 +47,8 @@ public class CustomizeManager : IAsyncDataContainer public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet) { - _icons = new IconStorage(textures, gameData); - var stopwatch = new Stopwatch(); + _icons = new TextureCache(gameData, textures); + var stopwatch = new Stopwatch(); var tmpTask = Task.Run(() => { stopwatch.Start(); @@ -72,7 +72,7 @@ public class CustomizeManager : IAsyncDataContainer public bool Finished => Awaiter.IsCompletedSuccessfully; - private readonly IconStorage _icons; + private readonly TextureCache _icons; private static readonly int ListSize = Clans.Count * Genders.Count; private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize]; diff --git a/Glamourer/GameData/CustomizeParameterData.cs b/Glamourer/GameData/CustomizeParameterData.cs index f10289f..3a04938 100644 --- a/Glamourer/GameData/CustomizeParameterData.cs +++ b/Glamourer/GameData/CustomizeParameterData.cs @@ -12,7 +12,9 @@ public struct CustomizeParameterData public Vector3 HairSpecular; public Vector3 HairHighlight; public Vector3 LeftEye; + public float LeftLimbalIntensity; public Vector3 RightEye; + public float RightLimbalIntensity; public Vector3 FeatureColor; public float FacePaintUvMultiplier; public float FacePaintUvOffset; @@ -33,7 +35,9 @@ public struct CustomizeParameterData CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular), CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight), CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye), + CustomizeParameterFlag.LeftLimbalIntensity => new CustomizeParameterValue(LeftLimbalIntensity), CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye), + CustomizeParameterFlag.RightLimbalIntensity => new CustomizeParameterValue(RightLimbalIntensity), CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor), CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor), CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier), @@ -57,7 +61,9 @@ public struct CustomizeParameterData CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple), CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple), CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple), + CustomizeParameterFlag.LeftLimbalIntensity => SetIfDifferent(ref LeftLimbalIntensity, value.Single), CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple), + CustomizeParameterFlag.RightLimbalIntensity => SetIfDifferent(ref RightLimbalIntensity, value.Single), CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple), CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple), CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single), @@ -77,30 +83,48 @@ public struct CustomizeParameterData _ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple, }; - parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.FacePaintUvMultiplier)) switch + parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.LeftLimbalIntensity)) switch { - 0 => parameters.LeftColor, - CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple, - CustomizeParameterFlag.FacePaintUvMultiplier => parameters.LeftColor with { W = FacePaintUvMultiplier }, - _ => new CustomizeParameterValue(LeftEye, FacePaintUvMultiplier).XivQuadruple, + 0 => parameters.LeftColor, + CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple, + CustomizeParameterFlag.LeftLimbalIntensity => parameters.LeftColor with { W = LeftLimbalIntensity }, + _ => new CustomizeParameterValue(LeftEye, LeftLimbalIntensity).XivQuadruple, }; - parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.FacePaintUvOffset)) switch + parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.RightLimbalIntensity)) switch { - 0 => parameters.RightColor, - CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple, - CustomizeParameterFlag.FacePaintUvOffset => parameters.RightColor with { W = FacePaintUvOffset }, - _ => new CustomizeParameterValue(RightEye, FacePaintUvOffset).XivQuadruple, + 0 => parameters.RightColor, + CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple, + CustomizeParameterFlag.RightLimbalIntensity => parameters.RightColor with { W = RightLimbalIntensity }, + _ => new CustomizeParameterValue(RightEye, RightLimbalIntensity).XivQuadruple, }; if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular)) parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple; if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse)) - parameters.MainColor = new CustomizeParameterValue(HairDiffuse).XivTriple; + { + // Vector3 is 0x10 byte for some reason. + var triple = new CustomizeParameterValue(HairDiffuse).XivTriple; + parameters.MainColor.X = triple.X; + parameters.MainColor.Y = triple.Y; + parameters.MainColor.Z = triple.Z; + } + if (flags.HasFlag(CustomizeParameterFlag.HairSpecular)) parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; if (flags.HasFlag(CustomizeParameterFlag.HairHighlight)) - parameters.MeshColor = new CustomizeParameterValue(HairHighlight).XivTriple; + { + // Vector3 is 0x10 byte for some reason. + var triple = new CustomizeParameterValue(HairHighlight).XivTriple; + parameters.MeshColor.X = triple.X; + parameters.MeshColor.Y = triple.Y; + parameters.MeshColor.Z = triple.Z; + } + + if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvMultiplier)) + GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier; + if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvOffset)) + GetUvOffsetWrite(ref parameters) = FacePaintUvOffset; if (flags.HasFlag(CustomizeParameterFlag.LipDiffuse)) parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; if (flags.HasFlag(CustomizeParameterFlag.FeatureColor)) @@ -132,13 +156,21 @@ public struct CustomizeParameterData parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; break; case CustomizeParameterFlag.HairDiffuse: - parameters.MainColor = new CustomizeParameterValue(HairDiffuse).XivTriple; + // Vector3 is 0x10 byte for some reason. + var triple1 = new CustomizeParameterValue(HairDiffuse).XivTriple; + parameters.MainColor.X = triple1.X; + parameters.MainColor.Y = triple1.Y; + parameters.MainColor.Z = triple1.Z; break; case CustomizeParameterFlag.HairSpecular: parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; break; case CustomizeParameterFlag.HairHighlight: - parameters.MeshColor = new CustomizeParameterValue(HairHighlight).XivTriple; + // Vector3 is 0x10 byte for some reason. + var triple2 = new CustomizeParameterValue(HairHighlight).XivTriple; + parameters.MeshColor.X = triple2.X; + parameters.MeshColor.Y = triple2.Y; + parameters.MeshColor.Z = triple2.Z; break; case CustomizeParameterFlag.LeftEye: parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple; @@ -150,10 +182,16 @@ public struct CustomizeParameterData parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple; break; case CustomizeParameterFlag.FacePaintUvMultiplier: - parameters.LeftColor.W = FacePaintUvMultiplier; + GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier; break; case CustomizeParameterFlag.FacePaintUvOffset: - parameters.RightColor.W = FacePaintUvOffset; + GetUvOffsetWrite(ref parameters) = FacePaintUvOffset; + break; + case CustomizeParameterFlag.LeftLimbalIntensity: + parameters.LeftColor.W = LeftLimbalIntensity; + break; + case CustomizeParameterFlag.RightLimbalIntensity: + parameters.RightColor.W = RightLimbalIntensity; break; } } @@ -161,8 +199,8 @@ public struct CustomizeParameterData public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal) => new() { - FacePaintUvOffset = parameter.RightColor.W, - FacePaintUvMultiplier = parameter.LeftColor.W, + FacePaintUvOffset = GetUvOffset(parameter), + FacePaintUvMultiplier = GetUvMultiplier(parameter), MuscleTone = parameter.SkinColor.W, SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple, SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple, @@ -171,7 +209,9 @@ public struct CustomizeParameterData HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple, HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple, LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple, + LeftLimbalIntensity = new CustomizeParameterValue(parameter.LeftColor.W).Single, RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple, + RightLimbalIntensity = new CustomizeParameterValue(parameter.RightColor.W).Single, FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple, DecalColor = FromParameter(decal), }; @@ -189,8 +229,8 @@ public struct CustomizeParameterData CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor), CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor), CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor), - CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(parameter.LeftColor.W), - CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(parameter.RightColor.W), + CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(GetUvMultiplier(parameter)), + CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(GetUvOffset(parameter)), _ => CustomizeParameterValue.Zero, }; @@ -223,4 +263,41 @@ public struct CustomizeParameterData val = @new; return true; } + + + private static unsafe float GetUvOffset(in CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ((float*)ptr)[23]; + } + } + + private static unsafe ref float GetUvOffsetWrite(ref CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ref ((float*)ptr)[23]; + } + } + + private static unsafe float GetUvMultiplier(in CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ((float*)ptr)[15]; + } + } + + private static unsafe ref float GetUvMultiplierWrite(ref CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ref ((float*)ptr)[15]; + } + } } diff --git a/Glamourer/GameData/CustomizeParameterFlag.cs b/Glamourer/GameData/CustomizeParameterFlag.cs index 59a3511..ff804d4 100644 --- a/Glamourer/GameData/CustomizeParameterFlag.cs +++ b/Glamourer/GameData/CustomizeParameterFlag.cs @@ -16,20 +16,27 @@ public enum CustomizeParameterFlag : ushort FacePaintUvMultiplier = 0x0400, FacePaintUvOffset = 0x0800, DecalColor = 0x1000, + LeftLimbalIntensity = 0x2000, + RightLimbalIntensity = 0x4000, } public static class CustomizeParameterExtensions { - public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x1FFF; + // Speculars are not available anymore. + public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x7FDB; public const CustomizeParameterFlag RgbTriples = All & ~(RgbaQuadruples | Percentages | Values); public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse; - public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone; + + public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone + | CustomizeParameterFlag.LeftLimbalIntensity + | CustomizeParameterFlag.RightLimbalIntensity; + public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier; - public static readonly IReadOnlyList AllFlags = [.. Enum.GetValues()]; + public static readonly IReadOnlyList AllFlags = [.. Enum.GetValues().Where(f => All.HasFlag(f))]; public static readonly IReadOnlyList RgbaFlags = AllFlags.Where(f => RgbaQuadruples.HasFlag(f)).ToArray(); public static readonly IReadOnlyList RgbFlags = AllFlags.Where(f => RgbTriples.HasFlag(f)).ToArray(); public static readonly IReadOnlyList PercentageFlags = AllFlags.Where(f => Percentages.HasFlag(f)).ToArray(); @@ -56,10 +63,12 @@ public static class CustomizeParameterExtensions CustomizeParameterFlag.HairHighlight => "Hair Highlights", CustomizeParameterFlag.LeftEye => "Left Eye Color", CustomizeParameterFlag.RightEye => "Right Eye Color", - CustomizeParameterFlag.FeatureColor => "Tattoo Color", + CustomizeParameterFlag.FeatureColor => "Feature Color", CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint", CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint", CustomizeParameterFlag.DecalColor => "Face Paint Color", + CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity", + CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity", _ => string.Empty, }; } diff --git a/Glamourer/GameData/CustomizeSet.cs b/Glamourer/GameData/CustomizeSet.cs index 0c80e13..8795c19 100644 --- a/Glamourer/GameData/CustomizeSet.cs +++ b/Glamourer/GameData/CustomizeSet.cs @@ -1,6 +1,8 @@ using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; namespace Glamourer.GameData; @@ -10,12 +12,15 @@ namespace Glamourer.GameData; /// public class CustomizeSet { - internal CustomizeSet(SubRace clan, Gender gender) + private readonly NpcCustomizeSet _npcCustomizations; + + internal CustomizeSet(NpcCustomizeSet npcCustomizations, SubRace clan, Gender gender) { - Gender = gender; - Clan = clan; - Race = clan.ToRace(); - SettingAvailable = 0; + _npcCustomizations = npcCustomizations; + Gender = gender; + Clan = clan; + Race = clan.ToRace(); + SettingAvailable = 0; } public Gender Gender { get; } @@ -38,9 +43,9 @@ public class CustomizeSet public string Option(CustomizeIndex index) => OptionName[(int)index]; - public IReadOnlyList Voices { get; internal init; } = null!; - public IReadOnlyList Types { get; internal set; } = null!; - public IReadOnlyDictionary Order { get; internal set; } = null!; + public IReadOnlyList Voices { get; internal init; } = null!; + public IReadOnlyList Types { get; internal set; } = null!; + public IReadOnlyDictionary Order { get; internal set; } = null!; // Always list selector. @@ -84,6 +89,7 @@ public class CustomizeSet { if (IsAvailable(index)) return DataByValue(index, value, out custom, face) >= 0 + || _npcCustomizations.CheckValue(index, value) || NpcOptions.Any(t => t.Type == index && t.Value == value); custom = null; @@ -97,9 +103,9 @@ public class CustomizeSet return type switch { - CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom), - CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom), - CharaMakeParams.MenuType.IconSelector => index switch + MenuType.ListSelector => GetInteger0(out custom), + MenuType.List1Selector => GetInteger1(out custom), + MenuType.IconSelector => index switch { CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom), CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles, @@ -109,7 +115,7 @@ public class CustomizeSet CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom), _ => Invalid(out custom), }, - CharaMakeParams.MenuType.ColorPicker => index switch + MenuType.ColorPicker => index switch { CustomizeIndex.SkinColor => Get(SkinColors, value, out custom), CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom), @@ -121,16 +127,16 @@ public class CustomizeSet CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), _ => Invalid(out custom), }, - CharaMakeParams.MenuType.DoubleColorPicker => index switch + MenuType.DoubleColorPicker => index switch { CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), _ => Invalid(out custom), }, - CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom), - CharaMakeParams.MenuType.Percentage => GetInteger0(out custom), - CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom), - _ => Invalid(out custom), + MenuType.IconCheckmark => GetBool(index, value, out custom), + MenuType.Percentage => GetInteger0(out custom), + MenuType.Checkmark => GetBool(index, value, out custom), + _ => Invalid(out custom), }; int Get(IEnumerable list, CustomizeValue v, out CustomizeData? output) @@ -208,10 +214,10 @@ public class CustomizeSet switch (Types[(int)index]) { - case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); - case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); - case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); - case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); + case MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); + case MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); + case MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); + case MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); } return index switch @@ -241,7 +247,7 @@ public class CustomizeSet } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public CharaMakeParams.MenuType Type(CustomizeIndex index) + public MenuType Type(CustomizeIndex index) => Types[(int)index]; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -256,9 +262,9 @@ public class CustomizeSet return Type(index) switch { - CharaMakeParams.MenuType.Percentage => 101, - CharaMakeParams.MenuType.IconCheckmark => 2, - CharaMakeParams.MenuType.Checkmark => 2, + MenuType.Percentage => 101, + MenuType.IconCheckmark => 2, + MenuType.Checkmark => 2, _ => index switch { CustomizeIndex.Face => Faces.Count, diff --git a/Glamourer/GameData/CustomizeSetFactory.cs b/Glamourer/GameData/CustomizeSetFactory.cs index ba892ec..77a6973 100644 --- a/Glamourer/GameData/CustomizeSetFactory.cs +++ b/Glamourer/GameData/CustomizeSetFactory.cs @@ -1,9 +1,9 @@ -using Dalamud; +using Dalamud.Game; using Dalamud.Plugin.Services; -using Dalamud.Utility; using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; using OtterGui.Classes; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Race = Penumbra.GameData.Enums.Race; @@ -13,11 +13,11 @@ namespace Glamourer.GameData; internal class CustomizeSetFactory( IDataManager _gameData, IPluginLog _log, - IconStorage _icons, + TextureCache _icons, NpcCustomizeSet _npcCustomizeSet, ColorParameters _colors) { - public CustomizeSetFactory(IDataManager gameData, IPluginLog log, IconStorage icons, NpcCustomizeSet npcCustomizeSet) + public CustomizeSetFactory(IDataManager gameData, IPluginLog log, TextureCache icons, NpcCustomizeSet npcCustomizeSet) : this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log)) { } @@ -28,10 +28,10 @@ internal class CustomizeSetFactory( var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var hrothgar = race.ToRace() == Race.Hrothgar; // Create the initial set with all the easily accessible parameters available for anyone. - var set = new CustomizeSet(race, gender) + var set = new CustomizeSet(_npcCustomizeSet, race, gender) { Name = GetName(race, gender), - Voices = row.Voices, + Voices = row.VoiceStruct, HairStyles = GetHairStyles(race, gender), HairColors = hair, SkinColors = skin, @@ -58,7 +58,7 @@ internal class CustomizeSetFactory( } /// Some data can not be set independently of the rest, so we need a post-processing step to finalize. - private void SetPostProcessing(CustomizeSet set, CharaMakeParams row) + private void SetPostProcessing(CustomizeSet set, in CharaMakeType row) { SetAvailability(set, row); SetFacialFeatures(set, row); @@ -76,18 +76,16 @@ internal class CustomizeSetFactory( CustomizeIndex.Hairstyle, CustomizeIndex.LipColor, CustomizeIndex.SkinColor, - CustomizeIndex.FacePaintColor, - CustomizeIndex.HighlightsColor, - CustomizeIndex.HairColor, - CustomizeIndex.FacePaint, - CustomizeIndex.TattooColor, - CustomizeIndex.EyeColorLeft, - CustomizeIndex.EyeColorRight, + CustomizeIndex.TailShape, }; - var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>(); + var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>() + { + (CustomizeIndex.Height, CustomizeValue.Max), + }; _npcCustomizeSet.Awaiter.Wait(); - foreach (var customize in _npcCustomizeSet.Select(s => s.Customize).Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1)) + foreach (var customize in _npcCustomizeSet.Select(s => s.Customize) + .Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1)) { foreach (var customizeIndex in customizeIndices) { @@ -106,10 +104,10 @@ internal class CustomizeSetFactory( } private readonly ColorParameters _colorParameters = new(_gameData, _log); - private readonly ExcelSheet _customizeSheet = _gameData.GetExcelSheet(ClientLanguage.English)!; - private readonly ExcelSheet _lobbySheet = _gameData.GetExcelSheet(ClientLanguage.English)!; - private readonly ExcelSheet _hairSheet = _gameData.GetExcelSheet(ClientLanguage.English)!; - private readonly ExcelSheet _tribeSheet = _gameData.GetExcelSheet(ClientLanguage.English)!; + private readonly ExcelSheet _customizeSheet = _gameData.GetExcelSheet(ClientLanguage.English); + private readonly ExcelSheet _lobbySheet = _gameData.GetExcelSheet(ClientLanguage.English); + private readonly ExcelSheet _hairSheet = _gameData.GetExcelSheet(ClientLanguage.English, "HairMakeType"); + private readonly ExcelSheet _tribeSheet = _gameData.GetExcelSheet(ClientLanguage.English); // Those color pickers are shared between all races. private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192); @@ -120,12 +118,7 @@ internal class CustomizeSetFactory( private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true); private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192); - private readonly ExcelSheet _charaMakeSheet = _gameData.Excel - .GetType() - .GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)? - .MakeGenericMethod(typeof(CharaMakeParams)) - .Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet - ?? null!; + private readonly ExcelSheet _charaMakeSheet = _gameData.Excel.GetSheet(); /// Obtain available skin and hair colors for the given clan and gender. private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender) @@ -144,29 +137,28 @@ internal class CustomizeSetFactory( private string GetName(SubRace race, Gender gender) => gender switch { - Gender.Male => _tribeSheet.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(), - Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? race.ToName(), + Gender.Male => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Masculine.ExtractText() : race.ToName(), + Gender.Female => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Feminine.ExtractText() : race.ToName(), _ => "Unknown", }; /// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. private CustomizeData[] GetHairStyles(SubRace race, Gender gender) { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender); // Unknown30 is the number of available hairstyles. - var hairList = new List(row.Unknown30); + var numHairs = row.ReadUInt8Column(30); + var hairList = new List(numHairs); // Hairstyles can be found starting at Unknown66. - for (var i = 0; i < row.Unknown30; ++i) + for (var i = 0; i < numHairs; ++i) { - var name = $"Unknown{66 + i * 9}"; - var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) - ?? uint.MaxValue; + // Hairs start at Unknown66. + var customizeIdx = row.ReadUInt32Column(66 + i * 9); if (customizeIdx == uint.MaxValue) continue; // Hair Row from CustomizeSheet might not be set in case of unlockable hair. - var hairRow = _customizeSheet.GetRow(customizeIdx); - if (hairRow == null) + if (!_customizeSheet.TryGetRow(customizeIdx, out var hairRow)) hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx)); else if (_icons.IconExists(hairRow.Icon)) hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, @@ -177,45 +169,40 @@ internal class CustomizeSetFactory( } /// Specific icons for tails or ears. - private CustomizeData[] GetTailEarShapes(CharaMakeParams row) - => row.Menus.Cast() - .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray() - ?? []; + private CustomizeData[] GetTailEarShapes(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.TailShape); /// Specific icons for faces. - private CustomizeData[] GetFaces(CharaMakeParams row) - => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx) - ?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray() - ?? []; + private CustomizeData[] GetFaces(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.Face); /// Specific icons for Hrothgar patterns. - private CustomizeData[] HrothgarFurPattern(CharaMakeParams row) - => row.Menus.Cast() - .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray() - ?? []; + private CustomizeData[] HrothgarFurPattern(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.LipColor); + + private CustomizeData[] ExtractValues(CharaMakeType row, CustomizeIndex type) + { + var data = row.CharaMakeStruct.FirstOrNull(m => m.Customize == type.ToByteAndMask().ByteIdx); + return data?.SubMenuParam.Take(data.Value.SubMenuNum).Select((v, i) => FromValueAndIndex(type, v, i)).ToArray() ?? []; + } /// Get face paints from the hair sheet via reflection since there are also unlockable face paints. private CustomizeData[] GetFacePaints(SubRace race, Gender gender) { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - var paintList = new List(row.Unknown37); + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender); // Number of available face paints is at Unknown37. - for (var i = 0; i < row.Unknown37; ++i) + var numPaints = row.ReadUInt8Column(37); + var paintList = new List(numPaints); + + for (var i = 0; i < numPaints; ++i) { // Face paints start at Unknown73. - var name = $"Unknown{73 + i * 9}"; - var customizeIdx = - (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) - ?? uint.MaxValue; + var customizeIdx = row.ReadUInt32Column(73 + i * 9); if (customizeIdx == uint.MaxValue) continue; - var paintRow = _customizeSheet.GetRow(customizeIdx); // Face paint Row from CustomizeSheet might not be set in case of unlockable face paints. - if (paintRow != null) + if (_customizeSheet.TryGetRow(customizeIdx, out var paintRow)) paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)); else @@ -226,21 +213,18 @@ internal class CustomizeSetFactory( } /// Get List sizes. - private static int GetListSize(CharaMakeParams row, CustomizeIndex index) + private static int GetListSize(CharaMakeType row, CustomizeIndex index) { var gameId = index.ToByteAndMask().ByteIdx; - var menu = row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == gameId); - return menu?.Size ?? 0; + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId); + return menu?.SubMenuNum ?? 0; } /// Get generic Features. private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index) - { - var row = _customizeSheet.GetRow(value); - return row == null - ? new CustomizeData(id, (CustomizeValue)(index + 1), value) - : new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId); - } + => _customizeSheet.TryGetRow(value, out var row) + ? new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId) + : new CustomizeData(id, (CustomizeValue)(index + 1), value); /// Create generic color sets from the parameters. private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num, @@ -258,28 +242,27 @@ internal class CustomizeSetFactory( } /// Set the specific option names for the given set of parameters. - private string[] GetOptionNames(CharaMakeParams row) + private string[] GetOptionNames(CharaMakeType row) { var nameArray = Enum.GetValues().Select(c => { // Find the first menu that corresponds to the Id. var byteId = c.ToByteAndMask().ByteIdx; - var menu = row.Menus - .Cast() - .FirstOrDefault(m => m!.Value.Customize == byteId); + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == byteId); if (menu == null) { // If none exists and the id corresponds to highlights, set the Highlights name. if (c == CustomizeIndex.Highlights) - return string.Intern(_lobbySheet.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights"); + return string.Intern(_lobbySheet.TryGetRow(237, out var text) ? text.Text.ExtractText() : "Highlights"); // Otherwise there is an error and we use the default name. return c.ToDefaultName(); } // Otherwise all is normal, get the menu name or if it does not work the default name. - var textRow = _lobbySheet.GetRow(menu.Value.Id); - return string.Intern(textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName()); + return string.Intern(_lobbySheet.TryGetRow(menu.Value.Menu.RowId, out var textRow) + ? textRow.Text.ExtractText() + : c.ToDefaultName()); }).ToArray(); // Add names for both eye colors. @@ -300,7 +283,7 @@ internal class CustomizeSetFactory( } /// Get the manu types for all available options. - private static CharaMakeParams.MenuType[] GetMenuTypes(CharaMakeParams row) + private static MenuType[] GetMenuTypes(CharaMakeType row) { // Set up the menu types for all customizations. return Enum.GetValues().Select(c => @@ -312,13 +295,13 @@ internal class CustomizeSetFactory( case CustomizeIndex.EyeColorLeft: case CustomizeIndex.EyeColorRight: case CustomizeIndex.FacePaintColor: - return CharaMakeParams.MenuType.ColorPicker; - case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing; + return MenuType.ColorPicker; + case CustomizeIndex.BodyType: return MenuType.Nothing; case CustomizeIndex.FacePaintReversed: case CustomizeIndex.Highlights: case CustomizeIndex.SmallIris: case CustomizeIndex.Lipstick: - return CharaMakeParams.MenuType.Checkmark; + return MenuType.Checkmark; case CustomizeIndex.FacialFeature1: case CustomizeIndex.FacialFeature2: case CustomizeIndex.FacialFeature3: @@ -327,29 +310,23 @@ internal class CustomizeSetFactory( case CustomizeIndex.FacialFeature6: case CustomizeIndex.FacialFeature7: case CustomizeIndex.LegacyTattoo: - return CharaMakeParams.MenuType.IconCheckmark; + return MenuType.IconCheckmark; } var gameId = c.ToByteAndMask().ByteIdx; // Otherwise find the first menu corresponding to the id. // If there is none, assume a list. - var menu = row.Menus - .Cast() - .FirstOrDefault(m => m!.Value.Customize == gameId); - var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector; - if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector) - ret = CharaMakeParams.MenuType.List1Selector; + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId); + var ret = (MenuType)(menu?.SubMenuType ?? (byte)MenuType.ListSelector); + if (c is CustomizeIndex.TailShape && ret is MenuType.ListSelector) + ret = MenuType.List1Selector; return ret; }).ToArray(); } /// Set the availability of options according to actual availability. - private static void SetAvailability(CustomizeSet set, CharaMakeParams row) + private static void SetAvailability(CustomizeSet set, CharaMakeType row) { - // TODO: Hrothgar female - if (set is { Race: Race.Hrothgar, Gender: Gender.Female }) - return; - Set(true, CustomizeIndex.Height); Set(set.Faces.Count > 0, CustomizeIndex.Face); Set(true, CustomizeIndex.Hairstyle); @@ -399,7 +376,7 @@ internal class CustomizeSetFactory( ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor; var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); - foreach (var type in Enum.GetValues()) + foreach (var type in Enum.GetValues()) dict.TryAdd(type, []); set.Order = dict; } @@ -423,7 +400,7 @@ internal class CustomizeSetFactory( bool Valid(CustomizeData c) { - var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; + var data = _customizeSheet.TryGetRow(c.CustomizeId, out var customize) ? customize.Unknown0 : 0; return data == 0 || data == i + set.Faces.Count; } } @@ -435,7 +412,7 @@ internal class CustomizeSetFactory( /// Create a list of lists of facial features and the legacy tattoo. /// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use. /// - private static void SetFacialFeatures(CustomizeSet set, CharaMakeParams row) + private static void SetFacialFeatures(CustomizeSet set, in CharaMakeType row) { var count = set.Faces.Count; set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count); @@ -444,14 +421,14 @@ internal class CustomizeSetFactory( var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray(); for (var i = 0; i < count; ++i) { - var data = row.FacialFeatureByFace[i].Icons; - tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]); - tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]); - tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]); - tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]); - tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]); - tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]); - tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]); + var data = row.FacialFeatureOption[i]; + tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, (uint)data.Option1); + tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, (uint)data.Option2); + tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, (uint)data.Option3); + tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, (uint)data.Option4); + tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, (uint)data.Option5); + tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, (uint)data.Option6); + tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, (uint)data.Option7); } set.FacialFeature1 = tmp[0]; diff --git a/Glamourer/GameData/MenuType.cs b/Glamourer/GameData/MenuType.cs new file mode 100644 index 0000000..a1d727b --- /dev/null +++ b/Glamourer/GameData/MenuType.cs @@ -0,0 +1,14 @@ +namespace Glamourer.GameData; + +public enum MenuType +{ + ListSelector = 0, + IconSelector = 1, + ColorPicker = 2, + DoubleColorPicker = 3, + IconCheckmark = 4, + Percentage = 5, + Checkmark = 6, // custom + Nothing = 7, // custom + List1Selector = 8, // custom, 1-indexed lists +} diff --git a/Glamourer/GameData/NpcCustomizeSet.cs b/Glamourer/GameData/NpcCustomizeSet.cs index d9d8f27..725f80f 100644 --- a/Glamourer/GameData/NpcCustomizeSet.cs +++ b/Glamourer/GameData/NpcCustomizeSet.cs @@ -1,9 +1,11 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; using OtterGui.Services; +using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.GameData; @@ -35,32 +37,51 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList /// The list of data. private readonly List _data = []; + private readonly BitArray _hairColors = new(256); + private readonly BitArray _eyeColors = new(256); + private readonly BitArray _facepaintColors = new(256); + private readonly BitArray _tattooColors = new(256); + private readonly BitArray _facepaints = new(128); + + public bool CheckValue(CustomizeIndex type, CustomizeValue value) + => type switch + { + CustomizeIndex.HairColor => _hairColors[value.Value], + CustomizeIndex.HighlightsColor => _hairColors[value.Value], + CustomizeIndex.EyeColorLeft => _eyeColors[value.Value], + CustomizeIndex.EyeColorRight => _eyeColors[value.Value], + CustomizeIndex.FacePaintColor => _facepaintColors[value.Value], + CustomizeIndex.TattooColor => _tattooColors[value.Value], + CustomizeIndex.FacePaint when value.Value < 128 => _facepaints[value.Value], + _ => false, + }; + /// Create the data when ready. public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames) { var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter); Awaiter = waitTask.ContinueWith(_ => - { - var watch = Stopwatch.StartNew(); - var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs)); - var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames)); - FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result); - Time = watch.ElapsedMilliseconds; - }); + { + var watch = Stopwatch.StartNew(); + var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs)); + var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames)); + FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result); + Time = watch.ElapsedMilliseconds; + }) + .ContinueWith(_ => CheckFacepaintFiles(data, _facepaints)); } /// Create data from event NPCs. private static List CreateEnpcData(IDataManager data, DictENpc eNpcs) { - var enpcSheet = data.GetExcelSheet()!; + var enpcSheet = data.GetExcelSheet(); var list = new List(eNpcs.Count); // Go through all event NPCs already collected into a dictionary. foreach (var (id, name) in eNpcs) { - var row = enpcSheet.GetRow(id.Id); // We only accept NPCs with valid names. - if (row == null || name.IsNullOrWhitespace()) + if (!enpcSheet.TryGetRow(id.Id, out var row) || name.IsNullOrWhitespace()) continue; // Check if the customization is a valid human. @@ -72,33 +93,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList { Name = name, Customize = customize, - ModelId = row.ModelChara.Row, + ModelId = row.ModelChara.RowId, Id = id, Kind = ObjectKind.EventNpc, }; // Event NPCs have a reference to NpcEquip but also contain the appearance in their own row. - // Prefer the NpcEquip reference if it is set, otherwise use the own. - if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip) - { + // Prefer the NpcEquip reference if it is set and the own does not appear to be set, otherwise use the own. + if (row.NpcEquip.RowId != 0 && row.NpcEquip.Value is { } equip && row is { ModelBody: 0, ModelLegs: 0 }) ApplyNpcEquip(ref ret, equip); - } else - { - ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); - ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); - ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); - ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); - ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); - ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); - ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); - ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); - ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); - ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); - ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); - ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); - ret.VisorToggled = row.Visor; - } + ApplyNpcEquip(ref ret, row); list.Add(ret); } @@ -109,14 +114,14 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList /// Create data from battle NPCs. private static List CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames) { - var bnpcSheet = data.GetExcelSheet()!; - var list = new List((int)bnpcSheet.RowCount); + var bnpcSheet = data.GetExcelSheet(); + var list = new List(bnpcSheet.Count); // We go through all battle NPCs in the sheet because the dictionary refers to names. foreach (var baseRow in bnpcSheet) { // Only accept humans. - if (baseRow.ModelChara.Value!.Type != 1) + if (baseRow.ModelChara.Value.Type != 1) continue; var bnpcNameIds = bNpcNames[baseRow.RowId]; @@ -125,15 +130,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList continue; // Check if the customization is a valid human. - var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!); + var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value); if (!valid) continue; - var equip = baseRow.NpcEquip.Value!; + var equip = baseRow.NpcEquip.Value; var ret = new NpcData { Customize = customize, - ModelId = baseRow.ModelChara.Row, + ModelId = baseRow.ModelChara.RowId, Id = baseRow.RowId, Kind = ObjectKind.BattleNpc, }; @@ -164,6 +169,12 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList for (var i = 0; i < duplicates.Count; ++i) { var current = duplicates[i]; + _hairColors[current.Customize[CustomizeIndex.HairColor].Value] = true; + _hairColors[current.Customize[CustomizeIndex.HighlightsColor].Value] = true; + _eyeColors[current.Customize[CustomizeIndex.EyeColorLeft].Value] = true; + _eyeColors[current.Customize[CustomizeIndex.EyeColorRight].Value] = true; + _facepaintColors[current.Customize[CustomizeIndex.FacePaintColor].Value] = true; + _tattooColors[current.Customize[CustomizeIndex.TattooColor].Value] = true; for (var j = 0; j < i; ++j) { if (current.DataEquals(duplicates[j])) @@ -202,18 +213,36 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList /// Apply equipment from a NpcEquip row. private static void ApplyNpcEquip(ref NpcData data, NpcEquip row) { - data.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); - data.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); - data.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); - data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); - data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); - data.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); - data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); - data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); - data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); - data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); - data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); - data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); + data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32)); + data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32)); + data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32)); + data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32)); + data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32)); + data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32)); + data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32)); + data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32)); + data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32)); + data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32)); + data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56)); + data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56)); + data.VisorToggled = row.Visor; + } + + /// Apply equipment from a ENpcBase Row row. + private static void ApplyNpcEquip(ref NpcData data, ENpcBase row) + { + data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32)); + data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32)); + data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32)); + data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32)); + data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32)); + data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32)); + data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32)); + data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32)); + data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32)); + data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32)); + data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56)); + data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56)); data.VisorToggled = row.Visor; } @@ -221,11 +250,11 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize) { var customize = new CustomizeArray(); - customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.Row); + customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.RowId); customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender); customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType); customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height); - customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.Row); + customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.RowId); customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face); customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle); customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight); @@ -259,15 +288,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList /// Obtain customizations from a ENpcBase row and check if the human is valid. private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase) { - if (enpcBase.ModelChara.Value?.Type != 1) + if (enpcBase.ModelChara.ValueNullable?.Type != 1) return (false, CustomizeArray.Default); var customize = new CustomizeArray(); - customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.Row); + customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.RowId); customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender); customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType); customize.SetByIndex(3, (CustomizeValue)enpcBase.Height); - customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.Row); + customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.RowId); customize.SetByIndex(5, (CustomizeValue)enpcBase.Face); customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle); customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight); @@ -298,6 +327,17 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList return (true, customize); } + /// Check decal files for existence. + private static void CheckFacepaintFiles(IDataManager data, BitArray facepaints) + { + for (byte i = 0; i < 128; ++i) + { + var path = GamePaths.Tex.FaceDecal(i); + if (data.FileExists(path)) + facepaints[i] = true; + } + } + /// public IEnumerator GetEnumerator() => _data.GetEnumerator(); diff --git a/Glamourer/GameData/NpcData.cs b/Glamourer/GameData/NpcData.cs index 6b5f2bd..0076bb6 100644 --- a/Glamourer/GameData/NpcData.cs +++ b/Glamourer/GameData/NpcData.cs @@ -13,7 +13,7 @@ public unsafe struct NpcData public CustomizeArray Customize; /// The equipment appearance of the NPC, 10 * CharacterArmor. - private fixed byte _equip[40]; + private fixed byte _equip[CharacterArmor.Size * 10]; /// The mainhand weapon appearance of the NPC. public CharacterWeapon Mainhand; @@ -54,36 +54,35 @@ public unsafe struct NpcData { sb.Append(span[i].Set.Id.ToString("D4")) .Append('-') - .Append(span[i].Variant.Id.ToString("D3")) - .Append('-') - .Append(span[i].Stain.Id.ToString("D3")) - .Append(", "); + .Append(span[i].Variant.Id.ToString("D3")); + foreach (var stain in span[i].Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); } sb.Append(Mainhand.Skeleton.Id.ToString("D4")) .Append('-') .Append(Mainhand.Weapon.Id.ToString("D4")) .Append('-') - .Append(Mainhand.Variant.Id.ToString("D3")) - .Append('-') - .Append(Mainhand.Stain.Id.ToString("D4")) - .Append(", ") + .Append(Mainhand.Variant.Id.ToString("D3")); + foreach (var stain in Mainhand.Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); + sb.Append(", ") .Append(Offhand.Skeleton.Id.ToString("D4")) .Append('-') .Append(Offhand.Weapon.Id.ToString("D4")) .Append('-') - .Append(Offhand.Variant.Id.ToString("D3")) - .Append('-') - .Append(Offhand.Stain.Id.ToString("D3")); + .Append(Offhand.Variant.Id.ToString("D3")); + foreach (var stain in Mainhand.Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); return sb.ToString(); } /// Set an equipment piece to a given value. - internal void Set(int idx, uint value) + internal void Set(int idx, ulong value) { fixed (byte* ptr = _equip) { - ((uint*)ptr)[idx] = value; + ((ulong*)ptr)[idx] = value; } } diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index b368195..33c67d5 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,5 +1,7 @@ using Dalamud.Plugin; using Glamourer.Api; +using Glamourer.Automation; +using Glamourer.Designs; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Services; @@ -7,6 +9,7 @@ using Glamourer.State; using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; +using Penumbra.GameData.Interop; namespace Glamourer; @@ -23,15 +26,17 @@ public class Glamourer : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; + public static DynamisIpc Dynamis { get; private set; } = null!; private readonly ServiceManager _services; - public Glamourer(DalamudPluginInterface pluginInterface) + public Glamourer(IDalamudPluginInterface pluginInterface) { try { - _services = StaticServiceManager.CreateProvider(pluginInterface, Log); + _services = StaticServiceManager.CreateProvider(pluginInterface, Log, this); Messager = _services.GetService(); + Dynamis = _services.GetService(); _services.EnsureRequiredServices(); _services.GetService(); @@ -50,6 +55,96 @@ public class Glamourer : IDalamudPlugin } } + public string GatherSupportInformation() + { + var sb = new StringBuilder(10240); + var config = _services.GetService(); + sb.AppendLine("**Settings**"); + sb.Append($"> **`Plugin Version: `** {Version}\n"); + sb.Append($"> **`Commit Hash: `** {CommitHash}\n"); + sb.Append($"> **`Enable Auto Designs: `** {config.EnableAutoDesigns}\n"); + sb.Append($"> **`Gear Protection: `** {config.UseRestrictedGearProtection}\n"); + sb.Append($"> **`Item Restriction: `** {config.UnlockedItemMode}\n"); + sb.Append($"> **`Keep Manual Changes: `** {config.RespectManualOnAutomationUpdate}\n"); + sb.Append($"> **`Auto-Reload Gear: `** {config.AutoRedrawEquipOnChanges}\n"); + sb.Append($"> **`Revert on Zone Change:`** {config.RevertManualChangesOnZoneChange}\n"); + sb.Append($"> **`Festival Easter-Eggs: `** {config.DisableFestivals}\n"); + sb.Append($"> **`Apply Entire Weapon: `** {config.ChangeEntireItem}\n"); + sb.Append($"> **`Apply Associated Mods:`** {config.AlwaysApplyAssociatedMods}\n"); + sb.Append($"> **`Attach to PCP: `** {config.AttachToPcp}\n"); + sb.Append($"> **`Hidden Panels: `** {config.HideDesignPanel}\n"); + sb.Append($"> **`Show QDB: `** {config.Ephemeral.ShowDesignQuickBar}\n"); + sb.Append($"> **`QDB Hotkey: `** {config.ToggleQuickDesignBar}\n"); + sb.Append($"> **`Smaller Equip Display:`** {config.SmallEquip}\n"); + sb.Append($"> **`Debug Mode: `** {config.DebugMode}\n"); + sb.Append($"> **`Cheat Codes: `** {(ulong)_services.GetService().AllEnabled:X8}\n"); + sb.AppendLine("**Plugins**"); + GatherRelevantPlugins(sb); + var designManager = _services.GetService(); + var autoManager = _services.GetService(); + var stateManager = _services.GetService(); + var objectManager = _services.GetService(); + var currentPlayer = objectManager.PlayerData.Identifier; + var states = stateManager.Where(kvp => objectManager.ContainsKey(kvp.Key)).ToList(); + + sb.AppendLine("**Statistics**"); + sb.Append($"> **`Current Player: `** {(currentPlayer.IsValid ? currentPlayer.Incognito(null) : "None")}\n"); + sb.Append($"> **`Saved Designs: `** {designManager.Designs.Count}\n"); + sb.Append($"> **`Automation Sets: `** {autoManager.Count} ({autoManager.Count(set => set.Enabled)} Enabled)\n"); + sb.Append( + $"> **`Actor States: `** {stateManager.Count} ({states.Count} Visible, {stateManager.Values.Count(s => s.IsLocked)} Locked)\n"); + + var enabledAutomation = autoManager.Where(s => s.Enabled).ToList(); + if (enabledAutomation.Count > 0) + { + sb.AppendLine("**Enabled Automation**"); + foreach (var set in enabledAutomation) + { + sb.Append( + $"> **`{set.Identifiers.First().Incognito(null) + ':',-24}`** {(set.Name.Length >= 2 ? $"{set.Name.AsSpan(0, 2)}..." : set.Name)} ({set.Designs.Count} {(set.Designs.Count == 1 ? "Design" : "Designs")})\n"); + } + } + + if (states.Count > 0) + { + sb.AppendLine("**State**"); + foreach (var (ident, state) in states) + { + var sources = Enum.GetValues().Select(s => (0, s)).ToArray(); + foreach (var source in StateIndex.All.Select(s => state.Sources[s])) + ++sources[(int)source].Item1; + foreach (var material in state.Materials.Values) + ++sources[(int)material.Value.Source].Item1; + var sourcesString = string.Join(", ", sources.Where(s => s.Item1 > 0).Select(s => $"{s.s} {s.Item1}")); + sb.Append( + $"> **`{ident.Incognito(null) + ':',-24}`** {(state.IsLocked ? "Locked, " : string.Empty)}Job {state.LastJob.Id}, Zone {state.LastTerritory}, Materials {state.Materials.Values.Count}, {sourcesString}\n"); + } + } + + return sb.ToString(); + } + + + private void GatherRelevantPlugins(StringBuilder sb) + { + ReadOnlySpan relevantPlugins = + [ + "Penumbra", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", + ]; + var plugins = _services.GetService().InstalledPlugins + .GroupBy(p => p.InternalName) + .ToDictionary(g => g.Key, g => + { + var item = g.OrderByDescending(p => p.IsLoaded).ThenByDescending(p => p.Version).First(); + return (item.IsLoaded, item.Version, item.Name); + }); + foreach (var plugin in relevantPlugins) + { + if (plugins.TryGetValue(plugin, out var data)) + sb.Append($"> **`{data.Name + ':',-22}`** {data.Version}{(data.IsLoaded ? string.Empty : " (Disabled)")}\n"); + } + } public void Dispose() => _services?.Dispose(); diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 9a1b95b..560621d 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -1,92 +1,32 @@ - + - net8.0-windows - preview - x64 Glamourer Glamourer 9.0.0.1 9.0.0.1 - SoftOtter Glamourer - Copyright © 2023 - true - Library + Copyright © 2025 4 - true - enable bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 - true - false - false - - - - true - full - false - DEBUG;TRACE - - - - pdbonly - true - TRACE - - - - OnOutputUpdated + + + PreserveNewest + - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)ImGui.NET.dll - False - - - $(DalamudLibPath)ImGuiScene.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - - + @@ -116,14 +56,4 @@ $(GitCommitHash) - - - - PreserveNewest - - - - - - \ No newline at end of file diff --git a/Glamourer/Glamourer.json b/Glamourer/Glamourer.json index cdf2cba..e2dbf8b 100644 --- a/Glamourer/Glamourer.json +++ b/Glamourer/Glamourer.json @@ -8,7 +8,7 @@ "AssemblyVersion": "9.0.0.1", "RepoUrl": "https://github.com/Ottermandias/Glamourer", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 14, "ImageUrls": null, "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png" } \ No newline at end of file diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index b7f9737..b2713eb 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; namespace Glamourer.Gui; @@ -29,6 +29,10 @@ public enum ColorId TriStateNeutral, BattleNpc, EventNpc, + ModdedItemMarker, + ContainsItemsEnabled, + ContainsItemsDisabled, + AdvancedDyeActive, } public static class Colors @@ -39,32 +43,36 @@ public static class Colors => color switch { // @formatter:off - ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ), - ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), - ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ), - ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), - ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), - ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), - ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), - ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), - ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), - ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ), - ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), - ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), - ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), - ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), - ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ), - ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ), - ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ), - ColorId.QuickDesignButton => (0x900A0A0A, "Quick Design Bar Button Background", "The color of button frames in the quick design bar." ), - ColorId.QuickDesignFrame => (0x90383838, "Quick Design Bar Combo Background", "The color of the combo background in the quick design bar." ), - ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ), - ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ), - ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ), - ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ), - ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ), - ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ), - _ => (0x00000000, string.Empty, string.Empty ), + ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ), + ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), + ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ), + ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), + ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), + ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), + ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), + ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), + ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ), + ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), + ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), + ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), + ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), + ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ), + ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ), + ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ), + ColorId.QuickDesignButton => (0x900A0A0A, "Quick Design Bar Button Background", "The color of button frames in the quick design bar." ), + ColorId.QuickDesignFrame => (0x90383838, "Quick Design Bar Combo Background", "The color of the combo background in the quick design bar." ), + ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ), + ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ), + ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ), + ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ), + ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ), + ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ), + ColorId.ModdedItemMarker => (0xFFFF20FF, "Modded Item Marker", "The color of dot in the unlocks overview tab signaling that the item is modded in the currently selected Penumbra collection." ), + ColorId.ContainsItemsEnabled => (0xFFA0F0A0, "Enabled Mod Contains Design Items", "The color of enabled mods in the associated mod dropdown menu when they contain items used in this design." ), + ColorId.ContainsItemsDisabled => (0x80A0F0A0, "Disabled Mod Contains Design Items", "The color of disabled mods in the associated mod dropdown menu when they contain items used in this design." ), + ColorId.AdvancedDyeActive => (0xFF58DDFF, "Advanced Dyes Active", "The highlight color for the advanced dye button and marker if any advanced dyes are active for this slot." ), + _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs index fb50f55..4f463d6 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -1,16 +1,83 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.GameData; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using System; namespace Glamourer.Gui.Customization; public partial class CustomizationDrawer { - private const string ColorPickerPopupName = "ColorPicker"; + private const string ColorPickerPopupName = "ColorPicker"; + private CustomizeValue _draggedColorValue; + private CustomizeIndex _draggedColorType; + + + private void DrawDragDropSource(CustomizeIndex index, CustomizeData custom) + { + using var dragDropSource = ImUtf8.DragDropSource(); + if (!dragDropSource) + return; + + if (!DragDropSource.SetPayload("##colorDragDrop"u8)) + _draggedColorValue = _customize[index]; + ImUtf8.Text( + $"Dragging {(custom.Color == 0 ? $"{_currentOption} (NPC)" : _currentOption)} #{_draggedColorValue.Value}..."); + _draggedColorType = index; + } + + private void DrawDragDropTarget(CustomizeIndex index) + { + using var dragDropTarget = ImUtf8.DragDropTarget(); + if (!dragDropTarget.Success || !dragDropTarget.IsDropping("##colorDragDrop"u8)) + return; + + var idx = _set.DataByValue(_draggedColorType, _draggedColorValue, out var draggedData, _customize.Face); + var bestMatch = _draggedColorValue; + if (draggedData.HasValue) + { + var draggedColor = draggedData.Value.Color; + var targetData = _set.Data(index, idx); + if (targetData.Color != draggedColor) + { + var bestDiff = Diff(targetData.Color, draggedColor); + var count = _set.Count(index); + for (var i = 0; i < count; ++i) + { + targetData = _set.Data(index, i); + if (targetData.Color == draggedColor) + { + UpdateValue(_draggedColorValue); + return; + } + + var diff = Diff(targetData.Color, draggedColor); + if (diff >= bestDiff) + continue; + + bestDiff = diff; + bestMatch = (CustomizeValue)i; + } + } + } + + UpdateValue(bestMatch); + return; + + static uint Diff(uint color1, uint color2) + { + var r = (color1 & 0xFF) - (color2 & 0xFF); + var g = ((color1 >> 8) & 0xFF) - ((color2 >> 8) & 0xFF); + var b = ((color1 >> 16) & 0xFF) - ((color2 >> 16) & 0xFF); + return 30 * r * r + 59 * g * g + 11 * b * b; + } + } private void DrawColorPicker(CustomizeIndex index) { @@ -21,13 +88,18 @@ public partial class CustomizationDrawer using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0)) { - if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) + if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.NoDragDrop, _framedIconSize)) + { ImGui.OpenPopup(ColorPickerPopupName); - else if (current >= 0 && CaptureMouseWheel(ref current, 0, _currentCount)) + } + else if (current >= 0 && !_locked && CaptureMouseWheel(ref current, 0, _currentCount)) { var data = _set.Data(_currentIndex, current, _customize.Face); UpdateValue(data.Value); } + + DrawDragDropSource(index, custom); + DrawDragDropTarget(index); } var npc = false; @@ -70,7 +142,7 @@ public partial class CustomizationDrawer for (var i = 0; i < _currentCount; ++i) { var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]); - if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)) && !_locked) { UpdateValue(custom.Value); ImGui.CloseCurrentPopup(); diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs index ae64075..26e9002 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; @@ -34,14 +34,13 @@ public partial class CustomizationDrawer private void DrawGenderSelector() { - using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) + using (ImRaii.Disabled(_locked || _lockedRedraw)) { var icon = _customize.Gender switch { - Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble, - Gender.Male => FontAwesomeIcon.Mars, - Gender.Female => FontAwesomeIcon.Venus, - _ => FontAwesomeIcon.Question, + Gender.Male => FontAwesomeIcon.Mars, + Gender.Female => FontAwesomeIcon.Venus, + _ => FontAwesomeIcon.Question, }; if (ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, @@ -56,7 +55,7 @@ public partial class CustomizationDrawer private void DrawRaceCombo() { - using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) + using (ImRaii.Disabled(_locked || _lockedRedraw)) { ImGui.SetNextItemWidth(_raceSelectorWidth); using (var combo = ImRaii.Combo("##subRaceCombo", _service.ClanName(_customize.Clan, _customize.Gender))) diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs index c0c45d2..8599f8c 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -1,7 +1,9 @@ -using Glamourer.GameData; +using Dalamud.Interface.Textures.TextureWraps; +using Glamourer.GameData; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -29,10 +31,11 @@ public partial class CustomizationDrawer npc = true; } - var icon = _service.Manager.GetIcon(custom!.Value.IconId); + var icon = _service.Manager.GetIcon(custom!.Value.IconId); + var hasIcon = icon.TryGetWrap(out var wrap, out _); using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) { - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize)) { ImGui.OpenPopup(IconSelectorPopup); } @@ -43,7 +46,8 @@ public partial class CustomizationDrawer } } - ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize); ImGui.SameLine(); using (_ = ImRaii.Group()) @@ -83,8 +87,9 @@ public partial class CustomizationDrawer using var frameColor = current == i ? ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed) : ImRaii.PushColor(ImGuiCol.Button, ColorId.FavoriteStarOn.Value(), isFavorite); + var hasIcon = icon.TryGetWrap(out var wrap, out var _); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize)) { UpdateValue(custom.Value); ImGui.CloseCurrentPopup(); @@ -96,8 +101,9 @@ public partial class CustomizationDrawer else _favorites.TryAdd(_set.Gender, _set.Clan, _currentIndex, custom.Value); - ImGuiUtil.HoverIconTooltip(icon, _iconSize, - FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty); + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize, + FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty); var text = custom.Value.ToString(); var textWidth = ImGui.CalcTextSize(text).X; @@ -188,25 +194,36 @@ public partial class CustomizationDrawer private void DrawMultiIcons() { - var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; + var options = _set.Order[MenuType.IconCheckmark]; using var group = ImRaii.Group(); var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face; foreach (var (featureIdx, idx) in options.WithIndex()) { - using var id = SetId(featureIdx); - var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; - var feature = _set.Data(featureIdx, 0, face); - var icon = featureIdx == CustomizeIndex.LegacyTattoo - ? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId) - : _service.Manager.GetIcon(feature.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, - Vector4.Zero, enabled ? Vector4.One : _redTint)) + using var id = SetId(featureIdx); + var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; + var feature = _set.Data(featureIdx, 0, face); + bool hasIcon; + IDalamudTextureWrap? wrap; + var icon = _service.Manager.GetIcon(feature.IconId); + if (featureIdx is CustomizeIndex.LegacyTattoo) + { + wrap = _legacyTattoo; + hasIcon = wrap != null; + } + else + { + hasIcon = icon.TryGetWrap(out wrap, out _); + } + + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize, Vector2.Zero, Vector2.One, + (int)ImGui.GetStyle().FramePadding.X, Vector4.Zero, enabled ? Vector4.One : _redTint)) { _customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max); Changed |= _currentFlag; } - ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize); if (idx % 4 != 3) ImGui.SameLine(); } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 5bb98c9..ec5523f 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGuiInternal; @@ -43,11 +43,13 @@ public partial class CustomizationDrawer var heightString = _config.HeightDisplayType switch { - HeightDisplayType.Centimetre => FormattableString.Invariant($"({height * 100:F1} cm)"), - HeightDisplayType.Metre => FormattableString.Invariant($"({height:F2} m)"), - HeightDisplayType.Wrong => FormattableString.Invariant($"({height * 100 / 2.539:F1} in)"), - HeightDisplayType.WrongFoot => $"({(int)(height * 100 / 2.539 / 12)}'{(int)(height * 100 / 2.539) % 12}'')", - _ => FormattableString.Invariant($"({height})"), + HeightDisplayType.Centimetre => FormattableString.Invariant($"({height * 100:F1} cm)"), + HeightDisplayType.Metre => FormattableString.Invariant($"({height:F2} m)"), + HeightDisplayType.Wrong => FormattableString.Invariant($"({height * 100 / 2.539:F1} in)"), + HeightDisplayType.WrongFoot => $"({(int)(height * 100 / 2.539 / 12)}'{(int)(height * 100 / 2.539) % 12}'')", + HeightDisplayType.Corgi => FormattableString.Invariant($"({height * 100 / 40.0:F1} Corgis)"), + HeightDisplayType.OlympicPool => FormattableString.Invariant($"({height / 3.0:F3} Pools)"), + _ => FormattableString.Invariant($"({height})"), }; ImGui.TextUnformatted(heightString); } @@ -293,7 +295,7 @@ public partial class CustomizationDrawer private void ApplyCheckbox(CustomizeIndex index) { - SetId(index); + using var id = SetId(index); if (UiHelpers.DrawCheckbox("##apply", $"Apply the {_currentOption} customization in this design.", _currentApply, out _, _locked)) ToggleApply(); } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index 60251df..349891c 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -1,10 +1,10 @@ -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; using Glamourer.GameData; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; @@ -13,16 +13,15 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; public partial class CustomizationDrawer( - DalamudPluginInterface pi, + ITextureProvider textures, CustomizeService _service, - CodeService _codes, Configuration _config, FavoriteManager _favorites, HeightService _heightService) : IDisposable { private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); - private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(pi); + private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(textures); private Exception? _terminate; @@ -117,30 +116,27 @@ public partial class CustomizationDrawer( try { - if (_codes.Enabled(CodeService.CodeFlag.Artisan)) - return DrawArtisan(); - DrawRaceGenderSelector(); DrawBodyType(); _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender); - foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) + foreach (var id in _set.Order[MenuType.Percentage]) PercentageSelector(id); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); + Functions.IteratePairwise(_set.Order[MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); DrawMultiIconSelector(); - foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector]) + foreach (var id in _set.Order[MenuType.ListSelector]) DrawListSelector(id, false); - foreach (var id in _set.Order[CharaMakeParams.MenuType.List1Selector]) + foreach (var id in _set.Order[MenuType.List1Selector]) DrawListSelector(id, true); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); + Functions.IteratePairwise(_set.Order[MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.Checkmark], DrawCheckbox, + Functions.IteratePairwise(_set.Order[MenuType.Checkmark], DrawCheckbox, () => ImGui.SameLine(_comboSelectorSize - _framedIconSize.X + ImGui.GetStyle().WindowPadding.X)); return Changed != 0 || ChangeApply != _initialApply; } @@ -154,31 +150,6 @@ public partial class CustomizationDrawer( } } - private unsafe bool DrawArtisan() - { - for (var i = 0; i < CustomizeArray.Size; ++i) - { - using var id = ImRaii.PushId(i); - int value = _customize.Data[i]; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt(string.Empty, ref value, 0, 0)) - { - var newValue = (byte)Math.Clamp(value, 0, byte.MaxValue); - if (newValue != _customize.Data[i]) - foreach (var flag in Enum.GetValues()) - { - var (j, _) = flag.ToByteAndMask(); - if (j == i) - Changed |= flag.ToFlag(); - } - - _customize.Data[i] = newValue; - } - } - - return Changed != 0; - } - private void UpdateSizes() { _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; @@ -190,7 +161,7 @@ public partial class CustomizationDrawer( _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; } - private static IDalamudTextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi) + private static IDalamudTextureWrap? GetLegacyTattooIcon(ITextureProvider textures) { using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); if (resource == null) @@ -199,7 +170,7 @@ public partial class CustomizationDrawer( var rawImage = new byte[resource.Length]; var length = resource.Read(rawImage, 0, (int)resource.Length); return length == resource.Length - ? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) + ? textures.CreateFromRaw(RawImageSpecification.Rgba32(192, 192), rawImage, "Glamourer.LegacyTattoo") : null; } } diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs index aa43b79..421caed 100644 --- a/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs @@ -6,8 +6,8 @@ namespace Glamourer.Gui.Customization; public struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data) { - private IDesignEditor _editor; - private object _object; + private IDesignEditor _editor = null!; + private object _object = null!; public readonly CustomizeParameterFlag Flag = flag; public bool Locked; public bool DisplayApplication; diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs index 9bfb2f8..4db6b14 100644 --- a/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs @@ -3,7 +3,7 @@ using Glamourer.Designs; using Glamourer.GameData; using Glamourer.Interop.PalettePlus; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; @@ -287,13 +287,13 @@ public class CustomizeParameterDrawer(Configuration config, PaletteImport import } private ImGuiColorEditFlags GetFlags() - => Format | Display | ImGuiColorEditFlags.HDR | ImGuiColorEditFlags.NoOptions; + => Format | Display | ImGuiColorEditFlags.Hdr | ImGuiColorEditFlags.NoOptions; private ImGuiColorEditFlags Format => config.UseFloatForColors ? ImGuiColorEditFlags.Float : ImGuiColorEditFlags.Uint8; private ImGuiColorEditFlags Display - => config.UseRgbForColors ? ImGuiColorEditFlags.DisplayRGB : ImGuiColorEditFlags.DisplayHSV; + => config.UseRgbForColors ? ImGuiColorEditFlags.DisplayRgb : ImGuiColorEditFlags.DisplayHsv; private ImRaii.IEndObject EnsureSize() { diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index 4bd18ab..2d8880e 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -2,11 +2,13 @@ using Dalamud.Interface.Utility.Raii; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Special; using Glamourer.Events; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Widgets; @@ -20,6 +22,7 @@ public abstract class DesignComboBase : FilterComboCache>> generator, Logger log, DesignChanged designChanged, TabSelected tabSelected, EphemeralConfig config, DesignColors designColors) @@ -29,7 +32,7 @@ public abstract class DesignComboBase : FilterComboCache _currentDesign == p.Item1); - UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null); - return CurrentSelectionIdx; - } - protected bool Draw(IDesignStandIn? currentDesign, string? label, float width) { _currentDesign = currentDesign; - InnerWidth = 400 * ImGuiHelpers.GlobalScale; + UpdateCurrentSelection(); + InnerWidth = 400 * ImGuiHelpers.GlobalScale; var name = label ?? "Select Design Here..."; bool ret; using (_ = currentDesign != null ? ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(currentDesign as Design)) : null) @@ -126,37 +123,60 @@ public abstract class DesignComboBase : FilterComboCache ReferenceEquals(s.Item1, CurrentSelection?.Item1)); - if (CurrentSelectionIdx >= 0) - { - UpdateSelection(Items[CurrentSelectionIdx]); - } - else if (Items.Count > 0) - { - CurrentSelectionIdx = 0; - UpdateSelection(Items[0]); - } - else - { - UpdateSelection(null); - } + if (!ReferenceEquals(_currentDesign, CurrentSelection?.Item1)) + CurrentSelectionIdx = -1; - if (!priorState) - Cleanup(); - break; + base.OnMouseWheel(preview, ref _2, steps); + } + + private void UpdateCurrentSelection() + { + if (!_isCurrentSelectionDirty) + return; + + var priorState = IsInitialized; + if (priorState) + Cleanup(); + CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1)); + if (CurrentSelectionIdx >= 0) + { + UpdateSelection(Items[CurrentSelectionIdx]); } + else if (Items.Count > 0) + { + CurrentSelectionIdx = 0; + UpdateSelection(Items[0]); + } + else + { + UpdateSelection(null); + } + + if (!priorState) + Cleanup(); + _isCurrentSelectionDirty = false; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1); + UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null); + return CurrentSelectionIdx; + } + + private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null) + { + _isCurrentSelectionDirty = type switch + { + DesignChanged.Type.Created => true, + DesignChanged.Type.Renamed => true, + DesignChanged.Type.ChangedColor => true, + DesignChanged.Type.Deleted => true, + DesignChanged.Type.QuickDesignBar => true, + _ => _isCurrentSelectionDirty, + }; } private void QuickSelectedDesignTooltip(IDesignStandIn? design) @@ -174,7 +194,7 @@ public abstract class DesignComboBase : FilterComboCache [ - .. designs.Designs - .Where(d => d.QuickDesign) - .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .. fileSystem + .Where(kvp => kvp.Key.QuickDesign) + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) .OrderBy(d => d.Item2), ]) { @@ -276,7 +295,6 @@ public sealed class QuickDesignCombo : DesignCombo } public sealed class LinkDesignCombo( - DesignManager designs, DesignFileSystem fileSystem, Logger log, DesignChanged designChanged, @@ -285,8 +303,8 @@ public sealed class LinkDesignCombo( DesignColors designColors) : DesignCombo(log, designChanged, tabSelected, config, designColors, () => [ - .. designs.Designs - .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .. fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) .OrderBy(d => d.Item2), ]); @@ -300,8 +318,8 @@ public sealed class RandomDesignCombo( DesignColors designColors) : DesignCombo(log, designChanged, tabSelected, config, designColors, () => [ - .. designs.Designs - .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .. fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) .OrderBy(d => d.Item2), ]) { @@ -327,7 +345,6 @@ public sealed class RandomDesignCombo( } public sealed class SpecialDesignCombo( - DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, DesignColors designColors, @@ -337,8 +354,8 @@ public sealed class SpecialDesignCombo( EphemeralConfig config, RandomDesignGenerator rng, QuickSelectedDesign quickSelectedDesign) - : DesignComboBase(() => designs.Designs - .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + : DesignComboBase(() => fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) .OrderBy(d => d.Item2) .Prepend(new Tuple(new RandomDesign(rng), string.Empty)) .Prepend(new Tuple(quickSelectedDesign, string.Empty)) diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index a0a341a..e8c0ce3 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -6,26 +6,28 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Glamourer.Automation; using Glamourer.Designs; -using Glamourer.Interop; -using Glamourer.Interop.Structs; +using Glamourer.Interop.Penumbra; using Glamourer.State; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; +using OtterGui.Text; using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; namespace Glamourer.Gui; [Flags] public enum QdbButtons { - ApplyDesign = 0x01, - RevertAll = 0x02, - RevertAutomation = 0x04, - RevertAdvanced = 0x08, - RevertEquip = 0x10, - RevertCustomize = 0x20, - ReapplyAutomation = 0x40, + ApplyDesign = 0x01, + RevertAll = 0x02, + RevertAutomation = 0x04, + RevertAdvancedDyes = 0x08, + RevertEquip = 0x10, + RevertCustomize = 0x20, + ReapplyAutomation = 0x40, + ResetSettings = 0x80, + RevertAdvancedCustomization = 0x100, } public sealed class DesignQuickBar : Window, IDisposable @@ -35,19 +37,21 @@ public sealed class DesignQuickBar : Window, IDisposable ? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing; - private readonly Configuration _config; - private readonly QuickDesignCombo _designCombo; - private readonly StateManager _stateManager; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly ObjectManager _objects; - private readonly IKeyState _keyState; - private readonly ImRaii.Style _windowPadding = new(); - private readonly ImRaii.Color _windowColor = new(); - private DateTime _keyboardToggle = DateTime.UnixEpoch; - private int _numButtons; + private readonly Configuration _config; + private readonly QuickDesignCombo _designCombo; + private readonly StateManager _stateManager; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly ActorObjectManager _objects; + private readonly PenumbraService _penumbra; + private readonly IKeyState _keyState; + private readonly ImRaii.Style _windowPadding = new(); + private readonly ImRaii.Color _windowColor = new(); + private DateTime _keyboardToggle = DateTime.UnixEpoch; + private int _numButtons; + private readonly StringBuilder _tooltipBuilder = new(512); public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState, - ObjectManager objects, AutoDesignApplier autoDesignApplier) + ActorObjectManager objects, AutoDesignApplier autoDesignApplier, PenumbraService penumbra) : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking) { _config = config; @@ -56,9 +60,11 @@ public sealed class DesignQuickBar : Window, IDisposable _keyState = keyState; _objects = objects; _autoDesignApplier = autoDesignApplier; + _penumbra = penumbra; IsOpen = _config.Ephemeral.ShowDesignQuickBar; DisableWindowSounds = true; Size = Vector2.Zero; + RespectCloseHotkey = false; } public void Dispose() @@ -70,6 +76,9 @@ public sealed class DesignQuickBar : Window, IDisposable IsOpen = _config.Ephemeral.ShowDesignQuickBar && _config.QdbButtons != 0; } + public override bool DrawConditions() + => _objects.Player.Valid; + public override void PreDraw() { Flags = GetFlags; @@ -100,7 +109,7 @@ public sealed class DesignQuickBar : Window, IDisposable private void Draw(float width) { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); var spacing = ImGui.GetStyle().ItemInnerSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); var buttonSize = new Vector2(ImGui.GetFrameHeight()); @@ -112,13 +121,15 @@ public sealed class DesignQuickBar : Window, IDisposable ImGui.SameLine(); DrawApplyButton(buttonSize); } - + DrawRevertButton(buttonSize); DrawRevertEquipButton(buttonSize); DrawRevertCustomizeButton(buttonSize); DrawRevertAdvancedCustomization(buttonSize); + DrawRevertAdvancedDyes(buttonSize); DrawRevertAutomationButton(buttonSize); DrawReapplyAutomationButton(buttonSize); + DrawResetSettingsButton(buttonSize); } private ActorIdentifier _playerIdentifier; @@ -141,33 +152,38 @@ public sealed class DesignQuickBar : Window, IDisposable { var design = _designCombo.Design as Design; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); + if (design == null) { - tooltip = "No design selected."; + _tooltipBuilder.Append("No design selected."); } else { if (_playerIdentifier.IsValid && _playerData.Valid) { available |= 1; - tooltip = $"Left-Click: Apply {design.ResolveName(_config.Ephemeral.IncognitoMode)} to yourself."; + _tooltipBuilder.Append("Left-Click: Apply ") + .Append(design.ResolveName(_config.Ephemeral.IncognitoMode)) + .Append(" to yourself."); } if (_targetIdentifier.IsValid && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Apply {design.ResolveName(_config.Ephemeral.IncognitoMode)} to {_targetIdentifier}."; + _tooltipBuilder.Append("Right-Click: Apply ") + .Append(design.ResolveName(_config.Ephemeral.IncognitoMode)) + .Append(" to ").Append(_config.Ephemeral.IncognitoMode ? _targetIdentifier.Incognito(null) : _targetIdentifier.ToName()); } if (available == 0) - tooltip = "Neither player character nor target available."; + _tooltipBuilder.Append("Neither player character nor target available."); } - var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.PlayCircle, size, tooltip, available); + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.PlayCircle, size, available); ImGui.SameLine(); if (!clicked) return; @@ -179,9 +195,8 @@ public sealed class DesignQuickBar : Window, IDisposable return; } - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } private void DrawRevertButton(Vector2 buttonSize) @@ -190,28 +205,32 @@ public sealed class DesignQuickBar : Window, IDisposable return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false }) { available |= 1; - tooltip = "Left-Click: Revert the player character to their game state."; + _tooltipBuilder.Append("Left-Click: Revert the player character to their game state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false }) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Revert {_targetIdentifier} to their game state."; + _tooltipBuilder.Append("Right-Click: Revert ") + .Append(_targetIdentifier) + .Append(" to their game state."); } if (available == 0) - tooltip = "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."; + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); - var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, available); ImGui.SameLine(); if (clicked) - _stateManager.ResetState(state!, StateSource.Manual); + _stateManager.ResetState(state!, StateSource.Manual, isFinal: true); } private void DrawRevertAutomationButton(Vector2 buttonSize) @@ -223,34 +242,37 @@ public sealed class DesignQuickBar : Window, IDisposable return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) { available |= 1; - tooltip = "Left-Click: Revert the player character to their automation state."; + _tooltipBuilder.Append("Left-Click: Revert the player character to their automation state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Revert {_targetIdentifier} to their automation state."; + _tooltipBuilder.Append("Right-Click: Revert ") + .Append(_targetIdentifier) + .Append(" to their automation state."); } if (available == 0) - tooltip = "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."; + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); - var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.SyncAlt, buttonSize, tooltip, available); + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.SyncAlt, buttonSize, available); ImGui.SameLine(); if (!clicked) return; foreach (var actor in data.Objects) { - _autoDesignApplier.ReapplyAutomation(actor, id, state!, true, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _autoDesignApplier.ReapplyAutomation(actor, id, state!, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, true, StateSource.Manual); } } @@ -263,69 +285,104 @@ public sealed class DesignQuickBar : Window, IDisposable return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) { available |= 1; - tooltip = "Left-Click: Reapply the player character's current automation on top of their current state."; + _tooltipBuilder.Append("Left-Click: Reapply the player character's current automation on top of their current state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Reapply {_targetIdentifier}'s current automation on top of their current state."; + _tooltipBuilder.Append("Right-Click: Reapply ") + .Append(_targetIdentifier) + .Append("'s current automation on top of their current state."); } if (available == 0) - tooltip = "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."; + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); - var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.Repeat, buttonSize, tooltip, available); + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.Repeat, buttonSize, available); ImGui.SameLine(); if (!clicked) return; foreach (var actor in data.Objects) { - _autoDesignApplier.ReapplyAutomation(actor, id, state!, false, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _autoDesignApplier.ReapplyAutomation(actor, id, state!, false, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual); } } private void DrawRevertAdvancedCustomization(Vector2 buttonSize) { - if (!_config.UseAdvancedParameters) - return; - - if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced)) + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization)) return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) { available |= 1; - tooltip = "Left-Click: Revert the advanced customizations and dyes of the player character to their game state."; + _tooltipBuilder.Append("Left-Click: Revert the advanced customizations of the player character to their game state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Revert the advanced customizations and dyes of {_targetIdentifier} to their game state."; + _tooltipBuilder.Append("Right-Click: Revert the advanced customizations of ") + .Append(_targetIdentifier) + .Append(" to their game state."); } if (available == 0) - tooltip = "Neither player character nor target are available or their state is locked."; + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); - var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, tooltip, available); + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.PaintBrush, buttonSize, available); ImGui.SameLine(); if (clicked) - _stateManager.ResetAdvancedState(state!, StateSource.Manual); + _stateManager.ResetAdvancedCustomizations(state!, StateSource.Manual); + } + + private void DrawRevertAdvancedDyes(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the advanced dyes of the player character to their game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert the advanced dyes of ") + .Append(_targetIdentifier) + .Append(" to their game state."); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetAdvancedDyes(state!, StateSource.Manual); } private void DrawRevertCustomizeButton(Vector2 buttonSize) @@ -334,26 +391,28 @@ public sealed class DesignQuickBar : Window, IDisposable return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) { available |= 1; - tooltip = "Left-Click: Revert the customizations of the player character to their game state."; + _tooltipBuilder.Append("Left-Click: Revert the customizations of the player character to their game state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Revert the customizations of {_targetIdentifier} to their game state."; + _tooltipBuilder.Append("Right-Click: Revert the customizations of ") + .Append(_targetIdentifier) + .Append(" to their game state."); } if (available == 0) - tooltip = "Neither player character nor target are available or their state is locked."; + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); - var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.User, buttonSize, tooltip, available); + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.User, buttonSize, available); ImGui.SameLine(); if (clicked) _stateManager.ResetCustomize(state!, StateSource.Manual); @@ -365,35 +424,80 @@ public sealed class DesignQuickBar : Window, IDisposable return; var available = 0; - var tooltip = string.Empty; + _tooltipBuilder.Clear(); if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) { available |= 1; - tooltip = "Left-Click: Revert the equipment of the player character to its game state."; + _tooltipBuilder.Append("Left-Click: Revert the equipment of the player character to its game state."); } if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) { if (available != 0) - tooltip += '\n'; + _tooltipBuilder.Append('\n'); available |= 2; - tooltip += $"Right-Click: Revert the equipment of {_targetIdentifier} to its game state."; + _tooltipBuilder.Append("Right-Click: Revert the equipment of ") + .Append(_targetIdentifier) + .Append(" to its game state."); } if (available == 0) - tooltip = "Neither player character nor target are available or their state is locked."; + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); - var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Vest, buttonSize, tooltip, available); + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Vest, buttonSize, available); ImGui.SameLine(); if (clicked) _stateManager.ResetEquip(state!, StateSource.Manual); } - private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, string tooltip, - int available) + private void DrawResetSettingsButton(Vector2 buttonSize) { - ImGuiUtil.DrawDisabledButton(icon.ToIconString(), buttonSize, tooltip, available == 0, true); + if (!_config.QdbButtons.HasFlag(QdbButtons.ResetSettings)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerData.Valid) + { + available |= 1; + _tooltipBuilder + .Append( + "Left-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ") + .Append(_playerIdentifier) + .Append('.'); + } + + if (_targetIdentifier.IsValid && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder + .Append( + "Right-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ") + .Append(_targetIdentifier) + .Append('.'); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available to identify their collections."); + + var (clicked, _, data, _) = ResolveTarget(FontAwesomeIcon.Cog, buttonSize, available); + ImGui.SameLine(); + if (clicked) + { + _penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Manual); + _penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Fixed); + } + } + + private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, int available) + { + var enumerator = _tooltipBuilder.GetChunks(); + var span = enumerator.MoveNext() ? enumerator.Current.Span : []; + ImUtf8.IconButton(icon, span, buttonSize, available == 0); if ((available & 1) == 1 && ImGui.IsItemClicked(ImGuiMouseButton.Left)) return (true, _playerIdentifier, _playerData, _playerState); if ((available & 2) == 2 && ImGui.IsItemClicked(ImGuiMouseButton.Right)) @@ -433,12 +537,16 @@ public sealed class DesignQuickBar : Window, IDisposable ++_numButtons; } - if ((_config.UseAdvancedParameters || _config.UseAdvancedDyes) && _config.QdbButtons.HasFlag(QdbButtons.RevertAdvanced)) + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) ++_numButtons; if (_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize)) ++_numButtons; if (_config.QdbButtons.HasFlag(QdbButtons.RevertEquip)) ++_numButtons; + if (_config.UseTemporarySettings && _config.QdbButtons.HasFlag(QdbButtons.ResetSettings)) + ++_numButtons; if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign)) { ++_numButtons; diff --git a/Glamourer/Gui/Equipment/BonusDrawData.cs b/Glamourer/Gui/Equipment/BonusDrawData.cs new file mode 100644 index 0000000..067c0c6 --- /dev/null +++ b/Glamourer/Gui/Equipment/BonusDrawData.cs @@ -0,0 +1,61 @@ +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public struct BonusDrawData(BonusItemFlag slot, in DesignData designData) +{ + private IDesignEditor _editor = null!; + private object _object = null!; + public readonly BonusItemFlag Slot = slot; + public bool Locked; + public bool DisplayApplication; + public bool AllowRevert; + public bool HasAdvancedDyes; + + public readonly bool IsDesign + => _object is Design; + + public readonly bool IsState + => _object is ActorState; + + public readonly void SetItem(EquipItem item) + => _editor.ChangeBonusItem(_object, Slot, item, ApplySettings.Manual); + + public readonly void SetApplyItem(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyBonusItem(design, Slot, value); + } + + public EquipItem CurrentItem = designData.BonusItem(slot); + public EquipItem GameItem = default; + public bool CurrentApply; + + public static BonusDrawData FromDesign(DesignManager manager, Design design, BonusItemFlag slot) + => new(slot, design.DesignData) + { + _editor = manager, + _object = design, + CurrentApply = design.DoApplyBonusItem(slot), + Locked = design.WriteProtected(), + DisplayApplication = true, + HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + }; + + public static BonusDrawData FromState(StateManager manager, ActorState state, BonusItemFlag slot) + => new(slot, state.ModelData) + { + _editor = manager, + _object = state, + Locked = state.IsLocked, + DisplayApplication = false, + GameItem = state.BaseData.BonusItem(slot), + AllowRevert = true, + HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + }; +} diff --git a/Glamourer/Gui/Equipment/BonusItemCombo.cs b/Glamourer/Gui/Equipment/BonusItemCombo.cs new file mode 100644 index 0000000..aa43da7 --- /dev/null +++ b/Glamourer/Gui/Equipment/BonusItemCombo.cs @@ -0,0 +1,121 @@ +using Dalamud.Plugin.Services; +using Glamourer.Services; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using Lumina.Excel.Sheets; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Log; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public sealed class BonusItemCombo : FilterComboCache +{ + private readonly FavoriteManager _favorites; + public readonly string Label; + private CustomItemId _currentItem; + private float _innerWidth; + + public PrimaryId CustomSetId { get; private set; } + public Variant CustomVariant { get; private set; } + + public BonusItemCombo(IDataManager gameData, ItemManager items, BonusItemFlag slot, Logger log, FavoriteManager favorites) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log) + { + _favorites = favorites; + Label = GetLabel(gameData, slot); + _currentItem = 0; + SearchByParts = true; + } + + protected override void DrawList(float width, float itemHeight) + { + base.DrawList(width, itemHeight); + if (NewSelection != null && Items.Count > NewSelection.Value) + CurrentSelection = Items[NewSelection.Value]; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + if (CurrentSelection.Id == _currentItem) + return currentSelected; + + CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem); + CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; + return base.UpdateCurrentSelected(CurrentSelectionIdx); + } + + public bool Draw(string previewName, BonusItemId previewIdx, float width, float innerWidth) + { + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; + return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + } + + protected override float GetFilterWidth() + => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var obj = Items[globalIdx]; + var name = ToString(obj); + if (UiHelpers.DrawFavoriteStar(_favorites, obj) && CurrentSelectionIdx == globalIdx) + { + CurrentSelectionIdx = -1; + _currentItem = obj.Id; + CurrentSelection = default; + } + + ImGui.SameLine(); + var ret = ImGui.Selectable(name, selected); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); + ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.Variant.Id})"); + return ret; + } + + protected override bool IsVisible(int globalIndex, LowerString filter) + => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); + + protected override string ToString(EquipItem obj) + => obj.Name; + + private static string GetLabel(IDataManager gameData, BonusItemFlag slot) + { + var sheet = gameData.GetExcelSheet()!; + + return slot switch + { + BonusItemFlag.Glasses => sheet.TryGetRow(16050, out var text) ? text.Text.ToString() : "Facewear", + BonusItemFlag.UnkSlot => sheet.TryGetRow(16051, out var text) ? text.Text.ToString() : "Facewear", + + _ => string.Empty, + }; + } + + private static List GetItems(FavoriteManager favorites, ItemManager items, BonusItemFlag slot) + { + var nothing = EquipItem.BonusItemNothing(slot); + return items.ItemData.ByType[slot.ToEquipType()].OrderByDescending(favorites.Contains).ThenBy(i => i.Id.Id).Prepend(nothing).ToList(); + } + + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant)) + return; + + CustomSetId = setId; + CustomVariant = variant; + } +} diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs index e6b5d0d..f32e22b 100644 --- a/Glamourer/Gui/Equipment/EquipDrawData.cs +++ b/Glamourer/Gui/Equipment/EquipDrawData.cs @@ -1,4 +1,5 @@ using Glamourer.Designs; +using Glamourer.Interop.Material; using Glamourer.State; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -7,9 +8,9 @@ namespace Glamourer.Gui.Equipment; public struct EquipDrawData(EquipSlot slot, in DesignData designData) { - private IDesignEditor _editor; - private object _object; - public readonly EquipSlot Slot = slot; + private IDesignEditor _editor = null!; + private object _object = null!; + public readonly EquipSlot Slot = slot; public bool Locked; public bool DisplayApplication; public bool AllowRevert; @@ -23,29 +24,31 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData) public readonly void SetItem(EquipItem item) => _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual); - public readonly void SetStain(StainId stain) - => _editor.ChangeStain(_object, Slot, stain, ApplySettings.Manual); + public readonly void SetStains(StainIds stains) + => _editor.ChangeStains(_object, Slot, stains, ApplySettings.Manual); + + public readonly void SetStain(int which, StainId stain) + => _editor.ChangeStains(_object, Slot, CurrentStains.With(which, stain), ApplySettings.Manual); public readonly void SetApplyItem(bool value) { var manager = (DesignManager)_editor; - var design = (Design)_object; - manager.ChangeApplyItem(design, Slot, value); + manager.ChangeApplyItem((Design)_object, Slot, value); } public readonly void SetApplyStain(bool value) { var manager = (DesignManager)_editor; - var design = (Design)_object; - manager.ChangeApplyStain(design, Slot, value); + manager.ChangeApplyStains((Design)_object, Slot, value); } - public EquipItem CurrentItem = designData.Item(slot); - public StainId CurrentStain = designData.Stain(slot); - public EquipItem GameItem = default; - public StainId GameStain = default; + public EquipItem CurrentItem = designData.Item(slot); + public StainIds CurrentStains = designData.Stain(slot); + public EquipItem GameItem = default; + public StainIds GameStains = default; public bool CurrentApply; public bool CurrentApplyStain; + public bool HasAdvancedDyes; public readonly Gender CurrentGender = designData.Customize.Gender; public readonly Race CurrentRace = designData.Customize.Race; @@ -58,6 +61,7 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData) CurrentApply = design.DoApplyEquip(slot), CurrentApplyStain = design.DoApplyStain(slot), Locked = design.WriteProtected(), + HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), DisplayApplication = true, }; @@ -69,7 +73,8 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData) Locked = state.IsLocked, DisplayApplication = false, GameItem = state.BaseData.Item(slot), - GameStain = state.BaseData.Stain(slot), + GameStains = state.BaseData.Stain(slot), + HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), AllowRevert = true, }; } diff --git a/Glamourer/Gui/Equipment/EquipItemSlotCache.cs b/Glamourer/Gui/Equipment/EquipItemSlotCache.cs new file mode 100644 index 0000000..20aaf11 --- /dev/null +++ b/Glamourer/Gui/Equipment/EquipItemSlotCache.cs @@ -0,0 +1,83 @@ +using Glamourer.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +[InlineArray(13)] +public struct EquipItemSlotCache +{ + private EquipItem _element; + + public EquipItem Dragged + { + get => this[^1]; + set => this[^1] = value; + } + + public void Clear() + => ((Span)this).Clear(); + + public EquipItem this[EquipSlot slot] + { + get => this[(int)slot.ToIndex()]; + set => this[(int)slot.ToIndex()] = value; + } + + public void Update(ItemManager items, in EquipItem item, EquipSlot startSlot) + { + if (item.Id == Dragged.Id && item.Type == Dragged.Type) + return; + + switch (startSlot) + { + case EquipSlot.MainHand: + { + Clear(); + this[EquipSlot.MainHand] = item; + if (item.Type is FullEquipType.Sword) + this[EquipSlot.OffHand] = items.FindClosestShield(item.ItemId, out var shield) ? shield : default; + else + this[EquipSlot.OffHand] = items.ItemData.Secondary.GetValueOrDefault(item.ItemId); + break; + } + case EquipSlot.OffHand: + { + Clear(); + if (item.Type is FullEquipType.Shield) + this[EquipSlot.MainHand] = items.FindClosestSword(item.ItemId, out var sword) ? sword : default; + else + this[EquipSlot.MainHand] = items.ItemData.Primary.GetValueOrDefault(item.ItemId); + this[EquipSlot.OffHand] = item; + break; + } + default: + { + this[EquipSlot.MainHand] = default; + this[EquipSlot.OffHand] = default; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + if (startSlot == slot) + { + this[slot] = item; + continue; + } + + var slotItem = items.Identify(slot, item.PrimaryId, item.Variant); + if (!slotItem.Valid || slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0) + { + slotItem = items.Identify(EquipSlot.OffHand, item.PrimaryId, item.SecondaryId, 1, item.Type); + if (slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0) + slotItem = default; + } + + this[slot] = slotItem; + } + + break; + } + } + + Dragged = item; + } +} diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index c5e5f7e..01ec938 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -5,10 +5,13 @@ using Glamourer.Events; using Glamourer.Gui.Materials; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; +using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using OtterGui.Widgets; +using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -23,29 +26,35 @@ public class EquipmentDrawer private readonly GlamourerColorCombo _stainCombo; private readonly DictStain _stainData; private readonly ItemCombo[] _itemCombo; + private readonly BonusItemCombo[] _bonusItemCombo; private readonly Dictionary _weaponCombo; - private readonly CodeService _codes; private readonly TextureService _textures; private readonly Configuration _config; private readonly GPoseService _gPose; private readonly AdvancedDyePopup _advancedDyes; + private readonly ItemCopyService _itemCopy; private float _requiredComboWidthUnscaled; private float _requiredComboWidth; - public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, CodeService codes, TextureService textures, - Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes) + private Stain? _draggedStain; + private EquipItemSlotCache _draggedItem; + private EquipSlot _dragTarget; + + public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, TextureService textures, + Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes, ItemCopyService itemCopy) { - _items = items; - _codes = codes; - _textures = textures; - _config = config; - _gPose = gPose; - _advancedDyes = advancedDyes; - _stainData = items.Stains; - _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites); - _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray(); - _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); + _items = items; + _textures = textures; + _config = config; + _gPose = gPose; + _advancedDyes = advancedDyes; + _itemCopy = itemCopy; + _stainData = items.Stains; + _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites); + _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray(); + _bonusItemCombo = BonusExtensions.AllFlags.Select(f => new BonusItemCombo(gameData, items, f, Glamourer.Log, favorites)).ToArray(); + _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); foreach (var type in Enum.GetValues()) { if (type.ToSlot() is EquipSlot.MainHand) @@ -59,6 +68,7 @@ public class EquipmentDrawer private Vector2 _iconSize; private float _comboLength; + private uint _advancedMaterialColor; public void Prepare() { @@ -70,7 +80,9 @@ public class EquipmentDrawer .Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X) / ImGuiHelpers.GlobalScale; - _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; + _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; + _advancedMaterialColor = ColorId.AdvancedDyeActive.Value(); + _dragTarget = EquipSlot.Unknown; } private bool VerifyRestrictedGear(EquipDrawData data) @@ -87,21 +99,34 @@ public class EquipmentDrawer if (_config.HideApplyCheckmarks) equipDrawData.DisplayApplication = false; - using var id = ImRaii.PushId((int)equipDrawData.Slot); + using var id = ImUtf8.PushId((int)equipDrawData.Slot); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) DrawEquipSmall(equipDrawData); - else if (!equipDrawData.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan)) - DrawEquipArtisan(equipDrawData); else DrawEquipNormal(equipDrawData); } + public void DrawBonusItem(BonusDrawData bonusDrawData) + { + if (_config.HideApplyCheckmarks) + bonusDrawData.DisplayApplication = false; + + using var id = ImUtf8.PushId(100 + (int)bonusDrawData.Slot); + var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + + if (_config.SmallEquip) + DrawBonusItemSmall(bonusDrawData); + else + DrawBonusItemNormal(bonusDrawData); + } + public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { - if (mainhand.CurrentItem.PrimaryId.Id == 0) + if (mainhand.CurrentItem.PrimaryId.Id == 0 && !allWeapons) return; if (_config.HideApplyCheckmarks) @@ -110,14 +135,12 @@ public class EquipmentDrawer offhand.DisplayApplication = false; } - using var id = ImRaii.PushId("Weapons"); + using var id = ImUtf8.PushId("Weapons"u8); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) DrawWeaponsSmall(mainhand, offhand, allWeapons); - else if (!mainhand.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan)) - DrawWeaponsArtisan(mainhand, offhand); else DrawWeaponsNormal(mainhand, offhand, allWeapons); } @@ -140,136 +163,31 @@ public class EquipmentDrawer } } - public bool DrawAllStain(out StainId ret, bool locked) + public bool DrawAllStain(out StainIds ret, bool locked) { using var disabled = ImRaii.Disabled(locked); var change = _stainCombo.Draw("Dye All Slots", Stain.None.RgbaColor, string.Empty, false, false, MouseWheelType.None); - ret = Stain.None.RowIndex; + ret = StainIds.None; if (change) if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out var stain)) - ret = stain.RowIndex; + ret = StainIds.All(stain.RowIndex); else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - ret = Stain.None.RowIndex; + ret = StainIds.None; if (!locked) { if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _config.DeleteDesignModifier.IsActive()) { - ret = Stain.None.RowIndex; + ret = StainIds.None; change = true; } - ImGuiUtil.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear."); + ImUtf8.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear."); } return change; } - #region Artisan - - private void DrawEquipArtisan(EquipDrawData data) - { - DrawStainArtisan(data); - ImGui.SameLine(); - DrawArmorArtisan(data); - if (!data.DisplayApplication) - return; - - ImGui.SameLine(); - DrawApply(data); - ImGui.SameLine(); - DrawApplyStain(data); - } - - private void DrawWeaponsArtisan(in EquipDrawData mainhand, in EquipDrawData offhand) - { - using (var _ = ImRaii.PushId(0)) - { - DrawStainArtisan(mainhand); - ImGui.SameLine(); - DrawWeapon(mainhand); - } - - using (var _ = ImRaii.PushId(1)) - { - DrawStainArtisan(offhand); - ImGui.SameLine(); - DrawWeapon(offhand); - } - - return; - - void DrawWeapon(in EquipDrawData current) - { - int setId = current.CurrentItem.PrimaryId.Id; - int type = current.CurrentItem.SecondaryId.Id; - int variant = current.CurrentItem.Variant.Id; - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != current.CurrentItem.PrimaryId.Id) - current.SetItem(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant)); - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##type", ref type, 0, 0)) - { - var newType = (SecondaryId)Math.Clamp(type, 0, ushort.MaxValue); - if (newType.Id != current.CurrentItem.SecondaryId.Id) - current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant)); - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant.Id != current.CurrentItem.Variant.Id) - current.SetItem(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId, - newVariant)); - } - } - } - - /// Draw an input for stain that can set arbitrary values instead of choosing valid stains. - private static void DrawStainArtisan(EquipDrawData data) - { - int stainId = data.CurrentStain.Id; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (!ImGui.InputInt("##stain", ref stainId, 0, 0)) - return; - - var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); - if (newStainId != data.CurrentStain.Id) - data.SetStain(newStainId); - } - - /// Draw an input for armor that can set arbitrary values instead of choosing items. - private void DrawArmorArtisan(EquipDrawData data) - { - int setId = data.CurrentItem.PrimaryId.Id; - int variant = data.CurrentItem.Variant.Id; - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != data.CurrentItem.PrimaryId.Id) - data.SetItem(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant)); - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant != data.CurrentItem.Variant) - data.SetItem(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant)); - } - } - - #endregion #region Small @@ -287,14 +205,31 @@ public class EquipmentDrawer } else if (equipDrawData.IsState) { - _advancedDyes.DrawButton(equipDrawData.Slot); + _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); } if (VerifyRestrictedGear(equipDrawData)) label += " (Restricted)"; + DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + } + + private void DrawBonusItemSmall(in BonusDrawData bonusDrawData) + { + ImGui.Dummy(new Vector2(StainId.NumStains * ImUtf8.FrameHeight + (StainId.NumStains - 1) * ImUtf8.ItemSpacing.X, ImUtf8.FrameHeight)); ImGui.SameLine(); - ImGui.TextUnformatted(label); + DrawBonusItem(bonusDrawData, out var label, true, false, false); + if (bonusDrawData.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(bonusDrawData); + } + else if (bonusDrawData.IsState) + { + _advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); } private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) @@ -311,12 +246,12 @@ public class EquipmentDrawer } else if (mainhand.IsState) { - _advancedDyes.DrawButton(EquipSlot.MainHand); + _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); } if (allWeapons) mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})"; - WeaponHelpMarker(mainhandLabel); + WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel); if (offhand.CurrentItem.Type is FullEquipType.Unknown) return; @@ -333,10 +268,10 @@ public class EquipmentDrawer } else if (offhand.IsState) { - _advancedDyes.DrawButton(EquipSlot.OffHand); + _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); } - WeaponHelpMarker(offhandLabel); + WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel); } #endregion @@ -357,8 +292,8 @@ public class EquipmentDrawer DrawApply(equipDrawData); } - ImGui.SameLine(); - ImGui.TextUnformatted(label); + DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + DrawStain(equipDrawData, false); if (equipDrawData.DisplayApplication) { @@ -367,16 +302,36 @@ public class EquipmentDrawer } else if (equipDrawData.IsState) { - _advancedDyes.DrawButton(equipDrawData.Slot); + _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); } if (VerifyRestrictedGear(equipDrawData)) { ImGui.SameLine(); - ImGui.TextUnformatted("(Restricted)"); + ImUtf8.Text("(Restricted)"u8); } } + private void DrawBonusItemNormal(in BonusDrawData bonusDrawData) + { + bonusDrawData.CurrentItem.DrawIcon(_textures, _iconSize, bonusDrawData.Slot); + var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); + var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + DrawBonusItem(bonusDrawData, out var label, false, right, left); + if (bonusDrawData.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(bonusDrawData); + } + else if (bonusDrawData.IsState) + { + _advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + } + private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, @@ -385,7 +340,7 @@ public class EquipmentDrawer mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); - using (ImRaii.Group()) + using (ImUtf8.Group()) { DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left); if (mainhand.DisplayApplication) @@ -394,7 +349,8 @@ public class EquipmentDrawer DrawApply(mainhand); } - WeaponHelpMarker(mainhandLabel, allWeapons ? mainhand.CurrentItem.Type.ToName() : null); + WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel, + allWeapons ? mainhand.CurrentItem.Type.ToName() : null); DrawStain(mainhand, false); if (mainhand.DisplayApplication) @@ -404,7 +360,7 @@ public class EquipmentDrawer } else if (mainhand.IsState) { - _advancedDyes.DrawButton(EquipSlot.MainHand); + _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); } } @@ -415,7 +371,7 @@ public class EquipmentDrawer var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); - using (ImRaii.Group()) + using (ImUtf8.Group()) { DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left); if (offhand.DisplayApplication) @@ -424,7 +380,7 @@ public class EquipmentDrawer DrawApply(offhand); } - WeaponHelpMarker(offhandLabel); + WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel); DrawStain(offhand, false); if (offhand.DisplayApplication) @@ -432,28 +388,62 @@ public class EquipmentDrawer ImGui.SameLine(); DrawApplyStain(offhand); } - else if (mainhand.IsState) + else if (offhand.IsState) { - _advancedDyes.DrawButton(EquipSlot.OffHand); + _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); } } } private void DrawStain(in EquipDrawData data, bool small) { - var found = _stainData.TryGetValue(data.CurrentStain, out var stain); using var disabled = ImRaii.Disabled(data.Locked); - var change = small - ? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) - : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); - if (change) - if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) - data.SetStain(stain.RowIndex); - else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - data.SetStain(Stain.None.RowIndex); + var width = (_comboLength - ImUtf8.ItemInnerSpacing.X * (data.CurrentStains.Count - 1)) / data.CurrentStains.Count; + foreach (var (stainId, index) in data.CurrentStains.WithIndex()) + { + using var id = ImUtf8.PushId(index); + var found = _stainData.TryGetValue(stainId, out var stain); + var change = small + ? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) + : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, width); - if (ResetOrClear(data.Locked, false, data.AllowRevert, true, data.CurrentStain, data.GameStain, Stain.None.RowIndex, out var newStain)) - data.SetStain(newStain); + _itemCopy.HandleCopyPaste(data, index); + if (!change) + DrawStainDragDrop(data, index, stain, found); + + if (index < data.CurrentStains.Count - 1) + ImUtf8.SameLineInner(); + + if (change) + if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) + data.SetStains(data.CurrentStains.With(index, stain.RowIndex)); + else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) + data.SetStains(data.CurrentStains.With(index, Stain.None.RowIndex)); + if (ResetOrClear(data.Locked, false, data.AllowRevert, true, stainId, data.GameStains[index], Stain.None.RowIndex, + out var newStain)) + data.SetStains(data.CurrentStains.With(index, newStain)); + } + } + + private void DrawStainDragDrop(in EquipDrawData data, int index, Stain stain, bool found) + { + if (found) + { + using var dragSource = ImUtf8.DragDropSource(); + if (dragSource.Success) + { + DragDropSource.SetPayload("stainDragDrop"u8); + _draggedStain = stain; + ImUtf8.Text($"Dragging {stain.Name}..."); + } + } + + using var dragTarget = ImUtf8.DragDropTarget(); + if (dragTarget.IsDropping("stainDragDrop"u8) && _draggedStain.HasValue) + { + data.SetStains(data.CurrentStains.With(index, _draggedStain.Value.RowIndex)); + _draggedStain = null; + } } private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open) @@ -468,16 +458,91 @@ public class EquipmentDrawer using var disabled = ImRaii.Disabled(data.Locked); var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); + DrawGearDragDrop(data); + if (change) + data.SetItem(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0) + data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); + _itemCopy.HandleCopyPaste(data); + + if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot), + out var item)) + data.SetItem(item); + } + + private void DrawBonusItem(in BonusDrawData data, out string label, bool small, bool clear, bool open) + { + var combo = _bonusItemCombo[data.Slot.ToIndex()]; + label = combo.Label; + if (!data.Locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + + using var disabled = ImRaii.Disabled(data.Locked); + var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.Id.BonusItem, + small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth); + if (ImGui.IsItemHovered() && ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsKeyPressed(ImGuiKey.C)) + _itemCopy.Copy(combo.CurrentSelection); + else if (ImGui.IsKeyPressed(ImGuiKey.V)) + _itemCopy.Paste(data.Slot.ToEquipType(), data.SetItem); + } + if (change) data.SetItem(combo.CurrentSelection); else if (combo.CustomVariant.Id > 0) data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); - if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot), + if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, EquipItem.BonusItemNothing(data.Slot), out var item)) data.SetItem(item); } + private void DrawGearDragDrop(in EquipDrawData data) + { + if (data.CurrentItem.Valid) + { + using var dragSource = ImUtf8.DragDropSource(); + if (dragSource.Success) + { + DragDropSource.SetPayload("equipDragDrop"u8); + _draggedItem.Update(_items, data.CurrentItem, data.Slot); + } + } + + using var dragTarget = ImUtf8.DragDropTarget(); + if (!dragTarget) + return; + + var item = _draggedItem[data.Slot]; + if (!item.Valid) + return; + + _dragTarget = data.Slot; + if (!dragTarget.IsDropping("equipDragDrop"u8)) + return; + + data.SetItem(item); + _draggedItem.Clear(); + } + + public unsafe void DrawDragDropTooltip() + { + var payload = ImGui.GetDragDropPayload().Handle; + if (payload is null) + return; + + if (!MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(ref payload->DataType_0)).SequenceEqual("equipDragDrop"u8)) + return; + + using var tt = ImUtf8.Tooltip(); + if (_dragTarget is EquipSlot.Unknown) + ImUtf8.Text($"Dragging {_draggedItem.Dragged.Name}..."); + else + ImUtf8.Text($"Converting to {_draggedItem[_dragTarget].Name}..."); + } + private static bool ResetOrClear(bool locked, bool clicked, bool allowRevert, bool allowClear, in T currentItem, in T revertItem, in T clearItem, out T? item) where T : IEquatable { @@ -501,7 +566,7 @@ public class EquipmentDrawer (false, true, _) => ("Right-click to clear.\nControl and mouse wheel to scroll.", clearItem, true), (false, false, _) => ("Control and mouse wheel to scroll.", default, false), }; - ImGuiUtil.HoverTooltip(tt); + ImUtf8.HoverTooltip(tt); return clicked && valid; } @@ -526,8 +591,13 @@ public class EquipmentDrawer if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) changedItem = combo.CurrentSelection; - else if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem, - default, out var c)) + else if (combo.CustomVariant.Id > 0 && (drawAll || ItemData.ConvertWeaponId(combo.CustomSetId) == mainhand.CurrentItem.Type)) + changedItem = _items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant); + _itemCopy.HandleCopyPaste(mainhand); + DrawGearDragDrop(mainhand); + + if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem, + default, out var c)) changedItem = c; if (changedItem != null) @@ -543,8 +613,9 @@ public class EquipmentDrawer } } - if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible."); + if (unknown) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "The weapon type could not be identified, thus changing it to other weapons of that type is not possible."u8); } private void DrawOffhand(in EquipDrawData mainhand, in EquipDrawData offhand, out string label, bool small, bool clear, bool open) @@ -557,13 +628,17 @@ public class EquipmentDrawer label = combo.Label; var locked = offhand.Locked - || !_gPose.InGPose && (offhand.CurrentItem.Type is FullEquipType.Unknown || mainhand.CurrentItem.Type is FullEquipType.Unknown); + || !_gPose.InGPose && (offhand.CurrentItem.Type.IsUnknown() || mainhand.CurrentItem.Type.IsUnknown()); using var disabled = ImRaii.Disabled(locked); if (!locked && open) UiHelpers.OpenCombo($"##{combo.Label}"); if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) offhand.SetItem(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0 && ItemData.ConvertWeaponId(combo.CustomSetId) == offhand.CurrentItem.Type) + offhand.SetItem(_items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant)); + _itemCopy.HandleCopyPaste(offhand); + DrawGearDragDrop(offhand); var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem); if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item)) @@ -577,6 +652,13 @@ public class EquipmentDrawer data.SetApplyItem(enabled); } + private static void DrawApply(in BonusDrawData data) + { + if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this bonus item when applying the Design.", data.CurrentApply, out var enabled, + data.Locked)) + data.SetApplyItem(enabled); + } + private static void DrawApplyStain(in EquipDrawData data) { if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this dye to the item when applying the Design.", data.CurrentApplyStain, @@ -587,14 +669,14 @@ public class EquipmentDrawer #endregion - private static void WeaponHelpMarker(string label, string? type = null) + private void WeaponHelpMarker(bool hasAdvancedDyes, string label, string? type = null) { ImGui.SameLine(); ImGuiComponents.HelpMarker( "Changing weapons to weapons of different types can cause crashes, freezes, soft- and hard locks and cheating, " + "thus it is only allowed to change weapons to other weapons of the same type."); - ImGui.SameLine(); - ImGui.TextUnformatted(label); + DrawEquipLabel(hasAdvancedDyes, label); + if (type == null) return; @@ -602,4 +684,17 @@ public class EquipmentDrawer pos.Y += ImGui.GetFrameHeightWithSpacing(); ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private void DrawEquipLabel(bool hasAdvancedDyes, string label) + { + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, _advancedMaterialColor, hasAdvancedDyes)) + { + ImUtf8.Text(label); + } + + if (hasAdvancedDyes) + ImUtf8.HoverTooltip("This design has advanced dyes setup for this slot."u8); + } } diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs index 527dbb5..3149e67 100644 --- a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -2,7 +2,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Structs; diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index 7e298c0..7c0c3bc 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -1,12 +1,13 @@ using Dalamud.Plugin.Services; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using OtterGui; +using Dalamud.Bindings.ImGui; +using Lumina.Excel.Sheets; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -75,32 +76,32 @@ public sealed class ItemCombo : FilterComboCache var ret = ImGui.Selectable(name, selected); ImGui.SameLine(); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); - ImGuiUtil.RightAlign($"({obj.ModelString})"); + ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.Variant.Id})"); return ret; } protected override bool IsVisible(int globalIndex, LowerString filter) - => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); + => base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower); protected override string ToString(EquipItem obj) => obj.Name; private static string GetLabel(IDataManager gameData, EquipSlot slot) { - var sheet = gameData.GetExcelSheet()!; + var sheet = gameData.GetExcelSheet(); return slot switch { - EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head", - EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body", - EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands", - EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs", - EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet", - EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears", - EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck", - EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists", - EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring", - EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring", + EquipSlot.Head => sheet.TryGetRow(740, out var text) ? text.Text.ToString() : "Head", + EquipSlot.Body => sheet.TryGetRow(741, out var text) ? text.Text.ToString() : "Body", + EquipSlot.Hands => sheet.TryGetRow(742, out var text) ? text.Text.ToString() : "Hands", + EquipSlot.Legs => sheet.TryGetRow(744, out var text) ? text.Text.ToString() : "Legs", + EquipSlot.Feet => sheet.TryGetRow(745, out var text) ? text.Text.ToString() : "Feet", + EquipSlot.Ears => sheet.TryGetRow(746, out var text) ? text.Text.ToString() : "Ears", + EquipSlot.Neck => sheet.TryGetRow(747, out var text) ? text.Text.ToString() : "Neck", + EquipSlot.Wrists => sheet.TryGetRow(748, out var text) ? text.Text.ToString() : "Wrists", + EquipSlot.RFinger => sheet.TryGetRow(749, out var text) ? text.Text.ToString() : "Right Ring", + EquipSlot.LFinger => sheet.TryGetRow(750, out var text) ? text.Text.ToString() : "Left Ring", _ => string.Empty, }; } diff --git a/Glamourer/Gui/Equipment/ItemCopyService.cs b/Glamourer/Gui/Equipment/ItemCopyService.cs new file mode 100644 index 0000000..6912f1f --- /dev/null +++ b/Glamourer/Gui/Equipment/ItemCopyService.cs @@ -0,0 +1,73 @@ +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui.Services; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public class ItemCopyService(ItemManager items, DictStain stainData) : IUiService +{ + public EquipItem? Item { get; private set; } + public Stain? Stain { get; private set; } + + public void Copy(in EquipItem item) + => Item = item; + + public void Copy(in Stain stain) + => Stain = stain; + + public void Paste(int which, Action setter) + { + if (Stain is { } stain) + setter(which, stain.RowIndex); + } + + public void Paste(FullEquipType type, Action setter) + { + if (Item is not { } item) + return; + + if (type != item.Type) + { + if (type.IsBonus()) + item = items.Identify(type.ToBonus(), item.PrimaryId, item.Variant); + else if (type.IsEquipment() || type.IsAccessory()) + item = items.Identify(type.ToSlot(), item.PrimaryId, item.Variant); + else + item = items.Identify(type.ToSlot(), item.PrimaryId, item.SecondaryId, item.Variant); + } + + if (item.Valid && item.Type == type) + setter(item); + } + + public void HandleCopyPaste(in EquipDrawData data) + { + if (ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + Paste(data.CurrentItem.Type, data.SetItem); + } + else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + { + Copy(data.CurrentItem); + } + } + + public void HandleCopyPaste(in EquipDrawData data, int which) + { + if (ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + Paste(which, data.SetStain); + } + else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) + && ImGui.IsMouseClicked(ImGuiMouseButton.Middle) + && stainData.TryGetValue(data.CurrentStains[which].Id, out var stain)) + { + Copy(stain); + } + } +} diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index f6531a2..3029db7 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -1,10 +1,11 @@ using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -18,6 +19,10 @@ public sealed class WeaponCombo : FilterComboCache private ItemId _currentItem; private float _innerWidth; + public PrimaryId CustomSetId { get; private set; } + public SecondaryId CustomWeaponId { get; private set; } + public Variant CustomVariant { get; private set; } + public WeaponCombo(ItemManager items, FullEquipType type, Logger log, FavoriteManager favorites) : base(() => GetWeapons(favorites, items, type), MouseWheelType.Control, log) { @@ -45,8 +50,9 @@ public sealed class WeaponCombo : FilterComboCache public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth) { - _innerWidth = innerWidth; - _currentItem = previewIdx; + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); } @@ -69,18 +75,36 @@ public sealed class WeaponCombo : FilterComboCache var ret = ImGui.Selectable(name, selected); ImGui.SameLine(); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); - ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant})"); + ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant.Id})"); return ret; } + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full tuple of set id, weapon id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 3 + || !ushort.TryParse(split[0], out var setId) + || !ushort.TryParse(split[1], out var weaponId) + || !byte.TryParse(split[2], out var variant)) + return; + + CustomSetId = setId; + CustomWeaponId = weaponId; + CustomVariant = variant; + } + protected override bool IsVisible(int globalIndex, LowerString filter) - => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); + => base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower); protected override string ToString(EquipItem obj) => obj.Name; private static string GetLabel(FullEquipType type) - => type is FullEquipType.Unknown ? "Mainhand" : type.ToName(); + => type.IsUnknown() ? "Mainhand" : type.ToName(); private static IReadOnlyList GetWeapons(FavoriteManager favorites, ItemManager items, FullEquipType type) { diff --git a/Glamourer/Gui/GenericPopupWindow.cs b/Glamourer/Gui/GenericPopupWindow.cs index 502af14..5061862 100644 --- a/Glamourer/Gui/GenericPopupWindow.cs +++ b/Glamourer/Gui/GenericPopupWindow.cs @@ -3,7 +3,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Glamourer.Gui.Materials; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; diff --git a/Glamourer/Gui/GlamourerChangelog.cs b/Glamourer/Gui/GlamourerChangelog.cs index e9173ec..686d4a1 100644 --- a/Glamourer/Gui/GlamourerChangelog.cs +++ b/Glamourer/Gui/GlamourerChangelog.cs @@ -34,6 +34,17 @@ public class GlamourerChangelog Add1_2_1_0(Changelog); AddDummy(Changelog); Add1_2_3_0(Changelog); + Add1_3_1_0(Changelog); + Add1_3_2_0(Changelog); + Add1_3_3_0(Changelog); + Add1_3_4_0(Changelog); + Add1_3_5_0(Changelog); + Add1_3_6_0(Changelog); + Add1_3_7_0(Changelog); + Add1_3_8_0(Changelog); + Add1_4_0_0(Changelog); + Add1_5_0_0(Changelog); + Add1_5_1_0(Changelog); } private (int, ChangeLogDisplayType) ConfigData() @@ -54,6 +65,209 @@ public class GlamourerChangelog } } + private static void Add1_5_1_0(Changelog log) + => log.NextVersion("Version 1.5.1.0") + .RegisterHighlight("Added support for Penumbras PCP functionality to add the current state of the character as a design.") + .RegisterEntry("On import, a design for the PCP is created and, if possible, applied to the character.", 1) + .RegisterEntry("No automation is assigned.", 1) + .RegisterEntry("Finer control about this can be found in the settings.", 1) + .RegisterEntry("Fixed an issue with static visors not toggling through Glamourer (1.5.0.7).") + .RegisterEntry("The advanced dye slot combo now contains glasses (1.5.0.7).") + .RegisterEntry("Several fixes for patch-related issues (1.5.0.1 - 1.5.0.6"); + + private static void Add1_5_0_0(Changelog log) + => log.NextVersion("Version 1.5.0.0") + .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") + .RegisterHighlight("Added the new Viera Ears state to designs. Old designs will not apply the state.") + .RegisterHighlight("Added the option to make newly created designs write-protected by default to the design defaults.") + .RegisterEntry("Fixed issues with reverting state and IPC.") + .RegisterEntry("Fixed an issue when using the mousewheel to scroll through designs (1.4.0.3).") + .RegisterEntry("Fixed an issue with invalid bonus items (1.4.0.3).") + .RegisterHighlight("Added drag & drop of equipment pieces which will try to match the corresponding model IDs in other slots if possible (1.4.0.2).") + .RegisterEntry("Heavily optimized some issues when having many designs and creating new ones or updating them (1.4.0.2)") + .RegisterEntry("Fixed an issue with staining templates (1.4.0.1).") + .RegisterEntry("Fixed an issue with the QDB buttons not counting correctly (1.4.0.1)."); + + private static void Add1_4_0_0(Changelog log) + => log.NextVersion("Version 1.4.0.0") + .RegisterHighlight("The design selector width is now draggable within certain restrictions that depend on the total window width.") + .RegisterEntry("The current behavior may not be final, let me know if you have any comments.", 1) + .RegisterEntry("Regular customization colors can now be dragged & dropped onto other customizations.") + .RegisterEntry( + "If no identical color is available in the target slot, the most similar color available (for certain values of similar) will be chosen instead.", + 1) + .RegisterEntry("Resetting advanced dyes and customizations has been split into two buttons for the quick design bar.") + .RegisterEntry("Weapons now also support custom ID input in the combo search box.") + .RegisterEntry("Added new IPC methods GetExtendedDesignData, AddDesign, DeleteDesign, GetDesignBase64, GetDesignJObject.") + .RegisterEntry("Added the option to prevent immediate repeats for random design selection (Thanks Diorik!).") + .RegisterEntry("Optimized some multi-design changes when selecting many designs and changing them at once.") + .RegisterEntry("Fixed item combos not starting from the currently selected item when scrolling them via mouse wheel.") + .RegisterEntry("Fixed some issue with Glamourer not searching mods by name for mod associations in some cases.") + .RegisterEntry("Fixed the IPC methods SetMetaState and SetMetaStateName not working (Thanks Caraxi!).") + .RegisterEntry("Added new IPC method GetDesignListExtended. (1.3.8.6)") + .RegisterEntry( + "Improved the naming of NPCs for identifiers by using Haselnussbombers new naming functionality (Thanks Hasel!). (1.3.8.6)") + .RegisterEntry( + "Added a modifier key separate from the delete modifier key that is used for less important key-checks, specifically toggling incognito mode. (1.3.8.5)") + .RegisterEntry("Used better Penumbra IPC for some things. (1.3.8.5)") + .RegisterEntry("Fixed an issue with advanced dyes for weapons. (1.3.8.5)") + .RegisterEntry("Fixed an issue with NPC automation due to missing job detection. (1.3.8.1)"); + + private static void Add1_3_8_0(Changelog log) + => log.NextVersion("Version 1.3.8.0") + .RegisterImportant("Updated Glamourer for update 7.20 and Dalamud API 12.") + .RegisterEntry( + "This is not thoroughly tested, but I decided to push to stable instead of testing because otherwise a lot of people would just go to testing just for early access again despite having no business doing so.", + 1) + .RegisterEntry( + "I also do not use most of the functionality of Glamourer myself, so I am unable to even encounter most issues myself.", 1) + .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) + .RegisterEntry("Added a chat command to clear temporary settings applied by Glamourer to Penumbra.") + .RegisterEntry("Fixed small issues with customizations not applicable to your race still applying."); + + private static void Add1_3_7_0(Changelog log) + => log.NextVersion("Version 1.3.7.0") + .RegisterImportant( + "The option to disable advanced customizations or advanced dyes has been removed. The functionality can no longer be disabled entirely, you can just decide not to use it, and to hide it.") + .RegisterHighlight( + "You can now configure which panels (like Customization, Equipment, Advanced Customization etc.) are displayed at all, and which are expanded by default. This does not disable any functionality.") + .RegisterHighlight( + "The Unlocks tab now shows whether items are modded in the currently selected collection in Penumbra in Overview mode and shows and can filter and sort for it in Detailed mode.") + .RegisterEntry("Added an optional button to the Quick Design Bar to reset all temporary settings applied by Glamourer.") + .RegisterHighlight( + "Any existing advanced dyes will now be highlighted on the corresponding Advanced Dye buttons in the actors panel and on the corresponding equip slot name in the design panel.") + .RegisterEntry("This also affects currently inactive advanced dyes, which can now be manually removed on the inactive materials.", + 1) + .RegisterHighlight( + "In the design list of an automation set, the design indices are now highlighted if a design contains advanced dyes, mod associations, or links to other designs.") + .RegisterHighlight("Some quality of life improvements:") + .RegisterEntry("Added some buttons for some application rule presets to the Application Rules panel.", 1) + .RegisterEntry("Added some buttons to enable, disable or delete all advanced dyes in a design.", 1) + .RegisterEntry("Some of those buttons are also available in multi-design selection to apply to all selected designs at once.", 1) + .RegisterEntry( + "A copied material color set from Penumbra should now be able to be imported into a advanced dye color set, as well as the other way around.") + .RegisterEntry( + "Automatically applied character updates when applying a design with mod associations and temporary settings are now skipped to prevent some issues with GPose. This should not affect anything else.") + .RegisterEntry("Glamourer now differentiates between temporary settings applied through manual or automatic application."); + + + private static void Add1_3_6_0(Changelog log) + => log.NextVersion("Version 1.3.6.0") + .RegisterHighlight("Added some new multi design selection functionality to change design settings of many designs at once.") + .RegisterEntry("Also added the number of selected designs and folders to the multi design selection display.", 1) + .RegisterEntry("Glamourer will now use temporary settings when saving mod associations, if they exist in Penumbra.") + .RegisterEntry( + "Actually added the checkbox to reset all temporary settings to Automation Sets (functionality was there, just not exposed to the UI...).") + .RegisterEntry( + "Adapted the behavior for identified copies of characters that have a different state than the character itself to deal with the associated Penumbra changes.") + .RegisterEntry( + "Added '/glamour resetdesign' as a command, that re-applies automation but resets randomly chosen designs (Thanks Diorik).") + .RegisterEntry("All existing facepaints should now be accepted in designs, including NPC facepaints.") + .RegisterEntry( + "Overwriting a design with your characters current state will now discard any prior advanced dyes and only add those from the current state.") + .RegisterEntry("Fixed an issue with racial mount and accessory scaling when changing zones on a changed race.") + .RegisterEntry("Fixed issues with the detection of gear set changes in certain circumstances (Thanks Cordelia).") + .RegisterEntry("Fixed an issue with the Force to Inherit checkbox in mod associations.") + .RegisterEntry( + "Added a new IPC event that fires only when Glamourer finalizes its current changes to a character (for/from Cordelia).") + .RegisterEntry("Added new IPC to set a meta flag on actors. (for/from Cordelia)."); + + private static void Add1_3_5_0(Changelog log) + => log.NextVersion("Version 1.3.5.0") + .RegisterHighlight( + "Added the usage of the new Temporary Mod Setting functionality from Penumbra to apply mod associations. This is on by default but can be turned back to permanent changes in the settings.") + .RegisterEntry("Designs now have a setting to always reset all prior temporary settings made by Glamourer on application.", 1) + .RegisterEntry("Automation Sets also have a setting to do this, independently of the designs contained in them.", 1) + .RegisterHighlight("More NPC customization options should now be accepted as valid for designs, regardless of clan/gender.") + .RegisterHighlight("The 'Apply' chat command had the currently selected design and the current quick bar design added as choices.") + .RegisterEntry( + "The application buttons for designs, NPCs or actors should now stick at the top of their respective panels even when scrolling down.") + .RegisterHighlight("Randomly chosen designs should now stay across loading screens or redrawing. (1.3.4.3)") + .RegisterEntry( + "In automation, Random designs now have an option to always choose another design, including during loading screens or redrawing.", + 1) + .RegisterEntry("Fixed an issue where disabling auto designs did not work as expected.") + .RegisterEntry("Fixed the inversion of application flags in IPC calls.") + .RegisterEntry("Fixed an issue with the scaling of the Advanced Dye popup with increased font sizes.") + .RegisterEntry("Fixed a bug when editing gear set conditions in the automation tab.") + .RegisterEntry("Fixed some ImGui issues."); + + private static void Add1_3_4_0(Changelog log) + => log.NextVersion("Version 1.3.4.0") + .RegisterEntry("Glamourer has been updated for Dalamud API 11 and patch 7.1.") + .RegisterEntry("Maybe fixed issues with shared weapon types and reset designs.") + .RegisterEntry("Fixed issues with resetting advanced dyes and certain weapon types."); + + private static void Add1_3_3_0(Changelog log) + => log.NextVersion("Version 1.3.3.0") + .RegisterHighlight("Added the option to create automations for owned human NPCs (like trust avatars).") + .RegisterEntry("Added some special filters to the Actors tab selector, hover over it to see the options.") + .RegisterEntry("Added an option for designs to always reset all previously applied advanced dyes.") + .RegisterEntry("Added some new NPC-only customizations to the valid customizations.") + .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke."); + + private static void Add1_3_2_0(Changelog log) + => log.NextVersion("Version 1.3.2.0") + .RegisterEntry("Fixed an issue with weapon hiding when leaving GPose or changing zones.") + .RegisterEntry("Added support for unnamed items to be previewed from Penumbra.") + .RegisterEntry( + "Item combos filters now check if the model string starts with the current filter, instead of checking if the primary ID contains the current filter.") + .RegisterEntry("Improved the handling of bonus items (glasses) in designs.") + .RegisterEntry("Imported .chara files now import bonus items.") + .RegisterEntry( + "Added a Debug Data rider in the Actors tab that is visible if Debug Mode is enabled and (currently) contains some IDs.") + .RegisterEntry("Fixed bonus items not reverting correctly in some cases.") + .RegisterEntry("Fixed an issue with the RNG in cheat codes and events skipping some possible entries.") + .RegisterEntry("Fixed the chat log context menu for glamourer Try-On.") + .RegisterEntry("Fixed some issues with cheat code sets.") + .RegisterEntry( + "Made the popped out Advanced Dye Window and Unlocks Window non-docking as that caused issues when docked to the main Glamourer window.") + .RegisterEntry("Refreshed NPC name associations.") + .RegisterEntry("Removed a now useless cheat code.") + .RegisterEntry("Added API for Bonus Items. (1.3.1.1)"); + + private static void Add1_3_1_0(Changelog log) + => log.NextVersion("Version 1.3.1.0") + .RegisterHighlight("Glamourer is now released for Dawntrail!") + .RegisterEntry("Added support for female Hrothgar.", 1) + .RegisterEntry("Added support for the Glasses slot.", 1) + .RegisterEntry("Added support for two dye slots.", 1) + .RegisterImportant( + "There were some issues with Advanced Dyes stored in Designs. When launching this update, Glamourer will try to migrate all your old designs into the new form.") + .RegisterEntry("Unfortunately, this is slightly based on guesswork and may cause false-positive migrations.", 1) + .RegisterEntry("In general, the values for Gloss and Specular Strength were swapped, so the migration swaps them back.", 1) + .RegisterEntry( + "In some cases this may not be correct, or the values stored were problematic to begin with and will now cause further issues.", + 1) + .RegisterImportant( + "If your designs lose their specular color, you need to verify that the Specular Strength is non-zero (usually in 0-100%).", 1) + .RegisterImportant( + "If your designs are extremely glossy and reflective, you need to verify that the Gloss value is greater than zero (usually a power of 2 >= 1, it should never be 0).", + 1) + .RegisterEntry( + "I am very sorry for the inconvenience but there is no way to salvage this sanely in all cases, especially with user-input values.", + 1) + .RegisterImportant( + "Any materials already using Dawntrails shaders will currently not be able to edit the Gloss or Specular Strength Values in Advanced Dyes.") + .RegisterImportant( + "Skin and Hair Shine from advanced customizations are not supported by the game any longer, so they are not displayed for the moment.") + .RegisterHighlight("All eyes now support Limbal rings (which use the Feature Color for their color).") + .RegisterHighlight("Dyes can now be dragged and dropped onto other dyes to replicate them.") + .RegisterEntry("The job filter in the Unlocks tab has been improved.") + .RegisterHighlight( + "Editing designs or actors now has a history and you can undo up to 16 of the last changes you made, separately per design or actor.") + .RegisterEntry( + "Some changes (like when a weapon applies its offhand) may count as multiple and have to be stepped back separately.", 1) + .RegisterEntry("You can now change the priority or enabled state of associated mods directly.") + .RegisterEntry("Glamourer now has a Support Info button akin to Penumbra's.") + .RegisterEntry("Glamourer now respects write protection on designs better.") + .RegisterEntry("The advanced dye window popup should now get focused when it is opening even in detached state.") + .RegisterEntry("Added API and IPC for bonus items, i.e. the Glasses slot.") + .RegisterHighlight("You can now display your characters height in Corgis or Olympic Swimming Pools.") + .RegisterEntry("Fixed some issues with advanced customizations and dyes applied via IPC. (1.2.3.2)") + .RegisterEntry( + "Glamourer now uses the last matching game object for advanced dyes instead of the first (mainly relevant for GPose). (1.2.3.1)"); + private static void Add1_2_3_0(Changelog log) => log.NextVersion("Version 1.2.3.0") .RegisterHighlight( diff --git a/Glamourer/Gui/GlamourerWindowSystem.cs b/Glamourer/Gui/GlamourerWindowSystem.cs index 6b34c78..f86f42b 100644 --- a/Glamourer/Gui/GlamourerWindowSystem.cs +++ b/Glamourer/Gui/GlamourerWindowSystem.cs @@ -7,11 +7,11 @@ namespace Glamourer.Gui; public class GlamourerWindowSystem : IDisposable { private readonly WindowSystem _windowSystem = new("Glamourer"); - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly MainWindow _ui; private readonly PenumbraChangedItemTooltip _penumbraTooltip; - public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip, + public GlamourerWindowSystem(IUiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip, Configuration config, UnlocksTab unlocksTab, GlamourerChangelog changelog, DesignQuickBar quick) { _uiBuilder = uiBuilder; diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index f7cd589..abde603 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -1,4 +1,5 @@ -using Dalamud.Interface.Windowing; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Glamourer.Designs; using Glamourer.Events; @@ -11,8 +12,9 @@ using Glamourer.Gui.Tabs.NpcTab; using Glamourer.Gui.Tabs.SettingsTab; using Glamourer.Gui.Tabs.UnlocksTab; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Classes; using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Services; @@ -62,7 +64,7 @@ public class MainWindow : Window, IDisposable public TabType SelectTab; - public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, + public MainWindow(IDalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar, NpcTab npcs, MainWindowPosition position, PenumbraService penumbra) : base("GlamourerMainWindow") @@ -100,6 +102,8 @@ public class MainWindow : Window, IDisposable SelectTab = _config.Ephemeral.SelectedTab; _event.Subscribe(OnTabSelected, TabSelected.Priority.MainWindow); IsOpen = _config.OpenWindowAtStart; + + _penumbra.DrawSettingsSection += Settings.DrawPenumbraIntegrationSettings; } public void OpenSettings() @@ -118,7 +122,10 @@ public class MainWindow : Window, IDisposable } public void Dispose() - => _event.Unsubscribe(OnTabSelected); + { + _event.Unsubscribe(OnTabSelected); + _penumbra.DrawSettingsSection -= Settings.DrawPenumbraIntegrationSettings; + } public override void Draw() { @@ -131,7 +138,12 @@ public class MainWindow : Window, IDisposable if (_penumbra.CurrentMajor == 0) DrawProblemWindow( "Could not attach to Penumbra. Please make sure Penumbra is installed and running.\n\nPenumbra is required for Glamourer to work properly."); - else if (_penumbra is { CurrentMajor: PenumbraService.RequiredPenumbraBreakingVersion, CurrentMinor: >= PenumbraService.RequiredPenumbraFeatureVersion }) + else if (_penumbra is + { + + CurrentMajor: PenumbraService.RequiredPenumbraBreakingVersion, + CurrentMinor: >= PenumbraService.RequiredPenumbraFeatureVersion, + }) DrawProblemWindow( $"You are currently not attached to Penumbra, seemingly by manually detaching from it.\n\nPenumbra's last API Version was {_penumbra.CurrentMajor}.{_penumbra.CurrentMinor}.\n\nPenumbra is required for Glamourer to work properly."); else @@ -184,22 +196,42 @@ public class MainWindow : Window, IDisposable return TabType.None; } + /// The longest support button text. + public static ReadOnlySpan SupportInfoButtonText + => "Copy Support Info to Clipboard"u8; + /// Draw the support button group on the right-hand side of the window. - public static void DrawSupportButtons(Changelog changelog) + public static void DrawSupportButtons(Glamourer glamourer, Changelog changelog) { - var width = ImGui.CalcTextSize("Join Discord for Support").X + ImGui.GetStyle().FramePadding.X * 2; + var width = ImUtf8.CalcTextSize(SupportInfoButtonText).X + ImGui.GetStyle().FramePadding.X * 2; var xPos = ImGui.GetWindowWidth() - width; ImGui.SetCursorPos(new Vector2(xPos, 0)); CustomGui.DrawDiscordButton(Glamourer.Messager, width); ImGui.SetCursorPos(new Vector2(xPos, ImGui.GetFrameHeightWithSpacing())); - CustomGui.DrawGuideButton(Glamourer.Messager, width); + DrawSupportButton(glamourer); ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); + CustomGui.DrawGuideButton(Glamourer.Messager, width); + + ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) changelog.ForceOpen = true; } + /// + /// Draw a button that copies the support info to clipboards. + /// + private static void DrawSupportButton(Glamourer glamourer) + { + if (!ImUtf8.Button(SupportInfoButtonText)) + return; + + var text = glamourer.GatherSupportInformation(); + ImGui.SetClipboardText(text); + Glamourer.Messager.NotificationMessage("Copied Support Info to Clipboard.", NotificationType.Success, false); + } + private void OnTabSelected(TabType type, Design? _) { SelectTab = type; diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs index 232541e..4499107 100644 --- a/Glamourer/Gui/Materials/AdvancedDyePopup.cs +++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs @@ -1,19 +1,23 @@ using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using Glamourer.Designs; using Glamourer.Interop.Material; using Glamourer.State; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; using Penumbra.String; +using Notification = OtterGui.Classes.Notification; namespace Glamourer.Gui.Materials; @@ -26,14 +30,16 @@ public sealed unsafe class AdvancedDyePopup( private MaterialValueIndex? _drawIndex; private ActorState _state = null!; private Actor _actor; + private ColorRow.Mode _mode; private byte _selectedMaterial = byte.MaxValue; private bool _anyChanged; + private bool _forceFocus; + + private const int RowsPerPage = 16; + private int _rowOffset; private bool ShouldBeDrawn() { - if (!config.UseAdvancedDyes) - return false; - if (_drawIndex is not { Valid: true }) return false; @@ -43,59 +49,69 @@ public sealed unsafe class AdvancedDyePopup( return true; } - public void DrawButton(EquipSlot slot) - => DrawButton(MaterialValueIndex.FromSlot(slot)); + public void DrawButton(EquipSlot slot, uint color) + => DrawButton(MaterialValueIndex.FromSlot(slot), color); - private void DrawButton(MaterialValueIndex index) + public void DrawButton(BonusItemFlag slot, uint color) + => DrawButton(MaterialValueIndex.FromSlot(slot), color); + + private void DrawButton(MaterialValueIndex index, uint color) { - if (!config.UseAdvancedDyes) + if (config.HideDesignPanel.HasFlag(DesignPanelFlag.AdvancedDyes)) return; ImGui.SameLine(); - using var id = ImRaii.PushId(index.SlotIndex | ((int)index.DrawObject << 8)); + using var id = ImUtf8.PushId(index.SlotIndex | ((int)index.DrawObject << 8)); var isOpen = index == _drawIndex; - using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), isOpen) - .Push(ImGuiCol.Text, ColorId.HeaderButtons.Value(), isOpen) - .Push(ImGuiCol.Border, ColorId.HeaderButtons.Value(), isOpen)) + var (textColor, buttonColor) = isOpen + ? (ColorId.HeaderButtons.Value(), ImGui.GetColorU32(ImGuiCol.ButtonActive)) + : (color, 0u); + + using (ImRaii.PushColor(ImGuiCol.Border, textColor, isOpen)) { using var frame = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, isOpen); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Palette.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - string.Empty, false, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Palette, ""u8, default, false, textColor, buttonColor)) { + _forceFocus = true; _selectedMaterial = byte.MaxValue; _drawIndex = isOpen ? null : index; } } - ImGuiUtil.HoverTooltip("Open advanced dyes for this slot."); + ImUtf8.HoverTooltip("Open advanced dyes for this slot."u8); } private (string Path, string GamePath) ResourceName(MaterialValueIndex index) { - var materialHandle = (MaterialResourceHandle*)_actor.Model.AsCharacterBase->MaterialsSpan[ - index.MaterialIndex + index.SlotIndex * MaterialService.MaterialsPerModel].Value; + var materialHandle = + (MaterialResourceHandle*)_actor.Model.AsCharacterBase->MaterialsSpan[ + index.MaterialIndex + index.SlotIndex * MaterialService.MaterialsPerModel].Value; var model = _actor.Model.AsCharacterBase->ModelsSpan[index.SlotIndex].Value; var modelHandle = model == null ? null : model->ModelResourceHandle; var path = materialHandle == null ? string.Empty - : ByteString.FromSpanUnsafe(materialHandle->ResourceHandle.FileName.AsSpan(), true).ToString(); + : ByteString.FromSpanUnsafe(materialHandle->FileName.AsSpan(), true).ToString(); var gamePath = modelHandle == null ? string.Empty - : modelHandle->GetMaterialFileNameBySlotAsString(index.MaterialIndex); + : modelHandle->GetMaterialFileNameBySlot(index.MaterialIndex).ToString(); return (path, gamePath); } - private void DrawTabBar(ReadOnlySpan> textures, ref bool firstAvailable) + private void DrawTabBar(ReadOnlySpan> textures, ReadOnlySpan> materials, ref bool firstAvailable) { - using var bar = ImRaii.TabBar("tabs"); + using var bar = ImUtf8.TabBar("tabs"u8); if (!bar) return; + var table = new ColorTable.Table(); + var highLightColor = ColorId.AdvancedDyeActive.Value(); for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) { - var index = _drawIndex!.Value with { MaterialIndex = i }; - var available = index.TryGetTexture(textures, out var texture) && directX.TryGetColorTable(*texture, out var table); + var index = _drawIndex!.Value with { MaterialIndex = i }; + var available = index.TryGetTexture(textures, materials, out var texture, out _mode) + && directX.TryGetColorTable(*texture, out table); + if (index == preview.LastValueIndex with { RowIndex = 0 }) table = preview.LastOriginalColorTable; @@ -108,50 +124,78 @@ public sealed unsafe class AdvancedDyePopup( if (available) firstAvailable = false; - using var tab = _label.TabItem(i, select); + var hasAdvancedDyes = _state.Materials.CheckExistenceMaterial(index); + using var c = ImRaii.PushColor(ImGuiCol.Text, highLightColor, hasAdvancedDyes); + using var tab = _label.TabItem(i, select); + c.Pop(); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { using var enabled = ImRaii.Enabled(); var (path, gamePath) = ResourceName(index); + using var tt = ImUtf8.Tooltip(); + if (gamePath.Length == 0 || path.Length == 0) - ImGui.SetTooltip("This material does not exist."); + ImUtf8.Text("This material does not exist."u8); else if (!available) - ImGui.SetTooltip($"This material does not have an associated color set.\n\n{gamePath}\n{path}"); + ImUtf8.Text($"This material does not have an associated color set.\n\n{gamePath}\n{path}"); else - ImGui.SetTooltip($"{gamePath}\n{path}"); + ImUtf8.Text($"{gamePath}\n{path}"); + + if (hasAdvancedDyes && !available) + { + ImUtf8.Text("\nRight-Click to remove ineffective advanced dyes."u8); + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + for (byte row = 0; row < ColorTable.NumRows; ++row) + stateManager.ResetMaterialValue(_state, index with { RowIndex = row }, ApplySettings.Game); + } } if ((tab.Success || select is ImGuiTabItemFlags.SetSelected) && available) { _selectedMaterial = i; + DrawToggle(); DrawTable(index, table); } } + } - using (ImRaii.PushFont(UiBuilder.IconFont)) + private void DrawToggle() + { + var buttonWidth = new Vector2(ImGui.GetContentRegionAvail().X / 2, 0); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.TabHovered)); + + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == 0 ? ImGuiCol.TabActive : ImGuiCol.Tab))) { - if (ImGui.TabItemButton($"{FontAwesomeIcon.Times.ToIconString()} ", ImGuiTabItemFlags.NoTooltip)) - _drawIndex = null; + if (ToggleButton.ButtonEx("Row Pairs 1-8 ", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersLeft)) + _rowOffset = 0; } - ImGuiUtil.HoverTooltip("Close the advanced dye window."); + ImGui.SameLine(0, 0); + + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == RowsPerPage ? ImGuiCol.TabActive : ImGuiCol.Tab))) + { + if (ToggleButton.ButtonEx("Row Pairs 9-16", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersRight)) + _rowOffset = RowsPerPage; + } } - private void DrawContent(ReadOnlySpan> textures) + private void DrawContent(ReadOnlySpan> textures, ReadOnlySpan> materials) { var firstAvailable = true; - DrawTabBar(textures, ref firstAvailable); + DrawTabBar(textures, materials, ref firstAvailable); if (firstAvailable) - ImGui.TextUnformatted("No Editable Materials available."); + ImUtf8.Text("No Editable Materials available."u8); } - private void DrawWindow(ReadOnlySpan> textures) + private void DrawWindow(ReadOnlySpan> textures, ReadOnlySpan> materials) { var flags = ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoResize; + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoDocking; // Set position to the right of the main window when attached // The downwards offset is implicit through child position. @@ -163,15 +207,26 @@ public sealed unsafe class AdvancedDyePopup( flags |= ImGuiWindowFlags.NoMove; } - var size = new Vector2(7 * ImGui.GetFrameHeight() + 3 * ImGui.GetStyle().ItemInnerSpacing.X + 300 * ImGuiHelpers.GlobalScale, - 18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 2 * ImGui.GetStyle().ItemSpacing.Y); - ImGui.SetNextWindowSize(size); + var width = 7 * ImGui.GetFrameHeight() // Buttons + + 3 * ImGui.GetStyle().ItemSpacing.X // around text + + 7 * ImGui.GetStyle().ItemInnerSpacing.X + + 200 * ImGuiHelpers.GlobalScale // Drags + + 7 * UiBuilder.MonoFont.GetCharAdvance(' ') * ImGuiHelpers.GlobalScale // Row + + 2 * ImGui.GetStyle().WindowPadding.X; + var height = 19 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 3 * ImGui.GetStyle().ItemSpacing.Y; + ImGui.SetNextWindowSize(new Vector2(width, height)); var window = ImGui.Begin("###Glamourer Advanced Dyes", flags); + if (ImGui.IsWindowAppearing() || _forceFocus) + { + ImGui.SetWindowFocus(); + _forceFocus = false; + } + try { if (window) - DrawContent(textures); + DrawContent(textures, materials); } finally { @@ -186,18 +241,22 @@ public sealed unsafe class AdvancedDyePopup( if (!ShouldBeDrawn()) return; - if (_drawIndex!.Value.TryGetTextures(actor, out var textures)) - DrawWindow(textures); + if (_drawIndex!.Value.TryGetTextures(actor, out var textures, out var materials)) + DrawWindow(textures, materials); } - private void DrawTable(MaterialValueIndex materialIndex, in LegacyColorTable table) + private void DrawTable(MaterialValueIndex materialIndex, ColorTable.Table table) { + if (!materialIndex.Valid) + return; + using var disabled = ImRaii.Disabled(_state.IsLocked); _anyChanged = false; - for (byte i = 0; i < LegacyColorTable.NumUsedRows; ++i) + for (byte i = 0; i < RowsPerPage; ++i) { - var index = materialIndex with { RowIndex = i }; - ref var row = ref table[i]; + var actualI = (byte)(i + _rowOffset); + var index = materialIndex with { RowIndex = actualI }; + ref var row = ref table[actualI]; DrawRow(ref row, index, table); } @@ -205,31 +264,89 @@ public sealed unsafe class AdvancedDyePopup( DrawAllRow(materialIndex, table); } - private void DrawAllRow(MaterialValueIndex materialIndex, in LegacyColorTable table) + private static void CopyToClipboard(in ColorTable.Table table) + { + try + { + fixed (ColorTable.Table* ptr = &table) + { + var data = new ReadOnlySpan(ptr, sizeof(ColorTable.Table)); + var base64 = Convert.ToBase64String(data); + ImGui.SetClipboardText(base64); + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not copy color table to clipboard:\n{ex}"); + } + } + + private static bool ImportFromClipboard(out ColorTable.Table table) + { + try + { + var base64 = ImGui.GetClipboardText(); + if (base64.Length > 0) + { + var data = Convert.FromBase64String(base64); + if (sizeof(ColorTable.Table) <= data.Length) + { + table = new ColorTable.Table(); + fixed (ColorTable.Table* tPtr = &table) + { + fixed (byte* ptr = data) + { + new ReadOnlySpan(ptr, sizeof(ColorTable.Table)).CopyTo(new Span(tPtr, sizeof(ColorTable.Table))); + return true; + } + } + } + } + + if (ColorRowClipboard.IsTableSet) + { + table = ColorRowClipboard.Table; + return true; + } + } + catch (Exception ex) + { + Glamourer.Messager.AddMessage(new Notification(ex, "Could not paste color table from clipboard.", + "Could not paste color table from clipboard.", NotificationType.Error)); + } + + table = default; + return false; + } + + private void DrawAllRow(MaterialValueIndex materialIndex, in ColorTable.Table table) { using var id = ImRaii.PushId(100); var buttonSize = new Vector2(ImGui.GetFrameHeight()); - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), buttonSize, "Highlight all affected colors on the character.", - false, true); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight all affected colors on the character."u8, buttonSize); if (ImGui.IsItemHovered()) preview.OnHover(materialIndex with { RowIndex = byte.MaxValue }, _actor.Index, table); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); using (ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.TextUnformatted("All Color Rows"); + ImUtf8.Text("All Color Row Pairs (1-16)"u8); } var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - ImGui.SameLine(ImGui.GetWindowSize().X - 3 * buttonSize.X - 3 * spacing); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), buttonSize, "Export this table to your clipboard.", false, - true)) + ImGui.SameLine(ImGui.GetWindowSize().X - 3 * buttonSize.X - 2 * spacing - ImGui.GetStyle().WindowPadding.X); + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this table to your clipboard."u8, buttonSize)) + { ColorRowClipboard.Table = table; + CopyToClipboard(table); + } + ImGui.SameLine(0, spacing); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), buttonSize, - "Import an exported table from your clipboard onto this table.", !ColorRowClipboard.IsTableSet, true)) - foreach (var (row, idx) in ColorRowClipboard.Table.WithIndex()) + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported table from your clipboard onto this table."u8, buttonSize) + && ImportFromClipboard(out var newTable)) + for (var idx = 0; idx < ColorTable.NumRows; ++idx) { + var row = newTable[idx]; var internalRow = new ColorRow(row); var slot = materialIndex.ToEquipSlot(); var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand @@ -240,23 +357,27 @@ public sealed unsafe class AdvancedDyePopup( } ImGui.SameLine(0, spacing); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this table to game state.", !_anyChanged, - true)) - for (byte i = 0; i < LegacyColorTable.NumUsedRows; ++i) + if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this table to game state."u8, buttonSize, !_anyChanged)) + for (byte i = 0; i < ColorTable.NumRows; ++i) stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = i }, ApplySettings.Game); } - private void DrawRow(ref LegacyColorTable.Row row, MaterialValueIndex index, in LegacyColorTable table) + private void DrawRow(ref ColorTableRow row, MaterialValueIndex index, in ColorTable.Table table) { - using var id = ImRaii.PushId(index.RowIndex); + using var id = ImUtf8.PushId(index.RowIndex); var changed = _state.Materials.TryGetValue(index, out var value); if (!changed) { var internalRow = new ColorRow(row); var slot = index.ToEquipSlot(); - var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand - ? _state.ModelData.Weapon(slot) - : _state.ModelData.Armor(slot).ToWeapon(0); + var weapon = slot switch + { + EquipSlot.MainHand => _state.ModelData.Weapon(EquipSlot.MainHand), + EquipSlot.OffHand => _state.ModelData.Weapon(EquipSlot.OffHand), + EquipSlot.Unknown => + _state.ModelData.BonusItem((index.SlotIndex - 16u).ToBonusSlot()).Armor().ToWeapon(0), // TODO: Handle better + _ => _state.ModelData.Armor(slot).ToWeapon(0), + }; value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual); } else @@ -266,8 +387,7 @@ public sealed unsafe class AdvancedDyePopup( } var buttonSize = new Vector2(ImGui.GetFrameHeight()); - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), buttonSize, "Highlight the affected colors on the character.", - false, true); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight the affected colors on the character."u8, buttonSize); if (ImGui.IsItemHovered()) preview.OnHover(index, _actor.Index, table); @@ -275,58 +395,107 @@ public sealed unsafe class AdvancedDyePopup( ImGui.AlignTextToFramePadding(); using (ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.TextUnformatted($"Row {index.RowIndex + 1:D2}"); + var rowIndex = index.RowIndex / 2 + 1; + var rowSuffix = (index.RowIndex & 1) == 0 ? 'A' : 'B'; + ImUtf8.Text($"Row {rowIndex,2}{rowSuffix}"); } ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X * 2); - var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", value.Model.Diffuse, - v => value.Model.Diffuse = v, "D"); + var applied = ImUtf8.ColorPicker("##diffuse"u8, "Change the diffuse value for this row."u8, value.Model.Diffuse, + v => value.Model.Diffuse = v, "D"u8); var spacing = ImGui.GetStyle().ItemInnerSpacing; ImGui.SameLine(0, spacing.X); - applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", value.Model.Specular, - v => value.Model.Specular = v, "S"); + applied |= ImUtf8.ColorPicker("##specular"u8, "Change the specular value for this row."u8, value.Model.Specular, + v => value.Model.Specular = v, "S"u8); + ImGui.SameLine(0, spacing.X); - applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", value.Model.Emissive, - v => value.Model.Emissive = v, "E"); + applied |= ImUtf8.ColorPicker("##emissive"u8, "Change the emissive value for this row."u8, value.Model.Emissive, + v => value.Model.Emissive = v, "E"u8); + ImGui.SameLine(0, spacing.X); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - applied |= ImGui.DragFloat("##Gloss", ref value.Model.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G") - && value.Model.GlossStrength > 0; - ImGuiUtil.HoverTooltip("Change the gloss strength for this row."); + if (_mode is not ColorRow.Mode.Dawntrail) + { + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + applied |= DragGloss(ref value.Model.GlossStrength); + ImUtf8.HoverTooltip("Change the gloss strength for this row."u8); + } + else + { + ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0)); + } + ImGui.SameLine(0, spacing.X); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - applied |= ImGui.DragFloat("##Specular Strength", ref value.Model.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS"); - ImGuiUtil.HoverTooltip("Change the specular strength for this row."); + if (_mode is not ColorRow.Mode.Dawntrail) + { + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + applied |= DragSpecularStrength(ref value.Model.SpecularStrength); + ImUtf8.HoverTooltip("Change the specular strength for this row."u8); + } + else + { + ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0)); + } + ImGui.SameLine(0, spacing.X); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), buttonSize, "Export this row to your clipboard.", false, - true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, buttonSize)) ColorRowClipboard.Row = value.Model; ImGui.SameLine(0, spacing.X); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), buttonSize, - "Import an exported row from your clipboard onto this row.", !ColorRowClipboard.IsSet, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, buttonSize, + !ColorRowClipboard.IsSet)) { value.Model = ColorRowClipboard.Row; applied = true; } ImGui.SameLine(0, spacing.X); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this row to game state.", !changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this row to game state."u8, buttonSize, !changed)) stateManager.ResetMaterialValue(_state, index, ApplySettings.Game); if (applied) stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual); } + public static bool DragGloss(ref float value) + { + var tmp = value; + var minValue = ImGui.GetIO().KeyCtrl ? 0f : (float)Half.Epsilon; + if (!ImUtf8.DragScalar("##Gloss"u8, ref tmp, "%.1f G"u8, 0.001f, minValue, Math.Max(0.01f, 0.005f * value), + ImGuiSliderFlags.AlwaysClamp)) + return false; + + var tmp2 = Math.Clamp(tmp, minValue, (float)Half.MaxValue); + if (tmp2 == value) + return false; + + value = tmp2; + return true; + } + + public static bool DragSpecularStrength(ref float value) + { + var tmp = value * 100f; + if (!ImUtf8.DragScalar("##SpecularStrength"u8, ref tmp, "%.0f%% SS"u8, 0f, (float)Half.MaxValue * 100f, 0.05f, + ImGuiSliderFlags.AlwaysClamp)) + return false; + + var tmp2 = Math.Clamp(tmp, 0f, (float)Half.MaxValue * 100f) / 100f; + if (tmp2 == value) + return false; + + value = tmp2; + return true; + } + private LabelStruct _label = new(); private struct LabelStruct { - private fixed byte _label[12]; + private fixed byte _label[5]; public ImRaii.IEndObject TabItem(byte materialIndex, ImGuiTabItemFlags flags) { - _label[10] = (byte)('1' + materialIndex); + _label[4] = (byte)('A' + materialIndex); fixed (byte* ptr = _label) { return ImRaii.TabItem(ptr, flags | ImGuiTabItemFlags.NoTooltip); @@ -335,17 +504,11 @@ public sealed unsafe class AdvancedDyePopup( public LabelStruct() { - _label[0] = (byte)'M'; - _label[1] = (byte)'a'; - _label[2] = (byte)'t'; - _label[3] = (byte)'e'; - _label[4] = (byte)'r'; - _label[5] = (byte)'i'; - _label[6] = (byte)'a'; - _label[7] = (byte)'l'; - _label[8] = (byte)' '; - _label[9] = (byte)'#'; - _label[11] = 0; + _label[0] = (byte)'M'; + _label[1] = (byte)'a'; + _label[2] = (byte)'t'; + _label[3] = (byte)' '; + _label[5] = 0; } } } diff --git a/Glamourer/Gui/Materials/ColorRowClipboard.cs b/Glamourer/Gui/Materials/ColorRowClipboard.cs index 4d99018..f7fac1d 100644 --- a/Glamourer/Gui/Materials/ColorRowClipboard.cs +++ b/Glamourer/Gui/Materials/ColorRowClipboard.cs @@ -5,14 +5,14 @@ namespace Glamourer.Gui.Materials; public static class ColorRowClipboard { - private static ColorRow _row; - private static LegacyColorTable _table; + private static ColorRow _row; + private static ColorTable.Table _table; public static bool IsSet { get; private set; } public static bool IsTableSet { get; private set; } - public static LegacyColorTable Table + public static ColorTable.Table Table { get => _table; set diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs index 26432e9..7c16372 100644 --- a/Glamourer/Gui/Materials/MaterialDrawer.cs +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -3,9 +3,10 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Glamourer.Designs; using Glamourer.Interop.Material; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Gui; @@ -17,7 +18,6 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) public const float GlossWidth = 100; public const float SpecularStrengthWidth = 125; - private EquipSlot _newSlot = EquipSlot.Head; private int _newMaterialIdx; private int _newRowIdx; private MaterialValueIndex _newKey = MaterialValueIndex.FromSlot(EquipSlot.Head); @@ -33,7 +33,11 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) var colorWidth = 4 * _buttonSize.X + (GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + 6 * _spacing - + ImGui.CalcTextSize("Revert").X; + + ImUtf8.CalcTextSize("Revert"u8).X; + DrawMultiButtons(design); + ImUtf8.Dummy(0); + ImGui.Separator(); + ImUtf8.Dummy(0); if (available > 1.95 * colorWidth) DrawSingleRow(design); else @@ -41,11 +45,44 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) DrawNew(design); } + private void DrawMultiButtons(Design design) + { + var any = design.Materials.Count > 0; + var disabled = !_config.DeleteDesignModifier.IsActive(); + var size = new Vector2(200 * ImUtf8.GlobalScale, 0); + if (ImUtf8.ButtonEx("Enable All Advanced Dyes"u8, + any + ? "Enable the application of all contained advanced dyes without deleting them."u8 + : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + _designManager.ChangeApplyMulti(design, null, null, null, null, null, null, true, null); + ; + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to enable."); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable All Advanced Dyes"u8, + any + ? "Disable the application of all contained advanced dyes without deleting them."u8 + : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + _designManager.ChangeApplyMulti(design, null, null, null, null, null, null, false, null); + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to disable."); + + if (ImUtf8.ButtonEx("Delete All Advanced Dyes"u8, any ? ""u8 : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + while (design.Materials.Count > 0) + _designManager.ChangeMaterialValue(design, MaterialValueIndex.FromKey(design.Materials[0].Item1), null); + + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to delete."); + } + private void DrawName(MaterialValueIndex index) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale).Push(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.Text)); - ImGuiUtil.DrawTextButton(index.ToString(), new Vector2((GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + _spacing, 0), 0); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed(index.ToString(), 0, new Vector2((GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + _spacing, 0), + borderColor: ImGui.GetColorU32(ImGuiCol.Text)); } private void DrawSingleRow(Design design) @@ -54,16 +91,17 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) { using var id = ImRaii.PushId(i); var (idx, value) = design.Materials[i]; - var key = MaterialValueIndex.FromKey(idx); + var key = MaterialValueIndex.FromKey(idx); DrawName(key); ImGui.SameLine(0, _spacing); - DeleteButton(design, key, ref i); + DeleteButton(design, key, ref i); ImGui.SameLine(0, _spacing); CopyButton(value.Value); ImGui.SameLine(0, _spacing); PasteButton(design, key); ImGui.SameLine(0, _spacing); + using var disabled = ImRaii.Disabled(design.WriteProtected()); EnabledToggle(design, key, value.Enabled); ImGui.SameLine(0, _spacing); DrawRow(design, key, value.Value, value.Revert); @@ -89,7 +127,7 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) PasteButton(design, key); ImGui.SameLine(0, _spacing); EnabledToggle(design, key, value.Enabled); - + DrawRow(design, key, value.Value, value.Revert); ImGui.SameLine(0, _spacing); @@ -101,9 +139,9 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) private void DeleteButton(Design design, MaterialValueIndex index, ref int idx) { var deleteEnabled = _config.DeleteDesignModifier.IsActive(); - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, - $"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}", - !deleteEnabled, true)) + if (!ImUtf8.IconButton(FontAwesomeIcon.Trash, + $"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}", disabled: + !deleteEnabled || design.WriteProtected())) return; _designManager.ChangeMaterialValue(design, index, null); @@ -112,76 +150,106 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) private void CopyButton(in ColorRow row) { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), _buttonSize, "Export this row to your clipboard.", - false, - true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8)) ColorRowClipboard.Row = row; } private void PasteButton(Design design, MaterialValueIndex index) { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), _buttonSize, - "Import an exported row from your clipboard onto this row.", !ColorRowClipboard.IsSet, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + disabled: !ColorRowClipboard.IsSet || design.WriteProtected())) _designManager.ChangeMaterialValue(design, index, ColorRowClipboard.Row); } private void EnabledToggle(Design design, MaterialValueIndex index, bool enabled) { - if (ImGui.Checkbox("Enabled", ref enabled)) + if (ImUtf8.Checkbox("Enabled"u8, ref enabled)) _designManager.ChangeApplyMaterialValue(design, index, enabled); } private void RevertToggle(Design design, MaterialValueIndex index, bool revert) { - if (ImGui.Checkbox("Revert", ref revert)) + if (ImUtf8.Checkbox("Revert"u8, ref revert)) _designManager.ChangeMaterialRevert(design, index, revert); - ImGuiUtil.HoverTooltip( - "If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row."); + ImUtf8.HoverTooltip( + "If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row."u8); + } + + public sealed class MaterialSlotCombo; + + private void DrawSlotCombo() + { + var width = ImUtf8.CalcTextSize(EquipSlot.OffHand.ToName()).X + ImGui.GetFrameHeightWithSpacing(); + ImGui.SetNextItemWidth(width); + using var combo = ImUtf8.Combo("##slot"u8, _newKey.SlotName()); + if (combo) + { + var currentSlot = _newKey.ToEquipSlot(); + foreach (var tmpSlot in EquipSlotExtensions.FullSlots) + { + if (ImUtf8.Selectable(tmpSlot.ToName(), tmpSlot == currentSlot) && currentSlot != tmpSlot) + _newKey = MaterialValueIndex.FromSlot(tmpSlot) with + { + MaterialIndex = (byte)_newMaterialIdx, + RowIndex = (byte)_newRowIdx, + }; + } + + var currentBonus = _newKey.ToBonusSlot(); + foreach (var bonusSlot in BonusExtensions.AllFlags) + { + if (ImUtf8.Selectable(bonusSlot.ToName(), bonusSlot == currentBonus) && bonusSlot != currentBonus) + _newKey = MaterialValueIndex.FromSlot(bonusSlot) with + { + MaterialIndex = (byte)_newMaterialIdx, + RowIndex = (byte)_newRowIdx, + }; + } + } + + ImUtf8.HoverTooltip("Choose a slot for an advanced dye row."u8); } public void DrawNew(Design design) { - if (EquipSlotCombo.Draw("##slot", "Choose a slot for an advanced dye row.", ref _newSlot)) - _newKey = MaterialValueIndex.FromSlot(_newSlot) with - { - MaterialIndex = (byte)_newMaterialIdx, - RowIndex = (byte)_newRowIdx, - }; - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + DrawSlotCombo(); + ImUtf8.SameLineInner(); DrawMaterialIdxDrag(); - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImUtf8.SameLineInner(); DrawRowIdxDrag(); - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImUtf8.SameLineInner(); var exists = design.GetMaterialDataRef().TryGetValue(_newKey, out _); - if (ImGuiUtil.DrawDisabledButton("Add New Row", Vector2.Zero, - exists ? "The selected advanced dye row already exists." : "Add the selected advanced dye row.", exists, false)) + if (ImUtf8.ButtonEx("Add New Row"u8, + exists ? "The selected advanced dye row already exists."u8 : "Add the selected advanced dye row."u8, Vector2.Zero, + exists || design.WriteProtected())) _designManager.ChangeMaterialValue(design, _newKey, ColorRow.Empty); } private void DrawMaterialIdxDrag() { - _newMaterialIdx += 1; - ImGui.SetNextItemWidth(ImGui.CalcTextSize("Material #000").X); - if (ImGui.DragInt("##Material", ref _newMaterialIdx, 0.01f, 1, MaterialService.MaterialsPerModel, "Material #%i")) + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Material AA"u8).X); + var format = $"Material {(char)('A' + _newMaterialIdx)}"; + if (ImUtf8.DragScalar("##Material"u8, ref _newMaterialIdx, format, 0, MaterialService.MaterialsPerModel - 1, 0.01f, + ImGuiSliderFlags.NoInput)) { - _newMaterialIdx = Math.Clamp(_newMaterialIdx, 1, MaterialService.MaterialsPerModel); - _newKey = _newKey with { MaterialIndex = (byte)(_newMaterialIdx - 1) }; + _newMaterialIdx = Math.Clamp(_newMaterialIdx, 0, MaterialService.MaterialsPerModel - 1); + _newKey = _newKey with { MaterialIndex = (byte)_newMaterialIdx }; } - _newMaterialIdx -= 1; + ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8); } private void DrawRowIdxDrag() { - _newRowIdx += 1; - ImGui.SetNextItemWidth(ImGui.CalcTextSize("Row #0000").X); - if (ImGui.DragInt("##Row", ref _newRowIdx, 0.01f, 1, LegacyColorTable.NumUsedRows, "Row #%i")) + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Row 0000"u8).X); + var format = $"Row {_newRowIdx / 2 + 1}{(char)(_newRowIdx % 2 + 'A')}"; + if (ImUtf8.DragScalar("##Row"u8, ref _newRowIdx, format, 0, ColorTable.NumRows - 1, 0.01f, ImGuiSliderFlags.NoInput)) { - _newRowIdx = Math.Clamp(_newRowIdx, 1, LegacyColorTable.NumUsedRows); - _newKey = _newKey with { RowIndex = (byte)(_newRowIdx - 1) }; + _newRowIdx = Math.Clamp(_newRowIdx, 0, ColorTable.NumRows - 1); + _newKey = _newKey with { RowIndex = (byte)_newRowIdx }; } - _newRowIdx -= 1; + ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8); } private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row, bool disabled) @@ -189,18 +257,18 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config) var tmp = row; using var _ = ImRaii.Disabled(disabled); var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", row.Diffuse, v => tmp.Diffuse = v, "D"); - ImGui.SameLine(0, _spacing); + ImUtf8.SameLineInner(); applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", row.Specular, v => tmp.Specular = v, "S"); - ImGui.SameLine(0, _spacing); + ImUtf8.SameLineInner(); applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", row.Emissive, v => tmp.Emissive = v, "E"); - ImGui.SameLine(0, _spacing); + ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(GlossWidth * ImGuiHelpers.GlobalScale); - applied |= ImGui.DragFloat("##Gloss", ref tmp.GlossStrength, 0.01f, 0.001f, float.MaxValue, "%.3f G"); - ImGuiUtil.HoverTooltip("Change the gloss strength for this row."); - ImGui.SameLine(0, _spacing); + applied |= AdvancedDyePopup.DragGloss(ref tmp.GlossStrength); + ImUtf8.HoverTooltip("Change the gloss strength for this row."u8); + ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(SpecularStrengthWidth * ImGuiHelpers.GlobalScale); - applied |= ImGui.DragFloat("##Specular Strength", ref tmp.SpecularStrength, 0.01f, float.MinValue, float.MaxValue, "%.3f SS"); - ImGuiUtil.HoverTooltip("Change the specular strength for this row."); + applied |= AdvancedDyePopup.DragSpecularStrength(ref tmp.SpecularStrength); + ImUtf8.HoverTooltip("Change the specular strength for this row."u8); if (applied) _designManager.ChangeMaterialValue(design, index, tmp); } diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index 6e4774b..dff9a6e 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -1,37 +1,40 @@ -using Dalamud; using Glamourer.Designs; using Glamourer.Events; -using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Gui; public sealed class PenumbraChangedItemTooltip : IDisposable { - private readonly PenumbraService _penumbra; - private readonly StateManager _stateManager; - private readonly ItemManager _items; - private readonly ObjectManager _objects; - private readonly CustomizeService _customize; - private readonly GPoseService _gpose; + private readonly PenumbraService _penumbra; + private readonly StateManager _stateManager; + private readonly ItemManager _items; + private readonly ActorObjectManager _objects; + private readonly CustomizeService _customize; + private readonly GPoseService _gpose; - private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2]; + private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2 + BonusExtensions.AllFlags.Count]; - public IEnumerable> LastItems - => EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems) - .Select(p => new KeyValuePair(p.First, p.Second)); + public IEnumerable> LastItems + => EquipSlotExtensions.EqdpSlots.Cast().Append(EquipSlot.MainHand).Append(EquipSlot.OffHand) + .Concat(BonusExtensions.AllFlags.Cast()).Zip(_lastItems) + .Select(p => new KeyValuePair(p.First, p.Second)); - public DateTime LastTooltip { get; private set; } = DateTime.MinValue; - public DateTime LastClick { get; private set; } = DateTime.MinValue; + public ChangedItemType LastType { get; private set; } = ChangedItemType.None; + public uint LastId { get; private set; } + public DateTime LastTooltip { get; private set; } = DateTime.MinValue; + public DateTime LastClick { get; private set; } = DateTime.MinValue; - public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ObjectManager objects, + public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ActorObjectManager objects, CustomizeService customize, GPoseService gpose) { _penumbra = penumbra; @@ -70,6 +73,21 @@ public sealed class PenumbraChangedItemTooltip : IDisposable if (!Player()) return; + var bonusSlot = item.Type.ToBonus(); + if (bonusSlot is not BonusItemFlag.Unknown) + { + // + 2 due to weapons. + var glasses = _lastItems[bonusSlot.ToSlot() + 2]; + using (_ = !openTooltip ? null : ImRaii.Tooltip()) + { + ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor."); + if (glasses.Valid) + ImGui.TextUnformatted($"{prefix}Control + Right-Click to re-apply {glasses.Name} to current actor."); + } + + return; + } + var slot = item.Type.ToSlot(); var last = _lastItems[slot.ToIndex()]; switch (slot) @@ -107,6 +125,27 @@ public sealed class PenumbraChangedItemTooltip : IDisposable public void ApplyItem(ActorState state, EquipItem item) { + var bonusSlot = item.Type.ToBonus(); + if (bonusSlot is not BonusItemFlag.Unknown) + { + // + 2 due to weapons. + var glasses = _lastItems[bonusSlot.ToSlot() + 2]; + if (ImGui.GetIO().KeyCtrl && glasses.Valid) + { + Glamourer.Log.Debug($"Re-Applying {glasses.Name} to {bonusSlot.ToName()}."); + SetLastItem(bonusSlot, default, state); + _stateManager.ChangeBonusItem(state, bonusSlot, glasses, ApplySettings.Manual); + } + else + { + Glamourer.Log.Debug($"Applying {item.Name} to {bonusSlot.ToName()}."); + SetLastItem(bonusSlot, item, state); + _stateManager.ChangeBonusItem(state, bonusSlot, item, ApplySettings.Manual); + } + + return; + } + var slot = item.Type.ToSlot(); var last = _lastItems[slot.ToIndex()]; switch (slot) @@ -160,6 +199,8 @@ public sealed class PenumbraChangedItemTooltip : IDisposable private void OnPenumbraTooltip(ChangedItemType type, uint id) { + LastType = type; + LastId = id; LastTooltip = DateTime.UtcNow; if (!Player()) return; @@ -168,13 +209,24 @@ public sealed class PenumbraChangedItemTooltip : IDisposable { case ChangedItemType.ItemOffhand: case ChangedItemType.Item: + { if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; CreateTooltip(item, "[Glamourer] ", false); return; + } + case ChangedItemType.CustomArmor: + { + var (model, variant, slot) = IdentifiedItem.Split(id); + var item = _items.Identify(slot.ToSlot(), model, variant); + if (item.Valid) + CreateTooltip(item, "[Glamourer] ", false); + return; + } case ChangedItemType.Customization: - var (race, gender, index, value) = ChangedItemExtensions.Split(id); + { + var (race, gender, index, value) = IdentifiedCustomization.Split(id); if (!_objects.Player.Model.IsHuman) return; @@ -183,6 +235,7 @@ public sealed class PenumbraChangedItemTooltip : IDisposable ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor."); return; + } } } @@ -192,7 +245,7 @@ public sealed class PenumbraChangedItemTooltip : IDisposable return true; var main = _objects.Player.GetMainhand(); - var mainItem = _items.Identify(slot, main.Skeleton, main.Weapon, main.Variant); + var mainItem = _items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); if (slot == EquipSlot.MainHand) return item.Type == mainItem.Type; @@ -212,17 +265,29 @@ public sealed class PenumbraChangedItemTooltip : IDisposable { case ChangedItemType.Item: case ChangedItemType.ItemOffhand: + { if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; ApplyItem(state, item); return; + } + case ChangedItemType.CustomArmor: + { + var (model, variant, slot) = IdentifiedItem.Split(id); + var item = _items.Identify(slot.ToSlot(), model, variant); + if (item.Valid) + ApplyItem(state, item); + return; + } case ChangedItemType.Customization: - var (race, gender, index, value) = ChangedItemExtensions.Split(id); + { + var (race, gender, index, value) = IdentifiedCustomization.Split(id); var customize = state.ModelData.Customize; if (CheckGenderRace(customize, race, gender) && VerifyValue(customize, index, value)) _stateManager.ChangeCustomize(state, index, value, ApplySettings.Manual); return; + } } } @@ -236,8 +301,23 @@ public sealed class PenumbraChangedItemTooltip : IDisposable else { var oldItem = state.ModelData.Item(slot); - if (oldItem.ItemId != item.ItemId) - _lastItems[slot.ToIndex()] = oldItem; + if (oldItem.Id != item.Id) + last = oldItem; + } + } + + private void SetLastItem(BonusItemFlag slot, EquipItem item, ActorState state) + { + ref var last = ref _lastItems[slot.ToSlot() + 2]; + if (!item.Valid) + { + last = default; + } + else + { + var oldItem = state.ModelData.BonusItem(slot); + if (oldItem.Id != item.Id) + last = oldItem; } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 8febcd9..224154b 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -1,25 +1,26 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Gui.Materials; using Glamourer.Interop; -using Glamourer.Interop.Structs; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.Gui.Tabs.ActorTab; @@ -32,15 +33,16 @@ public class ActorPanel private readonly AutoDesignApplier _autoDesignApplier; private readonly Configuration _config; private readonly DesignConverter _converter; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; private readonly DesignManager _designManager; private readonly ImportService _importService; private readonly ICondition _conditions; private readonly DictModelChara _modelChara; private readonly CustomizeParameterDrawer _parameterDrawer; private readonly AdvancedDyePopup _advancedDyes; - private readonly HeaderDrawer.Button[] _leftButtons; - private readonly HeaderDrawer.Button[] _rightButtons; + private readonly EditorHistory _editorHistory; + private readonly HeaderDrawer.Button[] _leftButtons; + private readonly HeaderDrawer.Button[] _rightButtons; public ActorPanel(ActorSelector selector, StateManager stateManager, @@ -49,13 +51,14 @@ public class ActorPanel AutoDesignApplier autoDesignApplier, Configuration config, DesignConverter converter, - ObjectManager objects, + ActorObjectManager objects, DesignManager designManager, ImportService importService, ICondition conditions, DictModelChara modelChara, CustomizeParameterDrawer parameterDrawer, - AdvancedDyePopup advancedDyes) + AdvancedDyePopup advancedDyes, + EditorHistory editorHistory) { _selector = selector; _stateManager = stateManager; @@ -71,16 +74,18 @@ public class ActorPanel _modelChara = modelChara; _parameterDrawer = parameterDrawer; _advancedDyes = advancedDyes; + _editorHistory = editorHistory; _leftButtons = [ new SetFromClipboardButton(this), new ExportToClipboardButton(this), new SaveAsDesignButton(this), + new UndoButton(this), ]; _rightButtons = [ new LockedButton(this), - new HeaderDrawer.IncognitoButton(_config.Ephemeral), + new HeaderDrawer.IncognitoButton(_config), ]; } @@ -99,7 +104,7 @@ public class ActorPanel { using var group = ImRaii.Group(); (_identifier, _data) = _selector.Selection; - _lockedRedraw = _identifier.Type is IdentifierType.Special + _lockedRedraw = _identifier.Type is IdentifierType.Special || _objects.IsInLobby || _conditions[ConditionFlag.OccupiedInCutSceneEvent]; (_actorName, _actor) = GetHeaderName(); DrawHeader(); @@ -147,10 +152,13 @@ public class ActorPanel private unsafe void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) return; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + ImGui.Dummy(Vector2.Zero); var transformationId = _actor.IsCharacter ? _actor.AsCharacter->CharacterData.TransformationId : 0; if (transformationId != 0) ImGuiUtil.DrawTextButton($"Currently transformed to Transformation {transformationId}.", @@ -161,13 +169,15 @@ public class ActorPanel DrawApplyToTarget(); RevertButtons(); + ImGui.TableNextColumn(); using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); else DrawMonsterPanel(); - _advancedDyes.Draw(_data.Objects.Last(), _state); + if (_data.Objects.Count > 0) + _advancedDyes.Draw(_data.Objects.Last(), _state); } private void DrawHumanPanel() @@ -175,14 +185,19 @@ public class ActorPanel DrawCustomizationsHeader(); DrawEquipmentHeader(); DrawParameterHeader(); + DrawDebugData(); } private void DrawCustomizationsHeader() { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + var header = _state!.ModelData.ModelId == 0 ? "Customization" : $"Customization (Model Id #{_state.ModelData.ModelId})###Customization"; - using var h = ImRaii.CollapsingHeader(header); + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); if (!h) return; @@ -195,7 +210,7 @@ public class ActorPanel private void DrawEquipmentHeader() { - using var h = ImRaii.CollapsingHeader("Equipment"); + using var h = DesignPanelFlag.Equipment.Header(_config); if (!h) return; @@ -207,30 +222,68 @@ public class ActorPanel var data = EquipDrawData.FromState(_stateManager, _state!, slot); _equipmentDrawer.DrawEquip(data); if (usedAllStain) - _stateManager.ChangeStain(_state, slot, newAllStain, ApplySettings.Manual); + _stateManager.ChangeStains(_state, slot, newAllStain, ApplySettings.Manual); } var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand); var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand); _equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose()); + foreach (var slot in BonusExtensions.AllFlags) + { + var data = BonusDrawData.FromState(_stateManager, _state!, slot); + _equipmentDrawer.DrawBonusItem(data); + } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawEquipmentMetaToggles(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + _equipmentDrawer.DrawDragDropTooltip(); } private void DrawParameterHeader() { - if (!_config.UseAdvancedParameters) - return; - - using var h = ImRaii.CollapsingHeader("Advanced Customizations"); + using var h = DesignPanelFlag.AdvancedCustomizations.Header(_config); if (!h) return; _parameterDrawer.Draw(_stateManager, _state!); } + private unsafe void DrawDebugData() + { + if (!_config.DebugMode) + return; + + using var h = DesignPanelFlag.DebugData.Header(_config); + if (!h) + return; + + using var t = ImUtf8.Table("table"u8, 2, ImGuiTableFlags.SizingFixedFit); + if (!t) + return; + + ImUtf8.DrawTableColumn("Object Index"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->ObjectIndex))}"); + ImUtf8.DrawTableColumn("Name ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetNameId()))}"); + ImUtf8.DrawTableColumn("Base ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->BaseId))}"); + ImUtf8.DrawTableColumn("Entity ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->EntityId))}"); + ImUtf8.DrawTableColumn("Owner ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->OwnerId))}"); + ImUtf8.DrawTableColumn("Game Object ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetGameObjectId().ObjectId))}"); + + static void DrawCopyColumn(ref Utf8StringHandler text) + { + ImUtf8.DrawTableColumn(ref text); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.SetClipboardText(TextStringHandlerBuffer.Span); + } + } + private void DrawEquipmentMetaToggles() { using (_ = ImRaii.Group()) @@ -252,6 +305,12 @@ public class ActorPanel EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.WeaponState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!)); } + + ImGui.SameLine(); + using (_ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.EarState, _stateManager, _state!)); + } } private void DrawMonsterPanel() @@ -333,7 +392,7 @@ public class ActorPanel { if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.", _state!.IsLocked)) - _stateManager.ResetState(_state!, StateSource.Manual); + _stateManager.ResetState(_state!, StateSource.Manual, isFinal: true); ImGui.SameLine(); @@ -341,8 +400,8 @@ public class ActorPanel "Reapply the current automation state for the character on top of its current state..", !_config.EnableAutoDesigns || _state!.IsLocked)) { - _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, out var forcedRedraw); - _stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); + _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, false, StateSource.Manual); } ImGui.SameLine(); @@ -350,15 +409,15 @@ public class ActorPanel "Try to revert the character to the state it would have using automated designs.", !_config.EnableAutoDesigns || _state!.IsLocked)) { - _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, out var forcedRedraw); - _stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); + _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, true, StateSource.Manual); } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero, "Try to reapply the configured state if something went wrong. Should generally not be necessary.", _state!.IsLocked)) - _stateManager.ReapplyState(_actor, false, StateSource.Manual); + _stateManager.ReapplyState(_actor, false, StateSource.Manual, true); } private void DrawApplyToSelf() @@ -371,7 +430,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual); + ApplySettings.Manual with { IsFinal = true }); } private void DrawApplyToTarget() @@ -388,7 +447,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual); + ApplySettings.Manual with { IsFinal = true }); } @@ -415,7 +474,7 @@ public class ActorPanel var text = ImGui.GetClipboardText(); var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _) ?? throw new Exception("The clipboard did not contain valid data."); - panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks); + panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } catch (Exception ex) { @@ -470,6 +529,24 @@ public class ActorPanel } } + private sealed class UndoButton(ActorPanel panel) : HeaderDrawer.Button + { + protected override string Description + => "Undo the last change."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Undo; + + public override bool Visible + => panel._state != null; + + protected override bool Disabled + => (panel._state?.IsLocked ?? true) || !panel._editorHistory.CanUndo(panel._state); + + protected override void OnClick() + => panel._editorHistory.Undo(panel._state!); + } + private sealed class LockedButton(ActorPanel panel) : HeaderDrawer.Button { protected override string Description diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs index e688bce..7d132a1 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs @@ -1,15 +1,17 @@ using Dalamud.Interface; -using Glamourer.Interop; -using Glamourer.Interop.Structs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.ActorTab; -public class ActorSelector(ObjectManager objects, ActorManager actors, EphemeralConfig config) +public class ActorSelector(ActorObjectManager objects, ActorManager actors, EphemeralConfig config) { private ActorIdentifier _identifier = ActorIdentifier.Invalid; @@ -25,6 +27,7 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral private LowerString _actorFilter = LowerString.Empty; private Vector2 _defaultItemSpacing; + private WorldId _world; private float _width; public (ActorIdentifier Identifier, ActorData Data) Selection @@ -36,12 +39,43 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral public void Draw(float width) { _width = width; - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); _defaultItemSpacing = ImGui.GetStyle().ItemSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FrameRounding, 0); ImGui.SetNextItemWidth(_width); LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text("Filter for names containing the input."u8); + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight() / 2)); + ImUtf8.Text("Special filters are:"u8); + var color = ColorId.HeaderButtons.Value(); + ImUtf8.Text("

"u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only player characters."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only owned game objects."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only NPCs."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only retainers."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only special screen characters."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only players from your world."u8); + } DrawSelector(); DrawSelectionButtons(); @@ -49,24 +83,35 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral private void DrawSelector() { - using var child = ImRaii.Child("##Selector", new Vector2(_width, -ImGui.GetFrameHeight()), true); + using var child = ImUtf8.Child("##Selector"u8, new Vector2(_width, -ImGui.GetFrameHeight()), true); if (!child) return; - objects.Update(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); - var remainder = ImGuiClip.FilteredClippedDraw(objects.Identifiers, skips, CheckFilter, DrawSelectable); + _world = new WorldId(objects.Player.Valid ? objects.Player.HomeWorld : (ushort)0); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); + var remainder = ImGuiClip.FilteredClippedDraw(objects.Where(p => p.Value.Objects.Any(a => a.Model)), skips, CheckFilter, + DrawSelectable); ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); } private bool CheckFilter(KeyValuePair pair) - => _actorFilter.IsEmpty || pair.Value.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase); + => _actorFilter.Lower switch + { + "" => true, + "

" => pair.Key.Type is IdentifierType.Player, + "" => pair.Key.Type is IdentifierType.Owned, + "" => pair.Key.Type is IdentifierType.Npc, + "" => pair.Key.Type is IdentifierType.Retainer, + "" => pair.Key.Type is IdentifierType.Special, + "" => pair.Key.Type is IdentifierType.Player && pair.Key.HomeWorld == _world, + _ => _actorFilter.IsContained(pair.Value.Label), + }; private void DrawSelectable(KeyValuePair pair) { var equals = pair.Key.Equals(_identifier); - if (ImGui.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals) + if (ImUtf8.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals) _identifier = pair.Key.CreatePermanent(); } @@ -76,15 +121,14 @@ public class ActorSelector(ObjectManager objects, ActorManager actors, Ephemeral .Push(ImGuiStyleVar.FrameRounding, 0); var buttonWidth = new Vector2(_width / 2, 0); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth - , "Select the local player character.", !objects.Player, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.UserCircle, "Select the local player character."u8, buttonWidth, !objects.Player)) _identifier = objects.Player.GetIdentifier(actors); ImGui.SameLine(); var (id, data) = objects.TargetData; var tt = data.Valid ? $"Select the current target {id} in the list." : id.IsValid ? $"The target {id} is not in the list." : "No target selected."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, tt, objects.IsInGPose || !data.Valid, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.HandPointer, tt, buttonWidth, objects.IsInGPose || !data.Valid)) _identifier = id; } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs index 4e5e15c..9751a71 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.ActorTab; diff --git a/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs b/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs index 831ee7c..da3b636 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.AutomationTab; diff --git a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs index 530e04a..1d3e711 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs @@ -1,11 +1,12 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; -using OtterGui.Custom; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; +using OtterGui.Custom; namespace Glamourer.Gui.Tabs.AutomationTab; diff --git a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs index 8498bc1..ba2e424 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Gui; @@ -20,6 +20,7 @@ public class IdentifierDrawer public ActorIdentifier PlayerIdentifier { get; private set; } = ActorIdentifier.Invalid; public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid; public ActorIdentifier MannequinIdentifier { get; private set; } = ActorIdentifier.Invalid; + public ActorIdentifier OwnedIdentifier { get; private set; } = ActorIdentifier.Invalid; public IdentifierDrawer(ActorManager actors, DictWorld dictWorld, DictModelChara dictModelChara, DictBNpcNames bNpcNames, DictBNpc bNpc, HumanModelList humans) @@ -60,6 +61,9 @@ public class IdentifierDrawer public bool CanSetNpc => NpcIdentifier.IsValid; + public bool CanSetOwned + => OwnedIdentifier.IsValid; + private void UpdateIdentifiers() { if (ByteString.FromString(_characterName, out var byteName)) @@ -67,6 +71,11 @@ public class IdentifierDrawer PlayerIdentifier = _actors.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key); RetainerIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell); MannequinIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin); + + if (_humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc) + OwnedIdentifier = _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]); + else + OwnedIdentifier = ActorIdentifier.Invalid; } NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc diff --git a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs index f125f36..8eba59b 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs @@ -4,10 +4,11 @@ using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Designs.Special; using Glamourer.Events; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; namespace Glamourer.Gui.Tabs.AutomationTab; @@ -277,7 +278,7 @@ public sealed class RandomRestrictionDrawer : IService, IDisposable private void LookupTooltip(IEnumerable designs) { using var _ = ImRaii.Tooltip(); - var tt = string.Join('\n', designs.Select(d => _designFileSystem.FindLeaf(d, out var l) ? l.FullName() : d.Name.Text).OrderBy(t => t)); + var tt = string.Join('\n', designs.Select(d => _designFileSystem.TryGetValue(d, out var l) ? l.FullName() : d.Name.Text).OrderBy(t => t)); ImGui.TextUnformatted(tt.Length == 0 ? "Matches no currently existing designs." : "Matches the following designs:"); @@ -385,6 +386,11 @@ public sealed class RandomRestrictionDrawer : IService, IDisposable ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y + ImGuiHelpers.GlobalScale); ImGui.Separator(); ImGui.Dummy(Vector2.Zero); + var reset = random.ResetOnRedraw; + if (ImUtf8.Checkbox("Reset Chosen Design On Every Redraw"u8, ref reset)) + _autoDesignManager.ChangeData(_set!, _designIndex, reset); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); var list = random.Predicates.ToList(); if (list.Count == 0) diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 7da95a3..8a85a45 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -6,10 +6,12 @@ using Glamourer.Designs.Special; using Glamourer.Interop; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -29,10 +31,10 @@ public class SetPanel( Configuration _config, RandomRestrictionDrawer _randomDrawer) { - private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log); - private readonly HeaderDrawer.Button[] _rightButtons = [new HeaderDrawer.IncognitoButton(_config.Ephemeral)]; - private string? _tempName; - private int _dragIndex = -1; + private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log); + private readonly HeaderDrawer.Button[] _rightButtons = [new HeaderDrawer.IncognitoButton(_config)]; + private string? _tempName; + private int _dragIndex = -1; private Action? _endAction; @@ -51,44 +53,59 @@ public class SetPanel( private void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); + using var child = ImUtf8.Child("##Panel"u8, -Vector2.One, true); if (!child || !_selector.HasSelection) return; var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; - using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + using (ImUtf8.Group()) { - var enabled = Selection.Enabled; - if (ImGui.Checkbox("##Enabled", ref enabled)) - _manager.SetState(_selector.SelectionIndex, enabled); - ImGuiUtil.LabeledHelpMarker("Enabled", - "Whether the designs in this set should be applied at all. Only one set can be enabled for a character at the same time."); - } - - ImGui.SameLine(); - using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) - { - var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game; - if (ImGui.Checkbox("##gameState", ref useGame)) - _manager.ChangeBaseState(_selector.SelectionIndex, useGame ? AutoDesignSet.Base.Game : AutoDesignSet.Base.Current); - ImGuiUtil.LabeledHelpMarker("Use Game State as Base", - "When this is enabled, the designs matching conditions will be applied successively on top of what your character is supposed to look like for the game. " - + "Otherwise, they will be applied on top of the characters actual current look using Glamourer."); - } - - ImGui.SameLine(); - using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) - { - var editing = _config.ShowAutomationSetEditing; - if (ImGui.Checkbox("##Show Editing", ref editing)) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) { - _config.ShowAutomationSetEditing = editing; - _config.Save(); + var enabled = Selection.Enabled; + if (ImUtf8.Checkbox("##Enabled"u8, ref enabled)) + _manager.SetState(_selector.SelectionIndex, enabled); + ImUtf8.LabeledHelpMarker("Enabled"u8, + "Whether the designs in this set should be applied at all. Only one set can be enabled for a character at the same time."u8); } - ImGuiUtil.LabeledHelpMarker("Show Editing", - "Show options to change the name or the associated character or NPC of this design set."); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game; + if (ImUtf8.Checkbox("##gameState"u8, ref useGame)) + _manager.ChangeBaseState(_selector.SelectionIndex, useGame ? AutoDesignSet.Base.Game : AutoDesignSet.Base.Current); + ImUtf8.LabeledHelpMarker("Use Game State as Base"u8, + "When this is enabled, the designs matching conditions will be applied successively on top of what your character is supposed to look like for the game. "u8 + + "Otherwise, they will be applied on top of the characters actual current look using Glamourer."u8); + } + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var editing = _config.ShowAutomationSetEditing; + if (ImUtf8.Checkbox("##Show Editing"u8, ref editing)) + { + _config.ShowAutomationSetEditing = editing; + _config.Save(); + } + + ImUtf8.LabeledHelpMarker("Show Editing"u8, + "Show options to change the name or the associated character or NPC of this design set."u8); + } + + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var resetSettings = _selector.Selection!.ResetTemporarySettings; + if (ImGui.Checkbox("##resetSettings", ref resetSettings)) + _manager.ChangeResetSettings(_selector.SelectionIndex, resetSettings); + + ImUtf8.LabeledHelpMarker("Reset Temporary Settings"u8, + "Always reset all temporary settings applied by Glamourer when this automation set is applied, regardless of active designs."u8); + } } if (_config.ShowAutomationSetEditing) @@ -144,42 +161,43 @@ public class SetPanel( (false, false) => 4, }; - using var table = ImRaii.Table("SetTable", numRows, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY); + using var table = ImUtf8.Table("SetTable"u8, numRows, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY); if (!table) return; - ImGui.TableSetupColumn("##del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("##Index", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("##del"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImUtf8.TableSetupColumn("##Index"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); if (singleRow) { - ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("Design"u8, ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale); if (_config.ShowAllAutomatedApplicationRules) - ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, + ImUtf8.TableSetupColumn("Application"u8, ImGuiTableColumnFlags.WidthFixed, 6 * ImGui.GetFrameHeight() + 10 * ImGuiHelpers.GlobalScale); else - ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); + ImUtf8.TableSetupColumn("Use"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } else { - ImGui.TableSetupColumn("Design / Job Restrictions", ImGuiTableColumnFlags.WidthFixed, 250 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("Design / Job Restrictions"u8, ImGuiTableColumnFlags.WidthFixed, + 250 * ImGuiHelpers.GlobalScale - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)); if (_config.ShowAllAutomatedApplicationRules) - ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, + ImUtf8.TableSetupColumn("Application"u8, ImGuiTableColumnFlags.WidthFixed, 3 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); else - ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); + ImUtf8.TableSetupColumn("Use"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } if (singleRow) - ImGui.TableSetupColumn("Job Restrictions", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Job Restrictions"u8, ImGuiTableColumnFlags.WidthStretch); if (_config.ShowUnlockedItemWarnings) - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 2 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn(""u8, ImGuiTableColumnFlags.WidthFixed, 2 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); foreach (var (design, idx) in Selection.Designs.WithIndex()) { - using var id = ImRaii.PushId(idx); + using var id = ImUtf8.PushId(idx); ImGui.TableNextColumn(); var keyValid = _config.DeleteDesignModifier.IsActive(); var tt = keyValid @@ -189,8 +207,8 @@ public class SetPanel( if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, !keyValid, true)) _endAction = () => _manager.DeleteDesign(Selection, idx); ImGui.TableNextColumn(); - ImGui.Selectable($"#{idx + 1:D2}"); - DrawDragDrop(Selection, idx); + DrawSelectable(idx, design.Design); + ImGui.TableNextColumn(); DrawRandomEditing(Selection, design, idx); _designCombo.Draw(Selection, design, idx); @@ -218,8 +236,7 @@ public class SetPanel( ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("New"); + ImUtf8.TextFrameAligned("New"u8); ImGui.TableNextColumn(); _designCombo.Draw(Selection, null, -1); ImGui.TableNextRow(); @@ -228,30 +245,74 @@ public class SetPanel( _endAction = null; } + private void DrawSelectable(int idx, IDesignStandIn design) + { + var highlight = 0u; + var sb = new StringBuilder(); + if (design is Design d) + { + var count = design.AllLinks(true).Count(); + if (count > 1) + { + sb.AppendLine($"This design contains {count - 1} links to other designs."); + highlight = ColorId.HeaderButtons.Value(); + } + + count = d.AssociatedMods.Count; + if (count > 0) + { + sb.AppendLine($"This design contains {count} mod associations."); + highlight = ColorId.ModdedItemMarker.Value(); + } + + count = design.GetMaterialData().Count(p => p.Item2.Enabled); + if (count > 0) + { + sb.AppendLine($"This design contains {count} enabled advanced dyes."); + highlight = ColorId.AdvancedDyeActive.Value(); + } + } + + using (ImRaii.PushColor(ImGuiCol.Text, highlight, highlight != 0)) + { + ImUtf8.Selectable($"#{idx + 1:D2}"); + } + + ImUtf8.HoverTooltip($"{sb}"); + + DrawDragDrop(Selection, idx); + } + private int _tmpGearset = int.MaxValue; + private int _whichIndex = -1; private void DrawConditions(AutoDesign design, int idx) { var usingGearset = design.GearsetIndex >= 0; - if (ImGui.Button($"{(usingGearset ? "Gearset:" : "Jobs:")}##usingGearset")) + if (ImUtf8.Button($"{(usingGearset ? "Gearset:" : "Jobs:")}##usingGearset")) { usingGearset = !usingGearset; _manager.ChangeGearsetCondition(Selection, idx, (short)(usingGearset ? 0 : -1)); } - ImGuiUtil.HoverTooltip("Click to switch between Job and Gearset restrictions."); + ImUtf8.HoverTooltip("Click to switch between Job and Gearset restrictions."u8); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); if (usingGearset) { - var set = 1 + (_tmpGearset == int.MaxValue ? design.GearsetIndex : _tmpGearset); + var set = 1 + (_tmpGearset == int.MaxValue || _whichIndex != idx ? design.GearsetIndex : _tmpGearset); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputInt("##whichGearset", ref set, 0, 0)) + if (ImUtf8.InputScalar("##whichGearset"u8, ref set)) + { + _whichIndex = idx; _tmpGearset = Math.Clamp(set, 1, 100); + } + if (ImGui.IsItemDeactivatedAfterEdit()) { _manager.ChangeGearsetCondition(Selection, idx, (short)(_tmpGearset - 1)); _tmpGearset = int.MaxValue; + _whichIndex = -1; } } else @@ -277,13 +338,13 @@ public class SetPanel( var size = new Vector2(ImGui.GetFrameHeight()); size.X += ImGuiHelpers.GlobalScale; - var (equipFlags, customizeFlags, _, _, _) = design.ApplyWhat(); + var collection = design.ApplyWhat(); var sb = new StringBuilder(); var designData = design.Design.GetDesignData(default); foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { var flag = slot.ToFlag(); - if (!equipFlags.HasFlag(flag)) + if (!collection.Equip.HasFlag(flag)) continue; var item = designData.Item(slot); @@ -308,7 +369,7 @@ public class SetPanel( foreach (var type in CustomizationExtensions.All) { var flag = type.ToFlag(); - if (!customizeFlags.HasFlag(flag)) + if (!collection.Customize.HasFlag(flag)) continue; if (flag.RequiresRedraw()) @@ -339,12 +400,12 @@ public class SetPanel( ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color); } - ImGuiUtil.HoverTooltip(sb.ToString()); + ImUtf8.HoverTooltip($"{sb}"); } else { ImGuiUtil.DrawTextButton(string.Empty, size, 0); - ImGuiUtil.HoverTooltip(good); + ImUtf8.HoverTooltip(good); } } } @@ -352,7 +413,7 @@ public class SetPanel( private void DrawDragDrop(AutoDesignSet set, int index) { const string dragDropLabel = "DesignDragDrop"; - using (var target = ImRaii.DragDropTarget()) + using (var target = ImUtf8.DragDropTarget()) { if (target.Success && ImGuiUtil.IsDropping(dragDropLabel)) { @@ -366,15 +427,15 @@ public class SetPanel( } } - using (var source = ImRaii.DragDropSource()) + using (var source = ImUtf8.DragDropSource()) { if (source) { - ImGui.TextUnformatted($"Moving design #{index + 1:D2}..."); - if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0)) + ImUtf8.Text($"Moving design #{index + 1:D2}..."); + if (ImGui.SetDragDropPayload(dragDropLabel, null, 0)) { _dragIndex = index; - _selector._dragDesignIndex = index; + _selector.DragDesignIndex = index; } } } @@ -393,16 +454,16 @@ public class SetPanel( } style.Pop(); - ImGuiUtil.HoverTooltip("Toggle all application modes at once."); + ImUtf8.HoverTooltip("Toggle all application modes at once."u8); if (_config.ShowAllAutomatedApplicationRules) { void Box(int idx) { var (type, description) = ApplicationTypeExtensions.Types[idx]; var value = design.Type.HasFlag(type); - if (ImGui.Checkbox($"##{(byte)type}", ref value)) + if (ImUtf8.Checkbox($"##{(byte)type}", ref value)) newType = value ? newType | type : newType & ~type; - ImGuiUtil.HoverTooltip(description); + ImUtf8.HoverTooltip(description); } ImGui.SameLine(); @@ -424,22 +485,26 @@ public class SetPanel( private void DrawIdentifierSelection(int setIndex) { - using var id = ImRaii.PushId("Identifiers"); + using var id = ImUtf8.PushId("Identifiers"u8); _identifierDrawer.DrawWorld(130); ImGui.SameLine(); _identifierDrawer.DrawName(200 - ImGui.GetStyle().ItemSpacing.X); _identifierDrawer.DrawNpcs(330); var buttonWidth = new Vector2(165 * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X / 2, 0); - if (ImGuiUtil.DrawDisabledButton("Set to Character", buttonWidth, string.Empty, !_identifierDrawer.CanSetPlayer)) + if (ImUtf8.ButtonEx("Set to Character"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetPlayer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.PlayerIdentifier); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Set to NPC", buttonWidth, string.Empty, !_identifierDrawer.CanSetNpc)) + if (ImUtf8.ButtonEx("Set to NPC"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetNpc)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.NpcIdentifier); - if (ImGuiUtil.DrawDisabledButton("Set to Retainer", buttonWidth, string.Empty, !_identifierDrawer.CanSetRetainer)) + + if (ImUtf8.ButtonEx("Set to Retainer"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetRetainer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.RetainerIdentifier); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Set to Mannequin", buttonWidth, string.Empty, !_identifierDrawer.CanSetRetainer)) + if (ImUtf8.ButtonEx("Set to Mannequin"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetRetainer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.MannequinIdentifier); + + if (ImUtf8.ButtonEx("Set to Owned NPC"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetOwned)) + _manager.ChangeIdentifier(setIndex, _identifierDrawer.OwnedIdentifier); } private sealed class JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index 950b735..8a235ae 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -2,12 +2,12 @@ using Dalamud.Interface.Utility; using Glamourer.Automation; using Glamourer.Events; -using Glamourer.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; -using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; using Penumbra.String; using ImGuiClip = OtterGui.ImGuiClip; @@ -18,8 +18,7 @@ public class SetSelector : IDisposable private readonly Configuration _config; private readonly AutoDesignManager _manager; private readonly AutomationChanged _event; - private readonly ActorManager _actors; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; private readonly List<(AutoDesignSet, int)> _list = []; public AutoDesignSet? Selection { get; private set; } @@ -38,14 +37,13 @@ public class SetSelector : IDisposable private int _dragIndex = -1; private Action? _endAction; - internal int _dragDesignIndex = -1; + internal int DragDesignIndex = -1; - public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorManager actors, ObjectManager objects) + public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorObjectManager objects) { _manager = manager; _event = @event; _config = config; - _actors = actors; _objects = objects; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.SetSelector); } @@ -94,7 +92,7 @@ public class SetSelector : IDisposable } private LowerString _filter = LowerString.Empty; - private uint _enabledFilter = 0; + private uint _enabledFilter; private float _width; private Vector2 _defaultItemSpacing; private Vector2 _selectableSize; @@ -146,7 +144,7 @@ public class SetSelector : IDisposable ImGui.SameLine(); var f = _enabledFilter; - if (ImGui.CheckboxFlags("##enabledFilter", ref f, 3)) + if (ImGui.CheckboxFlags("##enabledFilter", ref f, 3u)) { _enabledFilter = _enabledFilter switch { @@ -177,7 +175,6 @@ public class SetSelector : IDisposable UpdateList(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); _selectableSize = new Vector2(0, 2 * ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y); - _objects.Update(); ImGuiClip.ClippedDraw(_list, DrawSetSelectable, _selectableSize.Y + 2 * ImGui.GetStyle().ItemSpacing.Y); _endAction?.Invoke(); _endAction = null; @@ -186,7 +183,7 @@ public class SetSelector : IDisposable private void DrawSetSelectable((AutoDesignSet Set, int Index) pair) { using var id = ImRaii.PushId(pair.Index); - using (var color = ImRaii.PushColor(ImGuiCol.Text, pair.Set.Enabled ? ColorId.EnabledAutoSet.Value() : ColorId.DisabledAutoSet.Value())) + using (ImRaii.PushColor(ImGuiCol.Text, pair.Set.Enabled ? ColorId.EnabledAutoSet.Value() : ColorId.DisabledAutoSet.Value())) { if (ImGui.Selectable(GetSetName(pair.Set, pair.Index), pair.Set == Selection, ImGuiSelectableFlags.None, _selectableSize)) { @@ -285,9 +282,9 @@ public class SetSelector : IDisposable private void NewSetButton(Vector2 size) { - var id = _actors.GetCurrentPlayer(); + var id = _objects.Actors.GetCurrentPlayer(); if (!id.IsValid) - id = _actors.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); + id = _objects.Actors.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, $"Create a new Automatic Design Set for {id}. The associated player can be changed later.", !id.IsValid, true)) _manager.AddDesignSet("New Automation Set", id); @@ -332,15 +329,15 @@ public class SetSelector : IDisposable } else if (ImGuiUtil.IsDropping("DesignDragDrop")) { - if (_dragDesignIndex >= 0) + if (DragDesignIndex >= 0) { - var idx = _dragDesignIndex; + var idx = DragDesignIndex; var setTo = set; var setFrom = Selection!; _endAction = () => _manager.MoveDesignToSet(setFrom, idx, setTo); } - _dragDesignIndex = -1; + DragDesignIndex = -1; } } } @@ -350,7 +347,7 @@ public class SetSelector : IDisposable if (source) { ImGui.TextUnformatted($"Moving design set {GetSetName(set, index)} from position {index + 1}..."); - if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0)) + if (ImGui.SetDragDropPayload(dragDropLabel, null, 0)) _dragIndex = index; } } diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs index 4aa0163..35642a7 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs @@ -1,18 +1,17 @@ using Dalamud.Interface; using Glamourer.GameData; using Glamourer.Designs; -using Glamourer.Interop; -using Glamourer.Interop.Structs; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; namespace Glamourer.Gui.Tabs.DebugTab; -public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer +public class ActiveStatePanel(StateManager _stateManager, ActorObjectManager _objectManager) : IGameDataDrawer { public string Label => $"Active Actors ({_stateManager.Count})###Active Actors"; @@ -22,8 +21,7 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM public void Draw() { - _objectManager.Update(); - foreach (var (identifier, actors) in _objectManager.Identifiers) + foreach (var (identifier, actors) in _objectManager) { if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Trash.ToIconString()}##{actors.Label}", new Vector2(ImGui.GetFrameHeight()), string.Empty, !_stateManager.ContainsKey(identifier), true)) @@ -66,7 +64,15 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM static string ItemString(in DesignData data, EquipSlot slot) { var item = data.Item(slot); - return $"{item.Name} ({item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; + return + $"{item.Name} ({item.Id.ToDiscriminatingString()} {item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; + } + + static string BonusItemString(in DesignData data, BonusItemFlag slot) + { + var item = data.BonusItem(slot); + return + $"{item.Name} ({item.Id.ToDiscriminatingString()} {item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; } PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state.Sources[MetaIndex.ModelId]); @@ -81,20 +87,30 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), state.Sources[MetaIndex.VisorState]); ImGui.TableNextRow(); + PrintRow("Viera Ears Visible", state.BaseData.AreEarsVisible(), state.ModelData.AreEarsVisible(), + state.Sources[MetaIndex.EarState]); + ImGui.TableNextRow(); PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), state.Sources[MetaIndex.WeaponState]); ImGui.TableNextRow(); foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state.Sources[slot, false]); - ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Id.ToString()); - ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Id.ToString()); + ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).ToString()); + ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).ToString()); ImGuiUtil.DrawTableColumn(state.Sources[slot, true].ToString()); } + foreach (var slot in BonusExtensions.AllFlags) + { + PrintRow(slot.ToName(), BonusItemString(state.BaseData, slot), BonusItemString(state.ModelData, slot), state.Sources[slot]); + ImGui.TableNextRow(); + } + foreach (var type in Enum.GetValues()) { - PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state.Sources[type]); + PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, + state.Sources[type]); ImGui.TableNextRow(); } diff --git a/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs b/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs new file mode 100644 index 0000000..2202ceb --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs @@ -0,0 +1,135 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using Dalamud.Bindings.ImGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class AdvancedCustomizationDrawer(ActorObjectManager objects) : IGameDataDrawer +{ + public string Label + => "Advanced Customizations"; + + public bool Disabled + => false; + + public void Draw() + { + var (player, data) = objects.PlayerData; + if (!data.Valid) + { + ImUtf8.Text("Invalid player."u8); + return; + } + + var model = data.Objects[0].Model; + if (!model.IsHuman) + { + ImUtf8.Text("Invalid model."u8); + return; + } + + DrawCBuffer("Customize"u8, model.AsHuman->CustomizeParameterCBuffer, 0); + DrawCBuffer("Decal"u8, model.AsHuman->DecalColorCBuffer, 1); + DrawCBuffer("Unk1"u8, *(ConstantBuffer**)((byte*)model.AsHuman + 0xBA0), 2); + DrawCBuffer("Unk2"u8, *(ConstantBuffer**)((byte*)model.AsHuman + 0xBA8), 3); + } + + + private static void DrawCBuffer(ReadOnlySpan label, ConstantBuffer* cBuffer, int type) + { + using var tree = ImUtf8.TreeNode(label); + if (!tree) + return; + + if (cBuffer == null) + { + ImUtf8.Text("Invalid CBuffer."u8); + return; + } + + ImUtf8.Text($"{cBuffer->ByteSize / 4}"); + ImUtf8.Text($"{cBuffer->Flags}"); + ImUtf8.Text($"0x{(ulong)cBuffer:X}"); + var parameters = (float*)cBuffer->UnsafeSourcePointer; + if (parameters == null) + { + ImUtf8.Text("No Parameters."u8); + return; + } + + var start = parameters; + using (ImUtf8.Group()) + { + for (var end = start + cBuffer->ByteSize / 4; parameters < end; parameters += 2) + DrawParameters(parameters, type, (int)(parameters - start)); + } + + ImGui.SameLine(0, 50 * ImUtf8.GlobalScale); + parameters = start + 1; + using (ImUtf8.Group()) + { + for (var end = start + cBuffer->ByteSize / 4; parameters < end; parameters += 2) + DrawParameters(parameters, type, (int)(parameters - start)); + } + } + + private static void DrawParameters(float* param, int type, int idx) + { + using var id = ImUtf8.PushId((nint)param); + ImUtf8.TextFrameAligned($"{idx:D2}: "); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (TryGetKnown(type, idx, out var known)) + { + ImUtf8.DragScalar(known, ref *param, float.MinValue, float.MaxValue, 0.01f); + } + else + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImUtf8.DragScalar($"+0x{idx * 4:X2}", ref *param, float.MinValue, float.MaxValue, 0.01f); + } + } + + private static bool TryGetKnown(int type, int idx, out ReadOnlySpan text) + { + if (type == 0) + text = idx switch + { + 0 => "Diffuse.R"u8, + 1 => "Diffuse.G"u8, + 2 => "Diffuse.B"u8, + 3 => "Muscle Tone"u8, + 8 => "Lipstick.R"u8, + 9 => "Lipstick.G"u8, + 10 => "Lipstick.B"u8, + 11 => "Lipstick.Opacity"u8, + 12 => "Hair.R"u8, + 13 => "Hair.G"u8, + 14 => "Hair.B"u8, + 15 => "Facepaint.Offset"u8, + 20 => "Highlight.R"u8, + 21 => "Highlight.G"u8, + 22 => "Highlight.B"u8, + 23 => "Facepaint.Multiplier"u8, + 24 => "LeftEye.R"u8, + 25 => "LeftEye.G"u8, + 26 => "LeftEye.B"u8, + 27 => "LeftLimbal"u8, + 28 => "RightEye.R"u8, + 29 => "RightEye.G"u8, + 30 => "RightEye.B"u8, + 31 => "RightLimbal"u8, + 32 => "Feature.R"u8, + 33 => "Feature.G"u8, + 34 => "Feature.B"u8, + _ => [], + }; + else + text = []; + + return text.Length > 0; + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs index 98b7d9e..aee59b6 100644 --- a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs @@ -1,6 +1,7 @@ using Glamourer.Automation; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using Penumbra.GameData.Gui.Debug; diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs index 17e180e..6c0995c 100644 --- a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs @@ -1,10 +1,13 @@ -using Glamourer.GameData; +using Dalamud.Interface; +using Glamourer.GameData; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab; @@ -24,6 +27,75 @@ public class CustomizationServicePanel(CustomizeService customize) : IGameDataDr DrawCustomizationInfo(set); DrawNpcCustomizationInfo(set); } + + DrawFacepaintInfo(); + DrawColorInfo(); + } + + private void DrawFacepaintInfo() + { + using var tree = ImUtf8.TreeNode("NPC Facepaints"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("data"u8, 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Id"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Facepaint"u8); + + for (var i = 0; i < 128; ++i) + { + var index = new CustomizeValue((byte)i); + ImUtf8.DrawTableColumn($"{i:D3}"); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.FacePaint, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + } + } + private void DrawColorInfo() + { + using var tree = ImUtf8.TreeNode("NPC Colors"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("data"u8, 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Id"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Hair"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Eyes"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Facepaint"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Tattoos"u8); + + for (var i = 192; i < 256; ++i) + { + var index = new CustomizeValue((byte)i); + ImUtf8.DrawTableColumn($"{i:D3}"); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.HairColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.EyeColorLeft, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.FacePaintColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.TattooColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + } } private void DrawCustomizationInfo(CustomizeSet set) diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs index a53a677..4bf7d7b 100644 --- a/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs @@ -1,5 +1,5 @@ using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; diff --git a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs index 11f27fd..7c61392 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs @@ -1,5 +1,5 @@ using Glamourer.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using Penumbra.GameData.Files; using Penumbra.GameData.Gui.Debug; diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs index cd89aec..b760221 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs index 3dce124..3df425f 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs @@ -1,5 +1,4 @@ using Glamourer.Gui.Tabs.DebugTab.IpcTester; -using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using OtterGui.Raii; using Penumbra.GameData.Gui.Debug; @@ -36,9 +35,11 @@ public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees) provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService() ); public static DebugTabHeader CreateGameData(IServiceProvider provider) diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs index 2345abc..287d373 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Glamourer.Designs; using Glamourer.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs index 85b4010..7c60dda 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs @@ -1,8 +1,11 @@ using Dalamud.Interface; using Glamourer.Designs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; +using OtterGui.Filesystem; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; @@ -18,6 +21,7 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ public void Draw() { + DrawButtons(); foreach (var (design, idx) in _designManager.Designs.WithIndex()) { using var t = ImRaii.TreeNode($"{design.Name}##{idx}"); @@ -25,7 +29,8 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ continue; DrawDesign(design, _designFileSystem); - var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw, design.ApplyMeta, + var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.Application.Equip, design.Application.Customize, + design.Application.Meta, design.WriteProtected()); using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImGuiUtil.TextWrapped(base64); @@ -34,6 +39,26 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ } } + private void DrawButtons() + { + if (ImUtf8.Button("Generate 500 Test Designs"u8)) + for (var i = 0; i < 500; ++i) + { + var design = _designManager.CreateEmpty($"Test Designs/Test Design {i}", true); + _designManager.AddTag(design, "_DebugTest"); + } + + ImUtf8.SameLineInner(); + if (ImUtf8.Button("Remove All Test Designs"u8)) + { + var designs = _designManager.Designs.Where(d => d.Tags.Contains("_DebugTest")).ToArray(); + foreach (var design in designs) + _designManager.Delete(design); + if (_designFileSystem.Find("Test Designs", out var path) && path is DesignFileSystem.Folder { TotalChildren: 0 }) + _designFileSystem.Delete(path); + } + } + public static void DrawDesign(DesignBase design, DesignFileSystem? fileSystem) { using var table = ImRaii.Table("##equip", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); @@ -52,7 +77,7 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ ImGui.TableNextRow(); ImGuiUtil.DrawTableColumn("Design File System Path"); if (fileSystem != null) - ImGuiUtil.DrawTableColumn(fileSystem.FindLeaf(d, out var leaf) ? leaf.FullName() : "No Path Known"); + ImGuiUtil.DrawTableColumn(fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : "No Path Known"); ImGui.TableNextRow(); ImGuiUtil.DrawTableColumn("Creation"); @@ -89,6 +114,7 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _ ImGuiUtil.DrawTableColumn(index.ToName()); ImGuiUtil.DrawTableColumn(design.DesignData.GetMeta(index).ToString()); ImGuiUtil.DrawTableColumn(design.DoApplyMeta(index) ? "Apply" : "Keep"); + ImGui.TableNextRow(); } ImGuiUtil.DrawTableColumn("Model ID"); diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs index c893f4c..cf45077 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs @@ -1,8 +1,9 @@ using Dalamud.Interface; using Glamourer.Designs; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; diff --git a/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs new file mode 100644 index 0000000..92cd777 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs @@ -0,0 +1,16 @@ +using OtterGui.Services; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DynamisPanel(DynamisIpc dynamis) : IGameDataDrawer +{ + public string Label + => "Dynamis Interop"; + + public void Draw() + => dynamis.DrawDebugInfo(); + + public bool Disabled + => false; +} diff --git a/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs index c517070..370c4e5 100644 --- a/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs @@ -1,5 +1,5 @@ using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs index 2129c1f..f480f6d 100644 --- a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs @@ -3,22 +3,26 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game; using Glamourer.Designs; -using Glamourer.Interop; using Glamourer.Services; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; +using OtterGui.Text; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab; public unsafe class GlamourPlatePanel : IGameDataDrawer { - private readonly DesignManager _design; - private readonly ItemManager _items; - private readonly StateManager _state; - private readonly ObjectManager _objects; + private readonly DesignManager _design; + private readonly ItemManager _items; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; public string Label => "Glamour Plates"; @@ -26,7 +30,8 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer public bool Disabled => false; - public GlamourPlatePanel(IGameInteropProvider interop, ItemManager items, DesignManager design, StateManager state, ObjectManager objects) + public GlamourPlatePanel(IGameInteropProvider interop, ItemManager items, DesignManager design, StateManager state, + ActorObjectManager objects) { _items = items; _design = design; @@ -40,24 +45,24 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer var manager = MirageManager.Instance(); using (ImRaii.Group()) { - ImGui.TextUnformatted("Address:"); - ImGui.TextUnformatted("Number of Glamour Plates:"); - ImGui.TextUnformatted("Glamour Plates Requested:"); - ImGui.TextUnformatted("Glamour Plates Loaded:"); - ImGui.TextUnformatted("Is Applying Glamour Plates:"); + ImUtf8.Text("Address:"u8); + ImUtf8.Text("Number of Glamour Plates:"u8); + ImUtf8.Text("Glamour Plates Requested:"u8); + ImUtf8.Text("Glamour Plates Loaded:"u8); + ImUtf8.Text("Is Applying Glamour Plates:"u8); } ImGui.SameLine(); using (ImRaii.Group()) { - ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)manager:X}"); - ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesSpan.Length.ToString()); - ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesRequested.ToString()); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)manager:X}"); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlates.Length.ToString()); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlatesRequested.ToString()); ImGui.SameLine(); - if (ImGui.SmallButton("Request Update")) + if (ImUtf8.SmallButton("Request Update"u8)) RequestGlamour(); - ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesLoaded.ToString()); - ImGui.TextUnformatted(manager == null ? "-" : manager->IsApplyingGlamourPlate.ToString()); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlatesLoaded.ToString()); + ImUtf8.Text(manager == null ? "-" : manager->IsApplyingGlamourPlate.ToString()); } if (manager == null) @@ -67,35 +72,35 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer var (identifier, data) = _objects.PlayerData; var enabled = data.Valid && _state.GetOrCreate(identifier, data.Objects[0], out state); - for (var i = 0; i < manager->GlamourPlatesSpan.Length; ++i) + for (var i = 0; i < manager->GlamourPlates.Length; ++i) { - using var tree = ImRaii.TreeNode($"Plate #{i + 1:D2}"); + using var tree = ImUtf8.TreeNode($"Plate #{i + 1:D2}"); if (!tree) continue; - ref var plate = ref manager->GlamourPlatesSpan[i]; - if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled)) + ref var plate = ref manager->GlamourPlates[i]; + if (ImUtf8.ButtonEx("Apply to Player"u8, ""u8, Vector2.Zero, !enabled)) { var design = CreateDesign(plate); - _state.ApplyDesign(state!, design, ApplySettings.Manual); + _state.ApplyDesign(state!, design, ApplySettings.Manual with { IsFinal = true }); } using (ImRaii.Group()) { foreach (var slot in EquipSlotExtensions.FullSlots) - ImGui.TextUnformatted(slot.ToName()); + ImUtf8.Text(slot.ToName()); } ImGui.SameLine(); using (ImRaii.Group()) { foreach (var (_, index) in EquipSlotExtensions.FullSlots.WithIndex()) - ImGui.TextUnformatted($"{plate.ItemIds[index]:D6}, {plate.StainIds[index]:D3}"); + ImUtf8.Text($"{plate.ItemIds[index]:D6}, {StainIds.FromGlamourPlate(plate, index)}"); } } } - [Signature("E8 ?? ?? ?? ?? 32 C0 48 8B 5C 24 ?? 48 8B 6C 24 ?? 48 83 C4 ?? 5F")] + [Signature(Sigs.RequestGlamourPlates)] private readonly delegate* unmanaged _requestUpdate = null!; public void RequestGlamour() @@ -110,11 +115,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer public DesignBase CreateDesign(in MirageManager.GlamourPlate plate) { var design = _design.CreateTemporary(); - design.ApplyCustomize = 0; - design.ApplyCrest = 0; - design.ApplyMeta = 0; - design.ApplyParameters = 0; - design.ApplyEquip = 0; + design.Application = ApplicationCollection.None; foreach (var (slot, index) in EquipSlotExtensions.FullSlots.WithIndex()) { var itemId = plate.ItemIds[index]; @@ -126,8 +127,8 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer continue; design.GetDesignDataRef().SetItem(slot, item); - design.GetDesignDataRef().SetStain(slot, plate.StainIds[index]); - design.ApplyEquip |= slot.ToBothFlags(); + design.GetDesignDataRef().SetStain(slot, StainIds.FromGlamourPlate(plate, index)); + design.Application.Equip |= slot.ToBothFlags(); } return design; diff --git a/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs index bd447e7..ca9ff7b 100644 --- a/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs @@ -1,5 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Gui.Debug; @@ -23,7 +23,7 @@ public unsafe class InventoryPanel : IGameDataDrawer ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)inventory:X}"); var equip = inventory->GetInventoryContainer(InventoryType.EquippedItems); - if (equip == null || equip->Loaded == 0) + if (equip == null || equip->IsLoaded) return; ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)equip:X}"); @@ -43,8 +43,8 @@ public unsafe class InventoryPanel : IGameDataDrawer } else { - ImGuiUtil.DrawTableColumn(item->ItemID.ToString()); - ImGuiUtil.DrawTableColumn(item->GlamourID.ToString()); + ImGuiUtil.DrawTableColumn(item->ItemId.ToString()); + ImGuiUtil.DrawTableColumn(item->GlamourId.ToString()); ImGui.TableNextColumn(); ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)item:X}"); } diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs index 1a74778..8cbf57a 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs @@ -3,18 +3,20 @@ using Dalamud.Interface.Utility; using Dalamud.Plugin; using Glamourer.Api.Enums; using Glamourer.Api.IpcSubscribers; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; -public class DesignIpcTester(DalamudPluginInterface pluginInterface) : IUiService +public class DesignIpcTester(IDalamudPluginInterface pluginInterface) : IUiService { private Dictionary _designs = []; private int _gameObjectIndex; private string _gameObjectName = string.Empty; + private string _designName = string.Empty; private uint _key; private ApplyFlag _flags = ApplyFlagEx.DesignDefault; private Guid? _design; @@ -30,6 +32,7 @@ public class DesignIpcTester(DalamudPluginInterface pluginInterface) : IUiServic IpcTesterHelpers.IndexInput(ref _gameObjectIndex); IpcTesterHelpers.KeyInput(ref _key); IpcTesterHelpers.NameInput(ref _gameObjectName); + ImUtf8.InputText("##designName"u8, ref _designName, "Design Name..."u8); ImGuiUtil.GuidInput("##identifier", "Design Identifier...", string.Empty, ref _design, ref _designText, ImGui.GetContentRegionAvail().X); IpcTesterHelpers.DrawFlagInput(ref _flags); @@ -54,6 +57,48 @@ public class DesignIpcTester(DalamudPluginInterface pluginInterface) : IUiServic IpcTesterHelpers.DrawIntro(ApplyDesignName.Label); if (ImGuiUtil.DrawDisabledButton("Apply##Name", Vector2.Zero, string.Empty, !_design.HasValue)) _lastError = new ApplyDesignName(pluginInterface).Invoke(_design!.Value, _gameObjectName, _key, _flags); + + IpcTesterHelpers.DrawIntro(GetExtendedDesignData.Label); + if (_design.HasValue) + { + var (display, path, color, draw) = new GetExtendedDesignData(pluginInterface).Invoke(_design.Value); + if (path.Length > 0) + ImUtf8.Text($"{display} ({path}){(draw ? " in QDB"u8 : ""u8)}", color); + else + ImUtf8.Text("No Data"u8); + } + else + { + ImUtf8.Text("No Data"u8); + } + + IpcTesterHelpers.DrawIntro(GetDesignBase64.Label); + if (ImUtf8.Button("To Clipboard##Base64"u8) && _design.HasValue) + { + var data = new GetDesignBase64(pluginInterface).Invoke(_design.Value); + ImUtf8.SetClipboardText(data); + } + + IpcTesterHelpers.DrawIntro(AddDesign.Label); + if (ImUtf8.Button("Add from Clipboard"u8)) + try + { + var data = ImUtf8.GetClipboardText(); + _lastError = new AddDesign(pluginInterface).Invoke(data, _designName, out var newDesign); + if (_lastError is GlamourerApiEc.Success) + { + _design = newDesign; + _designText = newDesign.ToString(); + } + } + catch + { + _lastError = GlamourerApiEc.UnknownError; + } + + IpcTesterHelpers.DrawIntro(DeleteDesign.Label); + if (ImUtf8.Button("Delete##Design"u8) && _design.HasValue) + _lastError = new DeleteDesign(pluginInterface).Invoke(_design.Value); } private void DrawDesignsPopup() diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs index 500fddd..61dad53 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs @@ -1,8 +1,5 @@ using Glamourer.Api.Enums; -using Glamourer.Designs; -using ImGuiNET; -using OtterGui; -using static Penumbra.GameData.Files.ShpkFile; +using Dalamud.Bindings.ImGui; namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs index 5e6f4a2..22c7597 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs @@ -1,13 +1,13 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using Glamourer.Api.IpcSubscribers; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; public class IpcTesterPanel( - DalamudPluginInterface pluginInterface, + IDalamudPluginInterface pluginInterface, DesignIpcTester designs, ItemsIpcTester items, StateIpcTester state, @@ -33,6 +33,11 @@ public class IpcTesterPanel( ImGui.SameLine(); ImGui.TextUnformatted($"({major}.{minor:D4})"); + ImGui.TextUnformatted(AutoReloadGearEnabled.Label); + var autoRedraw = new AutoReloadGearEnabled(pluginInterface).Invoke(); + ImGui.SameLine(); + ImGui.TextUnformatted(autoRedraw ? "Enabled" : "Disabled"); + designs.Draw(); items.Draw(); state.Draw(); @@ -49,8 +54,10 @@ public class IpcTesterPanel( return; Glamourer.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); + state.AutoRedrawChanged.Enable(); state.GPoseChanged.Enable(); state.StateChanged.Enable(); + state.StateFinalized.Enable(); framework.Update += CheckUnsubscribe; _subscribed = true; } @@ -71,7 +78,9 @@ public class IpcTesterPanel( Glamourer.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester."); _subscribed = false; + state.AutoRedrawChanged.Disable(); state.GPoseChanged.Disable(); state.StateChanged.Disable(); + state.StateFinalized.Disable(); } } diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs index 3d61df7..ea95a9d 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs @@ -1,7 +1,7 @@ using Dalamud.Plugin; using Glamourer.Api.Enums; using Glamourer.Api.IpcSubscribers; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; @@ -11,7 +11,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; -public class ItemsIpcTester(DalamudPluginInterface pluginInterface) : IUiService +public class ItemsIpcTester(IDalamudPluginInterface pluginInterface) : IUiService { private int _gameObjectIndex; private string _gameObjectName = string.Empty; @@ -19,7 +19,8 @@ public class ItemsIpcTester(DalamudPluginInterface pluginInterface) : IUiService private ApplyFlag _flags = ApplyFlagEx.DesignDefault; private CustomItemId _customItemId; private StainId _stainId; - private EquipSlot _slot = EquipSlot.Head; + private EquipSlot _slot = EquipSlot.Head; + private BonusItemFlag _bonusSlot = BonusItemFlag.Glasses; private GlamourerApiEc _lastError; public void Draw() @@ -40,12 +41,22 @@ public class ItemsIpcTester(DalamudPluginInterface pluginInterface) : IUiService IpcTesterHelpers.DrawIntro(SetItem.Label); if (ImGui.Button("Set##Idx")) - _lastError = new SetItem(pluginInterface).Invoke(_gameObjectIndex, (ApiEquipSlot)_slot, _customItemId.Id, _stainId.Id, _key, + _lastError = new SetItem(pluginInterface).Invoke(_gameObjectIndex, (ApiEquipSlot)_slot, _customItemId.Id, [_stainId.Id], _key, _flags); IpcTesterHelpers.DrawIntro(SetItemName.Label); if (ImGui.Button("Set##Name")) - _lastError = new SetItemName(pluginInterface).Invoke(_gameObjectName, (ApiEquipSlot)_slot, _customItemId.Id, _stainId.Id, _key, + _lastError = new SetItemName(pluginInterface).Invoke(_gameObjectName, (ApiEquipSlot)_slot, _customItemId.Id, [_stainId.Id], _key, + _flags); + + IpcTesterHelpers.DrawIntro(SetBonusItem.Label); + if (ImGui.Button("Set##BonusIdx")) + _lastError = new SetBonusItem(pluginInterface).Invoke(_gameObjectIndex, ToApi(_bonusSlot), _customItemId.Id, _key, + _flags); + + IpcTesterHelpers.DrawIntro(SetBonusItemName.Label); + if (ImGui.Button("Set##BonusName")) + _lastError = new SetBonusItemName(pluginInterface).Invoke(_gameObjectName, ToApi(_bonusSlot), _customItemId.Id, _key, _flags); } @@ -57,6 +68,7 @@ public class ItemsIpcTester(DalamudPluginInterface pluginInterface) : IUiService if (ImGuiUtil.InputUlong("Custom Item ID", ref tmp)) _customItemId = tmp; EquipSlotCombo.Draw("Equip Slot", string.Empty, ref _slot, width); + BonusSlotCombo.Draw("Bonus Slot", string.Empty, ref _bonusSlot, width); var value = (int)_stainId.Id; ImGui.SetNextItemWidth(width); if (ImGui.InputInt("Stain ID", ref value, 1, 3)) @@ -65,4 +77,11 @@ public class ItemsIpcTester(DalamudPluginInterface pluginInterface) : IUiService _stainId = (StainId)value; } } + + private static ApiBonusSlot ToApi(BonusItemFlag slot) + => slot switch + { + BonusItemFlag.Glasses => ApiBonusSlot.Glasses, + _ => ApiBonusSlot.Unknown, + }; } diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs index 81c8cab..6fb9d68 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs @@ -1,16 +1,17 @@ -using Dalamud.Interface; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; using Glamourer.Api.Enums; using Glamourer.Api.Helpers; using Glamourer.Api.IpcSubscribers; using Glamourer.Designs; -using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Interop; using Penumbra.String; @@ -18,7 +19,7 @@ namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; public class StateIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private int _gameObjectIndex; private string _gameObjectName = string.Empty; @@ -30,10 +31,21 @@ public class StateIpcTester : IUiService, IDisposable private string _base64State = string.Empty; private string? _getStateString; + public readonly EventSubscriber AutoRedrawChanged; + private bool _lastAutoRedrawChangeValue; + private DateTime _lastAutoRedrawChangeTime; + public readonly EventSubscriber StateChanged; - private nint _lastStateChangeActor; - private ByteString _lastStateChangeName = ByteString.Empty; - private DateTime _lastStateChangeTime; + private nint _lastStateChangeActor; + private ByteString _lastStateChangeName = ByteString.Empty; + private DateTime _lastStateChangeTime; + private StateChangeType _lastStateChangeType; + + public readonly EventSubscriber StateFinalized; + private nint _lastStateFinalizeActor; + private ByteString _lastStateFinalizeName = ByteString.Empty; + private DateTime _lastStateFinalizeTime; + private StateFinalizationType _lastStateFinalizeType; public readonly EventSubscriber GPoseChanged; private bool _lastGPoseChangeValue; @@ -41,18 +53,24 @@ public class StateIpcTester : IUiService, IDisposable private int _numUnlocked; - public StateIpcTester(DalamudPluginInterface pluginInterface) + public StateIpcTester(IDalamudPluginInterface pluginInterface) { - _pluginInterface = pluginInterface; - StateChanged = Api.IpcSubscribers.StateChangedWithType.Subscriber(_pluginInterface, OnStateChanged); - GPoseChanged = Api.IpcSubscribers.GPoseChanged.Subscriber(_pluginInterface, OnGPoseChange); + _pluginInterface = pluginInterface; + AutoRedrawChanged = AutoReloadGearChanged.Subscriber(_pluginInterface, OnAutoRedrawChanged); + StateChanged = StateChangedWithType.Subscriber(_pluginInterface, OnStateChanged); + StateFinalized = Api.IpcSubscribers.StateFinalized.Subscriber(_pluginInterface, OnStateFinalized); + GPoseChanged = Api.IpcSubscribers.GPoseChanged.Subscriber(_pluginInterface, OnGPoseChange); + AutoRedrawChanged.Disable(); StateChanged.Disable(); + StateFinalized.Disable(); GPoseChanged.Disable(); } public void Dispose() { + AutoRedrawChanged.Dispose(); StateChanged.Dispose(); + StateFinalized.Dispose(); GPoseChanged.Dispose(); } @@ -72,87 +90,99 @@ public class StateIpcTester : IUiService, IDisposable IpcTesterHelpers.DrawIntro("Last Error"); ImGui.TextUnformatted(_lastError.ToString()); + IpcTesterHelpers.DrawIntro("Last Auto Redraw Change"); + ImGui.TextUnformatted($"{_lastAutoRedrawChangeValue} at {_lastAutoRedrawChangeTime.ToLocalTime().TimeOfDay}"); IpcTesterHelpers.DrawIntro("Last State Change"); - PrintName(); + PrintChangeName(); + IpcTesterHelpers.DrawIntro("Last State Finalization"); + PrintFinalizeName(); IpcTesterHelpers.DrawIntro("Last GPose Change"); ImGui.TextUnformatted($"{_lastGPoseChangeValue} at {_lastGPoseChangeTime.ToLocalTime().TimeOfDay}"); IpcTesterHelpers.DrawIntro(GetState.Label); DrawStatePopup(); - if (ImGui.Button("Get##Idx")) + if (ImUtf8.Button("Get##Idx"u8)) { (_lastError, _state) = new GetState(_pluginInterface).Invoke(_gameObjectIndex, _key); _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateName.Label); - if (ImGui.Button("Get##Name")) + if (ImUtf8.Button("Get##Name"u8)) { (_lastError, _state) = new GetStateName(_pluginInterface).Invoke(_gameObjectName, _key); _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateBase64.Label); - if (ImGui.Button("Get##Base64Idx")) + if (ImUtf8.Button("Get##Base64Idx"u8)) { (_lastError, _getStateString) = new GetStateBase64(_pluginInterface).Invoke(_gameObjectIndex, _key); _stateString = _getStateString ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateBase64Name.Label); - if (ImGui.Button("Get##Base64Idx")) + if (ImUtf8.Button("Get##Base64Idx"u8)) { (_lastError, _getStateString) = new GetStateBase64Name(_pluginInterface).Invoke(_gameObjectName, _key); _stateString = _getStateString ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(ApplyState.Label); if (ImGuiUtil.DrawDisabledButton("Apply Last##Idx", Vector2.Zero, string.Empty, _state == null)) _lastError = new ApplyState(_pluginInterface).Invoke(_state!, _gameObjectIndex, _key, _flags); ImGui.SameLine(); - if (ImGui.Button("Apply Base64##Idx")) + if (ImUtf8.Button("Apply Base64##Idx"u8)) _lastError = new ApplyState(_pluginInterface).Invoke(_base64State, _gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(ApplyStateName.Label); if (ImGuiUtil.DrawDisabledButton("Apply Last##Name", Vector2.Zero, string.Empty, _state == null)) _lastError = new ApplyStateName(_pluginInterface).Invoke(_state!, _gameObjectName, _key, _flags); ImGui.SameLine(); - if (ImGui.Button("Apply Base64##Name")) + if (ImUtf8.Button("Apply Base64##Name"u8)) _lastError = new ApplyStateName(_pluginInterface).Invoke(_base64State, _gameObjectName, _key, _flags); + IpcTesterHelpers.DrawIntro(ReapplyState.Label); + if (ImUtf8.Button("Reapply##Idx"u8)) + _lastError = new ReapplyState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(ReapplyStateName.Label); + if (ImUtf8.Button("Reapply##Name"u8)) + _lastError = new ReapplyStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); + IpcTesterHelpers.DrawIntro(RevertState.Label); - if (ImGui.Button("Revert##Idx")) + if (ImUtf8.Button("Revert##Idx"u8)) _lastError = new RevertState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(RevertStateName.Label); - if (ImGui.Button("Revert##Name")) + if (ImUtf8.Button("Revert##Name"u8)) _lastError = new RevertStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); IpcTesterHelpers.DrawIntro(UnlockState.Label); - if (ImGui.Button("Unlock##Idx")) + if (ImUtf8.Button("Unlock##Idx"u8)) _lastError = new UnlockState(_pluginInterface).Invoke(_gameObjectIndex, _key); IpcTesterHelpers.DrawIntro(UnlockStateName.Label); - if (ImGui.Button("Unlock##Name")) + if (ImUtf8.Button("Unlock##Name"u8)) _lastError = new UnlockStateName(_pluginInterface).Invoke(_gameObjectName, _key); IpcTesterHelpers.DrawIntro(UnlockAll.Label); - if (ImGui.Button("Unlock##All")) + if (ImUtf8.Button("Unlock##All"u8)) _numUnlocked = new UnlockAll(_pluginInterface).Invoke(_key); ImGui.SameLine(); ImGui.TextUnformatted($"Unlocked {_numUnlocked}"); IpcTesterHelpers.DrawIntro(RevertToAutomation.Label); - if (ImGui.Button("Revert##AutomationIdx")) + if (ImUtf8.Button("Revert##AutomationIdx"u8)) _lastError = new RevertToAutomation(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(RevertToAutomationName.Label); - if (ImGui.Button("Revert##AutomationName")) + if (ImUtf8.Button("Revert##AutomationName"u8)) _lastError = new RevertToAutomationName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); } @@ -162,44 +192,76 @@ public class StateIpcTester : IUiService, IDisposable if (_stateString == null) return; - using var p = ImRaii.Popup("State"); + using var p = ImUtf8.Popup("State"u8); if (!p) return; - if (ImGui.Button("Copy to Clipboard")) - ImGui.SetClipboardText(_stateString); + if (ImUtf8.Button("Copy to Clipboard"u8)) + ImUtf8.SetClipboardText(_stateString); if (_stateString[0] is '{') { ImGui.SameLine(); - if (ImGui.Button("Copy as Base64") && _state != null) - ImGui.SetClipboardText(DesignConverter.ToBase64(_state)); + if (ImUtf8.Button("Copy as Base64"u8) && _state != null) + ImUtf8.SetClipboardText(DesignConverter.ToBase64(_state)); } using var font = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(_stateString ?? string.Empty); + ImUtf8.TextWrapped(_stateString ?? string.Empty); - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); } - private unsafe void PrintName() + private unsafe void PrintChangeName() { - ImGuiNative.igTextUnformatted(_lastStateChangeName.Path, _lastStateChangeName.Path + _lastStateChangeName.Length); + ImUtf8.Text(_lastStateChangeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateChangeType})"); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGuiUtil.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); + ImUtf8.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); } ImGui.SameLine(); - ImGui.TextUnformatted($"at {_lastStateChangeTime.ToLocalTime().TimeOfDay}"); + ImUtf8.Text($"at {_lastStateChangeTime.ToLocalTime().TimeOfDay}"); } - private void OnStateChanged(nint actor, StateChangeType _) + private unsafe void PrintFinalizeName() + { + ImUtf8.Text(_lastStateFinalizeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateFinalizeType})"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.CopyOnClickSelectable($"0x{_lastStateFinalizeActor:X}"); + } + + ImGui.SameLine(); + ImUtf8.Text($"at {_lastStateFinalizeTime.ToLocalTime().TimeOfDay}"); + } + + private void OnAutoRedrawChanged(bool value) + { + _lastAutoRedrawChangeValue = value; + _lastAutoRedrawChangeTime = DateTime.UtcNow; + } + + private void OnStateChanged(nint actor, StateChangeType type) { _lastStateChangeActor = actor; _lastStateChangeTime = DateTime.UtcNow; _lastStateChangeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateChangeType = type; + } + + private void OnStateFinalized(nint actor, StateFinalizationType type) + { + _lastStateFinalizeActor = actor; + _lastStateFinalizeTime = DateTime.UtcNow; + _lastStateFinalizeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateFinalizeType = type; } private void OnGPoseChange(bool value) diff --git a/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs index afbb86b..f82bfb3 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs @@ -1,7 +1,7 @@ using Dalamud.Interface.Utility; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; diff --git a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs index c557064..185e19b 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs @@ -2,23 +2,26 @@ using Glamourer.GameData; using Glamourer.Interop; using Glamourer.Interop.Structs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.Gui.Tabs.DebugTab; public unsafe class ModelEvaluationPanel( - ObjectManager _objectManager, + ActorObjectManager _objectManager, VisorService _visorService, + VieraEarService _vieraEarService, UpdateSlotService _updateSlotService, ChangeCustomizeService _changeCustomizeService, - CrestService _crestService) : IGameDataDrawer + CrestService _crestService, + DictBonusItems bonusItems) : IGameDataDrawer { public string Label => "Model Evaluation"; @@ -31,7 +34,7 @@ public unsafe class ModelEvaluationPanel( public void Draw() { ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); - var actor = _objectManager[_gameObjectIndex]; + var actor = _objectManager.Objects[_gameObjectIndex]; var model = actor.Model; using var table = ImRaii.Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); ImGui.TableNextColumn(); @@ -43,17 +46,28 @@ public unsafe class ModelEvaluationPanel( ImGuiUtil.DrawTableColumn("Address"); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(actor.ToString()); + + Glamourer.Dynamis.DrawPointer(actor); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(model.ToString()); + Glamourer.Dynamis.DrawPointer(model); ImGui.TableNextColumn(); if (actor.IsCharacter) { - ImGui.TextUnformatted(actor.AsCharacter->CharacterData.ModelCharaId.ToString()); + ImGui.TextUnformatted(actor.AsCharacter->ModelContainer.ModelCharaId.ToString()); if (actor.AsCharacter->CharacterData.TransformationId != 0) ImGui.TextUnformatted($"Transformation Id: {actor.AsCharacter->CharacterData.TransformationId}"); - if (actor.AsCharacter->CharacterData.ModelCharaId_2 != -1) - ImGui.TextUnformatted($"ModelChara2 {actor.AsCharacter->CharacterData.ModelCharaId_2}"); + if (actor.AsCharacter->ModelContainer.ModelCharaId_2 != -1) + ImGui.TextUnformatted($"ModelChara2 {actor.AsCharacter->ModelContainer.ModelCharaId_2}"); + + ImGuiUtil.DrawTableColumn("Character Mode"); + ImGuiUtil.DrawTableColumn($"{actor.AsCharacter->Mode}"); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Animation"); + ImGuiUtil.DrawTableColumn($"{((ushort*)&actor.AsCharacter->Timeline)[0x78]}"); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } ImGuiUtil.DrawTableColumn("Mainhand"); @@ -71,6 +85,7 @@ public unsafe class ModelEvaluationPanel( ImGuiUtil.CopyOnClickSelectable(offhand.ToString()); DrawVisor(actor, model); + DrawVieraEars(actor, model); DrawHatState(actor, model); DrawWeaponState(actor, model); DrawWetness(actor, model); @@ -87,7 +102,7 @@ public unsafe class ModelEvaluationPanel( : "No CharacterBase"); } - private void DrawParameters(Actor actor, Model model) + private static void DrawParameters(Actor actor, Model model) { if (!model.IsHuman) return; @@ -122,6 +137,26 @@ public unsafe class ModelEvaluationPanel( _visorService.SetVisorState(model, !VisorService.GetVisorState(model)); } + private void DrawVieraEars(Actor actor, Model model) + { + using var id = ImRaii.PushId("Viera Ears"); + ImGuiUtil.DrawTableColumn("Viera Ears"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.ShowVieraEars.ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.VieraEarsVisible.ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Set True")) + _vieraEarService.SetVieraEarState(model, true); + ImGui.SameLine(); + if (ImGui.SmallButton("Set False")) + _vieraEarService.SetVieraEarState(model, false); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _vieraEarService.SetVieraEarState(model, !model.VieraEarsVisible); + } + private void DrawHatState(Actor actor, Model model) { using var id = ImRaii.PushId("HatState"); @@ -137,13 +172,13 @@ public unsafe class ModelEvaluationPanel( return; if (ImGui.SmallButton("Hide")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty); + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, CharacterArmor.Empty); ImGui.SameLine(); if (ImGui.SmallButton("Show")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); ImGui.SameLine(); if (ImGui.SmallButton("Toggle")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); } @@ -177,7 +212,7 @@ public unsafe class ModelEvaluationPanel( { using var id = ImRaii.PushId("Wetness"); ImGuiUtil.DrawTableColumn("Wetness"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.IsGPoseWet ? "GPose" : "None" : "No Character"); var modelString = model.IsCharacterBase ? $"{model.AsCharacterBase->SwimmingWetness:F4} Swimming\n" + $"{model.AsCharacterBase->WeatherWetness:F4} Weather\n" @@ -190,13 +225,13 @@ public unsafe class ModelEvaluationPanel( return; if (ImGui.SmallButton("GPose On")) - actor.AsCharacter->IsGPoseWet = true; + actor.IsGPoseWet = true; ImGui.SameLine(); if (ImGui.SmallButton("GPose Off")) - actor.AsCharacter->IsGPoseWet = false; + actor.IsGPoseWet = false; ImGui.SameLine(); if (ImGui.SmallButton("GPose Toggle")) - actor.AsCharacter->IsGPoseWet = !actor.AsCharacter->IsGPoseWet; + actor.IsGPoseWet = !actor.IsGPoseWet; } private void DrawEquip(Actor actor, Model model) @@ -214,13 +249,39 @@ public unsafe class ModelEvaluationPanel( if (ImGui.SmallButton("Change Piece")) _updateSlotService.UpdateArmor(model, slot, - new CharacterArmor((PrimaryId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0)); + new CharacterArmor((PrimaryId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, StainIds.None)); ImGui.SameLine(); if (ImGui.SmallButton("Change Stain")) - _updateSlotService.UpdateStain(model, slot, 5); + _updateSlotService.UpdateStain(model, slot, new StainIds(5, 7)); ImGui.SameLine(); if (ImGui.SmallButton("Reset")) - _updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot)); + _updateSlotService.UpdateEquipSlot(model, slot, actor.GetArmor(slot)); + } + + foreach (var slot in BonusExtensions.AllFlags) + { + using var id2 = ImRaii.PushId((int)slot.ToModelIndex()); + ImGuiUtil.DrawTableColumn(slot.ToName()); + if (!actor.IsCharacter) + { + ImGuiUtil.DrawTableColumn("No Character"); + } + else + { + var glassesId = actor.GetBonusItem(slot); + if (bonusItems.TryGetValue(glassesId, out var glasses)) + ImGuiUtil.DrawTableColumn($"{glasses.PrimaryId.Id},{glasses.Variant.Id} ({glassesId})"); + else + ImGuiUtil.DrawTableColumn($"{glassesId}"); + } + + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetBonus(slot).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (ImUtf8.SmallButton("Change Piece"u8)) + { + var data = model.GetBonus(slot); + _updateSlotService.UpdateBonusSlot(model, slot, data with { Variant = (Variant)((data.Variant.Id + 1) % 12) }); + } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs index 04537b5..0d93bb8 100644 --- a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs @@ -3,18 +3,18 @@ using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; using Glamourer.GameData; -using Glamourer.Interop; using Glamourer.State; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; using ImGuiClip = OtterGui.ImGuiClip; namespace Glamourer.Gui.Tabs.DebugTab; -public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectManager _objectManager, DesignConverter _designConverter) +public class NpcAppearancePanel(NpcCombo npcCombo, StateManager stateManager, ActorObjectManager objectManager, DesignConverter designConverter) : IGameDataDrawer { public string Label @@ -28,9 +28,9 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM public void Draw() { - ImGui.Checkbox("Compare Customize (or Gear)", ref _customizeOrGear); + ImUtf8.Checkbox("Compare Customize (or Gear)"u8, ref _customizeOrGear); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - var resetScroll = ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64); + var resetScroll = ImUtf8.InputText("##npcFilter"u8, ref _npcFilter, "Filter..."u8); using var table = ImRaii.Table("npcs", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 400 * ImGuiHelpers.GlobalScale)); @@ -40,19 +40,19 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM if (resetScroll) ImGui.SetScrollY(0); - ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300); - ImGui.TableSetupColumn("Kind", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Visor", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Compare", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Button"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Name"u8, ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300); + ImUtf8.TableSetupColumn("Kind"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Model"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Visor"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Compare"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(ImGui.GetFrameHeightWithSpacing()); ImGui.TableNextRow(); var idx = 0; - var remainder = ImGuiClip.FilteredClippedDraw(_npcCombo.Items, skips, + var remainder = ImGuiClip.FilteredClippedDraw(npcCombo.Items, skips, d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), DrawData); ImGui.TableNextColumn(); ImGuiClip.DrawEndDummy(remainder, ImGui.GetFrameHeightWithSpacing()); @@ -61,43 +61,31 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM void DrawData(NpcData data) { using var id = ImRaii.PushId(idx++); - var disabled = !_state.GetOrCreate(_objectManager.Player, out var state); + var disabled = !stateManager.GetOrCreate(objectManager.Player, out var state); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled)) + if (ImUtf8.ButtonEx("Apply"u8, ""u8, Vector2.Zero, disabled)) { - foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true)) - _state.ChangeEquip(state!, slot, item, stain, ApplySettings.Manual); - _state.ChangeMetaState(state!, MetaIndex.VisorState, data.VisorToggled, ApplySettings.Manual); - _state.ChangeEntireCustomize(state!, data.Customize, CustomizeFlagExtensions.All, ApplySettings.Manual); + foreach (var (slot, item, stain) in designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true)) + stateManager.ChangeEquip(state!, slot, item, stain, ApplySettings.Manual); + stateManager.ChangeMetaState(state!, MetaIndex.VisorState, data.VisorToggled, ApplySettings.Manual); + stateManager.ChangeEntireCustomize(state!, data.Customize, CustomizeFlagExtensions.All, ApplySettings.Manual); } - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(data.Name); + ImUtf8.DrawFrameColumn(data.Name); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(data.Kind is ObjectKind.BattleNpc ? "B" : "E"); + ImUtf8.DrawFrameColumn(data.Kind is ObjectKind.BattleNpc ? "B" : "E"); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(data.Id.Id.ToString()); + ImUtf8.DrawFrameColumn(data.Id.Id.ToString()); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(data.ModelId.ToString()); + ImUtf8.DrawFrameColumn(data.ModelId.ToString()); using (_ = ImRaii.PushFont(UiBuilder.IconFont)) { - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(data.VisorToggled ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawFrameColumn(data.VisorToggled ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Times.ToIconString()); } using var mono = ImRaii.PushFont(UiBuilder.MonoFont); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_customizeOrGear ? data.Customize.ToString() : data.WriteGear()); + ImUtf8.DrawFrameColumn(_customizeOrGear ? data.Customize.ToString() : data.WriteGear()); } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs index e519ea5..97847ae 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs @@ -1,13 +1,13 @@ -using Glamourer.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; -using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Actors; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; namespace Glamourer.Gui.Tabs.DebugTab; -public class ObjectManagerPanel(ObjectManager _objectManager, ActorManager _actors) : IGameDataDrawer +public class ObjectManagerPanel(ActorObjectManager _objectManager, ActorManager _actors) : IGameDataDrawer { public string Label => "Object Manager"; @@ -19,44 +19,45 @@ public class ObjectManagerPanel(ObjectManager _objectManager, ActorManager _acto public void Draw() { - _objectManager.Update(); - using (var table = ImRaii.Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + _objectManager.Objects.DrawDebug(); + + using (var table = ImUtf8.Table("##data"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { if (!table) return; - ImGuiUtil.DrawTableColumn("Last Update"); - ImGuiUtil.DrawTableColumn(_objectManager.LastUpdate.ToString(CultureInfo.InvariantCulture)); + ImUtf8.DrawTableColumn("World"u8); + ImUtf8.DrawTableColumn(_actors.Finished ? _actors.Data.ToWorldName(_objectManager.World) : "Service Missing"); + ImUtf8.DrawTableColumn(_objectManager.World.ToString()); + + ImUtf8.DrawTableColumn("Player Character"u8); + ImUtf8.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(_objectManager.Player.ToString()); + + ImUtf8.DrawTableColumn("In GPose"u8); + ImUtf8.DrawTableColumn(_objectManager.IsInGPose.ToString()); ImGui.TableNextColumn(); - ImGuiUtil.DrawTableColumn("World"); - ImGuiUtil.DrawTableColumn(_actors.Finished ? _actors.Data.ToWorldName(_objectManager.World) : "Service Missing"); - ImGuiUtil.DrawTableColumn(_objectManager.World.ToString()); - - ImGuiUtil.DrawTableColumn("Player Character"); - ImGuiUtil.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(_objectManager.Player.ToString()); - - ImGuiUtil.DrawTableColumn("In GPose"); - ImGuiUtil.DrawTableColumn(_objectManager.IsInGPose.ToString()); + ImUtf8.DrawTableColumn("In Lobby"u8); + ImUtf8.DrawTableColumn(_objectManager.IsInLobby.ToString()); ImGui.TableNextColumn(); if (_objectManager.IsInGPose) { - ImGuiUtil.DrawTableColumn("GPose Player"); - ImGuiUtil.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})"); + ImUtf8.DrawTableColumn("GPose Player"u8); + ImUtf8.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})"); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString()); + ImUtf8.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString()); } - ImGuiUtil.DrawTableColumn("Number of Players"); - ImGuiUtil.DrawTableColumn(_objectManager.Count.ToString()); + ImUtf8.DrawTableColumn("Number of Players"u8); + ImUtf8.DrawTableColumn(_objectManager.Count.ToString()); ImGui.TableNextColumn(); } - var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64); - using var table2 = ImRaii.Table("##data2", 3, + var filterChanged = ImUtf8.InputText("##Filter"u8, ref _objectFilter, "Filter..."u8); + using var table2 = ImUtf8.Table("##data2"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing())); if (!table2) @@ -69,13 +70,13 @@ public class ObjectManagerPanel(ObjectManager _objectManager, ActorManager _acto var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); ImGui.TableNextRow(); - var remainder = ImGuiClip.FilteredClippedDraw(_objectManager.Identifiers, skips, + var remainder = ImGuiClip.FilteredClippedDraw(_objectManager, skips, p => p.Value.Label.Contains(_objectFilter, StringComparison.OrdinalIgnoreCase), p => { - ImGuiUtil.DrawTableColumn(p.Key.ToString()); - ImGuiUtil.DrawTableColumn(p.Value.Label); - ImGuiUtil.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString()))); + ImUtf8.DrawTableColumn(p.Key.ToString()); + ImUtf8.DrawTableColumn(p.Value.Label); + ImUtf8.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString()))); }); ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing()); } diff --git a/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs index f1097e8..833ebe4 100644 --- a/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Utility; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.Api.Enums; @@ -49,7 +49,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem ImGui.TableNextColumn(); var address = _drawObject.Address; ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, (nint)(&address), nint.Zero, nint.Zero, "%llx", + if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, ref address, nint.Zero, nint.Zero, "%llx", ImGuiInputTextFlags.CharsHexadecimal)) _drawObject = address; ImGuiUtil.DrawTableColumn(_penumbra.Available @@ -61,7 +61,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0); ImGuiUtil.DrawTableColumn(_penumbra.Available - ? _penumbra.CutsceneParent((ushort) _gameObjectIndex).ToString() + ? _penumbra.CutsceneParent((ushort)_gameObjectIndex).ToString() : "Penumbra Unavailable"); ImGuiUtil.DrawTableColumn("Redraw Object"); @@ -76,7 +76,9 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem } ImGuiUtil.DrawTableColumn("Last Tooltip Date"); - ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue ? _penumbraTooltip.LastTooltip.ToLongTimeString() : "Never"); + ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue + ? $"{_penumbraTooltip.LastTooltip.ToLongTimeString()} ({_penumbraTooltip.LastType} {_penumbraTooltip.LastId})" + : "Never"); ImGui.TableNextColumn(); ImGuiUtil.DrawTableColumn("Last Click Date"); @@ -87,7 +89,13 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem ImGui.Separator(); foreach (var (slot, item) in _penumbraTooltip.LastItems) { - ImGuiUtil.DrawTableColumn($"{slot.ToName()} Revert-Item"); + switch (slot) + { + case EquipSlot e: ImGuiUtil.DrawTableColumn($"{e.ToName()} Revert-Item"); break; + case BonusItemFlag f: ImGuiUtil.DrawTableColumn($"{f.ToName()} Revert-Item"); break; + default: ImGuiUtil.DrawTableColumn("Unk Revert-Item"); break; + } + ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None"); ImGui.TableNextColumn(); } diff --git a/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs index 2abc1db..21f0c50 100644 --- a/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs @@ -3,10 +3,11 @@ using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Raii; using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; namespace Glamourer.Gui.Tabs.DebugTab; -public class RetainedStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer +public class RetainedStatePanel(StateManager _stateManager, ActorObjectManager _objectManager) : IGameDataDrawer { public string Label => "Retained States (Inactive Actors)"; diff --git a/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs index 99c79ed..b22008d 100644 --- a/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs @@ -1,7 +1,7 @@ using Dalamud.Interface.Utility; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs index e59be09..e9fe775 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs @@ -1,5 +1,5 @@ using Glamourer.Designs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -12,13 +12,6 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), MouseWheelType.Control, Glamourer.Log) { - protected override void OnMouseWheel(string preview, ref int current, int steps) - { - if (CurrentSelectionIdx < 0) - CurrentSelectionIdx = Items.IndexOf(preview); - base.OnMouseWheel(preview, ref current, steps); - } - protected override bool DrawSelectable(int globalIdx, bool selected) { var isAutomatic = !_skipAutomatic && globalIdx == 0; diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs index ecac046..8a3dd06 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs @@ -1,11 +1,12 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.DesignTab; @@ -13,6 +14,7 @@ namespace Glamourer.Gui.Tabs.DesignTab; public class DesignDetailTab { private readonly SaveService _saveService; + private readonly Configuration _config; private readonly DesignFileSystemSelector _selector; private readonly DesignFileSystem _fileSystem; private readonly DesignManager _manager; @@ -29,19 +31,20 @@ public class DesignDetailTab private DesignFileSystem.Leaf? _changeLeaf; public DesignDetailTab(SaveService saveService, DesignFileSystemSelector selector, DesignManager manager, DesignFileSystem fileSystem, - DesignColors colors) + DesignColors colors, Configuration config) { _saveService = saveService; _selector = selector; _manager = manager; _fileSystem = fileSystem; _colors = colors; + _config = config; _colorCombo = new DesignColorCombo(_colors, false); } public void Draw() { - using var h = ImRaii.CollapsingHeader("Design Details"); + using var h = DesignPanelFlag.DesignDetails.Header(_config); if (!h) return; @@ -54,19 +57,19 @@ public class DesignDetailTab private void DrawDesignInfoTable() { using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - using var table = ImRaii.Table("Details", 2); + using var table = ImUtf8.Table("Details"u8, 2); if (!table) return; - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); - ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Reset Temporary Settings"u8).X); + ImUtf8.TableSetupColumn("Data"u8, ImGuiTableColumnFlags.WidthStretch); - ImGuiUtil.DrawFrameColumn("Design Name"); + ImUtf8.DrawFrameColumn("Design Name"u8); ImGui.TableNextColumn(); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); var name = _newName ?? _selector.Selected!.Name; ImGui.SetNextItemWidth(width.X); - if (ImGui.InputText("##Name", ref name, 128)) + if (ImUtf8.InputText("##Name"u8, ref name)) { _newName = name; _changeDesign = _selector.Selected; @@ -80,10 +83,10 @@ public class DesignDetailTab } var identifier = _selector.Selected!.Identifier.ToString(); - ImGuiUtil.DrawFrameColumn("Unique Identifier"); + ImUtf8.DrawFrameColumn("Unique Identifier"u8); ImGui.TableNextColumn(); var fileName = _saveService.FileNames.DesignFile(_selector.Selected!); - using (var mono = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(identifier, width)) try @@ -100,14 +103,14 @@ public class DesignDetailTab ImGui.SetClipboardText(identifier); } - ImGuiUtil.HoverTooltip( + ImUtf8.HoverTooltip( $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); - ImGuiUtil.DrawFrameColumn("Full Selector Path"); + ImUtf8.DrawFrameColumn("Full Selector Path"u8); ImGui.TableNextColumn(); var path = _newPath ?? _selector.SelectedLeaf!.FullName(); ImGui.SetNextItemWidth(width.X); - if (ImGui.InputText("##Path", ref path, 1024)) + if (ImUtf8.InputText("##Path"u8, ref path)) { _newPath = path; _changeLeaf = _selector.SelectedLeaf!; @@ -125,25 +128,43 @@ public class DesignDetailTab Glamourer.Messager.NotificationMessage(ex, ex.Message, "Could not rename or move design", NotificationType.Error); } - ImGuiUtil.DrawFrameColumn("Quick Design Bar"); + ImUtf8.DrawFrameColumn("Quick Design Bar"u8); ImGui.TableNextColumn(); - if (ImGui.RadioButton("Display##qdb", _selector.Selected.QuickDesign)) + if (ImUtf8.RadioButton("Display##qdb"u8, _selector.Selected.QuickDesign)) _manager.SetQuickDesign(_selector.Selected!, true); var hovered = ImGui.IsItemHovered(); ImGui.SameLine(); - if (ImGui.RadioButton("Hide##qdb", !_selector.Selected.QuickDesign)) + if (ImUtf8.RadioButton("Hide##qdb"u8, !_selector.Selected.QuickDesign)) _manager.SetQuickDesign(_selector.Selected!, false); if (hovered || ImGui.IsItemHovered()) - ImGui.SetTooltip("Display or hide this design in your quick design bar."); + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text("Display or hide this design in your quick design bar."u8); + } var forceRedraw = _selector.Selected!.ForcedRedraw; - ImGuiUtil.DrawFrameColumn("Force Redrawing"); + ImUtf8.DrawFrameColumn("Force Redrawing"u8); ImGui.TableNextColumn(); - if (ImGui.Checkbox("##ForceRedraw", ref forceRedraw)) + if (ImUtf8.Checkbox("##ForceRedraw"u8, ref forceRedraw)) _manager.ChangeForcedRedraw(_selector.Selected!, forceRedraw); - ImGuiUtil.HoverTooltip("Set this design to always force a redraw when it is applied through any means."); + ImUtf8.HoverTooltip("Set this design to always force a redraw when it is applied through any means."u8); - ImGuiUtil.DrawFrameColumn("Color"); + var resetAdvancedDyes = _selector.Selected!.ResetAdvancedDyes; + ImUtf8.DrawFrameColumn("Reset Advanced Dyes"u8); + ImGui.TableNextColumn(); + if (ImUtf8.Checkbox("##ResetAdvancedDyes"u8, ref resetAdvancedDyes)) + _manager.ChangeResetAdvancedDyes(_selector.Selected!, resetAdvancedDyes); + ImUtf8.HoverTooltip("Set this design to reset any previously applied advanced dyes when it is applied through any means."u8); + + var resetTemporarySettings = _selector.Selected!.ResetTemporarySettings; + ImUtf8.DrawFrameColumn("Reset Temporary Settings"u8); + ImGui.TableNextColumn(); + if (ImUtf8.Checkbox("##ResetTemporarySettings"u8, ref resetTemporarySettings)) + _manager.ChangeResetTemporarySettings(_selector.Selected!, resetTemporarySettings); + ImUtf8.HoverTooltip( + "Set this design to reset any temporary settings previously applied to the associated collection when it is applied through any means."u8); + + ImUtf8.DrawFrameColumn("Color"u8); var colorName = _selector.Selected!.Color.Length == 0 ? DesignColors.AutomaticName : _selector.Selected!.Color; ImGui.TableNextColumn(); if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design.\n" @@ -168,21 +189,18 @@ public class DesignDetailTab else if (_selector.Selected!.Color.Length != 0) { ImGui.SameLine(); - var size = new Vector2(ImGui.GetFrameHeight()); - using var font = ImRaii.PushFont(UiBuilder.IconFont); - ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, 0, _colors.MissingColor); - ImGuiUtil.HoverTooltip("The color associated with this design does not exist."); + ImUtf8.Icon(FontAwesomeIcon.ExclamationCircle, "The color associated with this design does not exist."u8, _colors.MissingColor); } - ImGuiUtil.DrawFrameColumn("Creation Date"); + ImUtf8.DrawFrameColumn("Creation Date"u8); ImGui.TableNextColumn(); ImGuiUtil.DrawTextButton(_selector.Selected!.CreationDate.LocalDateTime.ToString("F"), width, 0); - ImGuiUtil.DrawFrameColumn("Last Update Date"); + ImUtf8.DrawFrameColumn("Last Update Date"u8); ImGui.TableNextColumn(); ImGuiUtil.DrawTextButton(_selector.Selected!.LastEdit.LocalDateTime.ToString("F"), width, 0); - ImGuiUtil.DrawFrameColumn("Tags"); + ImUtf8.DrawFrameColumn("Tags"u8); ImGui.TableNextColumn(); DrawTags(); } @@ -212,18 +230,18 @@ public class DesignDetailTab var size = new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeightWithSpacing()); if (!_editDescriptionMode) { - using (var textBox = ImRaii.ListBox("##desc", size)) + using (var textBox = ImUtf8.ListBox("##desc"u8, size)) { - ImGuiUtil.TextWrapped(desc); + ImUtf8.TextWrapped(desc); } - if (ImGui.Button("Edit Description")) + if (ImUtf8.Button("Edit Description"u8)) _editDescriptionMode = true; } else { var edit = _newDescription ?? desc; - if (ImGui.InputTextMultiline("##desc", ref edit, (uint)Math.Max(2000, 4 * edit.Length), size)) + if (ImUtf8.InputMultiLine("##desc"u8, ref edit, size)) _newDescription = edit; if (ImGui.IsItemDeactivatedAfterEdit()) @@ -232,7 +250,7 @@ public class DesignDetailTab _newDescription = null; } - if (ImGui.Button("Stop Editing")) + if (ImUtf8.Button("Stop Editing"u8)) _editDescriptionMode = false; } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index 2608dd3..e0e4543 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -1,16 +1,18 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; namespace Glamourer.Gui.Tabs.DesignTab; @@ -44,6 +46,29 @@ public sealed class DesignFileSystemSelector : FileSystemSelector _config.Ephemeral.CurrentDesignSelectorWidth * ImUtf8.GlobalScale; + + protected override float MinimumAbsoluteRemainder + => 470 * ImUtf8.GlobalScale; + + protected override float MinimumScaling + => _config.Ephemeral.DesignSelectorMinimumScale; + + protected override float MaximumScaling + => _config.Ephemeral.DesignSelectorMaximumScale; + + protected override void SetSize(Vector2 size) + { + base.SetSize(size); + var adaptedSize = MathF.Round(size.X / ImUtf8.GlobalScale); + if (adaptedSize == _config.Ephemeral.CurrentDesignSelectorWidth) + return; + + _config.Ephemeral.CurrentDesignSelectorWidth = adaptedSize; + _config.Ephemeral.Save(); + } + public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, IKeyState keyState, DesignChanged @event, Configuration config, DesignConverter converter, TabSelected selectionEvent, Logger log, DesignColors designColors, DesignApplier designApplier) @@ -150,7 +175,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector _config.OpenFoldersByDefault; - private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { switch (type) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs index 5a8c41c..d9517c8 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs @@ -3,14 +3,19 @@ using Dalamud.Interface.Utility; using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Designs.Links; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; namespace Glamourer.Gui.Tabs.DesignTab; -public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSelector _selector, LinkDesignCombo _combo, DesignColors _colorManager) : IUiService +public class DesignLinkDrawer( + DesignLinkManager _linkManager, + DesignFileSystemSelector _selector, + LinkDesignCombo _combo, + DesignColors _colorManager, + Configuration config) : IUiService { private int _dragDropIndex = -1; private LinkOrder _dragDropOrder = LinkOrder.None; @@ -19,12 +24,15 @@ public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSe public void Draw() { - using var header = ImRaii.CollapsingHeader("Design Links"); + using var h = DesignPanelFlag.DesignLinks.Header(config); + if (h.Disposed) + return; + ImGuiUtil.HoverTooltip( "Design links are links to other designs that will be applied to characters or during automation according to the rules set.\n" + "They apply from top to bottom just like automated design sets, so anything set by an earlier design will not not be set again by later designs, and order is important.\n" + "If a linked design links to other designs, they will also be applied, so circular links are prohibited. "); - if (!header) + if (!h) return; DrawList(); @@ -187,7 +195,7 @@ public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSe { if (source) { - ImGui.SetDragDropPayload("DraggingLink", IntPtr.Zero, 0); + ImGui.SetDragDropPayload("DraggingLink", null, 0); ImGui.TextUnformatted($"Reordering {design.Name}..."); _dragDropIndex = index; _dragDropOrder = order; diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index bf9ba69..e3c965c 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -1,20 +1,24 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using FFXIVClientStructs.FFXIV.Client.System.Framework; +using Glamourer.Api.Enums; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.GameData; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Gui.Materials; using Glamourer.Interop; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using static Glamourer.Gui.Tabs.HeaderDrawer; namespace Glamourer.Gui.Tabs.DesignTab; @@ -25,7 +29,7 @@ public class DesignPanel private readonly DesignFileSystemSelector _selector; private readonly CustomizationDrawer _customizationDrawer; private readonly DesignManager _manager; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; private readonly StateManager _state; private readonly EquipmentDrawer _equipmentDrawer; private readonly ModAssociationsTab _modAssociations; @@ -37,6 +41,7 @@ public class DesignPanel private readonly CustomizeParameterDrawer _parameterDrawer; private readonly DesignLinkDrawer _designLinkDrawer; private readonly MaterialDrawer _materials; + private readonly EditorHistory _history; private readonly Button[] _leftButtons; private readonly Button[] _rightButtons; @@ -44,7 +49,7 @@ public class DesignPanel public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, - ObjectManager objects, + ActorObjectManager objects, StateManager state, EquipmentDrawer equipmentDrawer, ModAssociationsTab modAssociations, @@ -55,7 +60,8 @@ public class DesignPanel MultiDesignPanel multiDesignPanel, CustomizeParameterDrawer parameterDrawer, DesignLinkDrawer designLinkDrawer, - MaterialDrawer materials) + MaterialDrawer materials, + EditorHistory history) { _selector = selector; _customizationDrawer = customizationDrawer; @@ -72,17 +78,19 @@ public class DesignPanel _parameterDrawer = parameterDrawer; _designLinkDrawer = designLinkDrawer; _materials = materials; + _history = history; _leftButtons = [ new SetFromClipboardButton(this), - new UndoButton(this), + new DesignUndoButton(this), new ExportToClipboardButton(this), new ApplyCharacterButton(this), + new UndoButton(this), ]; _rightButtons = [ new LockButton(this), - new IncognitoButton(_config.Ephemeral), + new IncognitoButton(_config), ]; } @@ -94,7 +102,7 @@ public class DesignPanel private void DrawEquipment() { - using var h = ImRaii.CollapsingHeader("Equipment"); + using var h = DesignPanelFlag.Equipment.Header(_config); if (!h) return; @@ -106,15 +114,23 @@ public class DesignPanel var data = EquipDrawData.FromDesign(_manager, _selector.Selected!, slot); _equipmentDrawer.DrawEquip(data); if (usedAllStain) - _manager.ChangeStain(_selector.Selected, slot, newAllStain); + _manager.ChangeStains(_selector.Selected, slot, newAllStain); } var mainhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.MainHand); var offhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.OffHand); _equipmentDrawer.DrawWeapons(mainhand, offhand, true); + + foreach (var slot in BonusExtensions.AllFlags) + { + var data = BonusDrawData.FromDesign(_manager, _selector.Selected!, slot); + _equipmentDrawer.DrawBonusItem(data); + } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawEquipmentMetaToggles(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + _equipmentDrawer.DrawDragDropTooltip(); } private void DrawEquipmentMetaToggles() @@ -138,18 +154,28 @@ public class DesignPanel EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.WeaponState, _manager, _selector.Selected!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.OffHand, _manager, _selector.Selected!)); } + + ImGui.SameLine(); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.EarState, _manager, _selector.Selected!)); + } } private void DrawCustomize() { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + var header = _selector.Selected!.DesignData.ModelId == 0 ? "Customization" : $"Customization (Model Id #{_selector.Selected!.DesignData.ModelId})###Customization"; - using var h = ImRaii.CollapsingHeader(header); + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); if (!h) return; - if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.ApplyCustomizeRaw, + if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.Application.Customize, _selector.Selected!.WriteProtected(), false)) foreach (var idx in Enum.GetValues()) { @@ -166,10 +192,7 @@ public class DesignPanel private void DrawCustomizeParameters() { - if (!_config.UseAdvancedParameters) - return; - - using var h = ImRaii.CollapsingHeader("Advanced Customizations"); + using var h = DesignPanelFlag.AdvancedCustomizations.Header(_config); if (!h) return; @@ -178,10 +201,7 @@ public class DesignPanel private void DrawMaterialValues() { - if (!_config.UseAdvancedDyes) - return; - - using var h = ImRaii.CollapsingHeader("Advanced Dyes"); + using var h = DesignPanelFlag.AdvancedDyes.Header(_config); if (!h) return; @@ -190,7 +210,7 @@ public class DesignPanel private void DrawCustomizeApplication() { - using var id = ImRaii.PushId("Customizations"); + using var id = ImUtf8.PushId("Customizations"u8); var set = _selector.Selected!.CustomizeSet; var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType; var flags = _selector.Selected!.ApplyCustomizeExcludingBodyType == 0 ? 0 : @@ -205,71 +225,74 @@ public class DesignPanel } var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan); - if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan)) + if (ImUtf8.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan)) _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan); var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender); - if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender)) + if (ImUtf8.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender)) _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender); foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable)) { var apply = _selector.Selected!.DoApplyCustomize(index); - if (ImGui.Checkbox($"Apply {set.Option(index)}", ref apply)) + if (ImUtf8.Checkbox($"Apply {set.Option(index)}", ref apply)) _manager.ChangeApplyCustomize(_selector.Selected!, index, apply); } } private void DrawCrestApplication() { - using var id = ImRaii.PushId("Crests"); - var flags = (uint)_selector.Selected!.ApplyCrest; + using var id = ImUtf8.PushId("Crests"u8); + var flags = (uint)_selector.Selected!.Application.Crest; var bigChange = ImGui.CheckboxFlags("Apply All Crests", ref flags, (uint)CrestExtensions.AllRelevant); foreach (var flag in CrestExtensions.AllRelevantSet) { var apply = bigChange ? ((CrestFlag)flags & flag) == flag : _selector.Selected!.DoApplyCrest(flag); - if (ImGui.Checkbox($"Apply {flag.ToLabel()} Crest", ref apply) || bigChange) + if (ImUtf8.Checkbox($"Apply {flag.ToLabel()} Crest", ref apply) || bigChange) _manager.ChangeApplyCrest(_selector.Selected!, flag, apply); } } private void DrawApplicationRules() { - using var h = ImRaii.CollapsingHeader("Application Rules"); + using var h = DesignPanelFlag.ApplicationRules.Header(_config); if (!h) return; - using (var _ = ImRaii.Group()) + using var disabled = ImRaii.Disabled(_selector.Selected!.WriteProtected()); + + DrawAllButtons(); + + using (var _ = ImUtf8.Group()) { DrawCustomizeApplication(); - ImGui.NewLine(); + ImUtf8.IconDummy(); DrawCrestApplication(); - ImGui.NewLine(); - if (_config.UseAdvancedParameters) - DrawMetaApplication(); + ImUtf8.IconDummy(); + DrawMetaApplication(); } - ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2); + ImGui.SameLine(210 * ImUtf8.GlobalScale + ImGui.GetStyle().ItemSpacing.X); using (var _ = ImRaii.Group()) { void ApplyEquip(string label, EquipFlag allFlags, bool stain, IEnumerable slots) { - var flags = (uint)(allFlags & _selector.Selected!.ApplyEquip); - using var id = ImRaii.PushId(label); + var flags = (uint)(allFlags & _selector.Selected!.Application.Equip); + using var id = ImUtf8.PushId(label); var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)allFlags); if (stain) foreach (var slot in slots) { var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) - _manager.ChangeApplyStain(_selector.Selected!, slot, apply); + if (ImUtf8.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) + _manager.ChangeApplyStains(_selector.Selected!, slot, apply); } else foreach (var slot in slots) { var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) + if (ImUtf8.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) _manager.ChangeApplyItem(_selector.Selected!, slot, apply); } } @@ -280,63 +303,173 @@ public class DesignPanel EquipSlot.OffHand, }); - ImGui.NewLine(); + ImUtf8.IconDummy(); ApplyEquip("Armor", ApplicationTypeExtensions.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); - ImGui.NewLine(); + ImUtf8.IconDummy(); ApplyEquip("Accessories", ApplicationTypeExtensions.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); - ImGui.NewLine(); + ImUtf8.IconDummy(); ApplyEquip("Dyes", ApplicationTypeExtensions.StainFlags, true, EquipSlotExtensions.FullSlots); - ImGui.NewLine(); - if (_config.UseAdvancedParameters) - DrawParameterApplication(); - else - DrawMetaApplication(); + ImUtf8.IconDummy(); + DrawParameterApplication(); + + ImUtf8.IconDummy(); + DrawBonusSlotApplication(); } } + private void DrawAllButtons() + { + var enabled = _config.DeleteDesignModifier.IsActive(); + bool? equip = null; + bool? customize = null; + var size = new Vector2(210 * ImUtf8.GlobalScale, 0); + if (ImUtf8.ButtonEx("Disable Everything"u8, + "Disable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness."u8, size, + !enabled)) + { + equip = false; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Enable Everything"u8, + "Enable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness."u8, size, + !enabled)) + { + equip = true; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Equipment Only"u8, + "Enable application of anything related to gear, disable anything that is not related to gear."u8, size, + !enabled)) + { + equip = true; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Customization Only"u8, + "Enable application of anything related to customization, disable anything that is not related to customization."u8, size, + !enabled)) + { + equip = false; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Default Application"u8, + "Set the application rules to the default values as if the design was newly created, without any advanced features or wetness."u8, + size, + !enabled)) + { + _manager.ChangeApplyMulti(_selector.Selected!, true, true, true, false, true, true, false, true); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.Wetness, false); + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable Advanced"u8, "Disable all advanced dyes and customizations but keep everything else as is."u8, + size, + !enabled)) + _manager.ChangeApplyMulti(_selector.Selected!, null, null, null, false, null, null, false, null); + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (equip is null && customize is null) + return; + + _manager.ChangeApplyMulti(_selector.Selected!, equip, customize, equip, customize.HasValue && !customize.Value ? false : null, null, + equip, equip, equip); + if (equip.HasValue) + { + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.HatState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.VisorState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.WeaponState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.EarState, equip.Value); + } + + if (customize.HasValue) + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.Wetness, customize.Value); + } + + private static readonly IReadOnlyList MetaLabels = + [ + "Apply Wetness", + "Apply Hat Visibility", + "Apply Visor State", + "Apply Weapon Visibility", + "Apply Viera Ear Visibility", + ]; + private void DrawMetaApplication() { - using var id = ImRaii.PushId("Meta"); + using var id = ImUtf8.PushId("Meta"); const uint all = (uint)MetaExtensions.All; - var flags = (uint)_selector.Selected!.ApplyMeta; + var flags = (uint)_selector.Selected!.Application.Meta; var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); - var labels = new[] - { - "Apply Wetness", - "Apply Hat Visibility", - "Apply Visor State", - "Apply Weapon Visibility", - }; - - foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(labels)) + foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(MetaLabels)) { var apply = bigChange ? ((MetaFlag)flags).HasFlag(index.ToFlag()) : _selector.Selected!.DoApplyMeta(index); - if (ImGui.Checkbox(label, ref apply) || bigChange) + if (ImUtf8.Checkbox(label, ref apply) || bigChange) _manager.ChangeApplyMeta(_selector.Selected!, index, apply); } } + private static readonly IReadOnlyList BonusSlotLabels = + [ + "Apply Facewear", + ]; + + private void DrawBonusSlotApplication() + { + using var id = ImUtf8.PushId("Bonus"u8); + var flags = _selector.Selected!.Application.BonusItem; + var bigChange = BonusExtensions.AllFlags.Count > 1 && ImUtf8.Checkbox("Apply All Bonus Slots"u8, ref flags, BonusExtensions.All); + foreach (var (index, label) in BonusExtensions.AllFlags.Zip(BonusSlotLabels)) + { + var apply = bigChange ? flags.HasFlag(index) : _selector.Selected!.DoApplyBonusItem(index); + if (ImUtf8.Checkbox(label, ref apply) || bigChange) + _manager.ChangeApplyBonusItem(_selector.Selected!, index, apply); + } + } + + private void DrawParameterApplication() { - using var id = ImRaii.PushId("Parameter"); - var flags = (uint)_selector.Selected!.ApplyParameters; + using var id = ImUtf8.PushId("Parameter"); + var flags = (uint)_selector.Selected!.Application.Parameters; var bigChange = ImGui.CheckboxFlags("Apply All Customize Parameters", ref flags, (uint)CustomizeParameterExtensions.All); foreach (var flag in CustomizeParameterExtensions.AllFlags) { var apply = bigChange ? ((CustomizeParameterFlag)flags).HasFlag(flag) : _selector.Selected!.DoApplyParameter(flag); - if (ImGui.Checkbox($"Apply {flag.ToName()}", ref apply) || bigChange) + if (ImUtf8.Checkbox($"Apply {flag.ToName()}", ref apply) || bigChange) _manager.ChangeApplyParameter(_selector.Selected!, flag, apply); } } public void Draw() { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); if (_selector.SelectedPaths.Count > 1) { _multiDesignPanel.Draw(); @@ -371,11 +504,19 @@ public class DesignPanel private void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || _selector.Selected == null) + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || _selector.Selected == null) return; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + if (_selector.Selected == null) + return; + + ImGui.Dummy(Vector2.Zero); DrawButtonRow(); + ImGui.TableNextColumn(); + DrawCustomize(); DrawEquipment(); DrawCustomizeParameters(); @@ -408,9 +549,8 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks); + using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } @@ -427,9 +567,8 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks); + using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } @@ -453,7 +592,7 @@ public class DesignPanel } private static unsafe string GetUserPath() - => Framework.Instance()->UserPath; + => Framework.Instance()->UserPathString; private sealed class LockButton(DesignPanel panel) : Button @@ -507,19 +646,19 @@ public class DesignPanel } } - private sealed class UndoButton(DesignPanel panel) : Button + private sealed class DesignUndoButton(DesignPanel panel) : Button { public override bool Visible => panel._selector.Selected != null; protected override bool Disabled - => !panel._manager.CanUndo(panel._selector.Selected); + => !panel._manager.CanUndo(panel._selector.Selected) || (panel._selector.Selected?.WriteProtected() ?? true); protected override string Description - => "Undo the last change if you accidentally overwrote your design with a different one."; + => "Undo the last time you applied an entire design onto this design, if you accidentally overwrote your design with a different one."; protected override FontAwesomeIcon Icon - => FontAwesomeIcon.Undo; + => FontAwesomeIcon.SyncAlt; protected override void OnClick() { @@ -570,6 +709,9 @@ public class DesignPanel protected override string Description => "Overwrite this design with your character's current state."; + protected override bool Disabled + => panel._selector.Selected?.WriteProtected() ?? true; + protected override FontAwesomeIcon Icon => FontAwesomeIcon.UserEdit; @@ -583,6 +725,7 @@ public class DesignPanel var design = panel._converter.Convert(state, ApplicationRules.FromModifiers(state)) ?? throw new Exception("The clipboard did not contain valid data."); + panel._selector.Selected!.GetMaterialDataRef().Clear(); panel._manager.ApplyDesign(panel._selector.Selected!, design); } catch (Exception ex) @@ -592,4 +735,22 @@ public class DesignPanel } } } + + private sealed class UndoButton(DesignPanel panel) : Button + { + protected override string Description + => "Undo the last change."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Undo; + + public override bool Visible + => panel._selector.Selected != null; + + protected override bool Disabled + => (panel._selector.Selected?.WriteProtected() ?? true) || !panel._history.CanUndo(panel._selector.Selected); + + protected override void OnClick() + => panel._history.Undo(panel._selector.Selected!); + } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs index 7fca8c2..1b92291 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs @@ -1,8 +1,8 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Glamourer.Designs; using Glamourer.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using OtterGui.Widgets; @@ -16,7 +16,7 @@ public class DesignTab(DesignFileSystemSelector _selector, DesignPanel _panel, I public void DrawContent() { - _selector.Draw(GetDesignSelectorSize()); + _selector.Draw(); if (_importService.CreateCharaTarget(out var designBase, out var name)) { var newDesign = _manager.CreateClone(designBase, name, true); @@ -27,7 +27,4 @@ public class DesignTab(DesignFileSystemSelector _selector, DesignPanel _panel, I _panel.Draw(); _importService.CreateCharaSource(); } - - public float GetDesignSelectorSize() - => 200f * ImGuiHelpers.GlobalScale; } diff --git a/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs index 1915241..587fe65 100644 --- a/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs @@ -1,24 +1,31 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Dalamud.Utility; using Glamourer.Designs; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Glamourer.State; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.Widget; namespace Glamourer.Gui.Tabs.DesignTab; -public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager) +public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager, Configuration config) { - private readonly ModCombo _modCombo = new(penumbra, Glamourer.Log); + private readonly ModCombo _modCombo = new(penumbra, Glamourer.Log, selector); private (Mod, ModSettings)[]? _copy; public void Draw() { - using var h = ImRaii.CollapsingHeader("Mod Associations"); + using var h = DesignPanelFlag.ModAssociations.Header(config); + if (h.Disposed) + return; + ImGuiUtil.HoverTooltip( "This tab can store information about specific mods associated with this design.\n\n" + "It does NOT change any mod settings automatically, though there is functionality to apply desired mod settings manually.\n" @@ -64,6 +71,8 @@ public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelect private void DrawApplyAllButton() { var (id, name) = penumbra.CurrentCollection; + if (config.Ephemeral.IncognitoMode) + name = id.ShortGuid(); if (ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {name}##applyAll", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, id == Guid.Empty)) ApplyAll(); @@ -81,21 +90,24 @@ public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelect public void ApplyAll() { foreach (var (mod, settings) in selector.Selected!.AssociatedMods) - penumbra.SetMod(mod, settings); + penumbra.SetMod(mod, settings, StateSource.Manual, false); } private void DrawTable() { - using var table = ImRaii.Table("Mods", 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("Mods"u8, config.UseTemporarySettings ? 7 : 6, ImGuiTableFlags.RowBg); if (!table) return; - ImGui.TableSetupColumn("##Buttons", ImGuiTableColumnFlags.WidthFixed, + ImUtf8.TableSetupColumn("##Buttons"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 2); - ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("State").X); - ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Priority").X); - ImGui.TableSetupColumn("##Options", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Applym").X); + ImUtf8.TableSetupColumn("Mod Name"u8, ImGuiTableColumnFlags.WidthStretch); + if (config.UseTemporarySettings) + ImUtf8.TableSetupColumn("Remove"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Remove"u8).X); + ImUtf8.TableSetupColumn("Inherit"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Inherit"u8).X); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("State"u8).X); + ImUtf8.TableSetupColumn("Priority"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Priority"u8).X); + ImUtf8.TableSetupColumn("##Options"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Applym"u8).X); ImGui.TableHeadersRow(); Mod? removedMod = null; @@ -124,37 +136,44 @@ public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelect removedMod = null; updatedMod = null; ImGui.TableNextColumn(); - var buttonSize = new Vector2(ImGui.GetFrameHeight()); - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, - "Delete this mod from associations.", false, true)) - removedMod = mod; + var canDelete = config.DeleteDesignModifier.IsActive(); + if (canDelete) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this mod from associations."u8)) + removedMod = mod; + } + else + { + ImUtf8.IconButton(FontAwesomeIcon.Trash, $"Delete this mod from associations.\nHold {config.DeleteDesignModifier} to delete.", + disabled: true); + } - ImGui.SameLine(0, spacing); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), buttonSize, - "Copy this mod setting to clipboard.", false, true)) + ImUtf8.SameLineInner(); + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Copy this mod setting to clipboard."u8)) _copy = [(mod, settings)]; - ImGui.SameLine(0, spacing); - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.RedoAlt.ToIconString(), buttonSize, - "Update the settings of this mod association.", false, true); + ImUtf8.SameLineInner(); + ImUtf8.IconButton(FontAwesomeIcon.RedoAlt, "Update the settings of this mod association."u8); if (ImGui.IsItemHovered()) { - var newSettings = penumbra.GetModSettings(mod); + var newSettings = penumbra.GetModSettings(mod, out var source); if (ImGui.IsItemClicked()) updatedMod = (mod, newSettings); using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale); - using var tt = ImRaii.Tooltip(); + using var tt = ImUtf8.Tooltip(); + if (source.Length > 0) + ImUtf8.Text($"Using temporary settings made by {source}."); ImGui.Separator(); var namesDifferent = mod.Name != mod.DirectoryName; ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0)); using (ImRaii.Group()) { if (namesDifferent) - ImGui.TextUnformatted("Directory Name"); - ImGui.TextUnformatted("Enabled"); - ImGui.TextUnformatted("Priority"); + ImUtf8.Text("Directory Name"u8); + ImUtf8.Text("Force Inherit"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); ModCombo.DrawSettingsLeft(newSettings); } @@ -162,32 +181,51 @@ public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelect using (ImRaii.Group()) { if (namesDifferent) - ImGui.TextUnformatted(mod.DirectoryName); - ImGui.TextUnformatted(newSettings.Enabled.ToString()); - ImGui.TextUnformatted(newSettings.Priority.ToString()); + ImUtf8.Text(mod.DirectoryName); + + ImUtf8.Text(newSettings.ForceInherit.ToString()); + ImUtf8.Text(newSettings.Enabled.ToString()); + ImUtf8.Text(newSettings.Priority.ToString()); ModCombo.DrawSettingsRight(newSettings); } } ImGui.TableNextColumn(); - - if (ImGui.Selectable($"{mod.Name}##name")) + + if (ImUtf8.Selectable($"{mod.Name}##name")) penumbra.OpenModPage(mod); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Mod Directory: {mod.DirectoryName}\n\nClick to open mod page in Penumbra."); - ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + if (config.UseTemporarySettings) { - ImGuiUtil.Center((settings.Enabled ? FontAwesomeIcon.Check : FontAwesomeIcon.Times).ToIconString()); + ImGui.TableNextColumn(); + var remove = settings.Remove; + if (TwoStateCheckbox.Instance.Draw("##Remove"u8, ref remove)) + updatedMod = (mod, settings with { Remove = remove }); + ImUtf8.HoverTooltip( + "Remove any temporary settings applied by Glamourer instead of applying the configured settings. Only works when using temporary settings, ignored otherwise."u8); } ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(settings.Priority.ToString()); + var inherit = settings.ForceInherit; + if (TwoStateCheckbox.Instance.Draw("##ForceInherit"u8, ref inherit)) + updatedMod = (mod, settings with { ForceInherit = inherit }); + ImUtf8.HoverTooltip("Force the mod to inherit its settings from inherited collections."u8); + ImGui.TableNextColumn(); + var enabled = settings.Enabled; + if (TwoStateCheckbox.Instance.Draw("##Enabled"u8, ref enabled)) + updatedMod = (mod, settings with { Enabled = enabled }); + + ImGui.TableNextColumn(); + var priority = settings.Priority; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + updatedMod = (mod, settings with { Priority = priority }); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton("Apply", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, !penumbra.Available)) { - var text = penumbra.SetMod(mod, settings); + var text = penumbra.SetMod(mod, settings, StateSource.Manual, false); if (text.Length > 0) Glamourer.Messager.NotificationMessage(text, NotificationType.Warning, false); } diff --git a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs index 53501b0..76579c2 100644 --- a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs @@ -1,22 +1,21 @@ using Dalamud.Interface.Utility; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.DesignTab; -public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> +public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings, int Count)> { - public ModCombo(PenumbraService penumbra, Logger log) - : base(penumbra.GetMods, MouseWheelType.None, log) - { - SearchByParts = false; - } + public ModCombo(PenumbraService penumbra, Logger log, DesignFileSystemSelector selector) + : base(() => penumbra.GetMods(selector.Selected?.FilteredItemNames.ToArray() ?? []), MouseWheelType.None, log) + => SearchByParts = false; - protected override string ToString((Mod Mod, ModSettings Settings) obj) + protected override string ToString((Mod Mod, ModSettings Settings, int Count) obj) => obj.Mod.Name; protected override bool IsVisible(int globalIndex, LowerString filter) @@ -24,36 +23,45 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> protected override bool DrawSelectable(int globalIdx, bool selected) { - using var id = ImRaii.PushId(globalIdx); - var (mod, settings) = Items[globalIdx]; + using var id = ImUtf8.PushId(globalIdx); + var (mod, settings, count) = Items[globalIdx]; bool ret; - using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !settings.Enabled)) + var color = settings.Enabled + ? count > 0 + ? ColorId.ContainsItemsEnabled.Value() + : ImGui.GetColorU32(ImGuiCol.Text) + : count > 0 + ? ColorId.ContainsItemsDisabled.Value() + : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using (ImRaii.PushColor(ImGuiCol.Text, color)) { - ret = ImGui.Selectable(mod.Name, selected); + ret = ImUtf8.Selectable(mod.Name, selected); } if (ImGui.IsItemHovered()) { using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale); - using var tt = ImRaii.Tooltip(); + using var tt = ImUtf8.Tooltip(); var namesDifferent = mod.Name != mod.DirectoryName; ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0)); - using (var group = ImRaii.Group()) + using (ImUtf8.Group()) { if (namesDifferent) - ImGui.TextUnformatted("Directory Name"); - ImGui.TextUnformatted("Enabled"); - ImGui.TextUnformatted("Priority"); + ImUtf8.Text("Directory Name"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); + ImUtf8.Text("Affected Design Items"u8); DrawSettingsLeft(settings); } ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale)); - using (var group = ImRaii.Group()) + using (ImUtf8.Group()) { if (namesDifferent) - ImGui.TextUnformatted(mod.DirectoryName); - ImGui.TextUnformatted(settings.Enabled.ToString()); - ImGui.TextUnformatted(settings.Priority.ToString()); + ImUtf8.Text(mod.DirectoryName); + ImUtf8.Text($"{settings.Enabled}"); + ImUtf8.Text($"{settings.Priority}"); + ImUtf8.Text($"{count}"); DrawSettingsRight(settings); } } @@ -65,7 +73,7 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> { foreach (var setting in settings.Settings) { - ImGui.TextUnformatted(setting.Key); + ImUtf8.Text(setting.Key); for (var i = 1; i < setting.Value.Count; ++i) ImGui.NewLine(); } @@ -76,10 +84,10 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> foreach (var setting in settings.Settings) { if (setting.Value.Count == 0) - ImGui.TextUnformatted(""); + ImUtf8.Text(""u8); else foreach (var option in setting.Value) - ImGui.TextUnformatted(option); + ImUtf8.Text(option); } } } diff --git a/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs index 6e5b1b1..a68c191 100644 --- a/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs @@ -1,97 +1,168 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Designs; -using ImGuiNET; -using OtterGui; -using OtterGui.Filesystem; +using Glamourer.Interop.Material; +using Dalamud.Bindings.ImGui; +using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; +using static Glamourer.Gui.Tabs.HeaderDrawer; namespace Glamourer.Gui.Tabs.DesignTab; -public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager _editor, DesignColors _colors) +public class MultiDesignPanel( + DesignFileSystemSelector selector, + DesignManager editor, + DesignColors colors, + Configuration config) { - private readonly DesignColorCombo _colorCombo = new(_colors, true); + private readonly Button[] _leftButtons = []; + private readonly Button[] _rightButtons = [new IncognitoButton(config)]; + + private readonly DesignColorCombo _colorCombo = new(colors, true); public void Draw() { - if (_selector.SelectedPaths.Count == 0) + if (selector.SelectedPaths.Count == 0) return; - var width = ImGuiHelpers.ScaledVector2(145, 0); - ImGui.NewLine(); - DrawDesignList(); + HeaderDrawer.Draw(string.Empty, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons); + using var child = ImUtf8.Child("##MultiPanel"u8, default, true); + if (!child) + return; + + var width = ImGuiHelpers.ScaledVector2(145, 0); + var treeNodePos = ImGui.GetCursorPos(); + _numDesigns = DrawDesignList(); + DrawCounts(treeNodePos); var offset = DrawMultiTagger(width); DrawMultiColor(width, offset); DrawMultiQuickDesignBar(offset); + DrawMultiLock(offset); + DrawMultiResetSettings(offset); + DrawMultiResetDyes(offset); + DrawMultiForceRedraw(offset); + DrawAdvancedButtons(offset); + DrawApplicationButtons(offset); } - private void DrawDesignList() + private void DrawCounts(Vector2 treeNodePos) { - using var tree = ImRaii.TreeNode("Currently Selected Objects", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + var startPos = ImGui.GetCursorPos(); + var numFolders = selector.SelectedPaths.Count - _numDesigns; + var text = (_numDesigns, numFolders) switch + { + (0, 0) => string.Empty, // should not happen + (> 0, 0) => $"{_numDesigns} Designs", + (0, > 0) => $"{numFolders} Folders", + _ => $"{_numDesigns} Designs, {numFolders} Folders", + }; + ImGui.SetCursorPos(treeNodePos); + ImUtf8.TextRightAligned(text); + ImGui.SetCursorPos(startPos); + } + + private void ResetCounts() + { + _numQuickDesignEnabled = 0; + _numDesignsLocked = 0; + _numDesignsForcedRedraw = 0; + _numDesignsResetSettings = 0; + _numDesignsResetDyes = 0; + _numDesignsWithAdvancedDyes = 0; + _numAdvancedDyes = 0; + } + + private bool CountLeaves(DesignFileSystem.IPath path) + { + if (path is not DesignFileSystem.Leaf l) + return false; + + if (l.Value.QuickDesign) + ++_numQuickDesignEnabled; + if (l.Value.WriteProtected()) + ++_numDesignsLocked; + if (l.Value.ResetTemporarySettings) + ++_numDesignsResetSettings; + if (l.Value.ForcedRedraw) + ++_numDesignsForcedRedraw; + if (l.Value.ResetAdvancedDyes) + ++_numDesignsResetDyes; + if (l.Value.Materials.Count > 0) + { + ++_numDesignsWithAdvancedDyes; + _numAdvancedDyes += l.Value.Materials.Count; + } + + return true; + } + + private int DrawDesignList() + { + ResetCounts(); + using var tree = ImUtf8.TreeNode("Currently Selected Objects"u8, ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); ImGui.Separator(); if (!tree) - return; + return selector.SelectedPaths.Count(CountLeaves); - var sizeType = ImGui.GetFrameHeight(); - var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; + var sizeType = new Vector2(ImGui.GetFrameHeight()); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType.X - 4 * ImGui.GetStyle().CellPadding.X) / 100; var sizeMods = availableSizePercent * 35; var sizeFolders = availableSizePercent * 65; - _numQuickDesignEnabled = 0; - _numDesigns = 0; - using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg)) + var numDesigns = 0; + using (var table = ImUtf8.Table("mods"u8, 3, ImGuiTableFlags.RowBg)) { if (!table) - return; + return selector.SelectedPaths.Count(l => l is DesignFileSystem.Leaf); - ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); - ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); + ImUtf8.TableSetupColumn("type"u8, ImGuiTableColumnFlags.WidthFixed, sizeType.X); + ImUtf8.TableSetupColumn("mod"u8, ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImUtf8.TableSetupColumn("path"u8, ImGuiTableColumnFlags.WidthFixed, sizeFolders); var i = 0; - foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) + foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) { using var id = ImRaii.PushId(i++); + var (icon, text) = path is DesignFileSystem.Leaf l + ? (FontAwesomeIcon.FileCircleMinus, l.Value.Name.Text) + : (FontAwesomeIcon.FolderMinus, string.Empty); ImGui.TableNextColumn(); - var icon = (path is DesignFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); - if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiSelection(path); + if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) + selector.RemovePathFromMultiSelection(path); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(path is DesignFileSystem.Leaf l ? l.Value.Name : string.Empty); + ImUtf8.DrawFrameColumn(text); + ImUtf8.DrawFrameColumn(fullName); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(fullName); - - if (path is not DesignFileSystem.Leaf l2) - continue; - - ++_numDesigns; - if (l2.Value.QuickDesign) - ++_numQuickDesignEnabled; + if (CountLeaves(path)) + ++numDesigns; } } ImGui.Separator(); + return numDesigns; } private string _tag = string.Empty; private int _numQuickDesignEnabled; + private int _numDesignsLocked; + private int _numDesignsForcedRedraw; + private int _numDesignsResetSettings; + private int _numDesignsResetDyes; + private int _numAdvancedDyes; + private int _numDesignsWithAdvancedDyes; private int _numDesigns; private readonly List _addDesigns = []; private readonly List<(Design, int)> _removeDesigns = []; private float DrawMultiTagger(Vector2 width) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Multi Tagger:"); + ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImGui.SameLine(); - var offset = ImGui.GetItemRectSize().X; + var offset = ImGui.GetItemRectSize().X + ImGui.GetStyle().WindowPadding.X; ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); - ImGui.InputTextWithHint("##tag", "Tag Name...", ref _tag, 128); + ImUtf8.InputText("##tag"u8, ref _tag, "Tag Name..."u8); UpdateTagCache(); var label = _addDesigns.Count > 0 @@ -103,9 +174,9 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager : $"All designs selected already contain the tag \"{_tag}\"." : $"Add the tag \"{_tag}\" to {_addDesigns.Count} designs as a local tag:\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addDesigns.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _addDesigns.Count == 0)) foreach (var design in _addDesigns) - _editor.AddTag(design, _tag); + editor.AddTag(design, _tag); label = _removeDesigns.Count > 0 ? $"Remove from {_removeDesigns.Count} Designs" @@ -116,41 +187,136 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager : $"No selected design contains the tag \"{_tag}\" locally." : $"Remove the local tag \"{_tag}\" from {_removeDesigns.Count} designs:\n\n\t{string.Join("\n\t", _removeDesigns.Select(m => m.Item1.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeDesigns.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _removeDesigns.Count == 0)) foreach (var (design, index) in _removeDesigns) - _editor.RemoveTag(design, index); + editor.RemoveTag(design, index); ImGui.Separator(); return offset; } private void DrawMultiQuickDesignBar(float offset) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Multi QDB:"); + ImUtf8.TextFrameAligned("Multi QDB:"u8); ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); var diff = _numDesigns - _numQuickDesignEnabled; var tt = diff == 0 ? $"All {_numDesigns} selected designs are already displayed in the quick design bar." : $"Display all {_numDesigns} selected designs in the quick design bar. Changes {diff} designs."; - if (ImGuiUtil.DrawDisabledButton("Display Selected Designs in QDB", buttonWidth, tt, diff == 0)) - foreach(var design in _selector.SelectedPaths.OfType()) - _editor.SetQuickDesign(design.Value, true); + if (ImUtf8.ButtonEx("Display Selected Designs in QDB"u8, tt, buttonWidth, diff == 0)) + { + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetQuickDesign(design.Value, true); + } ImGui.SameLine(); tt = _numQuickDesignEnabled == 0 ? $"All {_numDesigns} selected designs are already hidden in the quick design bar." : $"Hide all {_numDesigns} selected designs in the quick design bar. Changes {_numQuickDesignEnabled} designs."; - if (ImGuiUtil.DrawDisabledButton("Hide Selected Designs in QDB", buttonWidth, tt, _numQuickDesignEnabled == 0)) - foreach (var design in _selector.SelectedPaths.OfType()) - _editor.SetQuickDesign(design.Value, false); + if (ImUtf8.ButtonEx("Hide Selected Designs in QDB"u8, tt, buttonWidth, _numQuickDesignEnabled == 0)) + { + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetQuickDesign(design.Value, false); + } + + ImGui.Separator(); + } + + private void DrawMultiLock(float offset) + { + ImUtf8.TextFrameAligned("Multi Lock:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsLocked; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs are already write protected." + : $"Write-protect all {_numDesigns} designs. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Turn Write-Protected"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetWriteProtection(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs are write-protected." + : $"Remove the write protection of the {_numDesigns} selected designs. Changes {_numDesignsLocked} designs."; + if (ImUtf8.ButtonEx("Remove Write-Protection"u8, tt, buttonWidth, _numDesignsLocked == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetWriteProtection(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiResetSettings(float offset) + { + ImUtf8.TextFrameAligned("Settings:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsResetSettings; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already reset temporary settings." + : $"Make all {_numDesigns} selected designs reset temporary settings. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Set Reset Temp. Settings"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetTemporarySettings(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsResetSettings == 0 + ? $"None of the {_numDesigns} selected designs reset temporary settings." + : $"Stop all {_numDesigns} selected designs from resetting temporary settings. Changes {_numDesignsResetSettings} designs."; + if (ImUtf8.ButtonEx("Remove Reset Temp. Settings"u8, tt, buttonWidth, _numDesignsResetSettings == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetTemporarySettings(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiResetDyes(float offset) + { + ImUtf8.TextFrameAligned("Adv. Dyes:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsResetDyes; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already reset advanced dyes." + : $"Make all {_numDesigns} selected designs reset advanced dyes. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Set Reset Dyes"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetAdvancedDyes(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs reset advanced dyes." + : $"Stop all {_numDesigns} selected designs from resetting advanced dyes. Changes {_numDesignsResetDyes} designs."; + if (ImUtf8.ButtonEx("Remove Reset Dyes"u8, tt, buttonWidth, _numDesignsResetDyes == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetAdvancedDyes(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiForceRedraw(float offset) + { + ImUtf8.TextFrameAligned("Redrawing:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsForcedRedraw; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already force redraws." + : $"Make all {_numDesigns} designs force redraws. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Force Redraws"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeForcedRedraw(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs force redraws." + : $"Stop all {_numDesigns} selected designs from forcing redraws. Changes {_numDesignsForcedRedraw} designs."; + if (ImUtf8.ButtonEx("Remove Forced Redraws"u8, tt, buttonWidth, _numDesignsForcedRedraw == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeForcedRedraw(design.Value, false); ImGui.Separator(); } private void DrawMultiColor(Vector2 width, float offset) { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Multi Colors:"); + ImUtf8.TextFrameAligned("Multi Colors:"u8); ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); _colorCombo.Draw("##color", _colorCombo.CurrentSelection ?? string.Empty, "Select a design color.", ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X), ImGui.GetTextLineHeight()); @@ -168,9 +334,11 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager } : $"Set the color of {_addDesigns.Count} designs to \"{_colorCombo.CurrentSelection}\"\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addDesigns.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _addDesigns.Count == 0)) + { foreach (var design in _addDesigns) - _editor.ChangeColor(design, _colorCombo.CurrentSelection!); + editor.ChangeColor(design, _colorCombo.CurrentSelection!); + } label = _removeDesigns.Count > 0 ? $"Unset {_removeDesigns.Count} Designs" @@ -179,13 +347,140 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager ? "No selected design is set to a non-automatic color." : $"Set {_removeDesigns.Count} designs to use automatic color again:\n\n\t{string.Join("\n\t", _removeDesigns.Select(m => m.Item1.Name.Text))}"; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeDesigns.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _removeDesigns.Count == 0)) + { foreach (var (design, _) in _removeDesigns) - _editor.ChangeColor(design, string.Empty); + editor.ChangeColor(design, string.Empty); + } ImGui.Separator(); } + private void DrawAdvancedButtons(float offset) + { + ImUtf8.TextFrameAligned("Delete Adv."u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var enabled = config.DeleteDesignModifier.IsActive(); + var tt = _numDesignsWithAdvancedDyes is 0 + ? "No selected designs contain any advanced dyes." + : $"Delete {_numAdvancedDyes} advanced dyes from {_numDesignsWithAdvancedDyes} of the selected designs."; + if (ImUtf8.ButtonEx("Delete All Advanced Dyes"u8, tt, new Vector2(ImGui.GetContentRegionAvail().X, 0), + !enabled || _numDesignsWithAdvancedDyes is 0)) + + foreach (var design in selector.SelectedPaths.OfType()) + { + while (design.Value.Materials.Count > 0) + editor.ChangeMaterialValue(design.Value, MaterialValueIndex.FromKey(design.Value.Materials[0].Item1), null); + } + + if (!enabled && _numDesignsWithAdvancedDyes is not 0) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking to delete."); + ImGui.Separator(); + } + + private void DrawApplicationButtons(float offset) + { + ImUtf8.TextFrameAligned("Application"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var width = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var enabled = config.DeleteDesignModifier.IsActive(); + bool? equip = null; + bool? customize = null; + var group = ImUtf8.Group(); + if (ImUtf8.ButtonEx("Disable Everything"u8, + _numDesigns > 0 + ? $"Disable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = false; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Enable Everything"u8, + _numDesigns > 0 + ? $"Enable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = true; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Equipment Only"u8, + _numDesigns > 0 + ? $"Enable application of anything related to gear, disable anything that is not related to gear for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = true; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Customization Only"u8, + _numDesigns > 0 + ? $"Enable application of anything related to customization, disable anything that is not related to customization for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = false; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Default Application"u8, + _numDesigns > 0 + ? $"Set the application rules to the default values as if the {_numDesigns} were newly created,without any advanced features or wetness." + : "No designs selected.", width, !enabled)) + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + { + editor.ChangeApplyMulti(design, true, true, true, false, true, true, false, true); + editor.ChangeApplyMeta(design, MetaIndex.Wetness, false); + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable Advanced"u8, _numDesigns > 0 + ? $"Disable all advanced dyes and customizations but keep everything else as is for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + editor.ChangeApplyMulti(design, null, null, null, false, null, null, false, null); + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + group.Dispose(); + ImGui.Separator(); + if (equip is null && customize is null) + return; + + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + { + editor.ChangeApplyMulti(design, equip, customize, equip, customize.HasValue && !customize.Value ? false : null, null, equip, equip, + equip); + if (equip.HasValue) + { + editor.ChangeApplyMeta(design, MetaIndex.HatState, equip.Value); + editor.ChangeApplyMeta(design, MetaIndex.VisorState, equip.Value); + editor.ChangeApplyMeta(design, MetaIndex.WeaponState, equip.Value); + } + + if (customize.HasValue) + editor.ChangeApplyMeta(design, MetaIndex.Wetness, customize.Value); + } + } + private void UpdateTagCache() { _addDesigns.Clear(); @@ -193,9 +488,9 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager if (_tag.Length == 0) return; - foreach (var leaf in _selector.SelectedPaths.OfType()) + foreach (var leaf in selector.SelectedPaths.OfType()) { - var index = leaf.Value.Tags.IndexOf(_tag); + var index = leaf.Value.Tags.AsEnumerable().IndexOf(_tag); if (index >= 0) _removeDesigns.Add((leaf.Value, index)); else @@ -208,7 +503,7 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager _addDesigns.Clear(); _removeDesigns.Clear(); var selection = _colorCombo.CurrentSelection ?? DesignColors.AutomaticName; - foreach (var leaf in _selector.SelectedPaths.OfType()) + foreach (var leaf in selector.SelectedPaths.OfType()) { if (leaf.Value.Color.Length > 0) _removeDesigns.Add((leaf.Value, 0)); diff --git a/Glamourer/Gui/Tabs/HeaderDrawer.cs b/Glamourer/Gui/Tabs/HeaderDrawer.cs index 0e9237d..cb169ba 100644 --- a/Glamourer/Gui/Tabs/HeaderDrawer.cs +++ b/Glamourer/Gui/Tabs/HeaderDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; @@ -44,22 +44,36 @@ public static class HeaderDrawer } } - public sealed class IncognitoButton(EphemeralConfig config) : Button + public sealed class IncognitoButton(Configuration config) : Button { protected override string Description - => config.IncognitoMode - ? "Toggle incognito mode off." - : "Toggle incognito mode on."; + { + get + { + var hold = config.IncognitoModifier.IsActive(); + return (config.Ephemeral.IncognitoMode, hold) + switch + { + (true, true) => "Toggle incognito mode off.", + (false, true) => "Toggle incognito mode on.", + (true, false) => $"Toggle incognito mode off.\n\nHold {config.IncognitoModifier} while clicking to toggle.", + (false, false) => $"Toggle incognito mode on.\n\nHold {config.IncognitoModifier} while clicking to toggle.", + }; + } + } protected override FontAwesomeIcon Icon - => config.IncognitoMode + => config.Ephemeral.IncognitoMode ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; protected override void OnClick() { - config.IncognitoMode = !config.IncognitoMode; - config.Save(); + if (!config.IncognitoModifier.IsActive()) + return; + + config.Ephemeral.IncognitoMode = !config.Ephemeral.IncognitoMode; + config.Ephemeral.Save(); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs b/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs index 7941c13..1b70e27 100644 --- a/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs +++ b/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs @@ -2,7 +2,7 @@ using Glamourer.Designs; using Glamourer.GameData; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index c08d5c9..29fe7ef 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -1,23 +1,25 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Gui.Tabs.DesignTab; -using Glamourer.Interop; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using static Glamourer.Gui.Tabs.HeaderDrawer; namespace Glamourer.Gui.Tabs.NpcTab; public class NpcPanel { + private readonly Configuration _config; private readonly DesignColorCombo _colorCombo; private string _newName = string.Empty; private DesignBase? _newDesign; @@ -28,10 +30,10 @@ public class NpcPanel private readonly DesignConverter _converter; private readonly DesignManager _designManager; private readonly StateManager _state; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; private readonly DesignColors _colors; - private readonly Button[] _leftButtons; - private readonly Button[] _rightButtons; + private readonly Button[] _leftButtons; + private readonly Button[] _rightButtons; public NpcPanel(NpcSelector selector, LocalNpcAppearanceData favorites, @@ -40,8 +42,9 @@ public class NpcPanel DesignConverter converter, DesignManager designManager, StateManager state, - ObjectManager objects, - DesignColors colors) + ActorObjectManager objects, + DesignColors colors, + Configuration config) { _selector = selector; _favorites = favorites; @@ -52,6 +55,7 @@ public class NpcPanel _state = state; _objects = objects; _colors = colors; + _config = config; _colorCombo = new DesignColorCombo(colors, true); _leftButtons = [ @@ -114,11 +118,16 @@ public class NpcPanel private void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || !_selector.HasSelection) + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || !_selector.HasSelection) return; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + ImGui.Dummy(Vector2.Zero); DrawButtonRow(); + + ImGui.TableNextColumn(); DrawCustomization(); DrawEquipment(); DrawAppearanceInfo(); @@ -133,10 +142,14 @@ public class NpcPanel private void DrawCustomization() { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + var header = _selector.Selection.ModelId == 0 ? "Customization" : $"Customization (Model Id #{_selector.Selection.ModelId})###Customization"; - using var h = ImRaii.CollapsingHeader(header); + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); if (!h) return; @@ -146,7 +159,7 @@ public class NpcPanel private void DrawEquipment() { - using var h = ImRaii.CollapsingHeader("Equipment"); + using var h = DesignPanelFlag.Equipment.Header(_config); if (!h) return; @@ -185,15 +198,15 @@ public class NpcPanel private void DrawApplyToSelf() { var (id, data) = _objects.PlayerData; - if (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero, - "Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations.", - !data.Valid)) + if (!ImUtf8.ButtonEx("Apply to Yourself"u8, + "Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8, + Vector2.Zero, !data.Valid)) return; if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); } } @@ -202,44 +215,44 @@ public class NpcPanel var (id, data) = _objects.TargetData; var tt = id.IsValid ? data.Valid - ? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations." - : "The current target can not be manipulated." - : "No valid target selected."; - if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid)) + ? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8 + : "The current target can not be manipulated."u8 + : "No valid target selected."u8; + if (!ImUtf8.ButtonEx("Apply to Target"u8, tt, Vector2.Zero, !data.Valid)) return; if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); } } private void DrawAppearanceInfo() { - using var h = ImRaii.CollapsingHeader("Appearance Details"); + using var h = DesignPanelFlag.AppearanceDetails.Header(_config); if (!h) return; - using var table = ImRaii.Table("Details", 2); + using var table = ImUtf8.Table("Details"u8, 2); if (!table) return; using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); - ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); + ImUtf8.TableSetupColumn("Data"u8, ImGuiTableColumnFlags.WidthStretch); var selection = _selector.Selection; - CopyButton("NPC Name", selection.Name); - CopyButton("NPC ID", selection.Id.Id.ToString()); + CopyButton("NPC Name"u8, selection.Name); + CopyButton("NPC ID"u8, selection.Id.Id.ToString()); ImGuiUtil.DrawFrameColumn("NPC Type"); ImGui.TableNextColumn(); var width = ImGui.GetContentRegionAvail().X; ImGuiUtil.DrawTextButton(selection.Kind is ObjectKind.BattleNpc ? "Battle NPC" : "Event NPC", new Vector2(width, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); - ImGuiUtil.DrawFrameColumn("Color"); + ImUtf8.DrawFrameColumn("Color"u8); var color = _favorites.GetColor(selection); var colorName = color.Length == 0 ? DesignColors.AutomaticName : color; ImGui.TableNextColumn(); @@ -272,18 +285,18 @@ public class NpcPanel var size = new Vector2(ImGui.GetFrameHeight()); using var font = ImRaii.PushFont(UiBuilder.IconFont); ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, 0, _colors.MissingColor); - ImGuiUtil.HoverTooltip("The color associated with this design does not exist."); + ImUtf8.HoverTooltip("The color associated with this design does not exist."u8); } return; - static void CopyButton(string label, string text) + static void CopyButton(ReadOnlySpan label, string text) { - ImGuiUtil.DrawFrameColumn(label); + ImUtf8.DrawFrameColumn(label); ImGui.TableNextColumn(); - if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) - ImGui.SetClipboardText(text); - ImGuiUtil.HoverTooltip("Click to copy to clipboard."); + if (ImUtf8.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) + ImUtf8.SetClipboardText(text); + ImUtf8.HoverTooltip("Click to copy to clipboard."u8); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs index b3f0cef..8497ab4 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs @@ -1,6 +1,7 @@ using Glamourer.GameData; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using ImGuiClip = OtterGui.ImGuiClip; diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs b/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs index 4efa4c3..318e017 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.NpcTab; diff --git a/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs b/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs index e49b8d1..1dc9331 100644 --- a/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs +++ b/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Glamourer.Services; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Filesystem; using OtterGui.Raii; using OtterGui.Services; @@ -86,7 +86,7 @@ public class CodeDrawer(Configuration config, CodeService codeService, FunModule if (ImUtf8.IconButton(FontAwesomeIcon.Trash, $"Delete this cheat code.{(canDelete ? string.Empty : $"\nHold {config.DeleteDesignModifier} while clicking to delete.")}", - !canDelete)) + disabled: !canDelete)) { action!(false); config.Codes.RemoveAt(i--); diff --git a/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs b/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs index 98ba870..7080b4d 100644 --- a/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs +++ b/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Log; using OtterGui.Raii; diff --git a/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs index d976d28..5c4fec3 100644 --- a/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs +++ b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs @@ -1,19 +1,19 @@ using Dalamud.Interface; using Glamourer.Interop.Penumbra; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.GameData.Actors; -using ObjectManager = Glamourer.Interop.ObjectManager; +using Penumbra.GameData.Interop; namespace Glamourer.Gui.Tabs.SettingsTab; public class CollectionOverrideDrawer( CollectionOverrideService collectionOverrides, Configuration config, - ObjectManager objects, + ActorObjectManager objects, ActorManager actors, PenumbraService penumbra, CollectionCombo combo) : IService @@ -61,7 +61,8 @@ public class CollectionOverrideDrawer( DrawActorIdentifier(idx, actor); ImGui.TableNextColumn(); - if (combo.Draw("##collection", name, $"Select the overriding collection. Current GUID:", ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight())) + if (combo.Draw("##collection", name, $"Select the overriding collection. Current GUID:", ImGui.GetContentRegionAvail().X, + ImGui.GetTextLineHeight())) { var (guid, _, newName) = combo.CurrentSelection; collectionOverrides.ChangeOverride(idx, guid, newName); @@ -69,7 +70,7 @@ public class CollectionOverrideDrawer( if (ImGui.IsItemHovered()) { - using var tt = ImRaii.Tooltip(); + using var tt = ImRaii.Tooltip(); using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImGui.TextUnformatted($" {collection}"); } @@ -102,7 +103,7 @@ public class CollectionOverrideDrawer( return; using var tt2 = ImRaii.Tooltip(); - using var f = ImRaii.PushFont(UiBuilder.MonoFont); + using var f = ImRaii.PushFont(UiBuilder.MonoFont); ImGui.TextUnformatted(collection.ToString()); } @@ -122,7 +123,7 @@ public class CollectionOverrideDrawer( { if (source) { - ImGui.SetDragDropPayload("DraggingOverride", nint.Zero, 0); + ImGui.SetDragDropPayload("DraggingOverride", null, 0); ImGui.TextUnformatted($"Reordering Override #{idx + 1}..."); _dragDropIndex = idx; } @@ -146,7 +147,7 @@ public class CollectionOverrideDrawer( } catch (ActorIdentifierFactory.IdentifierParseError e) { - _exception = e; + _exception = e; _identifiers = []; } diff --git a/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs index 6259f06..e559841 100644 --- a/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs @@ -1,15 +1,20 @@ -using Dalamud.Game.ClientState.Keys; +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; +using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Interop; using Glamourer.Interop.PalettePlus; -using ImGuiNET; +using Glamourer.Interop.Penumbra; +using Glamourer.Services; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.SettingsTab; @@ -18,14 +23,17 @@ public class SettingsTab( Configuration config, DesignFileSystemSelector selector, ContextMenuService contextMenuService, - UiBuilder uiBuilder, + IUiBuilder uiBuilder, GlamourerChangelog changelog, IKeyState keys, DesignColorUi designColorUi, PaletteImport paletteImport, - PalettePlusChecker paletteChecker, CollectionOverrideDrawer overrides, - CodeDrawer codeDrawer) + CodeDrawer codeDrawer, + Glamourer glamourer, + AutoDesignApplier autoDesignApplier, + AutoRedrawChanged autoRedraw, + PcpService pcpService) : ITab { private readonly VirtualKey[] _validKeys = keys.GetValidVirtualKeys().Prepend(VirtualKey.NO_KEY).ToArray(); @@ -35,87 +43,164 @@ public class SettingsTab( public void DrawContent() { - using var child = ImRaii.Child("MainWindowChild"); + using var child = ImUtf8.Child("MainWindowChild"u8, default); if (!child) return; - Checkbox("Enable Auto Designs", - "Enable the application of designs associated to characters in the Automation tab to be applied automatically.", - config.EnableAutoDesigns, v => config.EnableAutoDesigns = v); + Checkbox("Enable Auto Designs"u8, + "Enable the application of designs associated to characters in the Automation tab to be applied automatically."u8, + config.EnableAutoDesigns, v => + { + config.EnableAutoDesigns = v; + autoDesignApplier.OnEnableAutoDesignsChanged(v); + }); + ImGui.NewLine(); ImGui.NewLine(); ImGui.NewLine(); ImGui.NewLine(); - using (var child2 = ImRaii.Child("SettingsChild")) + using (ImUtf8.Child("SettingsChild"u8, default)) { DrawBehaviorSettings(); + DrawDesignDefaultSettings(); DrawInterfaceSettings(); DrawColorSettings(); overrides.Draw(); codeDrawer.Draw(); } - MainWindow.DrawSupportButtons(changelog.Changelog); + MainWindow.DrawSupportButtons(glamourer, changelog.Changelog); + } + + public void DrawPenumbraIntegrationSettings() + { + DrawPenumbraIntegrationSettings1(); + DrawPenumbraIntegrationSettings2(); } private void DrawBehaviorSettings() { - if (!ImGui.CollapsingHeader("Glamourer Behavior")) + if (!ImUtf8.CollapsingHeader("Glamourer Behavior"u8)) return; - Checkbox("Always Apply Entire Weapon for Mainhand", - "When manually applying a mainhand item, will also apply a corresponding offhand and potentially gauntlets for certain fist weapons.", + Checkbox("Always Apply Entire Weapon for Mainhand"u8, + "When manually applying a mainhand item, will also apply a corresponding offhand and potentially gauntlets for certain fist weapons."u8, config.ChangeEntireItem, v => config.ChangeEntireItem = v); - Checkbox("Use Replacement Gear for Gear Unavailable to Your Race or Gender", - "Use different gender- and race-appropriate models as a substitute when detecting certain items not available for a characters current gender and race.", + Checkbox("Use Replacement Gear for Gear Unavailable to Your Race or Gender"u8, + "Use different gender- and race-appropriate models as a substitute when detecting certain items not available for a characters current gender and race."u8, config.UseRestrictedGearProtection, v => config.UseRestrictedGearProtection = v); - Checkbox("Do Not Apply Unobtained Items in Automation", - "Enable this if you want automatically applied designs to only consider items and customizations you have actually unlocked once, and skip those you have not.", + Checkbox("Do Not Apply Unobtained Items in Automation"u8, + "Enable this if you want automatically applied designs to only consider items and customizations you have actually unlocked once, and skip those you have not."u8, config.UnlockedItemMode, v => config.UnlockedItemMode = v); - Checkbox("Respect Manual Changes When Editing Automation", - "Whether changing any currently active automation group will respect manual changes to the character before re-applying the changed automation or not.", + Checkbox("Respect Manual Changes When Editing Automation"u8, + "Whether changing any currently active automation group will respect manual changes to the character before re-applying the changed automation or not."u8, config.RespectManualOnAutomationUpdate, v => config.RespectManualOnAutomationUpdate = v); - Checkbox("Enable Festival Easter-Eggs", - "Glamourer may do some fun things on specific dates. Disable this if you do not want your experience disrupted by this.", + Checkbox("Enable Festival Easter-Eggs"u8, + "Glamourer may do some fun things on specific dates. Disable this if you do not want your experience disrupted by this."u8, config.DisableFestivals == 0, v => config.DisableFestivals = v ? (byte)0 : (byte)2); - Checkbox("Auto-Reload Gear", - "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.", - config.AutoRedrawEquipOnChanges, v => config.AutoRedrawEquipOnChanges = v); - Checkbox("Revert Manual Changes on Zone Change", - "Restores the old behaviour of reverting your character to its game or automation base whenever you change the zone.", + DrawPenumbraIntegrationSettings1(); + Checkbox("Revert Manual Changes on Zone Change"u8, + "Restores the old behaviour of reverting your character to its game or automation base whenever you change the zone."u8, config.RevertManualChangesOnZoneChange, v => config.RevertManualChangesOnZoneChange = v); - Checkbox("Enable Advanced Customization Options", - "Enable the display and editing of advanced customization options like arbitrary colors.", - config.UseAdvancedParameters, paletteChecker.SetAdvancedParameters); PaletteImportButton(); - Checkbox("Enable Advanced Dye Options", - "Enable the display and editing of advanced dyes (color sets) for all equipment", - config.UseAdvancedDyes, v => config.UseAdvancedDyes = v); - Checkbox("Always Apply Associated Mods", - "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n" - + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n" - + "If you enable this setting, you are aware that any resulting misconfiguration is your own fault.", - config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v); + DrawPenumbraIntegrationSettings2(); + Checkbox("Prevent Random Design Repeats"u8, + "When using random designs, prevent the same design from being chosen twice in a row."u8, + config.PreventRandomRepeats, v => config.PreventRandomRepeats = v); ImGui.NewLine(); } + private void DrawPenumbraIntegrationSettings1() + { + Checkbox("Auto-Reload Gear"u8, + "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection."u8, + config.AutoRedrawEquipOnChanges, v => + { + config.AutoRedrawEquipOnChanges = v; + autoRedraw.Invoke(v); + }); + Checkbox("Attach to PCP Handling"u8, + "Add the actor's glamourer state when a PCP is created by Penumbra, and create a design and apply it if possible when a PCP is installed by Penumbra."u8, + config.AttachToPcp, pcpService.Set); + var active = config.DeleteDesignModifier.IsActive(); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Designs"u8, "Deletes all designs tagged with 'PCP' from the design list."u8, disabled: !active)) + pcpService.CleanPcpDesigns(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.DeleteDesignModifier} while clicking."); + } + + private void DrawPenumbraIntegrationSettings2() + { + Checkbox("Always Apply Associated Mods"u8, + "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n"u8 + + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n"u8 + + "If you enable this setting, you are aware that any resulting misconfiguration is your own fault."u8, + config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v); + Checkbox("Use Temporary Mod Settings"u8, + "Apply all settings as temporary settings so they will be reset when Glamourer or the game shut down."u8, + config.UseTemporarySettings, + v => config.UseTemporarySettings = v); + } + + private void DrawDesignDefaultSettings() + { + if (!ImUtf8.CollapsingHeader("Design Defaults")) + return; + + Checkbox("Locked Designs"u8, "Newly created designs will be locked to prevent unintended changes."u8, + config.DefaultDesignSettings.Locked, v => config.DefaultDesignSettings.Locked = v); + Checkbox("Show in Quick Design Bar"u8, "Newly created designs will be shown in the quick design bar by default."u8, + config.DefaultDesignSettings.ShowQuickDesignBar, v => config.DefaultDesignSettings.ShowQuickDesignBar = v); + Checkbox("Reset Advanced Dyes"u8, "Newly created designs will be configured to reset advanced dyes on application by default."u8, + config.DefaultDesignSettings.ResetAdvancedDyes, v => config.DefaultDesignSettings.ResetAdvancedDyes = v); + Checkbox("Always Force Redraw"u8, "Newly created designs will be configured to force character redraws on application by default."u8, + config.DefaultDesignSettings.AlwaysForceRedrawing, v => config.DefaultDesignSettings.AlwaysForceRedrawing = v); + Checkbox("Reset Temporary Settings"u8, + "Newly created designs will be configured to clear all advanced settings applied by Glamourer to the collection by default."u8, + config.DefaultDesignSettings.ResetTemporarySettings, v => config.DefaultDesignSettings.ResetTemporarySettings = v); + + var tmp = config.PcpFolder; + ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) + config.PcpFolder = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder", + "The folder any designs created due to penumbra character packs are moved to on creation.\nLeave blank to import into Root."); + + tmp = config.PcpColor; + ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputText("##pcpColor"u8, ref tmp)) + config.PcpColor = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Design Color", + "The name of the color group any designs created due to penumbra character packs are assigned.\nLeave blank for no specific color assignment."); + } + private void DrawInterfaceSettings() { - if (!ImGui.CollapsingHeader("Interface")) + if (!ImUtf8.CollapsingHeader("Interface"u8)) return; - EphemeralCheckbox("Show Quick Design Bar", - "Show a bar separate from the main window that allows you to quickly apply designs or revert your character and target.", + EphemeralCheckbox("Show Quick Design Bar"u8, + "Show a bar separate from the main window that allows you to quickly apply designs or revert your character and target."u8, config.Ephemeral.ShowDesignQuickBar, v => config.Ephemeral.ShowDesignQuickBar = v); - EphemeralCheckbox("Lock Quick Design Bar", "Prevent the quick design bar from being moved and lock it in place.", + EphemeralCheckbox("Lock Quick Design Bar"u8, "Prevent the quick design bar from being moved and lock it in place."u8, config.Ephemeral.LockDesignQuickBar, v => config.Ephemeral.LockDesignQuickBar = v); if (Widget.ModifiableKeySelector("Hotkey to Toggle Quick Design Bar", "Set a hotkey that opens or closes the quick design bar.", 100 * ImGuiHelpers.GlobalScale, config.ToggleQuickDesignBar, v => config.ToggleQuickDesignBar = v, _validKeys)) config.Save(); - Checkbox("Show Quick Design Bar in Main Window", - "Show the quick design bar in the tab selection part of the main window, too.", + + Checkbox("Show Quick Design Bar in Main Window"u8, + "Show the quick design bar in the tab selection part of the main window, too."u8, config.ShowQuickBarInTabs, v => config.ShowQuickBarInTabs = v); DrawQuickDesignBoxes(); @@ -123,8 +208,8 @@ public class SettingsTab( ImGui.Separator(); ImGui.Dummy(Vector2.Zero); - Checkbox("Enable Game Context Menus", "Whether to show a Try On via Glamourer button on context menus for equippable items.", - config.EnableGameContextMenu, v => + Checkbox("Enable Game Context Menus"u8, "Whether to show a Try On via Glamourer button on context menus for equippable items."u8, + config.EnableGameContextMenu, v => { config.EnableGameContextMenu = v; if (v) @@ -132,116 +217,142 @@ public class SettingsTab( else contextMenuService.Disable(); }); - Checkbox("Show Window when UI is Hidden", "Whether to show Glamourer windows even when the games UI is hidden.", - config.ShowWindowWhenUiHidden, v => + Checkbox("Show Window when UI is Hidden"u8, "Whether to show Glamourer windows even when the games UI is hidden."u8, + config.ShowWindowWhenUiHidden, v => { config.ShowWindowWhenUiHidden = v; uiBuilder.DisableUserUiHide = v; }); - Checkbox("Hide Window in Cutscenes", "Whether the main Glamourer window should automatically be hidden when entering cutscenes or not.", + Checkbox("Hide Window in Cutscenes"u8, + "Whether the main Glamourer window should automatically be hidden when entering cutscenes or not."u8, config.HideWindowInCutscene, v => { config.HideWindowInCutscene = v; uiBuilder.DisableCutsceneUiHide = !v; }); - EphemeralCheckbox("Lock Main Window", "Prevent the main window from being moved and lock it in place.", + EphemeralCheckbox("Lock Main Window"u8, "Prevent the main window from being moved and lock it in place."u8, config.Ephemeral.LockMainWindow, v => config.Ephemeral.LockMainWindow = v); - Checkbox("Open Main Window at Game Start", "Whether the main Glamourer window should be open or closed after launching the game.", - config.OpenWindowAtStart, v => config.OpenWindowAtStart = v); + Checkbox("Open Main Window at Game Start"u8, "Whether the main Glamourer window should be open or closed after launching the game."u8, + config.OpenWindowAtStart, v => config.OpenWindowAtStart = v); ImGui.Dummy(Vector2.Zero); ImGui.Separator(); ImGui.Dummy(Vector2.Zero); - Checkbox("Smaller Equip Display", "Use single-line display without icons and small dye buttons instead of double-line display.", - config.SmallEquip, v => config.SmallEquip = v); + Checkbox("Smaller Equip Display"u8, "Use single-line display without icons and small dye buttons instead of double-line display."u8, + config.SmallEquip, v => config.SmallEquip = v); DrawHeightUnitSettings(); - Checkbox("Show Application Checkboxes", - "Show the application checkboxes in the Customization and Equipment panels of the design tab, instead of only showing them under Application Rules.", + Checkbox("Show Application Checkboxes"u8, + "Show the application checkboxes in the Customization and Equipment panels of the design tab, instead of only showing them under Application Rules."u8, !config.HideApplyCheckmarks, v => config.HideApplyCheckmarks = !v); if (Widget.DoubleModifierSelector("Design Deletion Modifier", "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, config.DeleteDesignModifier, v => config.DeleteDesignModifier = v)) config.Save(); + if (Widget.DoubleModifierSelector("Incognito Modifier", + "A modifier you need to hold while clicking the Incognito button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, + config.IncognitoModifier, v => config.IncognitoModifier = v)) + config.Save(); DrawRenameSettings(); - Checkbox("Auto-Open Design Folders", - "Have design folders open or closed as their default state after launching.", config.OpenFoldersByDefault, + Checkbox("Auto-Open Design Folders"u8, + "Have design folders open or closed as their default state after launching."u8, config.OpenFoldersByDefault, v => config.OpenFoldersByDefault = v); DrawFolderSortType(); + ImGui.NewLine(); + ImUtf8.Text("Show the following panels in their respective tabs:"u8); + ImGui.Dummy(Vector2.Zero); + DesignPanelFlagExtensions.DrawTable("##panelTable"u8, config.HideDesignPanel, config.AutoExpandDesignPanel, v => + { + config.HideDesignPanel = v; + config.Save(); + }, v => + { + config.AutoExpandDesignPanel = v; + config.Save(); + }); + + ImGui.Dummy(Vector2.Zero); ImGui.Separator(); ImGui.Dummy(Vector2.Zero); - Checkbox("Allow Double-Clicking Designs to Apply", - "Tries to apply a design to the current player character When double-clicking it in the design selector.", + Checkbox("Allow Double-Clicking Designs to Apply"u8, + "Tries to apply a design to the current player character When double-clicking it in the design selector."u8, config.AllowDoubleClickToApply, v => config.AllowDoubleClickToApply = v); - Checkbox("Show all Application Rule Checkboxes for Automation", - "Show multiple separate application rule checkboxes for automated designs, instead of a single box for enabling or disabling.", + Checkbox("Show all Application Rule Checkboxes for Automation"u8, + "Show multiple separate application rule checkboxes for automated designs, instead of a single box for enabling or disabling."u8, config.ShowAllAutomatedApplicationRules, v => config.ShowAllAutomatedApplicationRules = v); - Checkbox("Show Unobtained Item Warnings", - "Show information whether you have unlocked all items and customizations in your automated design or not.", + Checkbox("Show Unobtained Item Warnings"u8, + "Show information whether you have unlocked all items and customizations in your automated design or not."u8, config.ShowUnlockedItemWarnings, v => config.ShowUnlockedItemWarnings = v); - if (config.UseAdvancedParameters) + Checkbox("Show Color Display Config"u8, "Show the Color Display configuration options in the Advanced Customization panels."u8, + config.ShowColorConfig, v => config.ShowColorConfig = v); + Checkbox("Show Palette+ Import Button"u8, + "Show the import button that allows you to import Palette+ palettes onto a design in the Advanced Customization options section for designs."u8, + config.ShowPalettePlusImport, v => config.ShowPalettePlusImport = v); + using (ImRaii.PushId(1)) { - Checkbox("Show Color Display Config", "Show the Color Display configuration options in the Advanced Customization panels.", - config.ShowColorConfig, v => config.ShowColorConfig = v); - Checkbox("Show Palette+ Import Button", - "Show the import button that allows you to import Palette+ palettes onto a design in the Advanced Customization options section for designs.", - config.ShowPalettePlusImport, v => config.ShowPalettePlusImport = v); - using var id = ImRaii.PushId(1); PaletteImportButton(); } - if (config.UseAdvancedDyes) - Checkbox("Keep Advanced Dye Window Attached", - "Keeps the advanced dye window expansion attached to the main window, or makes it freely movable.", - config.KeepAdvancedDyesAttached, v => config.KeepAdvancedDyesAttached = v); + Checkbox("Keep Advanced Dye Window Attached"u8, + "Keeps the advanced dye window expansion attached to the main window, or makes it freely movable."u8, + config.KeepAdvancedDyesAttached, v => config.KeepAdvancedDyesAttached = v); - Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use. Not recommended in general.", config.DebugMode, + Checkbox("Debug Mode"u8, "Show the debug tab. Only useful for debugging or advanced use. Not recommended in general."u8, + config.DebugMode, v => config.DebugMode = v); ImGui.NewLine(); } private void DrawQuickDesignBoxes() { - var showAuto = config.EnableAutoDesigns; - var showAdvanced = config.UseAdvancedParameters || config.UseAdvancedDyes; - var numColumns = 7 - (showAuto ? 0 : 2) - (showAdvanced ? 0 : 1); + var showAuto = config.EnableAutoDesigns; + var numColumns = 9 - (showAuto ? 0 : 2) - (config.UseTemporarySettings ? 0 : 1); ImGui.NewLine(); - ImGui.TextUnformatted("Show the Following Buttons in the Quick Design Bar:"); + ImUtf8.Text("Show the Following Buttons in the Quick Design Bar:"u8); ImGui.Dummy(Vector2.Zero); - using var table = ImRaii.Table("##tableQdb", numColumns, + using var table = ImUtf8.Table("##tableQdb"u8, numColumns, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders | ImGuiTableFlags.NoHostExtendX); if (!table) return; - var columns = new[] - { - (" Apply Design ", true, QdbButtons.ApplyDesign), - (" Revert All ", true, QdbButtons.RevertAll), - (" Revert to Auto ", showAuto, QdbButtons.RevertAutomation), - (" Reapply Auto ", showAuto, QdbButtons.ReapplyAutomation), - (" Revert Equip ", true, QdbButtons.RevertEquip), - (" Revert Customize ", true, QdbButtons.RevertCustomize), - (" Revert Advanced ", showAdvanced, QdbButtons.RevertAdvanced), - }; + ReadOnlySpan<(string, bool, QdbButtons)> columns = + [ + ("Apply Design", true, QdbButtons.ApplyDesign), + ("Revert All", true, QdbButtons.RevertAll), + ("Revert to Auto", showAuto, QdbButtons.RevertAutomation), + ("Reapply Auto", showAuto, QdbButtons.ReapplyAutomation), + ("Revert Equip", true, QdbButtons.RevertEquip), + ("Revert Customize", true, QdbButtons.RevertCustomize), + ("Revert Advanced Customization", true, QdbButtons.RevertAdvancedCustomization), + ("Revert Advanced Dyes", true, QdbButtons.RevertAdvancedDyes), + ("Reset Settings", config.UseTemporarySettings, QdbButtons.ResetSettings), + ]; - foreach (var (label, _, _) in columns.Where(t => t.Item2)) + for (var i = 0; i < columns.Length; ++i) { + if (!columns[i].Item2) + continue; + ImGui.TableNextColumn(); - ImGui.TableHeader(label); + ImUtf8.TableHeader(columns[i].Item1); } - foreach (var (_, _, flag) in columns.Where(t => t.Item2)) + for (var i = 0; i < columns.Length; ++i) { - using var id = ImRaii.PushId((int)flag); + if (!columns[i].Item2) + continue; + + var flag = columns[i].Item3; + using var id = ImUtf8.PushId((int)flag); ImGui.TableNextColumn(); var offset = (ImGui.GetContentRegionAvail().X - ImGui.GetFrameHeight()) / 2; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); var value = config.QdbButtons.HasFlag(flag); - if (!ImGui.Checkbox(string.Empty, ref value)) + if (!ImUtf8.Checkbox(""u8, ref value)) continue; var buttons = value ? config.QdbButtons | flag : config.QdbButtons & ~flag; @@ -255,35 +366,35 @@ public class SettingsTab( private void PaletteImportButton() { - if (!config.UseAdvancedParameters || !config.ShowPalettePlusImport) + if (!config.ShowPalettePlusImport) return; ImGui.SameLine(); - if (ImGui.Button("Import Palette+ to Designs")) + if (ImUtf8.Button("Import Palette+ to Designs"u8)) paletteImport.ImportDesigns(); - ImGuiUtil.HoverTooltip( + ImUtf8.HoverTooltip( $"Import all existing Palettes from your Palette+ Config into Designs at PalettePlus/[Name] if these do not exist. Existing Palettes are:\n\n\t - {string.Join("\n\t - ", paletteImport.Data.Keys)}"); } ///

Draw the entire Color subsection. private void DrawColorSettings() { - if (!ImGui.CollapsingHeader("Colors")) + if (!ImUtf8.CollapsingHeader("Colors"u8)) return; - using (var tree = ImRaii.TreeNode("Custom Design Colors")) + using (var tree = ImUtf8.TreeNode("Custom Design Colors"u8)) { if (tree) designColorUi.Draw(); } - using (var tree = ImRaii.TreeNode("Color Settings")) + using (var tree = ImUtf8.TreeNode("Color Settings"u8)) { if (tree) foreach (var color in Enum.GetValues()) { var (defaultColor, name, description) = color.Data(); - var currentColor = config.Colors.TryGetValue(color, out var current) ? current : defaultColor; + var currentColor = config.Colors.GetValueOrDefault(color, defaultColor); if (Widget.ColorPicker(name, description, currentColor, c => config.Colors[color] = c, defaultColor)) config.Save(); } @@ -293,33 +404,33 @@ public class SettingsTab( } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Checkbox(string label, string tooltip, bool current, Action setter) + private void Checkbox(ReadOnlySpan label, ReadOnlySpan tooltip, bool current, Action setter) { - using var id = ImRaii.PushId(label); + using var id = ImUtf8.PushId(label); var tmp = current; - if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + if (ImUtf8.Checkbox(""u8, ref tmp) && tmp != current) { setter(tmp); config.Save(); } ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(label, tooltip); + ImUtf8.LabeledHelpMarker(label, tooltip); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void EphemeralCheckbox(string label, string tooltip, bool current, Action setter) + private void EphemeralCheckbox(ReadOnlySpan label, ReadOnlySpan tooltip, bool current, Action setter) { - using var id = ImRaii.PushId(label); + using var id = ImUtf8.PushId(label); var tmp = current; - if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + if (ImUtf8.Checkbox(""u8, ref tmp) && tmp != current) { setter(tmp); config.Ephemeral.Save(); } ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(label, tooltip); + ImUtf8.LabeledHelpMarker(label, tooltip); } /// Different supported sort modes as a combo. @@ -327,29 +438,29 @@ public class SettingsTab( { var sortMode = config.SortMode; ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + using (var combo = ImUtf8.Combo("##sortMode"u8, sortMode.Name)) { if (combo) foreach (var val in Configuration.Constants.ValidSortModes) { - if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + if (ImUtf8.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) { config.SortMode = val; selector.SetFilterDirty(); config.Save(); } - ImGuiUtil.HoverTooltip(val.Description); + ImUtf8.HoverTooltip(val.Description); } } - ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the designs tab."); + ImUtf8.LabeledHelpMarker("Sort Mode"u8, "Choose the sort mode for the mod selector in the designs tab."u8); } private void DrawRenameSettings() { ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - using (var combo = ImRaii.Combo("##renameSettings", config.ShowRename.GetData().Name)) + using (var combo = ImUtf8.Combo("##renameSettings"u8, config.ShowRename.GetData().Name)) { if (combo) foreach (var value in Enum.GetValues()) @@ -362,7 +473,7 @@ public class SettingsTab( config.Save(); } - ImGuiUtil.HoverTooltip(desc); + ImUtf8.HoverTooltip(desc); } } @@ -371,19 +482,19 @@ public class SettingsTab( "Select which of the two renaming input fields are visible when opening the right-click context menu of a design in the design selector."; ImGuiComponents.HelpMarker(tt); ImGui.SameLine(); - ImGui.TextUnformatted("Rename Fields in Design Context Menu"); - ImGuiUtil.HoverTooltip(tt); + ImUtf8.Text("Rename Fields in Design Context Menu"u8); + ImUtf8.HoverTooltip(tt); } private void DrawHeightUnitSettings() { ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - using (var combo = ImRaii.Combo("##heightUnit", HeightDisplayTypeName(config.HeightDisplayType))) + using (var combo = ImUtf8.Combo("##heightUnit"u8, HeightDisplayTypeName(config.HeightDisplayType))) { if (combo) foreach (var type in Enum.GetValues()) { - if (ImGui.Selectable(HeightDisplayTypeName(type), type == config.HeightDisplayType) && type != config.HeightDisplayType) + if (ImUtf8.Selectable(HeightDisplayTypeName(type), type == config.HeightDisplayType) && type != config.HeightDisplayType) { config.HeightDisplayType = type; config.Save(); @@ -395,18 +506,20 @@ public class SettingsTab( const string tt = "Select how to display the height of characters in real-world units, if at all."; ImGuiComponents.HelpMarker(tt); ImGui.SameLine(); - ImGui.TextUnformatted("Character Height Display Type"); - ImGuiUtil.HoverTooltip(tt); + ImUtf8.Text("Character Height Display Type"u8); + ImUtf8.HoverTooltip(tt); } - private string HeightDisplayTypeName(HeightDisplayType type) + private static ReadOnlySpan HeightDisplayTypeName(HeightDisplayType type) => type switch { - HeightDisplayType.None => "Do Not Display", - HeightDisplayType.Centimetre => "Centimetres (000.0 cm)", - HeightDisplayType.Metre => "Metres (0.00 m)", - HeightDisplayType.Wrong => "Inches (00.0 in)", - HeightDisplayType.WrongFoot => "Feet (0'00'')", - _ => string.Empty, + HeightDisplayType.None => "Do Not Display"u8, + HeightDisplayType.Centimetre => "Centimetres (000.0 cm)"u8, + HeightDisplayType.Metre => "Metres (0.00 m)"u8, + HeightDisplayType.Wrong => "Inches (00.0 in)"u8, + HeightDisplayType.WrongFoot => "Feet (0'00'')"u8, + HeightDisplayType.Corgi => "Corgis (0.0 Corgis)"u8, + HeightDisplayType.OlympicPool => "Olympic-size swimming Pools (0.000 Pools)"u8, + _ => ""u8, }; } diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index aa67fb4..8644aeb 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -2,33 +2,39 @@ using Dalamud.Interface.Utility; using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using ImGuiClip = OtterGui.ImGuiClip; namespace Glamourer.Gui.Tabs.UnlocksTab; -public class UnlockOverview +public class UnlockOverview( + ItemManager items, + CustomizeService customizations, + ItemUnlockManager itemUnlocks, + CustomizeUnlockManager customizeUnlocks, + PenumbraChangedItemTooltip tooltip, + TextureService textures, + CodeService codes, + JobService jobs, + FavoriteManager favorites, + PenumbraService penumbra) { - private readonly ItemManager _items; - private readonly ItemUnlockManager _itemUnlocks; - private readonly CustomizeService _customizations; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly PenumbraChangedItemTooltip _tooltip; - private readonly TextureService _textures; - private readonly CodeService _codes; - private readonly JobService _jobs; - private readonly FavoriteManager _favorites; - private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f); private FullEquipType _selected1 = FullEquipType.Unknown; private SubRace _selected2 = SubRace.Unknown; private Gender _selected3 = Gender.Unknown; + private BonusItemFlag _selected4 = BonusItemFlag.Unknown; + + private uint _favoriteColor; + private uint _moddedColor; private void DrawSelector() { @@ -38,7 +44,7 @@ public class UnlockOverview foreach (var type in Enum.GetValues()) { - if (type.IsOffhandType() || !_items.ItemData.ByType.TryGetValue(type, out var items) || items.Count == 0) + if (type.IsOffhandType() || type.IsBonus() || !items.ItemData.ByType.TryGetValue(type, out var value) || value.Count == 0) continue; if (ImGui.Selectable(type.ToName(), _selected1 == type)) @@ -46,12 +52,21 @@ public class UnlockOverview _selected1 = type; _selected2 = SubRace.Unknown; _selected3 = Gender.Unknown; + _selected4 = BonusItemFlag.Unknown; } } + if (ImGui.Selectable("Bonus Items", _selected4 == BonusItemFlag.Glasses)) + { + _selected1 = FullEquipType.Unknown; + _selected2 = SubRace.Unknown; + _selected3 = Gender.Unknown; + _selected4 = BonusItemFlag.Glasses; + } + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - if (_customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0) + if (customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0) continue; if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", @@ -60,25 +75,11 @@ public class UnlockOverview _selected1 = FullEquipType.Unknown; _selected2 = clan; _selected3 = gender; + _selected4 = BonusItemFlag.Unknown; } } } - public UnlockOverview(ItemManager items, CustomizeService customizations, ItemUnlockManager itemUnlocks, - CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes, - JobService jobs, FavoriteManager favorites) - { - _items = items; - _customizations = customizations; - _itemUnlocks = itemUnlocks; - _customizeUnlocks = customizeUnlocks; - _tooltip = tooltip; - _textures = textures; - _codes = codes; - _jobs = jobs; - _favorites = favorites; - } - public void Draw() { using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.TableBorderStrong)); @@ -93,15 +94,20 @@ public class UnlockOverview if (!child) return; + _moddedColor = ColorId.ModdedItemMarker.Value(); + _favoriteColor = ColorId.FavoriteStarOn.Value(); + if (_selected1 is not FullEquipType.Unknown) DrawItems(); else if (_selected2 is not SubRace.Unknown && _selected3 is not Gender.Unknown) DrawCustomizations(); + else if (_selected4 is not BonusItemFlag.Unknown) + DrawBonusItems(); } private void DrawCustomizations() { - var set = _customizations.Manager.GetSet(_selected2, _selected3); + var set = customizations.Manager.GetSet(_selected2, _selected3); var spacing = IconSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); @@ -111,25 +117,25 @@ public class UnlockOverview var counter = 0; foreach (var customize in set.HairStyles.Concat(set.FacePaints)) { - if (!_customizeUnlocks.Unlockable.TryGetValue(customize, out var unlockData)) + if (!customizeUnlocks.Unlockable.TryGetValue(customize, out var unlockData)) continue; - var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time); - var icon = _customizations.Manager.GetIcon(customize.IconId); + var unlocked = customizeUnlocks.IsUnlocked(customize, out var time); + var icon = customizations.Manager.GetIcon(customize.IconId); + var hasIcon = icon.TryGetWrap(out var wrap, out _); + ImGui.Image(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); - ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, - unlocked || _codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); - - if (_favorites.Contains(_selected3, _selected2, customize.Index, customize.Value)) - ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(), + if (favorites.Contains(_selected3, _selected2, customize.Index, customize.Value)) + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor, 12 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 6 * ImGuiHelpers.GlobalScale); - if (ImGui.IsItemHovered()) + if (hasIcon && ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - var size = new Vector2(icon.Width, icon.Height); + var size = new Vector2(wrap!.Width, wrap.Height); if (size.X >= iconSize.X && size.Y >= iconSize.Y) - ImGui.Image(icon.ImGuiHandle, size); + ImGui.Image(wrap.Handle, size); ImGui.TextUnformatted(unlockData.Name); ImGui.TextUnformatted($"{customize.Index.ToDefaultName()} {customize.Value.Value}"); ImGui.TextUnformatted(unlocked ? $"Unlocked on {time:g}" : "Not unlocked."); @@ -147,24 +153,94 @@ public class UnlockOverview } } + private void DrawBonusItems() + { + var spacing = IconSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + var iconSize = ImGuiHelpers.ScaledVector2(64); + var iconsPerRow = IconsPerRow(iconSize.X, spacing.X); + var numRows = (items.DictBonusItems.Count + iconsPerRow - 1) / iconsPerRow; + var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f) + 1; + + var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y); + var start = skips * iconsPerRow; + var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.DictBonusItems.Count); + var counter = 0; + + foreach (var item in items.DictBonusItems.Values.Skip(start).Take(end - start)) + { + DrawItem(item); + if (counter != iconsPerRow - 1) + { + ImGui.SameLine(); + ++counter; + } + else + { + counter = 0; + } + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + var remainder = numRows - numVisibleRows - skips; + if (remainder > 0) + ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y); + + void DrawItem(EquipItem item) + { + // TODO check unlocks + var unlocked = true; + if (!textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) + return; + + var (icon, size) = (iconHandle.Handle, new Vector2(iconHandle.Width, iconHandle.Height)); + + ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); + if (favorites.Contains(item)) + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor, + 2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale); + + var mods = DrawModdedMarker(item, iconSize); + + // TODO handle clicking + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + if (size.X >= iconSize.X && size.Y >= iconSize.Y) + ImGui.Image(icon, size); + ImUtf8.Text(item.Name); + ImUtf8.Text($"{item.Type.ToName()}"); + ImUtf8.Text($"{item.Id.Id}"); + ImUtf8.Text($"{item.PrimaryId.Id}-{item.Variant.Id}"); + // TODO + ImUtf8.Text("Always Unlocked"u8); // : $"Unlocked on {time:g}" : "Not Unlocked."); + // TODO + //tooltip.CreateTooltip(item, string.Empty, false); + DrawModTooltip(mods); + } + } + } + private void DrawItems() { - if (!_items.ItemData.ByType.TryGetValue(_selected1, out var items)) + if (!items.ItemData.ByType.TryGetValue(_selected1, out var value)) return; var spacing = IconSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); var iconSize = ImGuiHelpers.ScaledVector2(64); var iconsPerRow = IconsPerRow(iconSize.X, spacing.X); - var numRows = (items.Count + iconsPerRow - 1) / iconsPerRow; + var numRows = (value.Count + iconsPerRow - 1) / iconsPerRow; var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f) + 1; var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y); - var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.Count); + var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, value.Count); var counter = 0; for (var idx = skips * iconsPerRow; idx < end; ++idx) { - DrawItem(items[idx]); + DrawItem(value[idx]); if (counter != iconsPerRow - 1) { ImGui.SameLine(); @@ -185,22 +261,25 @@ public class UnlockOverview void DrawItem(EquipItem item) { - var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time); - if (!_textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) + var unlocked = itemUnlocks.IsUnlocked(item.Id, out var time); + if (!textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) return; - var (icon, size) = (iconHandle.ImGuiHandle, new Vector2(iconHandle.Width, iconHandle.Height)); + var (icon, size) = (iconHandle.Handle, new Vector2(iconHandle.Width, iconHandle.Height)); - ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, unlocked || _codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); - if (_favorites.Contains(item)) + ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); + if (favorites.Contains(item)) ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(), 2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale); + var mods = DrawModdedMarker(item, iconSize); + if (ImGui.IsItemClicked()) Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); - if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) - _tooltip.ApplyItem(state, item); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && tooltip.Player(out var state)) + tooltip.ApplyItem(state, item); if (ImGui.IsItemHovered()) { @@ -212,7 +291,7 @@ public class UnlockOverview ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})"); if (item.Type.ValidOffhand().IsOffhandType()) ImGui.TextUnformatted( - $"{item.Weapon()}{(_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); + $"{item.Weapon()}{(items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); else ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}"); ImGui.TextUnformatted( @@ -220,26 +299,27 @@ public class UnlockOverview if (item.Level.Value <= 1) { - if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= _jobs.AllJobGroups.Count) + if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= jobs.AllJobGroups.Count) ImGui.TextUnformatted("For Everyone"); else - ImGui.TextUnformatted($"For all {_jobs.AllJobGroups[item.JobRestrictions.Id].Name}"); + ImGui.TextUnformatted($"For all {jobs.AllJobGroups[item.JobRestrictions.Id].Name}"); } else { - if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= _jobs.AllJobGroups.Count) + if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= jobs.AllJobGroups.Count) ImGui.TextUnformatted($"For Everyone of at least Level {item.Level}"); else - ImGui.TextUnformatted($"For all {_jobs.AllJobGroups[item.JobRestrictions.Id].Name} of at least Level {item.Level}"); + ImGui.TextUnformatted($"For all {jobs.AllJobGroups[item.JobRestrictions.Id].Name} of at least Level {item.Level}"); } - if (item.Flags.HasFlag(ItemFlags.IsDyable)) - ImGui.TextUnformatted("Dyable"); + if (item.Flags.HasFlag(ItemFlags.IsDyable1)) + ImGui.TextUnformatted(item.Flags.HasFlag(ItemFlags.IsDyable2) ? "Dyable (2 Slots)" : "Dyable"); if (item.Flags.HasFlag(ItemFlags.IsTradable)) ImGui.TextUnformatted("Tradable"); if (item.Flags.HasFlag(ItemFlags.IsCrestWorthy)) ImGui.TextUnformatted("Can apply Crest"); - _tooltip.CreateTooltip(item, string.Empty, false); + DrawModTooltip(mods); + tooltip.CreateTooltip(item, string.Empty, false); } } } @@ -249,4 +329,36 @@ public class UnlockOverview private static int IconsPerRow(float iconWidth, float iconSpacing) => (int)(ImGui.GetContentRegionAvail().X / (iconWidth + iconSpacing)); + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private (string ModDirectory, string ModName)[] DrawModdedMarker(in EquipItem item, Vector2 iconSize) + { + var mods = penumbra.CheckCurrentChangedItem(item.Name); + if (mods.Length == 0) + return mods; + + var center = ImGui.GetItemRectMin() + new Vector2(iconSize.X * 0.85f, iconSize.Y * 0.15f); + ImGui.GetWindowDrawList().AddCircleFilled(center, iconSize.X * 0.1f, _moddedColor); + ImGui.GetWindowDrawList().AddCircle(center, iconSize.X * 0.1f, 0xFF000000); + return mods; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private void DrawModTooltip((string ModDirectory, string ModName)[] mods) + { + switch (mods.Length) + { + case 0: return; + case 1: + ImUtf8.Text("Modded by: "u8, _moddedColor); + ImGui.SameLine(0, 0); + ImUtf8.Text(mods[0].ModName); + return; + default: + ImUtf8.Text("Modded by:"u8, _moddedColor); + foreach (var (_, mod) in mods) + ImUtf8.BulletText(mod); + return; + } + } } diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index d4fd4b0..2323ca2 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -1,13 +1,16 @@ using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Events; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Table; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -15,33 +18,63 @@ namespace Glamourer.Gui.Tabs.UnlocksTab; public class UnlockTable : Table, IDisposable { - private readonly ObjectUnlocked _event; + private readonly ObjectUnlocked _event; + private readonly PenumbraService _penumbra; + + private Guid _lastCurrentCollection = Guid.Empty; public UnlockTable(ItemManager items, TextureService textures, ItemUnlockManager itemUnlocks, - PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites) + PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites, PenumbraService penumbra) : base("ItemUnlockTable", new ItemList(items), new FavoriteColumn(favorites, @event) { Label = "F" }, + new ModdedColumn(penumbra) { Label = "M" }, new NameColumn(textures, tooltip) { Label = "Item Name..." }, - new SlotColumn() { Label = "Equip Slot" }, - new TypeColumn() { Label = "Item Type..." }, + new SlotColumn { Label = "Equip Slot" }, + new TypeColumn { Label = "Item Type..." }, new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" }, - new ItemIdColumn() { Label = "Item Id..." }, + new ItemIdColumn { Label = "Item Id..." }, new ModelDataColumn(items) { Label = "Model Data..." }, new JobColumn(jobs) { Label = "Jobs" }, - new RequiredLevelColumn() { Label = "Level..." }, - new DyableColumn() { Label = "Dye" }, - new CrestColumn() { Label = "Crest" }, - new TradableColumn() { Label = "Trade" } + new RequiredLevelColumn { Label = "Level..." }, + new DyableColumn { Label = "Dye" }, + new CrestColumn { Label = "Crest" }, + new TradableColumn { Label = "Trade" } ) { - _event = @event; - Sortable = true; - Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable; + _event = @event; + _penumbra = penumbra; + Sortable = true; + Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable; _event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable); + _penumbra.ModSettingChanged += OnModSettingsChanged; + + } + + private void OnModSettingsChanged(Penumbra.Api.Enums.ModSettingChange type, Guid collection, string mod, bool inherited) + { + if (collection != _lastCurrentCollection) + return; + + FilterDirty = true; + SortDirty = true; + } + + protected override void PreDraw() + { + var lastCurrentCollection = _penumbra.CurrentCollection.Id; + if (_lastCurrentCollection != lastCurrentCollection) + { + _lastCurrentCollection = lastCurrentCollection; + FilterDirty = true; + SortDirty = true; + } } public void Dispose() - => _event.Unsubscribe(OnObjectUnlock); + { + _event.Unsubscribe(OnObjectUnlock); + _penumbra.ModSettingChanged -= OnModSettingsChanged; + } private sealed class FavoriteColumn : YesNoColumn { @@ -75,6 +108,66 @@ public class UnlockTable : Table, IDisposable => _favorites.Contains(rhs).CompareTo(_favorites.Contains(lhs)); } + private sealed class ModdedColumn : YesNoColumn + { + public override float Width + => ImGui.GetFrameHeightWithSpacing(); + + private readonly PenumbraService _penumbra; + private readonly Dictionary _compareCache = []; + + public ModdedColumn(PenumbraService penumbra) + { + _penumbra = penumbra; + Flags |= ImGuiTableColumnFlags.NoResize; + } + + public override void PostSort() + { + _compareCache.Clear(); + } + + public override void DrawColumn(EquipItem item, int idx) + { + var value = _penumbra.CheckCurrentChangedItem(item.Name); + if (value.Length == 0) + return; + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ModdedItemMarker.Value()); + ImGuiUtil.Center(FontAwesomeIcon.Circle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var (_, mod) in value) + ImUtf8.BulletText(mod); + } + } + + public override bool FilterFunc(EquipItem item) + => FilterValue.HasFlag(_penumbra.CheckCurrentChangedItem(item.Name).Length > 0 ? YesNoFlag.Yes : YesNoFlag.No); + + public override int Compare(EquipItem lhs, EquipItem rhs) + { + if (!_compareCache.TryGetValue(lhs.Id, out var lhsCount)) + { + lhsCount = _penumbra.CheckCurrentChangedItem(lhs.Name).Length; + _compareCache[lhs.Id] = lhsCount; + } + + if (!_compareCache.TryGetValue(rhs.Id, out var rhsCount)) + { + rhsCount = _penumbra.CheckCurrentChangedItem(rhs.Name).Length; + _compareCache[rhs.Id] = rhsCount; + } + + return lhsCount.CompareTo(rhsCount); + } + } + private sealed class NameColumn : ColumnString { private readonly TextureService _textures; @@ -101,7 +194,7 @@ public class UnlockTable : Table, IDisposable ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - if (ImGui.Selectable(item.Name)) + if (ImGui.Selectable(item.Name) && !item.Id.IsBonusItem) Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) @@ -249,7 +342,7 @@ public class UnlockTable : Table, IDisposable => 70 * ImGuiHelpers.GlobalScale; public override int ToValue(EquipItem item) - => (int) item.Id.Id; + => (int)item.Id.Id; public ItemIdColumn() : base(ComparisonMethod.Equal) @@ -315,7 +408,6 @@ public class UnlockTable : Table, IDisposable { } } - private sealed class JobColumn : ColumnFlags { public override float Width @@ -333,11 +425,46 @@ public class UnlockTable : Table, IDisposable public JobColumn(JobService jobs) { _jobs = jobs; - _values = _jobs.Jobs.Values.Skip(1).Select(j => j.Flag).ToArray(); - _names = _jobs.Jobs.Values.Skip(1).Select(j => j.Abbreviation).ToArray(); + _values = _jobs.Jobs.Ordered.Select(j => j.Flag).ToArray(); + _names = _jobs.Jobs.Ordered.Select(j => j.Abbreviation).ToArray(); AllFlags = _values.Aggregate((l, r) => l | r); _filterValue = AllFlags; Flags &= ~ImGuiTableColumnFlags.NoResize; + ComboFlags |= ImGuiComboFlags.HeightLargest; + } + + protected override bool DrawCheckbox(int idx, out bool ret) + { + var job = _jobs.Jobs.Ordered[idx]; + var color = job.Role switch + { + Job.JobRole.Tank => 0xFFFFD0D0, + Job.JobRole.Melee => 0xFFD0D0FF, + Job.JobRole.RangedPhysical => 0xFFD0FFFF, + Job.JobRole.RangedMagical => 0xFFFFD0FF, + Job.JobRole.Healer => 0xFFD0FFD0, + Job.JobRole.Crafter => 0xFF808080, + Job.JobRole.Gatherer => 0xFFD0D0D0, + _ => ImGui.GetColorU32(ImGuiCol.Text), + }; + bool r; + using (ImRaii.PushColor(ImGuiCol.Text, color)) + { + r = base.DrawCheckbox(idx, out ret); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _filterValue = job.Flag & _filterValue; + ret = true; + r = true; + } + + ImUtf8.HoverTooltip("Right-Click to disable all other jobs."u8); + + if (idx < _names.Length - 1 && idx % 2 == 0) + ImGui.SameLine(ImGui.GetFrameHeight() * 4); + return r; } protected override void SetValue(JobFlag value, bool enable) @@ -378,13 +505,67 @@ public class UnlockTable : Table, IDisposable } } - private sealed class DyableColumn : YesNoColumn + private sealed class DyableColumn : ColumnFlags { - public DyableColumn() - => Tooltip = "Whether the item is dyable."; + [Flags] + public enum Dyable : byte + { + No = 1, + Yes = 2, + Two = 4, + } - protected override bool GetValue(EquipItem item) - => item.Flags.HasFlag(ItemFlags.IsDyable); + private Dyable _filterValue; + + public DyableColumn() + { + AllFlags = Dyable.No | Dyable.Yes | Dyable.Two; + Flags &= ~ImGuiTableColumnFlags.NoResize; + _filterValue = AllFlags; + } + + public override Dyable FilterValue + => _filterValue; + + protected override void SetValue(Dyable value, bool enable) + => _filterValue = enable ? _filterValue | value : _filterValue & ~value; + + public override float Width + => ImGui.GetFrameHeight() * 2; + + public override bool FilterFunc(EquipItem item) + => GetValue(item) switch + { + 0 => _filterValue.HasFlag(Dyable.No), + ItemFlags.IsDyable2 => _filterValue.HasFlag(Dyable.Yes), + ItemFlags.IsDyable1 => _filterValue.HasFlag(Dyable.Yes), + _ => _filterValue.HasFlag(Dyable.Two), + }; + + public override int Compare(EquipItem lhs, EquipItem rhs) + => GetValue(lhs).CompareTo(GetValue(rhs)); + + public override void DrawColumn(EquipItem item, int idx) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.Center(Icon(item)); + } + + ImGuiUtil.HoverTooltip("Whether the item is dyable, and how many slots it has."); + } + + private static string Icon(EquipItem item) + => GetValue(item) switch + { + 0 => FontAwesomeIcon.Times.ToIconString(), + ItemFlags.IsDyable2 => FontAwesomeIcon.Check.ToIconString(), + ItemFlags.IsDyable1 => FontAwesomeIcon.Check.ToIconString(), + _ => FontAwesomeIcon.DiceTwo.ToIconString(), + }; + + private static ItemFlags GetValue(EquipItem item) + => item.Flags & (ItemFlags.IsDyable1 | ItemFlags.IsDyable2); } private sealed class TradableColumn : YesNoColumn diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs index 1c82add..661b2a4 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Widgets; @@ -20,7 +20,8 @@ public class UnlocksTab : Window, ITab _overview = overview; _table = table; - IsOpen = false; + Flags |= ImGuiWindowFlags.NoDocking; + IsOpen = false; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(700, 675), diff --git a/Glamourer/Gui/ToggleDrawData.cs b/Glamourer/Gui/ToggleDrawData.cs index 75edc72..28afc2c 100644 --- a/Glamourer/Gui/ToggleDrawData.cs +++ b/Glamourer/Gui/ToggleDrawData.cs @@ -1,4 +1,5 @@ -using Glamourer.Designs; +using Glamourer.Api.Enums; +using Glamourer.Designs; using Glamourer.State; using Penumbra.GameData.Enums; diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index 7e22ff1..94ddb06 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -1,9 +1,8 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using Glamourer.GameData; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Misc; using OtterGui; using OtterGui.Raii; @@ -30,8 +29,30 @@ public static class UiHelpers if (empty) { var (bgColor, tint) = isEmpty - ? (ImGui.GetColorU32(ImGuiCol.FrameBg), new Vector4(0.1f, 0.1f, 0.1f, 0.5f)) - : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 0.8f)); + ? (ImGui.GetColorU32(ImGuiCol.FrameBg), Vector4.One) + : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 1f)); + var pos = ImGui.GetCursorScreenPos(); + ImGui.GetWindowDrawList().AddRectFilled(pos, pos + size, bgColor, 5 * ImGuiHelpers.GlobalScale); + if (ptr != nint.Zero) + ImGui.Image(ptr, size, Vector2.Zero, Vector2.One, tint); + else + ImGui.Dummy(size); + } + else + { + ImGuiUtil.HoverIcon(ptr, textureSize, size); + } + } + + public static void DrawIcon(this EquipItem item, TextureService textures, Vector2 size, BonusItemFlag slot) + { + var isEmpty = item.PrimaryId.Id == 0; + var (ptr, textureSize, empty) = textures.GetIcon(item, slot); + if (empty) + { + var (bgColor, tint) = isEmpty + ? (ImGui.GetColorU32(ImGuiCol.FrameBg), Vector4.One) + : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 1f)); var pos = ImGui.GetCursorScreenPos(); ImGui.GetWindowDrawList().AddRectFilled(pos, pos + size, bgColor, 5 * ImGuiHelpers.GlobalScale); if (ptr != nint.Zero) @@ -98,15 +119,6 @@ public static class UiHelpers return (currentValue != newValue, currentApply != newApply); } - public static (EquipFlag, CustomizeFlag, CrestFlag, CustomizeParameterFlag) ConvertKeysToFlags() - => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch - { - (false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, CustomizeParameterExtensions.All), - (true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, CustomizeParameterExtensions.All), - (true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0, CrestExtensions.All, 0), - (false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant, 0, CustomizeParameterExtensions.All), - }; - public static (bool, bool) ConvertKeysToBool() => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch { @@ -126,16 +138,15 @@ public static class UiHelpers using var c = ImRaii.PushColor(ImGuiCol.Text, hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value()); ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString()); - if (ImGui.IsItemClicked()) - { - if (favorite) - favorites.Remove(item); - else - favorites.TryAdd(item); - return true; - } + if (!ImGui.IsItemClicked()) + return false; + + if (favorite) + favorites.Remove(item); + else + favorites.TryAdd(item); + return true; - return false; } public static bool DrawFavoriteStar(FavoriteManager favorites, StainId stain) @@ -149,15 +160,14 @@ public static class UiHelpers hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value()); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString()); - if (ImGui.IsItemClicked()) - { - if (favorite) - favorites.Remove(stain); - else - favorites.TryAdd(stain); - return true; - } + if (!ImGui.IsItemClicked()) + return false; + + if (favorite) + favorites.Remove(stain); + else + favorites.TryAdd(stain); + return true; - return false; } -} +} \ No newline at end of file diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index cfff90f..495d69c 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -17,7 +17,7 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 _original; + private readonly delegate* unmanaged _original; private readonly Post _postEvent = new(); diff --git a/Glamourer/Interop/CharaFile/CharaFile.cs b/Glamourer/Interop/CharaFile/CharaFile.cs index 0613fb3..aabac2d 100644 --- a/Glamourer/Interop/CharaFile/CharaFile.cs +++ b/Glamourer/Interop/CharaFile/CharaFile.cs @@ -13,6 +13,7 @@ public sealed class CharaFile public DesignData Data = new(); public CustomizeFlag ApplyCustomize; public EquipFlag ApplyEquip; + public BonusItemFlag ApplyBonus; public static CharaFile ParseData(ItemManager items, string data, string? name = null) { @@ -24,6 +25,7 @@ public sealed class CharaFile ret.Name = jObj["Nickname"]?.ToObject() ?? name ?? "New Design"; ret.ApplyCustomize = ParseCustomize(jObj, ref ret.Data.Customize); ret.ApplyEquip = ParseEquipment(items, jObj, ref ret.Data); + ret.ApplyBonus = ParseBonusItems(items, jObj, ref ret.Data); return ret; } @@ -45,6 +47,13 @@ public sealed class CharaFile return ret; } + private static BonusItemFlag ParseBonusItems(ItemManager items, JObject jObj, ref DesignData data) + { + BonusItemFlag ret = 0; + ParseBonus(items, jObj, "Glasses", "GlassesId", BonusItemFlag.Glasses, ref data, ref ret); + return ret; + } + private static void ParseWeapon(ItemManager items, JObject jObj, string property, EquipSlot slot, ref DesignData data, ref EquipFlag flags) { var jTok = jObj[property]; @@ -60,7 +69,9 @@ public sealed class CharaFile return; data.SetItem(slot, item); - data.SetStain(slot, dye); + data.SetStain(slot, new StainIds(dye)); + if (slot is EquipSlot.MainHand) + data.SetItem(EquipSlot.OffHand, items.GetDefaultOffhand(item)); flags |= slot.ToFlag(); flags |= slot.ToStainFlag(); } @@ -79,11 +90,31 @@ public sealed class CharaFile return; data.SetItem(slot, item); - data.SetStain(slot, dye); + data.SetStain(slot, new StainIds(dye)); flags |= slot.ToFlag(); flags |= slot.ToStainFlag(); } + private static void ParseBonus(ItemManager items, JObject jObj, string property, string subProperty, BonusItemFlag slot, + ref DesignData data, ref BonusItemFlag flags) + { + var id = jObj[property]?[subProperty]?.ToObject(); + if (id is null) + return; + + if (id is 0) + { + data.SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); + flags |= slot; + } + + if (!items.DictBonusItems.TryGetValue((BonusItemId)id.Value, out var item) || item.Type.ToBonus() != slot) + return; + + data.SetBonusItem(slot, item); + flags |= slot; + } + private static CustomizeFlag ParseCustomize(JObject jObj, ref CustomizeArray customize) { CustomizeFlag ret = 0; diff --git a/Glamourer/Interop/CharaFile/CmaFile.cs b/Glamourer/Interop/CharaFile/CmaFile.cs index dab91ac..2e06588 100644 --- a/Glamourer/Interop/CharaFile/CmaFile.cs +++ b/Glamourer/Interop/CharaFile/CmaFile.cs @@ -58,7 +58,7 @@ public sealed class CmaFile if (idx * 4 + 3 >= byteData.Length) continue; - var armor = ((CharacterArmor*)ptr)[idx]; + var armor = ((LegacyCharacterArmor*)ptr)[idx]; var item = items.Identify(slot, armor.Set, armor.Variant); data.SetItem(slot, item); data.SetStain(slot, armor.Stain); @@ -74,7 +74,7 @@ public sealed class CmaFile if (mainhand == null) { data.SetItem(EquipSlot.MainHand, items.DefaultSword); - data.SetStain(EquipSlot.MainHand, 0); + data.SetStain(EquipSlot.MainHand, StainIds.None); return; } @@ -85,7 +85,7 @@ public sealed class CmaFile var item = items.Identify(EquipSlot.MainHand, set, type, variant); data.SetItem(EquipSlot.MainHand, item.Valid ? item : items.DefaultSword); - data.SetStain(EquipSlot.MainHand, stain); + data.SetStain(EquipSlot.MainHand, new StainIds(stain)); } private static void ParseOffHand(ItemManager items, JObject jObj, ref DesignData data) @@ -95,7 +95,7 @@ public sealed class CmaFile if (offhand == null) { data.SetItem(EquipSlot.MainHand, defaultOffhand); - data.SetStain(EquipSlot.MainHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand)); + data.SetStain(EquipSlot.MainHand, defaultOffhand.PrimaryId.Id == 0 ? StainIds.None : data.Stain(EquipSlot.MainHand)); return; } @@ -106,6 +106,6 @@ public sealed class CmaFile var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType); data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand); - data.SetStain(EquipSlot.OffHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : (StainId)stain); + data.SetStain(EquipSlot.OffHand, defaultOffhand.PrimaryId.Id == 0 ? StainIds.None : new StainIds(stain)); } } diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs index 8cd5391..1f85612 100644 --- a/Glamourer/Interop/ContextMenuService.cs +++ b/Glamourer/Interop/ContextMenuService.cs @@ -5,33 +5,31 @@ using Glamourer.Designs; using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Interop; public class ContextMenuService : IDisposable { - public const int ItemSearchContextItemId = 0x1738; - public const int ChatLogContextItemId = 0x948; + public const int ChatLogContextItemId = 0x958; - private readonly ItemManager _items; - private readonly IContextMenu _contextMenu; - private readonly StateManager _state; - private readonly ObjectManager _objects; - private readonly IGameGui _gameGui; - private EquipItem _lastItem; - private StainId _lastStain; + private readonly ItemManager _items; + private readonly IContextMenu _contextMenu; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; + private EquipItem _lastItem; + private readonly StainId[] _lastStains = new StainId[StainId.NumStains]; private readonly MenuItem _inventoryItem; - public ContextMenuService(ItemManager items, StateManager state, ObjectManager objects, IGameGui gameGui, Configuration config, + public ContextMenuService(ItemManager items, StateManager state, ActorObjectManager objects, Configuration config, IContextMenu context) { _contextMenu = context; _items = items; _state = state; _objects = objects; - _gameGui = gameGui; if (config.EnableGameContextMenu) Enable(); @@ -47,14 +45,15 @@ public class ContextMenuService : IDisposable }; } - private unsafe void OnMenuOpened(MenuOpenedArgs args) + private unsafe void OnMenuOpened(IMenuOpenedArgs args) { if (args.MenuType is ContextMenuType.Inventory) { var arg = (MenuTargetInventory)args.Target; if (arg.TargetItem.HasValue && HandleItem(arg.TargetItem.Value.ItemId)) { - _lastStain = arg.TargetItem.Value.Stain; + for (var i = 0; i < arg.TargetItem.Value.Stains.Length; ++i) + _lastStains[i] = arg.TargetItem.Value.Stains[i]; args.AddMenuItem(_inventoryItem); } } @@ -71,13 +70,44 @@ public class ContextMenuService : IDisposable } case "ChatLog": { - var agent = _gameGui.FindAgentInterface("ChatLog"); - if (agent == nint.Zero || !ValidateChatLogContext(agent)) + var agent = AgentChatLog.Instance(); + if (agent == null || !ValidateChatLogContext(agent)) return; if (HandleItem(*(ItemId*)(agent + ChatLogContextItemId))) { - _lastStain = 0; + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; + args.AddMenuItem(_inventoryItem); + } + + break; + } + case "RecipeNote": + { + var agent = AgentRecipeNote.Instance(); + if (agent == null) + return; + + if (HandleItem(agent->ContextMenuResultItemId)) + { + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; + args.AddMenuItem(_inventoryItem); + } + + break; + } + case "InclusionShop": + { + var agent = AgentRecipeItemContext.Instance(); + if (agent == null) + return; + + if (HandleItem(agent->ResultItemId)) + { + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; args.AddMenuItem(_inventoryItem); } @@ -96,7 +126,7 @@ public class ContextMenuService : IDisposable public void Dispose() => Disable(); - private void OnClick(MenuItemClickedArgs _) + private void OnClick(IMenuItemClickedArgs _) { var (id, playerData) = _objects.PlayerData; if (!playerData.Valid) @@ -106,15 +136,15 @@ public class ContextMenuService : IDisposable return; var slot = _lastItem.Type.ToSlot(); - _state.ChangeEquip(state, slot, _lastItem, _lastStain, ApplySettings.Manual); + _state.ChangeEquip(state, slot, _lastItem, _lastStains[0], ApplySettings.Manual); if (!_lastItem.Type.ValidOffhand().IsOffhandType()) return; if (_lastItem.PrimaryId.Id is > 1600 and < 1651 && _items.ItemData.TryGetValue(_lastItem.ItemId, EquipSlot.Hands, out var gauntlets)) - _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, _lastStain, ApplySettings.Manual); + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, _lastStains[0], ApplySettings.Manual); if (_items.ItemData.TryGetValue(_lastItem.ItemId, EquipSlot.OffHand, out var offhand)) - _state.ChangeEquip(state, EquipSlot.OffHand, offhand, _lastStain, ApplySettings.Manual); + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, _lastStains[0], ApplySettings.Manual); } private bool HandleItem(ItemId id) @@ -123,6 +153,6 @@ public class ContextMenuService : IDisposable return _items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out _lastItem); } - private static unsafe bool ValidateChatLogContext(nint agent) - => *(uint*)(agent + ChatLogContextItemId + 8) == 3; + private static unsafe bool ValidateChatLogContext(AgentChatLog* agent) + => *(&agent->ContextItemId + 8) == 3; } diff --git a/Glamourer/Interop/CrestService.cs b/Glamourer/Interop/CrestService.cs index 75e2a81..2b55f94 100644 --- a/Glamourer/Interop/CrestService.cs +++ b/Glamourer/Interop/CrestService.cs @@ -2,8 +2,10 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; @@ -30,12 +32,13 @@ public sealed unsafe class CrestService : EventWrapperRef3(_humanVTable[96], HumanSetFreeCompanyCrestVisibleOnSlotDetour); + interop.HookFromAddress(_humanVTable[109], HumanSetFreeCompanyCrestVisibleOnSlotDetour); _weaponSetFreeCompanyCrestVisibleOnSlot = - interop.HookFromAddress(_weaponVTable[96], WeaponSetFreeCompanyCrestVisibleOnSlotDetour); + interop.HookFromAddress(_weaponVTable[109], WeaponSetFreeCompanyCrestVisibleOnSlotDetour); _humanSetFreeCompanyCrestVisibleOnSlot.Enable(); _weaponSetFreeCompanyCrestVisibleOnSlot.Enable(); _crestChangeHook.Enable(); + _crestChangeCallerHook.Enable(); } public void UpdateCrests(Actor gameObject, CrestFlag flags) @@ -46,7 +49,7 @@ public sealed unsafe class CrestService : EventWrapperRef3DrawData, (byte) flags); gameObject.CrestBitfield = currentCrests; } @@ -59,16 +62,17 @@ public sealed unsafe class CrestService : EventWrapperRef3 _crestChangeHook = null!; - private void CrestChangeDetour(Character* character, byte crestFlags) + private void CrestChangeDetour(DrawDataContainer* container, byte crestFlags) { - var actor = (Actor)character; + var actor = (Actor)container->OwnerObject; foreach (var slot in CrestExtensions.AllRelevantSet) { var newValue = ((CrestFlag)crestFlags).HasFlag(slot); @@ -77,9 +81,31 @@ public sealed unsafe class CrestService : EventWrapperRef3 _crestChangeCallerHook = null!; + + private delegate void CrestChangeCallerDelegate(DrawDataContainer* container, byte* data); + + private void CrestChangeCallerDetour(DrawDataContainer* container, byte* data) + { + var actor = (Actor)container->OwnerObject; + ref var flags = ref data[16]; + foreach (var slot in CrestExtensions.AllRelevantSet) + { + var newValue = ((CrestFlag)flags).HasFlag(slot); + Invoke(actor, slot, ref newValue); + flags = (byte)(newValue ? flags | (byte)slot : flags & (byte)~slot); + } + Glamourer.Log.Verbose( + $"Called inlined CrestChange via CrestChangeCaller on {(ulong)container:X} with {(flags & 0x1F):X} and prior flags {actor.CrestBitfield}."); + + using var _ = _inUpdate.EnterMethod(); + _crestChangeCallerHook.Original(container, data); } public static bool GetModelCrest(Actor gameObject, CrestFlag slot) @@ -96,8 +122,7 @@ public sealed unsafe class CrestService : EventWrapperRef3)((nint*)model.AsCharacterBase->VTable)[95]; - return getter(model.AsHuman, index) != 0; + return model.AsHuman->IsFreeCompanyCrestVisibleOnSlot(index); } case CrestType.Offhand: { @@ -105,8 +130,7 @@ public sealed unsafe class CrestService : EventWrapperRef3)((nint*)model.AsCharacterBase->VTable)[95]; - return getter(model.AsWeapon, index) != 0; + return model.AsWeapon->IsFreeCompanyCrestVisibleOnSlot(index); } } @@ -117,10 +141,10 @@ public sealed unsafe class CrestService : EventWrapperRef3 _humanSetFreeCompanyCrestVisibleOnSlot; diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs index 9587feb..c6e90fd 100644 --- a/Glamourer/Interop/ImportService.cs +++ b/Glamourer/Interop/ImportService.cs @@ -1,9 +1,9 @@ using Dalamud.Interface.DragDrop; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; using Glamourer.Interop.CharaFile; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -65,7 +65,7 @@ public class ImportService(CustomizeService _customizations, IDragDropManager _d var file = CharaFile.CharaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); name = file.Name; - design = new DesignBase(_customizations, file.Data, file.ApplyEquip, file.ApplyCustomize); + design = new DesignBase(_customizations, file.Data, file.ApplyEquip, file.ApplyCustomize, file.ApplyBonus); } catch (Exception ex) { @@ -95,7 +95,7 @@ public class ImportService(CustomizeService _customizations, IDragDropManager _d throw new Exception(); name = file.Name; - design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, 0); } catch (Exception ex) { diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index 9ad8737..c30ae06 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Events; @@ -12,18 +13,17 @@ namespace Glamourer.Interop; public sealed unsafe class InventoryService : IDisposable, IRequiredService { - private readonly MovedEquipment _movedItemsEvent; - private readonly EquippedGearset _gearsetEvent; - private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12); + private readonly MovedEquipment _movedItemsEvent; + private readonly EquippedGearset _gearsetEvent; + private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { _movedItemsEvent = movedItemsEvent; - _gearsetEvent = gearsetEvent; + _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); - _equipGearsetHook = - interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetDetour); _moveItemHook.Enable(); _equipGearsetHook.Enable(); @@ -35,20 +35,20 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService _equipGearsetHook.Dispose(); } - private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId); + private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); - private readonly Hook _equipGearsetHook; + private readonly Hook _equipGearsetHook = null!; - private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) + private nint EquipGearSetDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) { var prior = module->CurrentGearsetIndex; - var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); - var set = module->GetGearset(gearsetId); - _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), gearsetId, prior, glamourPlateId, set->ClassJob); - Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + var set = module->GetGearset((int)gearsetId); + _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); + Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { - var entry = module->GetGearset(gearsetId); + var entry = module->GetGearset((int)gearsetId); if (entry == null) return ret; @@ -60,56 +60,56 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService if (glamourPlateId != 0) { - void Add(EquipSlot slot, uint glamourId, StainId glamourStain, ref RaptureGearsetModule.GearsetItem item) + void Add(EquipSlot slot, uint glamourId, StainIds glamourStain, ref RaptureGearsetModule.GearsetItem item) { - if (item.ItemID == 0) - _itemList.Add((slot, 0, 0)); + if (item.ItemId == 0) + _itemList.Add((slot, 0, StainIds.None)); else if (glamourId != 0) _itemList.Add((slot, glamourId, glamourStain)); else if (item.GlamourId != 0) - _itemList.Add((slot, item.GlamourId, item.Stain)); + _itemList.Add((slot, item.GlamourId, StainIds.FromGearsetItem(item))); else - _itemList.Add((slot, FixId(item.ItemID), item.Stain)); + _itemList.Add((slot, FixId(item.ItemId), StainIds.FromGearsetItem(item))); } - var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1]; - Add(EquipSlot.MainHand, plate.ItemIds[0], plate.StainIds[0], ref entry->ItemsSpan[0]); - Add(EquipSlot.OffHand, plate.ItemIds[1], plate.StainIds[1], ref entry->ItemsSpan[1]); - Add(EquipSlot.Head, plate.ItemIds[2], plate.StainIds[2], ref entry->ItemsSpan[2]); - Add(EquipSlot.Body, plate.ItemIds[3], plate.StainIds[3], ref entry->ItemsSpan[3]); - Add(EquipSlot.Hands, plate.ItemIds[4], plate.StainIds[4], ref entry->ItemsSpan[5]); - Add(EquipSlot.Legs, plate.ItemIds[5], plate.StainIds[5], ref entry->ItemsSpan[6]); - Add(EquipSlot.Feet, plate.ItemIds[6], plate.StainIds[6], ref entry->ItemsSpan[7]); - Add(EquipSlot.Ears, plate.ItemIds[7], plate.StainIds[7], ref entry->ItemsSpan[8]); - Add(EquipSlot.Neck, plate.ItemIds[8], plate.StainIds[8], ref entry->ItemsSpan[9]); - Add(EquipSlot.Wrists, plate.ItemIds[9], plate.StainIds[9], ref entry->ItemsSpan[10]); - Add(EquipSlot.RFinger, plate.ItemIds[10], plate.StainIds[10], ref entry->ItemsSpan[11]); - Add(EquipSlot.LFinger, plate.ItemIds[11], plate.StainIds[11], ref entry->ItemsSpan[12]); + var plate = MirageManager.Instance()->GlamourPlates[glamourPlateId - 1]; + Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); + Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); + Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); + Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); + Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); + Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); + Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); + Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); + Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); + Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); + Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); + Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); } else { void Add(EquipSlot slot, ref RaptureGearsetModule.GearsetItem item) { - if (item.ItemID == 0) - _itemList.Add((slot, 0, 0)); + if (item.ItemId == 0) + _itemList.Add((slot, 0, StainIds.None)); else if (item.GlamourId != 0) - _itemList.Add((slot, item.GlamourId, item.Stain)); + _itemList.Add((slot, item.GlamourId, StainIds.FromGearsetItem(item))); else - _itemList.Add((slot, FixId(item.ItemID), item.Stain)); + _itemList.Add((slot, FixId(item.ItemId), StainIds.FromGearsetItem(item))); } - Add(EquipSlot.MainHand, ref entry->ItemsSpan[0]); - Add(EquipSlot.OffHand, ref entry->ItemsSpan[1]); - Add(EquipSlot.Head, ref entry->ItemsSpan[2]); - Add(EquipSlot.Body, ref entry->ItemsSpan[3]); - Add(EquipSlot.Hands, ref entry->ItemsSpan[5]); - Add(EquipSlot.Legs, ref entry->ItemsSpan[6]); - Add(EquipSlot.Feet, ref entry->ItemsSpan[7]); - Add(EquipSlot.Ears, ref entry->ItemsSpan[8]); - Add(EquipSlot.Neck, ref entry->ItemsSpan[9]); - Add(EquipSlot.Wrists, ref entry->ItemsSpan[10]); - Add(EquipSlot.RFinger, ref entry->ItemsSpan[11]); - Add(EquipSlot.LFinger, ref entry->ItemsSpan[12]); + Add(EquipSlot.MainHand, ref entry->Items[0]); + Add(EquipSlot.OffHand, ref entry->Items[1]); + Add(EquipSlot.Head, ref entry->Items[2]); + Add(EquipSlot.Body, ref entry->Items[3]); + Add(EquipSlot.Hands, ref entry->Items[5]); + Add(EquipSlot.Legs, ref entry->Items[6]); + Add(EquipSlot.Feet, ref entry->Items[7]); + Add(EquipSlot.Ears, ref entry->Items[8]); + Add(EquipSlot.Neck, ref entry->Items[9]); + Add(EquipSlot.Wrists, ref entry->Items[10]); + Add(EquipSlot.RFinger, ref entry->Items[11]); + Add(EquipSlot.LFinger, ref entry->Items[12]); } _movedItemsEvent.Invoke(_itemList.ToArray()); @@ -155,7 +155,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService return ret; } - private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainId) tuple) + private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainIds) tuple) { tuple = default; if (sourceContainer is not InventoryType.EquippedItems) @@ -165,12 +165,12 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService if (slot is EquipSlot.Unknown) return false; - tuple = (slot, 0u, 0); + tuple = (slot, 0u, StainIds.None); return true; } private static bool InvokeTarget(InventoryManager* manager, InventoryType targetContainer, uint targetSlot, - out (EquipSlot, uint, StainId) tuple) + out (EquipSlot, uint, StainIds) tuple) { tuple = default; if (targetContainer is not InventoryType.EquippedItems) @@ -182,14 +182,14 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService // Invoked after calling Original, so the item is already moved. var inventory = manager->GetInventoryContainer(targetContainer); - if (inventory == null || inventory->Loaded == 0 || inventory->Size <= targetSlot) + if (inventory == null || inventory->IsLoaded || inventory->Size <= targetSlot) return false; var item = inventory->GetInventorySlot((int)targetSlot); if (item == null) return false; - tuple = (slot, item->GlamourID != 0 ? item->GlamourID : item->ItemID, item->Stain); + tuple = (slot, item->GlamourId != 0 ? item->GlamourId : item->ItemId, new StainIds(item->Stains)); return true; } diff --git a/Glamourer/Interop/Material/DirectXService.cs b/Glamourer/Interop/Material/DirectXService.cs index 6d9c71b..8006a2f 100644 --- a/Glamourer/Interop/Material/DirectXService.cs +++ b/Glamourer/Interop/Material/DirectXService.cs @@ -1,12 +1,9 @@ using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Lumina.Data.Files; using OtterGui.Services; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Functions; using SharpGen.Runtime; using Vortice.Direct3D11; -using Vortice.DXGI; using MapFlags = Vortice.Direct3D11.MapFlags; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; @@ -14,14 +11,14 @@ namespace Glamourer.Interop.Material; public unsafe class DirectXService(IFramework framework) : IService { - private readonly object _lock = new(); - private readonly ConcurrentDictionary _textures = []; + private readonly object _lock = new(); + private readonly ConcurrentDictionary _textures = []; /// Generate a color table the way the game does inside the original texture, and release the original. /// The original texture that will be replaced with a new one. /// The input color table. /// Success or failure. - public bool ReplaceColorTable(Texture** original, in LegacyColorTable colorTable) + public bool ReplaceColorTable(Texture** original, in ColorTable.Table colorTable) { if (original == null) return false; @@ -32,13 +29,11 @@ public unsafe class DirectXService(IFramework framework) : IService lock (_lock) { - using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, - (uint)TexFile.TextureFormat.R16G16B16A16F, - (uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false); + using var texture = new SafeTextureHandle(MaterialService.CreateColorTableTexture(), false); if (texture.IsInvalid) return false; - fixed (LegacyColorTable* ptr = &colorTable) + fixed (ColorTable.Table* ptr = &colorTable) { if (!texture.Texture->InitializeContents(ptr)) return false; @@ -51,7 +46,7 @@ public unsafe class DirectXService(IFramework framework) : IService return true; } - public bool TryGetColorTable(Texture* texture, out LegacyColorTable table) + public bool TryGetColorTable(Texture* texture, out ColorTable.Table table) { if (_textures.TryGetValue((nint)texture, out var p) && framework.LastUpdateUTC == p.Update) { @@ -73,7 +68,7 @@ public unsafe class DirectXService(IFramework framework) : IService /// A pointer to the internal texture struct containing the GPU handle. /// The returned color table. /// Whether the table could be fetched. - private static bool TextureColorTable(Texture* texture, out LegacyColorTable table) + private static bool TextureColorTable(Texture* texture, out ColorTable.Table table) { if (texture == null) { @@ -92,6 +87,7 @@ public unsafe class DirectXService(IFramework framework) : IService } catch { + table = default; return false; } } @@ -114,11 +110,11 @@ public unsafe class DirectXService(IFramework framework) : IService } /// Turn a mapped texture into a color table. - private static LegacyColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map) + private static ColorTable.Table GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map) { var desc = resource.Description1; - if (desc.Format is not Format.R16G16B16A16_Float + if (desc.Format is not Vortice.DXGI.Format.R16G16B16A16_Float || desc.Width != MaterialService.TextureWidth || desc.Height != MaterialService.TextureHeight || map.DepthPitch != map.RowPitch * desc.Height) @@ -133,14 +129,14 @@ public unsafe class DirectXService(IFramework framework) : IService /// The height of the texture. (Needs to be 16). /// The stride in the texture data. /// - private static LegacyColorTable ReadTexture(nint data, int length, int height, int pitch) + private static ColorTable.Table ReadTexture(nint data, int length, int height, int pitch) { // Check that the data has sufficient dimension and size. var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4; - if (length < expectedSize || sizeof(LegacyColorTable) != expectedSize || height != MaterialService.TextureHeight) + if (length < expectedSize || sizeof(ColorTable.Table) != expectedSize || height != MaterialService.TextureHeight) return default; - var ret = new LegacyColorTable(); + var ret = new ColorTable.Table(); var target = (byte*)&ret; // If the stride is the same as in the table, just copy. if (pitch == MaterialService.TextureWidth) diff --git a/Glamourer/Interop/Material/LiveColorTablePreviewer.cs b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs index aa4c358..3b9edb7 100644 --- a/Glamourer/Interop/Material/LiveColorTablePreviewer.cs +++ b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs @@ -1,5 +1,5 @@ using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Structs; @@ -13,11 +13,11 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable private readonly DirectXService _directXService; public MaterialValueIndex LastValueIndex { get; private set; } = MaterialValueIndex.Invalid; - public LegacyColorTable LastOriginalColorTable { get; private set; } + public ColorTable.Table LastOriginalColorTable { get; private set; } private MaterialValueIndex _valueIndex = MaterialValueIndex.Invalid; private ObjectIndex _lastObjectIndex = ObjectIndex.AnyIndex; private ObjectIndex _objectIndex = ObjectIndex.AnyIndex; - private LegacyColorTable _originalColorTable; + private ColorTable.Table _originalColorTable; public LiveColorTablePreviewer(global::Penumbra.GameData.Interop.ObjectManager objects, IFramework framework, DirectXService directXService) { @@ -73,15 +73,15 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable var table = LastOriginalColorTable; if (_valueIndex.RowIndex != byte.MaxValue) { - table[_valueIndex.RowIndex].Diffuse = diffuse; - table[_valueIndex.RowIndex].Emissive = emissive; + table[_valueIndex.RowIndex].DiffuseColor = (HalfColor)diffuse; + table[_valueIndex.RowIndex].EmissiveColor = (HalfColor)emissive; } else { - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) + for (var i = 0; i < ColorTable.NumRows; ++i) { - table[i].Diffuse = diffuse; - table[i].Emissive = emissive; + table[i].DiffuseColor = (HalfColor)diffuse; + table[i].EmissiveColor = (HalfColor)emissive; } } @@ -92,7 +92,7 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable _objectIndex = ObjectIndex.AnyIndex; } - public void OnHover(MaterialValueIndex index, ObjectIndex objectIndex, LegacyColorTable table) + public void OnHover(MaterialValueIndex index, ObjectIndex objectIndex, in ColorTable.Table table) { if (_valueIndex.DrawObject is not MaterialValueIndex.DrawObjectType.Invalid) return; @@ -114,8 +114,9 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable var frame = DateTimeOffset.UtcNow.UtcTicks; var hueByte = frame % (steps * frameLength) / frameLength; var hue = (float)hueByte / steps; - ImGui.ColorConvertHSVtoRGB(hue, 1, 1, out var r, out var g, out var b); - return new Vector3(r, g, b); + Vector3 ret; + ImGui.ColorConvertHSVtoRGB(hue, 1, 1, &ret.X, &ret.Y, &ret.Z); + return ret; } public void Dispose() diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs index 8e3936e..43e500b 100644 --- a/Glamourer/Interop/Material/MaterialManager.cs +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Havok.Animation.Rig; using Glamourer.Designs; using Glamourer.Interop.Penumbra; using Glamourer.State; @@ -38,17 +39,13 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable public void Dispose() => _event.Unsubscribe(OnPrepareColorSet); - private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainId stain, ref nint ret) + private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainIds stain, ref nint ret) { - if (!_config.UseAdvancedDyes) - return; - var actor = _penumbra.GameObjectFromDrawObject(characterBase); var validType = FindType(characterBase, actor, out var type); var (slotId, materialId) = FindMaterial(characterBase, material); if (!validType - || slotId > 9 || type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0 || !actor.Identifier(_actors, out var identifier) || !_stateManager.TryGetValue(identifier, out var state)) @@ -60,15 +57,16 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable if (values.Length == 0) return; - if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) + if (!PrepareColorSet.TryGetColorTable(material, stain, out var baseColorSet)) return; var drawData = type switch { - MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId), + MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, (HumanSlot)slotId), _ => GetTempSlot((Weapon*)characterBase), }; - UpdateMaterialValues(state, values, drawData, ref baseColorSet); + var mode = PrepareColorSet.GetMode(material); + UpdateMaterialValues(state, values, drawData, ref baseColorSet, mode); if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) ret = (nint)texture; @@ -76,7 +74,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable /// Update and apply the glamourer state of an actor according to the application sources when updated by the game. private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData, - ref LegacyColorTable colorTable) + ref ColorTable.Table colorTable, ColorRow.Mode mode) { var deleteList = _deleteList.Value!; deleteList.Clear(); @@ -87,22 +85,27 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable ref var row = ref colorTable[idx.RowIndex]; var newGame = new ColorRow(row); if (materialValue.EqualGame(newGame, drawData)) - materialValue.Model.Apply(ref row); + materialValue.Model.Apply(ref row, mode); else switch (materialValue.Source) { case StateSource.Pending: - materialValue.Model.Apply(ref row); + materialValue.Model.Apply(ref row, mode); state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual), out _); break; + case StateSource.IpcPending: + materialValue.Model.Apply(ref row, mode); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.IpcManual), + out _); + break; case StateSource.IpcManual: case StateSource.Manual: deleteList.Add(idx); break; case StateSource.Fixed: case StateSource.IpcFixed: - materialValue.Model.Apply(ref row); + materialValue.Model.Apply(ref row, mode); state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source), out _); break; @@ -154,38 +157,59 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable /// Find the type of the given draw object by checking the actors pointers. private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) { - type = MaterialValueIndex.DrawObjectType.Human; if (!actor.Valid) + { + type = MaterialValueIndex.DrawObjectType.Invalid; return false; + } - if (actor.Model.AsCharacterBase == characterBase) + if (actor.Model.AsCharacterBase == characterBase && ((Model)characterBase).IsHuman) + { + type = MaterialValueIndex.DrawObjectType.Human; return true; + } if (!actor.AsObject->IsCharacter()) + { + type = MaterialValueIndex.DrawObjectType.Invalid; return false; + } - if (actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject == characterBase) + if (actor.AsCharacter->DrawData.WeaponData[0].DrawObject == characterBase) { type = MaterialValueIndex.DrawObjectType.Mainhand; return true; } - if (actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject == characterBase) + if (actor.AsCharacter->DrawData.WeaponData[1].DrawObject == characterBase) { type = MaterialValueIndex.DrawObjectType.Offhand; return true; } + type = MaterialValueIndex.DrawObjectType.Invalid; return false; } /// We need to get the temporary set, variant and stain that is currently being set if it is available. - private static CharacterWeapon GetTempSlot(Human* human, byte slotId) + private static CharacterWeapon GetTempSlot(Human* human, HumanSlot slotId) { - if (human->ChangedEquipData == null) - return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0); + if (human->ChangedEquipData is null) + return slotId.ToSpecificEnum() switch + { + EquipSlot slot => ((Model)human).GetArmor(slot).ToWeapon(0), + BonusItemFlag bonus => ((Model)human).GetBonus(bonus).ToWeapon(0), + _ => default, + }; - return ((CharacterArmor*)human->ChangedEquipData + slotId * 3)->ToWeapon(0); + if (!slotId.ToSlotIndex(out var index)) + return default; + + var item = (ChangedEquipData*)human->ChangedEquipData + index; + if (index < 10) + return ((CharacterArmor*)item)->ToWeapon(0); + + return new CharacterWeapon(item->BonusModel, 0, item->BonusVariant, StainIds.None); } /// @@ -194,10 +218,11 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable /// private static CharacterWeapon GetTempSlot(Weapon* weapon) { - var changedData = *(void**)((byte*)weapon + 0x918); + var changedData = weapon->ChangedData; if (changedData == null) - return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, (StainId)weapon->ModelUnknown); + return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, StainIds.FromWeapon(*weapon)); - return new CharacterWeapon(weapon->ModelSetId, *(SecondaryId*)changedData, ((Variant*)changedData)[2], ((StainId*)changedData)[3]); + return new CharacterWeapon(weapon->ModelSetId, changedData->SecondaryId, changedData->Variant, + new StainIds(changedData->Stain0, changedData->Stain1)); } } diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs index 4c8706c..4893e14 100644 --- a/Glamourer/Interop/Material/MaterialService.cs +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using Lumina.Data.Files; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; @@ -9,22 +8,28 @@ namespace Glamourer.Interop.Material; public static unsafe class MaterialService { - public const int TextureWidth = 4; - public const int TextureHeight = LegacyColorTable.NumUsedRows; - public const int MaterialsPerModel = 4; + private const TextureFormat Format = TextureFormat.R16G16B16A16_FLOAT; + private const TextureFlags Flags = TextureFlags.TextureType2D | TextureFlags.Managed | TextureFlags.Immutable; - public static bool GenerateNewColorTable(in LegacyColorTable colorTable, out Texture* texture) + public const int TextureWidth = 8; + public const int TextureHeight = ColorTable.NumRows; + public const int MaterialsPerModel = 10; + + public static Texture* CreateColorTableTexture() { var textureSize = stackalloc int[2]; textureSize[0] = TextureWidth; textureSize[1] = TextureHeight; + return Device.Instance()->CreateTexture2D(textureSize, 1, Format, Flags, 7); + } - texture = Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F, - (uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7); + public static bool GenerateNewColorTable(in ColorTable.Table colorTable, out Texture* texture) + { + texture = CreateColorTableTexture(); if (texture == null) return false; - fixed (LegacyColorTable* ptr = &colorTable) + fixed (ColorTable.Table* ptr = &colorTable) { return texture->InitializeContents(ptr); } @@ -53,7 +58,7 @@ public static unsafe class MaterialService /// The model slot. /// The material slot in the model. /// A pointer to the color table or null. - public static LegacyColorTable* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot) + public static ColorTable.Table* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot) { if (!model.IsCharacterBase) return null; @@ -62,10 +67,10 @@ public static unsafe class MaterialService if (index < 0 || index >= model.AsCharacterBase->MaterialsSpan.Length) return null; - var material = (MaterialResourceHandle*)model.AsCharacterBase->MaterialsSpan[index].Value; - if (material == null || material->ColorTable == null) + var material = (MaterialResourceHandle*) model.AsCharacterBase->MaterialsSpan[index].Value; + if (material == null || material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable) return null; - return (LegacyColorTable*)material->ColorTable; + return (ColorTable.Table*)material->DataSet; } } diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs index 9bfcc4c..eb3f71f 100644 --- a/Glamourer/Interop/Material/MaterialValueIndex.cs +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -1,9 +1,11 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using Newtonsoft.Json; using Penumbra.GameData.Enums; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; +using CsMaterial = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material; namespace Glamourer.Interop.Material; @@ -42,6 +44,24 @@ public readonly record struct MaterialValueIndex( return Invalid; } + public static MaterialValueIndex FromSlot(BonusItemFlag slot) + { + var idx = slot.ToIndex(); + return idx > 2 ? Invalid : new MaterialValueIndex(DrawObjectType.Human, (byte)(idx + 16), 0, 0); + } + + public string SlotName() + { + var slot = ToEquipSlot(); + if (slot is not EquipSlot.Unknown) + return slot.ToName(); + + if (DrawObject is DrawObjectType.Human && SlotIndex is 16) + return BonusItemFlag.Glasses.ToString(); + + return EquipSlot.Unknown.ToName(); + } + public EquipSlot ToEquipSlot() => DrawObject switch { @@ -51,6 +71,13 @@ public readonly record struct MaterialValueIndex( _ => EquipSlot.Unknown, }; + public BonusItemFlag ToBonusSlot() + => DrawObject switch + { + DrawObjectType.Human when SlotIndex > 15 => ((uint)SlotIndex - 16).ToBonusSlot(), + _ => BonusItemFlag.Unknown, + }; + public unsafe bool TryGetModel(Actor actor, out Model model) { if (!actor.Valid) @@ -62,13 +89,30 @@ public readonly record struct MaterialValueIndex( model = DrawObject switch { DrawObjectType.Human => actor.Model, - DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject : Model.Null, - DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject : Model.Null, + DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponData[0].DrawObject : Model.Null, + DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponData[1].DrawObject : Model.Null, _ => Model.Null, }; return model.IsCharacterBase; } + public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan> textures, out ReadOnlySpan> materials) + { + if (!TryGetModel(actor, out var model) + || SlotIndex >= model.AsCharacterBase->SlotCount + || model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel) + { + textures = []; + materials = []; + return false; + } + + var from = SlotIndex * MaterialService.MaterialsPerModel; + textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(from, MaterialService.MaterialsPerModel); + materials = model.AsCharacterBase->MaterialsSpan.Slice(from, MaterialService.MaterialsPerModel); + return true; + } + public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan> textures) { if (!TryGetModel(actor, out var model) @@ -79,11 +123,12 @@ public readonly record struct MaterialValueIndex( return false; } - textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(SlotIndex * MaterialService.MaterialsPerModel, - MaterialService.MaterialsPerModel); + var from = SlotIndex * MaterialService.MaterialsPerModel; + textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(from, MaterialService.MaterialsPerModel); return true; } + public unsafe bool TryGetTexture(Actor actor, out Texture** texture) { if (TryGetTextures(actor, out var textures)) @@ -93,6 +138,38 @@ public readonly record struct MaterialValueIndex( return false; } + public unsafe bool TryGetTexture(Actor actor, out Texture** texture, out ColorRow.Mode mode) + { + if (TryGetTextures(actor, out var textures, out var materials)) + return TryGetTexture(textures, materials, out texture, out mode); + + mode = ColorRow.Mode.Dawntrail; + texture = null; + return false; + } + + public unsafe bool TryGetTexture(ReadOnlySpan> textures, ReadOnlySpan> materials, + out Texture** texture, out ColorRow.Mode mode) + { + mode = MaterialIndex >= materials.Length + ? ColorRow.Mode.Dawntrail + : PrepareColorSet.GetMode((MaterialResourceHandle*)materials[MaterialIndex].Value); + + + if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null) + { + texture = null; + return false; + } + + fixed (Pointer* ptr = textures) + { + texture = (Texture**)ptr + MaterialIndex; + } + + return true; + } + public unsafe bool TryGetTexture(ReadOnlySpan> textures, out Texture** texture) { if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null) @@ -109,6 +186,7 @@ public readonly record struct MaterialValueIndex( return true; } + public static MaterialValueIndex FromKey(uint key) => new(key); @@ -133,7 +211,7 @@ public readonly record struct MaterialValueIndex( public static bool ValidateSlot(DrawObjectType type, byte slotIndex) => type switch { - DrawObjectType.Human => slotIndex < 14, + DrawObjectType.Human => slotIndex < 18, DrawObjectType.Mainhand => slotIndex == 0, DrawObjectType.Offhand => slotIndex == 0, _ => false, @@ -143,7 +221,7 @@ public readonly record struct MaterialValueIndex( => materialIndex < MaterialService.MaterialsPerModel; public static bool ValidateRow(byte rowIndex) - => rowIndex < LegacyColorTable.NumUsedRows; + => rowIndex < ColorTable.NumRows; private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex) { @@ -161,18 +239,27 @@ public readonly record struct MaterialValueIndex( public override string ToString() => DrawObject switch { - DrawObjectType.Invalid => "Invalid", - DrawObjectType.Human when SlotIndex < 10 => - $"{((uint)SlotIndex).ToEquipSlot().ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Human when SlotIndex == 10 => $"BodySlot.Hair.ToString() Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Human when SlotIndex == 11 => $"BodySlot.Face.ToString() Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Human when SlotIndex == 12 => $"{BodySlot.Tail} / {BodySlot.Ear} Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Human when SlotIndex == 13 => $"Connectors Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Mainhand when SlotIndex == 0 => $"{EquipSlot.MainHand.ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - DrawObjectType.Offhand when SlotIndex == 0 => $"{EquipSlot.OffHand.ToName()} Material #{MaterialIndex + 1} Row #{RowIndex + 1}", - _ => $"{DrawObject} Slot {SlotIndex} Material #{MaterialIndex + 1} Row #{RowIndex + 1}", + DrawObjectType.Invalid => "Invalid", + DrawObjectType.Human when SlotIndex < 10 => $"{((uint)SlotIndex).ToEquipSlot().ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 10 => $"{BodySlot.Hair} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 11 => $"{BodySlot.Face} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 12 => $"{BodySlot.Tail} / {BodySlot.Ear} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 13 => $"Connectors {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 16 => $"{BonusItemFlag.Glasses.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 17 => $"{BonusItemFlag.UnkSlot.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Mainhand when SlotIndex == 0 => $"{EquipSlot.MainHand.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Offhand when SlotIndex == 0 => $"{EquipSlot.OffHand.ToName()} {MaterialString()} {RowString()}", + _ => $"{DrawObject} Slot {SlotIndex} {MaterialString()} {RowString()}", }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string MaterialString() + => $"Material {(char)(MaterialIndex + 'A')}"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string RowString() + => $"Row {RowIndex / 2 + 1}{(char)(RowIndex % 2 + 'A')}"; + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer) diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index 483e6af..01cb479 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -13,7 +13,13 @@ namespace Glamourer.Interop.Material; /// Values are not squared. public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength) { - public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 0, 0); + public enum Mode + { + Legacy, + Dawntrail, + } + + public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 1f, 1f); public Vector3 Diffuse = diffuse; public Vector3 Specular = specular; @@ -21,8 +27,10 @@ public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, floa public float SpecularStrength = specularStrength; public float GlossStrength = glossStrength; - public ColorRow(in LegacyColorTable.Row row) - : this(Root(row.Diffuse), Root(row.Specular), Root(row.Emissive), row.SpecularStrength, row.GlossStrength) + public ColorRow(in ColorTableRow row) + : this(Root((Vector3)row.DiffuseColor), Root((Vector3)row.SpecularColor), Root((Vector3)row.EmissiveColor), + (float)row.LegacySpecularStrength(), + (float)row.LegacyGloss()) { } public readonly bool NearEqual(in ColorRow rhs) @@ -44,46 +52,64 @@ public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, floa private static float Root(float value) => value < 0 ? MathF.Sqrt(-value) : MathF.Sqrt(value); - public readonly bool Apply(ref LegacyColorTable.Row row) + public readonly bool Apply(ref ColorTableRow row, Mode mode) { var ret = false; var d = Square(Diffuse); - if (!row.Diffuse.NearEqual(d)) + if (!((Vector3)row.DiffuseColor).NearEqual(d)) { - row.Diffuse = d; - ret = true; + row.DiffuseColor = (HalfColor)d; + ret = true; } var s = Square(Specular); - if (!row.Specular.NearEqual(s)) + if (!((Vector3)row.SpecularColor).NearEqual(s)) { - row.Specular = s; - ret = true; + row.SpecularColor = (HalfColor)s; + ret = true; } var e = Square(Emissive); - if (!row.Emissive.NearEqual(e)) + if (!((Vector3)row.EmissiveColor).NearEqual(e)) { - row.Emissive = e; - ret = true; - } - - if (!row.SpecularStrength.NearEqual(SpecularStrength)) - { - row.SpecularStrength = SpecularStrength; - ret = true; - } - - if (!row.GlossStrength.NearEqual(GlossStrength)) - { - row.GlossStrength = GlossStrength; + row.EmissiveColor = (HalfColor)e; ret = true; } + if (mode is Mode.Legacy) + { + if (!((float)row.LegacySpecularStrength()).NearEqual(SpecularStrength)) + { + row.LegacySpecularStrengthWrite() = (Half)SpecularStrength; + ret = true; + } + + if (!((float)row.LegacyGloss()).NearEqual(GlossStrength)) + { + row.LegacyGlossWrite() = (Half)GlossStrength; + ret = true; + } + } + return ret; } } +internal static class ColorTableRowExtensions +{ + internal static Half LegacySpecularStrength(this in ColorTableRow row) + => row[7]; + + internal static Half LegacyGloss(this in ColorTableRow row) + => row[3]; + + internal static ref Half LegacySpecularStrengthWrite(this ref ColorTableRow row) + => ref row[7]; + + internal static ref Half LegacyGlossWrite(this ref ColorTableRow row) + => ref row[3]; +} + [JsonConverter(typeof(Converter))] public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert) { @@ -176,7 +202,6 @@ public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert) } } -[StructLayout(LayoutKind.Explicit)] public struct MaterialValueState( in ColorRow game, in ColorRow model, @@ -187,23 +212,16 @@ public struct MaterialValueState( : this(gameRow, modelRow, armor.ToWeapon(0), source) { } - [FieldOffset(0)] - public ColorRow Game = game; - - [FieldOffset(44)] - public ColorRow Model = model; - - [FieldOffset(88)] + public ColorRow Game = game; + public ColorRow Model = model; public readonly CharacterWeapon DrawData = drawData; - - [FieldOffset(95)] - public readonly StateSource Source = source; + public readonly StateSource Source = source; public readonly bool EqualGame(in ColorRow rhsRow, CharacterWeapon rhsData) => DrawData.Skeleton == rhsData.Skeleton && DrawData.Weapon == rhsData.Weapon && DrawData.Variant == rhsData.Variant - && DrawData.Stain == rhsData.Stain + && DrawData.Stains == rhsData.Stains && Game.NearEqual(rhsRow); public readonly MaterialValueDesign Convert() @@ -262,6 +280,36 @@ public readonly struct MaterialValueManager return true; } + public bool CheckExistenceSlot(MaterialValueIndex index) + { + var key = CheckExistence(index); + return key.Valid && key.DrawObject == index.DrawObject && key.SlotIndex == index.SlotIndex; + } + + public bool CheckExistenceMaterial(MaterialValueIndex index) + { + var key = CheckExistence(index); + return key.Valid && key.DrawObject == index.DrawObject && key.SlotIndex == index.SlotIndex && key.MaterialIndex == index.MaterialIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private MaterialValueIndex CheckExistence(MaterialValueIndex index) + { + if (_values.Count == 0) + return MaterialValueIndex.Invalid; + + var key = index.Key; + var idx = Search(key); + if (idx >= 0) + return index; + + idx = ~idx; + if (idx >= _values.Count) + return MaterialValueIndex.Invalid; + + return MaterialValueIndex.FromKey(_values[idx].Key); + } + public bool RemoveValue(MaterialValueIndex index) => RemoveValue(index.Key); diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 1661037..821a152 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; @@ -12,17 +13,23 @@ using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; public sealed unsafe class PrepareColorSet - : EventWrapperPtr12Ref34, IHookService + : EventWrapperPtr12Ref34, IHookService { + private readonly UpdateColorSets _updateColorSets; + public enum Priority { /// MaterialManager = 0, } - public PrepareColorSet(HookManager hooks) + public PrepareColorSet(HookManager hooks, UpdateColorSets updateColorSets) : base("Prepare Color Set ") - => _task = hooks.CreateHook(Name, "40 55 56 41 56 48 83 EC ?? 80 BA", Detour, true); + { + _updateColorSets = updateColorSets; + hooks.Provider.InitializeFromAttributes(this); + _task = hooks.CreateHook(Name, Sigs.PrepareColorSet, Detour, true); + } private readonly Task> _task; @@ -41,65 +48,111 @@ public sealed unsafe class PrepareColorSet public bool Finished => _task.IsCompletedSuccessfully; - private delegate Texture* Delegate(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId); + private delegate Texture* Delegate(MaterialResourceHandle* material, StainId stainId1, StainId stainId2); - private Texture* Detour(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId) + private Texture* Detour(MaterialResourceHandle* material, StainId stainId1, StainId stainId2) { - Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stainId.Id}."); - var ret = nint.Zero; - Invoke(characterBase, material, ref stainId, ref ret); + Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)material:X} {stainId1.Id} {stainId2.Id}."); + var characterBase = _updateColorSets.Get(); + if (!characterBase.IsCharacterBase) + return _task.Result.Original(material, stainId1, stainId2); + + var ret = nint.Zero; + var stainIds = new StainIds(stainId1, stainId2); + Invoke(characterBase.AsCharacterBase, material, ref stainIds, ref ret); if (ret != nint.Zero) return (Texture*)ret; - return _task.Result.Original(characterBase, material, stainId); + return _task.Result.Original(material, stainIds.Stain1, stainIds.Stain2); } - public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId, - out LegacyColorTable table) + public static bool TryGetColorTable(MaterialResourceHandle* material, StainIds stainIds, + out ColorTable.Table table) { - if (material->ColorTable == null) + if (material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable) { table = default; return false; } - var newTable = *(LegacyColorTable*)material->ColorTable; - if (stainId.Id != 0) - characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&newTable)); + var newTable = *(ColorTable.Table*)material->DataSet; + if (GetDyeTable(material, out var dyeTable)) + { + if (stainIds.Stain1.Id != 0) + material->ReadStainingTemplate(dyeTable, stainIds.Stain1.Id, (Half*)&newTable, 0); + + if (stainIds.Stain2.Id != 0) + material->ReadStainingTemplate(dyeTable, stainIds.Stain2.Id, (Half*)&newTable, 1); + } + table = newTable; return true; } /// Assumes the actor is valid. - public static bool TryGetColorTable(Actor actor, MaterialValueIndex index, out LegacyColorTable table) + public static bool TryGetColorTable(Actor actor, MaterialValueIndex index, out ColorTable.Table table, out ColorRow.Mode mode) { var idx = index.SlotIndex * MaterialService.MaterialsPerModel + index.MaterialIndex; if (!index.TryGetModel(actor, out var model)) - return false; - - var handle = (MaterialResourceHandle*)model.AsCharacterBase->Materials[idx]; - if (handle == null) { + mode = ColorRow.Mode.Dawntrail; table = default; return false; } - return TryGetColorTable(model.AsCharacterBase, handle, GetStain(), out table); + var handle = (MaterialResourceHandle*)model.AsCharacterBase->Materials[idx]; + if (handle == null) + { + mode = ColorRow.Mode.Dawntrail; + table = default; + return false; + } - StainId GetStain() + mode = GetMode(handle); + return TryGetColorTable(handle, GetStains(), out table); + + StainIds GetStains() { switch (index.DrawObject) { case MaterialValueIndex.DrawObjectType.Human: - return index.SlotIndex < 10 ? actor.Model.GetArmor(((uint)index.SlotIndex).ToEquipSlot()).Stain : 0; + return index.SlotIndex < 10 ? actor.Model.GetArmor(((uint)index.SlotIndex).ToEquipSlot()).Stains : StainIds.None; case MaterialValueIndex.DrawObjectType.Mainhand: - var mainhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject; - return mainhand.IsWeapon ? (StainId)mainhand.AsWeapon->ModelUnknown : 0; + var mainhand = (Model)actor.AsCharacter->DrawData.WeaponData[0].DrawObject; + return mainhand.IsWeapon ? StainIds.FromWeapon(*mainhand.AsWeapon) : StainIds.None; case MaterialValueIndex.DrawObjectType.Offhand: - var offhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject; - return offhand.IsWeapon ? (StainId)offhand.AsWeapon->ModelUnknown : 0; - default: return 0; + var offhand = (Model)actor.AsCharacter->DrawData.WeaponData[1].DrawObject; + return offhand.IsWeapon ? StainIds.FromWeapon(*offhand.AsWeapon) : StainIds.None; + default: return StainIds.None; } } } + + /// Get the shader mode of the material. + public static ColorRow.Mode GetMode(MaterialResourceHandle* handle) + => handle == null + ? ColorRow.Mode.Dawntrail + : handle->ShpkName.AsSpan().SequenceEqual("characterlegacy.shpk"u8) + ? ColorRow.Mode.Legacy + : ColorRow.Mode.Dawntrail; + + /// Get the correct dye table for a material. + private static bool GetDyeTable(MaterialResourceHandle* material, out ushort* ptr) + { + ptr = null; + if (material->AdditionalDataSize is 0 || material->AdditionalData is null) + return false; + + var flags1 = material->AdditionalData[0]; + if ((flags1 & 0xF0) is 0) + { + ptr = (ushort*)material + 0x100; + return true; + } + + var flags2 = material->AdditionalData[1]; + var offset = 4 * (1 << (flags1 >> 4)) * (1 << (flags2 & 0x0F)); + ptr = (ushort*)material->DataSet + offset; + return true; + } } diff --git a/Glamourer/Interop/Material/UpdateColorSets.cs b/Glamourer/Interop/Material/UpdateColorSets.cs new file mode 100644 index 0000000..e503bc6 --- /dev/null +++ b/Glamourer/Interop/Material/UpdateColorSets.cs @@ -0,0 +1,25 @@ +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop.Material; + +public sealed class UpdateColorSets : FastHook +{ + public delegate void Delegate(Model model, uint unk); + + private readonly ThreadLocal _updatingModel = new(() => Model.Null); + + public UpdateColorSets(HookManager hooks) + => Task = hooks.CreateHook("Update Color Sets", Sigs.UpdateColorSets, Detour, true); + + private void Detour(Model model, uint unk) + { + _updatingModel.Value = model; + Task.Result.Original(model, unk); + _updatingModel.Value = Model.Null; + } + + public Model Get() + => _updatingModel.Value; +} diff --git a/Glamourer/Interop/MetaService.cs b/Glamourer/Interop/MetaService.cs index 1bc7a32..6225986 100644 --- a/Glamourer/Interop/MetaService.cs +++ b/Glamourer/Interop/MetaService.cs @@ -13,7 +13,7 @@ public unsafe class MetaService : IDisposable private readonly VisorStateChanged _visorEvent; private delegate void HideHatGearDelegate(DrawDataContainer* drawData, uint id, byte value); - private delegate void HideWeaponsDelegate(DrawDataContainer* drawData, bool value); + private delegate void HideWeaponsDelegate(DrawDataContainer* drawData, byte value); private readonly Hook _hideHatGearHook; private readonly Hook _hideWeaponsHook; @@ -49,11 +49,11 @@ public unsafe class MetaService : IDisposable return; // The function seems to not do anything if the head is 0, but also breaks for carbuncles turned human, sometimes? - var old = actor.AsCharacter->DrawData.Head.Id; - if (old == 0 && actor.AsCharacter->CharacterData.ModelCharaId == 0) - actor.AsCharacter->DrawData.Head.Id = 1; + var old = actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id; + if (old == 0 && actor.AsCharacter->ModelContainer.ModelCharaId == 0) + actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id = 1; _hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 0 : 1)); - actor.AsCharacter->DrawData.Head.Id = old; + actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id = old; } public void SetWeaponState(Actor actor, bool value) @@ -61,7 +61,9 @@ public unsafe class MetaService : IDisposable if (!actor.IsCharacter) return; - _hideWeaponsHook.Original(&actor.AsCharacter->DrawData, !value); + var old = actor.AsCharacter->DrawData.IsWeaponHidden; + _hideWeaponsHook.Original(&actor.AsCharacter->DrawData, (byte)(value ? 0 : 1)); + actor.AsCharacter->DrawData.IsWeaponHidden = old; } private void HideHatDetour(DrawDataContainer* drawData, uint id, byte value) @@ -72,7 +74,7 @@ public unsafe class MetaService : IDisposable return; } - Actor actor = drawData->Parent; + Actor actor = drawData->OwnerObject; var v = value == 0; _headGearEvent.Invoke(actor, ref v); value = (byte)(v ? 0 : 1); @@ -80,21 +82,21 @@ public unsafe class MetaService : IDisposable _hideHatGearHook.Original(drawData, id, value); } - private void HideWeaponsDetour(DrawDataContainer* drawData, bool value) + private void HideWeaponsDetour(DrawDataContainer* drawData, byte value) { - Actor actor = drawData->Parent; - value = !value; - _weaponEvent.Invoke(actor, ref value); - value = !value; + Actor actor = drawData->OwnerObject; + var v = value == 0; + _weaponEvent.Invoke(actor, ref v); Glamourer.Log.Verbose($"[MetaService] Hide Weapon triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}."); - _hideWeaponsHook.Original(drawData, value); + _hideWeaponsHook.Original(drawData, (byte)(v ? 0 : 1)); } - private void ToggleVisorDetour(DrawDataContainer* drawData, bool value) + private void ToggleVisorDetour(DrawDataContainer* drawData, byte value) { - Actor actor = drawData->Parent; - _visorEvent.Invoke(actor.Model, true, ref value); + Actor actor = drawData->OwnerObject; + var v = value != 0; + _visorEvent.Invoke(actor.Model, true, ref v); Glamourer.Log.Verbose($"[MetaService] Toggle Visor triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}."); - _toggleVisorHook.Original(drawData, value); + _toggleVisorHook.Original(drawData, (byte)(v ? 1 : 0)); } } diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs deleted file mode 100644 index f59e95c..0000000 --- a/Glamourer/Interop/ObjectManager.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Dalamud.Game.ClientState.Objects; -using Dalamud.Plugin; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Control; -using Glamourer.Interop.Structs; -using OtterGui.Log; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; - -namespace Glamourer.Interop; - -public class ObjectManager( - IFramework framework, - IClientState clientState, - IObjectTable objects, - DalamudPluginInterface pi, - Logger log, - ActorManager actors, - ITargetManager targets) - : global::Penumbra.GameData.Interop.ObjectManager(pi, log, framework, objects) -{ - public DateTime LastUpdate - => LastFrame; - - private DateTime _identifierUpdate; - public bool IsInGPose { get; private set; } - public ushort World { get; private set; } - - private readonly Dictionary _identifiers = new(200); - private readonly Dictionary _allWorldIdentifiers = new(200); - private readonly Dictionary _nonOwnedIdentifiers = new(200); - - public IReadOnlyDictionary Identifiers - => _identifiers; - - public override bool Update() - { - if (!base.Update() && _identifierUpdate >= LastUpdate) - return false; - - _identifierUpdate = LastUpdate; - World = (ushort)(this[0].Valid ? this[0].HomeWorld : 0); - _identifiers.Clear(); - _allWorldIdentifiers.Clear(); - _nonOwnedIdentifiers.Clear(); - - foreach (var actor in BattleNpcs.Concat(CutsceneCharacters)) - { - if (actor.Identifier(actors, out var identifier)) - HandleIdentifier(identifier, actor); - } - - void AddSpecial(ScreenActor idx, string label) - { - var actor = this[(int)idx]; - if (actor.Identifier(actors, out var ident)) - { - var data = new ActorData(actor, label); - _identifiers.Add(ident, data); - } - } - - AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor"); - AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor"); - AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor"); - AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor"); - AddSpecial(ScreenActor.Portrait, "Portrait Actor"); - AddSpecial(ScreenActor.Card6, "Card Actor 6"); - AddSpecial(ScreenActor.Card7, "Card Actor 7"); - AddSpecial(ScreenActor.Card8, "Card Actor 8"); - - foreach (var actor in EventNpcs) - { - if (actor.Identifier(actors, out var identifier)) - HandleIdentifier(identifier, actor); - } - - var gPose = GPosePlayer; - IsInGPose = gPose.Utf8Name.Length > 0; - return true; - } - - private void HandleIdentifier(ActorIdentifier identifier, Actor character) - { - if (!character.Model || !identifier.IsValid) - return; - - if (!_identifiers.TryGetValue(identifier, out var data)) - { - data = new ActorData(character, identifier.ToString()); - _identifiers[identifier] = data; - } - else - { - data.Objects.Add(character); - } - - if (identifier.Type is IdentifierType.Player or IdentifierType.Owned) - { - var allWorld = actors.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, - identifier.Kind, - identifier.DataId); - - if (!_allWorldIdentifiers.TryGetValue(allWorld, out var allWorldData)) - { - allWorldData = new ActorData(character, allWorld.ToString()); - _allWorldIdentifiers[allWorld] = allWorldData; - } - else - { - allWorldData.Objects.Add(character); - } - } - - if (identifier.Type is IdentifierType.Owned) - { - var nonOwned = actors.CreateNpc(identifier.Kind, identifier.DataId); - if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData)) - { - nonOwnedData = new ActorData(character, nonOwned.ToString()); - _nonOwnedIdentifiers[nonOwned] = nonOwnedData; - } - else - { - nonOwnedData.Objects.Add(character); - } - } - } - - public Actor GPosePlayer - => this[(int)ScreenActor.GPosePlayer]; - - public Actor Player - => this[0]; - - public unsafe Actor Target - => clientState.IsGPosing ? TargetSystem.Instance()->GPoseTarget : TargetSystem.Instance()->Target; - - public Actor Focus - => targets.FocusTarget?.Address ?? nint.Zero; - - public Actor MouseOver - => targets.MouseOverTarget?.Address ?? nint.Zero; - - public (ActorIdentifier Identifier, ActorData Data) PlayerData - { - get - { - Update(); - return Player.Identifier(actors, out var ident) && _identifiers.TryGetValue(ident, out var data) - ? (ident, data) - : (ident, ActorData.Invalid); - } - } - - public (ActorIdentifier Identifier, ActorData Data) TargetData - { - get - { - Update(); - return Target.Identifier(actors, out var ident) && _identifiers.TryGetValue(ident, out var data) - ? (ident, data) - : (ident, ActorData.Invalid); - } - } - - /// Also handles All Worlds players and non-owned NPCs. - public bool ContainsKey(ActorIdentifier key) - => Identifiers.ContainsKey(key) || _allWorldIdentifiers.ContainsKey(key) || _nonOwnedIdentifiers.ContainsKey(key); - - public bool TryGetValue(ActorIdentifier key, out ActorData value) - => Identifiers.TryGetValue(key, out value); - - public bool TryGetValueAllWorld(ActorIdentifier key, out ActorData value) - => _allWorldIdentifiers.TryGetValue(key, out value); - - public bool TryGetValueNonOwned(ActorIdentifier key, out ActorData value) - => _nonOwnedIdentifiers.TryGetValue(key, out value); - - public ActorData this[ActorIdentifier key] - => Identifiers[key]; - - public IEnumerable Keys - => Identifiers.Keys; - - public IEnumerable Values - => Identifiers.Values; - - public bool GetName(string lowerName, out Actor actor) - { - (actor, var ret) = lowerName switch - { - "" => (Actor.Null, true), - "" => (Player, true), - "self" => (Player, true), - "" => (Target, true), - "target" => (Target, true), - "" => (Focus, true), - "focus" => (Focus, true), - "" => (MouseOver, true), - "mouseover" => (MouseOver, true), - _ => (Actor.Null, false), - }; - return ret; - } -} diff --git a/Glamourer/Interop/PalettePlus/PaletteImport.cs b/Glamourer/Interop/PalettePlus/PaletteImport.cs index 8513036..4887255 100644 --- a/Glamourer/Interop/PalettePlus/PaletteImport.cs +++ b/Glamourer/Interop/PalettePlus/PaletteImport.cs @@ -6,7 +6,7 @@ using OtterGui.Services; namespace Glamourer.Interop.PalettePlus; -public class PaletteImport(DalamudPluginInterface pluginInterface, DesignManager designManager, DesignFileSystem designFileSystem) : IService +public class PaletteImport(IDalamudPluginInterface pluginInterface, DesignManager designManager, DesignFileSystem designFileSystem) : IService { private string ConfigFile => Path.Combine(Path.GetDirectoryName(pluginInterface.GetPluginConfigDirectory())!, "PalettePlus.json"); @@ -37,17 +37,13 @@ public class PaletteImport(DalamudPluginInterface pluginInterface, DesignManager } var design = designManager.CreateEmpty(fullPath, true); - design.ApplyCustomize = 0; - design.ApplyEquip = 0; - design.ApplyCrest = 0; - designManager.ChangeApplyMeta(design, MetaIndex.VisorState, false); - designManager.ChangeApplyMeta(design, MetaIndex.HatState, false); - designManager.ChangeApplyMeta(design, MetaIndex.WeaponState, false); + design.Application = ApplicationCollection.None; foreach (var flag in flags.Iterate()) { designManager.ChangeApplyParameter(design, flag, true); designManager.ChangeCustomizeParameter(design, flag, palette[flag]); } + Glamourer.Log.Information($"Added design for palette {name} at {fullPath}."); } } diff --git a/Glamourer/Interop/PalettePlus/PalettePlusChecker.cs b/Glamourer/Interop/PalettePlus/PalettePlusChecker.cs deleted file mode 100644 index 6a23e90..0000000 --- a/Glamourer/Interop/PalettePlus/PalettePlusChecker.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin; -using OtterGui.Classes; -using OtterGui.Services; - -namespace Glamourer.Interop.PalettePlus; - -public sealed class PalettePlusChecker : IRequiredService, IDisposable -{ - private readonly Timer _paletteTimer; - private readonly Configuration _config; - private readonly DalamudPluginInterface _pluginInterface; - - public PalettePlusChecker(Configuration config, DalamudPluginInterface pluginInterface) - { - _config = config; - _pluginInterface = pluginInterface; - _paletteTimer = new Timer(_ => PalettePlusCheck(), null, TimeSpan.FromSeconds(30), Timeout.InfiniteTimeSpan); - } - - public void Dispose() - => _paletteTimer.Dispose(); - - public void SetAdvancedParameters(bool value) - { - _config.UseAdvancedParameters = value; - PalettePlusCheck(); - } - - private void PalettePlusCheck() - { - if (!_config.UseAdvancedParameters) - return; - - try - { - var subscriber = _pluginInterface.GetIpcSubscriber("PalettePlus.ApiVersion"); - subscriber.InvokeFunc(); - Glamourer.Messager.AddMessage(new Notification( - "You currently have Palette+ installed. This conflicts with Glamourers advanced options and will cause invalid state.\n\n" - + "Please uninstall Palette+ and restart your game. Palette+ is deprecated and no longer supported by Mare Synchronos.", - NotificationType.Warning, 10000)); - } - catch - { - // ignored - } - } -} diff --git a/Glamourer/Interop/Penumbra/ModSettingApplier.cs b/Glamourer/Interop/Penumbra/ModSettingApplier.cs index fcdc7b7..b94be09 100644 --- a/Glamourer/Interop/Penumbra/ModSettingApplier.cs +++ b/Glamourer/Interop/Penumbra/ModSettingApplier.cs @@ -3,39 +3,42 @@ using Glamourer.Services; using Glamourer.State; using OtterGui.Services; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Interop.Penumbra; -public class ModSettingApplier(PenumbraService penumbra, Configuration config, ObjectManager objects, CollectionOverrideService overrides) +public class ModSettingApplier(PenumbraService penumbra, PenumbraAutoRedrawSkip autoRedrawSkip, Configuration config, ActorObjectManager objects, CollectionOverrideService overrides) : IService { - public void HandleStateApplication(ActorState state, MergedDesign design) + private readonly HashSet _collectionTracker = []; + + public void HandleStateApplication(ActorState state, MergedDesign design, StateSource source, bool skipAutoRedraw, bool respectManual) { - if (!config.AlwaysApplyAssociatedMods || design.AssociatedMods.Count == 0) + if (!config.AlwaysApplyAssociatedMods || (design.AssociatedMods.Count == 0 && !design.ResetTemporarySettings)) return; - objects.Update(); if (!objects.TryGetValue(state.Identifier, out var data)) { Glamourer.Log.Verbose( - $"[Mod Applier] No mod settings applied because no actor for {state.Identifier} could be found to associate collection."); + $"[Mod Applier] No mod settings applied because no actor for {state.Identifier.Incognito(null)} could be found to associate collection."); return; } - var collections = new HashSet(); - + _collectionTracker.Clear(); + using var skip = autoRedrawSkip.SkipAutoUpdates(skipAutoRedraw); foreach (var actor in data.Objects) { var (collection, _, overridden) = overrides.GetCollection(actor, state.Identifier); if (collection == Guid.Empty) continue; - if (!collections.Add(collection)) + if (!_collectionTracker.Add(collection)) continue; + var index = ResetOldSettings(collection, actor, source, design.ResetTemporarySettings, respectManual); foreach (var (mod, setting) in design.AssociatedMods) { - var message = penumbra.SetMod(mod, setting, collection); + var message = penumbra.SetMod(mod, setting, source, respectManual, collection, index); if (message.Length > 0) Glamourer.Log.Verbose($"[Mod Applier] Error applying mod settings: {message}"); else @@ -45,7 +48,8 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O } } - public (List Messages, int Applied, Guid Collection, string Name, bool Overridden) ApplyModSettings(IReadOnlyDictionary settings, Actor actor) + public (List Messages, int Applied, Guid Collection, string Name, bool Overridden) ApplyModSettings( + IReadOnlyDictionary settings, Actor actor, StateSource source, bool resetOther) { var (collection, name, overridden) = overrides.GetCollection(actor); if (collection == Guid.Empty) @@ -53,9 +57,11 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O var messages = new List(); var appliedMods = 0; + + var index = ResetOldSettings(collection, actor, source, resetOther, true); foreach (var (mod, setting) in settings) { - var message = penumbra.SetMod(mod, setting, collection); + var message = penumbra.SetMod(mod, setting, source, false, collection, index); if (message.Length > 0) messages.Add($"Error applying mod settings: {message}"); else @@ -64,4 +70,26 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O return (messages, appliedMods, collection, name, overridden); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ObjectIndex? ResetOldSettings(Guid collection, Actor actor, StateSource source, bool resetOther, bool respectManual) + { + ObjectIndex? index = actor.Valid ? actor.Index : null; + if (!resetOther) + return index; + + if (index == null) + { + penumbra.RemoveAllTemporarySettings(collection, source); + if (!respectManual && source.IsFixed()) + penumbra.RemoveAllTemporarySettings(collection, StateSource.Manual); + } + else + { + penumbra.RemoveAllTemporarySettings(index.Value, source); + if (!respectManual && source.IsFixed()) + penumbra.RemoveAllTemporarySettings(index.Value, StateSource.Manual); + } + return index; + } } diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs index 72fb554..4e3c8e3 100644 --- a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -1,26 +1,30 @@ using Dalamud.Plugin.Services; using Glamourer.Api.Enums; +using Glamourer.Designs.History; using Glamourer.Events; -using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Interop.Penumbra; public class PenumbraAutoRedraw : IDisposable, IRequiredService { - private const int WaitFrames = 5; - private readonly Configuration _config; - private readonly PenumbraService _penumbra; - private readonly StateManager _state; - private readonly ObjectManager _objects; - private readonly IFramework _framework; - private readonly StateChanged _stateChanged; + private const int WaitFrames = 5; + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; + private readonly IFramework _framework; + private readonly StateChanged _stateChanged; + private readonly PenumbraAutoRedrawSkip _skip; - public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects, IFramework framework, - StateChanged stateChanged) + + public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ActorObjectManager objects, + IFramework framework, + StateChanged stateChanged, PenumbraAutoRedrawSkip skip) { _penumbra = penumbra; _config = config; @@ -28,23 +32,24 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _objects = objects; _framework = framework; _stateChanged = stateChanged; + _skip = skip; _penumbra.ModSettingChanged += OnModSettingChange; _framework.Update += OnFramework; - _stateChanged.Subscribe(OnStateChange, StateChanged.Priority.PenumbraAutoRedraw); + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.PenumbraAutoRedraw); } public void Dispose() { _penumbra.ModSettingChanged -= OnModSettingChange; _framework.Update -= OnFramework; - _stateChanged.Unsubscribe(OnStateChange); + _stateChanged.Unsubscribe(OnStateChanged); } private readonly ConcurrentQueue<(ActorState, Action, int)> _actions = []; private readonly ConcurrentSet _skips = []; private DateTime _frame; - private void OnStateChange(StateChangeType type, StateSource source, ActorState state, ActorData _1, object? _2) + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData _1, ITransaction? _2) { if (type is StateChangeType.Design && source.IsIpc()) _skips.TryAdd(state); @@ -74,7 +79,6 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService { _framework.RunOnFrameworkThread(() => { - _objects.Update(); foreach (var (id, state) in _state) { if (!_objects.TryGetValue(id, out var actors) || !actors.Valid) @@ -87,13 +91,13 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _actions.Enqueue((state, () => { foreach (var actor in actors.Objects) - _state.ReapplyState(actor, state, false, StateSource.IpcManual); + _state.ReapplyState(actor, state, false, StateSource.IpcManual, true); Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}."); }, WaitFrames)); } }); } - else if (_config.AutoRedrawEquipOnChanges) + else if (_config.AutoRedrawEquipOnChanges && !_skip.Skip) { // Only update once per frame. var playerName = _penumbra.GetCurrentPlayerCollection(); @@ -107,7 +111,7 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _frame = currentFrame; _framework.RunOnFrameworkThread(() => { - _state.ReapplyState(_objects.Player, false, StateSource.IpcManual); + _state.ReapplyState(_objects.Player, false, StateSource.IpcManual, true); Glamourer.Log.Debug( $"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); }); diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs new file mode 100644 index 0000000..8ef522c --- /dev/null +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs @@ -0,0 +1,15 @@ +using OtterGui.Classes; +using OtterGui.Services; + +namespace Glamourer.Interop.Penumbra; + +public class PenumbraAutoRedrawSkip : IService +{ + private bool _skipAutoUpdates; + + public BoolSetter SkipAutoUpdates(bool skip) + => new(ref _skipAutoUpdates, skip); + + public bool Skip + => _skipAutoUpdates; +} diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index 58422d7..b2813cd 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -1,6 +1,9 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin; +using Dalamud.Plugin.Ipc.Exceptions; using Glamourer.Events; +using Glamourer.State; +using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -21,42 +24,66 @@ public readonly record struct Mod(string Name, string DirectoryName) : IComparab } } -public readonly record struct ModSettings(Dictionary> Settings, int Priority, bool Enabled) +public readonly record struct ModSettings(Dictionary> Settings, int Priority, bool Enabled, bool ForceInherit, bool Remove) { public ModSettings() - : this(new Dictionary>(), 0, false) + : this(new Dictionary>(), 0, false, false, false) { } public static ModSettings Empty => new(); } -public unsafe class PenumbraService : IDisposable +public class PenumbraService : IDisposable { public const int RequiredPenumbraBreakingVersion = 5; - public const int RequiredPenumbraFeatureVersion = 0; + public const int RequiredPenumbraFeatureVersion = 13; - private readonly DalamudPluginInterface _pluginInterface; + private const int KeyFixed = -1610; + private const string NameFixed = "Glamourer (Automation)"; + private const int KeyManual = -6160; + private const string NameManual = "Glamourer (Manually)"; + + private readonly IDalamudPluginInterface _pluginInterface; + private readonly Configuration _config; private readonly EventSubscriber _tooltipSubscriber; private readonly EventSubscriber _clickSubscriber; private readonly EventSubscriber _creatingCharacterBase; private readonly EventSubscriber _createdCharacterBase; private readonly EventSubscriber _modSettingChanged; + private readonly EventSubscriber _pcpParsed; + private readonly EventSubscriber _pcpCreated; - private global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier? _collectionByIdentifier; - private global::Penumbra.Api.IpcSubscribers.GetCollections? _collections; - private global::Penumbra.Api.IpcSubscribers.RedrawObject? _redraw; - private global::Penumbra.Api.IpcSubscribers.GetDrawObjectInfo? _drawObjectInfo; - private global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndex? _cutsceneParent; - private global::Penumbra.Api.IpcSubscribers.GetCollectionForObject? _objectCollection; - private global::Penumbra.Api.IpcSubscribers.GetModList? _getMods; - private global::Penumbra.Api.IpcSubscribers.GetCollection? _currentCollection; - private global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings? _getCurrentSettings; - private global::Penumbra.Api.IpcSubscribers.TrySetMod? _setMod; - private global::Penumbra.Api.IpcSubscribers.TrySetModPriority? _setModPriority; - private global::Penumbra.Api.IpcSubscribers.TrySetModSetting? _setModSetting; - private global::Penumbra.Api.IpcSubscribers.TrySetModSettings? _setModSettings; - private global::Penumbra.Api.IpcSubscribers.OpenMainWindow? _openModPage; + private global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier? _collectionByIdentifier; + private global::Penumbra.Api.IpcSubscribers.GetCollections? _collections; + private global::Penumbra.Api.IpcSubscribers.RedrawObject? _redraw; + private global::Penumbra.Api.IpcSubscribers.GetCollectionForObject? _objectCollection; + private global::Penumbra.Api.IpcSubscribers.GetModList? _getMods; + private global::Penumbra.Api.IpcSubscribers.GetCollection? _currentCollection; + private global::Penumbra.Api.IpcSubscribers.GetCurrentModSettingsWithTemp? _getCurrentSettingsWithTemp; + private global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings? _getCurrentSettings; + private global::Penumbra.Api.IpcSubscribers.GetAllModSettings? _getAllSettings; + private global::Penumbra.Api.IpcSubscribers.TryInheritMod? _inheritMod; + private global::Penumbra.Api.IpcSubscribers.TrySetMod? _setMod; + private global::Penumbra.Api.IpcSubscribers.TrySetModPriority? _setModPriority; + private global::Penumbra.Api.IpcSubscribers.TrySetModSetting? _setModSetting; + private global::Penumbra.Api.IpcSubscribers.TrySetModSettings? _setModSettings; + private global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings? _setTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer? _setTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings? _removeTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettingsPlayer? _removeTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings? _removeAllTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettingsPlayer? _removeAllTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettings? _queryTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettingsPlayer? _queryTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.OpenMainWindow? _openModPage; + private global::Penumbra.Api.IpcSubscribers.GetChangedItems? _getChangedItems; + private global::Penumbra.Api.IpcSubscribers.RegisterSettingsSection? _registerSettingsSection; + private global::Penumbra.Api.IpcSubscribers.UnregisterSettingsSection? _unregisterSettingsSection; + private IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>? _changedItems; + private Func? _checkCurrentChangedItems; + private Func? _checkCutsceneParent; + private Func? _getGameObject; private readonly IDisposable _initializedEvent; private readonly IDisposable _disposedEvent; @@ -68,10 +95,11 @@ public unsafe class PenumbraService : IDisposable public int CurrentMinor { get; private set; } public DateTime AttachTime { get; private set; } - public PenumbraService(DalamudPluginInterface pi, PenumbraReloaded penumbraReloaded) + public PenumbraService(IDalamudPluginInterface pi, PenumbraReloaded penumbraReloaded, Configuration config) { _pluginInterface = pi; _penumbraReloaded = penumbraReloaded; + _config = config; _initializedEvent = global::Penumbra.Api.IpcSubscribers.Initialized.Subscriber(pi, Reattach); _disposedEvent = global::Penumbra.Api.IpcSubscribers.Disposed.Subscriber(pi, Unattach); _tooltipSubscriber = global::Penumbra.Api.IpcSubscribers.ChangedItemTooltip.Subscriber(pi); @@ -79,6 +107,8 @@ public unsafe class PenumbraService : IDisposable _createdCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatedCharacterBase.Subscriber(pi); _creatingCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatingCharacterBase.Subscriber(pi); _modSettingChanged = global::Penumbra.Api.IpcSubscribers.ModSettingChanged.Subscriber(pi); + _pcpCreated = global::Penumbra.Api.IpcSubscribers.CreatingPcp.Subscriber(pi); + _pcpParsed = global::Penumbra.Api.IpcSubscribers.ParsingPcp.Subscriber(pi); Reattach(); } @@ -113,22 +143,36 @@ public unsafe class PenumbraService : IDisposable remove => _modSettingChanged.Event -= value; } + public event Action PcpCreated + { + add => _pcpCreated.Event += value; + remove => _pcpCreated.Event -= value; + } + + public event Action PcpParsed + { + add => _pcpParsed.Event += value; + remove => _pcpParsed.Event -= value; + } + + public event Action? DrawSettingsSection; + + private void InvokeDrawSettingsSection() + => DrawSettingsSection?.Invoke(); + public Dictionary GetCollections() => Available ? _collections!.Invoke() : []; - public ModSettings GetModSettings(in Mod mod) + public ModSettings GetModSettings(in Mod mod, out string source) { + source = string.Empty; if (!Available) return ModSettings.Empty; try { var collection = _currentCollection!.Invoke(ApiCollectionType.Current); - var (ec, tuple) = _getCurrentSettings!.Invoke(collection!.Value.Id, mod.DirectoryName); - if (ec is not PenumbraApiEc.Success) - return ModSettings.Empty; - - return tuple.HasValue ? new ModSettings(tuple.Value.Item3, tuple.Value.Item2, tuple.Value.Item1) : ModSettings.Empty; + return GetSettings(collection!.Value.Id, mod.DirectoryName, mod.Name, out source); } catch (Exception ex) { @@ -137,6 +181,38 @@ public unsafe class PenumbraService : IDisposable } } + private ModSettings GetSettings(Guid collection, string modDirectory, string modName, out string source) + { + if (_getCurrentSettingsWithTemp != null) + { + source = string.Empty; + var (ec, tuple) = _getCurrentSettingsWithTemp!.Invoke(collection, modDirectory, modName, false, false, KeyFixed); + if (ec is not PenumbraApiEc.Success) + return ModSettings.Empty; + + return tuple.HasValue + ? new ModSettings(tuple.Value.Item3, tuple.Value.Item2, tuple.Value.Item1, false, false) + : ModSettings.Empty; + } + + if (_queryTemporaryModSettings != null) + { + var tempEc = _queryTemporaryModSettings.Invoke(collection, modDirectory, out var tempTuple, out source, 0, modName); + if (tempEc is PenumbraApiEc.Success && tempTuple != null) + return new ModSettings(tempTuple.Value.Settings, tempTuple.Value.Priority, tempTuple.Value.Enabled, + tempTuple.Value.ForceInherit, false); + } + + source = string.Empty; + var (ec2, tuple2) = _getCurrentSettings!.Invoke(collection, modDirectory, modName); + if (ec2 is not PenumbraApiEc.Success) + return ModSettings.Empty; + + return tuple2.HasValue + ? new ModSettings(tuple2.Value.Item3, tuple2.Value.Item2, tuple2.Value.Item1, false, false) + : ModSettings.Empty; + } + public (Guid Id, string Name)? CollectionByIdentifier(string identifier) { if (!Available) @@ -149,27 +225,58 @@ public unsafe class PenumbraService : IDisposable return ret[0]; } - public IReadOnlyList<(Mod Mod, ModSettings Settings)> GetMods() + public IReadOnlyList<(Mod Mod, ModSettings Settings, int Count)> GetMods(IReadOnlyList data) { if (!Available) return []; try { - var allMods = _getMods!.Invoke(); - var collection = _currentCollection!.Invoke(ApiCollectionType.Current); - return allMods - .Select(m => (m.Key, m.Value, _getCurrentSettings!.Invoke(collection!.Value.Id, m.Key))) - .Where(t => t.Item3.Item1 is PenumbraApiEc.Success) - .Select(t => (new Mod(t.Item2, t.Item1), - !t.Item3.Item2.HasValue - ? ModSettings.Empty - : new ModSettings(t.Item3.Item2!.Value.Item3, t.Item3.Item2!.Value.Item2, t.Item3.Item2!.Value.Item1))) - .OrderByDescending(p => p.Item2.Enabled) - .ThenBy(p => p.Item1.Name) - .ThenBy(p => p.Item1.DirectoryName) - .ThenByDescending(p => p.Item2.Priority) - .ToList(); + var allMods = _getMods!.Invoke(); + var currentCollection = _currentCollection!.Invoke(ApiCollectionType.Current); + var withSettings = WithSettings(allMods, currentCollection!.Value.Id); + var withCounts = WithCounts(withSettings, allMods.Count); + return OrderList(withCounts, allMods.Count); + + IEnumerable<(Mod Mod, ModSettings Settings)> WithSettings(Dictionary mods, Guid collection) + { + if (_getAllSettings != null) + { + var allSettings = _getAllSettings.Invoke(collection, false, false, KeyFixed); + if (allSettings.Item1 is PenumbraApiEc.Success) + return mods.Select(m => (new Mod(m.Value, m.Key), + allSettings.Item2!.TryGetValue(m.Key, out var s) + ? new ModSettings(s.Item3, s.Item2, s.Item1, s is { Item4: true, Item5: true }, false) + : ModSettings.Empty)); + } + + return mods.Select(m => (new Mod(m.Value, m.Key), GetSettings(collection, m.Key, m.Value, out _))); + } + + IEnumerable<(Mod Mod, ModSettings Settings, int Count)> WithCounts(IEnumerable<(Mod Mod, ModSettings Settings)> mods, int count) + { + if (_changedItems != null && _changedItems.Count == count) + return mods.Select((m, idx) => (m.Mod, m.Settings, CountItems(_changedItems[idx].ChangedItems, data))); + + return mods.Select(p => (p.Item1, p.Item2, CountItems(_getChangedItems!.Invoke(p.Item1.DirectoryName, p.Item1.Name), data))); + + static int CountItems(IReadOnlyDictionary dict, IReadOnlyList data) + => data.Count(dict.ContainsKey); + } + + static IReadOnlyList<(Mod Mod, ModSettings Settings, int Count)> OrderList( + IEnumerable<(Mod Mod, ModSettings Settings, int Count)> enumerable, int count) + { + var array = new (Mod Mod, ModSettings Settings, int Count)[count]; + var i = 0; + foreach (var t in enumerable.OrderByDescending(p => p.Item2.Enabled) + .ThenByDescending(p => p.Item3) + .ThenBy(p => p.Item1.Name) + .ThenBy(p => p.Item1.DirectoryName) + .ThenByDescending(p => p.Item2.Priority)) + array[i++] = t; + return array; + } } catch (Exception ex) { @@ -183,7 +290,7 @@ public unsafe class PenumbraService : IDisposable if (!Available) return; - if (_openModPage!.Invoke(TabType.Mods, mod.DirectoryName) == PenumbraApiEc.ModMissing) + if (_openModPage!.Invoke(TabType.Mods, mod.DirectoryName, mod.Name) == PenumbraApiEc.ModMissing) Glamourer.Messager.NotificationMessage($"Could not open the mod {mod.Name}, no fitting mod was found in your Penumbra install.", NotificationType.Info, false); } @@ -195,7 +302,8 @@ public unsafe class PenumbraService : IDisposable /// Try to set all mod settings as desired. Only sets when the mod should be enabled. /// If it is disabled, ignore all other settings. /// - public string SetMod(Mod mod, ModSettings settings, Guid? collectionInput = null) + public string SetMod(Mod mod, ModSettings settings, StateSource source, bool respectManual, Guid? collectionInput = null, + ObjectIndex? index = null) { if (!Available) return "Penumbra is not available."; @@ -204,40 +312,10 @@ public unsafe class PenumbraService : IDisposable try { var collection = collectionInput ?? _currentCollection!.Invoke(ApiCollectionType.Current)!.Value.Id; - var ec = _setMod!.Invoke(collection, mod.DirectoryName, settings.Enabled); - switch (ec) - { - case PenumbraApiEc.ModMissing: return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found."; - case PenumbraApiEc.CollectionMissing: return $"The collection {collection} could not be found."; - } - - if (!settings.Enabled) - return string.Empty; - - ec = _setModPriority!.Invoke(collection, mod.DirectoryName, settings.Priority); - Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail."); - - foreach (var (setting, list) in settings.Settings) - { - ec = list.Count == 1 - ? _setModSetting!.Invoke(collection, mod.DirectoryName, setting, list[0]) - : _setModSettings!.Invoke(collection, mod.DirectoryName, setting, list); - switch (ec) - { - case PenumbraApiEc.OptionGroupMissing: - sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}."); - break; - case PenumbraApiEc.OptionMissing: - sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}."); - break; - case PenumbraApiEc.Success: - case PenumbraApiEc.NothingChanged: - break; - default: - sb.AppendLine($"Could not apply options in the option group {setting} in mod {mod.Name} for unknown reason {ec}."); - break; - } - } + if (_config.UseTemporarySettings && _setTemporaryModSettings != null) + SetModTemporary(sb, mod, settings, collection, respectManual, index, source); + else + SetModPermanent(sb, mod, settings, collection); return sb.ToString(); } @@ -247,6 +325,130 @@ public unsafe class PenumbraService : IDisposable } } + public void RemoveAllTemporarySettings(Guid collection, StateSource source) + => _removeAllTemporaryModSettings?.Invoke(collection, source.IsFixed() ? KeyFixed : KeyManual); + + public void RemoveAllTemporarySettings(ObjectIndex index, StateSource source) + => _removeAllTemporaryModSettingsPlayer?.Invoke(index.Index, source.IsFixed() ? KeyFixed : KeyManual); + + public void ClearAllTemporarySettings(bool fix, bool manual) + { + if (!Available || _removeAllTemporaryModSettings == null) + return; + + var collections = _collections!.Invoke(); + foreach (var collection in collections) + { + if (fix) + RemoveAllTemporarySettings(collection.Key, StateSource.Fixed); + if (manual) + RemoveAllTemporarySettings(collection.Key, StateSource.Manual); + } + } + + public (string ModDirectory, string ModName)[] CheckCurrentChangedItem(string changedItem) + => _checkCurrentChangedItems?.Invoke(changedItem) ?? []; + + private void SetModTemporary(StringBuilder sb, Mod mod, ModSettings settings, Guid collection, bool respectManual, ObjectIndex? index, + StateSource source) + { + var (key, name) = source.IsFixed() ? (KeyFixed, NameFixed) : (KeyManual, NameManual); + // Check for existing manual settings and do not apply fixed on top of them if respecting manual changes. + if (key is KeyFixed && respectManual) + { + var existingSource = string.Empty; + var ec = index.HasValue + ? _queryTemporaryModSettingsPlayer?.Invoke(index.Value.Index, mod.DirectoryName, out _, + out existingSource, key, mod.Name) + ?? PenumbraApiEc.InvalidArgument + : _queryTemporaryModSettings?.Invoke(collection, mod.DirectoryName, out _, + out existingSource, key, mod.Name) + ?? PenumbraApiEc.InvalidArgument; + if (ec is PenumbraApiEc.Success && existingSource is NameManual) + { + Glamourer.Log.Debug( + $"Skipped applying mod settings for [{mod.Name}] through automation because manual settings from Glamourer existed."); + return; + } + } + + var ex = settings.Remove + ? index.HasValue + ? _removeTemporaryModSettingsPlayer!.Invoke(index.Value.Index, mod.DirectoryName, key, mod.Name) + : _removeTemporaryModSettings!.Invoke(collection, mod.DirectoryName, key, mod.Name) + : index.HasValue + ? _setTemporaryModSettingsPlayer!.Invoke(index.Value.Index, mod.DirectoryName, settings.ForceInherit, settings.Enabled, + settings.Priority, + settings.Settings.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value), name, key, mod.Name) + : _setTemporaryModSettings!.Invoke(collection, mod.DirectoryName, settings.ForceInherit, settings.Enabled, settings.Priority, + settings.Settings.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value), name, key, mod.Name); + switch (ex) + { + case PenumbraApiEc.InvalidArgument: + sb.Append($"No actor with index {index!.Value.Index} could be identified."); + return; + case PenumbraApiEc.ModMissing: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] could not be found."); + return; + case PenumbraApiEc.CollectionMissing: + sb.Append($"The collection {collection} could not be found."); + return; + case PenumbraApiEc.TemporarySettingImpossible: + sb.Append($"The collection {collection} can not have settings."); + return; + case PenumbraApiEc.TemporarySettingDisallowed: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] already has temporary settings with a different key in {collection}."); + return; + case PenumbraApiEc.OptionGroupMissing: + case PenumbraApiEc.OptionMissing: + sb.Append($"The provided settings for {mod.Name} [{mod.DirectoryName}] did not correspond to its actual options."); + return; + } + } + + private void SetModPermanent(StringBuilder sb, Mod mod, ModSettings settings, Guid collection) + { + var ec = settings.ForceInherit + ? _inheritMod!.Invoke(collection, mod.DirectoryName, true, mod.Name) + : _setMod!.Invoke(collection, mod.DirectoryName, settings.Enabled, mod.Name); + switch (ec) + { + case PenumbraApiEc.ModMissing: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] could not be found."); + return; + case PenumbraApiEc.CollectionMissing: + sb.Append($"The collection {collection} could not be found."); + return; + } + + if (settings.ForceInherit || !settings.Enabled) + return; + + ec = _setModPriority!.Invoke(collection, mod.DirectoryName, settings.Priority, mod.Name); + Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail."); + + foreach (var (setting, list) in settings.Settings) + { + ec = list.Count == 1 + ? _setModSetting!.Invoke(collection, mod.DirectoryName, setting, list[0], mod.Name) + : _setModSettings!.Invoke(collection, mod.DirectoryName, setting, list, mod.Name); + switch (ec) + { + case PenumbraApiEc.OptionGroupMissing: sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}."); break; + case PenumbraApiEc.OptionMissing: + sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}."); + break; + case PenumbraApiEc.Success: + case PenumbraApiEc.NothingChanged: + break; + default: + sb.AppendLine($"Could not apply options in the option group {setting} in mod {mod.Name} for unknown reason {ec}."); + break; + } + } + } + + /// Obtain the name of the collection currently assigned to the player. public Guid GetCurrentPlayerCollection() { @@ -272,11 +474,11 @@ public unsafe class PenumbraService : IDisposable /// Obtain the game object corresponding to a draw object. public Actor GameObjectFromDrawObject(Model drawObject) - => Available ? _drawObjectInfo!.Invoke(drawObject.Address).Item1 : Actor.Null; + => _getGameObject?.Invoke(drawObject.Address) ?? Actor.Null; /// Obtain the parent of a cutscene actor if it is known. public short CutsceneParent(ushort idx) - => (short)(Available ? _cutsceneParent!.Invoke(idx) : -1); + => (short)(_checkCutsceneParent?.Invoke(idx) ?? -1); /// Try to redraw the given actor. public void RedrawObject(Actor actor, RedrawType settings) @@ -337,22 +539,46 @@ public unsafe class PenumbraService : IDisposable _clickSubscriber.Enable(); _creatingCharacterBase.Enable(); _createdCharacterBase.Enable(); + _pcpCreated.Enable(); + _pcpParsed.Enable(); _modSettingChanged.Enable(); _collectionByIdentifier = new global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier(_pluginInterface); - _collections = new global::Penumbra.Api.IpcSubscribers.GetCollections(_pluginInterface); - _redraw = new global::Penumbra.Api.IpcSubscribers.RedrawObject(_pluginInterface); - _drawObjectInfo = new global::Penumbra.Api.IpcSubscribers.GetDrawObjectInfo(_pluginInterface); - _cutsceneParent = new global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndex(_pluginInterface); - _objectCollection = new global::Penumbra.Api.IpcSubscribers.GetCollectionForObject(_pluginInterface); - _getMods = new global::Penumbra.Api.IpcSubscribers.GetModList(_pluginInterface); - _currentCollection = new global::Penumbra.Api.IpcSubscribers.GetCollection(_pluginInterface); - _getCurrentSettings = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings(_pluginInterface); - _setMod = new global::Penumbra.Api.IpcSubscribers.TrySetMod(_pluginInterface); - _setModPriority = new global::Penumbra.Api.IpcSubscribers.TrySetModPriority(_pluginInterface); - _setModSetting = new global::Penumbra.Api.IpcSubscribers.TrySetModSetting(_pluginInterface); - _setModSettings = new global::Penumbra.Api.IpcSubscribers.TrySetModSettings(_pluginInterface); - _openModPage = new global::Penumbra.Api.IpcSubscribers.OpenMainWindow(_pluginInterface); - Available = true; + _collections = new global::Penumbra.Api.IpcSubscribers.GetCollections(_pluginInterface); + _redraw = new global::Penumbra.Api.IpcSubscribers.RedrawObject(_pluginInterface); + _checkCutsceneParent = new global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndexFunc(_pluginInterface).Invoke(); + _getGameObject = new global::Penumbra.Api.IpcSubscribers.GetGameObjectFromDrawObjectFunc(_pluginInterface).Invoke(); + _objectCollection = new global::Penumbra.Api.IpcSubscribers.GetCollectionForObject(_pluginInterface); + _getMods = new global::Penumbra.Api.IpcSubscribers.GetModList(_pluginInterface); + _currentCollection = new global::Penumbra.Api.IpcSubscribers.GetCollection(_pluginInterface); + _getCurrentSettings = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings(_pluginInterface); + _inheritMod = new global::Penumbra.Api.IpcSubscribers.TryInheritMod(_pluginInterface); + _setMod = new global::Penumbra.Api.IpcSubscribers.TrySetMod(_pluginInterface); + _setModPriority = new global::Penumbra.Api.IpcSubscribers.TrySetModPriority(_pluginInterface); + _setModSetting = new global::Penumbra.Api.IpcSubscribers.TrySetModSetting(_pluginInterface); + _setModSettings = new global::Penumbra.Api.IpcSubscribers.TrySetModSettings(_pluginInterface); + _openModPage = new global::Penumbra.Api.IpcSubscribers.OpenMainWindow(_pluginInterface); + _getChangedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItems(_pluginInterface); + _setTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings(_pluginInterface); + _setTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer(_pluginInterface); + _removeTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings(_pluginInterface); + _removeTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettingsPlayer(_pluginInterface); + _removeAllTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings(_pluginInterface); + _removeAllTemporaryModSettingsPlayer = + new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettingsPlayer(_pluginInterface); + _queryTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettings(_pluginInterface); + _queryTemporaryModSettingsPlayer = + new global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettingsPlayer(_pluginInterface); + _getCurrentSettingsWithTemp = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettingsWithTemp(_pluginInterface); + _getAllSettings = new global::Penumbra.Api.IpcSubscribers.GetAllModSettings(_pluginInterface); + _changedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItemAdapterList(_pluginInterface).Invoke(); + _checkCurrentChangedItems = + new global::Penumbra.Api.IpcSubscribers.CheckCurrentChangedItemFunc(_pluginInterface).Invoke(); + _registerSettingsSection = new global::Penumbra.Api.IpcSubscribers.RegisterSettingsSection(_pluginInterface); + _unregisterSettingsSection = new global::Penumbra.Api.IpcSubscribers.UnregisterSettingsSection(_pluginInterface); + + _registerSettingsSection.Invoke(InvokeDrawSettingsSection); + + Available = true; _penumbraReloaded.Invoke(); Glamourer.Log.Debug("Glamourer attached to Penumbra."); } @@ -371,29 +597,57 @@ public unsafe class PenumbraService : IDisposable _creatingCharacterBase.Disable(); _createdCharacterBase.Disable(); _modSettingChanged.Disable(); + _pcpCreated.Disable(); + _pcpParsed.Disable(); + try + { + _unregisterSettingsSection?.Invoke(InvokeDrawSettingsSection); + } + catch (IpcNotReadyError) + { + // Ignore. + } + if (Available) { - _collectionByIdentifier = null; - _collections = null; - _redraw = null; - _drawObjectInfo = null; - _cutsceneParent = null; - _objectCollection = null; - _getMods = null; - _currentCollection = null; - _getCurrentSettings = null; - _setMod = null; - _setModPriority = null; - _setModSetting = null; - _setModSettings = null; - _openModPage = null; - Available = false; + _collectionByIdentifier = null; + _collections = null; + _redraw = null; + _getGameObject = null; + _checkCutsceneParent = null; + _objectCollection = null; + _getMods = null; + _currentCollection = null; + _getCurrentSettings = null; + _getCurrentSettingsWithTemp = null; + _getAllSettings = null; + _inheritMod = null; + _setMod = null; + _setModPriority = null; + _setModSetting = null; + _setModSettings = null; + _openModPage = null; + _setTemporaryModSettings = null; + _setTemporaryModSettingsPlayer = null; + _removeTemporaryModSettings = null; + _removeTemporaryModSettingsPlayer = null; + _removeAllTemporaryModSettings = null; + _removeAllTemporaryModSettingsPlayer = null; + _queryTemporaryModSettings = null; + _queryTemporaryModSettingsPlayer = null; + _getChangedItems = null; + _changedItems = null; + _checkCurrentChangedItems = null; + _registerSettingsSection = null; + _unregisterSettingsSection = null; + Available = false; Glamourer.Log.Debug("Glamourer detached from Penumbra."); } } public void Dispose() { + ClearAllTemporarySettings(true, true); Unattach(); _tooltipSubscriber.Dispose(); _clickSubscriber.Dispose(); @@ -402,5 +656,7 @@ public unsafe class PenumbraService : IDisposable _initializedEvent.Dispose(); _disposedEvent.Dispose(); _modSettingChanged.Dispose(); + _pcpCreated.Dispose(); + _pcpParsed.Dispose(); } } diff --git a/Glamourer/Interop/ScalingService.cs b/Glamourer/Interop/ScalingService.cs index 66036d9..2a89a25 100644 --- a/Glamourer/Interop/ScalingService.cs +++ b/Glamourer/Interop/ScalingService.cs @@ -1,26 +1,38 @@ -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Penumbra.GameData; using Penumbra.GameData.Interop; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.State; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; namespace Glamourer.Interop; public unsafe class ScalingService : IDisposable { - public ScalingService(IGameInteropProvider interop) + private readonly ActorManager _actors; + private readonly StateManager _state; + + public ScalingService(IGameInteropProvider interop, StateManager state, ActorManager actors) { + _state = state; + _actors = actors; interop.InitializeFromAttributes(this); _setupMountHook = - interop.HookFromAddress((nint)Character.MountContainer.MemberFunctionPointers.SetupMount, SetupMountDetour); - _setupOrnamentHook = interop.HookFromAddress((nint)Ornament.MemberFunctionPointers.SetupOrnament, SetupOrnamentDetour); + interop.HookFromAddress((nint)MountContainer.MemberFunctionPointers.SetupMount, SetupMountDetour); _calculateHeightHook = - interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); + interop.HookFromAddress((nint)ModelContainer.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); + _placeMinionHook = interop.HookFromAddress((nint)Companion.MemberFunctionPointers.PlaceCompanion, PlaceMinionDetour); + //_updateOrnamentHook = + // interop.HookFromAddress((nint)Ornament.MemberFunctionPointers.UpdateOrnament, UpdateOrnamentDetour); _setupMountHook.Enable(); - _setupOrnamentHook.Enable(); + _updateOrnamentHook.Enable(); _placeMinionHook.Enable(); _calculateHeightHook.Enable(); } @@ -28,52 +40,45 @@ public unsafe class ScalingService : IDisposable public void Dispose() { _setupMountHook.Dispose(); - _setupOrnamentHook.Dispose(); + _updateOrnamentHook.Dispose(); _placeMinionHook.Dispose(); _calculateHeightHook.Dispose(); } - private delegate void SetupMount(Character.MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4); - private delegate void SetupOrnament(Ornament* ornament, uint* unk1, float* unk2); + private delegate void SetupMount(MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4); + private delegate void UpdateOrnament(OrnamentContainer* ornament); private delegate void PlaceMinion(Companion* character); - private delegate float CalculateHeight(Character* character); + private delegate float CalculateHeight(ModelContainer* character); private readonly Hook _setupMountHook; - private readonly Hook _setupOrnamentHook; + // TODO: Use client structs sig. + [Signature(Sigs.UpdateOrnament, DetourName = nameof(UpdateOrnamentDetour))] + private readonly Hook _updateOrnamentHook = null!; private readonly Hook _calculateHeightHook; - // TODO: Use client structs sig. - [Signature("48 89 5C 24 ?? 55 57 41 57 48 8D 6C 24", DetourName = nameof(PlaceMinionDetour))] private readonly Hook _placeMinionHook = null!; - private void SetupMountDetour(Character.MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4) + private void SetupMountDetour(MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4) { - var (race, clan, gender) = GetScaleRelevantCustomize(&container->OwnerObject->Character); - SetScaleCustomize(&container->OwnerObject->Character, container->OwnerObject->Character.GameObject.DrawObject); + var (race, clan, gender) = GetScaleRelevantCustomize(container->OwnerObject); + SetScaleCustomize(container->OwnerObject, container->OwnerObject->DrawObject); _setupMountHook.Original(container, mountId, unk1, unk2, unk3, unk4); - SetScaleCustomize(&container->OwnerObject->Character, race, clan, gender); + SetScaleCustomize(container->OwnerObject, race, clan, gender); } - private void SetupOrnamentDetour(Ornament* ornament, uint* unk1, float* unk2) + private void UpdateOrnamentDetour(OrnamentContainer* container) { - var character = ornament->Character.GetParentCharacter(); - if (character == null) - { - _setupOrnamentHook.Original(ornament, unk1, unk2); - return; - } - - var (race, clan, gender) = GetScaleRelevantCustomize(character); - SetScaleCustomize(character, character->GameObject.DrawObject); - _setupOrnamentHook.Original(ornament, unk1, unk2); - SetScaleCustomize(character, race, clan, gender); + var (race, clan, gender) = GetScaleRelevantCustomize(container->OwnerObject); + SetScaleCustomize(container->OwnerObject, container->OwnerObject->DrawObject); + _updateOrnamentHook.Original(container); + SetScaleCustomize(container->OwnerObject, race, clan, gender); } private void PlaceMinionDetour(Companion* companion) { - var owner = (Actor)((nint*)companion)[0x386]; + var owner = (Actor)(GameObject*)companion->Owner; if (!owner.IsCharacter) { _placeMinionHook.Original(companion); @@ -83,18 +88,27 @@ public unsafe class ScalingService : IDisposable var mdl = owner.Model; var oldRace = owner.AsCharacter->DrawData.CustomizeData.Race; if (mdl.IsHuman) + { owner.AsCharacter->DrawData.CustomizeData.Race = mdl.AsHuman->Customize.Race; + } + else + { + var actor = _actors.FromObject(owner, out _, true, false, true); + if (_state.TryGetValue(actor, out var state)) + owner.AsCharacter->DrawData.CustomizeData.Race = (byte)state.ModelData.Customize.Race; + } + _placeMinionHook.Original(companion); owner.AsCharacter->DrawData.CustomizeData.Race = oldRace; } } - private float CalculateHeightDetour(Character* character) + private float CalculateHeightDetour(ModelContainer* container) { - var (gender, bodyType, clan, height) = GetHeightRelevantCustomize(character); - SetHeightCustomize(character, character->GameObject.DrawObject); - var ret = _calculateHeightHook.Original(character); - SetHeightCustomize(character, gender, bodyType, clan, height); + var (gender, bodyType, clan, height) = GetHeightRelevantCustomize(container->OwnerObject); + SetHeightCustomize(container->OwnerObject, container->OwnerObject->DrawObject); + var ret = _calculateHeightHook.Original(container); + SetHeightCustomize(container->OwnerObject, gender, bodyType, clan, height); return ret; } @@ -107,12 +121,20 @@ public unsafe class ScalingService : IDisposable character->DrawData.CustomizeData.Tribe, character->DrawData.CustomizeData[(int)CustomizeIndex.Height]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static void SetScaleCustomize(Character* character, Model model) + private void SetScaleCustomize(Character* character, Model model) { - if (!model.IsHuman) + if (model.IsHuman) + { + SetScaleCustomize(character, model.AsHuman->Customize.Race, model.AsHuman->Customize.Tribe, model.AsHuman->Customize.Sex); + return; + } + + var actor = _actors.FromObject(character, out _, true, false, true); + if (!_state.TryGetValue(actor, out var state)) return; - SetScaleCustomize(character, model.AsHuman->Customize.Race, model.AsHuman->Customize.Tribe, model.AsHuman->Customize.Sex); + ref var customize = ref state.ModelData.Customize; + SetScaleCustomize(character, (byte)customize.Race, (byte)customize.Clan, customize.Gender.ToGameByte()); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -124,13 +146,22 @@ public unsafe class ScalingService : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static void SetHeightCustomize(Character* character, Model model) + private void SetHeightCustomize(Character* character, Model model) { - if (!model.IsHuman) + if (model.IsHuman) + { + SetHeightCustomize(character, model.AsHuman->Customize.Sex, model.AsHuman->Customize.BodyType, model.AsHuman->Customize.Tribe, + model.AsHuman->Customize[(int)CustomizeIndex.Height]); + return; + } + + var actor = _actors.FromObject(character, out _, true, false, true); + if (!_state.TryGetValue(actor, out var state)) return; - SetHeightCustomize(character, model.AsHuman->Customize.Sex, model.AsHuman->Customize.BodyType, model.AsHuman->Customize.Tribe, - model.AsHuman->Customize[(int)CustomizeIndex.Height]); + ref var customize = ref state.ModelData.Customize; + SetHeightCustomize(character, customize.Gender.ToGameByte(), customize.BodyType.Value, (byte)customize.Clan, + customize[global::Penumbra.GameData.Enums.CustomizeIndex.Height].Value); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs deleted file mode 100644 index 5cfbcbe..0000000 --- a/Glamourer/Interop/Structs/ActorData.cs +++ /dev/null @@ -1,47 +0,0 @@ -using OtterGui.Log; -using Penumbra.GameData.Interop; - -namespace Glamourer.Interop.Structs; - -/// -/// A single actor with its label and the list of associated game objects. -/// -public readonly struct ActorData -{ - public readonly List Objects; - public readonly string Label; - - public bool Valid - => Objects.Count > 0; - - public ActorData(Actor actor, string label) - { - Objects = [actor]; - Label = label; - } - - public static readonly ActorData Invalid = new(false); - - private ActorData(bool _) - { - Objects = []; - Label = string.Empty; - } - - public LazyString ToLazyString(string invalid) - { - var objects = Objects; - return Valid - ? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString()))) - : new LazyString(() => invalid); - } - - private ActorData(List objects, string label) - { - Objects = objects; - Label = label; - } - - public ActorData OnlyGPose() - => new(Objects.Where(o => o.IsGPoseOrCutscene).ToList(), Label); -} diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f2f7423..3ef99d9 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,8 +1,11 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Network; using Glamourer.Events; using Penumbra.GameData; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; @@ -11,19 +14,34 @@ namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly SlotUpdating SlotUpdatingEvent; + public readonly EquipSlotUpdating EquipSlotUpdatingEvent; + public readonly BonusSlotUpdating BonusSlotUpdatingEvent; + public readonly GearsetDataLoaded GearsetDataLoadedEvent; + private readonly DictBonusItems _bonusItems; - public UpdateSlotService(SlotUpdating slotUpdating, IGameInteropProvider interop) + public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, + IGameInteropProvider interop, DictBonusItems bonusItems) { - SlotUpdatingEvent = slotUpdating; + EquipSlotUpdatingEvent = equipSlotUpdating; + BonusSlotUpdatingEvent = bonusSlotUpdating; + GearsetDataLoadedEvent = gearsetDataLoaded; + _bonusItems = bonusItems; + interop.InitializeFromAttributes(this); + _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); _flagSlotForUpdateHook.Enable(); + _flagBonusSlotForUpdateHook.Enable(); + _loadGearsetDataHook.Enable(); } public void Dispose() - => _flagSlotForUpdateHook.Dispose(); + { + _flagSlotForUpdateHook.Dispose(); + _flagBonusSlotForUpdateHook.Dispose(); + _loadGearsetDataHook.Dispose(); + } - public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) + public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) { if (!drawObject.IsCharacterBase) return; @@ -31,29 +49,98 @@ public unsafe class UpdateSlotService : IDisposable FlagSlotForUpdateInterop(drawObject, slot, data); } - public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain) - => UpdateSlot(drawObject, slot, armor.With(stain)); + public void UpdateBonusSlot(Model drawObject, BonusItemFlag slot, CharacterArmor data) + { + if (!drawObject.IsCharacterBase) + return; + + var index = slot.ToIndex(); + if (index == uint.MaxValue) + return; + + _flagBonusSlotForUpdateHook.Original(drawObject.Address, index, &data); + } + + public void UpdateGlasses(Model drawObject, BonusItemId id) + { + if (!_bonusItems.TryGetValue(id, out var glasses)) + return; + + var armor = new CharacterArmor(glasses.PrimaryId, glasses.Variant, StainIds.None); + _flagBonusSlotForUpdateHook.Original(drawObject.Address, BonusItemFlag.Glasses.ToIndex(), &armor); + } + + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainIds stains) + => UpdateEquipSlot(drawObject, slot, armor.With(stains)); public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor) - => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain); + => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stains); - public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) - => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain); + public void UpdateStain(Model drawObject, EquipSlot slot, StainIds stains) + => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stains); private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] private readonly Hook _flagSlotForUpdateHook = null!; + [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] + private readonly Hook _flagBonusSlotForUpdateHook = null!; + + /// Detours the func that makes all FlagSlotForUpdate calls on a gearset change or initial render of a given actor (Only Cases this is Called). + /// Logic done after returning the original hook executes After all equipment/weapon/crest data is loaded into the Actors BaseData. + /// + private delegate ulong LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); + private readonly Hook _loadGearsetDataHook; + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; - SlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); + EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } + private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) + { + var slot = slotIdx.ToBonusSlot(); + var returnValue = ulong.MaxValue; + BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + return returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + } + private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) - => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + { + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Glamourer-Invoked on 0x{drawObject.Address:X} on {slot} with item data {armor}."); + return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + } + private ulong LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) + { + var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); + var drawObject = drawDataContainer->OwnerObject->DrawObject; + GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject, drawObject); + Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + return ret; + } + + + private static string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) + { + var ret = + $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId}"; + for (var offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + { + var equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); + var dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset + var dyePtr = (byte*)&gearsetData + dyeOffset; + ret += $"\nEquipSlot {(EquipSlot)(dyeOffset - 60)}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; + } + return ret; + } } diff --git a/Glamourer/Interop/VieraEarService.cs b/Glamourer/Interop/VieraEarService.cs new file mode 100644 index 0000000..a6afd1d --- /dev/null +++ b/Glamourer/Interop/VieraEarService.cs @@ -0,0 +1,83 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Glamourer.Events; +using Penumbra.GameData; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop; + +public unsafe class VieraEarService : IDisposable +{ + private readonly PenumbraReloaded _penumbra; + private readonly IGameInteropProvider _interop; + public readonly VieraEarStateChanged Event; + + public VieraEarService(VieraEarStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra) + { + _interop = interop; + _penumbra = penumbra; + Event = visorStateChanged; + _setupVieraEarHook = Create(); + _penumbra.Subscribe(Restore, PenumbraReloaded.Priority.VieraEarService); + } + + public void Dispose() + { + _setupVieraEarHook.Dispose(); + _penumbra.Unsubscribe(Restore); + } + + /// Obtain the current state of viera ears for the given draw object (true: toggled). + public static unsafe bool GetVieraEarState(Model characterBase) + => characterBase is { IsCharacterBase: true, VieraEarsVisible: true }; + + /// Manually set the state of the Visor for the given draw object. + /// The draw object. + /// The desired state (true: toggled). + /// Whether the state was changed. + public bool SetVieraEarState(Model human, bool on) + { + if (!human.IsHuman) + return false; + + var oldState = GetVieraEarState(human); + Glamourer.Log.Verbose($"[SetVieraEarState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}."); + if (oldState == on) + return false; + + human.VieraEarsVisible = on; + return true; + } + + private delegate void UpdateVieraEarDelegateInternal(DrawDataContainer* drawData, byte on); + + private Hook _setupVieraEarHook; + + private void SetupVieraEarDetour(DrawDataContainer* drawData, byte value) + { + Actor actor = drawData->OwnerObject; + var originalOn = value != 0; + var on = originalOn; + // Invoke an event that can change the requested value + Event.Invoke(actor, ref on); + + Glamourer.Log.Verbose( + $"[SetVieraEarState] Invoked from game on 0x{actor.Address:X} switching to {on} (original {originalOn} from {value})."); + + _setupVieraEarHook.Original(drawData, on ? (byte)1 : (byte)0); + } + + private unsafe Hook Create() + { + var hook = _interop.HookFromSignature(Sigs.SetupVieraEars, SetupVieraEarDetour); + hook.Enable(); + return hook; + } + + private void Restore() + { + _setupVieraEarHook.Dispose(); + _setupVieraEarHook = Create(); + } +} diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index af66dd6..83262e4 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -9,17 +9,24 @@ namespace Glamourer.Interop; public class VisorService : IDisposable { - public readonly VisorStateChanged Event; + private readonly PenumbraReloaded _penumbra; + private readonly IGameInteropProvider _interop; + public readonly VisorStateChanged Event; - public unsafe VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop) + public VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra) { + _interop = interop; + _penumbra = penumbra; Event = visorStateChanged; - _setupVisorHook = interop.HookFromAddress((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour); - _setupVisorHook.Enable(); + _setupVisorHook = Create(); + _penumbra.Subscribe(Restore, PenumbraReloaded.Priority.VisorService); } public void Dispose() - => _setupVisorHook.Dispose(); + { + _setupVisorHook.Dispose(); + _penumbra.Unsubscribe(Restore); + } /// Obtain the current state of the Visor for the given draw object (true: toggled). public static unsafe bool GetVisorState(Model characterBase) @@ -29,7 +36,7 @@ public class VisorService : IDisposable /// The draw object. /// The desired state (true: toggled). /// Whether the state was changed. - public bool SetVisorState(Model human, bool on) + public unsafe bool SetVisorState(Model human, bool on) { if (!human.IsHuman) return false; @@ -39,13 +46,15 @@ public class VisorService : IDisposable if (oldState == on) return false; + // No clue what this flag does, but it's necessary for toggling static visors on or off, e.g. Alternate Cap (6229-1). + human.AsHuman->StateFlags |= (CharacterBase.StateFlag)0x40000000; SetupVisorDetour(human, human.GetArmor(EquipSlot.Head).Set.Id, on); return true; } private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, byte on); - private readonly Hook _setupVisorHook; + private Hook _setupVisorHook; private void SetupVisorDetour(nint human, ushort modelId, byte value) { @@ -72,4 +81,17 @@ public class VisorService : IDisposable human.AsCharacterBase->VisorToggled = on; _setupVisorHook.Original(human.Address, modelId, on ? (byte)1 : (byte)0); } + + private unsafe Hook Create() + { + var hook = _interop.HookFromAddress((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour); + hook.Enable(); + return hook; + } + + private void Restore() + { + _setupVisorHook.Dispose(); + _setupVisorHook = Create(); + } } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index d2aac1a..54f318b 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -13,8 +13,7 @@ public unsafe class WeaponService : IDisposable private readonly WeaponLoading _event; private readonly ThreadLocal _inUpdate = new(() => false); - - private readonly delegate* unmanaged[Stdcall] + private readonly delegate* unmanaged[Stdcall] _original; public WeaponService(WeaponLoading @event, IGameInteropProvider interop) @@ -23,7 +22,7 @@ public unsafe class WeaponService : IDisposable _loadWeaponHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _original = - (delegate* unmanaged[Stdcall] < DrawDataContainer*, uint, ulong, byte, byte, byte, byte, void >) + (delegate* unmanaged[Stdcall] < DrawDataContainer*, uint, ulong, byte, byte, byte, byte, int, void >) DrawDataContainer.MemberFunctionPointers.LoadWeapon; _loadWeaponHook.Enable(); } @@ -37,13 +36,14 @@ public unsafe class WeaponService : IDisposable // redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one. // skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change) // unk4 seemed to be the same as unk1. + // unk5 is new in 7.30 and is checked at the beginning of the function to call some timeline related function. private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, - byte skipGameObject, byte unk4); + byte skipGameObject, byte unk4, byte unk5); private readonly Hook _loadWeaponHook; private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weaponValue, byte redrawOnEquality, byte unk2, - byte skipGameObject, byte unk4) + byte skipGameObject, byte unk4, byte unk5) { if (!_inUpdate.Value) { @@ -65,21 +65,21 @@ public unsafe class WeaponService : IDisposable else if (weaponValue == actor.GetMainhand().Value && weaponValue != 0) _event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon); - _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4); + _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4, unk5); if (tmpWeapon.Value != weapon.Value) { if (tmpWeapon.Skeleton.Id == 0) - tmpWeapon.Stain = 0; - _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4); + tmpWeapon.Stains = StainIds.None; + _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4, unk5); } Glamourer.Log.Excessive( - $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); + $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}, {unk5}"); } else { - _loadWeaponHook.Original(drawData, slot, weaponValue, redrawOnEquality, unk2, skipGameObject, unk4); + _loadWeaponHook.Original(drawData, slot, weaponValue, redrawOnEquality, unk2, skipGameObject, unk4, unk5); } } @@ -90,29 +90,29 @@ public unsafe class WeaponService : IDisposable { case EquipSlot.MainHand: _inUpdate.Value = true; - _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); + _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0); _inUpdate.Value = false; return; case EquipSlot.OffHand: _inUpdate.Value = true; - _original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0); + _original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0, 0); _inUpdate.Value = false; return; case EquipSlot.BothHand: _inUpdate.Value = true; - _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); - _original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0); + _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0); + _original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0, 0); _inUpdate.Value = false; return; } } - public void LoadStain(Actor character, EquipSlot slot, StainId stain) + public void LoadStain(Actor character, EquipSlot slot, StainIds stains) { var mdl = character.Model; var (_, _, mh, oh) = mdl.GetWeapons(character); var value = slot == EquipSlot.OffHand ? oh : mh; - var weapon = value.With(value.Skeleton.Id == 0 ? 0 : stain); + var weapon = value.With(value.Skeleton.Id == 0 ? StainIds.None : stains); LoadWeapon(character, slot, weapon); } } diff --git a/Glamourer/Services/BackupService.cs b/Glamourer/Services/BackupService.cs index 3abf13a..511cca6 100644 --- a/Glamourer/Services/BackupService.cs +++ b/Glamourer/Services/BackupService.cs @@ -1,9 +1,10 @@ using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; namespace Glamourer.Services; -public class BackupService +public class BackupService : IAsyncService { private readonly Logger _logger; private readonly DirectoryInfo _configDirectory; @@ -14,7 +15,7 @@ public class BackupService _logger = logger; _fileNames = GlamourerFiles(fileNames); _configDirectory = new DirectoryInfo(fileNames.ConfigDirectory); - Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), _fileNames)); } /// Create a permanent backup with a given name for migrations. @@ -40,4 +41,9 @@ public class BackupService return list; } + + public Task Awaiter { get; } + + public bool Finished + => Awaiter.IsCompletedSuccessfully; } diff --git a/Glamourer/Services/CodeService.cs b/Glamourer/Services/CodeService.cs index 7d513dd..4a82f0e 100644 --- a/Glamourer/Services/CodeService.cs +++ b/Glamourer/Services/CodeService.cs @@ -23,20 +23,26 @@ public class CodeService OopsAuRa = 0x000400, OopsHrothgar = 0x000800, OopsViera = 0x001000, - Artisan = 0x002000, + //Artisan = 0x002000, SixtyThree = 0x004000, Shirts = 0x008000, World = 0x010000, Elephants = 0x020000, Crown = 0x040000, Dolphins = 0x080000, + Face = 0x100000, + Manderville = 0x200000, + Smiles = 0x400000, } public static readonly CodeFlag AllHintCodes = Enum.GetValues().Where(f => GetData(f).Display).Aggregate((CodeFlag)0, (f1, f2) => f1 | f2); - public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; - public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; + public const CodeFlag DyeCodes = + CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; + + public const CodeFlag GearCodes = + CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; public const CodeFlag RaceCodes = CodeFlag.OopsHyur | CodeFlag.OopsElezen @@ -44,12 +50,18 @@ public class CodeService | CodeFlag.OopsMiqote | CodeFlag.OopsRoegadyn | CodeFlag.OopsAuRa - | CodeFlag.OopsHrothgar; + | CodeFlag.OopsHrothgar + | CodeFlag.OopsViera; + + public const CodeFlag FullCodes = CodeFlag.Face | CodeFlag.Manderville | CodeFlag.Smiles; public const CodeFlag SizeCodes = CodeFlag.Dwarf | CodeFlag.Giant; private CodeFlag _enabled; + public CodeFlag AllEnabled + => _enabled; + public bool Enabled(CodeFlag flag) => _enabled.HasFlag(flag); @@ -158,26 +170,28 @@ public class CodeService private static CodeFlag GetMutuallyExclusive(CodeFlag flag) => flag switch { - CodeFlag.Clown => DyeCodes & ~CodeFlag.Clown, - CodeFlag.Emperor => GearCodes & ~CodeFlag.Emperor, - CodeFlag.Individual => 0, - CodeFlag.Dwarf => SizeCodes & ~CodeFlag.Dwarf, - CodeFlag.Giant => SizeCodes & ~CodeFlag.Giant, - CodeFlag.OopsHyur => RaceCodes & ~CodeFlag.OopsHyur, - CodeFlag.OopsElezen => RaceCodes & ~CodeFlag.OopsElezen, - CodeFlag.OopsLalafell => RaceCodes & ~CodeFlag.OopsLalafell, - CodeFlag.OopsMiqote => RaceCodes & ~CodeFlag.OopsMiqote, - CodeFlag.OopsRoegadyn => RaceCodes & ~CodeFlag.OopsRoegadyn, - CodeFlag.OopsAuRa => RaceCodes & ~CodeFlag.OopsAuRa, - CodeFlag.OopsHrothgar => RaceCodes & ~CodeFlag.OopsHrothgar, - CodeFlag.OopsViera => RaceCodes & ~CodeFlag.OopsViera, - CodeFlag.Artisan => 0, - CodeFlag.SixtyThree => 0, + CodeFlag.Clown => (FullCodes | DyeCodes) & ~CodeFlag.Clown, + CodeFlag.Emperor => (FullCodes | GearCodes) & ~CodeFlag.Emperor, + CodeFlag.Individual => FullCodes, + CodeFlag.Dwarf => (FullCodes | SizeCodes) & ~CodeFlag.Dwarf, + CodeFlag.Giant => (FullCodes | SizeCodes) & ~CodeFlag.Giant, + CodeFlag.OopsHyur => (FullCodes | RaceCodes) & ~CodeFlag.OopsHyur, + CodeFlag.OopsElezen => (FullCodes | RaceCodes) & ~CodeFlag.OopsElezen, + CodeFlag.OopsLalafell => (FullCodes | RaceCodes) & ~CodeFlag.OopsLalafell, + CodeFlag.OopsMiqote => (FullCodes | RaceCodes) & ~CodeFlag.OopsMiqote, + CodeFlag.OopsRoegadyn => (FullCodes | RaceCodes) & ~CodeFlag.OopsRoegadyn, + CodeFlag.OopsAuRa => (FullCodes | RaceCodes) & ~CodeFlag.OopsAuRa, + CodeFlag.OopsHrothgar => (FullCodes | RaceCodes) & ~CodeFlag.OopsHrothgar, + CodeFlag.OopsViera => (FullCodes | RaceCodes) & ~CodeFlag.OopsViera, + CodeFlag.SixtyThree => FullCodes, CodeFlag.Shirts => 0, - CodeFlag.World => (DyeCodes | GearCodes) & ~CodeFlag.World, - CodeFlag.Elephants => (DyeCodes | GearCodes) & ~CodeFlag.Elephants, - CodeFlag.Crown => 0, - CodeFlag.Dolphins => (DyeCodes | GearCodes) & ~CodeFlag.Dolphins, + CodeFlag.World => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.World, + CodeFlag.Elephants => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.Elephants, + CodeFlag.Crown => FullCodes, + CodeFlag.Dolphins => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.Dolphins, + CodeFlag.Face => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Face, + CodeFlag.Manderville => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Manderville, + CodeFlag.Smiles => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Smiles, _ => 0, }; @@ -197,13 +211,15 @@ public class CodeService CodeFlag.OopsAuRa => [ 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 ], CodeFlag.OopsHrothgar => [ 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 ], CodeFlag.OopsViera => [ 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B ], - CodeFlag.Artisan => [ 0xDE, 0x01, 0x32, 0x1E, 0x7F, 0x22, 0x80, 0x3D, 0x76, 0xDF, 0x74, 0x0E, 0xEC, 0x33, 0xD3, 0xF4, 0x1A, 0x98, 0x9E, 0x9D, 0x22, 0x5C, 0xAC, 0x3B, 0xFE, 0x0B, 0xC2, 0x13, 0xB9, 0x91, 0x24, 0x61 ], CodeFlag.SixtyThree => [ 0xA1, 0x65, 0x60, 0x99, 0xB0, 0x9F, 0xBF, 0xD7, 0x20, 0xC8, 0x29, 0xF6, 0x7B, 0x86, 0x27, 0xF5, 0xBE, 0xA9, 0x5B, 0xB0, 0x20, 0x5E, 0x57, 0x7B, 0xFF, 0xBC, 0x1E, 0x8C, 0x04, 0xF9, 0x35, 0xD3 ], CodeFlag.Shirts => [ 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB ], CodeFlag.World => [ 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 ], CodeFlag.Elephants => [ 0x9F, 0x4C, 0xCF, 0x6D, 0xC4, 0x01, 0x31, 0x46, 0x02, 0x05, 0x31, 0xED, 0xED, 0xB2, 0x66, 0x29, 0x31, 0x09, 0x1E, 0xE7, 0x47, 0xDE, 0x7B, 0x03, 0xB0, 0x3C, 0x06, 0x76, 0x26, 0x91, 0xDF, 0xB2 ], CodeFlag.Crown => [ 0x43, 0x8E, 0x34, 0x56, 0x24, 0xC9, 0xC6, 0xDE, 0x2A, 0x68, 0x3A, 0x5D, 0xF5, 0x8E, 0xCB, 0xEF, 0x0D, 0x4D, 0x5B, 0xDC, 0x23, 0xF9, 0xF9, 0xBD, 0xD9, 0x60, 0xAD, 0x53, 0xC5, 0xA0, 0x33, 0xC4 ], CodeFlag.Dolphins => [ 0x64, 0xC6, 0x2E, 0x7C, 0x22, 0x3A, 0x42, 0xF5, 0xC3, 0x93, 0x4F, 0x70, 0x1F, 0xFD, 0xFA, 0x3C, 0x98, 0xD2, 0x7C, 0xD8, 0x88, 0xA7, 0x3D, 0x1D, 0x0D, 0xD6, 0x70, 0x15, 0x28, 0x2E, 0x79, 0xE7 ], + CodeFlag.Face => [ 0xCA, 0x97, 0x81, 0x12, 0xCA, 0x1B, 0xBD, 0xCA, 0xFA, 0xC2, 0x31, 0xB3, 0x9B, 0x23, 0xDC, 0x4D, 0xA7, 0x86, 0xEF, 0xF8, 0x14, 0x7C, 0x4E, 0x72, 0xB9, 0x80, 0x77, 0x85, 0xAF, 0xEE, 0x48, 0xBB ], + CodeFlag.Manderville => [ 0x3E, 0x23, 0xE8, 0x16, 0x00, 0x39, 0x59, 0x4A, 0x33, 0x89, 0x4F, 0x65, 0x65, 0xE1, 0xB1, 0x34, 0x8B, 0xBD, 0x7A, 0x00, 0x88, 0xD4, 0x2C, 0x4A, 0xCB, 0x73, 0xEE, 0xAE, 0xD5, 0x9C, 0x00, 0x9D ], + CodeFlag.Smiles => [ 0x2E, 0x7D, 0x2C, 0x03, 0xA9, 0x50, 0x7A, 0xE2, 0x65, 0xEC, 0xF5, 0xB5, 0x36, 0x68, 0x85, 0xA5, 0x33, 0x93, 0xA2, 0x02, 0x9D, 0x24, 0x13, 0x94, 0x99, 0x72, 0x65, 0xA1, 0xA2, 0x5A, 0xEF, 0xC6 ], _ => [], }; @@ -229,7 +245,10 @@ public class CodeService CodeFlag.Elephants => (true, 1, "!", "Appropriate lyrics that can also be found in Glamourer's changelogs.", "Sets every player to the elephant costume in varying shades of pink."), CodeFlag.Crown => (true, 1, ".", "A famous Shakespearean line.", "Sets every player with a mentor symbol enabled to the clown's hat."), CodeFlag.Dolphins => (true, 5, ",", "The farewell of the second smartest species on Earth.", "Sets every player to a Namazu hat with different costume bodies."), - CodeFlag.Artisan => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + CodeFlag.Face => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + CodeFlag.Manderville => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + CodeFlag.Smiles => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), _ => (false, 0, string.Empty, string.Empty, string.Empty), }; } + diff --git a/Glamourer/Services/CollectionOverrideService.cs b/Glamourer/Services/CollectionOverrideService.cs index 691118f..99635d8 100644 --- a/Glamourer/Services/CollectionOverrideService.cs +++ b/Glamourer/Services/CollectionOverrideService.cs @@ -1,13 +1,14 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Interop.Penumbra; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; +using Notification = OtterGui.Classes.Notification; namespace Glamourer.Services; diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index b18c817..d2feac0 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -6,47 +6,48 @@ using Glamourer.Designs; using Glamourer.Designs.Special; using Glamourer.GameData; using Glamourer.Gui; +using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Interop.Penumbra; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.Services; public class CommandService : IDisposable, IApiService { - private const string RandomString = "random"; private const string MainCommandString = "/glamourer"; private const string ApplyCommandString = "/glamour"; - private readonly ICommandManager _commands; - private readonly MainWindow _mainWindow; - private readonly IChatGui _chat; - private readonly ActorManager _actors; - private readonly ObjectManager _objects; - private readonly StateManager _stateManager; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly AutoDesignManager _autoDesignManager; - private readonly DesignManager _designManager; - private readonly DesignConverter _converter; - private readonly DesignFileSystem _designFileSystem; - private readonly Configuration _config; - private readonly ModSettingApplier _modApplier; - private readonly ItemManager _items; - private readonly RandomDesignGenerator _randomDesign; - private readonly CustomizeService _customizeService; + private readonly ICommandManager _commands; + private readonly MainWindow _mainWindow; + private readonly IChatGui _chat; + private readonly ActorManager _actors; + private readonly ActorObjectManager _objects; + private readonly StateManager _stateManager; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly AutoDesignManager _autoDesignManager; + private readonly Configuration _config; + private readonly ModSettingApplier _modApplier; + private readonly ItemManager _items; + private readonly CustomizeService _customizeService; + private readonly DesignManager _designManager; + private readonly DesignConverter _converter; + private readonly DesignResolver _resolver; + private readonly PenumbraService _penumbra; - public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects, + public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ActorObjectManager objects, AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter, DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, ModSettingApplier modApplier, - ItemManager items, RandomDesignGenerator randomDesign, CustomizeService customizeService) + ItemManager items, RandomDesignGenerator randomDesign, CustomizeService customizeService, DesignFileSystemSelector designSelector, + QuickDesignCombo quickDesignCombo, DesignResolver resolver, PenumbraService penumbra) { _commands = commands; _mainWindow = mainWindow; @@ -57,13 +58,13 @@ public class CommandService : IDisposable, IApiService _stateManager = stateManager; _designManager = designManager; _converter = converter; - _designFileSystem = designFileSystem; _autoDesignManager = autoDesignManager; _config = config; _modApplier = modApplier; _items = items; - _randomDesign = randomDesign; _customizeService = customizeService; + _resolver = resolver; + _penumbra = penumbra; _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(ApplyCommandString, @@ -95,6 +96,12 @@ public class CommandService : IDisposable, IApiService _config.Ephemeral.LockMainWindow = !_config.Ephemeral.LockMainWindow; _config.Ephemeral.Save(); return; + case "automation": + var newValue = !_config.EnableAutoDesigns; + _config.EnableAutoDesigns = newValue; + _autoDesignApplier.OnEnableAutoDesignsChanged(newValue); + _config.Save(); + return; default: _chat.Print("Use without argument to toggle the main window."); _chat.Print(new SeStringBuilder().AddText("Use ").AddPurple("/glamour").AddText(" instead of ").AddRed("/glamourer") @@ -122,8 +129,10 @@ public class CommandService : IDisposable, IApiService "apply" => Apply(argument), "reapply" => ReapplyState(argument), "revert" => Revert(argument), - "reapplyautomation" => ReapplyAutomation(argument, "reapplyautomation", false), - "reverttoautomation" => ReapplyAutomation(argument, "reverttoautomation", true), + "reapplyautomation" => ReapplyAutomation(argument, "reapplyautomation", false, false), + "reverttoautomation" => ReapplyAutomation(argument, "reverttoautomation", true, false), + "resetdesign" => ReapplyAutomation(argument, "resetdesign", false, true), + "clearsettings" => ClearSettings(argument), "automation" => SetAutomation(argument), "copy" => CopyState(argument), "save" => SaveState(argument), @@ -152,6 +161,10 @@ public class CommandService : IDisposable, IApiService "Reapplies the current automation state on top of the characters current state.. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder().AddCommand("reverttoautomation", "Reverts a given character to its supposed state using automated designs. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("resetdesign", + "Reapplies the current automation and resets the random design. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("clearsettings", "Clears all temporary settings applied by Glamourer. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("copy", "Copy the current state of a character to clipboard. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder() @@ -166,6 +179,64 @@ public class CommandService : IDisposable, IApiService return true; } + private bool ClearSettings(string argument) + { + var argumentList = argument.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (argumentList.Length < 1) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour clearsettings ").AddGreen("[Character Identifier]").AddText(" | ") + .AddPurple("").AddText(" | ").AddBlue("").BuiltString); + PlayerIdentifierHelp(false, true); + _chat.Print(new SeStringBuilder() + .AddText(" 》 The character identifier specifies the collection to clear settings from. It also accepts '").AddGreen("all") + .AddText("' to clear all collections.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 The booleans are optional and default to 'true', the ").AddPurple("first") + .AddText(" determines whether ").AddPurple("manually").AddText(" applied settings are cleared, the ").AddBlue("second") + .AddText(" determines whether ").AddBlue("automatically").AddText(" applied settings are cleared.").BuiltString); + return false; + } + + var clearManual = true; + var clearAutomatic = true; + if (argumentList.Length > 1 && bool.TryParse(argumentList[1], out var m)) + clearManual = m; + if (argumentList.Length > 2 && bool.TryParse(argumentList[2], out var a)) + clearAutomatic = a; + + if (!clearManual && !clearAutomatic) + return true; + + if (argumentList[0].ToLowerInvariant() is "all") + { + _penumbra.ClearAllTemporarySettings(clearAutomatic, clearManual); + return true; + } + + if (!IdentifierHandling(argumentList[0], out var identifiers, false, true)) + return false; + + var set = new HashSet(); + foreach (var id in identifiers) + { + if (!_objects.TryGetValue(id, out var data) || !data.Valid) + continue; + + foreach (var obj in data.Objects) + { + var guid = _penumbra.GetActorCollection(obj, out _); + if (!set.Add(guid)) + continue; + + if (clearManual) + _penumbra.RemoveAllTemporarySettings(guid, StateSource.Manual); + if (clearAutomatic) + _penumbra.RemoveAllTemporarySettings(guid, StateSource.Fixed); + } + } + + return true; + } + private bool SetAutomation(string arguments) { var argumentList = arguments.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -281,21 +352,11 @@ public class CommandService : IDisposable, IApiService { switch (char.ToLowerInvariant(character)) { - case 'c': - applicationFlags |= ApplicationType.Customizations; - break; - case 'e': - applicationFlags |= ApplicationType.Armor; - break; - case 'a': - applicationFlags |= ApplicationType.Accessories; - break; - case 'd': - applicationFlags |= ApplicationType.GearCustomization; - break; - case 'w': - applicationFlags |= ApplicationType.Weapons; - break; + case 'c': applicationFlags |= ApplicationType.Customizations; break; + case 'e': applicationFlags |= ApplicationType.Armor; break; + case 'a': applicationFlags |= ApplicationType.Accessories; break; + case 'd': applicationFlags |= ApplicationType.GearCustomization; break; + case 'w': applicationFlags |= ApplicationType.Weapons; break; default: _chat.Print(new SeStringBuilder().AddText("The value ").AddPurple(split2[1], true) .AddText(" is not a valid set of application flags.").BuiltString); @@ -307,7 +368,7 @@ public class CommandService : IDisposable, IApiService return true; } - private bool ReapplyAutomation(string argument, string command, bool revert) + private bool ReapplyAutomation(string argument, string command, bool revert, bool forcedNew) { if (argument.Length == 0) { @@ -319,7 +380,6 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var data)) @@ -329,8 +389,8 @@ public class CommandService : IDisposable, IApiService { if (_stateManager.GetOrCreate(identifier, actor, out var state)) { - _autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, forcedNew, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, revert, StateSource.Manual); } } } @@ -353,7 +413,7 @@ public class CommandService : IDisposable, IApiService foreach (var identifier in identifiers) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ResetState(state, StateSource.Manual); + _stateManager.ResetState(state, StateSource.Manual, isFinal: true); } @@ -372,14 +432,13 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var data)) return true; foreach (var actor in data.Objects) - _stateManager.ReapplyState(actor, false, StateSource.Manual); + _stateManager.ReapplyState(actor, false, StateSource.Manual, true); } @@ -432,7 +491,6 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(split[1], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var actors)) @@ -515,7 +573,6 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(split[1], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var actors)) @@ -548,7 +605,7 @@ public class CommandService : IDisposable, IApiService if (baseValue != null) { var v = baseValue.Value; - if (set.Type(customizeIndex) is CharaMakeParams.MenuType.ListSelector) + if (set.Type(customizeIndex) is MenuType.ListSelector) --v; set.DataByValue(customizeIndex, new CustomizeValue(v), out var data, customize.Face); if (data != null) @@ -611,7 +668,7 @@ public class CommandService : IDisposable, IApiService if (split.Length is not 2) { _chat.Print(new SeStringBuilder().AddText("Use with /glamour apply ") - .AddYellow("[Design Name, Path or Identifier, Random, or Clipboard]") + .AddYellow("[Design Name, Path or Identifier, Quick, Selection, Random, or Clipboard]") .AddText(" | ") .AddGreen("[Character Identifier]") .AddText("; ") @@ -628,6 +685,10 @@ public class CommandService : IDisposable, IApiService _chat.Print(new SeStringBuilder() .AddText(" 》 The design path is the folder path in the selector, with '/' as separators. It is also case-insensitive.") .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 Quick will use the design currently selected in the Quick Design Bar, if any.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 Selection will use the design currently selected in the main interfaces Designs tab, if any.").BuiltString); _chat.Print(new SeStringBuilder() .AddText(" 》 Clipboard as a single word will try to apply a design string currently in your clipboard.").BuiltString); _chat.Print(new SeStringBuilder() @@ -656,16 +717,15 @@ public class CommandService : IDisposable, IApiService "y" => true, _ => false, }; - if (!GetDesign(split[0], out var design, true) || !IdentifierHandling(split2[0], out var identifiers, false, true)) + if (!_resolver.GetDesign(split[0], out var design, true) || !IdentifierHandling(split2[0], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var actors)) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } else { @@ -674,7 +734,7 @@ public class CommandService : IDisposable, IApiService if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) { ApplyModSettings(design, actor, applyMods); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } } } @@ -688,7 +748,8 @@ public class CommandService : IDisposable, IApiService if (!applyMods || design is not Design d) return; - var (messages, appliedMods, collection, name, overridden) = _modApplier.ApplyModSettings(d.AssociatedMods, actor); + var (messages, appliedMods, _, name, overridden) = + _modApplier.ApplyModSettings(d.AssociatedMods, actor, StateSource.Manual, d.ResetTemporarySettings); foreach (var message in messages) Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); @@ -717,7 +778,7 @@ public class CommandService : IDisposable, IApiService return false; } - if (!GetDesign(argument, out var designBase, false) || designBase is not Design d) + if (!_resolver.GetDesign(argument, out var designBase, false) || designBase is not Design d) return false; _designManager.Delete(d); @@ -736,7 +797,6 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_stateManager.TryGetValue(identifier, out var state) @@ -777,7 +837,6 @@ public class CommandService : IDisposable, IApiService if (!IdentifierHandling(split[1], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_stateManager.TryGetValue(identifier, out var state) @@ -796,81 +855,6 @@ public class CommandService : IDisposable, IApiService return false; } - private bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, bool allowSpecial) - { - design = null; - if (argument.Length == 0) - return false; - - if (allowSpecial) - { - if (string.Equals("clipboard", argument, StringComparison.OrdinalIgnoreCase)) - { - try - { - var clipboardText = ImGui.GetClipboardText(); - if (clipboardText.Length > 0) - design = _converter.FromBase64(clipboardText, true, true, out _); - } - catch - { - // ignored - } - - if (design != null) - return true; - - _chat.Print(new SeStringBuilder().AddText("Your current clipboard did not contain a valid design string.").BuiltString); - return false; - } - - if (argument.StartsWith(RandomString, StringComparison.OrdinalIgnoreCase)) - { - try - { - if (argument.Length == RandomString.Length) - design = _randomDesign.Design(); - else if (argument[RandomString.Length] == ':') - design = _randomDesign.Design(argument[(RandomString.Length + 1)..]); - if (design == null) - { - _chat.Print(new SeStringBuilder().AddText("No design matched your restrictions.").BuiltString); - return false; - } - - _chat.Print($"Chose random design {((Design)design).Name}."); - } - catch (Exception ex) - { - _chat.Print(new SeStringBuilder().AddText($"Error in the restriction string: {ex.Message}").BuiltString); - return false; - } - - return true; - } - } - - if (Guid.TryParse(argument, out var guid)) - { - design = _designManager.Designs.ByIdentifier(guid); - } - else - { - var lower = argument.ToLowerInvariant(); - design = _designManager.Designs.FirstOrDefault(d - => d.Name.Lower == lower || lower.Length > 3 && d.Identifier.ToString().StartsWith(lower)); - if (design == null && _designFileSystem.Find(lower, out var child) && child is DesignFileSystem.Leaf leaf) - design = leaf.Value; - } - - if (design != null) - return true; - - _chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") - .BuiltString); - return false; - } - private unsafe bool IdentifierHandling(string argument, out ActorIdentifier[] identifiers, bool allowAnyWorld, bool allowIndex) { try @@ -882,7 +866,7 @@ public class CommandService : IDisposable, IApiService { _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(argument) .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); - identifiers = Array.Empty(); + identifiers = []; return false; } @@ -913,7 +897,7 @@ public class CommandService : IDisposable, IApiService _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(argument, true) .AddText($" could not be converted to an identifier. {e.Message}") .BuiltString); - identifiers = Array.Empty(); + identifiers = []; return false; } } diff --git a/Glamourer/Services/ConfigMigrationService.cs b/Glamourer/Services/ConfigMigrationService.cs index 88eaf69..ef39f1a 100644 --- a/Glamourer/Services/ConfigMigrationService.cs +++ b/Glamourer/Services/ConfigMigrationService.cs @@ -23,16 +23,38 @@ public class ConfigMigrationService(SaveService saveService, FixedDesignMigrator MigrateV2To4(); MigrateV4To5(); MigrateV5To6(); + MigrateV6To7(); + MigrateV7To8(); AddColors(config, true); } + private void MigrateV7To8() + { + if (_config.Version > 7) + return; + + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) + _config.QdbButtons |= QdbButtons.RevertAdvancedCustomization; + _config.Version = 8; + } + + private void MigrateV6To7() + { + if (_config.Version > 6) + return; + + // Do not actually change anything in the config, just create a backup before designs are migrated. + backupService.CreateMigrationBackup("pre_gloss_specular_migration"); + _config.Version = 7; + } + private void MigrateV5To6() { if (_config.Version > 5) return; if (_data["ShowRevertAdvancedParametersButton"]?.ToObject() ?? true) - _config.QdbButtons |= QdbButtons.RevertAdvanced; + _config.QdbButtons |= QdbButtons.RevertAdvancedCustomization; _config.Version = 6; } diff --git a/Glamourer/Services/CustomizeService.cs b/Glamourer/Services/CustomizeService.cs index bb9737d..74f0b5b 100644 --- a/Glamourer/Services/CustomizeService.cs +++ b/Glamourer/Services/CustomizeService.cs @@ -35,11 +35,10 @@ public sealed class CustomizeService( } if (applyWhich.HasFlag(CustomizeFlag.Gender)) - if (ret.Race is not Race.Hrothgar || newValues.Gender is not Gender.Female) - { - changed |= ChangeGender(ref ret, newValues.Gender); - applied |= CustomizeFlag.Gender; - } + { + changed |= ChangeGender(ref ret, newValues.Gender); + applied |= CustomizeFlag.Gender; + } if (applyWhich.HasFlag(CustomizeFlag.BodyType)) { @@ -91,7 +90,7 @@ public sealed class CustomizeService( /// Returns whether a gender is valid for the given race. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsGenderValid(Race race, Gender gender) - => race is Race.Hrothgar ? gender == Gender.Male : CustomizeManager.Genders.Contains(gender); + => CustomizeManager.Genders.Contains(gender); /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -161,13 +160,6 @@ public sealed class CustomizeService( return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}."; } - // TODO: Female Hrothgar - if (gender is Gender.Female && race is Race.Hrothgar) - { - actualGender = Gender.Male; - return $"{Race.Hrothgar.ToName()} do not currently support {Gender.Female.ToName()} characters, reset to {Gender.Male.ToName()}."; - } - actualGender = gender; return string.Empty; } @@ -225,13 +217,6 @@ public sealed class CustomizeService( customize.Race = newRace; customize.Clan = newClan; - // TODO Female Hrothgar - if (newRace == Race.Hrothgar) - { - customize.Gender = Gender.Male; - flags |= CustomizeFlag.Gender; - } - var set = Manager.GetSet(customize.Clan, customize.Gender); return FixValues(set, ref customize) | flags; } @@ -242,10 +227,6 @@ public sealed class CustomizeService( if (customize.Gender == newGender) return 0; - // TODO Female Hrothgar - if (customize.Race is Race.Hrothgar) - return 0; - if (ValidateGender(customize.Race, newGender, out newGender).Length > 0) return 0; @@ -257,7 +238,29 @@ public sealed class CustomizeService( private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize) { CustomizeFlag flags = 0; - foreach (var idx in CustomizationExtensions.AllBasic) + + // Hrothgar face hack. + if (customize.Race is Race.Hrothgar) + { + if (customize.Face.Value is < 5) + { + customize.Face += 4; + flags |= CustomizeFlag.Face; + } + } + else if (customize.Face.Value is > 4 and < 9) + { + customize.Face -= 4; + flags |= CustomizeFlag.Face; + } + + if (ValidateCustomizeValue(set, customize.Face, CustomizeIndex.Face, customize.Face, out var fixedFace, false).Length > 0) + { + customize.Face = fixedFace; + flags |= CustomizeFlag.Face; + } + + foreach (var idx in CustomizationExtensions.AllBasicWithoutFace) { if (set.IsAvailable(idx)) { diff --git a/Glamourer/Services/DalamudServices.cs b/Glamourer/Services/DalamudServices.cs index fd001d7..e8a9f55 100644 --- a/Glamourer/Services/DalamudServices.cs +++ b/Glamourer/Services/DalamudServices.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.DragDrop; using Dalamud.Plugin; using Dalamud.Plugin.Services; @@ -6,15 +5,18 @@ using OtterGui.Services; namespace Glamourer.Services; +#pragma warning disable SeStringEvaluator + public class DalamudServices { - public static void AddServices(ServiceManager services, DalamudPluginInterface pi) + public static void AddServices(ServiceManager services, IDalamudPluginInterface pi) { services.AddExistingService(pi); services.AddExistingService(pi.UiBuilder); services.AddDalamudService(pi); services.AddDalamudService(pi); services.AddDalamudService(pi); + services.AddDalamudService(pi); services.AddDalamudService(pi); services.AddDalamudService(pi); services.AddDalamudService(pi); @@ -28,5 +30,6 @@ public class DalamudServices services.AddDalamudService(pi); services.AddDalamudService(pi); services.AddDalamudService(pi); + services.AddDalamudService(pi); } } diff --git a/Glamourer/Services/DesignApplier.cs b/Glamourer/Services/DesignApplier.cs index e0134d4..f0a9ba4 100644 --- a/Glamourer/Services/DesignApplier.cs +++ b/Glamourer/Services/DesignApplier.cs @@ -1,13 +1,12 @@ using Glamourer.Designs; -using Glamourer.Interop; -using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; namespace Glamourer.Services; -public sealed class DesignApplier(StateManager stateManager, ObjectManager objects) : IService +public sealed class DesignApplier(StateManager stateManager, ActorObjectManager objects) : IService { public void ApplyToPlayer(DesignBase design) { @@ -34,16 +33,10 @@ public sealed class DesignApplier(StateManager stateManager, ObjectManager objec } public void Apply(ActorIdentifier actor, DesignBase design) - { - objects.Update(); - Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, ApplySettings.ManualWithLinks); - } + => Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, ApplySettings.ManualWithLinks); public void Apply(ActorIdentifier actor, DesignBase design, ApplySettings settings) - { - objects.Update(); - Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, settings); - } + => Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, settings); public void Apply(ActorIdentifier actor, ActorData data, DesignBase design) => Apply(actor, data, design, ApplySettings.ManualWithLinks); diff --git a/Glamourer/Services/DesignResolver.cs b/Glamourer/Services/DesignResolver.cs new file mode 100644 index 0000000..8bb5cd2 --- /dev/null +++ b/Glamourer/Services/DesignResolver.cs @@ -0,0 +1,173 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.Services; +using Glamourer.Designs; +using Glamourer.Designs.Special; +using Glamourer.Gui; +using Glamourer.Gui.Tabs.DesignTab; +using Dalamud.Bindings.ImGui; +using OtterGui.Services; +using OtterGui.Classes; + +namespace Glamourer.Services; + +public class DesignResolver( + DesignFileSystemSelector designSelector, + QuickDesignCombo quickDesignCombo, + DesignConverter converter, + DesignManager manager, + DesignFileSystem designFileSystem, + RandomDesignGenerator randomDesign, + IChatGui chat) : IService +{ + private const string RandomString = "random"; + + public bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, bool allowSpecial) + { + if (GetDesign(argument, out design, out var error, out var message, allowSpecial)) + { + if (message != null) + chat.Print(message); + return true; + } + + if (error != null) + chat.Print(error); + return false; + } + + public bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, out SeString? error, out SeString? message, + bool allowSpecial) + { + design = null; + error = null; + message = null; + + if (argument.Length == 0) + return false; + + if (allowSpecial) + { + if (string.Equals("selection", argument, StringComparison.OrdinalIgnoreCase)) + return GetSelectedDesign(ref design, ref error); + + if (string.Equals("quick", argument, StringComparison.OrdinalIgnoreCase)) + return GetQuickDesign(ref design, ref error); + + if (string.Equals("clipboard", argument, StringComparison.OrdinalIgnoreCase)) + return GetClipboardDesign(ref design, ref error); + + if (argument.StartsWith(RandomString, StringComparison.OrdinalIgnoreCase)) + return GetRandomDesign(argument, ref design, ref error, ref message); + } + + return GetStandardDesign(argument, ref design, ref error); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetSelectedDesign(ref DesignBase? design, ref SeString? error) + { + design = designSelector.Selected; + if (design != null) + return true; + + error = "You do not have selected any design in the Designs Tab."; + return false; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetQuickDesign(ref DesignBase? design, ref SeString? error) + { + design = quickDesignCombo.Design as Design; + if (design != null) + return true; + + error = "You do not have selected any design in the Quick Design Bar."; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetClipboardDesign(ref DesignBase? design, ref SeString? error) + { + try + { + var clipboardText = ImGui.GetClipboardText(); + if (clipboardText.Length > 0) + design = converter.FromBase64(clipboardText, true, true, out _); + } + catch + { + // ignored + } + + if (design != null) + return true; + + error = "Your current clipboard did not contain a valid design string."; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetRandomDesign(string argument, ref DesignBase? design, ref SeString? error, ref SeString? message) + { + try + { + if (argument.Length == RandomString.Length) + design = randomDesign.Design(); + else if (argument[RandomString.Length] == ':') + design = randomDesign.Design(argument[(RandomString.Length + 1)..]); + if (design == null) + { + error = "No design matched your restrictions."; + return false; + } + + message = $"Chose random design {((Design)design).Name}."; + } + catch (Exception ex) + { + error = $"Error in the restriction string: {ex.Message}"; + return false; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetStandardDesign(string argument, ref DesignBase? design, ref SeString? error) + { + // As Guid + if (Guid.TryParse(argument, out var guid)) + { + design = manager.Designs.ByIdentifier(guid); + } + else + { + var lower = argument.ToLowerInvariant(); + // Search for design by name and partial identifier. + design = manager.Designs.FirstOrDefault(MatchNameAndIdentifier(lower)); + // Search for design by path, if nothing was found. + if (design == null && designFileSystem.Find(lower, out var child) && child is DesignFileSystem.Leaf leaf) + design = leaf.Value; + } + + if (design != null) + return true; + + error = new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") + .BuiltString; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Func MatchNameAndIdentifier(string lower) + { + // Check for names and identifiers, prefer names + if (lower.Length > 3) + return d => d.Name.Lower == lower || d.Identifier.ToString().StartsWith(lower); + + // Check only for names. + return d => d.Name.Lower == lower; + } +} diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index e19e289..cd25c64 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -19,7 +19,7 @@ public class FilenameService public readonly string NpcAppearanceFile; public readonly string CollectionOverrideFile; - public FilenameService(DalamudPluginInterface pi) + public FilenameService(IDalamudPluginInterface pi) { ConfigDirectory = pi.ConfigDirectory.FullName; ConfigFile = pi.ConfigFile.FullName; diff --git a/Glamourer/Services/HeightService.cs b/Glamourer/Services/HeightService.cs index 48f0dd6..0a6c7bb 100644 --- a/Glamourer/Services/HeightService.cs +++ b/Glamourer/Services/HeightService.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -9,7 +10,7 @@ namespace Glamourer.Services; public unsafe class HeightService : IService { - [Signature("E8 ?? ?? ?? FF 48 8B 0D ?? ?? ?? ?? 0F 28 F0")] + [Signature(Sigs.CalculateHeight)] private readonly delegate* unmanaged[Stdcall] _calculateHeight = null!; public HeightService(IGameInteropProvider interop) diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 8f52815..a885b54 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using Lumina.Excel; +using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; @@ -10,30 +11,32 @@ namespace Glamourer.Services; public class ItemManager { - public const string Nothing = "Nothing"; + public const string Nothing = EquipItem.Nothing; public const string SmallClothesNpc = "Smallclothes (NPC)"; public const ushort SmallClothesNpcModel = 9903; private readonly Configuration _config; - public readonly ObjectIdentification ObjectIdentification; - public readonly ExcelSheet ItemSheet; - public readonly DictStain Stains; - public readonly ItemData ItemData; - public readonly RestrictedGear RestrictedGear; + public readonly ObjectIdentification ObjectIdentification; + public readonly ExcelSheet ItemSheet; + public readonly DictStain Stains; + public readonly ItemData ItemData; + public readonly DictBonusItems DictBonusItems; + public readonly RestrictedGear RestrictedGear; public readonly EquipItem DefaultSword; public ItemManager(Configuration config, IDataManager gameData, ObjectIdentification objectIdentification, - ItemData itemData, DictStain stains, RestrictedGear restrictedGear) + ItemData itemData, DictStain stains, RestrictedGear restrictedGear, DictBonusItems dictBonusItems) { _config = config; - ItemSheet = gameData.GetExcelSheet()!; + ItemSheet = gameData.GetExcelSheet(); ObjectIdentification = objectIdentification; ItemData = itemData; Stains = stains; RestrictedGear = restrictedGear; - DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword + DictBonusItems = dictBonusItems; + DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)); // Weathered Shortsword } public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) @@ -124,6 +127,45 @@ public class ItemManager } } + public EquipItem Identify(BonusItemFlag slot, PrimaryId id, Variant variant) + { + var index = slot.ToIndex(); + if (index == uint.MaxValue) + return new EquipItem($"Invalid ({id.Id}-{variant})", 0, 0, id, 0, variant, slot.ToEquipType(), 0, 0, 0); + + return ObjectIdentification.Identify(id, variant, slot) + .FirstOrDefault(new EquipItem($"Invalid ({id.Id}-{variant})", 0, 0, id, 0, variant, slot.ToEquipType(), 0, 0, 0)); + } + + public EquipItem Resolve(BonusItemFlag slot, BonusItemId id) + => IsBonusItemValid(slot, id, out var item) ? item : new EquipItem($"Invalid ({id.Id})", id, 0, 0, 0, 0, slot.ToEquipType(), 0, 0, 0); + + public EquipItem Resolve(BonusItemFlag slot, CustomItemId id) + { + // Only from early designs as migration. + if (!id.IsBonusItem || id.Id == 0) + { + if (IsBonusItemValid(slot, (BonusItemId)id.Id, out var item)) + return item; + + return EquipItem.BonusItemNothing(slot); + } + + if (!id.IsCustom) + { + if (IsBonusItemValid(slot, id.BonusItem, out var item)) + return item; + + return EquipItem.BonusItemNothing(slot); + } + + var (model, variant, slot2) = id.SplitBonus; + if (slot != slot2) + return EquipItem.BonusItemNothing(slot); + + return Identify(slot, model, variant); + } + /// Return the default offhand for a given mainhand, that is for both handed weapons, return the correct offhand part, and for everything else Nothing. public EquipItem GetDefaultOffhand(EquipItem mainhand) { @@ -134,6 +176,36 @@ public class ItemManager return NothingItem(offhandType); } + public bool FindClosestShield(ItemId id, out EquipItem item) + { + var list = ItemData.ByType[FullEquipType.Shield]; + try + { + item = list.Where(i => i.ItemId.Id > id.Id && i.ItemId.Id - id.Id < 50).MinBy(i => i.ItemId.Id); + return true; + } + catch + { + item = default; + return false; + } + } + + public bool FindClosestSword(ItemId id, out EquipItem item) + { + var list = ItemData.ByType[FullEquipType.Sword]; + try + { + item = list.Where(i => i.ItemId.Id < id.Id && id.Id - i.ItemId.Id < 50).MaxBy(i => i.ItemId.Id); + return true; + } + catch + { + item = default; + return false; + } + } + public EquipItem Identify(EquipSlot slot, PrimaryId id, SecondaryId type, Variant variant, FullEquipType mainhandType = FullEquipType.Unknown) { @@ -161,6 +233,18 @@ public class ItemManager return item.Valid; } + /// Returns whether a bonus item id represents a valid item for a slot and gives the item. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsBonusItemValid(BonusItemFlag slot, BonusItemId itemId, out EquipItem item) + { + if (itemId.Id != 0) + return DictBonusItems.TryGetValue(itemId, out item) && slot == item.Type.ToBonus(); + + item = EquipItem.BonusItemNothing(slot); + return true; + } + + /// /// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.) /// The returned item is either the resolved correct item, or the Nothing item for that slot. @@ -195,16 +279,16 @@ public class ItemManager /// The returned stain id is either the input or 0. /// The return value is an empty string if there was no problem and a warning otherwise. /// - public string ValidateStain(StainId stain, out StainId ret, bool allowUnknown) + public string ValidateStain(StainIds stains, out StainIds ret, bool allowUnknown) { - if (allowUnknown || IsStainValid(stain)) + if (allowUnknown || stains.All(IsStainValid)) { - ret = stain; + ret = stains; return string.Empty; } - ret = 0; - return $"The Stain {stain} does not exist, reset to unstained."; + ret = StainIds.None; + return $"The Stain {stains} does not exist, reset to unstained."; } /// Returns whether an offhand is valid given the required offhand type. diff --git a/Glamourer/Services/PcpService.cs b/Glamourer/Services/PcpService.cs new file mode 100644 index 0000000..3363172 --- /dev/null +++ b/Glamourer/Services/PcpService.cs @@ -0,0 +1,119 @@ +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; + +namespace Glamourer.Services; + +public class PcpService : IRequiredService +{ + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly ActorObjectManager _objects; + private readonly StateManager _state; + private readonly DesignConverter _designConverter; + private readonly DesignManager _designManager; + + public PcpService(Configuration config, PenumbraService penumbra, ActorObjectManager objects, StateManager state, + DesignConverter designConverter, DesignManager designManager) + { + _config = config; + _penumbra = penumbra; + _objects = objects; + _state = state; + _designConverter = designConverter; + _designManager = designManager; + + _config.AttachToPcp = !_config.AttachToPcp; + Set(!_config.AttachToPcp); + } + + public void CleanPcpDesigns() + { + var designs = _designManager.Designs.Where(d => d.Tags.Contains("PCP")).ToList(); + Glamourer.Log.Information($"[PCPService] Deleting {designs.Count} designs containing the tag PCP."); + foreach (var design in designs) + _designManager.Delete(design); + } + + public void Set(bool value) + { + if (value == _config.AttachToPcp) + return; + + _config.AttachToPcp = value; + _config.Save(); + if (value) + { + Glamourer.Log.Information("[PCPService] Attached to PCP handling."); + _penumbra.PcpCreated += OnPcpCreation; + _penumbra.PcpParsed += OnPcpParse; + } + else + { + Glamourer.Log.Information("[PCPService] Detached from PCP handling."); + _penumbra.PcpCreated -= OnPcpCreation; + _penumbra.PcpParsed -= OnPcpParse; + } + } + + private void OnPcpParse(JObject jObj, string modDirectory, Guid collection) + { + Glamourer.Log.Debug("[PCPService] Parsing PCP file."); + if (jObj["Glamourer"] is not JObject glamourer) + return; + + if (glamourer["Version"]!.ToObject() is not 1) + return; + + if (_designConverter.FromJObject(glamourer["Design"] as JObject, true, true) is not { } designBase) + return; + + var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject); + if (!actorIdentifier.IsValid) + return; + + var time = new DateTimeOffset(jObj["Time"]?.ToObject() ?? DateTime.UtcNow); + var design = _designManager.CreateClone(designBase, + $"{_config.PcpFolder}/{actorIdentifier} - {jObj["Note"]?.ToObject() ?? string.Empty}", true); + _designManager.AddTag(design, "PCP"); + _designManager.SetWriteProtection(design, true); + _designManager.AddMod(design, new Mod(modDirectory, modDirectory), new ModSettings([], 0, true, false, false)); + _designManager.ChangeDescription(design, $"PCP design created for {actorIdentifier} on {time}."); + _designManager.ChangeResetAdvancedDyes(design, true); + _designManager.SetQuickDesign(design, false); + _designManager.ChangeColor(design, _config.PcpColor); + + Glamourer.Log.Debug("[PCPService] Created PCP design."); + if (_state.GetOrCreate(actorIdentifier, _objects.TryGetValue(actorIdentifier, out var data) ? data.Objects[0] : Actor.Null, + out var state)) + { + _state.ApplyDesign(state!, design, ApplySettings.Manual); + Glamourer.Log.Debug($"[PCPService] Applied PCP design to {actorIdentifier.Incognito(null)}"); + } + } + + private void OnPcpCreation(JObject jObj, ushort index, string path) + { + Glamourer.Log.Debug("[PCPService] Adding Glamourer data to PCP file."); + var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject); + if (!actorIdentifier.IsValid) + return; + + if (!_state.GetOrCreate(actorIdentifier, _objects.Objects[(int)index], out var state)) + { + Glamourer.Log.Debug($"[PCPService] Could not get or create state for actor {index}."); + return; + } + + var design = _designConverter.Convert(state, ApplicationRules.All); + jObj["Glamourer"] = new JObject + { + ["Version"] = 1, + ["Design"] = design.JsonSerialize(), + }; + } +} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index f06e014..6cfb4b6 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -27,13 +27,14 @@ using OtterGui.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Services; public static class StaticServiceManager { - public static ServiceManager CreateProvider(DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(IDalamudPluginInterface pi, Logger log, Glamourer glamourer) { EventWrapperBase.ChangeLogger(log); var services = new ServiceManager(log) @@ -44,7 +45,8 @@ public static class StaticServiceManager .AddData() .AddDesigns() .AddState() - .AddUi(); + .AddUi() + .AddExistingService(glamourer); DalamudServices.AddServices(services, pi); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Glamourer).Assembly); @@ -69,7 +71,8 @@ public static class StaticServiceManager private static ServiceManager AddEvents(this ServiceManager services) => services.AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -94,6 +97,7 @@ public static class StaticServiceManager private static ServiceManager AddInterop(this ServiceManager services) => services.AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -101,6 +105,7 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton(p => new CutsceneResolver(p.GetRequiredService().CutsceneParent)) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -109,7 +114,8 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static ServiceManager AddDesigns(this ServiceManager services) => services.AddSingleton() @@ -164,5 +170,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/Glamourer/Services/TextureService.cs b/Glamourer/Services/TextureService.cs index 0619279..a0ec443 100644 --- a/Glamourer/Services/TextureService.cs +++ b/Glamourer/Services/TextureService.cs @@ -1,5 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using OtterGui.Classes; using Penumbra.GameData.Enums; @@ -7,20 +8,35 @@ using Penumbra.GameData.Structs; namespace Glamourer.Services; -public sealed class TextureService(UiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) +public sealed class TextureService(IUiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) : TextureCache(dataManager, textureProvider), IDisposable { private readonly IDalamudTextureWrap?[] _slotIcons = CreateSlotIcons(uiBuilder); - public (nint, Vector2, bool) GetIcon(EquipItem item, EquipSlot slot) + public (ImTextureID, Vector2, bool) GetIcon(EquipItem item, EquipSlot slot) { if (item.IconId.Id != 0 && TryLoadIcon(item.IconId.Id, out var ret)) - return (ret.ImGuiHandle, new Vector2(ret.Width, ret.Height), false); + return (ret.Handle, new Vector2(ret.Width, ret.Height), false); var idx = slot.ToIndex(); return idx < 12 && _slotIcons[idx] != null - ? (_slotIcons[idx]!.ImGuiHandle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) - : (nint.Zero, Vector2.Zero, true); + ? (_slotIcons[idx]!.Handle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) + : (default, Vector2.Zero, true); + } + + public (ImTextureID, Vector2, bool) GetIcon(EquipItem item, BonusItemFlag slot) + { + if (item.IconId.Id != 0 && TryLoadIcon(item.IconId.Id, out var ret)) + return (ret.Handle, new Vector2(ret.Width, ret.Height), false); + + var idx = slot.ToIndex(); + if (idx == uint.MaxValue) + return (default, Vector2.Zero, true); + + idx += 12; + return idx < 13 && _slotIcons[idx] != null + ? (_slotIcons[idx]!.Handle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) + : (default, Vector2.Zero, true); } public void Dispose() @@ -32,11 +48,11 @@ public sealed class TextureService(UiBuilder uiBuilder, IDataManager dataManager } } - private static IDalamudTextureWrap?[] CreateSlotIcons(UiBuilder uiBuilder) + private static IDalamudTextureWrap?[] CreateSlotIcons(IUiBuilder uiBuilder) { - var ret = new IDalamudTextureWrap?[12]; + var ret = new IDalamudTextureWrap?[13]; - using var uldWrapper = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); + using var uldWrapper = uiBuilder.LoadUld("ui/uld/Character.uld"); if (!uldWrapper.Valid) { @@ -44,33 +60,37 @@ public sealed class TextureService(UiBuilder uiBuilder, IDataManager dataManager return ret; } - SetIcon(EquipSlot.Head, 1); - SetIcon(EquipSlot.Body, 2); - SetIcon(EquipSlot.Hands, 3); - SetIcon(EquipSlot.Legs, 5); - SetIcon(EquipSlot.Feet, 6); - SetIcon(EquipSlot.Ears, 8); - SetIcon(EquipSlot.Neck, 9); - SetIcon(EquipSlot.Wrists, 10); - SetIcon(EquipSlot.RFinger, 11); - SetIcon(EquipSlot.MainHand, 0); - SetIcon(EquipSlot.OffHand, 7); + SetIcon(EquipSlot.Head, 19); + SetIcon(EquipSlot.Body, 20); + SetIcon(EquipSlot.Hands, 21); + SetIcon(EquipSlot.Legs, 23); + SetIcon(EquipSlot.Feet, 24); + SetIcon(EquipSlot.Ears, 25); + SetIcon(EquipSlot.Neck, 26); + SetIcon(EquipSlot.Wrists, 27); + SetIcon(EquipSlot.RFinger, 28); + SetIcon(EquipSlot.MainHand, 17); + SetIcon(EquipSlot.OffHand, 18); + Set(BonusItemFlag.Glasses.ToName(), (int) BonusItemFlag.Glasses.ToIndex() + 12, 55); ret[EquipSlot.LFinger.ToIndex()] = ret[EquipSlot.RFinger.ToIndex()]; return ret; - void SetIcon(EquipSlot slot, int index) + void Set(string name, int slot, int index) { try { - ret[slot.ToIndex()] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", index)!; + ret[slot] = uldWrapper.LoadTexturePart("ui/uld/Character_hr1.tex", index)!; } catch (Exception ex) { - Glamourer.Log.Error($"Could not get empty slot texture for {slot.ToName()}, icon will be left empty. " + Glamourer.Log.Error($"Could not get empty slot texture for {name}, icon will be left empty. " + $"This may be because of incompatible mods affecting your character screen interface:\n{ex}"); - ret[slot.ToIndex()] = null; + ret[slot] = null; } } + + void SetIcon(EquipSlot slot, int index) + => Set(slot.ToName(), (int)slot.ToIndex(), index); } } diff --git a/Glamourer/State/FunEquipSet.cs b/Glamourer/State/FunEquipSet.cs index 91e6419..c1ae02e 100644 --- a/Glamourer/State/FunEquipSet.cs +++ b/Glamourer/State/FunEquipSet.cs @@ -1,5 +1,4 @@ -using Glamourer.Interop.Structs; -using Penumbra.GameData.Structs; +using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -21,8 +20,8 @@ internal class FunEquipSet { public Group(ushort headS, byte headV, ushort bodyS, byte bodyV, ushort handsS, byte handsV, ushort legsS, byte legsV, ushort feetS, byte feetV, StainId[]? stains = null) - : this(new CharacterArmor(headS, headV, 0), new CharacterArmor(bodyS, bodyV, 0), new CharacterArmor(handsS, handsV, 0), - new CharacterArmor(legsS, legsV, 0), new CharacterArmor(feetS, feetV, 0), stains) + : this(new CharacterArmor(headS, headV, StainIds.None), new CharacterArmor(bodyS, bodyV, StainIds.None), new CharacterArmor(handsS, handsV, StainIds.None), + new CharacterArmor(legsS, legsV, StainIds.None), new CharacterArmor(feetS, feetV, StainIds.None), stains) { } public static Group FullSetWithoutHat(ushort modelSet, byte variant, StainId[]? stains = null) diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index 25b8946..6abb03a 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -1,16 +1,17 @@ -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; +using Glamourer.GameData; using Glamourer.Gui; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using CustomizeIndex = Penumbra.GameData.Enums.CustomizeIndex; -using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.State; @@ -34,7 +35,8 @@ public unsafe class FunModule : IDisposable private readonly StateManager _stateManager; private readonly DesignConverter _designConverter; private readonly DesignManager _designManager; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; + private readonly NpcCustomizeSet _npcs; private readonly StainId[] _stains; public FestivalType CurrentFestival { get; private set; } = FestivalType.None; @@ -67,8 +69,8 @@ public unsafe class FunModule : IDisposable => OnDayChange(DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year); public FunModule(CodeService codes, CustomizeService customizations, ItemManager items, Configuration config, - GenericPopupWindow popupWindow, StateManager stateManager, ObjectManager objects, DesignConverter designConverter, - DesignManager designManager) + GenericPopupWindow popupWindow, StateManager stateManager, ActorObjectManager objects, DesignConverter designConverter, + DesignManager designManager, NpcCustomizeSet npcs) { _codes = codes; _customizations = customizations; @@ -79,6 +81,7 @@ public unsafe class FunModule : IDisposable _objects = objects; _designConverter = designConverter; _designManager = designManager; + _npcs = npcs; _rng = new Random(); _stains = _items.Stains.Keys.Prepend((StainId)0).ToArray(); ResetFestival(); @@ -102,19 +105,27 @@ public unsafe class FunModule : IDisposable return; } + if (actor.Index < ObjectIndex.CutsceneStart) + switch (_codes.Masked(CodeService.FullCodes)) + { + case CodeService.CodeFlag.Face when actor.Index != 0: + case CodeService.CodeFlag.Smiles: + case CodeService.CodeFlag.Manderville: + KeepOldArmor(actor, slot, ref armor); + return; + } + if (_codes.Enabled(CodeService.CodeFlag.Crown) && actor.OnlineStatus is OnlineStatus.PvEMentor or OnlineStatus.PvPMentor or OnlineStatus.TradeMentor && slot.IsEquipment()) { - armor = new CharacterArmor(6117, 1, 0); + armor = new CharacterArmor(6117, 1, StainIds.None); return; } switch (_codes.Masked(CodeService.GearCodes)) { - case CodeService.CodeFlag.Emperor: - SetRandomItem(slot, ref armor); - break; + case CodeService.CodeFlag.Emperor: SetRandomItem(slot, ref armor); break; case CodeService.CodeFlag.Elephants: case CodeService.CodeFlag.Dolphins: case CodeService.CodeFlag.World when actor.Index != 0: @@ -124,17 +135,135 @@ public unsafe class FunModule : IDisposable switch (_codes.Masked(CodeService.DyeCodes)) { - case CodeService.CodeFlag.Clown: - SetRandomDye(ref armor); - break; + case CodeService.CodeFlag.Clown: SetRandomDye(ref armor); break; } } + private sealed class PrioritizedList : List<(T Item, int Priority)> + { + private int _cumulative; + + public PrioritizedList(params (T Item, int Priority)[] list) + { + if (list.Length == 0) + return; + + AddRange(list.Where(p => p.Priority > 0).OrderByDescending(p => p.Priority).Select(p => (p.Item, _cumulative += p.Priority))); + } + + public T GetRandom(Random rng) + { + var val = rng.Next(0, _cumulative); + foreach (var (item, priority) in this) + { + if (val < priority) + return item; + } + + // Should never happen. + return this[^1].Item1; + } + } + + private static readonly PrioritizedList MandervilleMale = new + ( + //(0000000, 400), // Nothing + (1008264, 30), // Hildi + (1008731, 10), // Hildi, slightly damaged + (1011668, 3), // Zombi + (1016617, 5), // Hildi, heavily damaged + (1042518, 1), // Hildi of Light + (1006339, 2), // Godbert, naked + (1008734, 10), // Godbert, shorts + (1015921, 5), // Godbert, ripped + (1041606, 5), // Godbert, only shorts + (1041605, 5), // Godbert, summer + (1024501, 30), // Godbert, fully clothed + (1045184, 3), // Godbrand + (1044749, 1) // Brandihild + ); + + private static readonly PrioritizedList MandervilleFemale = new + ( + //(0000000, 400), // Nothing + (1025669, 5), // Hildi, Geisha + (1025670, 2), // Hildi, makeup, black + (1042477, 2), // Hildi, makeup, white + (1016798, 20), // Julyan, Winter + (1011707, 30), // Julyan + (1005714, 20), // Nashu + (1025668, 5), // Nashu, Kimono + (1025674, 5), // Nashu, fancy + (1042486, 30), // Nashu, inspector + (1017263, 3), // Gigi + (1017263, 1) // Gigi, buff + ); + + private static readonly PrioritizedList Smile = new + ( + (1046504, 75), // Normal + (1046501, 20), // Hat + (1050613, 4), // Armor + (1047625, 1) // Elephant + ); + + private static readonly PrioritizedList FaceMale = new + ( + //(0000000, 700), // Nothing + (1016136, 35), // Gerolt + (1032667, 2), // Gerolt, Suit + (1030519, 35), // Grenoldt + (1030519, 20), // Grenoldt, Short + (1046262, 2), // Grenoldt, Suit + (1048084, 15) // Genolt + ); + + private static readonly PrioritizedList FaceFemale = new + ( + //(0000000, 400), // Nothing + (1013713, 10), // Rowena, Togi + (1018496, 30), // Rowena, Poncho + (1032668, 2), // Rowena, Gown + (1042857, 10), // Rowena, Hannish + (1046255, 10), // Mowen, Miner + (1046263, 2), // Mowen, Gown + (1027544, 30), // Mowen, Bustle + (1049088, 15) // Rhodina + ); + + private bool ApplyFullCode(Actor actor, Span armor, ref CustomizeArray customize) + { + if (actor.Index >= ObjectIndex.CutsceneStart) + return false; + + var id = _codes.Masked(CodeService.FullCodes) switch + { + CodeService.CodeFlag.Face when customize.Gender is Gender.Female && actor.Index != 0 => FaceFemale.GetRandom(_rng), + CodeService.CodeFlag.Face when actor.Index != 0 => FaceMale.GetRandom(_rng), + CodeService.CodeFlag.Smiles => Smile.GetRandom(_rng), + CodeService.CodeFlag.Manderville when customize.Gender is Gender.Female => MandervilleFemale.GetRandom(_rng), + CodeService.CodeFlag.Manderville => MandervilleMale.GetRandom(_rng), + _ => (NpcId)0, + }; + + if (id.Id == 0 || !_npcs.FindFirst(n => n.Id == id, out var npc)) + return false; + + customize = npc.Customize; + var idx = 0; + foreach (ref var a in armor) + a = npc.Equip[idx++]; + return true; + } + public void ApplyFunOnLoad(Actor actor, Span armor, ref CustomizeArray customize) { if (!ValidFunTarget(actor)) return; + if (ApplyFullCode(actor, armor, ref customize)) + return; + // First set the race, if any. SetRace(ref customize); // Now apply the gender. @@ -171,11 +300,9 @@ public unsafe class FunModule : IDisposable break; case CodeService.CodeFlag.Dolphins: SetDolphin(EquipSlot.Body, ref armor[1]); - SetDolphin(EquipSlot.Head, ref armor[0]); - break; - case CodeService.CodeFlag.World when actor.Index != 0: - _worldSets.Apply(actor, _rng, armor); + SetDolphin(EquipSlot.Head, ref armor[0]); break; + case CodeService.CodeFlag.World when actor.Index != 0: _worldSets.Apply(actor, _rng, armor); break; } switch (_codes.Masked(CodeService.DyeCodes)) @@ -198,23 +325,23 @@ public unsafe class FunModule : IDisposable private static bool ValidFunTarget(Actor actor) => actor.IsCharacter - && actor.AsObject->ObjectKind is (byte)ObjectKind.Player + && actor.AsObject->ObjectKind is ObjectKind.Pc && !actor.IsTransformed - && actor.AsCharacter->CharacterData.ModelCharaId == 0; + && actor.AsCharacter->ModelContainer.ModelCharaId == 0; private static void KeepOldArmor(Actor actor, EquipSlot slot, ref CharacterArmor armor) => armor = actor.Model.Valid ? actor.Model.GetArmor(slot) : armor; private void SetRandomDye(ref CharacterArmor armor) { - var stainIdx = _rng.Next(0, _stains.Length - 1); - armor.Stain = _stains[stainIdx]; + var stainIdx = _rng.Next(0, _stains.Length); + armor.Stains = _stains[stainIdx]; } private void SetRandomItem(EquipSlot slot, ref CharacterArmor armor) { var list = _items.ItemData.ByType[slot.ToEquipType()]; - var rng = _rng.Next(0, list.Count - 1); + var rng = _rng.Next(0, list.Count); var item = list[rng]; armor.Set = item.PrimaryId; armor.Variant = item.Variant; @@ -235,25 +362,25 @@ public unsafe class FunModule : IDisposable private static IReadOnlyList DolphinBodies => [ - new CharacterArmor(6089, 1, 4), // Toad - new CharacterArmor(6089, 1, 4), // Toad - new CharacterArmor(6089, 1, 4), // Toad - new CharacterArmor(6023, 1, 4), // Swine - new CharacterArmor(6023, 1, 4), // Swine - new CharacterArmor(6023, 1, 4), // Swine - new CharacterArmor(6133, 1, 4), // Gaja - new CharacterArmor(6182, 1, 3), // Imp - new CharacterArmor(6182, 1, 3), // Imp - new CharacterArmor(6182, 1, 4), // Imp - new CharacterArmor(6182, 1, 4), // Imp + new(6089, 1, new StainIds(4)), // Toad + new(6089, 1, new StainIds(4)), // Toad + new(6089, 1, new StainIds(4)), // Toad + new(6023, 1, new StainIds(4)), // Swine + new(6023, 1, new StainIds(4)), // Swine + new(6023, 1, new StainIds(4)), // Swine + new(6133, 1, new StainIds(4)), // Gaja + new(6182, 1, new StainIds(3)), // Imp + new(6182, 1, new StainIds(3)), // Imp + new(6182, 1, new StainIds(4)), // Imp + new(6182, 1, new StainIds(4)), // Imp ]; private void SetDolphin(EquipSlot slot, ref CharacterArmor armor) { armor = slot switch { - EquipSlot.Body => DolphinBodies[_rng.Next(0, DolphinBodies.Count - 1)], - EquipSlot.Head => new CharacterArmor(5040, 1, 0), + EquipSlot.Body => DolphinBodies[_rng.Next(0, DolphinBodies.Count)], + EquipSlot.Head => new CharacterArmor(5040, 1, StainIds.None), _ => armor, }; } @@ -270,7 +397,7 @@ public unsafe class FunModule : IDisposable private static void SetCrown(Span armor) { - var clown = new CharacterArmor(6117, 1, 0); + var clown = new CharacterArmor(6117, 1, StainIds.None); armor[0] = clown; armor[1] = clown; armor[2] = clown; @@ -285,15 +412,12 @@ public unsafe class FunModule : IDisposable return; var targetClan = (SubRace)((int)race * 2 - (int)customize.Clan % 2); - // TODO Female Hrothgar - if (race is Race.Hrothgar && customize.Gender is Gender.Female) - targetClan = targetClan is SubRace.Lost ? SubRace.Seawolf : SubRace.Hellsguard; _customizations.ChangeClan(ref customize, targetClan); } private void SetGender(ref CustomizeArray customize) { - if (!_codes.Enabled(CodeService.CodeFlag.SixtyThree) || customize.Race is Race.Hrothgar) // TODO Female Hrothgar + if (!_codes.Enabled(CodeService.CodeFlag.SixtyThree)) return; _customizations.ChangeGender(ref customize, customize.Gender is Gender.Male ? Gender.Female : Gender.Male); @@ -310,7 +434,7 @@ public unsafe class FunModule : IDisposable if (index is CustomizeIndex.Face || !set.IsAvailable(index)) continue; - var valueIdx = _rng.Next(0, set.Count(index) - 1); + var valueIdx = _rng.Next(0, set.Count(index)); customize[index] = set.Data(index, valueIdx).Value; } } diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs index eaf7c21..69051c2 100644 --- a/Glamourer/State/InternalStateEditor.cs +++ b/Glamourer/State/InternalStateEditor.cs @@ -151,12 +151,24 @@ public class InternalStateEditor( return true; } + /// Change a single bonus item. + public bool ChangeBonusItem(ActorState state, BonusItemFlag slot, EquipItem item, StateSource source, out EquipItem oldItem, uint key = 0) + { + oldItem = state.ModelData.BonusItem(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetBonusItem(slot, item); + state.Sources[slot] = source; + return true; + } + /// Change a single piece of equipment including stain. - public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateSource source, out EquipItem oldItem, - out StainId oldStain, uint key = 0) + public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainIds stains, StateSource source, out EquipItem oldItem, + out StainIds oldStains, uint key = 0) { oldItem = state.ModelData.Item(slot); - oldStain = state.ModelData.Stain(slot); + oldStains = state.ModelData.Stain(slot); if (!state.CanUnlock(key)) return false; @@ -168,7 +180,7 @@ public class InternalStateEditor( return false; var old = oldItem; - var oldS = oldStain; + var oldS = oldStains; gPose.AddActionOnLeave(() => { if (old.Type == state.BaseData.Item(slot).Type) @@ -177,20 +189,20 @@ public class InternalStateEditor( } state.ModelData.SetItem(slot, item); - state.ModelData.SetStain(slot, stain); + state.ModelData.SetStain(slot, stains); state.Sources[slot, false] = source; state.Sources[slot, true] = source; return true; } /// Change only the stain of an equipment piece. - public bool ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateSource source, out StainId oldStain, uint key = 0) + public bool ChangeStains(ActorState state, EquipSlot slot, StainIds stains, StateSource source, out StainIds oldStains, uint key = 0) { - oldStain = state.ModelData.Stain(slot); + oldStains = state.ModelData.Stain(slot); if (!state.CanUnlock(key)) return false; - state.ModelData.SetStain(slot, stain); + state.ModelData.SetStain(slot, stains); state.Sources[slot, true] = source; return true; } diff --git a/Glamourer/State/JobChangeState.cs b/Glamourer/State/JobChangeState.cs index 0fe1820..d568375 100644 --- a/Glamourer/State/JobChangeState.cs +++ b/Glamourer/State/JobChangeState.cs @@ -24,11 +24,12 @@ public sealed class JobChangeState : IService public ActorIdentifier Identifier => State?.Identifier ?? ActorIdentifier.Invalid; - public bool TryGetValue(FullEquipType slot, JobId jobId, out (EquipItem, StateSource) data) - => _weaponList.TryGet(slot, jobId, out data); + public bool TryGetValue(FullEquipType slot, JobId jobId, bool gameStateAllowed, out (EquipItem, StateSource) data) + => _weaponList.TryGet(slot, jobId, gameStateAllowed, out data); public void Set(ActorState state, IEnumerable<(EquipItem, StateSource, JobFlag)> items) { + Reset(); foreach (var (item, source, flags) in items.Where(p => p.Item1.Valid)) _weaponList.TryAdd(item.Type, item, source, flags); State = state; diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 9c06ce5..9800445 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -7,6 +7,7 @@ using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -23,9 +24,8 @@ public class StateApplier( ItemManager _items, PenumbraService _penumbra, MetaService _metaService, - ObjectManager _objects, + ActorObjectManager _objects, CrestService _crests, - Configuration _config, DirectXService _directX) { /// Simply force a redraw regardless of conditions. @@ -75,7 +75,7 @@ public class StateApplier( } } - /// + /// public ActorData ChangeCustomize(ActorState state, bool apply) { var data = GetData(state); @@ -105,11 +105,11 @@ public class StateApplier( { var customize = mdl.GetCustomize(); var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender); - _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem); + _updateSlot.UpdateEquipSlot(actor.Model, slot, resolvedItem); } else { - _updateSlot.UpdateSlot(actor.Model, slot, armor); + _updateSlot.UpdateEquipSlot(actor.Model, slot, armor); } } } @@ -125,32 +125,59 @@ public class StateApplier( return data; } + public void ChangeBonusItem(ActorData data, BonusItemFlag slot, PrimaryId id, Variant variant) + { + var item = new CharacterArmor(id, variant, StainIds.None); + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + { + var mdl = actor.Model; + if (!mdl.IsHuman) + continue; + + _updateSlot.UpdateBonusSlot(actor.Model, slot, item); + } + } + + /// + public ActorData ChangeBonusItem(ActorState state, BonusItemFlag slot, bool apply) + { + // If the source is not IPC we do not want to apply restrictions. + var data = GetData(state); + if (apply) + { + var item = state.ModelData.BonusItem(slot); + ChangeBonusItem(data, slot, item.PrimaryId, item.Variant); + } + + return data; + } + /// /// Change the stain of a single piece of armor or weapon. /// If the offhand is empty, the stain will be fixed to 0 to prevent crashes. /// - public void ChangeStain(ActorData data, EquipSlot slot, StainId stain) + public void ChangeStain(ActorData data, EquipSlot slot, StainIds stains) { var idx = slot.ToIndex(); switch (idx) { case < 10: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _updateSlot.UpdateStain(actor.Model, slot, stain); + _updateSlot.UpdateStain(actor.Model, slot, stains); break; case 10: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadStain(actor, EquipSlot.MainHand, stain); + _weapon.LoadStain(actor, EquipSlot.MainHand, stains); break; case 11: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadStain(actor, EquipSlot.OffHand, stain); + _weapon.LoadStain(actor, EquipSlot.OffHand, stains); break; } } - /// + /// public ActorData ChangeStain(ActorState state, EquipSlot slot, bool apply) { var data = GetData(state); @@ -162,15 +189,15 @@ public class StateApplier( /// Apply a weapon to the appropriate slot. - public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain) + public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainIds stains) { if (slot is EquipSlot.MainHand) - ChangeMainhand(data, item, stain); + ChangeMainhand(data, item, stains); else - ChangeOffhand(data, item, stain); + ChangeOffhand(data, item, stains); } - /// + /// public ActorData ChangeWeapon(ActorState state, EquipSlot slot, bool apply, bool onlyGPose) { var data = GetData(state); @@ -186,30 +213,30 @@ public class StateApplier( /// /// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both. /// - public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeMainhand(ActorData data, EquipItem weapon, StainIds stains) { var slot = weapon.Type.ValidOffhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stains)); } /// Apply a weapon to the offhand. - public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeOffhand(ActorData data, EquipItem weapon, StainIds stains) { - stain = weapon.PrimaryId.Id == 0 ? 0 : stain; + stains = weapon.PrimaryId.Id == 0 ? StainIds.None : stains; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stains)); } /// Change a meta state. - public unsafe void ChangeMetaState(ActorData data, MetaIndex index, bool value) + public void ChangeMetaState(ActorData data, MetaIndex index, bool value) { switch (index) { case MetaIndex.Wetness: { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - actor.AsCharacter->IsGPoseWet = value; + actor.IsGPoseWet = value; return; } case MetaIndex.HatState: @@ -220,8 +247,13 @@ public class StateApplier( } case MetaIndex.WeaponState: { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _metaService.SetWeaponState(actor, value); + // Only apply to the GPose character because otherwise we get some weird incompatibility when leaving GPose. + if (_objects.IsInGPose) + foreach (var actor in data.Objects.Where(a => a.IsGPoseOrCutscene)) + _metaService.SetWeaponState(actor, value); + else + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _metaService.SetWeaponState(actor, value); return; } case MetaIndex.VisorState: @@ -230,6 +262,14 @@ public class StateApplier( _visor.SetVisorState(actor.Model, value); return; } + case MetaIndex.EarState: + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + { + var model = actor.Model; + model.VieraEarsVisible = value; + } + + return; } } @@ -259,45 +299,45 @@ public class StateApplier( } /// Change the customize parameters on models. Can change multiple at once. - public void ChangeParameters(ActorData data, CustomizeParameterFlag flags, in CustomizeParameterData values, bool force) + public void ChangeParameters(ActorData data, CustomizeParameterFlag flags, in CustomizeParameterData values) { - if (!force && !_config.UseAdvancedParameters || flags == 0) + if (flags == 0) return; foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) actor.Model.ApplyParameterData(flags, values); } - /// + /// public ActorData ChangeParameters(ActorState state, CustomizeParameterFlag flags, bool apply) { var data = GetData(state); if (apply) - ChangeParameters(data, flags, state.ModelData.Parameters, state.IsLocked); + ChangeParameters(data, flags, state.ModelData.Parameters); return data; } - public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force) + public unsafe void ChangeMaterialValue(ActorState state, ActorData data, MaterialValueIndex changedIndex, ColorRow? changedValue) { - if (!force && !_config.UseAdvancedDyes) - return; - foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) { - if (!index.TryGetTexture(actor, out var texture)) + if (!changedIndex.TryGetTexture(actor, out var texture)) continue; - if (!_directX.TryGetColorTable(*texture, out var table)) + if (!PrepareColorSet.TryGetColorTable(actor, changedIndex, out var baseTable, out var mode)) continue; - if (value.HasValue) - value.Value.Apply(ref table[index.RowIndex]); - else if (PrepareColorSet.TryGetColorTable(actor, index, out var baseTable)) - table[index.RowIndex] = baseTable[index.RowIndex]; - else - continue; + foreach (var (index, value) in state.Materials.GetValues( + MaterialValueIndex.Min(changedIndex.DrawObject, changedIndex.SlotIndex, changedIndex.MaterialIndex), + MaterialValueIndex.Max(changedIndex.DrawObject, changedIndex.SlotIndex, changedIndex.MaterialIndex))) + { + if (index == changedIndex.Key) + changedValue?.Apply(ref baseTable[changedIndex.RowIndex], mode); + else + value.Model.Apply(ref baseTable[MaterialValueIndex.FromKey(index).RowIndex], mode); + } - _directX.ReplaceColorTable(texture, table); + _directX.ReplaceColorTable(texture, baseTable); } } @@ -305,15 +345,13 @@ public class StateApplier( { var data = GetData(state); if (apply) - ChangeMaterialValue(data, index, state.Materials.TryGetValue(index, out var v) ? v.Model : null, state.IsLocked); + ChangeMaterialValue(state, data, index, state.Materials.TryGetValue(index, out var v) ? v.Model : null); + return data; } - public unsafe void ChangeMaterialValues(ActorData data, in StateMaterialManager materials, bool force) + public unsafe void ChangeMaterialValues(ActorData data, in StateMaterialManager materials) { - if (!force && !_config.UseAdvancedDyes) - return; - var groupedMaterialValues = materials.Values.Select(p => (MaterialValueIndex.FromKey(p.Key), p.Value)) .GroupBy(p => (p.Item1.DrawObject, p.Item1.SlotIndex, p.Item1.MaterialIndex)); @@ -326,11 +364,11 @@ public class StateApplier( if (!mainKey.TryGetTexture(actor, out var texture)) continue; - if (!PrepareColorSet.TryGetColorTable(actor, mainKey, out var table)) + if (!PrepareColorSet.TryGetColorTable(actor, mainKey, out var table, out var mode)) continue; foreach (var (key, value) in values) - value.Model.Apply(ref table[key.RowIndex]); + value.Model.Apply(ref table[key.RowIndex], mode); _directX.ReplaceColorTable(texture, table); } @@ -347,7 +385,7 @@ public class StateApplier( var actors = ChangeMetaState(state, MetaIndex.Wetness, true); if (redraw) { - if (withLock) + if (withLock && actors.Valid) state.TempLock(); ForceRedraw(actors); } @@ -356,6 +394,11 @@ public class StateApplier( ChangeCustomize(actors, state.ModelData.Customize); foreach (var slot in EquipSlotExtensions.EqdpSlots) ChangeArmor(actors, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible()); + foreach (var slot in BonusExtensions.AllFlags) + { + var item = state.ModelData.BonusItem(slot); + ChangeBonusItem(actors, slot, item.PrimaryId, item.Variant); + } var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); @@ -367,18 +410,16 @@ public class StateApplier( ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); + ChangeMetaState(actors, MetaIndex.EarState, state.ModelData.AreEarsVisible()); ChangeCrests(actors, state.ModelData.CrestVisibility); - ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); - ChangeMaterialValues(actors, state.Materials, state.IsLocked); + ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters); + ChangeMaterialValues(actors, state.Materials); } } return actors; } - private ActorData GetData(ActorState state) - { - _objects.Update(); - return _objects.TryGetValue(state.Identifier, out var data) ? data : ActorData.Invalid; - } + public ActorData GetData(ActorState state) + => _objects.TryGetValue(state.Identifier, out var data) ? data : ActorData.Invalid; } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index c20a69d..986bdc2 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,5 +1,6 @@ using Glamourer.Api.Enums; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; @@ -8,6 +9,7 @@ using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -16,6 +18,7 @@ public class StateEditor( InternalStateEditor editor, StateApplier applier, StateChanged stateChanged, + StateFinalized stateFinalized, JobChangeState jobChange, Configuration config, ItemManager items, @@ -23,11 +26,12 @@ public class StateEditor( ModSettingApplier modApplier, GPoseService gPose) : IDesignEditor { - protected readonly InternalStateEditor Editor = editor; - protected readonly StateApplier Applier = applier; - protected readonly StateChanged StateChanged = stateChanged; - protected readonly Configuration Config = config; - protected readonly ItemManager Items = items; + protected readonly InternalStateEditor Editor = editor; + protected readonly StateApplier Applier = applier; + protected readonly StateChanged StateChanged = stateChanged; + protected readonly StateFinalized StateFinalized = stateFinalized; + protected readonly Configuration Config = config; + protected readonly ItemManager Items = items; /// Turn an actor to. public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source, @@ -39,7 +43,8 @@ public class StateEditor( var actors = Applier.ForceRedraw(state, source.RequiresChange()); Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Model, source, state, actors, (old, modelId)); + StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); + StateFinalized.Invoke(StateFinalizationType.ModelChange, actors); } /// @@ -52,7 +57,7 @@ public class StateEditor( var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, (old, value, idx)); + StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, new CustomizeTransaction(idx, old, value)); } /// @@ -65,7 +70,8 @@ public class StateEditor( var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, (old, applied)); + StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, + new EntireCustomizeTransaction(applied, old, customizeInput)); } /// @@ -86,26 +92,56 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(type, settings.Source, state, actors, (old, item, slot)); + + if (type is StateChangeType.Equip) + { + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item, oldOff, oldGauntlets)); + } + else + { + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item, oldGauntlets)); + } + } + + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default) + { + var state = (ActorState)data; + if (!Editor.ChangeBonusItem(state, slot, item, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, new BonusItemTransaction(slot, old, item)); } /// - public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainId? stain, ApplySettings settings) + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings) { - switch (item.HasValue, stain.HasValue) + switch (item.HasValue, stains.HasValue) { case (false, false): return; case (true, false): ChangeItem(data, slot, item!.Value, settings); return; case (false, true): - ChangeStain(data, slot, stain!.Value, settings); + ChangeStains(data, slot, stains!.Value, settings); return; } var state = (ActorState)data; - if (!Editor.ChangeEquip(state, slot, item ?? state.ModelData.Item(slot), stain ?? state.ModelData.Stain(slot), settings.Source, - out var old, out var oldStain, settings.Key)) + if (!Editor.ChangeEquip(state, slot, item ?? state.ModelData.Item(slot), stains ?? state.ModelData.Stain(slot), settings.Source, + out var old, out var oldStains, settings.Key)) return; var type = slot.ToIndex() < 10 ? StateChangeType.Equip : StateChangeType.Weapon; @@ -115,25 +151,43 @@ public class StateEditor( item!.Value.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); if (slot is EquipSlot.MainHand) - ApplyMainhandPeriphery(state, item, stain, settings); + ApplyMainhandPeriphery(state, item, stains, settings); Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStain.Id} to {stain!.Value.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(type, settings.Source, state, actors, (old, item!.Value, slot)); - StateChanged.Invoke(StateChangeType.Stain, settings.Source, state, actors, (oldStain, stain!.Value, slot)); + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (type is StateChangeType.Equip) + { + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item!.Value)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item!.Value, oldOff, oldGauntlets)); + } + else + { + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item!.Value, oldGauntlets)); + } + + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, oldStains, stains!.Value)); } /// - public void ChangeStain(object data, EquipSlot slot, StainId stain, ApplySettings settings) + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings) { var state = (ActorState)data; - if (!Editor.ChangeStain(state, slot, stain, settings.Source, out var old, settings.Key)) + if (!Editor.ChangeStains(state, slot, stains, settings.Source, out var old, settings.Key)) return; var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); Glamourer.Log.Verbose( - $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Stain, settings.Source, state, actors, (old, stain, slot)); + $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old} to {stains}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, old, stains)); } /// @@ -146,7 +200,7 @@ public class StateEditor( var actors = Applier.ChangeCrests(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Crest, settings.Source, state, actors, (old, crest, slot)); + StateChanged.Invoke(StateChangeType.Crest, settings.Source, state, actors, new CrestTransaction(slot, old, crest)); } /// @@ -163,8 +217,8 @@ public class StateEditor( var @new = state.ModelData.Parameters[flag]; var actors = Applier.ChangeParameters(state, flag, settings.Source.RequiresChange()); Glamourer.Log.Verbose( - $"Set {flag} crest in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Parameter, settings.Source, state, actors, (old, @new, flag)); + $"Set {flag} in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Parameter, settings.Source, state, actors, new ParameterTransaction(flag, old, @new)); } public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings) @@ -176,7 +230,8 @@ public class StateEditor( var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index)); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, + new MaterialTransaction(index, oldValue, newValue.Game)); } public void ResetMaterialValue(object data, MaterialValueIndex index, ApplySettings settings) @@ -188,7 +243,7 @@ public class StateEditor( var actors = Applier.ChangeMaterialValue(state, index, true); Glamourer.Log.Verbose( $"Reset material value in state {state.Identifier.Incognito(null)} to game value. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, index); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, new MaterialTransaction(index, null, null)); } /// @@ -200,15 +255,15 @@ public class StateEditor( var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); Glamourer.Log.Verbose( - $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, (old, value, MetaIndex.HatState)); + $"Set {index.ToName()} in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, new MetaTransaction(index, old, value)); } /// public void ApplyDesign(object data, MergedDesign mergedDesign, ApplySettings settings) { var state = (ActorState)data; - modApplier.HandleStateApplication(state, mergedDesign); + modApplier.HandleStateApplication(state, mergedDesign, settings.Source, true, settings.RespectManual); if (!Editor.ChangeModelId(state, mergedDesign.Design.DesignData.ModelId, mergedDesign.Design.DesignData.Customize, mergedDesign.Design.GetDesignDataRef().GetEquipmentPtr(), settings.Source, out var oldModelId, settings.Key)) return; @@ -226,7 +281,7 @@ public class StateEditor( out _, settings.Key); } - var customizeFlags = mergedDesign.Design.ApplyCustomizeRaw; + var customizeFlags = mergedDesign.Design.Application.Customize; if (mergedDesign.Design.DoApplyCustomize(CustomizeIndex.Clan)) customizeFlags |= CustomizeFlag.Race; @@ -245,7 +300,7 @@ public class StateEditor( state.Sources[parameter] = StateSource.Game; } - foreach (var parameter in mergedDesign.Design.ApplyParameters.Iterate()) + foreach (var parameter in mergedDesign.Design.Application.Parameters.Iterate()) { if (settings.RespectManual && state.Sources[parameter].IsManual()) continue; @@ -269,15 +324,22 @@ public class StateEditor( if (mergedDesign.Design.DoApplyStain(slot)) if (!settings.RespectManual || !state.Sources[slot, true].IsManual()) - Editor.ChangeStain(state, slot, mergedDesign.Design.DesignData.Stain(slot), + Editor.ChangeStains(state, slot, mergedDesign.Design.DesignData.Stain(slot), Source(slot.ToState(true)), out _, settings.Key); } + foreach (var slot in BonusExtensions.AllFlags) + { + if (mergedDesign.Design.DoApplyBonusItem(slot)) + if (!settings.RespectManual || !state.Sources[slot].IsManual()) + Editor.ChangeBonusItem(state, slot, mergedDesign.Design.DesignData.BonusItem(slot), Source(slot), out _, settings.Key); + } + foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots) { if (mergedDesign.Design.DoApplyStain(weaponSlot)) if (!settings.RespectManual || !state.Sources[weaponSlot, true].IsManual()) - Editor.ChangeStain(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot), + Editor.ChangeStains(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot), Source(weaponSlot.ToState(true)), out _, settings.Key); if (!mergedDesign.Design.DoApplyEquip(weaponSlot)) @@ -301,7 +363,7 @@ public class StateEditor( } var currentType = state.BaseData.Item(weaponSlot).Type; - if (mergedDesign.Weapons.TryGet(currentType, state.LastJob, out var weapon)) + if (mergedDesign.Weapons.TryGet(currentType, state.LastJob, true, out var weapon)) { var source = settings.UseSingleSource ? settings.Source : weapon.Item2 is StateSource.Game ? StateSource.Game : settings.Source; @@ -322,7 +384,7 @@ public class StateEditor( Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); } - if (settings.ResetMaterials) + if (settings.ResetMaterials || !settings.RespectManual && mergedDesign.ResetAdvancedDyes) state.Materials.Clear(); foreach (var (key, value) in mergedDesign.Design.Materials) @@ -346,7 +408,8 @@ public class StateEditor( } else if (!value.Revert) { - Editor.ChangeMaterialValue(state, idx, new MaterialValueState(ColorRow.Empty, value.Value, CharacterWeapon.Empty, source), + Editor.ChangeMaterialValue(state, idx, + new MaterialValueState(ColorRow.Empty, value.Value, CharacterWeapon.Empty, source), settings.Source, out _, settings.Key); } } @@ -358,7 +421,9 @@ public class StateEditor( Glamourer.Log.Verbose( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, mergedDesign.Design); + StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later + if (settings.IsFinal) + StateFinalized.Invoke(StateFinalizationType.DesignApplied, actors); return; @@ -379,7 +444,8 @@ public class StateEditor( if (!settings.MergeLinks || design is not Design d) merged = new MergedDesign(design); else - merged = merger.Merge(d.AllLinks, state.ModelData.IsHuman ? state.ModelData.Customize : CustomizeArray.Default, state.BaseData, + merged = merger.Merge(d.AllLinks(true), state.ModelData.IsHuman ? state.ModelData.Customize : CustomizeArray.Default, + state.BaseData, false, Config.AlwaysApplyAssociatedMods); ApplyDesign(data, merged, settings with @@ -392,19 +458,23 @@ public class StateEditor( /// Apply offhand item and potentially gauntlets if configured. - private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, StainId? newStain, ApplySettings settings) + private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, StainIds? newStains, ApplySettings settings) { if (!Config.ChangeEntireItem || !settings.Source.IsManual()) return; - var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); + var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); + // Do not change Shields to nothing. + if (mh.Type is FullEquipType.Sword) + return; + var offhand = newMainhand != null ? Items.GetDefaultOffhand(mh) : state.ModelData.Item(EquipSlot.OffHand); - var stain = newStain ?? state.ModelData.Stain(EquipSlot.MainHand); + var stains = newStains ?? state.ModelData.Stain(EquipSlot.MainHand); if (offhand.Valid) - ChangeEquip(state, EquipSlot.OffHand, offhand, stain, settings); + ChangeEquip(state, EquipSlot.OffHand, offhand, stains, settings); if (mh is { Type: FullEquipType.Fists } && Items.ItemData.Tertiary.TryGetValue(mh.ItemId, out var gauntlets)) ChangeEquip(state, EquipSlot.Hands, newMainhand != null ? gauntlets : state.ModelData.Item(EquipSlot.Hands), - stain, settings); + stains, settings); } } diff --git a/Glamourer/State/StateIndex.cs b/Glamourer/State/StateIndex.cs index a55d6b1..dff05a3 100644 --- a/Glamourer/State/StateIndex.cs +++ b/Glamourer/State/StateIndex.cs @@ -1,4 +1,5 @@ -using Glamourer.Designs; +using Glamourer.Api.Enums; +using Glamourer.Designs; using Glamourer.GameData; using Penumbra.GameData.Enums; @@ -38,6 +39,13 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators Invalid, }; + public static implicit operator StateIndex(BonusItemFlag flag) + => flag switch + { + BonusItemFlag.Glasses => new StateIndex(BonusItemGlasses), + _ => Invalid, + }; + public static implicit operator StateIndex(CustomizeIndex index) => index switch { @@ -103,7 +111,9 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators new StateIndex(ParamHairSpecular), CustomizeParameterFlag.HairHighlight => new StateIndex(ParamHairHighlight), CustomizeParameterFlag.LeftEye => new StateIndex(ParamLeftEye), + CustomizeParameterFlag.LeftLimbalIntensity => new StateIndex(ParamLeftLimbalIntensity), CustomizeParameterFlag.RightEye => new StateIndex(ParamRightEye), + CustomizeParameterFlag.RightLimbalIntensity => new StateIndex(ParamRightLimbalIntensity), CustomizeParameterFlag.FeatureColor => new StateIndex(ParamFeatureColor), CustomizeParameterFlag.FacePaintUvMultiplier => new StateIndex(ParamFacePaintUvMultiplier), CustomizeParameterFlag.FacePaintUvOffset => new StateIndex(ParamFacePaintUvOffset), @@ -179,8 +189,9 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators All + public const int Size = BonusItemGlasses + 1; + + public static IEnumerable All => Enumerable.Range(0, Size - 1).Select(i => new StateIndex(i)); - public bool GetApply(DesignBase data) - => GetFlag() switch - { - EquipFlag e => data.ApplyEquip.HasFlag(e), - CustomizeFlag c => data.ApplyCustomize.HasFlag(c), - MetaFlag m => data.ApplyMeta.HasFlag(m), - CrestFlag c => data.ApplyCrest.HasFlag(c), - CustomizeParameterFlag c => data.ApplyParameters.HasFlag(c), - bool v => v, - _ => false, - }; - public string ToName() => GetFlag() switch { @@ -223,6 +226,7 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators m.ToIndex().ToName(), CrestFlag c => c.ToLabel(), CustomizeParameterFlag c => c.ToName(), + BonusItemFlag b => b.ToName(), bool v => "Model ID", _ => "Unknown", }; @@ -297,6 +301,7 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators MetaFlag.HatState, MetaVisorState => MetaFlag.VisorState, MetaWeaponState => MetaFlag.WeaponState, + MetaEarState => MetaFlag.EarState, MetaModelId => true, CrestHead => CrestFlag.Head, @@ -311,110 +316,19 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators CustomizeParameterFlag.HairSpecular, ParamHairHighlight => CustomizeParameterFlag.HairHighlight, ParamLeftEye => CustomizeParameterFlag.LeftEye, + ParamLeftLimbalIntensity => CustomizeParameterFlag.LeftLimbalIntensity, ParamRightEye => CustomizeParameterFlag.RightEye, + ParamRightLimbalIntensity => CustomizeParameterFlag.RightLimbalIntensity, ParamFeatureColor => CustomizeParameterFlag.FeatureColor, ParamFacePaintUvMultiplier => CustomizeParameterFlag.FacePaintUvMultiplier, ParamFacePaintUvOffset => CustomizeParameterFlag.FacePaintUvOffset, ParamDecalColor => CustomizeParameterFlag.DecalColor, + BonusItemGlasses => BonusItemFlag.Glasses, + _ => -1, }; - public object? GetValue(in DesignData data) - { - return Value switch - { - EquipHead => data.Item(EquipSlot.Head), - EquipBody => data.Item(EquipSlot.Body), - EquipHands => data.Item(EquipSlot.Hands), - EquipLegs => data.Item(EquipSlot.Legs), - EquipFeet => data.Item(EquipSlot.Feet), - EquipEars => data.Item(EquipSlot.Ears), - EquipNeck => data.Item(EquipSlot.Neck), - EquipWrist => data.Item(EquipSlot.Wrists), - EquipRFinger => data.Item(EquipSlot.RFinger), - EquipLFinger => data.Item(EquipSlot.LFinger), - EquipMainhand => data.Item(EquipSlot.MainHand), - EquipOffhand => data.Item(EquipSlot.OffHand), - - StainHead => data.Stain(EquipSlot.Head), - StainBody => data.Stain(EquipSlot.Body), - StainHands => data.Stain(EquipSlot.Hands), - StainLegs => data.Stain(EquipSlot.Legs), - StainFeet => data.Stain(EquipSlot.Feet), - StainEars => data.Stain(EquipSlot.Ears), - StainNeck => data.Stain(EquipSlot.Neck), - StainWrist => data.Stain(EquipSlot.Wrists), - StainRFinger => data.Stain(EquipSlot.RFinger), - StainLFinger => data.Stain(EquipSlot.LFinger), - StainMainhand => data.Stain(EquipSlot.MainHand), - StainOffhand => data.Stain(EquipSlot.OffHand), - - CustomizeRace => data.Customize[CustomizeIndex.Race], - CustomizeGender => data.Customize[CustomizeIndex.Gender], - CustomizeBodyType => data.Customize[CustomizeIndex.BodyType], - CustomizeHeight => data.Customize[CustomizeIndex.Height], - CustomizeClan => data.Customize[CustomizeIndex.Clan], - CustomizeFace => data.Customize[CustomizeIndex.Face], - CustomizeHairstyle => data.Customize[CustomizeIndex.Hairstyle], - CustomizeHighlights => data.Customize[CustomizeIndex.Highlights], - CustomizeSkinColor => data.Customize[CustomizeIndex.SkinColor], - CustomizeEyeColorRight => data.Customize[CustomizeIndex.EyeColorRight], - CustomizeHairColor => data.Customize[CustomizeIndex.HairColor], - CustomizeHighlightsColor => data.Customize[CustomizeIndex.HighlightsColor], - CustomizeFacialFeature1 => data.Customize[CustomizeIndex.FacialFeature1], - CustomizeFacialFeature2 => data.Customize[CustomizeIndex.FacialFeature2], - CustomizeFacialFeature3 => data.Customize[CustomizeIndex.FacialFeature3], - CustomizeFacialFeature4 => data.Customize[CustomizeIndex.FacialFeature4], - CustomizeFacialFeature5 => data.Customize[CustomizeIndex.FacialFeature5], - CustomizeFacialFeature6 => data.Customize[CustomizeIndex.FacialFeature6], - CustomizeFacialFeature7 => data.Customize[CustomizeIndex.FacialFeature7], - CustomizeLegacyTattoo => data.Customize[CustomizeIndex.LegacyTattoo], - CustomizeTattooColor => data.Customize[CustomizeIndex.TattooColor], - CustomizeEyebrows => data.Customize[CustomizeIndex.Eyebrows], - CustomizeEyeColorLeft => data.Customize[CustomizeIndex.EyeColorLeft], - CustomizeEyeShape => data.Customize[CustomizeIndex.EyeShape], - CustomizeSmallIris => data.Customize[CustomizeIndex.SmallIris], - CustomizeNose => data.Customize[CustomizeIndex.Nose], - CustomizeJaw => data.Customize[CustomizeIndex.Jaw], - CustomizeMouth => data.Customize[CustomizeIndex.Mouth], - CustomizeLipstick => data.Customize[CustomizeIndex.Lipstick], - CustomizeLipColor => data.Customize[CustomizeIndex.LipColor], - CustomizeMuscleMass => data.Customize[CustomizeIndex.MuscleMass], - CustomizeTailShape => data.Customize[CustomizeIndex.TailShape], - CustomizeBustSize => data.Customize[CustomizeIndex.BustSize], - CustomizeFacePaint => data.Customize[CustomizeIndex.FacePaint], - CustomizeFacePaintReversed => data.Customize[CustomizeIndex.FacePaintReversed], - CustomizeFacePaintColor => data.Customize[CustomizeIndex.FacePaintColor], - - MetaWetness => data.GetMeta(MetaIndex.Wetness), - MetaHatState => data.GetMeta(MetaIndex.HatState), - MetaVisorState => data.GetMeta(MetaIndex.VisorState), - MetaWeaponState => data.GetMeta(MetaIndex.WeaponState), - MetaModelId => data.ModelId, - - CrestHead => data.Crest(CrestFlag.Head), - CrestBody => data.Crest(CrestFlag.Body), - CrestOffhand => data.Crest(CrestFlag.OffHand), - - ParamSkinDiffuse => data.Parameters[CustomizeParameterFlag.SkinDiffuse], - ParamMuscleTone => data.Parameters[CustomizeParameterFlag.MuscleTone], - ParamSkinSpecular => data.Parameters[CustomizeParameterFlag.SkinSpecular], - ParamLipDiffuse => data.Parameters[CustomizeParameterFlag.LipDiffuse], - ParamHairDiffuse => data.Parameters[CustomizeParameterFlag.HairDiffuse], - ParamHairSpecular => data.Parameters[CustomizeParameterFlag.HairSpecular], - ParamHairHighlight => data.Parameters[CustomizeParameterFlag.HairHighlight], - ParamLeftEye => data.Parameters[CustomizeParameterFlag.LeftEye], - ParamRightEye => data.Parameters[CustomizeParameterFlag.RightEye], - ParamFeatureColor => data.Parameters[CustomizeParameterFlag.FeatureColor], - ParamFacePaintUvMultiplier => data.Parameters[CustomizeParameterFlag.FacePaintUvMultiplier], - ParamFacePaintUvOffset => data.Parameters[CustomizeParameterFlag.FacePaintUvOffset], - ParamDecalColor => data.Parameters[CustomizeParameterFlag.DecalColor], - - _ => null, - }; - } - private static string GetName(EquipFlag flag) { var slot = flag.ToSlot(out var stain); diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 73c8f0d..4b70718 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -9,11 +9,13 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.GameData; using Penumbra.GameData.DataContainers; using Glamourer.Designs; using Penumbra.GameData.Interop; -using ObjectManager = Glamourer.Interop.ObjectManager; +using Glamourer.Api.Enums; namespace Glamourer.State; @@ -26,17 +28,21 @@ public class StateListener : IDisposable { private readonly Configuration _config; private readonly ActorManager _actors; - private readonly ObjectManager _objects; + private readonly ActorObjectManager _objects; private readonly StateManager _manager; private readonly StateApplier _applier; private readonly ItemManager _items; private readonly CustomizeService _customizations; private readonly PenumbraService _penumbra; - private readonly SlotUpdating _slotUpdating; + private readonly EquipSlotUpdating _equipSlotUpdating; + private readonly BonusSlotUpdating _bonusSlotUpdating; + private readonly GearsetDataLoaded _gearsetDataLoaded; private readonly WeaponLoading _weaponLoading; private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; + private readonly VieraEarStateChanged _vieraEarState; private readonly WeaponVisibilityChanged _weaponVisibility; + private readonly StateFinalized _stateFinalized; private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; @@ -46,23 +52,27 @@ public class StateListener : IDisposable private readonly CrestService _crestService; private readonly ICondition _condition; + private readonly Dictionary _fistOffhands = []; + private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid; + private bool _isPlayerNpc; private ActorState? _creatingState; private ActorState? _customizeState; - private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty; public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config, - SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, - HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, - StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, - ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, CrestService crestService) + EquipSlotUpdating equipSlotUpdating, GearsetDataLoaded gearsetDataLoaded, WeaponLoading weaponLoading, VisorStateChanged visorState, + WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, + FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ActorObjectManager objects, + GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, + CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized, VieraEarStateChanged vieraEarState) { _manager = manager; _items = items; _penumbra = penumbra; _actors = actors; _config = config; - _slotUpdating = slotUpdating; + _equipSlotUpdating = equipSlotUpdating; + _gearsetDataLoaded = gearsetDataLoaded; _weaponLoading = weaponLoading; _visorState = visorState; _weaponVisibility = weaponVisibility; @@ -78,6 +88,9 @@ public class StateListener : IDisposable _customizations = customizations; _condition = condition; _crestService = crestService; + _bonusSlotUpdating = bonusSlotUpdating; + _stateFinalized = stateFinalized; + _vieraEarState = vieraEarState; Subscribe(); } @@ -113,11 +126,13 @@ public class StateListener : IDisposable return; _creatingIdentifier = actor.GetIdentifier(_actors); - ref var modelId = ref *(uint*)modelPtr; ref var customize = ref *(CustomizeArray*)customizePtr; if (_autoDesignApplier.Reduce(actor, _creatingIdentifier, out _creatingState)) { + _isPlayerNpc = _creatingIdentifier.Type is IdentifierType.Player + && actor.IsCharacter + && actor.AsCharacter->GetObjectKind() is ObjectKind.EventNpc; switch (UpdateBaseData(actor, _creatingState, modelId, customizePtr, equipDataPtr)) { // TODO handle right @@ -192,9 +207,7 @@ public class StateListener : IDisposable } break; - case UpdateState.NoChange: - customize = state.ModelData.Customize; - break; + case UpdateState.NoChange: customize = state.ModelData.Customize; break; } } @@ -202,7 +215,7 @@ public class StateListener : IDisposable /// A draw model loads a new equipment piece. /// Update base data, apply or update model data, and protect against restricted gear. /// - private void OnSlotUpdating(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) + private void OnEquipSlotUpdating(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) { var actor = _penumbra.GameObjectFromDrawObject(model); if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) @@ -227,9 +240,49 @@ public class StateListener : IDisposable (_, armor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } - private void OnMovedEquipment((EquipSlot, uint, StainId)[] items) + private void OnBonusSlotUpdating(Model model, BonusItemFlag slot, ref CharacterArmor item, ref ulong returnValue) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (actor.Identifier(_actors, out var identifier) + && _manager.TryGetValue(identifier, out var state)) + switch (UpdateBaseData(actor, state, slot, item)) + { + // Base data changed equipment while actors were not there. + // Update model state if not on fixed design. + case UpdateState.Change: + var apply = false; + if (!state.Sources[slot].IsFixed()) + _manager.ChangeBonusItem(state, slot, state.BaseData.BonusItem(slot), ApplySettings.Game); + else + apply = true; + if (apply) + item = state.ModelData.BonusItem(slot).Armor(); + break; + // Use current model data. + case UpdateState.NoChange: item = state.ModelData.BonusItem(slot).Armor(); break; + case UpdateState.Transformed: break; + } + } + + private void OnGearsetDataLoaded(Actor actor, Model model) + { + if (!actor.Valid || _condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + // ensure actor and state are valid. + if (!actor.Identifier(_actors, out var identifier)) + return; + + if (_objects.TryGetValue(identifier, out var actors) && actors.Valid) + _stateFinalized.Invoke(StateFinalizationType.Gearset, actors); + } + + + private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items) { - _objects.Update(); var (identifier, objects) = _objects.PlayerData; if (!identifier.IsValid || !_manager.TryGetValue(identifier, out var state)) return; @@ -250,14 +303,14 @@ public class StateListener : IDisposable && current.Weapon == changed.Weapon && !state.Sources[slot, false].IsFixed(); - var stainChanged = current.Stain == changed.Stain && !state.Sources[slot, true].IsFixed(); + var stainChanged = current.Stains == changed.Stains && !state.Sources[slot, true].IsFixed(); - switch ((itemChanged, stainChanged)) + switch (itemChanged, stainChanged) { case (true, true): - _manager.ChangeEquip(state, slot, currentItem, current.Stain, ApplySettings.Game); + _manager.ChangeEquip(state, slot, currentItem, current.Stains, ApplySettings.Game); if (slot is EquipSlot.MainHand or EquipSlot.OffHand) - _applier.ChangeWeapon(objects, slot, currentItem, current.Stain); + _applier.ChangeWeapon(objects, slot, currentItem, current.Stains); else _applier.ChangeArmor(objects, slot, current.ToArmor(), !state.Sources[slot, false].IsFixed(), state.ModelData.IsHatVisible()); @@ -265,14 +318,14 @@ public class StateListener : IDisposable case (true, false): _manager.ChangeItem(state, slot, currentItem, ApplySettings.Game); if (slot is EquipSlot.MainHand or EquipSlot.OffHand) - _applier.ChangeWeapon(objects, slot, currentItem, model.Stain); + _applier.ChangeWeapon(objects, slot, currentItem, model.Stains); else - _applier.ChangeArmor(objects, slot, current.ToArmor(model.Stain), !state.Sources[slot, false].IsFixed(), + _applier.ChangeArmor(objects, slot, current.ToArmor(model.Stains), !state.Sources[slot, false].IsFixed(), state.ModelData.IsHatVisible()); break; case (false, true): - _manager.ChangeStain(state, slot, current.Stain, ApplySettings.Game); - _applier.ChangeStain(objects, slot, current.Stain); + _manager.ChangeStains(state, slot, current.Stains, ApplySettings.Game); + _applier.ChangeStain(objects, slot, current.Stains); break; } } @@ -289,8 +342,14 @@ public class StateListener : IDisposable return; // Fist weapon gauntlet hack. - if (slot is EquipSlot.OffHand && weapon.Variant == 0 && weapon.Weapon.Id != 0 && _lastFistOffhand.Weapon.Id != 0) - weapon = _lastFistOffhand; + if (slot is EquipSlot.OffHand + && weapon.Variant == 0 + && weapon.Weapon.Id != 0 + && _fistOffhands.TryGetValue(actor, out var lastFistOffhand)) + { + Glamourer.Log.Verbose($"Applying stored fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + weapon = lastFistOffhand; + } if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) @@ -308,13 +367,11 @@ public class StateListener : IDisposable apply = true; if (!state.Sources[slot, true].IsFixed()) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); + _manager.ChangeStains(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; break; - case UpdateState.NoChange: - apply = true; - break; + case UpdateState.NoChange: apply = true; break; } var baseType = slot is EquipSlot.OffHand ? state.BaseData.MainhandType.Offhand() : state.BaseData.MainhandType; @@ -332,7 +389,7 @@ public class StateListener : IDisposable else { if (weapon.Skeleton.Id != 0) - weapon = weapon.With(newWeapon.Stain); + weapon = weapon.With(newWeapon.Stains); // Force unlock if necessary. _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game with { Key = state.Combination }); } @@ -340,8 +397,12 @@ public class StateListener : IDisposable // Fist Weapon Offhand hack. if (slot is EquipSlot.MainHand && weapon.Skeleton.Id is > 1600 and < 1651) - _lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant, - weapon.Stain); + { + lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant, + weapon.Stains); + _fistOffhands[actor] = lastFistOffhand; + Glamourer.Log.Excessive($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + } _funModule.ApplyFunToWeapon(actor, ref weapon, slot); } @@ -365,7 +426,7 @@ public class StateListener : IDisposable { var item = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); state.BaseData.SetItem(EquipSlot.Head, item); - state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stain); + state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stains); return UpdateState.Change; } @@ -377,15 +438,22 @@ public class StateListener : IDisposable } var baseData = state.BaseData.Armor(slot); - var change = UpdateState.NoChange; - if (baseData.Stain != armor.Stain) + + var change = UpdateState.NoChange; + if (baseData.Stains != armor.Stains) { - state.BaseData.SetStain(slot, armor.Stain); + if (_isPlayerNpc) + return UpdateState.Transformed; + + state.BaseData.SetStain(slot, armor.Stains); change = UpdateState.Change; } if (baseData.Set.Id != armor.Set.Id || baseData.Variant != armor.Variant && !fistWeapon) { + if (_isPlayerNpc) + return UpdateState.Transformed; + var item = _items.Identify(slot, armor.Set, armor.Variant); state.BaseData.SetItem(slot, item); change = UpdateState.Change; @@ -403,6 +471,31 @@ public class StateListener : IDisposable } } + private UpdateState UpdateBaseData(Actor actor, ActorState state, BonusItemFlag slot, CharacterArmor item) + { + var actorItemId = actor.GetBonusItem(slot); + if (!_items.IsBonusItemValid(slot, actorItemId, out var actorItem)) + return UpdateState.NoChange; + + // The actor item does not correspond to the model item, thus the actor is transformed. + if (actorItem.PrimaryId != item.Set || actorItem.Variant != item.Variant) + return UpdateState.Transformed; + + var baseData = state.BaseData.BonusItem(slot); + var change = UpdateState.NoChange; + if (baseData.Id != actorItem.Id || baseData.PrimaryId != item.Set || baseData.Variant != item.Variant) + { + if (_isPlayerNpc) + return UpdateState.Transformed; + + var identified = _items.Identify(slot, item.Set, item.Variant); + state.BaseData.SetBonusItem(slot, identified); + change = UpdateState.Change; + } + + return change; + } + /// Handle a full equip slot update for base data and model data. private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) { @@ -418,7 +511,7 @@ public class StateListener : IDisposable apply = true; if (!state.Sources[slot, true].IsFixed()) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); + _manager.ChangeStains(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; @@ -503,14 +596,17 @@ public class StateListener : IDisposable if (slot is EquipSlot.OffHand && weapon.Value == 0 && actor.GetMainhand().Skeleton.Id is > 1600 and < 1651) return UpdateState.NoChange; - if (baseData.Stain != weapon.Stain) + if (baseData.Stains != weapon.Stains) { - state.BaseData.SetStain(slot, weapon.Stain); + state.BaseData.SetStain(slot, weapon.Stains); change = UpdateState.Change; } if (baseData.Skeleton.Id != weapon.Skeleton.Id || baseData.Weapon.Id != weapon.Weapon.Id || baseData.Variant != weapon.Variant) { + if (_isPlayerNpc) + return UpdateState.Transformed; + var item = _items.Identify(slot, weapon.Skeleton, weapon.Weapon, weapon.Variant, slot is EquipSlot.OffHand ? state.BaseData.MainhandType : FullEquipType.Unknown); state.BaseData.SetItem(slot, item); @@ -528,7 +624,7 @@ public class StateListener : IDisposable private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData) { // Model ID does not agree between game object and new draw object => Transformation. - if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId) + if (modelId != (uint)actor.AsCharacter->ModelContainer.ModelCharaId) return UpdateState.Transformed; // Model ID did not change to stored state. @@ -558,6 +654,10 @@ public class StateListener : IDisposable if (checkTransform && !actor.Customize->Equals(customize)) return UpdateState.Transformed; + // Check for player NPCs with a different game state. + if (_isPlayerNpc && !actor.Customize->Equals(state.BaseData.Customize)) + return UpdateState.Transformed; + // Customize array did not change to stored state. if (state.BaseData.Customize.Equals(customize)) return UpdateState.NoChange; // TODO: handle wrong base data. @@ -578,6 +678,8 @@ public class StateListener : IDisposable // We do not need to handle fixed designs, // since a fixed design would already have established state-tracking. var actor = _penumbra.GameObjectFromDrawObject(model); + if (!actor.IsCharacter) + return; // Only actually change anything if the actor state changed, // when equipping headgear the method is called with the current draw object state, @@ -611,6 +713,44 @@ public class StateListener : IDisposable } } + /// Handle visor state changes made by the game. + private void OnVieraEarChange(Actor actor, ref bool value) + { + // Value is inverted compared to our own handling. + + // Skip updates when in customize update. + if (ChangeCustomizeService.InUpdate.InMethod) + return; + + if (!actor.IsCharacter) + return; + + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors, out var identifier)) + return; + + if (!_manager.TryGetValue(identifier, out var state)) + return; + + // Update visor base state. + if (state.BaseData.SetEarsVisible(!value)) + { + // if base state changed, either overwrite the actual value if we have fixed values, + // or overwrite the stored model state with the new one. + if (state.Sources[MetaIndex.EarState].IsFixed()) + value = !state.ModelData.AreEarsVisible(); + else + _manager.ChangeMetaState(state, MetaIndex.EarState, !value, ApplySettings.Game); + } + else + { + // if base state did not change, overwrite the value with the model state one. + value = !state.ModelData.AreEarsVisible(); + } + } + /// Handle Hat Visibility changes. These act on the game object. private void OnHeadGearVisibilityChange(Actor actor, ref bool value) { @@ -697,10 +837,13 @@ public class StateListener : IDisposable { _penumbra.CreatingCharacterBase += OnCreatingCharacterBase; _penumbra.CreatedCharacterBase += OnCreatedCharacterBase; - _slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener); + _equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener); + _bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener); + _gearsetDataLoaded.Subscribe(OnGearsetDataLoaded, GearsetDataLoaded.Priority.StateListener); _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); + _vieraEarState.Subscribe(OnVieraEarChange, VieraEarStateChanged.Priority.StateListener); _headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener); _weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener); _changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener); @@ -713,10 +856,13 @@ public class StateListener : IDisposable { _penumbra.CreatingCharacterBase -= OnCreatingCharacterBase; _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; - _slotUpdating.Unsubscribe(OnSlotUpdating); + _equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating); + _bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating); + _gearsetDataLoaded.Unsubscribe(OnGearsetDataLoaded); _movedEquipment.Unsubscribe(OnMovedEquipment); _weaponLoading.Unsubscribe(OnWeaponLoading); _visorState.Unsubscribe(OnVisorChange); + _vieraEarState.Unsubscribe(OnVieraEarChange); _headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange); _weaponVisibility.Unsubscribe(OnWeaponVisibilityChange); _changeCustomizeService.Unsubscribe(OnCustomizeChange); @@ -776,7 +922,7 @@ public class StateListener : IDisposable case StateSource.Manual: if (state.BaseData.Parameters.Set(flag, newValue)) _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); - else if (_config.UseAdvancedParameters) + else model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; case StateSource.IpcManual: @@ -787,8 +933,7 @@ public class StateListener : IDisposable break; case StateSource.Fixed: state.BaseData.Parameters.Set(flag, newValue); - if (_config.UseAdvancedParameters) - model.ApplySingleParameterData(flag, state.ModelData.Parameters); + model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; case StateSource.IpcFixed: state.BaseData.Parameters.Set(flag, newValue); @@ -797,8 +942,12 @@ public class StateListener : IDisposable case StateSource.Pending: state.BaseData.Parameters.Set(flag, newValue); state.Sources[flag] = StateSource.Manual; - if (_config.UseAdvancedParameters) - model.ApplySingleParameterData(flag, state.ModelData.Parameters); + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.IpcPending: + state.BaseData.Parameters.Set(flag, newValue); + state.Sources[flag] = StateSource.IpcManual; + model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index f057580..e8926d6 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -18,19 +18,20 @@ using Penumbra.GameData.Interop; namespace Glamourer.State; public sealed class StateManager( - ActorManager _actors, + ActorManager actors, ItemManager items, - StateChanged @event, + StateChanged changeEvent, + StateFinalized finalizeEvent, StateApplier applier, InternalStateEditor editor, - HumanModelList _humans, - IClientState _clientState, + HumanModelList humans, + IClientState clientState, Configuration config, JobChangeState jobChange, DesignMerger merger, ModSettingApplier modApplier, GPoseService gPose) - : StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier, gPose), + : StateEditor(editor, applier, changeEvent, finalizeEvent, jobChange, config, items, merger, modApplier, gPose), IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -61,7 +62,7 @@ public sealed class StateManager( /// public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) - => GetOrCreate(actor.GetIdentifier(_actors), actor, out state); + => GetOrCreate(actor.GetIdentifier(actors), actor, out state); /// Try to obtain or create a new state for an existing actor. Returns false if no state could be created. public unsafe bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -81,7 +82,7 @@ public sealed class StateManager( ModelData = FromActor(actor, true, false), BaseData = FromActor(actor, false, false), LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), - LastTerritory = _clientState.TerritoryType, + LastTerritory = clientState.TerritoryType, }; // state.Identifier is owned. _states.Add(state.Identifier, state); @@ -114,14 +115,14 @@ public sealed class StateManager( // Model ID is only unambiguously contained in the game object. // The draw object only has the object type. // TODO reverse search model data to get model id from model. - if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) + if (!humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) { - ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData, - (nint)(&actor.AsCharacter->DrawData.Head)); + ret.LoadNonHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData, + (nint)Unsafe.AsPointer(ref actor.AsCharacter->DrawData.EquipmentModelIds[0])); return ret; } - ret.ModelId = (uint)actor.AsCharacter->CharacterData.ModelCharaId; + ret.ModelId = (uint)actor.AsCharacter->ModelContainer.ModelCharaId; ret.IsHuman = true; CharacterWeapon main; @@ -141,7 +142,7 @@ public sealed class StateManager( var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); var headItem = Items.Identify(EquipSlot.Head, head.Set, head.Variant); ret.SetItem(EquipSlot.Head, headItem); - ret.SetStain(EquipSlot.Head, head.Stain); + ret.SetStain(EquipSlot.Head, head.Stains); // The other slots can be used from the draw object. foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) @@ -149,7 +150,7 @@ public sealed class StateManager( var armor = model.GetArmor(slot); var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); - ret.SetStain(slot, armor.Stain); + ret.SetStain(slot, armor.Stains); } // Weapons use the draw objects of the weapons, but require the game object either way. @@ -157,9 +158,17 @@ public sealed class StateManager( // Visor state is a flag on the game object, but we can see the actual state on the draw object. ret.SetVisor(VisorService.GetVisorState(model)); + ret.SetEarsVisible(model.VieraEarsVisible); foreach (var slot in CrestExtensions.AllRelevantSet) ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot)); + + foreach (var slot in BonusExtensions.AllFlags) + { + var data = model.GetBonus(slot); + var item = Items.Identify(slot, data.Set, data.Variant); + ret.SetBonusItem(slot, item); + } } else { @@ -171,29 +180,36 @@ public sealed class StateManager( var armor = actor.GetArmor(slot); var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); - ret.SetStain(slot, armor.Stain); + ret.SetStain(slot, armor.Stains); } main = actor.GetMainhand(); off = actor.GetOffhand(); FistWeaponHack(ref ret, ref main, ref off); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); - + ret.SetEarsVisible(actor.ShowVieraEars); foreach (var slot in CrestExtensions.AllRelevantSet) ret.SetCrest(slot, actor.GetCrest(slot)); + + foreach (var slot in BonusExtensions.AllFlags) + { + var id = actor.GetBonusItem(slot); + var item = Items.Resolve(slot, id); + ret.SetBonusItem(slot, item); + } } // Set the weapons regardless of source. var mainItem = Items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); var offItem = Items.Identify(EquipSlot.OffHand, off.Skeleton, off.Weapon, off.Variant, mainItem.Type); ret.SetItem(EquipSlot.MainHand, mainItem); - ret.SetStain(EquipSlot.MainHand, main.Stain); + ret.SetStain(EquipSlot.MainHand, main.Stains); ret.SetItem(EquipSlot.OffHand, offItem); - ret.SetStain(EquipSlot.OffHand, off.Stain); + ret.SetStain(EquipSlot.OffHand, off.Stains); // Wetness can technically only be set in GPose or via external tools. // It is only available in the game object. - ret.SetIsWet(actor.AsCharacter->IsGPoseWet); + ret.SetIsWet(actor.IsGPoseWet); // Weapon visibility could technically be inferred from the weapon draw objects, // but since we use hat visibility from the game object we can also use weapon visibility from it. @@ -214,14 +230,14 @@ public sealed class StateManager( offhand.Variant = mainhand.Variant; offhand.Weapon = mainhand.Weapon; ret.SetItem(EquipSlot.Hands, gauntlets); - ret.SetStain(EquipSlot.Hands, mainhand.Stain); + ret.SetStain(EquipSlot.Hands, mainhand.Stains); } /// Turn an actor human. public void TurnHuman(ActorState state, StateSource source, uint key = 0) => ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); - public void ResetState(ActorState state, StateSource source, uint key = 0) + public void ResetState(ActorState state, StateSource source, uint key = 0, bool isFinal = false) { if (!state.Unlock(key)) return; @@ -241,6 +257,9 @@ public sealed class StateManager( state.Sources[slot, false] = StateSource.Game; } + foreach (var slot in BonusExtensions.AllFlags) + state.Sources[slot] = StateSource.Game; + foreach (var type in Enum.GetValues()) state.Sources[type] = StateSource.Game; @@ -252,13 +271,63 @@ public sealed class StateManager( state.Materials.Clear(); - var actors = ActorData.Invalid; + var objects = ActorData.Invalid; if (source is not StateSource.Game) - actors = Applier.ApplyAll(state, redraw, true); + objects = Applier.ApplyAll(state, redraw, true); Glamourer.Log.Verbose( - $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); + $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // only invoke if we define this reset call as the final call in our state update. + if (isFinal) + StateFinalized.Invoke(StateFinalizationType.Revert, objects); + } + + public void ResetAdvancedDyes(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + state.ModelData.Parameters = state.BaseData.Parameters; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + var objects = Applier.GetData(state); + if (source is not StateSource.Game) + foreach (var (idx, mat) in state.Materials.Values) + Applier.ChangeMaterialValue(state, objects, MaterialValueIndex.FromKey(idx), mat.Game); + + state.Materials.Clear(); + + Glamourer.Log.Verbose( + $"Reset advanced dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, objects); + } + + public void ResetAdvancedCustomizations(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + state.ModelData.Parameters = state.BaseData.Parameters; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + var objects = ActorData.Invalid; + if (source is not StateSource.Game) + objects = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); + + state.Materials.Clear(); + + Glamourer.Log.Verbose( + $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, objects); } public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) @@ -276,7 +345,7 @@ public sealed class StateManager( { actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); foreach (var (idx, mat) in state.Materials.Values) - Applier.ChangeMaterialValue(actors, MaterialValueIndex.FromKey(idx), mat.Game, true); + Applier.ChangeMaterialValue(state, actors, MaterialValueIndex.FromKey(idx), mat.Game); } state.Materials.Clear(); @@ -284,6 +353,8 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, actors); } public void ResetCustomize(ActorState state, StateSource source, uint key = 0) @@ -294,12 +365,15 @@ public sealed class StateManager( foreach (var flag in CustomizationExtensions.All) state.Sources[flag] = StateSource.Game; - state.ModelData = state.BaseData; + state.ModelData.ModelId = state.BaseData.ModelId; + state.ModelData.Customize = state.BaseData.Customize; var actors = ActorData.Invalid; if (source is not StateSource.Game) actors = Applier.ChangeCustomize(state, true); Glamourer.Log.Verbose( $"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertCustomize, actors); } public void ResetEquip(ActorState state, StateSource source, uint key = 0) @@ -318,6 +392,13 @@ public sealed class StateManager( } } + foreach (var slot in BonusExtensions.AllFlags) + { + state.Sources[slot] = StateSource.Game; + if (source is not StateSource.Game) + state.ModelData.SetBonusItem(slot, state.BaseData.BonusItem(slot)); + } + var actors = ActorData.Invalid; if (source is not StateSource.Game) { @@ -328,6 +409,12 @@ public sealed class StateManager( state.ModelData.IsHatVisible()); } + foreach (var slot in BonusExtensions.AllFlags) + { + var item = state.ModelData.BonusItem(slot); + Applier.ChangeBonusItem(actors, slot, item.PrimaryId, item.Variant); + } + var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; Applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; @@ -336,6 +423,8 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertEquipment, actors); } public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) @@ -364,6 +453,15 @@ public sealed class StateManager( } } + foreach (var slot in BonusExtensions.AllFlags) + { + if (state.Sources[slot] is StateSource.Fixed) + { + state.Sources[slot] = StateSource.Game; + state.ModelData.SetBonusItem(slot, state.BaseData.BonusItem(slot)); + } + } + foreach (var slot in CrestExtensions.AllRelevantSet) { if (state.Sources[slot] is StateSource.Fixed) @@ -403,19 +501,44 @@ public sealed class StateManager( } } - public void ReapplyState(Actor actor, bool forceRedraw, StateSource source) + public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isFinal = false) { if (!GetOrCreate(actor, out var state)) return; - ReapplyState(actor, state, forceRedraw, source); + ReapplyState(actor, state, forceRedraw, source, isFinal); } - public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source) + public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isFinal) { var data = Applier.ApplyAll(state, - forceRedraw || !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + forceRedraw + || !actor.Model.IsHuman + || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + if (isFinal) + StateFinalized.Invoke(StateFinalizationType.Reapply, data); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, bool forceRedraw, bool wasReset, StateSource source) + { + if (!GetOrCreate(actor, out var state)) + return; + + ReapplyAutomationState(actor, state, forceRedraw, wasReset, source); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, ActorState state, bool forceRedraw, bool wasReset, StateSource source) + { + var data = Applier.ApplyAll(state, + forceRedraw + || !actor.Model.IsHuman + || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + // invoke the automation update based on what reset is. + StateFinalized.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier) diff --git a/Glamourer/State/StateSource.cs b/Glamourer/State/StateSource.cs index d489814..9a12214 100644 --- a/Glamourer/State/StateSource.cs +++ b/Glamourer/State/StateSource.cs @@ -10,8 +10,9 @@ public enum StateSource : byte IpcFixed, IpcManual, - // Only used for CustomizeParameters. + // Only used for CustomizeParameters and advanced dyes. Pending, + IpcPending, } public static class StateSourceExtensions @@ -19,9 +20,10 @@ public static class StateSourceExtensions public static StateSource Base(this StateSource source) => source switch { - StateSource.Manual or StateSource.IpcManual or StateSource.Pending => StateSource.Manual, - StateSource.Fixed or StateSource.IpcFixed => StateSource.Fixed, - _ => StateSource.Game, + StateSource.Manual or StateSource.Pending => StateSource.Manual, + StateSource.IpcManual or StateSource.IpcPending => StateSource.Manual, + StateSource.Fixed or StateSource.IpcFixed => StateSource.Fixed, + _ => StateSource.Game, }; public static bool IsGame(this StateSource source) @@ -34,7 +36,12 @@ public static class StateSourceExtensions => source.Base() is StateSource.Fixed; public static StateSource SetPending(this StateSource source) - => source is StateSource.Manual ? StateSource.Pending : source; + => source switch + { + StateSource.Manual => StateSource.Pending, + StateSource.IpcManual => StateSource.IpcPending, + _ => source, + }; public static bool RequiresChange(this StateSource source) => source switch @@ -46,7 +53,7 @@ public static class StateSourceExtensions }; public static bool IsIpc(this StateSource source) - => source is StateSource.IpcManual or StateSource.IpcFixed; + => source is StateSource.IpcManual or StateSource.IpcFixed or StateSource.IpcPending; } public unsafe struct StateSources @@ -97,6 +104,7 @@ public unsafe struct StateSources case (byte)StateSource.Manual | ((byte)StateSource.Fixed << 4): case (byte)StateSource.IpcFixed | ((byte)StateSource.Fixed << 4): case (byte)StateSource.Pending | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcPending | ((byte)StateSource.Fixed << 4): case (byte)StateSource.IpcManual | ((byte)StateSource.Fixed << 4): _data[i] = (byte)((value & 0x0F) | ((byte)StateSource.Manual << 4)); break; @@ -104,6 +112,7 @@ public unsafe struct StateSources case ((byte)StateSource.Manual << 4) | (byte)StateSource.Fixed: case ((byte)StateSource.IpcFixed << 4) | (byte)StateSource.Fixed: case ((byte)StateSource.Pending << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcPending << 4) | (byte)StateSource.Fixed: case ((byte)StateSource.IpcManual << 4) | (byte)StateSource.Fixed: _data[i] = (byte)((value & 0xF0) | (byte)StateSource.Manual); break; diff --git a/Glamourer/State/WorldSets.cs b/Glamourer/State/WorldSets.cs index eca0988..958a2ed 100644 --- a/Glamourer/State/WorldSets.cs +++ b/Glamourer/State/WorldSets.cs @@ -22,7 +22,7 @@ public class WorldSets [(Gender.Male, Race.AuRa)] = FunEquipSet.Group.FullSetWithoutHat(0257, 2), [(Gender.Female, Race.AuRa)] = FunEquipSet.Group.FullSetWithoutHat(0258, 2), [(Gender.Male, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0597, 1), - [(Gender.Female, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0000, 0), // TODO Hrothgar Female + [(Gender.Female, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0829, 1), [(Gender.Male, Race.Viera)] = FunEquipSet.Group.FullSetWithoutHat(0744, 1), [(Gender.Female, Race.Viera)] = FunEquipSet.Group.FullSetWithoutHat(0581, 1), }; @@ -70,6 +70,8 @@ public class WorldSets (CharacterWeapon.Int(2601, 13, 01), CharacterWeapon.Int(2651, 13, 1)), // DNC, High Steel Chakrams (CharacterWeapon.Int(2802, 13, 01), CharacterWeapon.Empty), // RPR, Deepgold War Scythe (CharacterWeapon.Int(2702, 08, 01), CharacterWeapon.Empty), // SGE, Stonegold Milpreves + (CharacterWeapon.Int(3101, 04, 03), CharacterWeapon.Int(3151, 04, 3)), // VPR, High Durium Twinfangs + (CharacterWeapon.Int(2901, 25, 01), CharacterWeapon.Int(2951, 25, 1)), // PCT, Chocobo Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _50Artifact = @@ -115,6 +117,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 1)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _60Artifact = @@ -160,6 +164,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 01)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _70Artifact = @@ -205,6 +211,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 01)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _80Artifact = @@ -250,6 +258,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(543, 1), CharacterWeapon.Int(2601, 001, 1), CharacterWeapon.Int(2651, 01, 001)), // DNC, Dancer, Krishna (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 013, 1), CharacterWeapon.Empty), // RPR, Harvester's, Demon Slicer (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 008, 1), CharacterWeapon.Empty), // SGE, Therapeute's, Horkos + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 6, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 2, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _90Artifact = @@ -295,29 +305,80 @@ public class WorldSets (FunEquipSet.Group.FullSet(694, 1), CharacterWeapon.Int(2607, 001, 1), CharacterWeapon.Int(2657, 001, 001)), // DNC, Etoile, Terpsichore (FunEquipSet.Group.FullSet(695, 1), CharacterWeapon.Int(2801, 001, 1), CharacterWeapon.Empty), // RPR, Reaper, Death Sickle (FunEquipSet.Group.FullSet(696, 1), CharacterWeapon.Int(2701, 006, 1), CharacterWeapon.Empty), // SGE, Didact, Hagneia + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 006, 1), CharacterWeapon.Int(3151, 006, 001)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 002, 1), CharacterWeapon.Int(2951, 002, 001)), // PCT, Painter's, Round Brush + }; + + private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _100Artifact = + { + (FunEquipSet.Group.FullSet(000, 0), CharacterWeapon.Empty, CharacterWeapon.Empty), // ADV, Nothing + (FunEquipSet.Group.FullSet(772, 1), CharacterWeapon.Int(0220, 002, 01), CharacterWeapon.Int(0114, 002, 001)), // GLA, Caballarius, Clarent, Galahad + (FunEquipSet.Group.FullSet(773, 1), CharacterWeapon.Int(0338, 001, 01), CharacterWeapon.Int(0388, 001, 001)), // PGL, Hesychast's, Suwaiyas + (FunEquipSet.Group.FullSet(774, 1), CharacterWeapon.Int(0417, 001, 01), CharacterWeapon.Empty), // MRD, Agoge, Ferocity + (FunEquipSet.Group.FullSet(775, 1), CharacterWeapon.Int(0528, 001, 01), CharacterWeapon.Empty), // LNC, Heavensbound, Gae Assail + (FunEquipSet.Group.FullSet(776, 1), CharacterWeapon.Int(0635, 001, 01), CharacterWeapon.Int(0698, 130, 001)), // ARC, Bihu, Gastraphetes + (FunEquipSet.Group.FullSet(777, 1), CharacterWeapon.Int(0831, 002, 01), CharacterWeapon.Empty), // CNJ, Theophany, Xoanon + (FunEquipSet.Group.FullSet(778, 1), CharacterWeapon.Int(1033, 002, 01), CharacterWeapon.Empty), // THM, Archmage's, Gridarvor + (FunEquipSet.Group.FullSet(791, 1), CharacterWeapon.Int(5004, 001, 16), CharacterWeapon.Int(5041, 001, 016)), // CRP, Millrise + (FunEquipSet.Group.FullSet(792, 1), CharacterWeapon.Int(5103, 001, 01), CharacterWeapon.Int(5141, 001, 017)), // BSM, Forgerise + (FunEquipSet.Group.FullSet(793, 1), CharacterWeapon.Int(5201, 011, 01), CharacterWeapon.Int(5241, 001, 017)), // ARM, Hammerrise + (FunEquipSet.Group.FullSet(794, 1), CharacterWeapon.Int(5301, 011, 01), CharacterWeapon.Int(5341, 001, 001)), // GSM, Gemrise + (FunEquipSet.Group.FullSet(795, 1), CharacterWeapon.Int(5405, 001, 01), CharacterWeapon.Int(5441, 001, 016)), // LTW, Hiderise + (FunEquipSet.Group.FullSet(796, 1), CharacterWeapon.Int(5503, 001, 01), CharacterWeapon.Int(5571, 001, 001)), // WVR, Boltrise + (FunEquipSet.Group.FullSet(797, 1), CharacterWeapon.Int(5603, 008, 01), CharacterWeapon.Int(5641, 001, 017)), // ALC, Cauldronrise + (FunEquipSet.Group.FullSet(798, 1), CharacterWeapon.Int(5701, 012, 01), CharacterWeapon.Int(5741, 001, 017)), // CUL, Galleyrise + (FunEquipSet.Group.FullSet(799, 1), CharacterWeapon.Int(7004, 001, 01), CharacterWeapon.Int(7051, 001, 017)), // MIN, Minerise + (FunEquipSet.Group.FullSet(800, 1), CharacterWeapon.Int(7101, 012, 01), CharacterWeapon.Int(7151, 001, 017)), // BTN, Fieldrise + (FunEquipSet.Group.FullSet(801, 1), CharacterWeapon.Int(7202, 001, 01), CharacterWeapon.Int(7255, 001, 001)), // FSH, Tacklerise + (FunEquipSet.Group.FullSet(772, 1), CharacterWeapon.Int(0220, 002, 01), CharacterWeapon.Int(0114, 002, 001)), // PLD, Caballarius, Clarent, Galahad + (FunEquipSet.Group.FullSet(773, 1), CharacterWeapon.Int(0338, 001, 01), CharacterWeapon.Int(0388, 001, 001)), // MNK, Hesychast's, Suwaiyas + (FunEquipSet.Group.FullSet(774, 1), CharacterWeapon.Int(0417, 001, 01), CharacterWeapon.Empty), // WAR, Agoge, Ferocity + (FunEquipSet.Group.FullSet(775, 1), CharacterWeapon.Int(0528, 001, 01), CharacterWeapon.Empty), // DRG, Heavensbound, Gae Assail + (FunEquipSet.Group.FullSet(776, 1), CharacterWeapon.Int(0635, 001, 01), CharacterWeapon.Int(0698, 130, 001)), // BRD, Bihu, Gastraphetes + (FunEquipSet.Group.FullSet(777, 1), CharacterWeapon.Int(0831, 002, 01), CharacterWeapon.Empty), // WHM, Theophany, Xoanon + (FunEquipSet.Group.FullSet(778, 1), CharacterWeapon.Int(1033, 002, 01), CharacterWeapon.Empty), // BLM, Archmage's, Gridarvor + (FunEquipSet.Group.FullSet(779, 1), CharacterWeapon.Int(1752, 001, 01), CharacterWeapon.Empty), // ACN, Glyphic, The Grand Grimoire + (FunEquipSet.Group.FullSet(779, 1), CharacterWeapon.Int(1752, 001, 01), CharacterWeapon.Empty), // SMN, Glyphic, The Grand Grimoire + (FunEquipSet.Group.FullSet(780, 1), CharacterWeapon.Int(1753, 001, 01), CharacterWeapon.Empty), // SCH, Pedagogy, Eclecticism + (FunEquipSet.Group.FullSet(781, 1), CharacterWeapon.Int(1801, 128, 01), CharacterWeapon.Int(1851, 128, 001)), // ROG, Momochi, Shiranui + (FunEquipSet.Group.FullSet(781, 1), CharacterWeapon.Int(1801, 128, 01), CharacterWeapon.Int(1851, 128, 001)), // NIN, Momochi, Shiranui + (FunEquipSet.Group.FullSet(783, 1), CharacterWeapon.Int(2026, 002, 01), CharacterWeapon.Int(2099, 001, 001)), // MCH, Forerider's, Sthalmann Special + (FunEquipSet.Group.FullSet(782, 1), CharacterWeapon.Int(1519, 002, 01), CharacterWeapon.Empty), // DRK, Fallen's, Maleficus + (FunEquipSet.Group.FullSet(784, 1), CharacterWeapon.Int(2136, 082, 01), CharacterWeapon.Int(2199, 001, 188)), // AST, Ephemerist's, Metis + (FunEquipSet.Group.FullSet(785, 1), CharacterWeapon.Int(2215, 001, 01), CharacterWeapon.Int(2259, 003, 001)), // SAM, Sakonji, Kogarasumaru + (FunEquipSet.Group.FullSet(786, 1), CharacterWeapon.Int(2301, 097, 01), CharacterWeapon.Int(2380, 001, 001)), // RDM, Roseblood, Colada + (FunEquipSet.Group.FullSet(811, 1), CharacterWeapon.Int(2401, 005, 01), CharacterWeapon.Empty), // BLU, Phantasmal, Blue-eyes + (FunEquipSet.Group.FullSet(787, 1), CharacterWeapon.Int(2501, 064, 01), CharacterWeapon.Empty), // GNB, Bastion's, Chastiefol + (FunEquipSet.Group.FullSet(788, 1), CharacterWeapon.Int(2611, 002, 01), CharacterWeapon.Int(2661, 002, 001)), // DNC, Horos, Soma + (FunEquipSet.Group.FullSet(790, 1), CharacterWeapon.Int(2816, 001, 01), CharacterWeapon.Empty), // RPR, Assassin's, Vendetta + (FunEquipSet.Group.FullSet(789, 1), CharacterWeapon.Int(2701, 026, 01), CharacterWeapon.Empty), // SGE, Metanoia, Asklepian + (FunEquipSet.Group.FullSet(840, 1), CharacterWeapon.Int(3101, 001, 01), CharacterWeapon.Int(3151, 001, 001)), // VPR, Viper's, Sargatanas + (FunEquipSet.Group.FullSet(841, 1), CharacterWeapon.Int(2901, 001, 01), CharacterWeapon.Int(2951, 001, 001)), // PCT, Pictomancer's, Angel Brush }; // @formatter:on private (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)? GetGroup(byte level, byte job, Race race, Gender gender, Random rng) { - const int weight50 = 200; - const int weight60 = 500; - const int weight70 = 700; - const int weight80 = 800; - const int weight90 = 900; - const int weight100 = 1000; + const int weight50 = 200; + const int weight60 = 500; + const int weight70 = 700; + const int weight80 = 800; + const int weight90 = 900; + const int weight100 = 1000; + const int weight110 = 1100; if (job >= StarterWeapons.Length) return null; var maxWeight = level switch { - < 50 => weight50, - < 60 => weight60, - < 70 => weight70, - < 80 => weight80, - < 90 => weight90, - _ => weight100, + < 50 => weight50, + < 60 => weight60, + < 70 => weight70, + < 80 => weight80, + < 90 => weight90, + < 100 => weight100, + _ => weight110, }; var weight = rng.Next(0, maxWeight + 1); @@ -332,11 +393,12 @@ public class WorldSets var list = weight switch { - < weight60 => _50Artifact, - < weight70 => _60Artifact, - < weight80 => _70Artifact, - < weight90 => _80Artifact, - _ => _90Artifact, + < weight60 => _50Artifact, + < weight70 => _60Artifact, + < weight80 => _70Artifact, + < weight90 => _80Artifact, + < weight100 => _90Artifact, + _ => _100Artifact, }; Glamourer.Log.Verbose($"Chose weight {weight}/{maxWeight} for World set [Character: {level} {job} {race} {gender}]."); diff --git a/Glamourer/Unlocks/CustomizeUnlockManager.cs b/Glamourer/Unlocks/CustomizeUnlockManager.cs index b63a98e..bd13f99 100644 --- a/Glamourer/Unlocks/CustomizeUnlockManager.cs +++ b/Glamourer/Unlocks/CustomizeUnlockManager.cs @@ -1,23 +1,24 @@ -using Dalamud; +using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Glamourer.GameData; using Glamourer.Events; using Glamourer.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; +using Penumbra.GameData; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Unlocks; public class CustomizeUnlockManager : IDisposable, ISavable { - private readonly SaveService _saveService; - private readonly IClientState _clientState; - private readonly ObjectUnlocked _event; - + private readonly SaveService _saveService; + private readonly IClientState _clientState; + private readonly ObjectUnlocked _event; + private readonly ActorObjectManager _objects; private readonly Dictionary _unlocked = new(); public readonly IReadOnlyDictionary Unlockable; @@ -26,12 +27,13 @@ public class CustomizeUnlockManager : IDisposable, ISavable => _unlocked; public CustomizeUnlockManager(SaveService saveService, CustomizeService customizations, IDataManager gameData, - IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop) + IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop, ActorObjectManager objects) { interop.InitializeFromAttributes(this); _saveService = saveService; _clientState = clientState; _event = @event; + _objects = objects; Unlockable = CreateUnlockableCustomizations(customizations, gameData); Load(); _setUnlockLinkValueHook.Enable(); @@ -93,7 +95,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable /// Scan and update all unlockable customizations for their current game state. public unsafe void Scan() { - if (_clientState.LocalPlayer == null) + if (!_objects.Player.Valid) return; Glamourer.Log.Debug("[UnlockManager] Scanning for new unlocked customizations."); @@ -128,7 +130,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable private delegate void SetUnlockLinkValueDelegate(nint uiState, uint data, byte value); - [Signature("48 83 EC ?? 8B C2 44 8B D2", DetourName = nameof(SetUnlockLinkValueDetour))] + [Signature(Sigs.SetUnlockLinkValue, DetourName = nameof(SetUnlockLinkValueDetour))] private readonly Hook _setUnlockLinkValueHook = null!; private void SetUnlockLinkValueDetour(nint uiState, uint data, byte value) @@ -174,31 +176,31 @@ public class CustomizeUnlockManager : IDisposable, ISavable IDataManager gameData) { var ret = new Dictionary(); - var sheet = gameData.GetExcelSheet(ClientLanguage.English)!; + var sheet = gameData.GetExcelSheet(ClientLanguage.English); foreach (var (clan, gender) in CustomizeManager.AllSets()) { var list = customizations.Manager.GetSet(clan, gender); foreach (var hair in list.HairStyles) { - var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value); + var x = sheet.FirstOrNull(f => f.FeatureID == hair.Value.Value); if (x?.IsPurchasable == true) { - var name = x.FeatureID == 61 + var name = x.Value.FeatureID == 61 ? "Eternal Bond" - : x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty) + : x.Value.HintItem.ValueNullable?.Name.ExtractText().Replace("Modern Aesthetics - ", string.Empty) ?? string.Empty; - ret.TryAdd(hair, (x.Data, name)); + ret.TryAdd(hair, (x.Value.UnlockLink, name)); } } foreach (var paint in list.FacePaints) { - var x = sheet.FirstOrDefault(f => f.FeatureID == paint.Value.Value); + var x = sheet.FirstOrNull(f => f.FeatureID == paint.Value.Value); if (x?.IsPurchasable == true) { - var name = x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty) + var name = x.Value.HintItem.ValueNullable?.Name.ExtractText().Replace("Modern Cosmetics - ", string.Empty) ?? string.Empty; - ret.TryAdd(paint, (x.Data, name)); + ret.TryAdd(paint, (x.Value.UnlockLink, name)); } } } diff --git a/Glamourer/Unlocks/FavoriteManager.cs b/Glamourer/Unlocks/FavoriteManager.cs index 33242c9..01a2507 100644 --- a/Glamourer/Unlocks/FavoriteManager.cs +++ b/Glamourer/Unlocks/FavoriteManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Services; using Newtonsoft.Json; using OtterGui.Classes; @@ -12,7 +12,7 @@ public class FavoriteManager : ISavable private readonly record struct FavoriteHairStyle(Gender Gender, SubRace Race, CustomizeIndex Type, CustomizeValue Id) { public uint ToValue() - => (uint)Id.Value | ((uint)Type << 8) | ((uint)Race << 16) | ((uint)Gender << 24); + => Id.Value | ((uint)Type << 8) | ((uint)Race << 16) | ((uint)Gender << 24); public FavoriteHairStyle(uint value) : this((Gender)((value >> 24) & 0xFF), (SubRace)((value >> 16) & 0xFF), (CustomizeIndex)((value >> 8) & 0xFF), @@ -25,6 +25,7 @@ public class FavoriteManager : ISavable private readonly HashSet _favorites = []; private readonly HashSet _favoriteColors = []; private readonly HashSet _favoriteHairStyles = []; + private readonly HashSet _favoriteBonusItems = []; public FavoriteManager(SaveService saveService) { @@ -60,8 +61,9 @@ public class FavoriteManager : ISavable { case 1: _favorites.UnionWith(load!.FavoriteItems.Select(i => (ItemId)i)); - _favoriteColors.UnionWith(load!.FavoriteColors.Select(i => (StainId)i)); - _favoriteHairStyles.UnionWith(load!.FavoriteHairStyles.Select(t => new FavoriteHairStyle(t))); + _favoriteColors.UnionWith(load.FavoriteColors.Select(i => (StainId)i)); + _favoriteHairStyles.UnionWith(load.FavoriteHairStyles.Select(t => new FavoriteHairStyle(t))); + _favoriteBonusItems.UnionWith(load.FavoriteBonusItems.Select(b => new BonusItemId(b))); break; default: throw new Exception($"Unknown Version {load?.Version ?? 0}"); @@ -92,28 +94,44 @@ public class FavoriteManager : ISavable using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); + j.WritePropertyName(nameof(LoadIntermediary.Version)); j.WriteValue(CurrentVersion); + j.WritePropertyName(nameof(LoadIntermediary.FavoriteItems)); j.WriteStartArray(); foreach (var item in _favorites) j.WriteValue(item.Id); j.WriteEndArray(); + j.WritePropertyName(nameof(LoadIntermediary.FavoriteColors)); j.WriteStartArray(); foreach (var stain in _favoriteColors) j.WriteValue(stain.Id); j.WriteEndArray(); + j.WritePropertyName(nameof(LoadIntermediary.FavoriteHairStyles)); j.WriteStartArray(); foreach (var hairStyle in _favoriteHairStyles) j.WriteValue(hairStyle.ToValue()); j.WriteEndArray(); + + j.WritePropertyName(nameof(LoadIntermediary.FavoriteBonusItems)); + j.WriteStartArray(); + foreach (var item in _favoriteBonusItems) + j.WriteValue(item.Id); + j.WriteEndArray(); + j.WriteEndObject(); } public bool TryAdd(EquipItem item) - => TryAdd(item.ItemId); + { + if (item.Id.IsBonusItem) + return TryAdd(item.Id.BonusItem); + + return TryAdd(item.ItemId); + } public bool TryAdd(ItemId item) { @@ -124,8 +142,14 @@ public class FavoriteManager : ISavable return true; } - public bool TryAdd(Stain stain) - => TryAdd(stain.RowIndex); + public bool TryAdd(BonusItemId item) + { + if (item.Id == 0 || !_favoriteBonusItems.Add(item)) + return false; + + Save(); + return true; + } public bool TryAdd(StainId stain) { @@ -146,7 +170,11 @@ public class FavoriteManager : ISavable } public bool Remove(EquipItem item) - => Remove(item.ItemId); + { + if (item.Id.IsBonusItem) + Remove(item.Id.BonusItem); + return Remove(item.ItemId); + } public bool Remove(ItemId item) { @@ -157,8 +185,14 @@ public class FavoriteManager : ISavable return true; } - public bool Remove(Stain stain) - => Remove(stain.RowIndex); + public bool Remove(BonusItemId item) + { + if (!_favoriteBonusItems.Remove(item)) + return false; + + Save(); + return true; + } public bool Remove(StainId stain) { @@ -179,25 +213,31 @@ public class FavoriteManager : ISavable } public bool Contains(EquipItem item) - => _favorites.Contains(item.ItemId); + { + if (item.Id.IsBonusItem) + return _favoriteBonusItems.Contains(item.Id.BonusItem); - public bool Contains(Stain stain) - => _favoriteColors.Contains(stain.RowIndex); - - public bool Contains(ItemId item) - => _favorites.Contains(item); + return _favorites.Contains(item.ItemId); + } public bool Contains(StainId stain) => _favoriteColors.Contains(stain); + public bool Contains(ItemId itemId) + => _favorites.Contains(itemId); + + public bool Contains(BonusItemId bonusItemId) + => _favoriteBonusItems.Contains(bonusItemId); + public bool Contains(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) => _favoriteHairStyles.Contains(new FavoriteHairStyle(gender, race, type, value)); private class LoadIntermediary { - public int Version = CurrentVersion; - public uint[] FavoriteItems = []; - public byte[] FavoriteColors = []; - public uint[] FavoriteHairStyles = []; + public int Version = CurrentVersion; + public uint[] FavoriteItems = []; + public byte[] FavoriteColors = []; + public uint[] FavoriteHairStyles = []; + public ushort[] FavoriteBonusItems = []; } } diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index de35335..6708267 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -3,11 +3,11 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Glamourer.Events; using Glamourer.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet; +using Cabinet = Lumina.Excel.Sheets.Cabinet; namespace Glamourer.Unlocks; @@ -144,8 +144,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary(mirageManager->PrismBoxItemIds, 800); + var span = mirageManager->PrismBoxItemIds; foreach (var item in span) changes |= AddItem(item, time); } @@ -154,10 +153,9 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionaryGlamourPlatesSpan) + foreach (var plate in mirageManager->GlamourPlates) { - // TODO: Make independent from hardcoded value - var span = new ReadOnlySpan(plate.ItemIds, 12); + var span = plate.ItemIds; foreach (var item in span) changes |= AddItem(item, time); } @@ -170,14 +168,14 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionaryGetInventoryContainer(type); - if (container != null && container->Loaded != 0 && _currentInventoryIndex < container->Size) + if (container != null && container->IsLoaded && _currentInventoryIndex < container->Size) { Glamourer.Log.Excessive($"[UnlockScanner] Scanning {_currentInventory} {type} {_currentInventoryIndex}/{container->Size}."); var item = container->GetInventorySlot(_currentInventoryIndex++); if (item != null) { - changes |= AddItem(item->ItemID, time); - changes |= AddItem(item->GlamourID, time); + changes |= AddItem(item->ItemId, time); + changes |= AddItem(item->GlamourId, time); } } else @@ -194,7 +192,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary= _items.ItemSheet.RowCount) + if (itemId.Id >= (uint) _items.ItemSheet.Count) { time = DateTimeOffset.MinValue; return true; @@ -275,32 +273,31 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary CreateUnlockData(IDataManager gameData, ItemManager items) { var ret = new Dictionary(); - var cabinet = gameData.GetExcelSheet()!; + var cabinet = gameData.GetExcelSheet(); foreach (var row in cabinet) { - if (items.ItemData.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (items.ItemData.TryGetValue(row.Item.RowId, EquipSlot.MainHand, out var item)) ret.TryAdd(item.ItemId, new UnlockRequirements(row.RowId, 0, 0, 0, UnlockType.Cabinet)); } - var gilShopItem = gameData.GetExcelSheet()!; - var gilShop = gameData.GetExcelSheet()!; - foreach (var row in gilShopItem) + var gilShopItem = gameData.GetSubrowExcelSheet(); + var gilShop = gameData.GetExcelSheet(); + foreach (var row in gilShopItem.SelectMany(g => g)) { - if (!items.ItemData.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (!items.ItemData.TryGetValue(row.Item.RowId, EquipSlot.MainHand, out var item)) continue; - var quest1 = row.QuestRequired[0].Row; - var quest2 = row.QuestRequired[1].Row; - var achievement = row.AchievementRequired.Row; + var quest1 = row.QuestRequired[0].RowId; + var quest2 = row.QuestRequired[1].RowId; + var achievement = row.AchievementRequired.RowId; var state = row.StateRequired; - var shop = gilShop.GetRow(row.RowId); - if (shop != null && shop.Quest.Row != 0) + if (gilShop.TryGetRow(row.RowId, out var shop) && shop.Quest.RowId != 0) { if (quest1 == 0) - quest1 = shop.Quest.Row; + quest1 = shop.Quest.RowId; else if (quest2 == 0) - quest2 = shop.Quest.Row; + quest2 = shop.Quest.RowId; } var type = (quest1 != 0 ? UnlockType.Quest1 : 0) diff --git a/Glamourer/Unlocks/UnlockDictionaryHelpers.cs b/Glamourer/Unlocks/UnlockDictionaryHelpers.cs index 28f5793..edc9472 100644 --- a/Glamourer/Unlocks/UnlockDictionaryHelpers.cs +++ b/Glamourer/Unlocks/UnlockDictionaryHelpers.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; namespace Glamourer.Unlocks; diff --git a/Glamourer/packages.lock.json b/Glamourer/packages.lock.json new file mode 100644 index 0000000..b15c917 --- /dev/null +++ b/Glamourer/packages.lock.json @@ -0,0 +1,120 @@ +{ + "version": 1, + "dependencies": { + "net10.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[14.0.1, )", + "resolved": "14.0.1", + "contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" + }, + "Vortice.Direct3D11": { + "type": "Direct", + "requested": "[3.4.2-beta, )", + "resolved": "3.4.2-beta", + "contentHash": "CWVMTF7ebylzzXbQXVp5C9UpBB/L+EpX2OxSdb2wlzcsdEmrev/Ith8wVs0WjZ6DbA0WiiybnYAWqB5v0nOO/A==", + "dependencies": { + "SharpGen.Runtime": "2.1.2-beta", + "Vortice.DXGI": "3.4.2-beta" + } + }, + "FlatSharp.Compiler": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A==" + }, + "FlatSharp.Runtime": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" + }, + "SharpGen.Runtime": { + "type": "Transitive", + "resolved": "2.1.2-beta", + "contentHash": "nqZAjfEG1jX1ivvdZLsi6Pkt0DiOJyuOgRgldNFsmjXFPhxUbXQibofLSwuDZidL2kkmtTF8qLoRIeqeVdXgYw==" + }, + "SharpGen.Runtime.COM": { + "type": "Transitive", + "resolved": "2.1.2-beta", + "contentHash": "HBCrb6HfnUWx9v5/GjJeBr5DuodZLnHlFQQYXPrQs1Hbe1c6Wd0uCXf+SJp4hW8fQNxjXEu0FgiyHGlA/SRzRw==", + "dependencies": { + "SharpGen.Runtime": "2.1.2-beta" + } + }, + "Vortice.DirectX": { + "type": "Transitive", + "resolved": "3.4.2-beta", + "contentHash": "EwDbemXkmEiDGZVDem25uiEcZBYOMb+wzePuta+M/k2LXrQVGPknZhZUK56+QlHhI+Ducf/d+J75wgBzEjKi2g==", + "dependencies": { + "SharpGen.Runtime": "2.1.2-beta", + "SharpGen.Runtime.COM": "2.1.2-beta", + "Vortice.Mathematics": "1.7.6" + } + }, + "Vortice.DXGI": { + "type": "Transitive", + "resolved": "3.4.2-beta", + "contentHash": "T4S3pp6l/SGJ6SH3ebCbodN/bimGOkIBiIYKeBpVEis7+/ac1XIjyzgSTJ5XsH3o3hSH7DqSbP6Yo6mL9nyFQA==", + "dependencies": { + "SharpGen.Runtime": "2.1.2-beta", + "Vortice.DirectX": "3.4.2-beta" + } + }, + "Vortice.Mathematics": { + "type": "Transitive", + "resolved": "1.7.6", + "contentHash": "W8FNv850lPGxmHphwLyi1qnUlQHZBxh/62EenFJTaY6acPP29Fk0xMQJI60G+YNlsVJb3fSoriuW+ong5sM5UQ==" + }, + "glamourer.api": { + "type": "Project" + }, + "ottergui": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2024.3.0, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" + } + }, + "penumbra.api": { + "type": "Project" + }, + "penumbra.gamedata": { + "type": "Project", + "dependencies": { + "FlatSharp.Compiler": "[7.9.0, )", + "FlatSharp.Runtime": "[7.9.0, )", + "OtterGui": "[1.0.0, )", + "Penumbra.Api": "[5.13.0, )", + "Penumbra.String": "[1.0.7, )" + } + }, + "penumbra.string": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/OtterGui b/OtterGui index 0b5afff..ff1e654 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0b5afffda19d3e16aec9e8682d18c8f11f67f1c6 +Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf diff --git a/Penumbra.Api b/Penumbra.Api index f1e4e52..52a3216 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f1e4e520daaa8f23e5c8b71d55e5992b8f6768e2 +Subproject commit 52a3216a525592205198303df2844435e382cf87 diff --git a/Penumbra.GameData b/Penumbra.GameData index fed687b..0e973ed 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fed687b536b7c709484db251b690b8821c5ef403 +Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60 diff --git a/Penumbra.String b/Penumbra.String index caa58c5..9bd016f 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488 +Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592 diff --git a/repo.json b/repo.json index 7b730f3..31b4c6e 100644 --- a/repo.json +++ b/repo.json @@ -17,18 +17,19 @@ "Character" ], "InternalName": "Glamourer", - "AssemblyVersion": "1.2.3.0", - "TestingAssemblyVersion": "1.2.3.0", + "AssemblyVersion": "1.5.1.7", + "TestingAssemblyVersion": "1.5.1.7", "RepoUrl": "https://github.com/Ottermandias/Glamourer", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 14, + "TestingDalamudApiLevel": 14, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 1, "LastUpdate": 1618608322, - "DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/releases/download/1.2.3.0/Glamourer.zip", - "DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/releases/download/1.2.3.0/Glamourer.zip", - "DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/releases/download/1.2.3.0/Glamourer.zip", + "DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.7/Glamourer.zip", + "DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.7/Glamourer.zip", + "DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.7/Glamourer.zip", "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/main/images/icon.png" } ]