From 70b6383f23a468b3e85e988899593302594194e5 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:05:55 -0300 Subject: [PATCH 1/3] Add wait detection and duration parsing --- .../Services/Conditions/BlockInterpreter.cs | 14 +++++++++++++- Tests/BlockInterpreterTests.cs | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Silkstring/Services/Conditions/BlockInterpreter.cs b/Silkstring/Services/Conditions/BlockInterpreter.cs index 1aeddfe..b8272f5 100644 --- a/Silkstring/Services/Conditions/BlockInterpreter.cs +++ b/Silkstring/Services/Conditions/BlockInterpreter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace Silkstring.Services.Conditions; @@ -9,7 +10,8 @@ internal enum BlockKind If, Else, EndIf, - Set + Set, + Wait } internal sealed class BlockInterpreter @@ -23,6 +25,7 @@ public static (BlockKind Kind, string Expression) Classify(string line) if (line.Equals(":else", StringComparison.OrdinalIgnoreCase)) return (BlockKind.Else, ""); if (line.Equals(":endif", StringComparison.OrdinalIgnoreCase)) return (BlockKind.EndIf, ""); if (line.StartsWith(":set ", StringComparison.OrdinalIgnoreCase)) return (BlockKind.Set, line[5..]); + if (line.StartsWith(":wait ", StringComparison.OrdinalIgnoreCase)) return (BlockKind.Wait, line[6..]); return (BlockKind.Command, line); } @@ -33,6 +36,15 @@ public static (string Name, string Value) ParseSet(string expression) return space < 0 ? (trimmed, "") : (trimmed[..space], trimmed[(space + 1)..].Trim()); } + public static bool TryParseDuration(string text, out int milliseconds) + { + milliseconds = 0; + if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds) || seconds < 0) + return false; + milliseconds = (int)Math.Min(seconds * 1000, 60000); + return true; + } + public void EnterIf(bool conditionMet) { var parentActive = Active; diff --git a/Tests/BlockInterpreterTests.cs b/Tests/BlockInterpreterTests.cs index 91b9f6a..072feea 100644 --- a/Tests/BlockInterpreterTests.cs +++ b/Tests/BlockInterpreterTests.cs @@ -12,6 +12,9 @@ public class BlockInterpreterTests [InlineData(":set foo bar", "Set", "foo bar")] [InlineData(":SET foo bar", "Set", "foo bar")] [InlineData(":set", "Command", ":set")] + [InlineData(":wait 2", "Wait", "2")] + [InlineData(":WAIT 1.5", "Wait", "1.5")] + [InlineData(":wait", "Command", ":wait")] public void Classifies(string line, string kind, string expression) { var (k, e) = BlockInterpreter.Classify(line); @@ -32,6 +35,21 @@ public void ParsesSet(string expression, string name, string value) Assert.Equal(value, v); } + [Theory] + [InlineData("2", true, 2000)] + [InlineData("1.5", true, 1500)] + [InlineData("0", true, 0)] + [InlineData("120", true, 60000)] + [InlineData("-1", false, 0)] + [InlineData("abc", false, 0)] + [InlineData("{0}", false, 0)] + [InlineData("", false, 0)] + public void ParsesDuration(string text, bool expected, int milliseconds) + { + Assert.Equal(expected, BlockInterpreter.TryParseDuration(text, out var ms)); + Assert.Equal(milliseconds, ms); + } + [Fact] public void EmptyIsActive() => Assert.True(new BlockInterpreter().Active); From 51d3154ebfe810132ed887c44b648f9bcee73188 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:36:58 -0300 Subject: [PATCH 2/3] add validation and execution of waits --- Silkstring/Services/AliasValidator.cs | 21 +++++++++++++++------ Silkstring/Services/CommandHandler.cs | 13 +++++++++++++ Silkstring/UI/Panels/AliasEditPanel.cs | 2 +- Tests/AliasValidatorTests.cs | 9 +++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Silkstring/Services/AliasValidator.cs b/Silkstring/Services/AliasValidator.cs index c51e84c..fa97421 100644 --- a/Silkstring/Services/AliasValidator.cs +++ b/Silkstring/Services/AliasValidator.cs @@ -42,7 +42,6 @@ public static List FindCycle(AliasEntry target, IEnumerable break; } } - return elseSeen.Count > 0 ? "Unclosed :if (missing :endif)" : null; } @@ -59,16 +58,27 @@ public static List FindCycle(AliasEntry target, IEnumerable return null; } + public static string? ValidateWaits(AliasEntry alias) + { + foreach (var command in alias.Output) + { + var (kind, expression) = BlockInterpreter.Classify(command.Command); + if (kind != BlockKind.Wait) continue; + var (value, _) = BlockInterpreter.ParseSet(expression); + if (string.IsNullOrEmpty(value)) return ":wait needs a duration"; + if (expression.Contains('{')) continue; + if (!BlockInterpreter.TryParseDuration(expression, out _)) return $"Invalid :wait duration: {expression}"; + } + return null; + } + 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); - } + foreach (var trigger in triggers) lookup.TryAdd(trigger, alias); } return lookup; } @@ -113,7 +123,6 @@ private static List Dfs( 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 8c51deb..896e31d 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -56,6 +56,19 @@ await _framework.RunOnFrameworkThread(() => continue; } + if (kind == BlockKind.Wait) + { + if (blocks.Active) + { + var resolved = await _framework.RunOnFrameworkThread(() => _resolver.Resolve(expression, args)); + if (BlockInterpreter.TryParseDuration(resolved, out var ms)) + await Task.Delay(ms, cancellationToken); + else + Log.Warning("Invalid :wait duration: {Duration}", resolved); + } + continue; + } + if (!blocks.Active) continue; var cmd = await _framework.RunOnFrameworkThread(() => _resolver.Resolve(line, args)); diff --git a/Silkstring/UI/Panels/AliasEditPanel.cs b/Silkstring/UI/Panels/AliasEditPanel.cs index ad486ee..c8cbbc5 100644 --- a/Silkstring/UI/Panels/AliasEditPanel.cs +++ b/Silkstring/UI/Panels/AliasEditPanel.cs @@ -155,7 +155,7 @@ private void RefreshCycleCheck() if (_selectedAlias == null) return; _detectedCycle = AliasValidator.FindCycle(_selectedAlias, _configuration.GetAliases()); var defined = new HashSet(_configuration.UserVariables.Select(v => v.Name), StringComparer.OrdinalIgnoreCase); - _blockError = AliasValidator.ValidateBlocks(_selectedAlias) ?? AliasValidator.ValidateSets(_selectedAlias, defined); + _blockError = AliasValidator.ValidateBlocks(_selectedAlias) ?? AliasValidator.ValidateSets(_selectedAlias, defined) ?? AliasValidator.ValidateWaits(_selectedAlias); } private void SyncMultilineBuffer(AliasEntry alias) diff --git a/Tests/AliasValidatorTests.cs b/Tests/AliasValidatorTests.cs index ca972d3..af6a3eb 100644 --- a/Tests/AliasValidatorTests.cs +++ b/Tests/AliasValidatorTests.cs @@ -62,4 +62,13 @@ public void NonSlashLineIsNotADependency() [Fact] public void SetNoName() => Assert.NotNull(AliasValidator.ValidateSets(Alias("a", ":set "), Defined("foo"))); [Fact] public void SetOneBadAmongGood() => Assert.NotNull(AliasValidator.ValidateSets(Alias("a", ":set foo a", ":set bar b"), Defined("foo"))); [Fact] public void SetNonSetLinesIgnored() => Assert.Null(AliasValidator.ValidateSets(Alias("a", "/say hi"), Defined())); + + [Fact] public void WaitValid() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 2"))); + [Fact] public void WaitDecimal() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 1.5"))); + [Fact] public void WaitToken() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait {0}"))); + [Fact] public void WaitOverCapIsValid() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", ":wait 120"))); + [Fact] public void WaitInvalid() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait potato"))); + [Fact] public void WaitNegative() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait -1"))); + [Fact] public void WaitEmpty() => Assert.NotNull(AliasValidator.ValidateWaits(Alias("a", ":wait "))); + [Fact] public void WaitNonWaitLinesIgnored() => Assert.Null(AliasValidator.ValidateWaits(Alias("a", "/say hi"))); } From aa564b52234896b8ee48fb3fdf1a45b4c93cc8b6 Mon Sep 17 00:00:00 2001 From: devoreofox <232652342+devoreofox@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:43:01 -0300 Subject: [PATCH 3/3] Update documentation for waits --- CHANGELOG.md | 6 ++++++ README.md | 10 ++++++++++ Silkstring/Windows/HelpWindow.cs | 14 +++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ce13e..fd2abd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.4.0.0 - TBD +### Added +- Waits: add a `:wait` line to pause an alias for a set number of seconds before the next line, for example `:wait 2`. Decimals work too, like `:wait 1.5` +- A `:wait` can take its duration from a variable or parameter, such as `:wait {0}`, and is capped at 60 seconds +- The alias editor warns you when a `:wait` has an invalid duration + ## v1.3.0.0 - 2026-06-30 ### Added - Your own variables: create variables in the new Variables window (open it with `/silkstring variables`), give each one a value, and use it in any alias with `{name}`, just like the built-in ones diff --git a/README.md b/README.md index 9ad6690..412281f 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,16 @@ A condition compares values with `==`, `!=`, `<`, `>`, `<=`, `>=`, and you can c Text comparisons are case-insensitive, and numbers compare as numbers. The `:if`, `:else`, and `:endif` lines are never sent to chat, and the alias editor warns you if a block is left open or a condition cannot be understood. +You can also pause between lines with `:wait`, followed by a number of seconds: + +``` +/ac Raise +:wait 2 +/say Up you get! +``` + +The wait accepts decimals (`:wait 1.5`) and can take its duration from a variable or parameter (`:wait {0}`). Waits are capped at 60 seconds, and a `:wait` inside a condition only pauses when that branch actually runs. Like the other control lines, `:wait` is never sent to chat, and the editor warns you if a duration is invalid. + ### User Variables Alongside the built-in variables, you can define your own. Open the Variables window with `/silkstring variables`, type a name, and give it a value. Names can use letters, numbers, and underscores, and cannot reuse a built-in variable name. diff --git a/Silkstring/Windows/HelpWindow.cs b/Silkstring/Windows/HelpWindow.cs index 5a4c151..d30fb0b 100644 --- a/Silkstring/Windows/HelpWindow.cs +++ b/Silkstring/Windows/HelpWindow.cs @@ -178,7 +178,7 @@ private void DrawCommandsHelp() ImGui.TextColored(HeadingColor, "Conditionals"); ImGui.Separator(); ImGui.Spacing(); - ImGui.TextWrapped("Run commands only when a condition is true with :if / :else / :endif blocks. See the Conditionals tab."); + ImGui.TextWrapped("Run commands only when a condition is true with :if / :else / :endif blocks, and pause with :wait. See the Conditionals tab."); ImGui.Spacing(); ImGui.TextColored(HeadingColor, "Macros"); @@ -364,5 +364,17 @@ private void DrawConditionalsHelp() ImGui.TextDisabled(":if {0} == on"); ImGui.Unindent(); ImGui.Spacing(); + + ImGui.TextColored(HeadingColor, "Waits"); + ImGui.Separator(); + ImGui.Spacing(); + ImGui.TextWrapped("Pause between lines with :wait, followed by a number of seconds. Decimals work, and the duration can come from a variable or parameter. Waits are capped at 60 seconds, and a :wait inside a condition only pauses when that branch runs. Like the other control lines, it is never sent to chat."); + ImGui.Spacing(); + ImGui.Indent(); + ImGui.TextDisabled("/ac Raise"); + ImGui.TextDisabled(":wait 2"); + ImGui.TextDisabled("/say Up you get!"); + ImGui.Unindent(); + ImGui.Spacing(); } }