Browse Source

Extensible string substitution system

String substitution now works for Fungus variables, localisation keys,
Lua global variables and string variable keys. The system can be easily
extended by implementing the ISubstitutionHandler interface.
master
chrisgregan 9 years ago
parent
commit
734ca8c870
  1. 92
      Assets/Fungus/Flowchart/Scripts/Flowchart.cs
  2. 29
      Assets/Fungus/Lua/Scripts/LuaUtils.cs
  3. 76
      Assets/Fungus/Lua/Scripts/StringSubstituter.cs
  4. 12
      Assets/Fungus/Lua/Scripts/StringSubstituter.cs.meta
  5. 55
      Assets/Fungus/Narrative/Scripts/Localization.cs
  6. 9
      Assets/Tests/StringSubstitution.meta
  7. 1745
      Assets/Tests/StringSubstitution/StringSubstitutionTests.unity
  8. 8
      Assets/Tests/StringSubstitution/StringSubstitutionTests.unity.meta
  9. 0
      Assets/Tests/TestAssets/CSV.meta
  10. 0
      Assets/Tests/TestAssets/CSV/localization_Commands.csv
  11. 0
      Assets/Tests/TestAssets/CSV/localization_Commands.csv.meta
  12. 0
      Assets/Tests/TestAssets/CSV/localization_Mac.csv
  13. 0
      Assets/Tests/TestAssets/CSV/localization_Mac.csv.meta
  14. 0
      Assets/Tests/TestAssets/CSV/localization_Narrative.csv
  15. 0
      Assets/Tests/TestAssets/CSV/localization_Narrative.csv.meta
  16. 0
      Assets/Tests/TestAssets/CSV/localization_Windows.csv
  17. 0
      Assets/Tests/TestAssets/CSV/localization_Windows.csv.meta
  18. 0
      Assets/Tests/TestAssets/CSV/localization_dottest.csv
  19. 0
      Assets/Tests/TestAssets/CSV/localization_dottest.csv.meta

92
Assets/Fungus/Flowchart/Scripts/Flowchart.cs

