diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8d7564e..2652b92 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -10,6 +10,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + submodules: recursive - name: Install .NET uses: actions/setup-dotnet@v5 with: @@ -24,6 +26,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: Silkstring - path: .\Silkstring\bin\x64\Release\* + path: .\Silkstring\bin\Release\* if-no-files-found: error retention-days: 1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e62e2..700a337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v1.6.0.0 - 2026-07-09 +### Added +- Emote variables: `{emoting}` tells you whether you are holding an emote or pose, `{emote}` gives its name, and `{pose}` gives the pose number. They appear in the Variables tab alongside the other built-ins +- Your own variables can now have a description, which you set in the Variables window and see next to them in the Variables tab of the help window +- Until: add a `:until` line to hold an alias at that point until a condition becomes true, then carry on, for example `:until {emoting} == false`. Handy for waiting on your own state before the next line runs, such as holding a statement until an emote ends +- An `-unsafe` option on `:until` lets it wait with no time limit. It has to be turned on in the settings first, and normal untils use a timeout you can set there too +- A `/silkstring cancel` command stops any aliases that are currently running +- The editor now colors option flags like `-unsafe`, in a color you can change in the settings +- A new Editor tab in the help window shows what each highlight color means + ## v1.5.0.0 - 2026-07-02 ### Added - Syntax highlighting in the multiline editor: commands, keywords (`:if`, `:set`, `:wait`), variables, parameters, and quoted text are each given their own color as you type, and anything malformed is shown in red (a bad `:wait` time, an unfinished `:if`, a `:set` for a variable you have not created, a mistyped keyword, or an unclosed `{`) diff --git a/README.md b/README.md index 1beabd9..cd29810 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A Dalamud plugin for FFXIV that lets you define custom command aliases. Each ali - Enable or disable an alias without deleting it - Built-in help window with a live command tester - Syntax highlighting in the multiline editor, with customizable colors +- Hold an alias until a condition is met with `:until`, and stop running aliases with `/silkstring cancel` - Cycle detection warns you if aliases would trigger each other in a loop ## Installation @@ -86,7 +87,9 @@ Variables let you insert live game values into a line using curly brace syntax. /say I am {character}, a level {level} {job} from {world}! ``` -Silkstring includes variables for your character, job, HP and MP, combat state, target, currency, and more, and the list keeps growing. For the full, always-current list with live values, open the Variables tab of the help window (`/silkstring help`). The command tester at the top of that window shows resolved output as you type. +Silkstring includes variables for your character, job, HP and MP, combat state, target, currency, emote and pose state, and more, and the list keeps growing. For the full, always-current list with live values, open the Variables tab of the help window (`/silkstring help`). The command tester at the top of that window shows resolved output as you type. + +You can also define your own variables with `/silkstring variables` and use them just like the built-in ones (see User Variables below). ### Parameters @@ -146,9 +149,19 @@ You can also pause between lines with `:wait`, followed by a number of seconds: 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. +You can also hold an alias in place until a condition becomes true with `:until`, then let it carry on: + +``` +/sit +:until {emoting} == false +/say Up you get! +``` + +`:until` pauses at that line until the condition is true, for example waiting until an emote or pose ends. If the condition never comes true, it gives up after a timeout you can set in the settings and then continues. Add `-unsafe` to wait with no time limit, such as `:until {emoting} == false -unsafe`, but unsafe waits have to be turned on in the settings first. You can stop any running alias at any time with `/silkstring cancel`. + ### 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. +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. You can also give each one an optional description, which appears next to it in the Variables tab of the help window. Your variables work just like the built-in ones: insert them anywhere with `{name}`, and they appear in the Variables tab of the help window. @@ -175,11 +188,11 @@ Create a folder with the **New Folder** button, then drag aliases into or out of ### Multiline entry -By default each line of an alias is its own row. You can switch to a single multiline editor in the settings, which is handy for pasting or editing longer aliases. The editor highlights your syntax as you type: commands, keywords, variables, parameters, and quoted text each get their own color, and anything malformed (a bad `:wait`, an unfinished `:if`, an unknown `:set` name, a mistyped keyword, or an unclosed `{`) is shown in red. It also shows line numbers, which you can turn off in the settings. +By default each line of an alias is its own row. You can switch to a single multiline editor in the settings, which is handy for pasting or editing longer aliases. The editor highlights your syntax as you type: commands, keywords, variables, parameters, quoted text, and option flags each get their own color, and anything malformed (a bad `:wait`, an unfinished `:if`, an unknown `:set` name, a mistyped keyword, or an unclosed `{`) is shown in red. It also shows line numbers, which you can turn off in the settings. ### Settings -Open settings with the cog icon in the Silkstring title bar, or the cog next to Silkstring in `/xlplugins`. You can set the delay between lines in milliseconds (0 to 1000, default 100), toggle multiline command entry, toggle line numbers in the editor, and open the Colors section to recolor the editor and interface to your taste. +Open settings with the cog icon in the Silkstring title bar, or the cog next to Silkstring in `/xlplugins`. You can set the delay between lines in milliseconds (0 to 1000, default 100), set how long an `:until` waits before giving up, allow unsafe waits that have no time limit, toggle multiline command entry, toggle line numbers in the editor, and open the Colors section to recolor the editor and interface to your taste. ## Notes diff --git a/Silkstring/Configuration.cs b/Silkstring/Configuration.cs index cdc8332..a7bdc38 100644 --- a/Silkstring/Configuration.cs +++ b/Silkstring/Configuration.cs @@ -21,6 +21,14 @@ public int CommandDelay set => _commandDelay = Math.Clamp(value, 0, 1000); } + private int _untilTimeoutSeconds = 30; + public int UntilTimeoutSeconds + { + get => _untilTimeoutSeconds; + set => _untilTimeoutSeconds = Math.Clamp(value, 1, 600); + } + public bool AllowUnsafeWaits { get; set; } + public const int CurrentVersion = 2; public string? LastSeenVersion { get; set; } public int Version { get; set; } = CurrentVersion; diff --git a/Silkstring/Models/ThemeColors.cs b/Silkstring/Models/ThemeColors.cs index 82ec42d..be69cc3 100644 --- a/Silkstring/Models/ThemeColors.cs +++ b/Silkstring/Models/ThemeColors.cs @@ -12,7 +12,8 @@ public class ThemeColors public Vector4 Variable = new(0.9f, 0.8f, 0.4f, 1.0f); public Vector4 Parameter = new(1.0f, 0.6f, 0.3f, 1.0f); public Vector4 Keyword = new(0.4f, 0.8f, 1.0f, 1.0f); - public Vector4 String = new(0.6f, 0.8f, 0.5f, 1.0f); public Vector4 Command = new(0.7f, 0.5f, 1.0f, 1.0f); + public Vector4 Flag = new(0.85f, 0.55f, 0.85f, 1.0f); + public Vector4 String = new(0.6f, 0.8f, 0.5f, 1.0f); public Vector4 Text = new(0.9f, 0.9f, 0.9f, 1.0f); } diff --git a/Silkstring/Models/UserVariable.cs b/Silkstring/Models/UserVariable.cs index 3bf0788..fcf3aaf 100644 --- a/Silkstring/Models/UserVariable.cs +++ b/Silkstring/Models/UserVariable.cs @@ -9,6 +9,7 @@ public class UserVariable private static int _nextId = 0; public string Name = string.Empty; + public string Description = string.Empty; public string Value = string.Empty; [NonSerialized] diff --git a/Silkstring/Plugin.cs b/Silkstring/Plugin.cs index 350d445..c8beeef 100644 --- a/Silkstring/Plugin.cs +++ b/Silkstring/Plugin.cs @@ -27,6 +27,7 @@ public sealed class Plugin : IDalamudPlugin [PluginService] internal static IPlayerState PlayerState { get; private set; } = null!; [PluginService] internal static IClientState ClientState { get; private set; } = null!; [PluginService] internal static ICondition Condition { get; private set; } = null!; + [PluginService] internal static IDataManager DataManager { get; private set; } = null!; private const string CommandName = "/silkstring"; @@ -70,6 +71,7 @@ public Plugin() new VitalsVariablesProvider(ClientState), new CombatVariablesProvider(Condition, TargetManager), new CurrencyVariablesProvider(), + new EmoteVariablesProvider(ClientState, DataManager), ]; var reserved = builtIn.SelectMany(p => p.GetVariables()).Select(v => v.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); @@ -142,7 +144,15 @@ private void OnCommand(string command, string args) case "help": ToggleHelpUi(); break; case "changelog": ToggleChangelogUi(); break; case "variables": ToggleVariablesUi(); break; - case "edit": ToggleConfigUi(); break; + case "cancel": { + var cancelled = _chatInterceptor.CancelRunning(); + NotificationManager.AddNotification(new Notification + { + Content = cancelled ? "Cancelled running aliases." : "Nothing to cancel.", + Type = NotificationType.Info + }); + break; + } default: MainWindow.Toggle(); break; } } diff --git a/Silkstring/Services/AliasValidator.cs b/Silkstring/Services/AliasValidator.cs index fa97421..8b89311 100644 --- a/Silkstring/Services/AliasValidator.cs +++ b/Silkstring/Services/AliasValidator.cs @@ -72,6 +72,21 @@ public static List FindCycle(AliasEntry target, IEnumerable return null; } + public static string? ValidateUntils(AliasEntry alias, bool allowUnsafe) + { + foreach (var command in alias.Output) + { + var (kind, expression) = BlockInterpreter.Classify(command.Command); + if (kind != BlockKind.Until) continue; + var (isUnsafe, condition) = BlockInterpreter.ParseUntil(expression); + if (string.IsNullOrWhiteSpace(condition)) return ":until needs a condition"; + try { new Parser(Tokenizer.Tokenize(condition)).Parse(); } + catch (ConditionException ex) { return $"Invalid :until condition: {ex.Message}"; } + if (isUnsafe && !allowUnsafe) return "This :until uses -unsafe, but unsafe waits are off in settings"; + } + return null; + } + private static Dictionary BuildTriggerLookup(IEnumerable allAliases) { var lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/Silkstring/Services/ChatInterceptor.cs b/Silkstring/Services/ChatInterceptor.cs index 22c2809..b3a8346 100644 --- a/Silkstring/Services/ChatInterceptor.cs +++ b/Silkstring/Services/ChatInterceptor.cs @@ -18,7 +18,7 @@ public sealed unsafe class ChatInterceptor : IDisposable private readonly CommandHandler _commandHandler; private readonly IFramework _framework; - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource _cts = new(); private readonly HashSet _executingAliases = new(StringComparer.OrdinalIgnoreCase); private readonly Hook _hook; @@ -44,6 +44,15 @@ public void Dispose() _hook?.Dispose(); } + public bool CancelRunning() + { + var wasRunning = _executingAliases.Count > 0; + _cts.Cancel(); + _cts.Dispose(); + _cts = new CancellationTokenSource(); + return wasRunning; + } + private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8String* message, UIModule* uiModule) { try @@ -69,7 +78,9 @@ private void ProcessChatInputDetour(ShellCommandModule* shellCommandModule, Utf8 bool ShouldSkip(string cmd) => _executingAliases.Contains(cmd); _commandHandler.ExecuteAsync(commands, args, _configuration.CommandDelay, _cts.Token, - shouldSkip: ShouldSkip) + shouldSkip: ShouldSkip, + untilTimeoutMs: _configuration.UntilTimeoutSeconds * 1000, + allowUnsafe: _configuration.AllowUnsafeWaits) .ContinueWith(t => Log.Error(t.Exception, "Command execution failed"), TaskContinuationOptions.OnlyOnFaulted) .ContinueWith(_ => _framework.RunOnFrameworkThread(() => diff --git a/Silkstring/Services/CommandHandler.cs b/Silkstring/Services/CommandHandler.cs index 896e31d..51200e5 100644 --- a/Silkstring/Services/CommandHandler.cs +++ b/Silkstring/Services/CommandHandler.cs @@ -23,7 +23,7 @@ public CommandHandler(CommandResolver resolver, IFramework framework, Func commands, IReadOnlyList args, int delayMs = 100, CancellationToken cancellationToken = default, Func? shouldSkip = null) + public async Task ExecuteAsync(IReadOnlyList commands, IReadOnlyList args, int delayMs = 100, CancellationToken cancellationToken = default, Func? shouldSkip = null, int untilTimeoutMs = 30000, bool allowUnsafe = false) { var blocks = new BlockInterpreter(); var sent = false; @@ -69,6 +69,16 @@ await _framework.RunOnFrameworkThread(() => continue; } + if (kind == BlockKind.Until) + { + if (blocks.Active) + { + var (isUnsafe, condition) = BlockInterpreter.ParseUntil(expression); + await WaitUntilAsync(condition, args, isUnsafe && allowUnsafe, untilTimeoutMs, cancellationToken); + } + continue; + } + if (!blocks.Active) continue; var cmd = await _framework.RunOnFrameworkThread(() => _resolver.Resolve(line, args)); @@ -102,4 +112,17 @@ private bool EvaluateSafe(string expression, IReadOnlyList args) return false; } } + + private async Task WaitUntilAsync(string condition, IReadOnlyList args, bool isUnsafe, int capMs, CancellationToken token) + { + const int pollMs = 100; + var elapsed = 0; + while (!await _framework.RunOnFrameworkThread(() => EvaluateSafe(condition, args))) + { + await Task.Delay(pollMs, token); + if (isUnsafe) continue; + elapsed += pollMs; + if (elapsed >= capMs) break; + } + } } diff --git a/Silkstring/Services/Conditions/BlockInterpreter.cs b/Silkstring/Services/Conditions/BlockInterpreter.cs index b8272f5..74945da 100644 --- a/Silkstring/Services/Conditions/BlockInterpreter.cs +++ b/Silkstring/Services/Conditions/BlockInterpreter.cs @@ -11,7 +11,8 @@ internal enum BlockKind Else, EndIf, Set, - Wait + Wait, + Until } internal sealed class BlockInterpreter @@ -26,9 +27,17 @@ public static (BlockKind Kind, string Expression) Classify(string line) 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..]); + if (line.StartsWith(":until ", StringComparison.OrdinalIgnoreCase)) return (BlockKind.Until, line[7..]); return (BlockKind.Command, line); } + public static (bool Unsafe, string Condition) ParseUntil(string expression) + { + var trimmed = expression.Trim(); + if (trimmed.EndsWith(" -unsafe", StringComparison.OrdinalIgnoreCase)) return (true, trimmed[..^8].TrimEnd()); + return (false, trimmed); + } + public static (string Name, string Value) ParseSet(string expression) { var trimmed = expression.Trim(); diff --git a/Silkstring/Services/Variables/EmoteVariablesProvider.cs b/Silkstring/Services/Variables/EmoteVariablesProvider.cs new file mode 100644 index 0000000..b306399 --- /dev/null +++ b/Silkstring/Services/Variables/EmoteVariablesProvider.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Dalamud.Plugin.Services; +using ECommons.DalamudServices.Legacy; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Lumina.Excel.Sheets; + +namespace Silkstring.Services.Variables; + +public sealed class EmoteVariablesProvider : IVariableProvider +{ + private readonly IClientState _clientState; + private readonly IDataManager _dataManager; + + public EmoteVariablesProvider(IClientState clientState, IDataManager dataManager) + { + _clientState = clientState; + _dataManager = dataManager; + } + + public IEnumerable GetVariables() + { + yield return new("emoting", "Whether you are currently emoting or posing", "Emote", () => IsEmoting() ? "true" : "false"); + yield return new ("emote", "Name of the emote you are currently doing", "Emote", EmoteName); + yield return new ("pose", "Pose of the emote you are currently doing", "Emote", Pose); + } + + private unsafe Character* Local() + { + var player = _clientState.LocalPlayer; + return player is null ? null : (Character*)player.Address; + } + + private unsafe bool IsEmoting() + { + var character = Local(); + return character is not null && character->Mode is CharacterModes.InPositionLoop or CharacterModes.EmoteLoop; + } + + private unsafe string? EmoteName() + { + var character = Local(); + if (character is null || character->Mode is not CharacterModes.InPositionLoop or CharacterModes.EmoteLoop) return null; + var mode = _dataManager.GetExcelSheet().GetRowOrDefault(character->ModeParam); + if (mode is null) return null; + var emote = mode.Value.StartEmote; + if (!emote.IsValid || emote.RowId == 0) return null; + return emote.Value.Name.ToString(); + } + + private unsafe string? Pose() + { + var character = Local(); + if (character is null || !IsEmoting()) return null; + return character->EmoteController.CPoseState.ToString(); + } +} diff --git a/Silkstring/Services/Variables/UserVariableProvider.cs b/Silkstring/Services/Variables/UserVariableProvider.cs index cc63551..52032b3 100644 --- a/Silkstring/Services/Variables/UserVariableProvider.cs +++ b/Silkstring/Services/Variables/UserVariableProvider.cs @@ -19,7 +19,7 @@ public IEnumerable GetVariables() foreach (var variable in _variables()) { var name = variable.Name; - yield return new(name, "User-defined variable", "User", () => Lookup(name)); + yield return new(name, variable.Description, "User", () => Lookup(name)); } } diff --git a/Silkstring/UI/Palette.cs b/Silkstring/UI/Palette.cs index a1e90de..9ec635c 100644 --- a/Silkstring/UI/Palette.cs +++ b/Silkstring/UI/Palette.cs @@ -16,6 +16,7 @@ public static class Palette public static Vector4 Folder; public static Vector4 Success; public static Vector4 LineNumber; + public static Vector4 Flag; public static void Apply(ThemeColors t) { @@ -30,5 +31,6 @@ public static void Apply(ThemeColors t) Folder = t.Folder; Success = t.Success; LineNumber = t.LineNumber; + Flag = t.Flag; } } diff --git a/Silkstring/UI/Panels/AliasEditPanel.cs b/Silkstring/UI/Panels/AliasEditPanel.cs index e8de8cc..7b9eafa 100644 --- a/Silkstring/UI/Panels/AliasEditPanel.cs +++ b/Silkstring/UI/Panels/AliasEditPanel.cs @@ -160,7 +160,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) ?? AliasValidator.ValidateWaits(_selectedAlias); + _blockError = AliasValidator.ValidateBlocks(_selectedAlias) ?? AliasValidator.ValidateSets(_selectedAlias, defined) ?? AliasValidator.ValidateWaits(_selectedAlias) ?? AliasValidator.ValidateUntils(_selectedAlias, _configuration.AllowUnsafeWaits); } private static void ApplyMultiline(AliasEntry alias, string text) diff --git a/Silkstring/UI/SilkstringHighlighter.cs b/Silkstring/UI/SilkstringHighlighter.cs index f4bdde4..07799ed 100644 --- a/Silkstring/UI/SilkstringHighlighter.cs +++ b/Silkstring/UI/SilkstringHighlighter.cs @@ -13,10 +13,12 @@ namespace Silkstring.UI; public sealed class SilkstringHighlighter : ISyntaxHighlighter { private const PaletteIndex Error = PaletteIndex.CharLiteral; + private const PaletteIndex Flag = PaletteIndex.Preprocessor; private static readonly object Empty = new(); private static readonly Regex TokenRegex = new(@"\{(\d+\.\.\d+|\.\.\d+|\d+\.\.|\w+|\*)\}", RegexOptions.Compiled); private static readonly Regex StringRegex = new("\"[^\"]*\"?", RegexOptions.Compiled); + private static readonly Regex FlagRegex = new(@"(?> _definedVariables; @@ -67,6 +69,13 @@ public object Colorize(Span line, object? state) else PaintContent(line, text, exprStart); break; + case BlockKind.Until: + Paint(line, 0, exprStart, PaletteIndex.Keyword); + var (_, untilCond) = BlockInterpreter.ParseUntil(expression); + if (TryParseCondition(untilCond)) PaintContent(line, text, exprStart); + else Paint(line, exprStart, text.Length, Error); + break; + default: if (text.Length > 1 && text[0] == ':' && char.IsLetter(text[1])) { @@ -101,10 +110,18 @@ private static void PaintContent(Span line, string text, int from) if (from >= text.Length) return; foreach (Match m in StringRegex.Matches(text, from)) Paint(line, m.Index, m.Index + m.Length, PaletteIndex.String); + PaintFlags(line, text, from); PaintTokens(line, text, from); PaintMalformedBraces(line, from); } + private static void PaintFlags(Span line, string text, int from) + { + if (from >= text.Length) return; + foreach (Match m in FlagRegex.Matches(text, from)) + Paint(line, m.Index, m.Index + m.Length, Flag); + } + private static void PaintMalformedBraces(Span line, int from) { for (var i = from; i < line.Length; i++) @@ -147,6 +164,7 @@ public static void ApplyPalette(TextEditor editor) r.SetColor(PaletteIndex.String, U32(Palette.String)); r.SetColor(PaletteIndex.CharLiteral, U32(Palette.Error)); r.SetColor(PaletteIndex.LineNumber, U32(Palette.LineNumber)); + r.SetColor(PaletteIndex.Preprocessor, U32(Palette.Flag)); } private static uint U32(Vector4 c) => ImGui.ColorConvertFloat4ToU32(c); diff --git a/Silkstring/Windows/ConfigWindow.cs b/Silkstring/Windows/ConfigWindow.cs index 8eb9578..3c63b8e 100644 --- a/Silkstring/Windows/ConfigWindow.cs +++ b/Silkstring/Windows/ConfigWindow.cs @@ -53,6 +53,22 @@ public override void Draw() configuration.MarkDirty(); } + ImGui.Text("Until timeout (seconds)"); + var untilTimeout = configuration.UntilTimeoutSeconds; + ImGui.SetNextItemWidth(150); + if (ImGui.InputInt("###untilTimeout", ref untilTimeout, 1, 5)) + { + configuration.UntilTimeoutSeconds = untilTimeout; + configuration.MarkDirty(); + } + + var allowUnsafe = configuration.AllowUnsafeWaits; + if (ImGui.Checkbox("Allow unsafe (indefinite) waits", ref allowUnsafe)) + { + configuration.AllowUnsafeWaits = allowUnsafe; + configuration.Save(); + } + var multiline = configuration.MultilineCommands; if (ImGui.Checkbox("Multiline command entry", ref multiline)) { diff --git a/Silkstring/Windows/HelpWindow.cs b/Silkstring/Windows/HelpWindow.cs index d4f2085..53081c3 100644 --- a/Silkstring/Windows/HelpWindow.cs +++ b/Silkstring/Windows/HelpWindow.cs @@ -18,6 +18,7 @@ private enum Tab Variables = 1, Parameters = 2, Conditionals = 3, + Editor = 4, } private Tab _selectedTab = Tab.Commands; @@ -70,6 +71,7 @@ public override void Draw() case Tab.Variables: DrawVariablesHelp(); break; case Tab.Parameters: DrawParametersHelp(); break; case Tab.Conditionals: DrawConditionalsHelp(); break; + case Tab.Editor: DrawEditorHelp(); break; } ImGui.EndChild(); @@ -177,7 +179,7 @@ private void DrawCommandsHelp() ImGui.TextColored(Palette.Heading, "Conditionals"); ImGui.Separator(); ImGui.Spacing(); - ImGui.TextWrapped("Run commands only when a condition is true with :if / :else / :endif blocks, and pause with :wait. See the Conditionals tab."); + ImGui.TextWrapped("Run commands only when a condition is true with :if / :else / :endif blocks, pause with :wait, and hold until something happens with :until. See the Conditionals tab."); ImGui.Spacing(); ImGui.TextColored(Palette.Heading, "Macros"); @@ -318,7 +320,7 @@ private void DrawParameterRow(string token, string meaning) { ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.TextDisabled(token); + ImGui.TextColored(Palette.Parameter, token); ImGui.TableNextColumn(); ImGui.TextWrapped(meaning); } @@ -375,5 +377,54 @@ private void DrawConditionalsHelp() ImGui.TextDisabled("/say Up you get!"); ImGui.Unindent(); ImGui.Spacing(); + + ImGui.TextColored(Palette.Heading, "Until"); + ImGui.Separator(); + ImGui.Spacing(); + ImGui.TextWrapped("Hold an alias at a line until a condition becomes true with :until, then carry on. It is handy for waiting on your own state, for example holding a pose until an emote ends."); + ImGui.Spacing(); + ImGui.Indent(); + ImGui.TextDisabled("/sit"); + ImGui.TextDisabled(":until {emoting} == false"); + ImGui.TextDisabled("/say Up you get!"); + ImGui.Unindent(); + ImGui.Spacing(); + ImGui.TextWrapped("If the condition never comes true, :until gives up after a timeout you can set in Settings and then continues. Add -unsafe to wait with no time limit, which must be turned on in Settings first. Stop any running alias at any time with /silkstring cancel."); + ImGui.Spacing(); + } + + private void DrawEditorHelp() + { + ImGui.TextColored(Palette.Heading, "Editor Colors"); + ImGui.Separator(); + ImGui.Spacing(); + ImGui.TextWrapped("In the multiline editor, Silkstring colors each part of a line as you type. Anything it cannot make sense of is shown in the error color so you can spot a mistake before it runs. Every color here can be changed in Settings."); + ImGui.Spacing(); + if (ImGui.BeginTable("###editorColors", 3, ImGuiTableFlags.BordersInnerH | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter)) + { + ImGui.TableSetupColumn("Element", ImGuiTableColumnFlags.WidthFixed, 140); + ImGui.TableSetupColumn("Example", ImGuiTableColumnFlags.WidthFixed, 200); + ImGui.TableSetupColumn("Meaning", ImGuiTableColumnFlags.WidthStretch); + DrawColorRow(Palette.Command, "Command", "/emote waves", "A line that runs as a game command"); + DrawColorRow(Palette.Keyword, "Keyword", ":if :until :wait", "Control words that shape how an alias runs"); + DrawColorRow(Palette.Variable, "Variable", "{character}", "A value filled in when the alias runs"); + DrawColorRow(Palette.Parameter, "Parameter", "{0} {1..}", "An argument typed after the trigger"); + DrawColorRow(Palette.String, "Quoted text", "\"Jane Doe\"", "A quoted value kept as one piece"); + DrawColorRow(Palette.Flag, "Flag", "-unsafe", "An option that changes how a line behaves"); + DrawColorRow(Palette.Error, "Problem", ":wait abc", "Something malformed that will not work as written"); + ImGui.EndTable(); + } + ImGui.Spacing(); + } + + private void DrawColorRow(Vector4 color, string element, string example, string meaning) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextColored(color, element); + ImGui.TableNextColumn(); + ImGui.TextColored(color, example); + ImGui.TableNextColumn(); + ImGui.TextWrapped(meaning); } } diff --git a/Silkstring/Windows/VariablesWindow.cs b/Silkstring/Windows/VariablesWindow.cs index a59da56..87fb252 100644 --- a/Silkstring/Windows/VariablesWindow.cs +++ b/Silkstring/Windows/VariablesWindow.cs @@ -82,6 +82,12 @@ private void DrawList() if (ImGuiComponents.IconButton(variable.UniqueId, FontAwesomeIcon.Trash)) toRemove = variable; ImGui.EndDisabled(); ImGuiUtil.Tooltip("Hold Shift + Ctrl to delete", true); + ImGui.Indent(150); + ImGui.SetNextItemWidth(-60); + if (ImGui.InputTextWithHint($"###desc{variable.UniqueId}", "description (optional)", ref variable.Description, 200)) _store.MarkChanged(); + if (ImGui.IsItemDeactivatedAfterEdit()) _resolver.Refresh(); + ImGui.Unindent(150); + ImGui.Spacing(); } if (toRemove != null)