Finish CollectionTab rework.

This commit is contained in:
Ottermandias 2023-04-21 18:42:54 +02:00
parent 25cb46525a
commit 9c4f7b7562
22 changed files with 1350 additions and 1543 deletions

@ -1 +1 @@
Subproject commit 51c350b5f129b53afda3a51b057c228e152a6b88
Subproject commit ee7815a4f4c91ec04a240c9e52733f2f5ffa15d0

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Newtonsoft.Json.Linq;
@ -65,6 +66,17 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
public bool IsValid
=> Type is not (IdentifierType.UnkObject or IdentifierType.Invalid);
public string Incognito(string? name)
{
name ??= ToString();
if (Type is not (IdentifierType.Player or IdentifierType.Owned))
return name;
var parts = name.Split(' ', 3);
return string.Join(" ",
parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2]));
}
public override string ToString()
=> Manager?.ToString(this)
?? Type switch

View file

@ -113,7 +113,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return false;
}
var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
var newCollection = duplicate?.Duplicate(name, _collections.Count)
?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
@ -195,6 +196,13 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.UnusedSettings).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary>
/// Check if a name is valid to use for a collection.
/// Does not check for uniqueness.
@ -214,7 +222,8 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var file in _saveService.FileNames.CollectionFiles)
{
if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance) || !IsValidName(name))
if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)
|| !IsValidName(name))
continue;
if (ByName(name, out _))
@ -224,7 +233,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
continue;
}
var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings);
var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning",
@ -305,4 +314,4 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
}
}
}

View file

@ -430,150 +430,13 @@ public static class CollectionTypeExtensions
public static string ToDescription(this CollectionType collectionType)
=> collectionType switch
{
CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerChild =>
"This collection applies to all non-player characters with a child body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerElderly =>
"This collection applies to all non-player characters with an elderly body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.MalePlayerCharacter =>
"This collection applies to all male player characters that do not have a more specific character or racial collections associated.",
CollectionType.MaleNonPlayerCharacter =>
"This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.MaleMidlander =>
"This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlander =>
"This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwood =>
"This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwight =>
"This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolk =>
"This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolk =>
"This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSun =>
"This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoon =>
"This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolf =>
"This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguard =>
"This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaen =>
"This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaela =>
"This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelion =>
"This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLost =>
"This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRava =>
"This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeena =>
"This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.MaleMidlanderNpc =>
"This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlanderNpc =>
"This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwoodNpc =>
"This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwightNpc =>
"This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolkNpc =>
"This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolkNpc =>
"This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSunNpc =>
"This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoonNpc =>
"This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolfNpc =>
"This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguardNpc =>
"This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaenNpc =>
"This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaelaNpc =>
"This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelionNpc =>
"This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLostNpc =>
"This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRavaNpc =>
"This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeenaNpc =>
"This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemalePlayerCharacter =>
"This collection applies to all female player characters that do not have a more specific character or racial collections associated.",
CollectionType.FemaleNonPlayerCharacter =>
"This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.FemaleMidlander =>
"This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlander =>
"This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwood =>
"This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwight =>
"This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolk =>
"This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolk =>
"This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSun =>
"This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoon =>
"This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolf =>
"This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguard =>
"This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaen =>
"This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaela =>
"This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelion =>
"This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLost =>
"This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRava =>
"This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeena =>
"This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemaleMidlanderNpc =>
"This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlanderNpc =>
"This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwoodNpc =>
"This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwightNpc =>
"This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolkNpc =>
"This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolkNpc =>
"This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSunNpc =>
"This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoonNpc =>
"This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolfNpc =>
"This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguardNpc =>
"This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaenNpc =>
"This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaelaNpc =>
"This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelionNpc =>
"This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLostNpc =>
"This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRavaNpc =>
"This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeenaNpc =>
"This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.Default => "World, Music, Furniture, baseline for characters and monsters not specialized.",
CollectionType.Interface => "User Interface, Icons, Maps, Styles.",
CollectionType.Yourself => "Your characters, regardless of name, race or gender. Applies in the login screen.",
CollectionType.MalePlayerCharacter => "Baseline for male player characters.",
CollectionType.FemalePlayerCharacter => "Baseline for female player characters.",
CollectionType.MaleNonPlayerCharacter => "Baseline for humanoid male non-player characters.",
CollectionType.FemaleNonPlayerCharacter => "Baseline for humanoid female non-player characters.",
_ => string.Empty,
};
}

View file

@ -11,10 +11,11 @@ using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using Penumbra.Util;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
@ -72,16 +73,17 @@ public class Configuration : IPluginConfiguration, ISavable
[JsonProperty(Order = int.MaxValue)]
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
public bool ScaleModSelector { get; set; } = false;
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2;
public string DefaultImportFolder { get; set; } = string.Empty;
public string QuickMoveFolder1 { get; set; } = string.Empty;
public string QuickMoveFolder2 { get; set; } = string.Empty;
public string QuickMoveFolder3 { get; set; } = string.Empty;
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public bool ScaleModSelector { get; set; } = false;
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2;
public string DefaultImportFolder { get; set; } = string.Empty;
public string QuickMoveFolder1 { get; set; } = string.Empty;
public string QuickMoveFolder2 { get; set; } = string.Empty;
public string QuickMoveFolder3 { get; set; } = string.Empty;
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool FixMainWindow { get; set; } = false;

View file

@ -223,18 +223,6 @@ public class Penumbra : IDalamudPlugin
sb.Append(
$"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n");
string CharacterName(ActorIdentifier id, string name)
{
if (id.Type is IdentifierType.Player or IdentifierType.Owned)
{
var parts = name.Split(' ', 3);
return string.Join(" ",
parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2]));
}
return name + ':';
}
void PrintCollection(ModCollection c, CollectionCache _)
=> sb.Append($"**Collection {c.AnonymizedName}**\n"
+ $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n"
@ -256,7 +244,7 @@ public class Penumbra : IDalamudPlugin
}
foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments)
sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n");
sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n");
foreach (var (collection, cache) in CollectionManager.Caches.Active)
PrintCollection(collection, cache);

View file

@ -17,7 +17,11 @@ public enum ColorId
FolderLine,
ItemId,
IncreasedMetaValue,
DecreasedMetaValue,
DecreasedMetaValue,
SelectedCollection,
RedundantAssignment,
NoModsAssignment,
NoAssignment,
}
public static class Colors
@ -27,8 +31,6 @@ public static class Colors
public const uint MetaInfoText = 0xAAFFFFFF;
public const uint RedTableBgTint = 0x40000080;
public const uint DiscordColor = 0xFFDA8972;
public const uint SelectedColor = 0x6069C056;
public const uint RedundantColor = 0x6050D0D0;
public const uint FilterActive = 0x807070FF;
public const uint TutorialMarker = 0xFF20FFFF;
public const uint TutorialBorder = 0xD00000FF;
@ -40,20 +42,24 @@ public static class Colors
=> color switch
{
// @formatter:off
ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ),
ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ),
ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ),
ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ),
ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."),
ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ),
ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ),
ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ),
ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ),
ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ),
ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ),
ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ),
ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."),
ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."),
ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ),
ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ),
ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ),
ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ),
ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."),
ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ),
ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ),
ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ),
ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ),
ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ),
ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ),
ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ),
ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."),
ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."),
ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."),
ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."),
ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."),
ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."),
_ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ),
// @formatter:on
};

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -11,23 +12,26 @@ namespace Penumbra.UI.CollectionTab;
public sealed class CollectionCombo : FilterComboCache<ModCollection>
{
private readonly CollectionManager _collectionManager;
private readonly ImRaii.Color _color = new();
public CollectionCombo(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items)
: base(items)
=> _collectionManager = manager;
public void Draw(string label, float width, int individualIdx)
protected override void DrawFilter(int currentSelected, float width)
{
var (_, collection) = _collectionManager.Active.Individuals[individualIdx];
if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null)
_collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx);
_color.Dispose();
base.DrawFilter(currentSelected, width);
}
public void Draw(string label, float width, CollectionType type)
public void Draw(string label, float width, uint color)
{
var current = _collectionManager.Active.ByType(type, ActorIdentifier.Invalid);
var current = _collectionManager.Active.ByType(CollectionType.Current, ActorIdentifier.Invalid);
_color.Push(ImGuiCol.FrameBg, color).Push(ImGuiCol.FrameBgHovered, color);
if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null)
_collectionManager.Active.SetCollection(CurrentSelection, type);
_collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Current);
_color.Dispose();
}
protected override string ToString(ModCollection obj)

View file

