diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 2a0ab7e..7ceea6a 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -1,4 +1,5 @@ using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.GameData; using Glamourer.Interop.Structs; using Newtonsoft.Json.Linq; @@ -9,8 +10,6 @@ namespace Glamourer.Automation; public class AutoDesign { - public const string RevertName = "Revert"; - public IDesignStandIn Design = new RevertDesign(); public JobGroup Jobs; public ApplicationType Type; diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 332f9ee..5cd9cf5 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -131,6 +131,7 @@ public sealed class AutoDesignApplier : IDisposable case AutomationChanged.Type.ChangedDesign: case AutomationChanged.Type.ChangedConditions: case AutomationChanged.Type.ChangedType: + case AutomationChanged.Type.ChangedData: ApplyNew(set); break; } diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 710336e..c1e361d 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Services; @@ -347,6 +348,20 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, applicationType)); } + public void ChangeData(AutoDesignSet set, int which, object data) + { + if (which >= set.Designs.Count || which < 0) + return; + + var design = set.Designs[which]; + if (!design.Design.ChangeData(data)) + return; + + Save(); + Glamourer.Log.Debug($"Changed additional design data for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedData, set, (which, data)); + } + public string ToFilename(FilenameService fileNames) => fileNames.AutomationFile; @@ -499,6 +514,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos design = d; } + design.ParseData(jObj); // ApplicationType is a migration from an older property name. diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index e01c09e..123f15a 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -81,6 +81,9 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn public void ParseData(JObject _) { } + public bool ChangeData(object data) + => false; + #endregion #region Serialization diff --git a/Glamourer/Designs/IDesignStandIn.cs b/Glamourer/Designs/IDesignStandIn.cs index 33122e2..a6ee702 100644 --- a/Glamourer/Designs/IDesignStandIn.cs +++ b/Glamourer/Designs/IDesignStandIn.cs @@ -20,4 +20,6 @@ public interface IDesignStandIn : IEquatable public void AddData(JObject jObj); public void ParseData(JObject jObj); + + public bool ChangeData(object data); } diff --git a/Glamourer/Designs/RandomDesignGenerator.cs b/Glamourer/Designs/RandomDesignGenerator.cs deleted file mode 100644 index 4b8f13d..0000000 --- a/Glamourer/Designs/RandomDesignGenerator.cs +++ /dev/null @@ -1,91 +0,0 @@ -using OtterGui; -using OtterGui.Services; -using System; - -namespace Glamourer.Designs; - -public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem) : IService -{ - private readonly Random _rng = new(); - - public Design? Design(IReadOnlyList localDesigns) - { - if (localDesigns.Count == 0) - return null; - - var idx = _rng.Next(0, localDesigns.Count - 1); - Glamourer.Log.Verbose($"[Random Design] Chose design {idx} out of {localDesigns.Count}: {localDesigns[idx].Incognito}."); - return localDesigns[idx]; - } - - public Design? Design() - => Design(designs); - - public Design? Design(string restrictions) - { - if (restrictions.Length == 0) - return Design(designs); - - List> predicates = []; - - switch (restrictions[0]) - { - case '{': - var end = restrictions.IndexOf('}'); - if (end == -1) - throw new ArgumentException($"The restriction group '{restrictions}' is not properly terminated."); - - restrictions = restrictions[1..end]; - var split = restrictions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var item in split.Distinct()) - predicates.Add(item[0] == '/' ? CreatePredicateSlash(item) : CreatePredicate(item)); - break; - case '/': - predicates.Add(CreatePredicateSlash(restrictions)); - break; - default: - predicates.Add(CreatePredicate(restrictions)); - break; - } - - if (predicates.Count == 1) - { - var p = predicates[0]; - return Design(designs.Select(Transform).Where(t => p(t.NameLower, t.Identifier, t.PathLower)).Select(t => t.Design).ToList()); - } - - return Design(designs.Select(Transform).Where(t => predicates.Any(p => p(t.NameLower, t.Identifier, t.PathLower))).Select(t => t.Design) - .ToList()); - - (Design Design, string NameLower, string Identifier, string PathLower) Transform(Design design) - { - var name = design.Name.Lower; - var identifier = design.Identifier.ToString(); - var path = fileSystem.FindLeaf(design, out var leaf) ? leaf.FullName().ToLowerInvariant() : string.Empty; - return (design, name, identifier, path); - } - - Func CreatePredicate(string input) - { - var value = input.ToLowerInvariant(); - return (string nameLower, string identifier, string pathLower) => - { - if (nameLower.Contains(value)) - return true; - if (identifier.Contains(value)) - return true; - if (pathLower.Contains(value)) - return true; - - return false; - }; - } - - Func CreatePredicateSlash(string input) - { - var value = input[1..].ToLowerInvariant(); - return (string nameLower, string identifier, string pathLower) - => pathLower.StartsWith(value); - } - } -} diff --git a/Glamourer/Designs/RandomDesign.cs b/Glamourer/Designs/Special/RandomDesign.cs similarity index 61% rename from Glamourer/Designs/RandomDesign.cs rename to Glamourer/Designs/Special/RandomDesign.cs index 3b9ba2a..b40ba92 100644 --- a/Glamourer/Designs/RandomDesign.cs +++ b/Glamourer/Designs/Special/RandomDesign.cs @@ -3,7 +3,7 @@ using Glamourer.Interop.Material; using Glamourer.State; using Newtonsoft.Json.Linq; -namespace Glamourer.Designs; +namespace Glamourer.Designs.Special; public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn { @@ -11,14 +11,14 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn public const string ResolvedName = "Random"; private Design? _currentDesign; - public string Restrictions { get; internal set; } = string.Empty; + public IReadOnlyList Predicates { get; private set; } = []; public string ResolveName(bool _) => ResolvedName; public ref readonly DesignData GetDesignData(in DesignData baseRef) { - _currentDesign ??= rng.Design(Restrictions); + _currentDesign ??= rng.Design(Predicates); if (_currentDesign == null) return ref baseRef; @@ -27,7 +27,7 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() { - _currentDesign ??= rng.Design(Restrictions); + _currentDesign ??= rng.Design(Predicates); if (_currentDesign == null) return []; @@ -38,7 +38,9 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn => SerializedName; public bool Equals(IDesignStandIn? other) - => other is RandomDesign r && string.Equals(r.Restrictions, Restrictions, StringComparison.OrdinalIgnoreCase); + => other is RandomDesign r + && string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates), + StringComparison.OrdinalIgnoreCase); public StateSource AssociatedSource() => StateSource.Manual; @@ -47,7 +49,7 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn { get { - _currentDesign = rng.Design(Restrictions); + _currentDesign = rng.Design(Predicates); if (_currentDesign == null) yield break; @@ -58,12 +60,21 @@ public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn public void AddData(JObject jObj) { - jObj["Restrictions"] = Restrictions; + jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates); } public void ParseData(JObject jObj) { var restrictions = jObj["Restrictions"]?.ToObject() ?? string.Empty; - Restrictions = restrictions; + Predicates = RandomPredicate.GeneratePredicates(restrictions); + } + + public bool ChangeData(object data) + { + if (data is not List predicates) + return false; + + Predicates = predicates; + return true; } } diff --git a/Glamourer/Designs/Special/RandomDesignGenerator.cs b/Glamourer/Designs/Special/RandomDesignGenerator.cs new file mode 100644 index 0000000..8b2e050 --- /dev/null +++ b/Glamourer/Designs/Special/RandomDesignGenerator.cs @@ -0,0 +1,37 @@ +using OtterGui.Services; + +namespace Glamourer.Designs.Special; + +public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem) : IService +{ + private readonly Random _rng = new(); + + public Design? Design(IReadOnlyList localDesigns) + { + if (localDesigns.Count == 0) + return null; + + var idx = _rng.Next(0, localDesigns.Count - 1); + Glamourer.Log.Verbose($"[Random Design] Chose design {idx} out of {localDesigns.Count}: {localDesigns[idx].Incognito}."); + return localDesigns[idx]; + } + + public Design? Design() + => Design(designs); + + public Design? Design(IDesignPredicate predicate) + => Design(predicate.Get(designs, fileSystem).ToList()); + + public Design? Design(IReadOnlyList predicates) + { + if (predicates.Count == 0) + return Design(); + if (predicates.Count == 1) + return Design(predicates[0]); + + return Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()); + } + + public Design? Design(string restrictions) + => Design(RandomPredicate.GeneratePredicates(restrictions)); +} diff --git a/Glamourer/Designs/Special/RandomPredicate.cs b/Glamourer/Designs/Special/RandomPredicate.cs new file mode 100644 index 0000000..efb3233 --- /dev/null +++ b/Glamourer/Designs/Special/RandomPredicate.cs @@ -0,0 +1,163 @@ +using OtterGui.Classes; + +namespace Glamourer.Designs.Special; + +public interface IDesignPredicate +{ + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath); + + public bool Invoke((Design Design, string LowerName, string Identifier, string LowerPath) args) + => Invoke(args.Design, args.LowerName, args.Identifier, args.LowerPath); + + public IEnumerable Get(IEnumerable designs, DesignFileSystem fileSystem) + => designs.Select(d => Transform(d, fileSystem)) + .Where(Invoke) + .Select(t => t.Design); + + public static IEnumerable Get(IReadOnlyList predicates, IEnumerable designs, DesignFileSystem fileSystem) + => predicates.Count > 0 + ? designs.Select(d => Transform(d, fileSystem)) + .Where(t => predicates.Any(p => p.Invoke(t))) + .Select(t => t.Design) + : designs; + + private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs) + => (d, d.Name.Lower, d.Identifier.ToString(), fs.FindLeaf(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty); +} + +public static class RandomPredicate +{ + public readonly struct StartsWith(string value) : IDesignPredicate + { + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + => lowerPath.StartsWith(Value.Lower); + + public override string ToString() + => $"/{Value.Text}"; + } + + public readonly struct Contains(string value) : IDesignPredicate + { + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + { + if (lowerName.Contains(Value.Lower)) + return true; + if (identifier.Contains(Value.Lower)) + return true; + if (lowerPath.Contains(Value.Lower)) + return true; + + return false; + } + + public override string ToString() + => Value.Text; + } + + public readonly struct Exact(Exact.Type type, string value) : IDesignPredicate + { + public enum Type : byte + { + Name, + Path, + Identifier, + Tag, + Color, + } + + public Type Which { get; } = type; + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + => Which switch + { + Type.Name => lowerName == Value.Lower, + Type.Path => lowerPath == Value.Lower, + Type.Identifier => identifier == Value.Lower, + Type.Tag => IsContained(Value, design.Tags), + Type.Color => design.Color == Value, + _ => false, + }; + + private static bool IsContained(LowerString value, IEnumerable data) + => data.Any(t => t == value); + + public override string ToString() + => $"\"{Which switch { Type.Name => 'n', Type.Identifier => 'i', Type.Path => 'p', Type.Tag => 't', Type.Color => 'c', _ => '?' }}?{Value.Text}\""; + } + + public static IDesignPredicate CreateSinglePredicate(string restriction) + { + switch (restriction[0]) + { + case '/': return new StartsWith(restriction[1..]); + case '"': + var end = restriction.IndexOf('"', 1); + if (end < 3) + return new Contains(restriction); + + switch (restriction[1], restriction[2]) + { + case ('n', '?'): + case ('N', '?'): + return new Exact(Exact.Type.Name, restriction[3..end]); + case ('p', '?'): + case ('P', '?'): + return new Exact(Exact.Type.Path, restriction[3..end]); + case ('i', '?'): + case ('I', '?'): + return new Exact(Exact.Type.Identifier, restriction[3..end]); + case ('t', '?'): + case ('T', '?'): + return new Exact(Exact.Type.Tag, restriction[3..end]); + case ('c', '?'): + case ('C', '?'): + return new Exact(Exact.Type.Color, restriction[3..end]); + default: return new Contains(restriction); + } + default: return new Contains(restriction); + } + } + + public static List GeneratePredicates(string restrictions) + { + if (restrictions.Length == 0) + return []; + + List predicates = new(1); + if (restrictions[0] is '{') + { + var end = restrictions.IndexOf('}'); + if (end == -1) + { + predicates.Add(CreateSinglePredicate(restrictions)); + } + else + { + restrictions = restrictions[1..end]; + var split = restrictions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + predicates.AddRange(split.Distinct().Select(CreateSinglePredicate)); + } + } + else + { + predicates.Add(CreateSinglePredicate(restrictions)); + } + + return predicates; + } + + public static string GeneratePredicateString(IReadOnlyCollection predicates) + { + if (predicates.Count == 0) + return string.Empty; + if (predicates.Count == 1) + return predicates.First()!.ToString()!; + + return $"{{{string.Join("; ", predicates)}}}"; + } +} diff --git a/Glamourer/Designs/RevertDesign.cs b/Glamourer/Designs/Special/RevertDesign.cs similarity index 87% rename from Glamourer/Designs/RevertDesign.cs rename to Glamourer/Designs/Special/RevertDesign.cs index 28a490e..0f0207b 100644 --- a/Glamourer/Designs/RevertDesign.cs +++ b/Glamourer/Designs/Special/RevertDesign.cs @@ -3,7 +3,7 @@ using Glamourer.Interop.Material; using Glamourer.State; using Newtonsoft.Json.Linq; -namespace Glamourer.Designs; +namespace Glamourer.Designs.Special; public class RevertDesign : IDesignStandIn { @@ -38,4 +38,7 @@ public class RevertDesign : IDesignStandIn public void ParseData(JObject jObj) { } + + public bool ChangeData(object data) + => false; } diff --git a/Glamourer/Events/AutomationChanged.cs b/Glamourer/Events/AutomationChanged.cs index 1d45084..26f799a 100644 --- a/Glamourer/Events/AutomationChanged.cs +++ b/Glamourer/Events/AutomationChanged.cs @@ -46,7 +46,7 @@ public sealed class AutomationChanged() /// Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. MovedDesign, - /// Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. + /// Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, IDesignStandIn, IDesignStandIn)]. ChangedDesign, /// Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. @@ -54,6 +54,9 @@ public sealed class AutomationChanged() /// Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. ChangedType, + + /// Change the additional data for a specific design type. Additional data is the index of the changed associated design and the new data. [(int, object)] + ChangedData, } public enum Priority @@ -62,6 +65,9 @@ public sealed class AutomationChanged() SetSelector = 0, /// - AutoDesignApplier, + AutoDesignApplier = 0, + + /// + RandomRestrictionDrawer = -1, } } diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index f497f79..a4bfadb 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility.Raii; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.Events; using ImGuiNET; using OtterGui; @@ -207,6 +208,42 @@ public sealed class LinkDesignCombo( .OrderBy(d => d.Item2), ]); +public sealed class RandomDesignCombo( + DesignManager designs, + DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : DesignCombo(log, designChanged, tabSelected, config, designColors, () => + [ + .. designs.Designs + .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .OrderBy(d => d.Item2), + ]) +{ + private Design? GetDesign(RandomPredicate.Exact exact) + { + return exact.Which switch + { + RandomPredicate.Exact.Type.Name => designs.Designs.FirstOrDefault(d => d.Name == exact.Value), + RandomPredicate.Exact.Type.Path => fileSystem.Find(exact.Value.Text, out var c) && c is DesignFileSystem.Leaf l ? l.Value : null, + RandomPredicate.Exact.Type.Identifier => designs.Designs.ByIdentifier(Guid.TryParse(exact.Value.Text, out var g) ? g : Guid.Empty), + _ => null, + }; + } + + public bool Draw(RandomPredicate.Exact exact, float width) + { + var design = GetDesign(exact); + return Draw(design, design?.ResolveName(Incognito) ?? $"Not Found [{exact.Value.Text}]", width); + } + + public bool Draw(IDesignStandIn? design, float width) + => Draw(design, design?.ResolveName(Incognito) ?? string.Empty, width); +} + public sealed class SpecialDesignCombo( DesignManager designs, DesignFileSystem fileSystem, diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs index f863e1f..36e4320 100644 --- a/Glamourer/Gui/Materials/AdvancedDyePopup.cs +++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs @@ -158,16 +158,14 @@ public sealed unsafe class AdvancedDyePopup( if (config.KeepAdvancedDyesAttached) { var position = ImGui.GetWindowPos(); - position.X += ImGui.GetWindowSize().X; + position.X += ImGui.GetWindowSize().X + ImGui.GetStyle().WindowPadding.X; ImGui.SetNextWindowPos(position); flags |= ImGuiWindowFlags.NoMove; } - var size = new Vector2(7 * ImGui.GetFrameHeight() + 3 * ImGui.GetStyle().ItemInnerSpacing.X + 300 * ImGuiHelpers.GlobalScale, - 18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + ImGui.GetStyle().ItemSpacing.Y); + 18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 2 * ImGui.GetStyle().ItemSpacing.Y); ImGui.SetNextWindowSize(size); - var window = ImGui.Begin("###Glamourer Advanced Dyes", flags); try { diff --git a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs new file mode 100644 index 0000000..f125f36 --- /dev/null +++ b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs @@ -0,0 +1,432 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Designs.Special; +using Glamourer.Events; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; + +namespace Glamourer.Gui.Tabs.AutomationTab; + +public sealed class RandomRestrictionDrawer : IService, IDisposable +{ + private AutoDesignSet? _set; + private int _designIndex = -1; + + private readonly AutomationChanged _automationChanged; + private readonly Configuration _config; + private readonly AutoDesignManager _autoDesignManager; + private readonly RandomDesignCombo _randomDesignCombo; + private readonly SetSelector _selector; + private readonly DesignStorage _designs; + private readonly DesignFileSystem _designFileSystem; + + private string _newText = string.Empty; + private string? _newDefinition; + private Design? _newDesign = null; + + public RandomRestrictionDrawer(AutomationChanged automationChanged, Configuration config, AutoDesignManager autoDesignManager, + RandomDesignCombo randomDesignCombo, SetSelector selector, DesignFileSystem designFileSystem, DesignStorage designs) + { + _automationChanged = automationChanged; + _config = config; + _autoDesignManager = autoDesignManager; + _randomDesignCombo = randomDesignCombo; + _selector = selector; + _designFileSystem = designFileSystem; + _designs = designs; + _automationChanged.Subscribe(OnAutomationChange, AutomationChanged.Priority.RandomRestrictionDrawer); + } + + public void Dispose() + { + _automationChanged.Unsubscribe(OnAutomationChange); + } + + public void DrawButton(AutoDesignSet set, int designIndex) + { + var isOpen = set == _set && designIndex == _designIndex; + using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), isOpen) + .Push(ImGuiCol.Text, ColorId.HeaderButtons.Value(), isOpen) + .Push(ImGuiCol.Border, ColorId.HeaderButtons.Value(), isOpen)) + { + using var frame = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, isOpen); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + string.Empty, false, true)) + { + if (isOpen) + Close(); + else + Open(set, designIndex); + } + } + + ImGuiUtil.HoverTooltip("Edit restrictions for this random design."); + } + + private void Open(AutoDesignSet set, int designIndex) + { + if (designIndex < 0 || designIndex >= set.Designs.Count) + return; + + var design = set.Designs[designIndex]; + if (design.Design is not RandomDesign) + return; + + _set = set; + _designIndex = designIndex; + } + + private void Close() + { + _set = null; + _designIndex = -1; + } + + public void Draw() + { + if (_set == null || _designIndex < 0 || _designIndex >= _set.Designs.Count) + return; + + if (_set != _selector.Selection) + { + Close(); + return; + } + + var design = _set.Designs[_designIndex]; + if (design.Design is not RandomDesign random) + return; + + DrawWindow(random); + } + + private void DrawWindow(RandomDesign random) + { + var flags = ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoCollapse + | ImGuiWindowFlags.NoResize; + + // Set position to the right of the main window when attached + // The downwards offset is implicit through child position. + if (_config.KeepAdvancedDyesAttached) + { + var position = ImGui.GetWindowPos(); + position.X += ImGui.GetWindowSize().X + ImGui.GetStyle().WindowPadding.X; + ImGui.SetNextWindowPos(position); + flags |= ImGuiWindowFlags.NoMove; + } + + using var color = ImRaii.PushColor(ImGuiCol.TitleBgActive, ImGui.GetColorU32(ImGuiCol.TitleBg)); + + var size = new Vector2(7 * ImGui.GetFrameHeight() + 3 * ImGui.GetStyle().ItemInnerSpacing.X + 300 * ImGuiHelpers.GlobalScale, + 18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + ImGui.GetStyle().ItemSpacing.Y); + ImGui.SetNextWindowSize(size); + + var open = true; + var window = ImGui.Begin($"{_set!.Name} #{_designIndex + 1:D2}###Glamourer Random Design", ref open, flags); + try + { + if (window) + DrawContent(random); + } + finally + { + ImGui.End(); + } + + if (!open) + Close(); + } + + private void DrawTable(RandomDesign random, List list) + { + using var table = ImRaii.Table("##table", 3); + if (!table) + return; + + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + var descWidth = ImGui.CalcTextSize("or that are set to the color").X; + ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, descWidth); + ImGui.TableSetupColumn("input", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("del", ImGuiTableColumnFlags.WidthFixed, buttonSize.X * 2 + ImGui.GetStyle().ItemInnerSpacing.X); + + var orSize = ImGui.CalcTextSize("or "); + for (var i = 0; i < random.Predicates.Count; ++i) + { + using var id = ImRaii.PushId(i); + var predicate = random.Predicates[i]; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + if (i != 0) + ImGui.TextUnformatted("or "); + else + ImGui.Dummy(orSize); + ImGui.SameLine(0, 0); + ImGui.AlignTextToFramePadding(); + switch (predicate) + { + case RandomPredicate.Contains contains: + { + ImGui.TextUnformatted("that contain"); + ImGui.TableNextColumn(); + var data = contains.Value.Text; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##match", "Name, Path, or Identifier Contains...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Contains(data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.StartsWith startsWith: + { + ImGui.TextUnformatted("whose path starts with"); + ImGui.TableNextColumn(); + var data = startsWith.Value.Text; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##startsWith", "Path Starts With...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.StartsWith(data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact { Which: RandomPredicate.Exact.Type.Tag } exact: + { + ImGui.TextUnformatted("that contain the tag"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var data = exact.Value.Text; + if (ImGui.InputTextWithHint("##color", "Contained tag...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Tag, data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact { Which: RandomPredicate.Exact.Type.Color } exact: + { + ImGui.TextUnformatted("that are set to the color"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var data = exact.Value.Text; + if (ImGui.InputTextWithHint("##color", "Assigned Color is...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Color, data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact exact: + { + ImGui.TextUnformatted("that are exactly"); + ImGui.TableNextColumn(); + if (_randomDesignCombo.Draw(exact, ImGui.GetContentRegionAvail().X) && _randomDesignCombo.Design is Design d) + { + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, d.Identifier.ToString()); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + } + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this restriction.", false, true)) + { + list.RemoveAt(i); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + ImGui.SameLine(); + DrawLookup(predicate, buttonSize); + } + } + + private void DrawLookup(IDesignPredicate predicate, Vector2 buttonSize) + { + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.MagnifyingGlassChart.ToIconString(), buttonSize, string.Empty, false, true); + if (!ImGui.IsItemHovered()) + return; + + var designs = predicate.Get(_designs, _designFileSystem); + LookupTooltip(designs); + } + + private void LookupTooltip(IEnumerable designs) + { + using var _ = ImRaii.Tooltip(); + var tt = string.Join('\n', designs.Select(d => _designFileSystem.FindLeaf(d, out var l) ? l.FullName() : d.Name.Text).OrderBy(t => t)); + ImGui.TextUnformatted(tt.Length == 0 + ? "Matches no currently existing designs." + : "Matches the following designs:"); + ImGui.Separator(); + ImGui.TextUnformatted(tt); + } + + private void DrawNewButtons(List list) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##newText", "Add New Restriction...", ref _newText, 128); + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var invalid = _newText.Length == 0; + + var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - 3 * spacing) / 4, 0); + var changed = ImGuiUtil.DrawDisabledButton("Starts With", buttonSize, + "Add a new condition that design paths must start with the given text.", invalid) + && Add(new RandomPredicate.StartsWith(_newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Contains", buttonSize, + "Add a new condition that design paths, names or identifiers must contain the given text.", invalid) + && Add(new RandomPredicate.Contains(_newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Has Tag", buttonSize, + "Add a new condition that the design must contain the given tag.", invalid) + && Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Tag, _newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Assigned Color", buttonSize, + "Add a new condition that the design must be assigned to the given color.", invalid) + && Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Color, _newText)); + + if (_randomDesignCombo.Draw(_newDesign, ImGui.GetContentRegionAvail().X - spacing - buttonSize.X)) + _newDesign = _randomDesignCombo.CurrentSelection?.Item1 as Design; + ImGui.SameLine(0, spacing); + if (ImGuiUtil.DrawDisabledButton("Exact Design", buttonSize, "Add a single, specific design.", _newDesign == null)) + { + Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, _newDesign!.Identifier.ToString())); + changed = true; + _newDesign = null; + } + + if (changed) + _autoDesignManager.ChangeData(_set!, _designIndex, list); + + return; + + bool Add(IDesignPredicate predicate) + { + list.Add(predicate); + return true; + } + } + + private void DrawManualInput(IReadOnlyList list) + { + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + DrawTotalPreview(list); + var currentDefinition = RandomPredicate.GeneratePredicateString(list); + var definition = _newDefinition ?? currentDefinition; + definition = definition.Replace(";", ";\n\t").Replace("{", "{\n\t").Replace("}", "\n}"); + var lines = definition.Count(c => c is '\n'); + if (ImGui.InputTextMultiline("##definition", ref definition, 2000, + new Vector2(ImGui.GetContentRegionAvail().X, (lines + 1) * ImGui.GetTextLineHeight() + ImGui.GetFrameHeight()), + ImGuiInputTextFlags.CtrlEnterForNewLine)) + _newDefinition = definition; + if (ImGui.IsItemDeactivatedAfterEdit() && _newDefinition != null && _newDefinition != currentDefinition) + { + var predicates = RandomPredicate.GeneratePredicates(_newDefinition.Replace("\n", string.Empty).Replace("\t", string.Empty)); + _autoDesignManager.ChangeData(_set!, _designIndex, predicates); + _newDefinition = null; + } + + if (ImGui.Button("Copy to Clipboard Without Line Breaks", new Vector2(ImGui.GetContentRegionAvail().X, 0))) + { + try + { + ImGui.SetClipboardText(currentDefinition); + } + catch + { + // ignored + } + } + } + + private void DrawTotalPreview(IReadOnlyList list) + { + var designs = IDesignPredicate.Get(list, _designs, _designFileSystem).ToList(); + var button = designs.Count > 0 + ? $"All Restrictions Combined Match {designs.Count} Designs" + : "None of the Restrictions Matches Any Designs"; + ImGuiUtil.DrawDisabledButton(button, new Vector2(ImGui.GetContentRegionAvail().X, 0), + string.Empty, false, false); + if (ImGui.IsItemHovered()) + LookupTooltip(designs); + } + + private void DrawContent(RandomDesign random) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y + ImGuiHelpers.GlobalScale); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + var list = random.Predicates.ToList(); + if (list.Count == 0) + { + ImGui.TextUnformatted("No Restrictions Set. Selects among all existing Designs."); + } + else + { + ImGui.TextUnformatted("Select among designs..."); + DrawTable(random, list); + } + + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + DrawNewButtons(list); + DrawManualInput(list); + } + + private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? data) + { + if (set != _set || _set == null) + return; + + switch (type) + { + case AutomationChanged.Type.DeletedSet: + case AutomationChanged.Type.DeletedDesign when data is int index && _designIndex == index: + Close(); + break; + case AutomationChanged.Type.MovedDesign when data is (int from, int to): + if (_designIndex == from) + _designIndex = to; + else if (_designIndex < from && _designIndex > to) + _designIndex++; + else if (_designIndex > to && _designIndex < from) + _designIndex--; + break; + case AutomationChanged.Type.ChangedDesign when data is (int index, IDesignStandIn _, IDesignStandIn _) && index == _designIndex: + Close(); + break; + } + } +} diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 19d6fc5..bdf7861 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.Interop; using Glamourer.Services; using Glamourer.Unlocks; @@ -25,7 +26,8 @@ public class SetPanel( CustomizeUnlockManager _customizeUnlocks, CustomizeService _customizations, IdentifierDrawer _identifierDrawer, - Configuration _config) + Configuration _config, + RandomRestrictionDrawer _randomDrawer) { private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log); @@ -115,6 +117,7 @@ public class SetPanel( ImGui.Separator(); ImGui.Dummy(Vector2.Zero); DrawDesignTable(); + _randomDrawer.Draw(); } @@ -190,6 +193,7 @@ public class SetPanel( ImGui.Selectable($"#{idx + 1:D2}"); DrawDragDrop(Selection, idx); ImGui.TableNextColumn(); + DrawRandomEditing(Selection, design, idx); _designCombo.Draw(Selection, design, idx); DrawDragDrop(Selection, idx); if (singleRow) @@ -257,6 +261,15 @@ public class SetPanel( } } + private void DrawRandomEditing(AutoDesignSet set, AutoDesign design, int designIdx) + { + if (design.Design is not RandomDesign) + return; + + _randomDrawer.DrawButton(set, designIdx); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + } + private void DrawWarnings(AutoDesign design) { if (design.Design is not DesignBase) @@ -266,7 +279,7 @@ public class SetPanel( size.X += ImGuiHelpers.GlobalScale; var (equipFlags, customizeFlags, _, _, _) = design.ApplyWhat(); - var sb = new StringBuilder(); + var sb = new StringBuilder(); var designData = design.Design.GetDesignData(default); foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index 4e63361..950b735 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -290,7 +290,7 @@ public class SetSelector : IDisposable id = _actors.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, $"Create a new Automatic Design Set for {id}. The associated player can be changed later.", !id.IsValid, true)) - _manager.AddDesignSet("New Design", id); + _manager.AddDesignSet("New Automation Set", id); } private void DuplicateSetButton(Vector2 size) diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 3ca684e..f9f5636 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -473,12 +474,7 @@ public class CommandService : IDisposable _chat.Print(new SeStringBuilder() .AddText(" 》 Clipboard as a single word will try to apply a design string currently in your clipboard.").BuiltString); _chat.Print(new SeStringBuilder() - .AddText(" 》 ").AddYellow("Random").AddText(" supports the following restrictions:").BuiltString); - _chat.Print(new SeStringBuilder() - .AddText(" 》》》 ").AddYellow("Random").AddText(", choosing a random design out of all your designs.").BuiltString); - _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddYellow("Random:{List of [text] or /[text]}").AddText(", containing a list of restrictions within swirly braces, separated by semicolons.").BuiltString); - _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddYellow("Random:[text]").AddText(", choosing a random design where the path, name or identifier contains 'text' (no brackets).").BuiltString); - _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddYellow("Random:/[text]").AddText(", choosing a random design where the path starts with 'text' (no brackets).").BuiltString); + .AddText(" 》 ").AddYellow("Random").AddText(" supports many restrictions, see the Restriction Builder when adding a Random design to Automations for valid strings.").BuiltString); _chat.Print(new SeStringBuilder() .AddText(" 》 ").AddBlue("").AddText(" is optional and can be omitted (together with the ;), ").AddBlue("true") .AddText(" or ").AddBlue("false").AddText(".").BuiltString); diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 2d3507d..24a3902 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -149,6 +149,7 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton()