Browse Source

Added a robust CSV parser. Localisation file is read at load time.

master
chrisgregan 10 years ago
parent
commit
e063596031
  1. 218
      Assets/CsvParser.cs
  2. 12
      Assets/CsvParser.cs.meta
  3. 25
      Assets/Fungus/Flowchart/Scripts/CSVSupport.cs
  4. 131
      Assets/Fungus/Flowchart/Scripts/Language.cs
  5. 29
      Assets/Fungus/Narrative/Scripts/Character.cs
  6. 29
      Assets/Fungus/Narrative/Scripts/Commands/Menu.cs
  7. 29
      Assets/Fungus/Narrative/Scripts/Commands/Say.cs

218
Assets/CsvParser.cs

@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Ideafixxxer.CsvParser
{
public class CsvParser
{
private const char CommaCharacter = ',';
private const char QuoteCharacter = '"';
#region Nested types
private abstract class ParserState
{
public static readonly LineStartState LineStartState = new LineStartState();
public static readonly ValueStartState ValueStartState = new ValueStartState();
public static readonly ValueState ValueState = new ValueState();
public static readonly QuotedValueState QuotedValueState = new QuotedValueState();
public static readonly QuoteState QuoteState = new QuoteState();
public abstract ParserState AnyChar(char ch, ParserContext context);
public abstract ParserState Comma(ParserContext context);
public abstract ParserState Quote(ParserContext context);
public abstract ParserState EndOfLine(ParserContext context);
}
private class LineStartState : ParserState
{
public override ParserState AnyChar(char ch, ParserContext context)
{
context.AddChar(ch);
return ValueState;
}
public override ParserState Comma(ParserContext context)
{
context.AddValue();
return ValueStartState;
}
public override ParserState Quote(ParserContext context)
{
return QuotedValueState;
}
public override ParserState EndOfLine(ParserContext context)
{
context.AddLine();
return LineStartState;
}
}
private class ValueStartState : LineStartState
{
public override ParserState EndOfLine(ParserContext context)
{
context.AddValue();
context.AddLine();
return LineStartState;
}
}
private class ValueState : ParserState
{
public override ParserState AnyChar(char ch, ParserContext context)
{
context.AddChar(ch);
return ValueState;
}
public override ParserState Comma(ParserContext context)
{
context.AddValue();
return ValueStartState;
}
public override ParserState Quote(ParserContext context)
{
context.AddChar(QuoteCharacter);
return ValueState;
}
public override ParserState EndOfLine(ParserContext context)
{
context.AddValue();
context.AddLine();
return LineStartState;
}
}
private class QuotedValueState : ParserState
{
public override ParserState AnyChar(char ch, ParserContext context)
{
context.AddChar(ch);
return QuotedValueState;
}
public override ParserState Comma(ParserContext context)
{
context.AddChar(CommaCharacter);
return QuotedValueState;
}
public override ParserState Quote(ParserContext context)
{
return QuoteState;
}
public override ParserState EndOfLine(ParserContext context)
{
context.AddChar('\r');
context.AddChar('\n');
return QuotedValueState;
}
}
private class QuoteState : ParserState
{
public override ParserState AnyChar(char ch, ParserContext context)
{
//undefined, ignore "
context.AddChar(ch);
return QuotedValueState;
}
public override ParserState Comma(ParserContext context)
{
context.AddValue();
return ValueStartState;
}
public override ParserState Quote(ParserContext context)
{
context.AddChar(QuoteCharacter);
return QuotedValueState;
}
public override ParserState EndOfLine(ParserContext context)
{
context.AddValue();
context.AddLine();
return LineStartState;
}
}
private class ParserContext
{
private readonly StringBuilder _currentValue = new StringBuilder();
private readonly List<string[]> _lines = new List<string[]>();
private readonly List<string> _currentLine = new List<string>();
public void AddChar(char ch)
{
_currentValue.Append(ch);
}
public void AddValue()
{
_currentLine.Add(_currentValue.ToString());
_currentValue.Remove(0, _currentValue.Length);
}
public void AddLine()
{
_lines.Add(_currentLine.ToArray());
_currentLine.Clear();
}
public List<string[]> GetAllLines()
{
if (_currentValue.Length > 0)
{
AddValue();
}
if (_currentLine.Count > 0)
{
AddLine();
}
return _lines;
}
}
#endregion
public string[][] Parse(string csvData)
{
var context = new ParserContext();
string[] lines = csvData.Split('\n');
ParserState currentState = ParserState.LineStartState;
foreach (string next in lines)
{
foreach (char ch in next)
{
switch (ch)
{
case CommaCharacter:
currentState = currentState.Comma(context);
break;
case QuoteCharacter:
currentState = currentState.Quote(context);
break;
default:
currentState = currentState.AnyChar(ch, context);
break;
}
}
currentState = currentState.EndOfLine(context);
}
List<string[]> allLines = context.GetAllLines();
return allLines.ToArray();
}
}
}