@ -0,0 +1,671 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab;
public sealed class CollectionPanel : IDisposable
{
private readonly Configuration _config;
private readonly CollectionStorage _collections;
private readonly ActiveCollections _active;
private readonly CollectionSelector _selector;
private readonly ActorService _actors;
private readonly TargetManager _targets;
private readonly IndividualAssignmentUi _individualAssignmentUi;
private readonly InheritanceUi _inheritanceUi;
private readonly ModStorage _mods;
private readonly GameFontHandle _nameFont;
private static readonly IReadOnlyDictionary<CollectionType, (string Name, uint Border)> Buttons = CreateButtons();
private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree();
private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new();
public CollectionPanel(DalamudPluginInterface pi, Configuration config, CommunicatorService communicator, CollectionManager manager,
CollectionSelector selector, ActorService actors, TargetManager targets, ModStorage mods)
{
_config = config;
_collections = manager.Storage;
_active = manager.Active;
_selector = selector;
_actors = actors;
_targets = targets;
_mods = mods;
_individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager);
_inheritanceUi = new InheritanceUi(_config, manager, _selector);
_nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23));
}
public void Dispose()
{
_individualAssignmentUi.Dispose();
_nameFont.Dispose();
}
/// <summary> Draw the panel containing beginners information and simple assignments. </summary>
public void DrawSimple()
{
ImGuiUtil.TextWrapped("A collection is a set of mod configurations. You can have as many collections as you desire.\n"
+ "The collection you are currently editing in the mod tab can be selected here and is highlighted.\n");
ImGuiUtil.TextWrapped(
"There are functions you can assign these collections to, so different mod configurations apply for different things.\n"
+ "You can assign an existing collection to such a function by clicking the function or dragging the collection over.");
ImGui.Separator();
var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
DrawSimpleCollectionButton(CollectionType.Default, buttonWidth);
DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth);
DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth);
DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth);
ImGuiUtil.DrawColoredText(("Individual ", ColorId.NewMod.Value(_config)),
("Assignments take precedence before anything else and only apply to one specific character or monster.", 0));
ImGui.Dummy(Vector2.UnitX);
var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale };
DrawCurrentCharacter(specialWidth);
ImGui.SameLine();
DrawCurrentTarget(specialWidth);
DrawIndividualCollections(buttonWidth);
var first = true;
void Button(CollectionType type)
{
var (name, border) = Buttons[type];
var collection = _active.ByType(type);
if (collection == null)
return;
if (first)
{
ImGui.Separator();
ImGui.TextUnformatted("Currently Active Advanced Assignments");
first = false;
}
DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, 's', collection);
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X)
ImGui.NewLine();
}
Button(CollectionType.NonPlayerChild);
Button(CollectionType.NonPlayerElderly);
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false));
Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false));
Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true));
Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true));
}
}
/// <summary> Draw the panel containing new and existing individual assignments. </summary>
public void DrawIndividualPanel()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
var width = new Vector2(300 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
ImGui.Dummy(Vector2.One);
DrawCurrentCharacter(width);
ImGui.SameLine();
DrawCurrentTarget(width);
ImGui.Separator();
ImGui.Dummy(Vector2.One);
style.Pop();
_individualAssignmentUi.DrawWorldCombo(width.X / 2);
ImGui.SameLine();
_individualAssignmentUi.DrawNewPlayerCollection(width.X);
_individualAssignmentUi.DrawObjectKindCombo(width.X / 2);
ImGui.SameLine();
_individualAssignmentUi.DrawNewNpcCollection(width.X);
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned.");
ImGui.Dummy(Vector2.One);
ImGui.Separator();
style.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
DrawNewPlayer(width);
ImGui.SameLine();
ImGuiUtil.TextWrapped("Also check General Settings for UI characters and inheritance through ownership.");
ImGui.Separator();
DrawNewRetainer(width);
ImGui.SameLine();
ImGuiUtil.TextWrapped("Bell Retainers apply to Mannequins, but not to outdoor retainers, since those only carry their owners name.");
ImGui.Separator();
DrawNewNpc(width);
ImGui.SameLine();
ImGuiUtil.TextWrapped("Some NPCs are available as Battle - and Event NPCs and need to be setup for both if desired.");
ImGui.Separator();
DrawNewOwned(width);
ImGui.SameLine();
ImGuiUtil.TextWrapped("Owned NPCs take precedence before unowned NPCs of the same type.");
ImGui.Separator();
DrawIndividualCollections(width with { X = 200 * ImGuiHelpers.GlobalScale });
}
/// <summary> Draw the panel containing all special group assignments. </summary>
public void DrawGroupPanel()
{
ImGui.Dummy(Vector2.One);
using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
var dummy = new Vector2(1, 0);
foreach (var (type, pre, post, name, border) in AdvancedTree)
{
ImGui.TableNextColumn();
if (type is CollectionType.Inactive)
continue;
if (pre)
ImGui.Dummy(dummy);
DrawAssignmentButton(type, buttonWidth, name, border);
if (post)
ImGui.Dummy(dummy);
}
}
/// <summary> Draw the collection detail panel with inheritance, visible mod settings and statistics. </summary>
public void DrawDetailsPanel()
{
var collection = _active.Current;
DrawCollectionName(collection);
DrawStatistics(collection);
_inheritanceUi.Draw();
ImGui.Separator();
DrawInactiveSettingsList(collection);
DrawSettingsList(collection);
}
private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix)
{
var label = $"{type}{identifier}{suffix}";
if (open)
ImGui.OpenPopup(label);
using var context = ImRaii.Popup(label);
if (!context)
return;
using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor))
{
if (ImGui.MenuItem("Use no mods."))
_active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier));
}
if (collection != null)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
if (ImGui.MenuItem("Remove this assignment."))
_active.SetCollection(null, type, _active.Individuals.GetGroup(identifier));
}
foreach (var coll in _collections)
{
if (coll != collection && ImGui.MenuItem($"Use {coll.Name}."))
_active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier));
}
}
private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, char suffix,
ModCollection? collection = null)
{
using var group = ImRaii.Group();
var invalid = type == CollectionType.Individual && !id.IsValid;
var redundancy = _active.RedundancyCheck(type, id);
collection ??= _active.ByType(type, id);
using var color = ImRaii.PushColor(ImGuiCol.Button,
collection == null
? ColorId.NoAssignment.Value(_config)
: redundancy.Length > 0
? ColorId.RedundantAssignment.Value(_config)
: collection == _active.Current
? ColorId.SelectedCollection.Value(_config)
: collection == ModCollection.Empty
? ColorId.NoModsAssignment.Value(_config)
: ImGui.GetColorU32(ImGuiCol.Button), !invalid)
.Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor);
using var disabled = ImRaii.Disabled(invalid);
var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right);
var hovered = redundancy.Length > 0 && ImGui.IsItemHovered();
if (!invalid)
{
_selector.DragTargetAssignment(type, id);
var name = Name(collection);
var size = ImGui.CalcTextSize(name);
var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding;
ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name);
DrawContext(button, collection, type, id, suffix);
}
if (hovered)
ImGui.SetTooltip(redundancy);
return button;
}
private void DrawSimpleCollectionButton(CollectionType type, Vector2 width)
{
DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid, 's');
ImGui.SameLine();
using (var group = ImRaii.Group())
{
ImGuiUtil.TextWrapped(type.ToDescription());
switch (type)
{
case CollectionType.Default:
ImGui.TextUnformatted("Overruled by any other Assignment.");
break;
case CollectionType.Yourself:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0));
break;
case CollectionType.MalePlayerCharacter:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial Player", Colors.DiscordColor), (", ", 0),
("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0),
("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0));
break;
case CollectionType.FemalePlayerCharacter:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial Player", Colors.ReniColorActive), (", ", 0),
("Your Character", ColorId.HandledConflictMod.Value(_config)), (", or ", 0),
("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0));
break;
case CollectionType.MaleNonPlayerCharacter:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Male Racial NPC", Colors.DiscordColor), (", ", 0),
("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0),
("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0));
break;
case CollectionType.FemaleNonPlayerCharacter:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Female Racial NPC", Colors.ReniColorActive), (", ", 0),
("Children", ColorId.FolderLine.Value(_config)), (", ", 0), ("Elderly", Colors.MetaInfoText), (", or ", 0),
("Individual ", ColorId.NewMod.Value(_config)), ("Assignments.", 0));
break;
}
}
ImGui.Separator();
}
private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color)
=> DrawButton(name, type, width, color, ActorIdentifier.Invalid, 's', _active.ByType(type));
/// <summary> Respect incognito mode for names of identifiers. </summary>
private string Name(ActorIdentifier id, string? name)
=> _selector.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned
? id.Incognito(name)
: name ?? id.ToString();
/// <summary> Respect incognito mode for names of collections. </summary>
private string Name(ModCollection? collection)
=> collection == null ? "Unassigned" :
collection == ModCollection.Empty ? "Use No Mods" :
_selector.IncognitoMode ? collection.AnonymizedName : collection.Name;
private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers)
{
if (identifiers.Length > 0 && identifiers[0].IsValid)
{
DrawButton($"{intro} ({Name(identifiers[0], null)})", CollectionType.Individual, width, 0, identifiers[0], suffix);
}
else
{
if (tooltip.Length == 0 && identifiers.Length > 0)
tooltip = $"The current target {identifiers[0].PlayerName} is not valid for an assignment.";
DrawButton($"{intro} (Unavailable)", CollectionType.Individual, width, 0, ActorIdentifier.Invalid, suffix);
}
ImGuiUtil.HoverTooltip(tooltip);
}
private void DrawCurrentCharacter(Vector2 width)
=> DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.AwaitedService.GetCurrentPlayer());
private void DrawCurrentTarget(Vector2 width)
=> DrawIndividualButton("Current Target", width, string.Empty, 't',
_actors.AwaitedService.FromObject(_targets.Target, false, true, true));
private void DrawNewPlayer(Vector2 width)
=> DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p',
_individualAssignmentUi.PlayerIdentifiers.FirstOrDefault());
private void DrawNewRetainer(Vector2 width)
=> DrawIndividualButton("New Bell Retainer", width, _individualAssignmentUi.RetainerTooltip, 'r',
_individualAssignmentUi.RetainerIdentifiers.FirstOrDefault());
private void DrawNewNpc(Vector2 width)
=> DrawIndividualButton("New NPC", width, _individualAssignmentUi.NpcTooltip, 'n',
_individualAssignmentUi.NpcIdentifiers.FirstOrDefault());
private void DrawNewOwned(Vector2 width)
=> DrawIndividualButton("New Owned NPC", width, _individualAssignmentUi.OwnedTooltip, 'o',
_individualAssignmentUi.OwnedIdentifiers.FirstOrDefault());
private void DrawIndividualCollections(Vector2 width)
{
for (var i = 0; i < _active.Individuals.Count; ++i)
{
var (name, ids, coll) = _active.Individuals.Assignments[i];
DrawButton(Name(ids[0], name), CollectionType.Individual, width, 0, ids[0], 'i', coll);
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < width.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
&& i < _active.Individuals.Count - 1)
ImGui.NewLine();
}
if (_active.Individuals.Count > 0)
ImGui.NewLine();
}
private void DrawCollectionName(ModCollection collection)
{
ImGui.Dummy(Vector2.One);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale);
using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available);
var name = Name(collection);
var size = ImGui.CalcTextSize(name).X;
var pos = ImGui.GetContentRegionAvail().X - size + ImGui.GetStyle().FramePadding.X * 2;
if (pos > 0)
ImGui.SetCursorPosX(pos / 2);
ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0);
ImGui.Dummy(Vector2.One);
}
private void DrawStatistics(ModCollection collection)
{
GatherInUse(collection);
ImGui.Separator();
var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing();
if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0)
{
ImGui.Dummy(Vector2.One);
using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available);
ImGuiUtil.DrawTextButton("Collection is not used.", new Vector2(ImGui.GetContentRegionAvail().X, buttonHeight),
Colors.PressEnterWarningBg);
ImGui.Dummy(Vector2.One);
ImGui.Separator();
}
else
{
var buttonWidth = new Vector2(175 * ImGuiHelpers.GlobalScale, buttonHeight);
DrawInUseStatistics(collection, buttonWidth);
DrawInheritanceStatistics(collection, buttonWidth);
}
}
private void GatherInUse(ModCollection collection)
{
_inUseCache.Clear();
foreach (var special in CollectionTypeExtensions.Special.Select(t => t.Item1)
.Prepend(CollectionType.Default)
.Prepend(CollectionType.Interface)
.Where(t => _active.ByType(t) == collection))
_inUseCache.Add((special, ActorIdentifier.Invalid));
foreach (var (_, id, coll) in _active.Individuals.Assignments.Where(t
=> t.Collection == collection && t.Identifiers.FirstOrDefault().IsValid))
_inUseCache.Add((CollectionType.Individual, id[0]));
}
private void DrawInUseStatistics(ModCollection collection, Vector2 buttonWidth)
{
if (_inUseCache.Count <= 0)
return;
using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero))
{
ImGuiUtil.DrawTextButton("In Use By", ImGui.GetContentRegionAvail() with { Y = 0 }, 0);
}
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale)
.Push(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero);
foreach (var ((type, id), idx) in _inUseCache.WithIndex())
{
var name = type == CollectionType.Individual ? Name(id, null) : Buttons[type].Name;
var color = Buttons.TryGetValue(type, out var p) ? p.Border : 0;
DrawButton(name, type, buttonWidth, color, id, 's', collection);
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
&& idx != _inUseCache.Count - 1)
ImGui.NewLine();
}
ImGui.NewLine();
ImGui.Dummy(Vector2.One);
ImGui.Separator();
}
private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth)
{
if (collection.DirectParentOf.Count <= 0)
return;
using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero))
{
ImGuiUtil.DrawTextButton("Inherited by", ImGui.GetContentRegionAvail() with { Y = 0 }, 0);
}
using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText);
ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0);
var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2
+ ImGui.GetStyle().ItemSpacing.X
+ ImGui.GetStyle().WindowPadding.X;
foreach (var parent in collection.DirectParentOf.Skip(1))
{
var name = Name(parent);
var size = ImGui.CalcTextSize(name).X;
ImGui.SameLine();
if (constOffset + size >= ImGui.GetContentRegionAvail().X)
ImGui.NewLine();
ImGuiUtil.DrawTextButton(name, Vector2.Zero, 0);
}
ImGui.Dummy(Vector2.One);
ImGui.Separator();
}
private void DrawSettingsList(ModCollection collection)
{
ImGui.Dummy(Vector2.One);
var size = new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetFrameHeightWithSpacing());
using var table = ImRaii.Table("##activeSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size);
if (!table)
return;
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, 5f * ImGui.GetFrameHeight());
ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight());
ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight());
ImGui.TableHeadersRow();
foreach (var (mod, (settings, parent)) in _mods.Select(m => (m, collection[m.Index]))
.Where(t => t.Item2.Settings != null)
.OrderBy(t => t.m.Name))
{
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(mod.Name);
ImGui.TableNextColumn();
if (parent != collection)
ImGui.TextUnformatted(Name(parent));
ImGui.TableNextColumn();
var enabled = settings!.Enabled;
using (var dis = ImRaii.Disabled())
{
ImGui.Checkbox("##check", ref enabled);
}
ImGui.TableNextColumn();
ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X);
}
}
private void DrawInactiveSettingsList(ModCollection collection)
{
if (collection.UnusedSettings.Count == 0)
return;
ImGui.Dummy(Vector2.One);
var text = collection.UnusedSettings.Count > 1
? $"Clear all {collection.UnusedSettings.Count} unused settings from deleted mods."
: "Clear the currently unused setting from a deleted mods.";
if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0)))
_collections.CleanUnavailableSettings(collection);
ImGui.Dummy(Vector2.One);
var size = new Vector2(ImGui.GetContentRegionAvail().X,
Math.Min(10, collection.UnusedSettings.Count + 1) * ImGui.GetFrameHeightWithSpacing());
using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size);
if (!table)
return;
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X);
ImGui.TableSetupColumn("Unused Mod Identifier", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight());
ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight());
ImGui.TableHeadersRow();
string? delete = null;
foreach (var (name, settings) in collection.UnusedSettings.OrderBy(n => n.Key))
{
using var id = ImRaii.PushId(name);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this unused setting.", false, true))
delete = name;
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(name);
ImGui.TableNextColumn();
var enabled = settings.Enabled;
using (var dis = ImRaii.Disabled())
{
ImGui.Checkbox("##check", ref enabled);
}
ImGui.TableNextColumn();
ImGuiUtil.RightAlign(settings.Priority.ToString(), ImGui.GetStyle().WindowPadding.X);
}
_collections.CleanUnavailableSetting(collection, delete);
ImGui.Separator();
}
/// <summary> Create names and border colors for special assignments. </summary>
private static IReadOnlyDictionary<CollectionType, (string Name, uint Border)> CreateButtons()
{
var ret = Enum.GetValues<CollectionType>().ToDictionary(t => t, t => (t.ToName(), 0u));
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
var color = race switch
{
SubRace.Midlander => 0xAA5C9FE4u,
SubRace.Highlander => 0xAA5C9FE4u,
SubRace.Wildwood => 0xAA5C9F49u,
SubRace.Duskwight => 0xAA5C9F49u,
SubRace.Plainsfolk => 0xAAEF8CB6u,
SubRace.Dunesfolk => 0xAAEF8CB6u,
SubRace.SeekerOfTheSun => 0xAA8CEFECu,
SubRace.KeeperOfTheMoon => 0xAA8CEFECu,
SubRace.Seawolf => 0xAAEFE68Cu,
SubRace.Hellsguard => 0xAAEFE68Cu,
SubRace.Raen => 0xAAB5EF8Cu,
SubRace.Xaela => 0xAAB5EF8Cu,
SubRace.Helion => 0xAAFFFFFFu,
SubRace.Lost => 0xAAFFFFFFu,
SubRace.Rava => 0xAA607FA7u,
SubRace.Veena => 0xAA607FA7u,
_ => 0u,
};
ret[CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color);
ret[CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color);
ret[CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color);
ret[CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color);
}
ret[CollectionType.MalePlayerCharacter] = ("♂ Player", 0);
ret[CollectionType.FemalePlayerCharacter] = ("♀ Player", 0);
ret[CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0);
ret[CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0);
return ret;
}
/// <summary> Create the special assignment tree in order and with free spaces. </summary>
private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree()
{
var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count);
void Add(CollectionType type, bool pre, bool post)
{
var (name, border) = Buttons[type];
ret.Add((type, pre, post, name, border));
}
Add(CollectionType.Default, false, false);
Add(CollectionType.Interface, false, false);
Add(CollectionType.Inactive, false, false);
Add(CollectionType.Inactive, false, false);
Add(CollectionType.Yourself, false, true);
Add(CollectionType.Inactive, false, true);
Add(CollectionType.NonPlayerChild, false, true);
Add(CollectionType.NonPlayerElderly, false, true);
Add(CollectionType.MalePlayerCharacter, true, true);
Add(CollectionType.FemalePlayerCharacter, true, true);
Add(CollectionType.MaleNonPlayerCharacter, true, true);
Add(CollectionType.FemaleNonPlayerCharacter, true, true);
var pre = true;
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre);
pre = !pre;
}
return ret;
}
}