@ -24,7 +24,7 @@ namespace Fungus
* Flowchart objects may be edited visually using the Flowchart editor window.
*/
[ExecuteInEditMode]
public class Flowchart : MonoBehaviour
public class Flowchart : MonoBehaviour, StringSubstituter.ISubstitutionHandler
{
/**
* The current version of the Flowchart. Used for updating components.
@ -159,6 +159,8 @@ namespace Fungus
protected static bool eventSystemPresent;
protected StringSubstituter stringSubstituer;
/**
* Returns the next id to assign to a new flowchart item.
* Item ids increase monotically so they are guaranteed to
@ -191,7 +193,7 @@ namespace Fungus
{
CheckEventSystem();
}
// There must be an Event System in the scene for Say and Menu input to work.
// This method will automatically instantiate one if none exists.
protected virtual void CheckEventSystem()
@ -946,64 +948,84 @@ namespace Fungus
return executingBlocks;
}
public virtual string SubstituteVariables(string text)
/**
* Implementation of StringSubstituter.ISubstitutionHandler which matches any public variable in the Flowchart.
* To perform full variable substitution with all substitution handlers in the scene, you should
* use the SubstituteVariables() method instead.
*/
[MoonSharp.Interpreter.MoonSharpHidden]
public virtual string SubstituteStrings(string input)
{
string subbedText = text;
string subbedText = input;
// Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}");
// Match the regular expression pattern against a text string.
var results = r.Matches(text);
var results = r.Matches(input);
foreach (Match match in results)
{
string key = match.Value.Substring(2, match.Value.Length - 3);
// Look for any matching variables in this Flowchart first (public or private)
// Look for any matching public variables in this Flowchart
foreach (Variable variable in variables)
{
if (variable == null)
continue;
if (variable.key == key)
if (variable.scope == VariableScope.Public &&
variable.key == key)
{
string value = variable.ToString();
subbedText = subbedText.Replace(match.Value, value);
}
}
}
return subbedText;
}
/**
* Substitute variables in the input text with the format {$VarName}
* This will first match with private variables in this Flowchart, and then
* with public variables in all Flowcharts in the scene (and any component
* in the scene that implements StringSubstituter.ISubstitutionHandler).
*/
public virtual string SubstituteVariables(string input)
{
if (stringSubstituer == null)
{
stringSubstituer = new StringSubstituter();
}
string subbedText = input;
// Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}");
// Match the regular expression pattern against a text string.
var results = r.Matches(input);
foreach (Match match in results)
{
string key = match.Value.Substring(2, match.Value.Length - 3);
// Now search all public variables in all scene Flowcharts in the scene
foreach (Flowchart flowchart in cachedFlowcharts)
// Look for any matching private variables in this Flowchart first
foreach (Variable variable in variables)
{
if (flowchart == this)
{
// We've already searched this flowchart
if (variable == null)
continue;
}
foreach (Variable variable in flowchart.variables)
{
if (variable == null)
continue;
if (variable.scope == VariableScope.Public &&
variable.key == key)
{
string value = variable.ToString();
subbedText = subbedText.Replace(match.Value, value);
}
if (variable.scope == VariableScope.Private &&
variable.key == key)
{
string value = variable.ToString();
subbedText = subbedText.Replace(match.Value, value);
}
}
// Next look for matching localized string
string localizedString = Localization.GetLocalizedString(key);
if (localizedString != null)
{
subbedText = subbedText.Replace(match.Value, localizedString);
}
}
return subbedText;
// Now do all other substitutions in the scene
return stringSubstituer.SubstituteStrings(subbedText);
}
}

29
Assets/Fungus/Lua/Scripts/LuaUtils.cs

@ -13,7 +13,7 @@ using MoonSharp.RemoteDebugger;
namespace Fungus
{
public class LuaUtils : LuaEnvironment.Initializer
public class LuaUtils : LuaEnvironment.Initializer, StringSubstituter.ISubstitutionHandler
{
/// <summary>
/// Lua script file which defines the global string table used for localisation.
@ -57,6 +57,8 @@ namespace Fungus
/// </summary>
protected LuaEnvironment luaEnvironment;
protected StringSubstituter stringSubstituter;
/// <summary>
/// Called by LuaEnvironment when initializing.
/// </summary>
@ -193,6 +195,8 @@ namespace Fungus
LuaEnvironment.LogException(ex.DecoratedMessage, stringTable.text);
}
}
stringSubstituter = new StringSubstituter();
}
/// <summary>
@ -219,32 +223,34 @@ namespace Fungus
}
/// <summary>
/// Implementation of StringSubstituter.ISubstitutionHandler
/// Substitutes specially formatted tokens in the text with global variables and string table values.
/// The string table value used depends on the currently loaded string table and active language.
/// </summary>
public virtual string Substitute(string text)
[MoonSharpHidden]
public virtual string SubstituteStrings(string input)
{
if (luaEnvironment == null)
{
UnityEngine.Debug.LogError("No Lua Environment found");
return text;
return input;
}
if (luaEnvironment.Interpreter == null)
{
UnityEngine.Debug.LogError("No Lua interpreter found");
return text;
return input;
}
MoonSharp.Interpreter.Script interpreter = luaEnvironment.Interpreter;
string subbedText = text;
string subbedText = input;
// Instantiate the regular expression object.
Regex r = new Regex("\\[\\$.*?\\]");
Regex r = new Regex("\\{\\$.*?\\}");
// Match the regular expression pattern against a text string.
var results = r.Matches(text);
var results = r.Matches(input);
foreach (Match match in results)
{
string key = match.Value.Substring(2, match.Value.Length - 3);
@ -276,6 +282,15 @@ namespace Fungus
return subbedText;
}
/// <summary>
/// Performs string substitution on the input string, replacing tokens of the form {$VarName} with
/// matching variables, localised strings, etc. in the scene.
/// </summary>
public virtual string Substitute(string input)
{
return stringSubstituter.SubstituteStrings(input);
}
/// <summary>
/// Returns the time since level load, multiplied by timeScale.
/// If timeScale is negative then returns the same as Time.timeSinceLevelLoaded.

76
Assets/Fungus/Lua/Scripts/StringSubstituter.cs

@ -0,0 +1,76 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Fungus
{
/// <summary>
/// Replaces special tokens in a string with substituted values (typically variables or localisation strings).
/// </summary>
public class StringSubstituter
{
/// <summary>
/// Interface for components that support substituting strings.
/// </summary>
public interface ISubstitutionHandler
{
/// <summary>
/// Returns a new string with tokens replaced by subsituted values.
/// It's up to clients how to implement substitution but the convention looks like:
/// "Hi {$VarName}" => "Hi John" where VarName == "John"
/// </summary>
string SubstituteStrings(string input);
}
protected List<ISubstitutionHandler> substitutionHandlers = new List<ISubstitutionHandler>();
/// <summary>
/// Constructor which caches all components in the scene that implement ISubstitutionHandler.
/// </summary>
public StringSubstituter()
{
CacheSubstitutionHandlers();
}
/// <summary>
/// Populates a cache of all components in the scene that implement ISubstitutionHandler.
/// </summary>
public virtual void CacheSubstitutionHandlers()
{
// Use reflection to find all components in the scene that implement ISubstitutionHandler
var types = this.GetType().Assembly.GetTypes().Where(type => type.IsClass &&
!type.IsAbstract &&
typeof(ISubstitutionHandler).IsAssignableFrom(type));
substitutionHandlers.Clear();
foreach (System.Type t in types)
{
Object[] objects = GameObject.FindObjectsOfType(t);
foreach (Object o in objects)
{
ISubstitutionHandler handler = o as ISubstitutionHandler;
if (handler != null)
{
substitutionHandlers.Add(handler);
}
}
}
}
/// <summary>
/// Returns a new string that has been processed by all substitution handlers in the scene.
/// </summary>
public virtual string SubstituteStrings(string input)
{
string newString = input;
foreach (ISubstitutionHandler handler in substitutionHandlers)
{
newString = handler.SubstituteStrings(newString);
}
return newString;
}
}
}

12
Assets/Fungus/Lua/Scripts/StringSubstituter.cs.meta

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 32205fac1893a4bbf9e2cb588b1b9dc9
timeCreated: 1459784535
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

55
Assets/Fungus/Narrative/Scripts/Localization.cs

@ -5,6 +5,7 @@ using UnityEditor;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.IO;
using Ideafixxxer.CsvParser;
@ -22,7 +23,7 @@ namespace Fungus
/**
* Multi-language localization support.
*/
public class Localization : MonoBehaviour
public class Localization : MonoBehaviour, StringSubstituter.ISubstitutionHandler
{
/**
* Language to use at startup, usually defined by a two letter language code (e.g DE = German)
@ -56,6 +57,8 @@ namespace Fungus
[NonSerialized]
public string notificationText = "";
protected bool initialized;
public virtual void OnLevelWasLoaded(int level)
{
// Check if a language has been selected using the Set Language command in a previous scene.
@ -68,13 +71,29 @@ namespace Fungus
public virtual void Start()
{
Init();
}
/**
* String subsitution can happen during the Start of another component, so we
* may need to call Init() from other methods.
*/
protected virtual void Init()
{
if (initialized)
{
return;
}
CacheLocalizeableObjects();
if (localizationFile != null &&
localizationFile.text.Length > 0)
localizationFile.text.Length > 0)
{
SetActiveLanguage(activeLanguage);
}
initialized = true;
}
public virtual void ClearLocalizeableCache()
@ -487,6 +506,38 @@ namespace Fungus
notificationText = "Updated " + updatedCount + " standard text items.";
}
/**
* Implementation of StringSubstituter.ISubstitutionHandler.
* Relaces tokens of the form {$KeyName} with the localized value corresponding to that key.
*/
public virtual string SubstituteStrings(string input)
{
// This method could be called from the Start method of another component, so we
// may need to initilize the localization system.
Init();
string subbedText = input;
// Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}");
// Match the regular expression pattern against a text string.
var results = r.Matches(input);
foreach (Match match in results)
{
string key = match.Value.Substring(2, match.Value.Length - 3);
// Next look for matching localized string
string localizedString = Localization.GetLocalizedString(key);
if (localizedString != null)
{
subbedText = subbedText.Replace(match.Value, localizedString);
}
}
return subbedText;
}
}
}