12
Assets/CsvParser.cs.meta

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

25
Assets/Fungus/Flowchart/Scripts/CSVSupport.cs

@ -40,31 +40,6 @@ namespace Fungus
return s; return s;
} }
public static string[] SplitCSVLine(string line)
{
// '(?<val>[^'\\]*(?:\\[\S\s][^'\\]*)*)' # Either $1: Single quoted string,
string pattern = @"
# Match one value in valid CSV string.
(?!\s*$) # Don't match empty last value.
\s* # Strip whitespace before value.
(?: # Group for value alternatives.
| ""(?<val>[^""\\]*(?:\\[\S\s][^""\\]*)*)"" # or $2: Double quoted string,
| (?<val>[^,'""\s\\]*(?:\s+[^,'""\s\\]+)*) # or $3: Non-comma, non-quote stuff.
) # End group of value alternatives.
\s* # Strip whitespace after value.
(?:,|$) # Field ends on comma or EOS.
";
string[] values = (from Match m in Regex.Matches(line, pattern,
RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline)
select m.Groups[1].Value).ToArray();
return values;
}
private const string QUOTE = "\""; private const string QUOTE = "\"";
private const string ESCAPED_QUOTE = "\"\""; private const string ESCAPED_QUOTE = "\"\"";
// private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' }; // private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' };

131
Assets/Fungus/Flowchart/Scripts/Language.cs