View file

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private readonly TutorialService _tutorial;
private ModCollection? _dragging;
public bool IncognitoMode;
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
TutorialService tutorial)
: base(new List<ModCollection>(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{
_config = config;
_communicator = communicator;
_storage = storage;
_active = active;
_tutorial = tutorial;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
// Set items.
OnCollectionChange(CollectionType.Inactive, null, null, string.Empty);
// Set selection.
OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty);
}
protected override bool OnDelete(int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.RemoveCollection(Items[idx]);
}
protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current
? $"The selected collection {Name(Current)} can not be deleted."
: $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.AddCollection(name, Items[idx]);
}
protected override bool Filtered(int idx)
=> !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
private const string PayloadString = "Collection";
protected override bool OnDraw(int idx)
{
using var color = ImRaii.PushColor(ImGuiCol.Header, ColorId.SelectedCollection.Value(_config));
var ret = ImGui.Selectable(Name(Items[idx]), idx == CurrentIdx);
using var source = ImRaii.DragDropSource();
if (idx == CurrentIdx)
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
if (source)
{
_dragging = Items[idx];
ImGui.SetDragDropPayload(PayloadString, nint.Zero, 0);
ImGui.TextUnformatted($"Assigning {Name(_dragging)} to...");
}
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
return ret;
}
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
{
using var target = ImRaii.DragDropTarget();
if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping(PayloadString))
return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
_dragging = null;
}
public void Dispose()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
private string Name(ModCollection collection)
=> IncognitoMode ? collection.AnonymizedName : collection.Name;
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3)
{
switch (type)
{
case CollectionType.Temporary: return;
case CollectionType.Current:
if (@new != null)
SetCurrent(@new);
SetFilterDirty();
return;
case CollectionType.Inactive:
Items.Clear();
foreach (var c in _storage.OrderBy(c => c.Name))
Items.Add(c);
if (old == Current)
ClearCurrentSelection();
else
TryRestoreCurrent();
SetFilterDirty();
return;
default:
SetFilterDirty();
return;
}
}
}

