Advertisement
Powerhoof

PowerText

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