@ -2,23 +2,12 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Ideafixxxer.CsvParser;
namespace Fungus namespace Fungus
{ {
interface ILocalizable
{
string GetLocalizationID();
string GetStandardText();
void SetStandardText(string standardText);
string GetTimestamp();
string GetDescription();
}
/** /**
* Multi-language localization support. * Multi-language localization support.
*/ */
@ -36,7 +25,6 @@ namespace Fungus
*/ */
protected class LanguageItem protected class LanguageItem
{ {
public string timeStamp;
public string description; public string description;
public string standardText; public string standardText;
public Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); public Dictionary<string, string> localizedStrings = new Dictionary<string, string>();
@ -73,7 +61,7 @@ namespace Fungus
} }
// Build CSV header row and a list of the language codes currently in use // Build CSV header row and a list of the language codes currently in use
string csvHeader = "Key,Timestamp,Description,Standard"; string csvHeader = "Key,Description,Standard";
List<string> languageCodes = new List<string>(); List<string> languageCodes = new List<string>();
foreach (LanguageItem languageItem in languageItems.Values) foreach (LanguageItem languageItem in languageItems.Values)
{ {
@ -94,7 +82,6 @@ namespace Fungus
LanguageItem languageItem = languageItems[stringId]; LanguageItem languageItem = languageItems[stringId];
string row = CSVSupport.Escape(stringId); string row = CSVSupport.Escape(stringId);
row += "," + CSVSupport.Escape(languageItem.timeStamp);
row += "," + CSVSupport.Escape(languageItem.description); row += "," + CSVSupport.Escape(languageItem.description);
row += "," + CSVSupport.Escape(languageItem.standardText); row += "," + CSVSupport.Escape(languageItem.standardText);
@ -121,6 +108,7 @@ namespace Fungus
Dictionary<string, LanguageItem> languageItems = new Dictionary<string, LanguageItem>(); Dictionary<string, LanguageItem> languageItems = new Dictionary<string, LanguageItem>();
// Export all Say and Menu commands in the scene // Export all Say and Menu commands in the scene
// To make it easier to localize, we preserve the command order in each exported block.
Flowchart[] flowcharts = GameObject.FindObjectsOfType<Flowchart>(); Flowchart[] flowcharts = GameObject.FindObjectsOfType<Flowchart>();
foreach (Flowchart flowchart in flowcharts) foreach (Flowchart flowchart in flowcharts)
{ {
@ -131,6 +119,7 @@ namespace Fungus
{ {
string stringID = ""; string stringID = "";
string standardText = ""; string standardText = "";
string description = "";
System.Type type = command.GetType(); System.Type type = command.GetType();
if (type == typeof(Say)) if (type == typeof(Say))
@ -138,12 +127,14 @@ namespace Fungus
stringID = "SAY." + flowchart.name + "." + command.itemId; stringID = "SAY." + flowchart.name + "." + command.itemId;
Say sayCommand = command as Say; Say sayCommand = command as Say;
standardText = sayCommand.storyText; standardText = sayCommand.storyText;
description = sayCommand.description;
} }
else if (type == typeof(Menu)) else if (type == typeof(Menu))
{ {
stringID = "MENU." + flowchart.name + "." + command.itemId; stringID = "MENU." + flowchart.name + "." + command.itemId;
Menu menuCommand = command as Menu; Menu menuCommand = command as Menu;
standardText = menuCommand.text; standardText = menuCommand.text;
description = menuCommand.description;
} }
else else
{ {
@ -162,9 +153,8 @@ namespace Fungus
} }
// Update basic properties,leaving localised strings intact // Update basic properties,leaving localised strings intact
languageItem.timeStamp = "10/10/2015";
languageItem.description = "Note";
languageItem.standardText = standardText; languageItem.standardText = standardText;
languageItem.description = description;
} }
} }
} }
@ -174,33 +164,37 @@ namespace Fungus
protected virtual void AddLocalisedStrings(Dictionary<string, LanguageItem> languageItems, string csvData) protected virtual void AddLocalisedStrings(Dictionary<string, LanguageItem> languageItems, string csvData)
{ {
// Split into lines CsvParser csvParser = new CsvParser();
// Excel on Mac exports csv files with \r line endings, so we need to support that too. string[][] csvTable = csvParser.Parse(csvData);
string[] lines = csvData.Split('\n', '\r');
if (lines.Length == 0) if (csvTable.Length <= 1)
{ {
// Early out if no data in file // No data rows in file
return; return;
} }
// Parse header row // Parse header row
string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); string[] columnNames = csvTable[0];
for (int i = 1; i < lines.Length; ++i) for (int i = 1; i < csvTable.Length; ++i)
{ {
string line = lines[i]; string[] fields = csvTable[i];
string[] fields = CSVSupport.SplitCSVLine(line);
if (fields.Length < 4) if (fields.Length < 4)
{ {
// No localized string fields present
continue; continue;
} }
string stringId = fields[0]; string stringId = fields[0];
if (!languageItems.ContainsKey(stringId))
{
continue;
}
// Store localized strings for this string id // Store localized strings for this string id
for (int j = 4; j < fields.Length; ++j) LanguageItem languageItem = languageItems[stringId];
for (int j = 3; j < fields.Length; ++j)
{ {
if (j >= columnNames.Length) if (j >= columnNames.Length)
{ {
@ -211,10 +205,7 @@ namespace Fungus
if (languageEntry.Length > 0) if (languageEntry.Length > 0)
{ {
if (languageItems.ContainsKey(stringId)) languageItem.localizedStrings[languageCode] = languageEntry;
{
languageItems[stringId].localizedStrings[languageCode] = languageEntry;
}
} }
} }
} }
@ -230,18 +221,17 @@ namespace Fungus
localizedStrings.Clear(); localizedStrings.Clear();
// Split into lines CsvParser csvParser = new CsvParser();
// Excel on Mac exports csv files with \r line endings, so we need to support that too. string[][] csvTable = csvParser.Parse(csvData);
string[] lines = csvData.Split('\n', '\r');
if (lines.Length == 0) if (csvTable.Length <= 1)
{ {
// No data rows in file // No data rows in file
return; return;
} }
// Parse header row // Parse header row
string[] columnNames = CSVSupport.SplitCSVLine(lines[0]); string[] columnNames = csvTable[0];
if (columnNames.Length < 5) if (columnNames.Length < 5)
{ {
@ -250,7 +240,7 @@ namespace Fungus
} }
int languageIndex = -1; int languageIndex = -1;
for (int i = 4; i < columnNames.Length; ++i) for (int i = 3; i < columnNames.Length; ++i)
{ {
if (columnNames[i] == languageCode) if (columnNames[i] == languageCode)
{ {
@ -265,11 +255,10 @@ namespace Fungus
return; return;
} }
for (int i = 1; i < lines.Length; ++i) for (int i = 1; i < csvTable.Length; ++i)
{ {
string line = lines[i]; string[] fields = csvTable[i];
string[] fields = CSVSupport.SplitCSVLine(line);
if (fields.Length < languageIndex + 1) if (fields.Length < languageIndex + 1)
{ {
continue; continue;
@ -342,64 +331,6 @@ namespace Fungus
} }
} }
} }
/**
* Import strings from a CSV file.
* 1. Any changes to standard text items will be applied to the corresponding scene object.
* 2. Any localized strings will be added to the localization dictionary.
*/
public virtual void ImportCSV(string csvData)
{
/*
// Split into lines
// Excel on Mac exports csv files with \r line endings, so we need to support that too.
string[] lines = csvData.Split('\n', '\r');
if (lines.Length == 0)
{
return;
}
localizedStrings.Clear();
// Parse header row
string[] columnNames = CSVSupport.SplitCSVLine(lines[0]);
for (int i = 1; i < lines.Length; ++i)
{
string line = lines[i];
string[] fields = CSVSupport.SplitCSVLine(line);
if (fields.Length < 4)
{
continue;
}
string stringId = fields[0];
// Ignore timestamp & notes fields
string standardText = CSVSupport.Unescape(fields[3]);
PopulateGameString(stringId, standardText);
// Store localized string in stringDict
for (int j = 4; j < fields.Length; ++j)
{
if (j >= columnNames.Length)
{
continue;
}
string languageCode = columnNames[j];
string languageEntry = CSVSupport.Unescape(fields[j]);
if (languageEntry.Length > 0)
{
// The dictionary key is the basic string id with .<LanguageCode> appended
localizedStrings[stringId + "." + languageCode] = languageEntry;
}
}
}
*/
}
} }
} }