View file

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Enums;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Services;
namespace Penumbra.UI.CollectionTab;
public class IndividualAssignmentUi : IDisposable
{
private readonly CommunicatorService _communicator;
private readonly ActorService _actorService;
private readonly CollectionManager _collectionManager;
private WorldCombo _worldCombo = null!;
private NpcCombo _mountCombo = null!;
private NpcCombo _companionCombo = null!;
private NpcCombo _ornamentCombo = null!;
private NpcCombo _bnpcCombo = null!;
private NpcCombo _enpcCombo = null!;
private bool _ready;
public IndividualAssignmentUi(CommunicatorService communicator, ActorService actors, CollectionManager collectionManager)
{
_communicator = communicator;
_actorService = actors;
_collectionManager = collectionManager;
_communicator.CollectionChange.Subscribe(UpdateIdentifiers);
if (_actorService.Valid)
SetupCombos();
else
_actorService.FinishedCreation += SetupCombos;
}
public string PlayerTooltip { get; private set; } = NewPlayerTooltipEmpty;
public string RetainerTooltip { get; private set; } = NewRetainerTooltipEmpty;
public string NpcTooltip { get; private set; } = NewNpcTooltipEmpty;
public string OwnedTooltip { get; private set; } = NewPlayerTooltipEmpty;
public ActorIdentifier[] PlayerIdentifiers
=> _playerIdentifiers;
public ActorIdentifier[] RetainerIdentifiers
=> _retainerIdentifiers;
public ActorIdentifier[] NpcIdentifiers
=> _npcIdentifiers;
public ActorIdentifier[] OwnedIdentifiers
=> _ownedIdentifiers;
public void DrawWorldCombo(float width)
{
if (_ready && _worldCombo.Draw(width))
UpdateIdentifiers();
}
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;
UpdateIdentifiers();
}
}
public void DrawNewPlayerCollection(float width)
{
if (!_ready)
return;
ImGui.SetNextItemWidth(width);
if (ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32))
UpdateIdentifiers();
}
public void DrawNewNpcCollection(float width)
{
if (!_ready)
return;
var combo = GetNpcCombo(_newKind);
if (combo.Draw(width))
UpdateIdentifiers();
}
public void Dispose()
=> _communicator.CollectionChange.Unsubscribe(UpdateIdentifiers);
// Input Selections.
private string _newCharacterName = string.Empty;
private ObjectKind _newKind = ObjectKind.BattleNpc;
private ActorIdentifier[] _playerIdentifiers = Array.Empty<ActorIdentifier>();
private ActorIdentifier[] _retainerIdentifiers = Array.Empty<ActorIdentifier>();
private ActorIdentifier[] _npcIdentifiers = Array.Empty<ActorIdentifier>();
private ActorIdentifier[] _ownedIdentifiers = Array.Empty<ActorIdentifier>();
private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'.";
private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name.";
private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character.";
private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer.";
private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection.";
private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first.";
private static readonly IReadOnlyList<ObjectKind> ObjectKinds = new[]
{
ObjectKind.BattleNpc,
ObjectKind.EventNpc,
ObjectKind.Companion,
ObjectKind.MountType,
ObjectKind.Ornament,
};
private NpcCombo GetNpcCombo(ObjectKind kind)
=> kind switch
{
ObjectKind.BattleNpc => _bnpcCombo,
ObjectKind.EventNpc => _enpcCombo,
ObjectKind.MountType => _mountCombo,
ObjectKind.Companion => _companionCombo,
ObjectKind.Ornament => _ornamentCombo,
_ => throw new NotImplementedException(),
};
/// <summary> Create combos when ready. </summary>
private void SetupCombos()
{
_worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds);
_mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts);
_companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions);
_ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments);
_bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs);
_enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs);
_ready = true;
_actorService.FinishedCreation -= SetupCombos;
}
private void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3)
{
if (type == CollectionType.Individual)
UpdateIdentifiers();
}
private void UpdateIdentifiers()
{
var combo = GetNpcCombo(_newKind);
PlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName,
_worldCombo.CurrentSelection.Key, ObjectKind.None,
Array.Empty<uint>(), out _playerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
RetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None,
Array.Empty<uint>(), out _retainerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
if (combo.CurrentSelection.Ids != null)
{
NpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
combo.CurrentSelection.Ids, out _npcIdentifiers) switch
{
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
OwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName,
_worldCombo.CurrentSelection.Key, _newKind,
combo.CurrentSelection.Ids, out _ownedIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
}
else
{
NpcTooltip = NewNpcTooltipEmpty;
OwnedTooltip = NewNpcTooltipEmpty;
_npcIdentifiers = Array.Empty<ActorIdentifier>();
_ownedIdentifiers = Array.Empty<ActorIdentifier>();
}
}
}

View file

@ -1,358 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Services;
namespace Penumbra.UI.CollectionTab;
public class IndividualCollectionUi
{
private readonly ActorService _actorService;
private readonly CollectionManager _collectionManager;
private readonly CollectionCombo _withEmpty;
public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionCombo withEmpty)
{
_actorService = actors;
_collectionManager = collectionManager;
_withEmpty = withEmpty;
if (_actorService.Valid)
SetupCombos();
else
_actorService.FinishedCreation += SetupCombos;
}
/// <summary> Draw all individual assignments as well as the options to create a new one. </summary>
public void Draw()
{
if (!_ready)
return;
using var _ = ImRaii.Group();
using var mainId = ImRaii.PushId("Individual");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"Individual {TutorialService.ConditionalIndividual}s");
ImGui.SameLine();
ImGuiComponents.HelpMarker("Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n"
+ $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n"
+ "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections.");
ImGui.Separator();
for (var i = 0; i < _collectionManager.Active.Individuals.Count; ++i)
{
DrawIndividualAssignment(i);
}
UiHelpers.DefaultLineSpace();
DrawNewIndividualCollection();
}
public void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3)
{
if (type == CollectionType.Individual)
UpdateIdentifiers();
}
// Input Selections.
private string _newCharacterName = string.Empty;
private ObjectKind _newKind = ObjectKind.BattleNpc;
private WorldCombo _worldCombo = null!;
private NpcCombo _mountCombo = null!;
private NpcCombo _companionCombo = null!;
private NpcCombo _ornamentCombo = null!;
private NpcCombo _bnpcCombo = null!;
private NpcCombo _enpcCombo = null!;
private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'.";
private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name.";
private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character.";
private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer.";
private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection.";
private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first.";
private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty<ActorIdentifier>();
private string _newPlayerTooltip = NewPlayerTooltipEmpty;
private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty<ActorIdentifier>();
private string _newRetainerTooltip = NewRetainerTooltipEmpty;
private ActorIdentifier[] _newNpcIdentifiers = Array.Empty<ActorIdentifier>();
private string _newNpcTooltip = NewNpcTooltipEmpty;
private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty<ActorIdentifier>();
private string _newOwnedTooltip = NewPlayerTooltipEmpty;
private bool _ready;
/// <summary> Create combos when ready. </summary>
private void SetupCombos()
{
_worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds);
_mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts);
_companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions);
_ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments);
_bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs);
_enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs);
_ready = true;
_actorService.FinishedCreation -= SetupCombos;
}
private static readonly IReadOnlyList<ObjectKind> ObjectKinds = new[]
{
ObjectKind.BattleNpc,
ObjectKind.EventNpc,
ObjectKind.Companion,
ObjectKind.MountType,
ObjectKind.Ornament,
};
/// <summary> Draw the Object Kind Selector. </summary>
private bool DrawNewObjectKindOptions(float width)
{
ImGui.SetNextItemWidth(width);
using var combo = ImRaii.Combo("##newKind", _newKind.ToName());
if (!combo)
return false;
var ret = false;
foreach (var kind in ObjectKinds)
{
if (!ImGui.Selectable(kind.ToName(), _newKind == kind))
continue;
_newKind = kind;
ret = true;
}
return ret;
}
private int _individualDragDropIdx = -1;
/// <summary> Draw a single individual assignment. </summary>
private void DrawIndividualAssignment(int idx)
{
var (name, _) = _collectionManager.Active.Individuals[idx];
using var id = ImRaii.PushId(idx);
_withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty,
false, true))
_collectionManager.Active.RemoveIndividualCollection(idx);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Selectable(name);
using (var source = ImRaii.DragDropSource())
{
if (source)
{
ImGui.SetDragDropPayload("Individual", nint.Zero, 0);
_individualDragDropIdx = idx;
}
}
using var target = ImRaii.DragDropTarget();
if (!target.Success || !ImGuiUtil.IsDropping("Individual"))
return;
if (_individualDragDropIdx >= 0)
_collectionManager.Active.MoveIndividualCollection(_individualDragDropIdx, idx);
_individualDragDropIdx = -1;
}
private bool DrawNewPlayerCollection(Vector2 buttonWidth, float width)
{
var change = _worldCombo.Draw(width);
ImGui.SameLine();
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width);
change |= ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip,
_newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0))
{
_collectionManager.Active.CreateIndividualCollection(_newPlayerIdentifiers);
change = true;
}
return change;
}
private bool DrawNewNpcCollection(NpcCombo combo, Vector2 buttonWidth, float width)
{
var comboWidth = UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width;
var change = DrawNewObjectKindOptions(width);
ImGui.SameLine();
change |= combo.Draw(comboWidth);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip,
_newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0))
{
_collectionManager.Active.CreateIndividualCollection(_newNpcIdentifiers);
change = true;
}
return change;
}
private bool DrawNewOwnedCollection(Vector2 buttonWidth)
{
if (!ImGuiUtil.DrawDisabledButton("Assign Owned NPC", buttonWidth, _newOwnedTooltip,
_newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0))
return false;
_collectionManager.Active.CreateIndividualCollection(_newOwnedIdentifiers);
return true;
}
private bool DrawNewRetainerCollection(Vector2 buttonWidth)
{
if (!ImGuiUtil.DrawDisabledButton("Assign Bell Retainer", buttonWidth, _newRetainerTooltip,
_newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0))
return false;
_collectionManager.Active.CreateIndividualCollection(_newRetainerIdentifiers);
return true;
}
private NpcCombo GetNpcCombo(ObjectKind kind)
=> kind switch
{
ObjectKind.BattleNpc => _bnpcCombo,
ObjectKind.EventNpc => _enpcCombo,
ObjectKind.MountType => _mountCombo,
ObjectKind.Companion => _companionCombo,
ObjectKind.Ornament => _ornamentCombo,
_ => throw new NotImplementedException(),
};
private void DrawNewIndividualCollection()
{
var width = (UiHelpers.InputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X) / 3;
var buttonWidth1 = new Vector2(90 * UiHelpers.Scale, 0);
var buttonWidth2 = new Vector2(120 * UiHelpers.Scale, 0);
var assignWidth = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
var change = DrawNewCurrentPlayerCollection(assignWidth);
ImGui.SameLine();
change |= DrawNewTargetCollection(assignWidth);
change |= DrawNewPlayerCollection(buttonWidth1, width);
ImGui.SameLine();
change |= DrawNewRetainerCollection(buttonWidth2);
var combo = GetNpcCombo(_newKind);
change |= DrawNewNpcCollection(combo, buttonWidth1, width);
ImGui.SameLine();
change |= DrawNewOwnedCollection(buttonWidth2);
if (change)
UpdateIdentifiers();
}
private bool DrawNewCurrentPlayerCollection(Vector2 width)
{
var player = _actorService.AwaitedService.GetCurrentPlayer();
var result = _collectionManager.Active.Individuals.CanAdd(player);
var tt = result switch
{
IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.",
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
IndividualCollections.AddResult.Invalid => "No logged-in character detected.",
_ => string.Empty,
};
if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid))
return false;
_collectionManager.Active.CreateIndividualCollection(player);
return true;
}
private bool DrawNewTargetCollection(Vector2 width)
{
var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true);
var result = _collectionManager.Active.Individuals.CanAdd(target);
var tt = result switch
{
IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.",
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
IndividualCollections.AddResult.Invalid => "No valid character in target detected.",
_ => string.Empty,
};
if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid))
{
_collectionManager.Active.CreateIndividualCollection(_collectionManager.Active.Individuals.GetGroup(target));
return true;
}
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n"
+ "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n"
+ "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned.");
return false;
}
private void UpdateIdentifiers()
{
var combo = GetNpcCombo(_newKind);
_newPlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName,
_worldCombo.CurrentSelection.Key, ObjectKind.None,
Array.Empty<uint>(), out _newPlayerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
_newRetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None,
Array.Empty<uint>(), out _newRetainerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
if (combo.CurrentSelection.Ids != null)
{
_newNpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch
{
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
_newOwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName,
_worldCombo.CurrentSelection.Key, _newKind,
combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
}
else
{
_newNpcTooltip = NewNpcTooltipEmpty;
_newOwnedTooltip = NewNpcTooltipEmpty;
_newNpcIdentifiers = Array.Empty<ActorIdentifier>();
_newOwnedIdentifiers = Array.Empty<ActorIdentifier>();
}
}
}

