Advertisement
Powerhoof

PowerText

Mar 20th, 2017
722
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 15.00 KB | None | 0 0
  1. //-----------------------------------------
  2. //          PowerText
  3. // More info/instructions:
  4. //  http://powerhoof.com/PowerText
  5. //  dave@powerhoof.com
  6. //  @DuzzOnDrums
  7. //----------------------------------------
  8.  
  9. using UnityEngine;
  10. using System.Collections;
  11. using System.Collections.Generic;
  12. using System.Text.RegularExpressions;
  13. using System.IO;
  14. using System.Text;
  15. using PowerTools;
  16. using PowerTools.PowerTextUtils;
  17.  
  18. namespace PowerTools
  19. {
  20.  
  21. #region Class: PowerText
  22.  
  23. /** <summary>
  24. Usage:
  25. - Create a new PowerText().
  26. - Call Parse(string inputText); to parse the input text.
  27. - Call GetString(string groupName); to generate a string from the name of a group.
  28.  
  29. See http://tools.powerhoof.com for examples
  30. */
  31. public class PowerText
  32. {
  33.     #endregion
  34.     #region Definitions
  35.  
  36.     // Matches any nonspace character between two semicolons, eg. :ANIMAL:
  37.     static readonly Regex s_regexGroup = new Regex(@"^:(?<group> \S+?):", RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);  
  38.  
  39.     // Matches a stuff on a line within a group. This is probably insantely inefficient :D
  40.     static readonly Regex s_regexLine = new Regex(
  41.             @"(^//.*) |"                        // Comments are a line starting with '//'
  42.             + @"(^\[(?<weight>\d*\.?\d+)\]) | " // Weight is a group that's just got a number in it at the start of a line (this controls how often that string will be used)
  43.             + @"(?<empty>\[\s*\]) | "           // Empty line is ignored unless it has [] on it
  44.             + @"(?<ps> \[\#\]) | "              // Start of pluralized denoted by [#] (eg: I have [#]40 hat[s])
  45.             + @"(?<pe> \[\\\#\]) | "             // End of pluralized denoted by [\#] (eg: I have [#]40 hat[s])
  46.             + @"(?<s>\[s\]) | (?<es>\[es\]) | (\[/(?<plr> \w+)\]) | "  // Plurals are [s], [es] or [/PluralOfPreviousWord]
  47.             + @"(\[(?<ref> \S+?)\]) | "          // Ref (reference to another group) is nonspace character between brackets eg. [ANIMAL] // TODO: Check why that 2nd ? is in there...
  48.             + @"(?<text> [^\[\r]+)",             // Text is anything that's not a new line.
  49.         RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
  50.  
  51.     // Matches first letter of line, or first letter after a full stop or equivalent
  52.     static readonly Regex s_regexCapital = new Regex(@"((?<=^\W*)\w) | ((?<=[.:!?]\s*)\w)", RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
  53.  
  54.     // Matches a followed by space and a vowel (unless directly after non-whitespace, ie the end of a word)
  55.     static readonly Regex s_regexAn = new Regex(@"(?<!\S)a(?=\s+[aeiou])", RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
  56.  
  57.     static readonly char[] LINEDELIM = {'\n'};
  58.  
  59.     static readonly string MATCH_GROUP = "group";
  60.     static readonly string MATCH_EMPTY = "empty";
  61.     static readonly string MATCH_WEIGHT = "weight";
  62.     static readonly string MATCH_REF = "ref";
  63.     static readonly string MATCH_TEXT = "text";
  64.     static readonly string MATCH_PLURALISE_START = "ps";
  65.     static readonly string MATCH_PLURALISE_END = "pe";
  66.     static readonly string MATCH_PLURAL_S = "s";
  67.     static readonly string MATCH_PLURAL_ES = "es";
  68.     static readonly string MATCH_PLURAL = "plr";
  69.  
  70.     #endregion
  71.     #region Vars: private
  72.  
  73.     Dictionary<string, PowerTextNodeGroup> m_groups = new Dictionary<string, PowerTextNodeGroup>();
  74.  
  75.     #endregion
  76.     #region Funcs: public
  77.  
  78.     /// Find a string from the group name (the thing between the colons. eg. :WeaponName: )
  79.     public string GetString(string groupName)
  80.     {
  81.         string result = string.Empty;
  82.         PowerTextNodeGroup group = null;
  83.         if ( m_groups.TryGetValue(groupName.ToUpper(), out group) )
  84.         {
  85.             bool plural = false;
  86.             StringBuilder builder = new StringBuilder();
  87.             group.Build(builder, ref plural);
  88.             result = builder.ToString();   
  89.             result = PostProcess(result);
  90.         }
  91.         return result;
  92.     }
  93.  
  94.  
  95.     /// Takes the input text and parses it. Do this before "GetString"
  96.     public void Parse(string text)
  97.     {
  98.         string[] textLines = text.Split(LINEDELIM,10000);
  99.  
  100.         PowerTextNodeGroup currentGroup = null;
  101.         PowerTextNodeLine currentLine = null;
  102.  
  103.         for (int i = 0; i < textLines.Length; ++i )
  104.         {
  105.             string lineText = textLines[i];            
  106.             Match groupMatch = s_regexGroup.Match(lineText);
  107.             if ( groupMatch.Success )
  108.             {
  109.                 // Parse group name
  110.                     string groupName = groupMatch.Groups[MATCH_GROUP].Value;
  111.                 currentGroup = FindOrCreateGroup(groupName);
  112.             }
  113.             else if ( currentGroup != null )
  114.             {
  115.                 // Add node for line
  116.                 currentLine = new PowerTextNodeLine();
  117.  
  118.                 bool lineHasContent = false;
  119.                 PowerTextNodeString lastStringNode = null;
  120.    
  121.                 MatchCollection lineMatches = s_regexLine.Matches(lineText);
  122.                 foreach( Match lineMatch in lineMatches )
  123.                 {
  124.                     if ( lineMatch.Success && lineMatch.Groups == null )
  125.                         continue;                  
  126.  
  127.                     if ( lineMatch.Groups[MATCH_EMPTY].Success)
  128.                     {
  129.                         lineHasContent = true;
  130.                         currentLine.Append( (PowerTextNodeString)(string.Empty) );
  131.                     }
  132.                     if ( lineMatch.Groups[MATCH_WEIGHT].Success)
  133.                     {
  134.                         lineHasContent = true;
  135.                             float.TryParse(lineMatch.Groups[MATCH_WEIGHT].Value, out currentLine.m_weight);
  136.                     }
  137.                     if ( lineMatch.Groups[MATCH_REF].Success)
  138.                     {
  139.                         lineHasContent = true;
  140.                             currentLine.Append( FindOrCreateGroup(lineMatch.Groups[MATCH_REF].Value) );
  141.                     }
  142.                     if ( lineMatch.Groups[MATCH_TEXT].Success )
  143.                     {
  144.                         string value = lineMatch.Groups[MATCH_TEXT].Value;
  145.                         if ( string.IsNullOrEmpty(value) == false )
  146.                         {
  147.                             lineHasContent = true;
  148.                             lastStringNode = (PowerTextNodeString)value;
  149.                             currentLine.Append( lastStringNode );
  150.                         }
  151.                     }
  152.                     if ( lineMatch.Groups[MATCH_PLURALISE_START].Success )
  153.                     {
  154.                         lineHasContent = true;
  155.                         currentLine.Append( new PowerTextNodePluralFlag(true) );
  156.                     }
  157.                     if ( lineMatch.Groups[MATCH_PLURALISE_END].Success )
  158.                     {
  159.                         currentLine.Append( new PowerTextNodePluralFlag(false) );
  160.                     }
  161.  
  162.                     // Parse "plural" stuff
  163.                     if ( lastStringNode != null )
  164.                     {
  165.                         if ( lineMatch.Groups[MATCH_PLURAL_S].Success ) lastStringNode.SetPlural(PowerTextNodeString.ePluralType.s);
  166.                         if ( lineMatch.Groups[MATCH_PLURAL_ES].Success ) lastStringNode.SetPlural(PowerTextNodeString.ePluralType.es);
  167.                         if ( lineMatch.Groups[MATCH_PLURAL].Success )
  168.                         {
  169.                             string value = lineMatch.Groups[MATCH_PLURAL].Value;
  170.                             if ( string.IsNullOrEmpty(value) == false )
  171.                             {
  172.                                 lastStringNode.SetPlural(PowerTextNodeString.ePluralType.Custom, value);
  173.                             }                              
  174.                         }
  175.                     }
  176.                 }      
  177.                          
  178.                 if ( lineHasContent )
  179.                     currentGroup.AddOption(currentLine);                
  180.             }
  181.         }
  182.     }
  183.  
  184.     /// Sets a text variable by name (in the script, write [name] to use the variable)
  185.     public void SetVariable(string name, string value)
  186.     {
  187.         ParseGroup(name, value);
  188.     }
  189.  
  190.     /// Sets an int variable by name, marking it as plural if it's not 1 (in the script, write [name] to use the variable)
  191.     public void SetVariable(string name, int value)
  192.     {
  193.         if ( value != 1 )            
  194.             ParseGroup(name, value.ToString()+"[#]");
  195.         else
  196.             ParseGroup(name, value.ToString());
  197.     }
  198.  
  199.  
  200.     #endregion
  201.     #region Funcs: Private
  202.  
  203.     PowerTextNodeGroup FindOrCreateGroup(string name)
  204.     {
  205.         PowerTextNodeGroup node = null;
  206.         name = name.ToUpper();
  207.         if ( m_groups.TryGetValue(name, out node) )
  208.         {
  209.             return node;
  210.         }
  211.         node = new PowerTextNodeGroup();
  212.         m_groups.Add(name, node);
  213.         return node;
  214.     }
  215.  
  216.     PowerTextNodeGroup ParseGroup(string name, string lines)
  217.     {
  218.         PowerTextNodeGroup group = FindOrCreateGroup(name);
  219.         return group;
  220.     }    
  221.  
  222.     string PostProcess( string text )
  223.     {
  224.         // Capitalise first alphanumeric of sentance, or after . ; - ! ?
  225.         text = s_regexCapital.Replace(text, ReplaceCapital);
  226.  
  227.         // Change a to an
  228.         return s_regexAn.Replace(text,ReplaceAn);
  229.     }
  230.      
  231.     string ReplaceAn(Match match)
  232.     {
  233.         return string.Concat(match.ToString(),'n');
  234.     }
  235.  
  236.     string ReplaceCapital(Match match)
  237.     {
  238.         return match.ToString().ToUpper();
  239.     }
  240.     #endregion
  241. }
  242.  
  243.  
  244.  
  245. #region Class: PowerTextNodes
  246.  
  247. // Interface to PowerTextNode with generic function to generate the string
  248. public interface IPowerTextNode
  249. {
  250.     void Build( StringBuilder builder, ref bool pluralize );
  251.     float GetWeight();
  252. }
  253.  
  254. // Node that returns random node from list
  255. public class PowerTextNodeGroup: IPowerTextNode
  256. {  
  257.     public List<IPowerTextNode> m_options = null;
  258.     ShuffledIndex m_shuffledIndex;
  259.     float m_maxWeight = 1;
  260.  
  261.     public void Build( StringBuilder builder, ref bool plural )
  262.     {
  263.         if ( m_options == null || m_options.Count <= 0 )
  264.             return;
  265.  
  266.         if ( m_shuffledIndex == null ) m_shuffledIndex = new ShuffledIndex(m_options.Count);
  267.  
  268.         for ( int i = 0; i <= m_shuffledIndex.Length*2; ++i ) // Loop through shuffled list until find an entry that satisfies weight constraint (only necessary if all have weight of zero).
  269.         {
  270.             m_shuffledIndex.Next();
  271.             float weight = m_options[m_shuffledIndex].GetWeight();
  272.             if ( weight >= m_maxWeight || (weight > 0.0f && weight >= Random.value * m_maxWeight) )
  273.             {
  274.                 m_options[m_shuffledIndex].Build(builder, ref plural);
  275.                 break;
  276.             }
  277.         }
  278.  
  279.     }
  280.  
  281.     public float GetWeight() { return 1.0f; }
  282.  
  283.     public void AddOption(IPowerTextNode node)
  284.     {
  285.         if ( m_options == null )
  286.             m_options = new List<IPowerTextNode>();
  287.         m_options.Add(node);
  288.         m_maxWeight = Mathf.Max(m_maxWeight, node.GetWeight());
  289.     }
  290.  
  291. }
  292.  
  293. // Node that returns a bunch of nodes concatenated (eg: "hello " + [name] + " how are you today?" is 3 PowerTextNodeLines)
  294. public class PowerTextNodeLine: IPowerTextNode
  295. {
  296.     public List<IPowerTextNode> m_strings;
  297.  
  298.     public float m_weight = 1;
  299.  
  300.     public void Build( StringBuilder builder, ref bool plural )
  301.     {
  302.         if ( m_strings == null )
  303.             return;
  304.         for ( int i = 0; i < m_strings.Count; ++i )                
  305.             m_strings[i].Build(builder, ref plural);                
  306.     }
  307.  
  308.     public float GetWeight() { return m_weight; }
  309.  
  310.     public void Append(IPowerTextNode node)
  311.     {
  312.         if ( m_strings == null )
  313.             m_strings = new List<IPowerTextNode>();
  314.         m_strings.Add(node);        
  315.     }
  316. }
  317.  
  318. public class PowerTextNodeString : IPowerTextNode
  319. {
  320.     // Text nodes can have an alternate plural, migth j ust append 's' or 'es' or might replace the whole word
  321.     public enum ePluralType
  322.     {
  323.         None,
  324.         s,
  325.         es,
  326.         Custom
  327.     }
  328.  
  329.     string m_string = null;
  330.     ePluralType m_pluralType = ePluralType.None;
  331.     string m_pluralString = null;
  332.  
  333.     public PowerTextNodeString(string str) {m_string = str;}
  334.     public static implicit operator PowerTextNodeString(string str)
  335.     {
  336.         return new PowerTextNodeString(str);
  337.     }
  338.  
  339.     public void SetPlural( ePluralType type, string customString = null )
  340.     {
  341.         m_pluralType = type;
  342.         m_pluralString = customString;
  343.     }
  344.  
  345.     public void Build( StringBuilder builder, ref bool plural )
  346.     {
  347.         // Check if it should make the text end with a plural
  348.         if ( m_pluralType != ePluralType.None && plural )
  349.         {
  350.             if ( m_pluralType == ePluralType.Custom )
  351.             {
  352.                 // Custom plural means it'll replace the entire last word (eg:  life/lives)
  353.                 int pos = m_string.LastIndexOf(' ');
  354.                 if ( pos > 0 && pos < m_string.Length )
  355.                     builder.Append(m_string.Substring(0,pos));
  356.                 builder.Append(m_pluralString);
  357.             }
  358.             else if ( m_pluralType == ePluralType.s )
  359.             {
  360.                 builder.Append(m_string);
  361.                 builder.Append('s');
  362.             }
  363.             else if ( m_pluralType == ePluralType.es )
  364.             {
  365.                 builder.Append(m_string);
  366.                 builder.Append("es");
  367.             }
  368.         }
  369.         else
  370.         {
  371.             builder.Append(m_string);
  372.         }
  373.     }
  374.  
  375.     public float GetWeight() { return 1.0f; }
  376. }
  377.  
  378. public class PowerTextNodePluralFlag : IPowerTextNode
  379. {
  380.     bool m_pluralize = false;
  381.     public PowerTextNodePluralFlag(bool pluralize) { m_pluralize = pluralize; }
  382.     public void Build( StringBuilder builder, ref bool pluralize ) { pluralize = m_pluralize; }
  383.     public float GetWeight() { return 1.0f; }
  384. }
  385.  
  386.  
  387. #endregion
  388.  
  389. }
  390.  
  391. #region Class: Utils
  392.  
  393. // Utils- Normally these would be in seperate file.
  394.  
  395. namespace PowerTools.PowerTextUtils
  396. {
  397.  
  398. public class ShuffledIndex
  399. {
  400.     int m_current = -2; // -2 so if next() is called first, it'll still reshuffle the first time because the index will still be invalid
  401.     int[] m_ids = null;
  402.    
  403.     public ShuffledIndex(int count)
  404.     {
  405.         m_ids = new int[count];
  406.         for (int i = 0; i < count; ++i )
  407.         {
  408.             m_ids[i] = i;
  409.         }
  410.     }
  411.    
  412.     public int Next()
  413.     {
  414.         m_current++;
  415.         return (int)this;
  416.     }
  417.  
  418.     public static implicit operator int(ShuffledIndex m)
  419.     {
  420.         if ( m.m_ids.Length == 0 )
  421.             return -1;
  422.         if ( m.m_current < 0 || m.m_current >= m.m_ids.Length )
  423.         {          
  424.             int previousValue = m.m_ids[m.m_ids.Length - 1];
  425.            
  426.             m.m_ids.Shuffle();          
  427.             m.m_current = 0;
  428.            
  429.             // Check if it's repeating the last value and if so, swap the first elements around so you don't get 2 in a row (only if more than 1 element)
  430.             if ( m.m_ids.Length > 1 && previousValue == m.m_ids[0] )
  431.                 Utils.Swap(ref m.m_ids[0], ref m.m_ids[1]);
  432.         }
  433.         return m.m_ids[m.m_current];
  434.     }
  435.  
  436.     public int Length { get{ return m_ids.Length; } }
  437. }
  438.  
  439. public static class Utils
  440. {
  441.     // Extention method to shuffle an array
  442.    public static void Shuffle<T>( this T[] list )
  443.    {
  444.         T temp;
  445.         int j = 0;
  446.         int count = list.Length;
  447.         for ( int i = count - 1; i >= 1; --i )
  448.         {
  449.             j = Random.Range(0,i+1);
  450.             // swap
  451.             temp = list[i];
  452.             list[i] = list[j];
  453.             list[j] = temp;
  454.         }
  455.     }
  456.  
  457.     // Usually I have this in a util class it's pretty useful :)
  458.     public static void Swap<T>(ref T lhs, ref T rhs)
  459.     {
  460.         T temp;
  461.         temp = lhs;
  462.         lhs = rhs;
  463.         rhs = temp;
  464.     }
  465. }
  466.  
  467. }
  468. #endregion
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement