Skip to content
Open
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
199 changes: 199 additions & 0 deletions DawnLib/src/API/Terminal/TerminalTextModifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System.Linq;
using System.Text.RegularExpressions;
using Dawn.Internal;
using Dawn.Utils;

namespace Dawn;

/// <summary>
/// For simple text modifications.
/// </summary>
/// <remarks>
/// FirstIndex = get the Index of first match,
/// LastIndex = get the Index of last match,
/// EveryIndex = get every match
/// </remarks>
public enum TextIndex
{
FirstIndex,
LastIndex,
EveryIndex
}

/// <summary>
/// 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.
/// </summary>
public class TerminalTextModifier
{
// required for this class
private string TextToFind;
private IProvider<string> AddedTextProvider;

// optional stuff, defaults to replace every matching text in every node (null NodeToProcess & NodeKeyword), not using Regex
private bool ShouldReplaceText = true;
private bool RegexPattern = false;
private TerminalNode? NodeToProcess;
private string? NodeKeyword;
private TextIndex IndexStyle = TextIndex.EveryIndex;


/// <summary>
/// Create a TerminalTextModifier instance. It will automatically be subscribed to the event that runs during Terminal.TextPostProcess
/// </summary>
/// <param name="textToFind">The specific text string you wish to find and modify</param>
/// <param name="additionalTextProvider">The string provider that will be used during text modification. If unsure what kind of provider to use, use SimpleProvider</param>
public TerminalTextModifier(string textToFind, IProvider<string> additionalTextProvider)
{
TextToFind = textToFind;
AddedTextProvider = additionalTextProvider;
TerminalPatches.OnProcessNodeText += Process;
}

/// <summary>
/// Use this method to change the TextToFind string at runtime.
/// </summary>
/// <param name="textToFind">The specific text string you wish to find and modify</param>
public TerminalTextModifier ChangeTextToFind(string textToFind)
{
TextToFind = textToFind;
return this;
}

/// <summary>
/// Use this method to change the AddedTextProvider at runtime.
/// </summary>
/// <param name="addedTextProvider">The string provider that will be used during text modification</param>
/// <returns></returns>
public TerminalTextModifier ChangeAddedTextProvider(IProvider<string> addedTextProvider)
{
AddedTextProvider = addedTextProvider;
return this;
}

/// <summary>
/// Determine whether this textmodifier should replace the text or simply insert after it.
/// </summary>
/// <param name="value">True = Replace, False = Insert After</param>
/// <remarks>
/// NOTE: This value does not need to be set when using Regex instead of simple matching.
/// </remarks>
public TerminalTextModifier SetReplaceText(bool value)
{
ShouldReplaceText = value;
return this;
}

/// <summary>
/// Set the TextIndex style for simple text modification. This will determine what matching text is modified.
/// </summary>
/// <param name="indexStyle">The expected style which can be: FirstIndex, LastIndex, EveryIndex</param>
public TerminalTextModifier SetIndexStyle(TextIndex indexStyle)
{
IndexStyle = indexStyle;
return this;
}

/// <summary>
/// Set the TerminalNode this modifier should perform text post processing directly to a given TerminalNode
/// </summary>
/// <param name="node">The TerminalNode text post processing should be performed on</param>
/// <remarks>
/// NOTE: Ensure you are resetting this any time the node is destroyed and recreated!
/// </remarks>
public TerminalTextModifier SetNodeDirect(TerminalNode node)
{
NodeToProcess = node;
return this;
}

/// <summary>
/// Set TerminalNode this modifier should perform text post processing on by it's keyword
/// </summary>
/// <param name="keyword">The keyword typed into the terminal that returns the expected terminal node</param>
public TerminalTextModifier SetNodeFromKeyword(string keyword)
{
NodeKeyword = keyword;
return this;
}

/// <summary>
/// Set your TerminalTextModifier to use Regex pattern matching and replacing methods.
/// </summary>
/// <param name="value">True = Enabled, False = Disabled</param>
/// <remarks>
/// 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 <![CDATA[$&]]>
/// </remarks>
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);
DawnPlugin.Logger.LogDebug($"""
Processing text modifier (regex) on node - {terminalNode}
Regex ({TextToFind}) matches found {regex.Matches(TextToFind).Count}
AddedText - {textToAdd} (before regex format conversion)
""");
DawnPlugin.Logger.LogDebug($"");
currentText = regex.Replace(currentText, textToAdd);
return;
}

DawnPlugin.Logger.LogDebug($"""
Processing text modifier (non-regex) on node - {terminalNode}
IndexStyle - {IndexStyle}
TextToFind - {TextToFind}
AddedText - {textToAdd}
""");

if (ShouldReplaceText)
{
currentText = currentText.TextReplacer(IndexStyle, TextToFind, textToAdd);
}
else
{
currentText = currentText.TextAdder(IndexStyle, TextToFind, textToAdd);
}
}