View file

@ -7,32 +7,53 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Collections.Manager;
using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab;
public class InheritanceUi
{
private const int InheritedCollectionHeight = 9;
private const string InheritanceDragDropLabel = "##InheritanceMove";
private const int InheritedCollectionHeight = 9;
private const string InheritanceDragDropLabel = "##InheritanceMove";
private readonly CollectionManager _collectionManager;
private readonly Configuration _config;
private readonly CollectionStorage _collections;
private readonly ActiveCollections _active;
private readonly InheritanceManager _inheritance;
private readonly CollectionSelector _selector;
public InheritanceUi(Configuration config, CollectionManager collectionManager, CollectionSelector selector)
{
_config = config;
_selector = selector;
_collections = collectionManager.Storage;
_active = collectionManager.Active;
_inheritance = collectionManager.Inheritances;
}
public InheritanceUi(CollectionManager collectionManager)
=> _collectionManager = collectionManager;
/// <summary> Draw the whole inheritance block. </summary>
public void Draw()
{
using var group = ImRaii.Group();
using var id = ImRaii.PushId("##Inheritance");
ImGui.TextUnformatted($"The {TutorialService.SelectedCollection} inherits from:");
DrawCurrentCollectionInheritance();
using var id = ImRaii.PushId("##Inheritance");
ImGuiUtil.DrawColoredText(($"The {TutorialService.SelectedCollection} ", 0), (Name(_active.Current), ColorId.SelectedCollection.Value(_config) | 0xFF000000), (" inherits from:", 0));
ImGui.Dummy(Vector2.One);
DrawCurrentCollectionInheritance();
ImGui.SameLine();
DrawInheritanceTrashButton();
ImGui.SameLine();
DrawRightText();
DrawNewInheritanceSelection();
DelayedActions();
}
ImGui.SameLine();
if (ImGui.Button("More Information about Inheritance", new Vector2(ImGui.GetContentRegionAvail().X, 0)))
ImGui.OpenPopup("InheritanceHelp");
DrawHelpPopup();
DelayedActions();
}
// Keep for reuse.
private readonly HashSet<ModCollection> _seenInheritedCollections = new(32);
@ -40,8 +61,45 @@ public class InheritanceUi
// Execute changes only outside of loops.
private ModCollection? _newInheritance;
private ModCollection? _movedInheritance;
private (int, int)? _inheritanceAction;
private ModCollection? _newCurrentCollection;
private (int, int)? _inheritanceAction;
private ModCollection? _newCurrentCollection;
private void DrawRightText()
{
using var group = ImRaii.Group();
ImGuiUtil.TextWrapped(
"Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod.");
ImGuiUtil.TextWrapped(
"You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can.");
}
private void DrawHelpPopup()
=> ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () =>
{
ImGui.NewLine();
ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'.");
ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections.");
ImGui.BulletText(
"If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances.");
ImGui.BulletText(
"If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used.");
ImGui.BulletText("If no such collection is found, the mod will be treated as disabled.");
ImGui.BulletText(
"Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before.");
ImGui.NewLine();
ImGui.TextUnformatted("Example");
ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled.");
ImGui.BulletText(
"Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A.");
ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured.");
ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured.");
using var indent = ImRaii.PushIndent();
ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings.");
ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings.");
ImGui.BulletText(
"D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A).");
});
/// <summary>
/// If an inherited collection is expanded,
@ -49,15 +107,15 @@ public class InheritanceUi
/// </summary>
private void DrawInheritedChildren(ModCollection collection)
{
using var id = ImRaii.PushId(collection.Index);
using var id = ImRaii.PushId(collection.Index);
using var indent = ImRaii.PushIndent();
// Get start point for the lines (top of the selector).
// Tree line stuff.
var lineStart = ImGui.GetCursorScreenPos();
var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2;
var drawList = ImGui.GetWindowDrawList();
var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale);
var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2;
var drawList = ImGui.GetWindowDrawList();
var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale);
lineStart.X += offsetX;
lineStart.Y -= 2 * UiHelpers.Scale;
var lineEnd = lineStart;
@ -70,7 +128,7 @@ public class InheritanceUi
_seenInheritedCollections.Contains(inheritance));
_seenInheritedCollections.Add(inheritance);
ImRaii.TreeNode(inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet);
ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet);
var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax());
DrawInheritanceTreeClicks(inheritance, false);
@ -95,7 +153,7 @@ public class InheritanceUi
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config),
_seenInheritedCollections.Contains(collection));
_seenInheritedCollections.Add(collection);
using var tree = ImRaii.TreeNode(collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen);
using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen);
color.Pop();
DrawInheritanceTreeClicks(collection, true);
DrawInheritanceDropSource(collection);
@ -117,16 +175,15 @@ public class InheritanceUi
return;
_seenInheritedCollections.Clear();
_seenInheritedCollections.Add(_collectionManager.Active.Current);
foreach (var collection in _collectionManager.Active.Current.DirectlyInheritsFrom.ToList())
_seenInheritedCollections.Add(_active.Current);
foreach (var collection in _active.Current.DirectlyInheritsFrom.ToList())
DrawInheritance(collection);
}
/// <summary> Draw a drag and drop button to delete. </summary>
private void DrawInheritanceTrashButton()
{
ImGui.SameLine();
var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight };
var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight };
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
// Prevent hovering from highlighting the button.
using var color = ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor)
@ -136,7 +193,7 @@ public class InheritanceUi
using var target = ImRaii.DragDropTarget();
if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel))
_inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1);
_inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1);
}
/// <summary>
@ -147,7 +204,7 @@ public class InheritanceUi
{
if (_newCurrentCollection != null)
{
_collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current);
_active.SetCollection(_newCurrentCollection, CollectionType.Current);
_newCurrentCollection = null;
}
@ -157,9 +214,9 @@ public class InheritanceUi
if (_inheritanceAction.Value.Item1 >= 0)
{
if (_inheritanceAction.Value.Item2 == -1)
_collectionManager.Inheritances.RemoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1);
_inheritance.RemoveInheritance(_active.Current, _inheritanceAction.Value.Item1);
else
_collectionManager.Inheritances.MoveInheritance(_collectionManager.Active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2);
_inheritance.MoveInheritance(_active.Current, _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2);
}
_inheritanceAction = null;
@ -173,57 +230,23 @@ public class InheritanceUi
{
DrawNewInheritanceCombo();
ImGui.SameLine();
var inheritance = InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, _newInheritance);
var inheritance = InheritanceManager.CheckValidInheritance(_active.Current, _newInheritance);
var tt = inheritance switch
{
InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.",
InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.",
InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.",
InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.",
InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.",
InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.",
InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.",
InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.",
_ => string.Empty,
InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.",
_ => string.Empty,
};
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt,
inheritance != InheritanceManager.ValidInheritance.Valid, true)
&& _collectionManager.Inheritances.AddInheritance(_collectionManager.Active.Current, _newInheritance!))
&& _inheritance.AddInheritance(_active.Current, _newInheritance!))
_newInheritance = null;
if (inheritance != InheritanceManager.ValidInheritance.Valid)
_newInheritance = null;
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), UiHelpers.IconButtonSize, "What is Inheritance?",
false, true))
ImGui.OpenPopup("InheritanceHelp");
ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 21 * ImGui.GetTextLineHeightWithSpacing()), () =>
{
ImGui.NewLine();
ImGui.TextWrapped(
"Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod.");
ImGui.NewLine();
ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'.");
ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections.");
ImGui.BulletText(
"If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances.");
ImGui.BulletText(
"If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used.");
ImGui.BulletText("If no such collection is found, the mod will be treated as disabled.");
ImGui.BulletText(
"Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before.");
ImGui.NewLine();
ImGui.TextUnformatted("Example");
ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled.");
ImGui.BulletText(
"Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A.");
ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured.");
ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured.");
using var indent = ImRaii.PushIndent();
ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings.");
ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings.");
ImGui.BulletText(
"D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A).");
});
}
/// <summary>
@ -233,18 +256,18 @@ public class InheritanceUi
private void DrawNewInheritanceCombo()
{
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton);
_newInheritance ??= _collectionManager.Storage.FirstOrDefault(c
=> c != _collectionManager.Active.Current && !_collectionManager.Active.Current.DirectlyInheritsFrom.Contains(c))
_newInheritance ??= _collections.FirstOrDefault(c
=> c != _active.Current && !_active.Current.DirectlyInheritsFrom.Contains(c))
?? ModCollection.Empty;
using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name);
using var combo = ImRaii.Combo("##newInheritance", Name(_newInheritance));
if (!combo)
return;
foreach (var collection in _collectionManager.Storage
.Where(c => InheritanceManager.CheckValidInheritance(_collectionManager.Active.Current, c) == InheritanceManager.ValidInheritance.Valid)
foreach (var collection in _collections
.Where(c => InheritanceManager.CheckValidInheritance(_active.Current, c) == InheritanceManager.ValidInheritance.Valid)
.OrderBy(c => c.Name))
{
if (ImGui.Selectable(collection.Name, _newInheritance == collection))
if (ImGui.Selectable(Name(collection), _newInheritance == collection))
_newInheritance = collection;
}
}
@ -261,8 +284,8 @@ public class InheritanceUi
if (_movedInheritance != null)
{
var idx1 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance);
var idx2 = _collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection);
var idx1 = _active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance);
var idx2 = _active.Current.DirectlyInheritsFrom.IndexOf(collection);
if (idx1 >= 0 && idx2 >= 0)
_inheritanceAction = (idx1, idx2);
}
@ -279,7 +302,7 @@ public class InheritanceUi
ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0);
_movedInheritance = collection;
ImGui.TextUnformatted($"Moving {_movedInheritance?.Name ?? "Unknown"}...");
ImGui.TextUnformatted($"Moving {(_movedInheritance != null ? Name(_movedInheritance) : "Unknown")}...");
}
/// <summary>
@ -292,7 +315,7 @@ public class InheritanceUi
if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (withDelete && ImGui.GetIO().KeyShift)
_inheritanceAction = (_collectionManager.Active.Current.DirectlyInheritsFrom.IndexOf(collection), -1);
_inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(collection), -1);
else
_newCurrentCollection = collection;
}
@ -300,4 +323,7 @@ public class InheritanceUi
ImGuiUtil.HoverTooltip($"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one."
+ (withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty));
}
private string Name(ModCollection collection)
=> _selector.IncognitoMode ? collection.AnonymizedName : collection.Name;
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Utility;
using ImGuiNET;
using OtterGui.Widgets;
@ -10,7 +12,8 @@ public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)>
private readonly string _label;
public NpcCombo(string label, IReadOnlyDictionary<uint, string> names)
: base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key).ToList())
: 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)
@ -28,4 +31,24 @@ public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)>
public bool Draw(float width)
=> Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
/// <summary> Compare strings in a way that letters and numbers are sorted before any special symbols. </summary>
private class NameComparer : IComparer<string>
{
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();
}

