Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Silkstring/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Dalamud.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using Silkstring.Models;

namespace Silkstring;
Expand Down Expand Up @@ -44,4 +45,9 @@ public void TrySave(TimeSpan debounce)
_isDirty = false;
}
}

public IEnumerable<AliasEntry> GetAliases()
{
return Aliases.Concat(Folders.SelectMany(f => f.Aliases));
}
}
16 changes: 12 additions & 4 deletions Silkstring/Plugin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Dalamud.Game.Command;
Expand Down Expand Up @@ -46,6 +47,7 @@ public sealed unsafe class Plugin : IDalamudPlugin
private Hook<ShellCommandModule.Delegates.ExecuteCommandInner> processChatInputHook;

private readonly CancellationTokenSource _cts = new();
private readonly HashSet<string> _executingAliases = new(StringComparer.OrdinalIgnoreCase);

public Plugin()
{
Expand Down Expand Up @@ -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)));
Expand All @@ -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;
}
}
Expand Down
71 changes: 71 additions & 0 deletions Silkstring/Services/AliasValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Silkstring.Models;

namespace Silkstring.Services;

public static class AliasValidator
{
public static List<string> FindCycle(AliasEntry target, IEnumerable<AliasEntry> allAliases)
{
var lookup = BuildTriggerLookup(allAliases);
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var path = new List<string>();
return Dfs(target, lookup, visited, path);
}

private static Dictionary<string, AliasEntry> BuildTriggerLookup(IEnumerable<AliasEntry> allAliases)
{
var lookup = new Dictionary<string, AliasEntry>(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<AliasEntry> GetDependencies(AliasEntry alias, Dictionary<string, AliasEntry> 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<string> Dfs(
AliasEntry current, Dictionary<string, AliasEntry> lookup, HashSet<string> visited, List<string> path)
{
var triggers = current.Name.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (triggers.Length == 0) return new List<string>();

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<string>(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<string>();
}
}
15 changes: 14 additions & 1 deletion Silkstring/Services/CommandHandler.cs
Original file line number Diff line number Diff line change
@@ -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<string> commands, int delayMs = 100, CancellationToken cancellationToken = default)
public static async Task ExecuteAsync(IReadOnlyList<string> commands, int delayMs = 100, CancellationToken cancellationToken = default, Func<string, bool>? 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);
}
Expand Down
34 changes: 30 additions & 4 deletions Silkstring/UI/AliasEditPanel.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,13 +16,19 @@ public class AliasEditPanel
private readonly Configuration _configuration;

private AliasEntry? _selectedAlias;
private List<string>? _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;
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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;
Expand All @@ -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());
}
}
Loading