29
Assets/Fungus/Narrative/Scripts/Character.cs

@ -8,7 +8,7 @@ namespace Fungus
{ {
[ExecuteInEditMode] [ExecuteInEditMode]
public class Character : MonoBehaviour, ILocalizable public class Character : MonoBehaviour
{ {
public string nameText; // We need a separate name as the object name is used for character variations (e.g. "Smurf Happy", "Smurf Sad") public string nameText; // We need a separate name as the object name is used for character variations (e.g. "Smurf Happy", "Smurf Sad")
public Color nameColor = Color.white; public Color nameColor = Color.white;
@ -36,33 +36,6 @@ namespace Fungus
{ {
activeCharacters.Remove(this); activeCharacters.Remove(this);
} }
// ILocalizable methods
public virtual string GetLocalizationID()
{
return "CHARACTER." + nameText;
}
public virtual string GetStandardText()
{
return nameText;
}
public virtual void SetStandardText(string standardText)
{
nameText = standardText;
}
public virtual string GetTimestamp()
{
return DateTime.Now.ToShortDateString();
}
public virtual string GetDescription()
{
return description;
}
} }
} }

29
Assets/Fungus/Narrative/Scripts/Commands/Menu.cs

@ -11,7 +11,7 @@ namespace Fungus
"Menu", "Menu",
"Displays a multiple choice menu")] "Displays a multiple choice menu")]
[AddComponentMenu("")] [AddComponentMenu("")]
public class Menu : Command, ILocalizable public class Menu : Command
{ {
// Menu displays a menu button which will execute the target block when clicked // Menu displays a menu button which will execute the target block when clicked
@ -116,33 +116,6 @@ namespace Fungus
{ {
return false; return false;
} }
// ILocalizable methods
public virtual string GetLocalizationID()
{
return "MENU." + itemId.ToString();
}
public virtual string GetStandardText()
{
return text;
}
public virtual void SetStandardText(string standardText)
{
text = standardText;
}
public virtual string GetTimestamp()
{
return DateTime.Now.ToShortDateString();
}
public virtual string GetDescription()
{
return description;
}
} }
} }

29
Assets/Fungus/Narrative/Scripts/Commands/Say.cs

@ -9,7 +9,7 @@ namespace Fungus
"Say", "Say",
"Writes text in a dialog box.")] "Writes text in a dialog box.")]
[AddComponentMenu("")] [AddComponentMenu("")]
public class Say : Command, ILocalizable public class Say : Command
{ {
[TextArea(5,10)] [TextArea(5,10)]
public string storyText = ""; public string storyText = "";
@ -164,33 +164,6 @@ namespace Fungus
{ {
executionCount = 0; executionCount = 0;
} }
// ILocalizable methods
public virtual string GetLocalizationID()
{
return "SAY." + itemId.ToString();
}
public virtual string GetStandardText()
{
return storyText;
}
public virtual void SetStandardText(string standardText)
{
storyText = standardText;
}
public virtual string GetTimestamp()
{
return DateTime.Now.ToShortDateString();
}
public virtual string GetDescription()
{
return description;
}
} }
} }
Loading…
Cancel
Save