View file

@ -1,42 +0,0 @@
using ImGuiNET;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.Collections.Manager;
namespace Penumbra.UI.CollectionTab;
public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)>
{
private readonly CollectionManager _collectionManager;
public (CollectionType, string, string)? CurrentType
=> CollectionTypeExtensions.Special[CurrentIdx];
public int CurrentIdx;
private readonly float _unscaledWidth;
private readonly string _label;
public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth)
: base(CollectionTypeExtensions.Special, false)
{
_collectionManager = collectionManager;
_label = label;
_unscaledWidth = unscaledWidth;
}
public void Draw()
{
var preview = CurrentIdx >= 0 ? Items[CurrentIdx].Item2 : string.Empty;
Draw(_label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * UiHelpers.Scale,
ImGui.GetTextLineHeightWithSpacing());
}
protected override string ToString((CollectionType, string, string) obj)
=> obj.Item2;
protected override bool IsVisible(int globalIdx, LowerString filter)
{
var obj = Items[globalIdx];
return filter.IsContained(obj.Item2) && _collectionManager.Active.ByType(obj.Item1) == null;
}
}

View file

@ -44,7 +44,7 @@ public sealed class ConfigWindow : Window
RespectCloseHotkey = true;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(900, 600),
MinimumSize = new Vector2(900, 675),
MaximumSize = new Vector2(4096, 2160),
};
tutorial.UpdateTutorialStep();

