diff --git a/DawnLib/src/API/Terminal/TerminalTextModifier.cs b/DawnLib/src/API/Terminal/TerminalTextModifier.cs new file mode 100644 index 00000000..731c212f --- /dev/null +++ b/DawnLib/src/API/Terminal/TerminalTextModifier.cs @@ -0,0 +1,221 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Dawn.Internal; +using Dawn.Utils; + +namespace Dawn; + +/// +/// The match "index" for non-regex text modifications +/// +/// +/// First finds the index of the first match. +/// Last finds the index of the last match. +/// All finds every index of the match. +/// +public enum MatchIndex +{ + First, + Last, + All +} + +/// +/// The match "insert style" for non-regex text modifications +/// +/// +/// ReplaceMatch will just replace the matching text with the specified text. +/// Before will insert the specified text before the matching text. +/// After will insert the specified text after the matching text. +/// +[Flags] +public enum MatchInsert +{ + ReplaceMatch = 0, + Before = 1 << 0, + After = 1 << 1 +} + +/// +/// This class is used to modify TerminalNode display text after Terminal.TextPostProcess, which is run every time a node is loaded. +/// It will safely modify the resulting display text without modifying the node's displaytext directly. +/// +public class TerminalTextModifier +{ + // required for this class + private string TextToFind; + private IProvider AddedTextProvider; + + // optional stuff, defaults to replace every matching text in every node (null NodeToProcess & NodeKeyword), not using Regex + private bool RegexPattern = false; + private TerminalNode? NodeToProcess; + private string? NodeKeyword; + private MatchIndex IndexStyle = MatchIndex.All; + private MatchInsert InsertStyle = MatchInsert.ReplaceMatch; + + + /// + /// Create a TerminalTextModifier instance. It will automatically be subscribed to the event that runs during Terminal.TextPostProcess + /// + /// The specific text string you wish to find and modify + /// The string provider that will be used during text modification. If unsure what kind of provider to use, use + public TerminalTextModifier(string textToFind, IProvider additionalTextProvider) + { + TextToFind = textToFind; + AddedTextProvider = additionalTextProvider; + TerminalPatches.OnProcessNodeText += Process; + } + + /// + /// Use this method to change the string you are parsing for at runtime. + /// + /// The specific string you wish to find and modify + public TerminalTextModifier ChangeTextToFind(string textToFind) + { + TextToFind = textToFind; + return this; + } + + /// + /// Use this method to change the AddedTextProvider at runtime. + /// + /// The string provider that will be used during text modification + /// + public TerminalTextModifier ChangeAddedTextProvider(IProvider addedTextProvider) + { + AddedTextProvider = addedTextProvider; + return this; + } + + /// + /// Set a MatchInsert style for this text modifier, the default MatchInsert style will replace the matching text completely. + /// + /// + /// ReplaceMatch (default) completely replaces the matching text with your added content. + /// Before will insert your added content before the matching text. + /// After will insert your added content after the matching text. + /// + /// + /// NOTE: You can set this value to both Before and After to insert your content before and after the matching content (at the specified MatchIndex) + /// Also, MatchInsert is not used with Regex Pattern matching. + /// + public TerminalTextModifier SetInsertStyle(MatchInsert style) + { + InsertStyle = style; + return this; + } + + /// + /// Set the MatchIndex style for this text modifier. This will determine which matching text should be modified. + /// + /// The expected style which can be: First, Last, All + /// + /// NOTE: MatchIndex is not used with Regex Pattern matching. + /// + public TerminalTextModifier SetIndexStyle(MatchIndex indexStyle) + { + IndexStyle = indexStyle; + return this; + } + + /// + /// Set the TerminalNode this modifier should perform text post processing directly to a given TerminalNode + /// + /// The TerminalNode text post processing should be performed on + /// + /// NOTE: Ensure you are resetting this any time the node is destroyed and recreated! + /// + public TerminalTextModifier SetNodeDirect(TerminalNode node) + { + NodeToProcess = node; + return this; + } + + /// + /// Set TerminalNode this modifier should perform text post processing on by it's keyword + /// + /// The keyword typed into the terminal that returns the expected terminal node + public TerminalTextModifier SetNodeFromKeyword(string keyword) + { + NodeKeyword = keyword; + return this; + } + + /// + /// Set your TerminalTextModifier to use Regex pattern matching and replacing methods. + /// + /// True = Enabled, False = Disabled + /// + /// NOTE: Regex is less performant and should only be used for more complex pattern matching/replacements. + /// Also, in order to use the matching value in your replacement text use a regex pattern in the AddedTextProvider like + /// + public TerminalTextModifier UseRegexPattern(bool value) + { + RegexPattern = value; + return this; + } + + // called from event that is invoked after TextPostProcess + // most likely should not be public + internal void Process(ref string currentText, TerminalNode terminalNode) + { + // get node from keyword and assign it, only if node is null to not run every textpostprocess + if (!string.IsNullOrEmpty(NodeKeyword) && NodeToProcess == null) + NodeToProcess = GetNodeFromWord(NodeKeyword); + + // only skip processing when NodeToProcess is assigned and does not match + if (NodeToProcess != null && terminalNode != NodeToProcess) + return; + + // in case the provider tracks the amount of times it's been run, we only run it once + string textToAdd = AddedTextProvider.Provide(); + + // uses regex to replace the text rather than our simple string methods + if (RegexPattern) + { + Regex regex = new(TextToFind); + + // uncomment below if any issues are encountered with text modification via regex + /* DawnPlugin.Logger.LogDebug($""" + Processing text modifier (regex) on node - {terminalNode} + Regex ({TextToFind}) matches found {regex.Matches(TextToFind).Count} + AddedText - {textToAdd} (before regex format conversion) + """); */ + + currentText = regex.Replace(currentText, textToAdd); + return; + } + + // uncomment below if any issues are encountered with text modification + + /* DawnPlugin.Logger.LogDebug($""" + Processing text modifier (non-regex) on node - {terminalNode} + IndexStyle - {IndexStyle} + TextToFind - {TextToFind} + AddedText - {textToAdd} + """); */ + + currentText = currentText.TextModify(IndexStyle, InsertStyle, TextToFind, textToAdd); + } + + // maybe worthy of a terminal extension in the future, keeping private for now + private static TerminalNode GetNodeFromWord(string word) + { + if (TerminalRefs.Instance.TryGetKeyword(word, out TerminalKeyword? terminalKeyword)) + { + if (terminalKeyword.specialKeywordResult == null) + { + CompatibleNoun compatibleNoun = terminalKeyword.defaultVerb.compatibleNouns.FirstOrDefault(k => k.noun == terminalKeyword); + if (compatibleNoun != null) + return compatibleNoun.result; + } + else + { + return terminalKeyword.specialKeywordResult; + } + } + + return null!; + } +} diff --git a/DawnLib/src/DawnTesting.cs b/DawnLib/src/DawnTesting.cs index 51079c00..fe7a21ca 100644 --- a/DawnLib/src/DawnTesting.cs +++ b/DawnLib/src/DawnTesting.cs @@ -1,11 +1,49 @@ using System; +using System.Collections.Generic; namespace Dawn; internal class DawnTesting { + private static string AdjectivesExample() + { + List values = ["VERY ", "MANY ", "INCREDIBLY "]; + var rand = new Random(); + return values[rand.Next(0, values.Count)]; + } + internal static void TestCommands() { + // replaces any mention of the word planet for the word moon with a yellowish highlight in any node + TerminalTextModifier changeAll = new("planet", new SimpleProvider("moon")); + + // inserts quotes before and after any mention of the word commands in any node + TerminalTextModifier insertBeforeAndAfter = new TerminalTextModifier("commands", new SimpleProvider("\"")) + .SetInsertStyle(MatchInsert.Before | MatchInsert.After); + + // inserts a random adjective from a set list before the word useful in any node + TerminalTextModifier insertBefore = new TerminalTextModifier("useful", new FuncProvider(AdjectivesExample)) + .SetInsertStyle(MatchInsert.Before); + + // inserts an italicized ing after any mention of the phrase "the list" in any node + TerminalTextModifier insertAll = new TerminalTextModifier("the list", new SimpleProvider("ing")) + .SetInsertStyle(MatchInsert.After); + + // inserts some stylized dashes after the first 3 newlines of any node shown in the terminal (top of the screen) + TerminalTextModifier InsertFirst = new TerminalTextModifier("\n\n\n", new SimpleProvider("---\n")) + .SetInsertStyle(MatchInsert.After) + .SetIndexStyle(MatchIndex.First); + + // replaces the last new line of any node with some stylized dashes + TerminalTextModifier ReplaceLast = new TerminalTextModifier("\n", new SimpleProvider("\n----\n\n")) + .SetIndexStyle(MatchIndex.Last); + + // regex example, will make sure to capture the specific line containing record regardless of what kind of changes the other modifiers make + // text to add must use $& to keep the existing text + TerminalTextModifier helpAdd = new TerminalTextModifier("record.*\\S", new SimpleProvider("$&\n\n>VERSION\nDisplay Dawnlib's current version")) + .UseRegexPattern(true) + .SetNodeFromKeyword("help"); + TerminalCommandBasicInformation versionCommandBasicInformation = new TerminalCommandBasicInformation("DawnLibVersionCommand", "Test", "Prints the version of DawnLib!", ClearText.Result | ClearText.Query); DawnLib.DefineTerminalCommand(NamespacedKey.From("dawn_lib", "version_command"), versionCommandBasicInformation, builder => { diff --git a/DawnLib/src/Internal/Patches/TerminalPatches.cs b/DawnLib/src/Internal/Patches/TerminalPatches.cs index 44927ce6..33304934 100644 --- a/DawnLib/src/Internal/Patches/TerminalPatches.cs +++ b/DawnLib/src/Internal/Patches/TerminalPatches.cs @@ -7,10 +7,14 @@ namespace Dawn.Internal; static class TerminalPatches { + // below delegate/event is used for TerminalTextModifiers + internal delegate void TextPostProcess(ref string currentText, TerminalNode node); + internal static event TextPostProcess? OnProcessNodeText; internal static void Init() { On.Terminal.LoadNewNodeIfAffordable += HandlePredicate; On.Terminal.TextPostProcess += UpdateItemPrices; + On.Terminal.TextPostProcess += HandleTerminalTextModifiers; IL.Terminal.TextPostProcess += HideResults; IL.Terminal.TextPostProcess += UseFailedNameResults; } @@ -102,6 +106,15 @@ private static void HideResults(ILContext il) c.Emit(OpCodes.Brfalse, c.Instrs[targetIndex]); } + private static string HandleTerminalTextModifiers(On.Terminal.orig_TextPostProcess orig, Terminal self, string modifieddisplaytext, TerminalNode node) + { + modifieddisplaytext = orig(self, modifieddisplaytext, node); + + // all text modifiers are invoked here sequentially. So the last modifier to invoke may have vastly different text from the original + OnProcessNodeText?.Invoke(ref modifieddisplaytext, node); + + return modifieddisplaytext; + } private static string UpdateItemPrices(On.Terminal.orig_TextPostProcess orig, Terminal self, string modifieddisplaytext, TerminalNode node) { diff --git a/DawnLib/src/Utils/Extensions/StringExtensions.cs b/DawnLib/src/Utils/Extensions/StringExtensions.cs index ff1e41d8..c22e1dda 100644 --- a/DawnLib/src/Utils/Extensions/StringExtensions.cs +++ b/DawnLib/src/Utils/Extensions/StringExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Text.RegularExpressions; namespace Dawn.Utils; @@ -120,6 +119,48 @@ public static string GetExactMatch(this string input, string query, bool ignoreC return result; } + /// + /// Modify a string to insert/replace added content at the position of a specific matching string + /// + /// Full string being modified + /// This will determine what matching text values are modified. + /// This will determine whether we replace the matching string completely or insert our text before/after it + /// This is the matching string we are searching for to add our content. + /// This is the string content we are adding. + public static string TextModify(this string value, MatchIndex indexStyle, MatchInsert insertStyle, string matching, string addedContent) + { + if (string.IsNullOrEmpty(value) || !value.Contains(matching)) + { + return value; + } + + if (indexStyle is MatchIndex.All) + { + return insertStyle switch + { + MatchInsert.ReplaceMatch => value.Replace(matching, addedContent), + MatchInsert.Before => value.Replace(matching, addedContent + matching), + MatchInsert.After => value.Replace(matching, matching + addedContent), + MatchInsert.Before | MatchInsert.After => value.Replace(matching, addedContent + matching + addedContent), + _ => value, + }; + } + else + { + // depending on the style will either return the first or last index value of the textToFind + int index = (indexStyle is MatchIndex.First) ? value.IndexOf(matching) : value.LastIndexOf(matching); + + return insertStyle switch + { + MatchInsert.ReplaceMatch => value.Remove(index, matching.Length).Insert(index, addedContent), + MatchInsert.Before => value.Insert(index, addedContent), + MatchInsert.After => value.Insert(index + matching.Length, addedContent), + MatchInsert.Before | MatchInsert.After => value.Remove(index, matching.Length).Insert(index, addedContent + matching + addedContent), + _ => value, + }; + } + } + private static readonly Regex ConfigCleanerRegex = new(@"[\n\t""`\[\]']"); internal static string CleanStringForConfig(this string input) {