9
Assets/Tests/StringSubstitution.meta

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 1e9c64a0b75ad4f8fa20114b171c5553
folderAsset: yes
timeCreated: 1459849429
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

1745
Assets/Tests/StringSubstitution/StringSubstitutionTests.unity

File diff suppressed because it is too large Load Diff

8
Assets/Tests/StringSubstitution/StringSubstitutionTests.unity.meta

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 36de0a0b13ca44b54a426afd535261ea
timeCreated: 1459849440
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

0
Assets/Tests/Localisation/CSV.meta → Assets/Tests/TestAssets/CSV.meta

0
Assets/Tests/Localisation/CSV/localization_Commands.csv → Assets/Tests/TestAssets/CSV/localization_Commands.csv

1 Key Description Standard FR
2 SETTEXT.Flowchart.6 English text texte français
3 SETTEXT.Flowchart.5 English text texte français
4 WRITE.Flowchart.3 English text texte français
5 WRITE.Flowchart.1 English text texte français

0
Assets/Tests/Localisation/CSV/localization_Commands.csv.meta → Assets/Tests/TestAssets/CSV/localization_Commands.csv.meta

0
Assets/Tests/Localisation/CSV/localization_Mac.csv → Assets/Tests/TestAssets/CSV/localization_Mac.csv