View file

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
@ -20,514 +15,44 @@ using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public sealed class CollectionTree
{
private readonly CollectionStorage _collections;
private readonly ActiveCollections _active;
private readonly CollectionSelector2 _selector;
private readonly ActorService _actors;
private readonly TargetManager _targets;
private static readonly IReadOnlyList<(string Name, uint Border)> Buttons = CreateButtons();
private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree();
public CollectionTree(CollectionManager manager, CollectionSelector2 selector, ActorService actors,
TargetManager targets)
{
_collections = manager.Storage;
_active = manager.Active;
_selector = selector;
_actors = actors;
_targets = targets;
}
public void DrawSimple()
{
var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
DrawSimpleCollectionButton(CollectionType.Default, buttonWidth);
DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth);
DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth);
DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth);
var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale };
var player = _actors.AwaitedService.GetCurrentPlayer();
DrawButton($"Current Character ({(player.IsValid ? player.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0,
player);
ImGui.SameLine();
var target = _actors.AwaitedService.FromObject(_targets.Target, false, true, true);
DrawButton($"Current Target ({(target.IsValid ? target.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, target);
if (_active.Individuals.Count > 0)
{
ImGui.TextUnformatted("Currently Active Individual Assignments");
for (var i = 0; i < _active.Individuals.Count; ++i)
{
var (name, ids, coll) = _active.Individuals.Assignments[i];
DrawButton(name, CollectionType.Individual, buttonWidth, 0, ids[0], coll);
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
&& i < _active.Individuals.Count - 1)
ImGui.NewLine();
}
ImGui.NewLine();
}
var first = true;
void Button(CollectionType type)
{
var (name, border) = Buttons[(int)type];
var collection = _active.ByType(type);
if (collection == null)
return;
if (first)
{
ImGui.Separator();
ImGui.TextUnformatted("Currently Active Advanced Assignments");
first = false;
}
DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, collection);
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X)
ImGui.NewLine();
}
Button(CollectionType.NonPlayerChild);
Button(CollectionType.NonPlayerElderly);
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false));
Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false));
Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true));
Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true));
}
}
public void DrawAdvanced()
{
using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
.Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
var dummy = new Vector2(1, 0);
foreach (var (type, pre, post, name, border) in AdvancedTree)
{
ImGui.TableNextColumn();
if (type is CollectionType.Inactive)
continue;
if (pre)
ImGui.Dummy(dummy);
DrawAssignmentButton(type, buttonWidth, name, border);
if (post)
ImGui.Dummy(dummy);
}
}
private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix = 'i')
{
var label = $"{type}{identifier}{suffix}";
if (open)
ImGui.OpenPopup(label);
using var context = ImRaii.Popup(label);
if (context)
{
using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor))
{
if (ImGui.MenuItem("Use no mods."))
_active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier));
}
if (collection != null)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
if (ImGui.MenuItem("Remove this assignment."))
_active.SetCollection(null, type, _active.Individuals.GetGroup(identifier));
}
foreach (var coll in _collections)
{
if (coll != collection && ImGui.MenuItem($"Use {coll.Name}."))
_active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier));
}
}
}
private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, ModCollection? collection = null)
{
using var group = ImRaii.Group();
var invalid = type == CollectionType.Individual && !id.IsValid;
var redundancy = _active.RedundancyCheck(type, id);
collection ??= _active.ByType(type, id);
using var color = ImRaii.PushColor(ImGuiCol.Button,
collection == null
? 0
: redundancy.Length > 0
? Colors.RedundantColor
: collection == _active.Current
? Colors.SelectedColor
: collection == ModCollection.Empty
? Colors.RedTableBgTint
: ImGui.GetColorU32(ImGuiCol.Button), !invalid)
.Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor);
using var disabled = ImRaii.Disabled(invalid);
var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right);
var hovered = redundancy.Length > 0 && ImGui.IsItemHovered();
if (!invalid)
{
_selector.DragTarget(type, id);
var name = collection == ModCollection.Empty ? "Use No Mods" : collection?.Name ?? "Unassigned";
var size = ImGui.CalcTextSize(name);
var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding;
ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name);
DrawContext(button, collection, type, id);
}
if (hovered)
ImGui.SetTooltip(redundancy);
return button;
}
private void DrawSimpleCollectionButton(CollectionType type, Vector2 width)
{
DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid);
ImGui.SameLine();
var secondLine = string.Empty;
foreach (var parent in type.InheritanceOrder())
{
var coll = _active.ByType(parent);
if (coll == null)
continue;
secondLine = $"\nWill behave as {parent.ToName()} ({coll.Name}) while unassigned.";
break;
}
ImGui.TextUnformatted(type.ToDescription() + secondLine);
ImGui.Separator();
}
private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color)
=> DrawButton(name, type, width, color, ActorIdentifier.Invalid, _active.ByType(type));
private static IReadOnlyList<(string Name, uint Border)> CreateButtons()
{
var ret = Enum.GetValues<CollectionType>().Select(t => (t.ToName(), 0u)).ToArray();
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
var color = race switch
{
SubRace.Midlander => 0xAA5C9FE4u,
SubRace.Highlander => 0xAA5C9FE4u,
SubRace.Wildwood => 0xAA5C9F49u,
SubRace.Duskwight => 0xAA5C9F49u,
SubRace.Plainsfolk => 0xAAEF8CB6u,
SubRace.Dunesfolk => 0xAAEF8CB6u,
SubRace.SeekerOfTheSun => 0xAA8CEFECu,
SubRace.KeeperOfTheMoon => 0xAA8CEFECu,
SubRace.Seawolf => 0xAAEFE68Cu,
SubRace.Hellsguard => 0xAAEFE68Cu,
SubRace.Raen => 0xAAB5EF8Cu,
SubRace.Xaela => 0xAAB5EF8Cu,
SubRace.Helion => 0xAAFFFFFFu,
SubRace.Lost => 0xAAFFFFFFu,
SubRace.Rava => 0xAA607FA7u,
SubRace.Veena => 0xAA607FA7u,
_ => 0u,
};
ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color);
ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color);
ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color);
ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color);
}
ret[(int)CollectionType.MalePlayerCharacter] = ("♂ Player", 0);
ret[(int)CollectionType.FemalePlayerCharacter] = ("♀ Player", 0);
ret[(int)CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0);
ret[(int)CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0);
return ret;
}
private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree()
{
var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count);
void Add(CollectionType type, bool pre, bool post)
{
var (name, border) = (int)type >= Buttons.Count ? (type.ToName(), 0) : Buttons[(int)type];
ret.Add((type, pre, post, name, border));
}
Add(CollectionType.Default, false, false);
Add(CollectionType.Interface, false, false);
Add(CollectionType.Inactive, false, false);
Add(CollectionType.Inactive, false, false);
Add(CollectionType.Yourself, false, true);
Add(CollectionType.Inactive, false, true);
Add(CollectionType.NonPlayerChild, false, true);
Add(CollectionType.NonPlayerElderly, false, true);
Add(CollectionType.MalePlayerCharacter, true, true);
Add(CollectionType.FemalePlayerCharacter, true, true);
Add(CollectionType.MaleNonPlayerCharacter, true, true);
Add(CollectionType.FemaleNonPlayerCharacter, true, true);
var pre = true;
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
{
Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre);
Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre);
pre = !pre;
}
return ret;
}
}
public sealed class CollectionPanel
{
private readonly CollectionManager _manager;
private readonly ModStorage _modStorage;
private readonly InheritanceUi _inheritanceUi;
public CollectionPanel(CollectionManager manager, ModStorage modStorage)
{
_manager = manager;
_modStorage = modStorage;
_inheritanceUi = new InheritanceUi(_manager);
}
public void Draw()
{
var collection = _manager.Active.Current;
DrawName(collection);
DrawStatistics(collection);
_inheritanceUi.Draw();
DrawSettingsList(collection);
DrawInactiveSettingsList(collection);
}
private void DrawName(ModCollection collection)
{
ImGui.TextUnformatted($"{collection.Name} ({collection.AnonymizedName})");
}
private void DrawStatistics(ModCollection collection)
{
ImGui.TextUnformatted("Used for:");
var sb = new StringBuilder(128);
if (_manager.Active.Default == collection)
sb.Append(CollectionType.Default.ToName()).Append(", ");
if (_manager.Active.Interface == collection)
sb.Append(CollectionType.Interface.ToName()).Append(", ");
foreach (var (type, _) in _manager.Active.SpecialAssignments.Where(p => p.Value == collection))
sb.Append(type.ToName()).Append(", ");
foreach (var (name, _) in _manager.Active.Individuals.Where(p => p.Collection == collection))
sb.Append(name).Append(", ");
ImGui.SameLine();
ImGuiUtil.TextWrapped(sb.Length == 0 ? "Nothing" : sb.ToString(0, sb.Length - 2));
if (collection.DirectParentOf.Count > 0)
{
ImGui.TextUnformatted("Inherited by:");
ImGui.SameLine();
ImGuiUtil.TextWrapped(string.Join(", ", collection.DirectParentOf.Select(c => c.Name)));
}
}
private void DrawSettingsList(ModCollection collection)
{
using var box = ImRaii.ListBox("##activeSettings");
if (!box)
return;
foreach (var (mod, (settings, parent)) in _modStorage.Select(m => (m, collection[m.Index])).Where(t => t.Item2.Settings != null)
.OrderBy(t => t.m.Name))
ImGui.TextUnformatted($"{mod}{(parent != collection ? $" (inherited from {parent.Name})" : string.Empty)}");
}
private void DrawInactiveSettingsList(ModCollection collection)
{
if (collection.UnusedSettings.Count == 0)
return;
if (ImGui.Button("Clear Unused Settings"))
_manager.Storage.CleanUnavailableSettings(collection);
using var box = ImRaii.ListBox("##inactiveSettings");
if (!box)
return;
foreach (var name in collection.UnusedSettings.Keys)
ImGui.TextUnformatted(name);
}
}
public sealed class CollectionSelector2 : ItemSelector<ModCollection>, IDisposable
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private ModCollection? _dragging;
public CollectionSelector2(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active)
: base(new List<ModCollection>(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{
_config = config;
_communicator = communicator;
_storage = storage;
_active = active;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
// Set items.
OnCollectionChange(CollectionType.Inactive, null, null, string.Empty);
// Set selection.
OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty);
}
protected override bool OnDelete(int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.RemoveCollection(Items[idx]);
}
protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current
? $"The selected collection {Current.Name} can not be deleted."
: $"Delete the currently selected collection {Current?.Name}. Hold {_config.DeleteModModifier} to delete.";
protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.AddCollection(name, Items[idx]);
}
protected override bool Filtered(int idx)
=> !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
protected override bool OnDraw(int idx)
{
using var color = ImRaii.PushColor(ImGuiCol.Header, Colors.SelectedColor);
var ret = ImGui.Selectable(Items[idx].Name, idx == CurrentIdx);
using var source = ImRaii.DragDropSource();
if (source)
{
_dragging = Items[idx];
ImGui.SetDragDropPayload("Assignment", nint.Zero, 0);
ImGui.TextUnformatted($"Assigning {_dragging.Name} to...");
}
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
return ret;
}
public void DragTarget(CollectionType type, ActorIdentifier identifier)
{
using var target = ImRaii.DragDropTarget();
if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping("Assignment"))
return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
_dragging = null;
}
public void Dispose()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3)
{
switch (type)
{
case CollectionType.Temporary: return;
case CollectionType.Current:
if (@new != null)
SetCurrent(@new);
SetFilterDirty();
return;
case CollectionType.Inactive:
Items.Clear();
foreach (var c in _storage.OrderBy(c => c.Name))
Items.Add(c);
if (old == Current)
ClearCurrentSelection();
else
TryRestoreCurrent();
SetFilterDirty();
return;
default:
SetFilterDirty();
return;
}
}
}
public class CollectionsTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly Configuration _configuration;
private readonly CollectionManager _collectionManager;
private readonly CollectionSelector2 _selector;
private readonly CollectionPanel _panel;
private readonly CollectionTree _tree;
private readonly Configuration _config;
private readonly CollectionSelector _selector;
private readonly CollectionPanel _panel;
private readonly TutorialService _tutorial;
public enum PanelMode
{
SimpleAssignment,
ComplexAssignment,
IndividualAssignment,
GroupAssignment,
Details,
};
public PanelMode Mode = PanelMode.SimpleAssignment;
public CollectionsTab(CommunicatorService communicator, Configuration configuration, CollectionManager collectionManager,
ModStorage modStorage, ActorService actors, TargetManager targets)
public PanelMode Mode
{
_communicator = communicator;
_configuration = configuration;
_collectionManager = collectionManager;
_selector = new CollectionSelector2(_configuration, _communicator, _collectionManager.Storage, _collectionManager.Active);
_panel = new CollectionPanel(_collectionManager, modStorage);
_tree = new CollectionTree(collectionManager, _selector, actors, targets);
get => _config.CollectionPanel;
set
{
_config.CollectionPanel = value;
_config.Save();
}
}
public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator,
CollectionManager collectionManager, ModStorage modStorage, ActorService actors, TargetManager targets, TutorialService tutorial)
{
_config = configuration;
_tutorial = tutorial;
_selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial);
_panel = new CollectionPanel(pi, configuration, communicator, collectionManager, _selector, actors, targets, modStorage);
}
public void Dispose()
{
_selector.Dispose();
_panel.Dispose();
}
public ReadOnlySpan<byte> Label
@ -535,41 +60,79 @@ public class CollectionsTab : IDisposable, ITab
public void DrawContent()
{
var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnn").X;
_selector.Draw(width);
var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnnnn").X;
using (var group = ImRaii.Group())
{
_selector.Draw(width);
}
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
ImGui.SameLine();
using var group = ImRaii.Group();
DrawHeaderLine();
DrawPanel();
using (var group = ImRaii.Group())
{
DrawHeaderLine();
DrawPanel();
}
}
public void DrawHeader()
{
_tutorial.OpenTutorial(BasicTutorialSteps.Collections);
}
private void DrawHeaderLine()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 3f, 0);
var withSpacing = ImGui.GetFrameHeightWithSpacing();
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - withSpacing) / 4f, ImGui.GetFrameHeight());
using var _ = ImRaii.Group();
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.SimpleAssignment);
if (ImGui.Button("Simple Assignments", buttonSize))
Mode = PanelMode.SimpleAssignment;
ImGui.SameLine();
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
ImGui.SameLine();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.IndividualAssignment);
if (ImGui.Button("Individual Assignments", buttonSize))
Mode = PanelMode.IndividualAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
ImGui.SameLine();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.GroupAssignment);
if (ImGui.Button("Group Assignments", buttonSize))
Mode = PanelMode.GroupAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
ImGui.SameLine();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.Details);
if (ImGui.Button("Collection Details", buttonSize))
Mode = PanelMode.Details;
ImGui.SameLine();
color.Pop();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.ComplexAssignment);
if (ImGui.Button("Advanced Assignments", buttonSize))
Mode = PanelMode.ComplexAssignment;
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
ImGui.SameLine();
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value(_config))
.Push(ImGuiCol.Border, ColorId.FolderExpanded.Value(_config));
if (ImGuiUtil.DrawDisabledButton(
$"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode",
buttonSize with { X = withSpacing }, string.Empty, false, true))
_selector.IncognitoMode = !_selector.IncognitoMode;
var hovered = ImGui.IsItemHovered();
_tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
color.Pop(2);
if (hovered)
ImGui.SetTooltip(_selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on.");
}
private void DrawPanel()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
using var child = ImRaii.Child("##CollectionSettings", new Vector2(-1, 0), true, ImGuiWindowFlags.HorizontalScrollbar);
using var child = ImRaii.Child("##CollectionSettings", new Vector2(ImGui.GetContentRegionAvail().X, 0), true);
if (!child)
return;
@ -577,13 +140,16 @@ public class CollectionsTab : IDisposable, ITab
switch (Mode)
{
case PanelMode.SimpleAssignment:
_tree.DrawSimple();
_panel.DrawSimple();
break;
case PanelMode.ComplexAssignment:
_tree.DrawAdvanced();
case PanelMode.IndividualAssignment:
_panel.DrawIndividualPanel();
break;
case PanelMode.GroupAssignment:
_panel.DrawGroupPanel();
break;
case PanelMode.Details:
_panel.Draw();
_panel.DrawDetailsPanel();
break;
}

View file

