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)
{