diff --git a/OtterGui b/OtterGui
index adce3030..d43be328 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit adce3030c9dc125f2ebbaefbef6c756977c047c3
+Subproject commit d43be3287a4782be091635e81ef2ec64849ba462
diff --git a/Penumbra.GameData/Data/HumanModelList.cs b/Penumbra.GameData/Data/HumanModelList.cs
new file mode 100644
index 00000000..d4177e51
--- /dev/null
+++ b/Penumbra.GameData/Data/HumanModelList.cs
@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Linq;
+using Dalamud;
+using Dalamud.Data;
+using Dalamud.Plugin;
+using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
+using Lumina.Excel.GeneratedSheets;
+
+namespace Penumbra.GameData.Data;
+
+public sealed class HumanModelList : DataSharer
+{
+ public const string Tag = "HumanModels";
+ public const int CurrentVersion = 1;
+
+ private readonly BitArray _humanModels;
+
+ public HumanModelList(DalamudPluginInterface pluginInterface, DataManager gameData)
+ : base(pluginInterface, ClientLanguage.English, CurrentVersion)
+ {
+ _humanModels = TryCatchData(Tag, () => GetValidHumanModels(gameData));
+ }
+
+ public bool IsHuman(uint modelId)
+ => modelId < _humanModels.Count && _humanModels[(int)modelId];
+
+ protected override void DisposeInternal()
+ {
+ DisposeTag(Tag);
+ }
+
+ ///
+ /// Go through all ModelChara rows and return a bitfield of those that resolve to human models.
+ ///
+ private static BitArray GetValidHumanModels(DataManager gameData)
+ {
+ var sheet = gameData.GetExcelSheet()!;
+ var ret = new BitArray((int)sheet.RowCount, false);
+ foreach (var (_, idx) in sheet.Select((m, i) => (m, i)).Where(p => p.m.Type == (byte)CharacterBase.ModelType.Human))
+ ret[idx] = true;
+
+ return ret;
+ }
+}
diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs
index d670fc42..c719891e 100644
--- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs
+++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json.Linq;
+using OtterGui.Custom;
using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.String;
diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs
index a3005f07..91ab49c3 100644
--- a/Penumbra/Collections/Manager/IndividualCollections.cs
+++ b/Penumbra/Collections/Manager/IndividualCollections.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
+using OtterGui.Custom;
using OtterGui.Filesystem;
using Penumbra.GameData.Actors;
using Penumbra.Services;
diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs
index c5b797fd..8220c629 100644
--- a/Penumbra/Interop/PathResolving/CollectionResolver.cs
+++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs
@@ -1,15 +1,11 @@
using System;
-using System.Collections;
-using System.Linq;
-using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
-using Lumina.Excel.GeneratedSheets;
-using OtterGui;
using Penumbra.Collections;
-using Penumbra.Collections.Manager;
+using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
+using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.Services;
using Penumbra.Util;
@@ -23,7 +19,7 @@ public unsafe class CollectionResolver
{
private readonly PerformanceTracker _performance;
private readonly IdentifiedCollectionCache _cache;
- private readonly BitArray _validHumanModels;
+ private readonly HumanModelList _humanModels;
private readonly ClientState _clientState;
private readonly GameGui _gameGui;
@@ -36,8 +32,8 @@ public unsafe class CollectionResolver
private readonly DrawObjectState _drawObjectState;
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
- DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
- TempCollectionManager tempCollections, DrawObjectState drawObjectState)
+ ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
+ TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels)
{
_performance = performance;
_cache = cache;
@@ -49,7 +45,7 @@ public unsafe class CollectionResolver
_collectionManager = collectionManager;
_tempCollections = tempCollections;
_drawObjectState = drawObjectState;
- _validHumanModels = GetValidHumanModels(gameData);
+ _humanModels = humanModels;
}
///
@@ -115,7 +111,7 @@ public unsafe class CollectionResolver
/// Return whether the given ModelChara id refers to a human-type model.
public bool IsModelHuman(uint modelCharaId)
- => modelCharaId < _validHumanModels.Length && _validHumanModels[(int)modelCharaId];
+ => _humanModels.IsHuman(modelCharaId);
/// Return whether the given character has a human model.
public bool IsModelHuman(Character* character)
@@ -254,17 +250,4 @@ public unsafe class CollectionResolver
return CheckYourself(id, owner)
?? CollectionByAttributes(owner, ref notYetReady);
}
-
- ///
- /// Go through all ModelChara rows and return a bitfield of those that resolve to human models.
- ///
- private static BitArray GetValidHumanModels(DataManager gameData)
- {
- var sheet = gameData.GetExcelSheet()!;
- var ret = new BitArray((int)sheet.RowCount, false);
- foreach (var (_, idx) in sheet.WithIndex().Where(p => p.Value.Type == (byte)CharacterBase.ModelType.Human))
- ret[idx] = true;
-
- return ret;
- }
}
diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs
index 2408bf67..9189327c 100644
--- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs
@@ -4,7 +4,7 @@ using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
-using Penumbra.GameData;
+using OtterGui.Custom;
using Penumbra.GameData.Actors;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs
index 79554797..8e6e729c 100644
--- a/Penumbra/Mods/Manager/ModFileSystem.cs
+++ b/Penumbra/Mods/Manager/ModFileSystem.cs
@@ -83,12 +83,12 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable
// Update sort order when defaulted mod names change.
private void OnDataChange(ModDataChangeType type, Mod mod, string? oldName)
{
- if (type.HasFlag(ModDataChangeType.Name) && oldName != null)
- {
- var old = oldName.FixName();
- if (Find(old, out var child) && child is not Folder)
- Rename(child, mod.Name.Text);
- }
+ if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf))
+ return;
+
+ var old = oldName.FixName();
+ if (old == leaf.Name || leaf.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
+ RenameWithDuplicates(leaf, mod.Name.Text);
}
// Update the filesystem if a mod has been added or removed.
@@ -98,13 +98,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable
switch (type)
{
case ModPathChangeType.Added:
- var originalName = mod.Name.Text.FixName();
- var name = originalName;
- var counter = 1;
- while (Find(name, out _))
- name = $"{originalName} ({++counter})";
-
- CreateLeaf(Root, name, mod);
+ CreateDuplicateLeaf(Root, mod.Name.Text, mod);
break;
case ModPathChangeType.Deleted:
if (FindLeaf(mod, out var leaf))
diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs
index 1ab1f313..782d40a0 100644
--- a/Penumbra/Services/ServiceManager.cs
+++ b/Penumbra/Services/ServiceManager.cs
@@ -1,4 +1,3 @@
-using System.Collections.Concurrent;
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
@@ -18,7 +17,7 @@ using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.UI;
using Penumbra.UI.AdvancedWindow;
-using Penumbra.UI.Classes;
+using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
using Penumbra.UI.Tabs;
@@ -70,7 +69,8 @@ public static class ServiceManager
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton();
private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton()
diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs
index 81e0a862..376b7ad8 100644
--- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs
+++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Enums;
using ImGuiNET;
-using OtterGui.Raii;
+using OtterGui.Custom;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@@ -63,22 +63,8 @@ public class IndividualAssignmentUi : IDisposable
public void DrawObjectKindCombo(float width)
{
- if (!_ready)
- return;
-
- ImGui.SetNextItemWidth(width);
- using var combo = ImRaii.Combo("##newKind", _newKind.ToName());
- if (!combo)
- return;
-
- foreach (var kind in ObjectKinds)
- {
- if (!ImGui.Selectable(kind.ToName(), _newKind == kind))
- continue;
-
- _newKind = kind;
+ if (_ready && IndividualHelpers.DrawObjectKindCombo(width, _newKind, out _newKind, ObjectKinds))
UpdateIdentifiersInternal();
- }
}
public void DrawNewPlayerCollection(float width)
diff --git a/Penumbra/UI/CollectionTab/NpcCombo.cs b/Penumbra/UI/CollectionTab/NpcCombo.cs
deleted file mode 100644
index 7095a78b..00000000
--- a/Penumbra/UI/CollectionTab/NpcCombo.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Dalamud.Utility;
-using ImGuiNET;
-using OtterGui.Widgets;
-
-namespace Penumbra.UI.CollectionTab;
-
-public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)>
-{
- private readonly string _label;
-
- public NpcCombo(string label, IReadOnlyDictionary names)
- : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key, Comparer)
- .ToList())
- => _label = label;
-
- protected override string ToString((string Name, uint[] Ids) obj)
- => obj.Name;
-
- protected override bool DrawSelectable(int globalIdx, bool selected)
- {
- var (name, ids) = Items[globalIdx];
- var ret = ImGui.Selectable(name, selected);
- if (ImGui.IsItemHovered())
- ImGui.SetTooltip(string.Join('\n', ids.Select(i => i.ToString())));
-
- return ret;
- }
-
- public bool Draw(float width)
- => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
-
-
- /// Compare strings in a way that letters and numbers are sorted before any special symbols.
- private class NameComparer : IComparer
- {
- public int Compare(string? x, string? y)
- {
- if (x.IsNullOrEmpty() || y.IsNullOrEmpty())
- return StringComparer.OrdinalIgnoreCase.Compare(x, y);
-
- return (char.IsAsciiLetterOrDigit(x[0]), char.IsAsciiLetterOrDigit(y[0])) switch
- {
- (true, false) => -1,
- (false, true) => 1,
- _ => StringComparer.OrdinalIgnoreCase.Compare(x, y),
- };
- }
- }
-
- private static readonly NameComparer Comparer = new();
-}
diff --git a/Penumbra/UI/CollectionTab/WorldCombo.cs b/Penumbra/UI/CollectionTab/WorldCombo.cs
deleted file mode 100644
index 5441dbaa..00000000
--- a/Penumbra/UI/CollectionTab/WorldCombo.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using ImGuiNET;
-using OtterGui.Widgets;
-
-namespace Penumbra.UI.CollectionTab;
-
-public sealed class WorldCombo : FilterComboCache>
-{
- private static readonly KeyValuePair AllWorldPair = new(ushort.MaxValue, "Any World");
-
- public WorldCombo(IReadOnlyDictionary worlds)
- : base(worlds.OrderBy(kvp => kvp.Value).Prepend(AllWorldPair))
- {
- CurrentSelection = AllWorldPair;
- CurrentSelectionIdx = 0;
- }
-
- protected override string ToString(KeyValuePair obj)
- => obj.Value;
-
- public bool Draw(float width)
- => Draw("##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
-}