// could probably be private or moved to another class if useful elsewhere, not sure at the moment
internal 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!;
}
}
17 changes: 17 additions & 0 deletions DawnLib/src/DawnTesting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ internal class DawnTesting
{
internal static void TestCommands()
{
TerminalTextModifier changeAll = new("planet", new SimpleProvider<string>("<mark=#ffff001A>moon</mark>"));
TerminalTextModifier insertAll = new TerminalTextModifier("the list", new SimpleProvider<string>("<i>ing</i>"))
.SetReplaceText(false);
TerminalTextModifier InsertFirst = new TerminalTextModifier("\n\n\n", new SimpleProvider<string>("<align=\"center\">---</align>\n"))
.SetReplaceText(false)
.SetIndexStyle(TextIndex.FirstIndex);
TerminalTextModifier ReplaceLast = new TerminalTextModifier("\n", new SimpleProvider<string>("\n<align=\"center\">----</align>\n\n"))
.SetIndexStyle(TextIndex.LastIndex);


// 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<string>("$&\n\n>VERSION\nDisplay Dawnlib's current version"))
.SetReplaceText(true)
.UseRegexPattern(true)
.SetNodeFromKeyword("help");

TerminalCommandBasicInformation versionCommandBasicInformation = new TerminalCommandBasicInformation("DawnLibVersionCommand", "Test", "Prints the version of DawnLib!", ClearText.Result | ClearText.Query);
DawnLib.DefineTerminalCommand(NamespacedKey<DawnTerminalCommandInfo>.From("dawn_lib", "version_command"), versionCommandBasicInformation, builder =>
{
Expand Down
13 changes: 13 additions & 0 deletions DawnLib/src/Internal/Patches/TerminalPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = null!;
internal static void Init()
{
On.Terminal.LoadNewNodeIfAffordable += HandlePredicate;
On.Terminal.TextPostProcess += UpdateItemPrices;
On.Terminal.TextPostProcess += HandleTerminalTextModifiers;
IL.Terminal.TextPostProcess += HideResults;
IL.Terminal.TextPostProcess += UseFailedNameResults;
}
Expand Down Expand Up @@ -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)
{
Expand Down
57 changes: 56 additions & 1 deletion DawnLib/src/Utils/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace Dawn.Utils;
Expand Down Expand Up @@ -120,6 +119,62 @@ public static string GetExactMatch(this string input, string query, bool ignoreC
return result;
}

/// <summary>
/// Replace matching string with a replacement string
/// </summary>
/// <param name="value">Full string being modified</param>
/// <param name="indexStyle">This will determine what matching text values are modified.</param>
/// <param name="matching">This is the matching string we are finding and replacing</param>
/// <param name="replacement">This is the string content we are replacing the matching string with</param>
public static string TextReplacer(this string value, TextIndex indexStyle, string matching, string replacement)
{
if (string.IsNullOrEmpty(value) || !value.Contains(matching))
{
//DawnPlugin.Logger.LogWarning($"TextReplacer: Unable to find expected text - {textToReplace} in string - {value}. Text remains unchanged");
return value;
}

if (indexStyle is TextIndex.EveryIndex)
{
// replace every instance of our matching string
return value.Replace(matching, replacement);
}
else
{
// depending on the style will either return the first or last index value of the textToReplace
int index = (indexStyle is TextIndex.FirstIndex) ? value.IndexOf(matching) : value.LastIndexOf(matching);
return value.Remove(index, matching.Length).Insert(index, replacement);
}
}

/// <summary>
/// Add text after a matching string value
/// </summary>
/// <param name="value">Full string being modified</param>
/// <param name="indexStyle">This will determine what matching text values are modified.</param>
/// <param name="matching">This is the matching string we are finding and adding content after</param>
/// <param name="addedContent">This is the string content we are adding after the matching string</param>
public static string TextAdder(this string value, TextIndex indexStyle, string matching, string addedContent)
{
if (string.IsNullOrEmpty(value) || !value.Contains(matching))
{
//DawnPlugin.Logger.LogWarning($"TextAdder: Unable to find expected text - {textToFind} in string - {value}. Text remains unchanged");
return value;
}

if (indexStyle is TextIndex.EveryIndex)
{
// add content to every instance of our matching string
return value.Replace(matching, matching + addedContent);
}
else
{
// depending on the style will either return the first or last index value of the textToFind
int index = (indexStyle is TextIndex.FirstIndex) ? value.IndexOf(matching) : value.LastIndexOf(matching);
return value.Insert(index + matching.Length, addedContent);
}
}

private static readonly Regex ConfigCleanerRegex = new(@"[\n\t""`\[\]']");
internal static string CleanStringForConfig(this string input)
{
Expand Down
Loading