@ -1,299 +0,0 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public class CollectionsTabOld : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial;
private readonly SpecialCombo _specialCollectionCombo;
private readonly CollectionCombo _collectionsWithEmpty;
private readonly CollectionCombo _collectionCombo;
private readonly InheritanceUi _inheritance;
private readonly IndividualCollectionUi _individualCollections;
public CollectionsTabOld(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
TutorialService tutorial, Configuration config)
{
_communicator = communicator;
_collectionManager = collectionManager;
_tutorial = tutorial;
_config = config;
_specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350);
_collectionsWithEmpty = new CollectionCombo(_collectionManager,
() => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList());
_collectionCombo = new CollectionCombo(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList());
_inheritance = new InheritanceUi(_collectionManager);
_individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty);
_communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers);
}
public ReadOnlySpan<byte> Label
=> "Collections"u8;
/// <summary> Draw a collection selector of a certain width for a certain type. </summary>
public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty)
=> (withEmpty ? _collectionsWithEmpty : _collectionCombo).Draw(label, width, collectionType);
public void Dispose()
=> _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers);
/// <summary> Draw a tutorial step regardless of tab selection. </summary>
public void DrawHeader()
=> _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
public void DrawContent()
{
using var child = ImRaii.Child("##collections", -Vector2.One);
if (child)
{
DrawActiveCollectionSelectors();
DrawMainSelectors();
}
}
#region New Collections
// Input text fields.
private string _newCollectionName = string.Empty;
private bool _canAddCollection;
/// <summary>
/// Create a new collection that is either empty or a duplicate of the current collection.
/// Resets the new collection name.
/// </summary>
private void CreateNewCollection(bool duplicate)
{
if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null))
_newCollectionName = string.Empty;
}
/// <summary> Draw the Clean Unused Settings button if there are any. </summary>
private void DrawCleanCollectionButton(Vector2 width)
{
if (_collectionManager.Active.Current.UnusedSettings.Count == 0)
return;
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(
$"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width
, "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk."
, false))
_collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current);
}
/// <summary> Draw the new collection input as well as its buttons. </summary>
private void DrawNewCollectionInput(Vector2 width)
{
// Input for new collection name. Also checks for validity when changed.
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64))
_canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _);
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n"
+ "You can use multiple collections to quickly switch between sets of enabled mods.");
// Creation buttons.
var tt = _canAddCollection
? string.Empty
: "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection.";
if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection))
CreateNewCollection(false);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection))
CreateNewCollection(true);
}
#endregion
#region Collection Selection
/// <summary> Draw all collection assignment selections. </summary>
private void DrawActiveCollectionSelectors()
{
UiHelpers.DefaultLineSpace();
var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen);
_tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections);
if (!open)
return;
UiHelpers.DefaultLineSpace();
DrawDefaultCollectionSelector();
_tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection);
DrawInterfaceCollectionSelector();
_tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection);
UiHelpers.DefaultLineSpace();
DrawSpecialAssignments();
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1);
UiHelpers.DefaultLineSpace();
_individualCollections.Draw();
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2);
UiHelpers.DefaultLineSpace();
}
private void DrawCurrentCollectionSelector(Vector2 width)
{
using var group = ImRaii.Group();
DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection,
"This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything.");
// Deletion conditions.
var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName;
var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive();
var tt = deleteCondition
? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection."
: $"You can not delete the collection {ModCollection.DefaultCollectionName}.";
if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld))
_collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current);
DrawCleanCollectionButton(width);
}
/// <summary> Draw the selector for the default collection assignment. </summary>
private void DrawDefaultCollectionSelector()
{
using var group = ImRaii.Group();
DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection,
$"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game,"
+ "as well as any character for whom no more specific conditions from below apply.");
}
/// <summary> Draw the selector for the interface collection assignment. </summary>
private void DrawInterfaceCollectionSelector()
{
using var group = ImRaii.Group();
DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection,
$"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves.");
}
/// <summary> Description for character groups used in multiple help markers. </summary>
private const string CharacterGroupDescription =
$"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n"
+ $"All of them take precedence before the {TutorialService.DefaultCollection},\n"
+ $"but all {TutorialService.IndividualAssignments} take precedence before them.";
/// <summary> Draw the entire group assignment section. </summary>
private void DrawSpecialAssignments()
{
using var _ = ImRaii.Group();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(TutorialService.CharacterGroups);
ImGuiComponents.HelpMarker(CharacterGroupDescription);
ImGui.Separator();
DrawSpecialCollections();
ImGui.Dummy(Vector2.Zero);
DrawNewSpecialCollection();
}
/// <summary> Draw a new combo to select special collections as well as button to create it. </summary>
private void DrawNewSpecialCollection()
{
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (_specialCollectionCombo.CurrentIdx == -1
|| _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null)
{
_specialCollectionCombo.ResetFilter();
_specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special
.IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null);
}
if (_specialCollectionCombo.CurrentType == null)
return;
_specialCollectionCombo.Draw();
ImGui.SameLine();
var disabled = _specialCollectionCombo.CurrentType == null;
var tt = disabled
? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n"
+ CharacterGroupDescription
: CharacterGroupDescription;
if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled))
return;
_collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1);
_specialCollectionCombo.CurrentIdx = -1;
}
#endregion
#region Current Collection Editing
/// <summary> Draw the current collection selection, the creation of new collections and the inheritance block. </summary>
private void DrawMainSelectors()
{
UiHelpers.DefaultLineSpace();
var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen);
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
if (!open)
return;
var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
UiHelpers.DefaultLineSpace();
DrawCurrentCollectionSelector(width);
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
UiHelpers.DefaultLineSpace();
DrawNewCollectionInput(width);
UiHelpers.DefaultLineSpace();
_inheritance.Draw();
_tutorial.OpenTutorial(BasicTutorialSteps.Inheritance);
}
/// <summary> Draw all currently set special collections. </summary>
private void DrawSpecialCollections()
{
foreach (var (type, name, desc) in CollectionTypeExtensions.Special)
{
var collection = _collectionManager.Active.ByType(type);
if (collection == null)
continue;
using var id = ImRaii.PushId((int)type);
DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty,
false, true))
{
_collectionManager.Active.RemoveSpecialCollection(type);
_specialCollectionCombo.ResetFilter();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGuiUtil.LabeledHelpMarker(name, desc);
}
}
#endregion
}

View file

@ -37,8 +37,8 @@ public class ConfigTabBar
Tabs = new ITab[]
{
Settings,
Mods,
Collections,
Mods,
ChangedItems,
Effective,
OnScreenTab,

View file

@ -172,7 +172,7 @@ public class ModsTab : ITab
ImGui.SameLine();
DrawInheritedCollectionButton(3 * buttonSize);
ImGui.SameLine();
_collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, CollectionType.Current);
_collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, ColorId.SelectedCollection.Value(_config));
}
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);

View file

@ -60,7 +60,6 @@ public class SettingsTab : ITab
_tutorial.OpenTutorial(BasicTutorialSteps.Fin);
_tutorial.OpenTutorial(BasicTutorialSteps.Faq1);
_tutorial.OpenTutorial(BasicTutorialSteps.Faq2);
_tutorial.OpenTutorial(BasicTutorialSteps.Faq3);
}
public void DrawContent()
@ -633,7 +632,7 @@ public class SettingsTab : ITab
private void DrawAdvancedSettings()
{
var header = ImGui.CollapsingHeader("Advanced");
_tutorial.OpenTutorial(BasicTutorialSteps.AdvancedSettings);
_tutorial.OpenTutorial(BasicTutorialSteps.Deprecated1);
if (!header)
return;

View file

@ -2,6 +2,7 @@ using System;
using System.Runtime.CompilerServices;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
@ -12,17 +13,17 @@ public enum BasicTutorialSteps
GeneralTooltips,
ModDirectory,
EnableMods,
AdvancedSettings,
Deprecated1,
GeneralSettings,
Collections,
EditingCollections,
CurrentCollection,
Inheritance,
ActiveCollections,
DefaultCollection,
InterfaceCollection,
SpecialCollections1,
SpecialCollections2,
SimpleAssignments,
IndividualAssignments,
GroupAssignments,
CollectionDetails,
Incognito,
Deprecated2,
Mods,
ModImport,
AdvancedHelp,
@ -33,9 +34,9 @@ public enum BasicTutorialSteps
Priority,
ModOptions,
Fin,
Deprecated3,
Faq1,
Faq2,
Faq3,
Favorites,
Tags,
}
@ -49,9 +50,6 @@ public class TutorialService
public const string ActiveCollections = "Active Collections";
public const string AssignedCollections = "Assigned Collections";
public const string GroupAssignment = "Group Assignment";
public const string CharacterGroups = "Character Groups";
public const string ConditionalGroup = "Group";
public const string ConditionalIndividual = "Character";
public const string IndividualAssignments = "Individual Assignments";
public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n"
@ -86,35 +84,26 @@ public class TutorialService
.Register("Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n"
+ "This is our next stop!\n\n"
+ "Go here after setting up your root folder to continue the tutorial!")
.Register("Initial Setup, Step 4: Editing Collections", "First, we need to open the Collection Settings.\n\n"
+ "In here, we can create new collections, delete collections, or make them inherit from each other.")
.Register("Initial Setup, Step 4: Managing Collections", "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n"
+ $"There will always be one collection called {ModCollection.DefaultCollectionName} that can not be deleted.")
.Register($"Initial Setup, Step 5: {SelectedCollection}",
$"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection."
+ $"We should already have a collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n")
.Register("Inheritance",
"This is a more advanced feature. Click the help button for more information, but we will ignore this for now.")
.Register($"Initial Setup, Step 6: {ActiveCollections}",
$"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n"
+ "Any collection assigned here will apply to the game under certain conditions.\n\n"
+ $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n"
+ "Open this now to continue.")
.Register($"Initial Setup, Step 7: {DefaultCollection}",
$"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollectionName} - is the main one.\n\n"
+ $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n"
+ "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.")
.Register("Interface Collection",
$"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n"
+ $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.")
.Register(GroupAssignment + 's',
"Collections assigned here are used for groups of characters for which specific conditions are met.\n\n"
+ "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n"
+ $"{IndividualAssignments} always take precedence before groups.")
.Register(IndividualAssignments,
"Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n"
+ "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n")
.Register("Initial Setup, Step 8: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n"
$"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n"
+ $"We should already have the collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n")
.Register("Initial Setup, Step 6: Simple Assignments", "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n"
+ "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n"
+ $"If you are just starting, you can see that the {ModCollection.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n"
+ "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.")
.Register("Individual Assignments", "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.")
.Register("Group Assignments", "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age.")
.Register("Collection Details", "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n"
+ "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself.")
.Register("Incognito Mode", "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n"
+ "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n"
+ "It is strongly recommended to not show your characters name in public screenshots when using Penumbra.")
.Deprecated()
.Register("Initial Setup, Step 7: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n"
+ $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking.")
.Register("Initial Setup, Step 9: Mod Import",
.Register("Initial Setup, Step 8: Mod Import",
"Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n"
+ "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n"
+ "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") // TODO
@ -129,10 +118,10 @@ public class TutorialService
"Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n"
+ "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n"
+ "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too.")
.Register("Initial Setup, Step 11: Enabling Mods",
.Register("Initial Setup, Step 9: Enabling Mods",
"Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n"
+ "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance.")
.Register("Initial Setup, Step 12: Priority",
.Register("Initial Setup, Step 10: Priority",
"If two enabled mods in one collection change the same files, there is a conflict.\n\n"
+ "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n"
+ "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible.")
@ -140,10 +129,10 @@ public class TutorialService
+ "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately.")
.Register("Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n"
+ "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page.")
.Register("FAQ 1", "Penumbra can not easily change which items a mod applies to.")
.Register("FAQ 2",
.Deprecated()
.Register("FAQ 1",
"It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices.")
.Register("FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing.")
.Register("FAQ 2", "Penumbra can change the skin material a mod uses. This is under advanced editing.")
.Register("Favorites",
"You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections.")
.Register("Tags",