Browse Source

Merge pull request #442 from FungusGames/string-substitution

Extensible string substitution system
master
Chris Gregan 9 years ago
parent
commit
546b0ddb99
  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