1 Key Description Standard ES FR
2 SAY.LocalizationDemo.12. This text is in English Este texto está en español Ce texte est en français

0
Assets/Tests/Localisation/CSV/localization_Mac.csv.meta → Assets/Tests/TestAssets/CSV/localization_Mac.csv.meta

0
Assets/Tests/Localisation/CSV/localization_Narrative.csv → Assets/Tests/TestAssets/CSV/localization_Narrative.csv

1 Key Description Standard FR
2 SAY.Flowchart.1.Character Name Say text Dites texte
3 MENU.Flowchart.2 Option text Texte Option
4 CHARACTER.Character Name Character Name Le nom du personnage

0
Assets/Tests/Localisation/CSV/localization_Narrative.csv.meta → Assets/Tests/TestAssets/CSV/localization_Narrative.csv.meta

0
Assets/Tests/Localisation/CSV/localization_Windows.csv → Assets/Tests/TestAssets/CSV/localization_Windows.csv

1 Key Description Standard ES FR
2 SAY.LocalizationDemo.12. This text is in English Este texto está en español Ce texte est en français

0
Assets/Tests/Localisation/CSV/localization_Windows.csv.meta → Assets/Tests/TestAssets/CSV/localization_Windows.csv.meta

0
Assets/Tests/Localisation/CSV/localization_dottest.csv → Assets/Tests/TestAssets/CSV/localization_dottest.csv

1 Key,Description,Standard,FR
2 SAY.Flowchart.1.Dr. Character,,English,French
3 CHARACTER.Dr. Character,Character with a . in the name,Dr. Character

0
Assets/Tests/Localisation/CSV/localization_dottest.csv.meta → Assets/Tests/TestAssets/CSV/localization_dottest.csv.meta

Loading…
Cancel
Save