From a0f61c376a28a959c2be7c4859a6111f79653175 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Fri, 29 May 2026 18:54:26 -0300 Subject: [PATCH] Prevent recursion and provide validation --- Silkstring/Configuration.cs | 6 +++ Silkstring/Plugin.cs | 16 ++++-- Silkstring/Services/AliasValidator.cs | 71 +++++++++++++++++++++++++++ Silkstring/Services/CommandHandler.cs | 15 +++++- Silkstring/UI/AliasEditPanel.cs | 34 +++++++++++-- 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 Silkstring/Services/AliasValidator.cs diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index c6e3a61..fd1fd13 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -1,6 +1,7 @@ using Dalamud.Configuration; using System; using System.Collections.Generic; +using System.Linq; using Silkstring.Models; namespace Silkstring; @@ -44,4 +45,9 @@ public void TrySave(TimeSpan debounce) _isDirty = false; } } + + public IEnumerable GetAliases() + { + return Aliases.Concat(Folders.SelectMany(f => f.Aliases)); + } } diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 6d038fc..dc35275 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using Dalamud.Game.Command; @@ -46,6 +47,7 @@ public sealed unsafe class Plugin : IDalamudPlugin private Hook processChatInputHook; private readonly CancellationTokenSource _cts = new(); + private readonly HashSet _executingAliases = new(StringComparer.OrdinalIgnoreCase); public Plugin() { @@ -120,8 +122,7 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 { var splitString = inputString.Split(' '); var commandName = splitString[0][1..]; - var alias = Configuration.Aliases.Concat( - Configuration.Folders.SelectMany(g => g.Aliases)).FirstOrDefault(a => + var alias = Configuration.GetAliases().FirstOrDefault(a => a.Enabled && a.IsValid() && a.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); @@ -131,10 +132,17 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 .Where(c => !string.IsNullOrWhiteSpace(c.Command)) .Select(c => "/" + c.Strip()) .ToList(); + var names = alias.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + foreach (var name in names) _executingAliases.Add(name); + bool ShouldSkip(string cmd) => _executingAliases.Contains(cmd); - CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay, _cts.Token) - .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted); + CommandHandler.ExecuteAsync(commands, Configuration.CommandDelay, _cts.Token, shouldSkip: ShouldSkip) + .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted) + .ContinueWith(_ => Framework.RunOnFrameworkThread(() => + { + foreach (var name in names) _executingAliases.Remove(name); + })); return; } } diff --git a/Silkstring/Services/AliasValidator.cs b/Silkstring/Services/AliasValidator.cs new file mode 100644 index 0000000..74745fe --- /dev/null +++ b/Silkstring/Services/AliasValidator.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Silkstring.Models; + +namespace Silkstring.Services; + +public static class AliasValidator +{ + public static List FindCycle(AliasEntry target, IEnumerable allAliases) + { + var lookup = BuildTriggerLookup(allAliases); + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var path = new List(); + return Dfs(target, lookup, visited, path); + } + + private static Dictionary BuildTriggerLookup(IEnumerable allAliases) + { + var lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var alias in allAliases) + { + var triggers = alias.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + foreach (var trigger in triggers) + { + lookup.TryAdd(trigger, alias); + } + } + return lookup; + } + + private static IEnumerable GetDependencies(AliasEntry alias, Dictionary lookup) + { + foreach (var command in alias.Output) + { + if (string.IsNullOrWhiteSpace(command.Command)) continue; + var commandName = command.Command.TrimStart('/').Split(' ', StringSplitOptions.RemoveEmptyEntries)[0]; + if (lookup.TryGetValue(commandName, out var dependency)) yield return dependency; + } + } + + private static List Dfs( + AliasEntry current, Dictionary lookup, HashSet visited, List path) + { + var triggers = current.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (triggers.Length == 0) return new List(); + + var trigger = triggers[0]; + path.Add(trigger); + + foreach (var dependency in GetDependencies(current, lookup)) + { + var depTrigger = dependency.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)[0]; + + if (path.Contains(depTrigger)) + { + var cycle = new List(path) { depTrigger }; + return cycle; + } + + if (!visited.Contains(depTrigger)) + { + var result = Dfs(dependency, lookup, visited, path); + if (result.Count > 0) return result; + } + } + + path.Remove(trigger); + visited.Add(trigger); + return new List(); + } +} diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index d9d9618..7713a5c 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -1,17 +1,30 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ECommons.Automation; +using Serilog; namespace Silkstring.Services; public static class CommandHandler { - public static async Task ExecuteAsync(IReadOnlyList commands, int delayMs = 100, CancellationToken cancellationToken = default) + public static async Task ExecuteAsync(IReadOnlyList commands, int delayMs = 100, CancellationToken cancellationToken = default, Func? shouldSkip = null) { for (var i = 0; i < commands.Count; i++) { var cmd = commands[i]; + if (shouldSkip != null) + { + var parts = cmd.TrimStart('/').Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) continue; + var commandName = parts[0]; + if (shouldSkip(commandName)) + { + Log.Warning("Skipping recursive command: {Command}", cmd); + continue; + } + } await Plugin.Framework.RunOnFrameworkThread(() => Chat.SendMessage(cmd)); if (i < commands.Count - 1) await Task.Delay(delayMs, cancellationToken); } diff --git a/Silkstring/UI/AliasEditPanel.cs b/Silkstring/UI/AliasEditPanel.cs index 6cb2b61..861bc4f 100644 --- a/Silkstring/UI/AliasEditPanel.cs +++ b/Silkstring/UI/AliasEditPanel.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; using Silkstring.Models; +using Silkstring.Services; using Silkstring.Windows; namespace Silkstring.Ui; @@ -14,13 +16,19 @@ public class AliasEditPanel private readonly Configuration _configuration; private AliasEntry? _selectedAlias; + private List? _detectedCycle; private string _multilineBuffer = string.Empty; private int _multilineAliasId = -1; public AliasEditPanel(Configuration configuration, MainWindow mainWindow) { - mainWindow.SelectionChanged += (alias, _) => _selectedAlias = alias; + mainWindow.SelectionChanged += (alias, _) => + { + _selectedAlias = alias; + if (alias != null) RefreshCycleCheck(); + else _detectedCycle = null; + }; _configuration = configuration; } @@ -56,8 +64,15 @@ private void DrawAliasHeader(AliasEntry alias) if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltipText); ImGui.SameLine(); ImGui.SetNextItemWidth(-1); - if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) _configuration.MarkDirty(); - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Separate multiple aliases with | e.g. mew|meow|mreow"); + if (ImGui.InputTextWithHint($"###aliasName{alias.UniqueId}", "activation command", ref alias.Name, 100)) + { + _configuration.MarkDirty(); + RefreshCycleCheck(); + } + var inputTooltip = _detectedCycle is { Count: > 0 } + ? $"Cycle detected: {string.Join(" → ", _detectedCycle)}" + : "Separate multiple aliases with | e.g. mew|meow|mreow"; + if (ImGui.IsItemHovered()) ImGui.SetTooltip(inputTooltip); } private void DrawCommandList(AliasEntry alias) @@ -92,6 +107,7 @@ private void DrawMultilineView(AliasEntry alias) if (alias.Output.Count > lines.Count) alias.Output.RemoveRange(lines.Count, alias.Output.Count - lines.Count); _configuration.MarkDirty(); + RefreshCycleCheck(); } } @@ -118,7 +134,11 @@ private void DrawListView(AliasEntry alias) private void DrawCommandRow(CommandEntry command) { ImGui.SetNextItemWidth(-60); - if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) _configuration.MarkDirty(); + if (ImGui.InputText($"###cmd{command.UniqueId}", ref command.Command, 200)) + { + _configuration.MarkDirty(); + RefreshCycleCheck(); + } ImGui.SameLine(); var canDelete = ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl; @@ -127,4 +147,10 @@ private void DrawCommandRow(CommandEntry command) ImGui.EndDisabled(); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("Hold Shift + Ctrl to delete"); } + + private void RefreshCycleCheck() + { + if (_selectedAlias == null) return; + _detectedCycle = AliasValidator.FindCycle(_selectedAlias, _configuration.GetAliases()); + } }