chrisgregan
10 years ago
29 changed files with 1334 additions and 422 deletions
@ -1,187 +0,0 @@
|
||||
using UnityEditor; |
||||
using UnityEngine; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using System.IO; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
|
||||
/** |
||||
* Import and export a Fungus story in the .fountain screenplay format. |
||||
* The exported file contains special tags in note blocks which map the |
||||
* story text to the corresponding commands. |
||||
*/ |
||||
public class FountainExporter |
||||
{ |
||||
|
||||
public static void ExportStrings(Flowchart flowchart) |
||||
{ |
||||
if (flowchart == null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
string path = EditorUtility.SaveFilePanel("Export strings", "", |
||||
flowchart.name + ".txt", ""); |
||||
|
||||
if(path.Length == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// Write out character names |
||||
|
||||
string exportText = "Title: " + flowchart.name + "\n"; |
||||
exportText += "Draft date: " + System.DateTime.Today.ToString("d") + "\n"; |
||||
exportText += "\n"; |
||||
|
||||
// In every block, write out Say & Menu text in order |
||||
Block[] blocks = flowchart.GetComponentsInChildren<Block>(); |
||||
foreach (Block block in blocks) |
||||
{ |
||||
// Check for any Say, Menu or Comment commands |
||||
bool hasText = false; |
||||
foreach (Command c in block.commandList) |
||||
{ |
||||
System.Type t = c.GetType(); |
||||
if (t == typeof(Say) || |
||||
t == typeof(Menu) || |
||||
t == typeof(Comment)) |
||||
{ |
||||
hasText = true; |
||||
} |
||||
} |
||||
if (!hasText) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
exportText += "." + block.blockName.ToUpper() + "\n\n"; |
||||
|
||||
foreach (Command c in block.commandList) |
||||
{ |
||||
if (c.GetType() == typeof(Say)) |
||||
{ |
||||
string idText = ""; |
||||
Say say = c as Say; |
||||
|
||||
if (say.character == null) |
||||
{ |
||||
exportText += "NO CHARACTER\n"; |
||||
} |
||||
else |
||||
{ |
||||
exportText += say.character.nameText.ToUpper() + "\n"; |
||||
} |
||||
|
||||
idText += "[[Say," + c.itemId + "]]\n"; |
||||
|
||||
exportText += idText; |
||||
|
||||
// Fountain requires blank dialogue lines to contain 2 spaces or else |
||||
// they will be interpreted as ACTION text. |
||||
string trimmedText = say.storyText.Trim(); |
||||
string[] lines = trimmedText.Split(new [] { '\r', '\n' }); |
||||
foreach (string line in lines) |
||||
{ |
||||
string trimmed = line.Trim(); |
||||
if (line.Length == 0) |
||||
{ |
||||
exportText += " \n"; |
||||
} |
||||
else |
||||
{ |
||||
exportText += trimmed + "\n"; |
||||
} |
||||
} |
||||
|
||||
exportText += "\n"; |
||||
} |
||||
else if (c.GetType() == typeof(Menu)) |
||||
{ |
||||
exportText += "MENU\n"; |
||||
|
||||
string idText = ""; |
||||
Menu menu = c as Menu; |
||||
idText += "[[Menu," + c.itemId + "]]\n"; |
||||
|
||||
exportText += idText + menu.text.Trim() + "\n\n"; |
||||
} |
||||
else if (c.GetType() == typeof(Comment)) |
||||
{ |
||||
string idText = ""; |
||||
Comment comment = c as Comment; |
||||
idText += "[[Comment," + c.itemId + "]]\n"; |
||||
|
||||
exportText += idText + comment.commentText.Trim() + "\n\n"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
File.WriteAllText(path, exportText); |
||||
} |
||||
|
||||
public static void ImportStrings(Flowchart flowchart) |
||||
{ |
||||
string path = EditorUtility.OpenFilePanel("Import strings", "", ""); |
||||
|
||||
if(path.Length == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
string stringsFile = File.ReadAllText(path); |
||||
|
||||
StringsParser parser = new StringsParser(); |
||||
List<StringsParser.StringItem> items = parser.ProcessText(stringsFile); |
||||
|
||||
// Build dict of commands |
||||
Dictionary<int, Command> commandDict = new Dictionary<int, Command>(); |
||||
foreach (Command c in flowchart.gameObject.GetComponentsInChildren<Command>()) |
||||
{ |
||||
commandDict.Add (c.itemId, c); |
||||
} |
||||
|
||||
foreach (StringsParser.StringItem item in items) |
||||
{ |
||||
if (item.parameters.Length != 2) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
string stringType = item.parameters[0]; |
||||
if (stringType == "Say") |
||||
{ |
||||
int itemId = int.Parse(item.parameters[1]); |
||||
Say sayCommand = commandDict[itemId] as Say; |
||||
if (sayCommand != null) |
||||
{ |
||||
sayCommand.storyText = item.bodyText; |
||||
} |
||||
} |
||||
else if (stringType == "Menu") |
||||
{ |
||||
int itemId = int.Parse(item.parameters[1]); |
||||
Menu menuCommand = commandDict[itemId] as Menu; |
||||
if (menuCommand != null) |
||||
{ |
||||
menuCommand.text = item.bodyText; |
||||
} |
||||
} |
||||
else if (stringType == "Comment") |
||||
{ |
||||
int itemId = int.Parse(item.parameters[1]); |
||||
Comment commentCommand = commandDict[itemId] as Comment; |
||||
if (commentCommand != null) |
||||
{ |
||||
commentCommand.commentText = item.bodyText; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,96 +0,0 @@
|
||||
using UnityEngine; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Text.RegularExpressions; |
||||
using Fungus; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
/** |
||||
* Parses an exported strings file using the Fountain file format for screenplays |
||||
* See http://fountain.io for details. |
||||
* We only support a small subset of Fountain markup, and use note tags to embed meta data to |
||||
* bind dialogue text to the corresponding Say / Menu commands. |
||||
*/ |
||||
public class StringsParser |
||||
{ |
||||
public class StringItem |
||||
{ |
||||
public string[] parameters; |
||||
public string bodyText; |
||||
} |
||||
|
||||
public virtual List<StringItem> ProcessText(string text) |
||||
{ |
||||
List<StringItem> items = new List<StringItem>(); |
||||
|
||||
// Split text into lines. Add a newline at end to ensure last command is always parsed |
||||
string[] lines = Regex.Split(text + "\n", "(?<=\n)"); |
||||
|
||||
int i = 0; |
||||
while (i < lines.Length) |
||||
{ |
||||
string line = lines[i].Trim(); |
||||
|
||||
if (!(line.StartsWith("[[") && line.EndsWith("]]"))) |
||||
{ |
||||
i++; |
||||
continue; |
||||
} |
||||
|
||||
string blockTag = line.Substring(2, line.Length - 4); |
||||
|
||||
// Find next empty line, #, [[ or eof |
||||
int start = i + 1; |
||||
int end = lines.Length - 1; |
||||
for (int j = start; j <= end; ++j) |
||||
{ |
||||
string line2 = lines[j].Trim(); |
||||
|
||||
if (line2.Length == 0 || |
||||
line2.StartsWith("#") || |
||||
line2.StartsWith("[[")) |
||||
{ |
||||
end = j; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (end > start) |
||||
{ |
||||
string blockBuffer = ""; |
||||
for (int j = start; j <= end; ++j) |
||||
{ |
||||
blockBuffer += lines[j].Trim() + "\n"; |
||||
} |
||||
|
||||
blockBuffer = blockBuffer.Trim(); |
||||
|
||||
StringItem item = CreateItem(blockTag, blockBuffer); |
||||
if (item != null) |
||||
{ |
||||
items.Add(item); |
||||
} |
||||
} |
||||
|
||||
i = end + 1; |
||||
} |
||||
|
||||
return items; |
||||
} |
||||
|
||||
protected StringItem CreateItem(string commandInfo, string bodyText) |
||||
{ |
||||
string[] parameters = commandInfo.Split(new char[] { ',' }); |
||||
if (parameters.Length > 0) |
||||
{ |
||||
StringItem item = new StringItem(); |
||||
item.parameters = parameters; |
||||
item.bodyText = bodyText; |
||||
return item; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,106 @@
|
||||
using UnityEditor; |
||||
using UnityEngine; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using Rotorz.ReorderableList; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
|
||||
[CustomEditor(typeof(Localization))] |
||||
public class LocalizationEditor : Editor |
||||
{ |
||||
protected SerializedProperty activeLanguageProp; |
||||
protected SerializedProperty localizationFileProp; |
||||
|
||||
protected virtual void OnEnable() |
||||
{ |
||||
activeLanguageProp = serializedObject.FindProperty("activeLanguage"); |
||||
localizationFileProp = serializedObject.FindProperty("localizationFile"); |
||||
} |
||||
|
||||
public override void OnInspectorGUI() |
||||
{ |
||||
serializedObject.Update(); |
||||
|
||||
Localization localization = target as Localization; |
||||
|
||||
EditorGUILayout.PropertyField(activeLanguageProp); |
||||
EditorGUILayout.PropertyField(localizationFileProp); |
||||
|
||||
if (GUILayout.Button(new GUIContent("Export Localization File"))) |
||||
{ |
||||
ExportLocalizationFile(localization); |
||||
} |
||||
|
||||
if (GUILayout.Button(new GUIContent("Export Standard Text"))) |
||||
{ |
||||
ExportStandardText(localization); |
||||
} |
||||
|
||||
if (GUILayout.Button(new GUIContent("Import Standard Text"))) |
||||
{ |
||||
ImportStandardText(localization); |
||||
} |
||||
|
||||
serializedObject.ApplyModifiedProperties(); |
||||
} |
||||
|
||||
public virtual void ExportLocalizationFile(Localization localization) |
||||
{ |
||||
string path = EditorUtility.SaveFilePanel("Export Localization File", "Assets/", |
||||
"localization.csv", ""); |
||||
if (path.Length == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
string csvData = localization.GetCSVData(); |
||||
File.WriteAllText(path, csvData); |
||||
AssetDatabase.Refresh(); |
||||
|
||||
ShowNotification(localization); |
||||
} |
||||
|
||||
public virtual void ExportStandardText(Localization localization) |
||||
{ |
||||
string path = EditorUtility.SaveFilePanel("Export Standard Text", "Assets/", "standard.txt", ""); |
||||
if (path.Length == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
string textData = localization.GetStandardText(); |
||||
File.WriteAllText(path, textData); |
||||
AssetDatabase.Refresh(); |
||||
|
||||
ShowNotification(localization); |
||||
} |
||||
|
||||
public virtual void ImportStandardText(Localization localization) |
||||
{ |
||||
string path = EditorUtility.OpenFilePanel("Import Standard Text", "Assets/", "txt"); |
||||
if (path.Length == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
string textData = File.ReadAllText(path); |
||||
localization.SetStandardText(textData); |
||||
|
||||
ShowNotification(localization); |
||||
} |
||||
|
||||
protected virtual void ShowNotification(Localization localization) |
||||
{ |
||||
EditorWindow window = EditorWindow.GetWindow(typeof(FlowchartWindow), false, "Flowchart"); |
||||
if (window != null) |
||||
{ |
||||
window.ShowNotification(new GUIContent(localization.notificationText)); |
||||
localization.notificationText = ""; |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,6 +1,6 @@
|
||||
fileFormatVersion: 2 |
||||
guid: c91ad6ef6a0734046bd93dde4b0e59d1 |
||||
timeCreated: 1426502899 |
||||
guid: ab0caac085485491fb32dbf86efefef1 |
||||
timeCreated: 1428581512 |
||||
licenseType: Free |
||||
MonoImporter: |
||||
serializedVersion: 2 |
@ -0,0 +1,54 @@
|
||||
%YAML 1.1 |
||||
%TAG !u! tag:unity3d.com,2011: |
||||
--- !u!1 &149266 |
||||
GameObject: |
||||
m_ObjectHideFlags: 0 |
||||
m_PrefabParentObject: {fileID: 0} |
||||
m_PrefabInternal: {fileID: 100100000} |
||||
serializedVersion: 4 |
||||
m_Component: |
||||
- 4: {fileID: 480768} |
||||
- 114: {fileID: 11438504} |
||||
m_Layer: 0 |
||||
m_Name: Localization |
||||
m_TagString: Untagged |
||||
m_Icon: {fileID: 0} |
||||
m_NavMeshLayer: 0 |
||||
m_StaticEditorFlags: 0 |
||||
m_IsActive: 1 |
||||
--- !u!4 &480768 |
||||
Transform: |
||||
m_ObjectHideFlags: 1 |
||||
m_PrefabParentObject: {fileID: 0} |
||||
m_PrefabInternal: {fileID: 100100000} |
||||
m_GameObject: {fileID: 149266} |
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} |
||||
m_LocalPosition: {x: 2.05546069, y: -3.16485739, z: 0} |
||||
m_LocalScale: {x: 1, y: 1, z: 1} |
||||
m_Children: [] |
||||
m_Father: {fileID: 0} |
||||
m_RootOrder: 0 |
||||
--- !u!114 &11438504 |
||||
MonoBehaviour: |
||||
m_ObjectHideFlags: 1 |
||||
m_PrefabParentObject: {fileID: 0} |
||||
m_PrefabInternal: {fileID: 100100000} |
||||
m_GameObject: {fileID: 149266} |
||||
m_Enabled: 1 |
||||
m_EditorHideFlags: 0 |
||||
m_Script: {fileID: 11500000, guid: e5724422a635e425bae0af9ffe2615d6, type: 3} |
||||
m_Name: |
||||
m_EditorClassIdentifier: |
||||
activeLanguage: |
||||
localizationFile: {fileID: 0} |
||||
--- !u!1001 &100100000 |
||||
Prefab: |
||||
m_ObjectHideFlags: 1 |
||||
serializedVersion: 2 |
||||
m_Modification: |
||||
m_TransformParent: {fileID: 0} |
||||
m_Modifications: [] |
||||
m_RemovedComponents: [] |
||||
m_ParentPrefab: {fileID: 0} |
||||
m_RootGameObject: {fileID: 149266} |
||||
m_IsPrefabParent: 1 |
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2 |
||||
guid: ffbd0831d997545eab75c364da082c1b |
||||
timeCreated: 1428580452 |
||||
licenseType: Free |
||||
NativeFormatImporter: |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
@ -0,0 +1,35 @@
|
||||
using UnityEngine; |
||||
using System.Collections; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
[CommandInfo("Narrative", |
||||
"Set Language", |
||||
"Set the active language for the scene. A Localization object with a localization file must be present in the scene.")] |
||||
[AddComponentMenu("")] |
||||
public class SetLanguage : Command |
||||
{ |
||||
public string languageCode; |
||||
|
||||
public override void OnEnter() |
||||
{ |
||||
Localization localization = GameObject.FindObjectOfType<Localization>(); |
||||
if (localization != null) |
||||
{ |
||||
localization.SetActiveLanguage(languageCode); |
||||
} |
||||
|
||||
Continue(); |
||||
} |
||||
|
||||
public override string GetSummary() |
||||
{ |
||||
return languageCode; |
||||
} |
||||
|
||||
public override Color GetButtonColor() |
||||
{ |
||||
return new Color32(184, 210, 235, 255); |
||||
} |
||||
} |
||||
} |
@ -1,8 +1,12 @@
|
||||
fileFormatVersion: 2 |
||||
guid: 0f02aedc631824200a4abe95774a44f5 |
||||
guid: 3fc625e237d6048bf86f34835d8266d9 |
||||
timeCreated: 1428591017 |
||||
licenseType: Free |
||||
MonoImporter: |
||||
serializedVersion: 2 |
||||
defaultReferences: [] |
||||
executionOrder: 0 |
||||
icon: {instanceID: 0} |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
@ -0,0 +1,601 @@
|
||||
/** |
||||
* CSVParser by Ideafixxxer. http://www.codeproject.com/Tips/741941/CSV-Parser-Csharp |
||||
* This code is licensed under the CPOL open source license. |
||||
* http://www.codeproject.com/info/cpol10.aspx |
||||
*/ |
||||
|
||||
using UnityEngine; |
||||
#if UNITY_EDITOR |
||||
using UnityEditor; |
||||
#endif |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using Ideafixxxer.CsvParser; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
|
||||
/** |
||||
* Multi-language localization support. |
||||
*/ |
||||
public class Localization : MonoBehaviour |
||||
{ |
||||
/** |
||||
* Currently active language, usually defined by a two letter language code (e.g DE = German) |
||||
*/ |
||||
public string activeLanguage = ""; |
||||
|
||||
protected static Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); |
||||
|
||||
/** |
||||
* Temp storage for a single item of standard text and its localizations |
||||
*/ |
||||
protected class TextItem |
||||
{ |
||||
public string description = ""; |
||||
public string standardText = ""; |
||||
public Dictionary<string, string> localizedStrings = new Dictionary<string, string>(); |
||||
} |
||||
|
||||
/** |
||||
* CSV file containing localization data |
||||
*/ |
||||
public TextAsset localizationFile; |
||||
|
||||
/** |
||||
* Stores any notification message from export / import methods. |
||||
*/ |
||||
[NonSerialized] |
||||
public string notificationText = ""; |
||||
|
||||
public virtual void Start() |
||||
{ |
||||
if (localizationFile != null && |
||||
localizationFile.text.Length > 0) |
||||
{ |
||||
SetActiveLanguage(activeLanguage); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Looks up the specified string in the localized strings table. |
||||
* For this to work, a localization file and active language must have been set previously. |
||||
* Return null if the string is not found. |
||||
*/ |
||||
public static string GetLocalizedString(string stringId) |
||||
{ |
||||
if (localizedStrings == null) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
if (localizedStrings.ContainsKey(stringId)) |
||||
{ |
||||
return localizedStrings[stringId]; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Convert all text items and localized strings to an easy to edit CSV format. |
||||
*/ |
||||
public virtual string GetCSVData() |
||||
{ |
||||
// Collect all the text items present in the scene |
||||
Dictionary<string, TextItem> textItems = FindTextItems(); |
||||
|
||||
// Update text items with localization data from CSV file |
||||
if (localizationFile != null && |
||||
localizationFile.text.Length > 0) |
||||
{ |
||||
AddCSVDataItems(textItems, localizationFile.text); |
||||
} |
||||
|
||||
// Build CSV header row and a list of the language codes currently in use |
||||
string csvHeader = "Key,Description,Standard"; |
||||
List<string> languageCodes = new List<string>(); |
||||
foreach (TextItem textItem in textItems.Values) |
||||
{ |
||||
foreach (string languageCode in textItem.localizedStrings.Keys) |
||||
{ |
||||
if (!languageCodes.Contains(languageCode)) |
||||
{ |
||||
languageCodes.Add(languageCode); |
||||
csvHeader += "," + languageCode; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Build the CSV file using collected text items |
||||
int rowCount = 0; |
||||
string csvData = csvHeader + "\n"; |
||||
foreach (string stringId in textItems.Keys) |
||||
{ |
||||
TextItem textItem = textItems[stringId]; |
||||
|
||||
string row = CSVSupport.Escape(stringId); |
||||
row += "," + CSVSupport.Escape(textItem.description); |
||||
row += "," + CSVSupport.Escape(textItem.standardText); |
||||
|
||||
foreach (string languageCode in languageCodes) |
||||
{ |
||||
if (textItem.localizedStrings.ContainsKey(languageCode)) |
||||
{ |
||||
row += "," + CSVSupport.Escape(textItem.localizedStrings[languageCode]); |
||||
} |
||||
else |
||||
{ |
||||
row += ","; // Empty field |
||||
} |
||||
} |
||||
|
||||
csvData += row + "\n"; |
||||
rowCount++; |
||||
} |
||||
|
||||
notificationText = "Exported " + rowCount + " localization text items."; |
||||
|
||||
return csvData; |
||||
} |
||||
|
||||
/** |
||||
* Buidls a dictionary of localizable text items in the scene. |
||||
*/ |
||||
protected Dictionary<string, TextItem> FindTextItems() |
||||
{ |
||||
Dictionary<string, TextItem> textItems = new Dictionary<string, TextItem>(); |
||||
|
||||
// Export all character names |
||||
foreach (Character character in GameObject.FindObjectsOfType<Character>()) |
||||
{ |
||||
// String id for character names is CHARACTER.<Character Name> |
||||
TextItem textItem = new TextItem(); |
||||
textItem.standardText = character.nameText; |
||||
textItem.description = character.description; |
||||
string stringId = "CHARACTER." + character.nameText; |
||||
textItems[stringId] = textItem; |
||||
} |
||||
|
||||
// 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>(); |
||||
foreach (Flowchart flowchart in flowcharts) |
||||
{ |
||||
// Have to set a unique localization id to export strings |
||||
if (flowchart.localizationId.Length == 0) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
Block[] blocks = flowchart.GetComponentsInChildren<Block>(); |
||||
foreach (Block block in blocks) |
||||
{ |
||||
foreach (Command command in block.commandList) |
||||
{ |
||||
string stringId = ""; |
||||
string standardText = ""; |
||||
string description = ""; |
||||
|
||||
System.Type type = command.GetType(); |
||||
if (type == typeof(Say)) |
||||
{ |
||||
// String id for Say commands is SAY.<Flowchart id>.<Command id>.<Character Name> |
||||
Say sayCommand = command as Say; |
||||
standardText = sayCommand.storyText; |
||||
description = sayCommand.description; |
||||
stringId = "SAY." + flowchart.localizationId + "." + sayCommand.itemId + "."; |
||||
if (sayCommand.character != null) |
||||
{ |
||||
stringId += sayCommand.character.nameText; |
||||
} |
||||
} |
||||
else if (type == typeof(Menu)) |
||||
{ |
||||
// String id for Menu commands is MENU.<Flowchart id>.<Command id> |
||||
Menu menuCommand = command as Menu; |
||||
standardText = menuCommand.text; |
||||
description = menuCommand.description; |
||||
stringId = "MENU." + flowchart.localizationId + "." + menuCommand.itemId; |
||||
} |
||||
else |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
TextItem textItem = null; |
||||
if (textItems.ContainsKey(stringId)) |
||||
{ |
||||
textItem = textItems[stringId]; |
||||
} |
||||
else |
||||
{ |
||||
textItem = new TextItem(); |
||||
textItems[stringId] = textItem; |
||||
} |
||||
|
||||
// Update basic properties,leaving localised strings intact |
||||
textItem.standardText = standardText; |
||||
textItem.description = description; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return textItems; |
||||
} |
||||
|
||||
/** |
||||
* Adds localized strings from CSV file data to a dictionary of text items in the scene. |
||||
*/ |
||||
protected virtual void AddCSVDataItems(Dictionary<string, TextItem> textItems, string csvData) |
||||
{ |
||||
CsvParser csvParser = new CsvParser(); |
||||
string[][] csvTable = csvParser.Parse(csvData); |
||||
|
||||
if (csvTable.Length <= 1) |
||||
{ |
||||
// No data rows in file |
||||
return; |
||||
} |
||||
|
||||
// Parse header row |
||||
string[] columnNames = csvTable[0]; |
||||
|
||||
for (int i = 1; i < csvTable.Length; ++i) |
||||
{ |
||||
string[] fields = csvTable[i]; |
||||
if (fields.Length < 3) |
||||
{ |
||||
// No standard text or localized string fields present |
||||
continue; |
||||
} |
||||
|
||||
string stringId = fields[0]; |
||||
|
||||
if (!textItems.ContainsKey(stringId)) |
||||
{ |
||||
if (stringId.StartsWith("CHARACTER.") || |
||||
stringId.StartsWith("SAY.") || |
||||
stringId.StartsWith("MENU.")) |
||||
{ |
||||
// If it's a 'built-in' type this probably means that item has been deleted from its flowchart, |
||||
// so there's no need to add a text item for it. |
||||
continue; |
||||
} |
||||
|
||||
// Key not found. Assume it's a custom string that we want to retain, so add a text item for it. |
||||
TextItem newTextItem = new TextItem(); |
||||
newTextItem.description = CSVSupport.Unescape(fields[1]); |
||||
newTextItem.standardText = CSVSupport.Unescape(fields[2]); |
||||
textItems[stringId] = newTextItem; |
||||
} |
||||
|
||||
TextItem textItem = textItems[stringId]; |
||||
|
||||
for (int j = 3; j < fields.Length; ++j) |
||||
{ |
||||
if (j >= columnNames.Length) |
||||
{ |
||||
continue; |
||||
} |
||||
string languageCode = columnNames[j]; |
||||
string languageEntry = CSVSupport.Unescape(fields[j]); |
||||
|
||||
if (languageEntry.Length > 0) |
||||
{ |
||||
textItem.localizedStrings[languageCode] = languageEntry; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Scan a localization CSV file and copies the strings for the specified language code |
||||
* into the text properties of the appropriate scene objects. |
||||
*/ |
||||
public virtual void SetActiveLanguage(string languageCode) |
||||
{ |
||||
if (!Application.isPlaying) |
||||
{ |
||||
// This function should only ever be called when the game is playing (not in editor). |
||||
return; |
||||
} |
||||
|
||||
if (localizationFile == null) |
||||
{ |
||||
// No localization file set |
||||
return; |
||||
} |
||||
|
||||
localizedStrings.Clear(); |
||||
|
||||
CsvParser csvParser = new CsvParser(); |
||||
string[][] csvTable = csvParser.Parse(localizationFile.text); |
||||
|
||||
if (csvTable.Length <= 1) |
||||
{ |
||||
// No data rows in file |
||||
return; |
||||
} |
||||
|
||||
// Parse header row |
||||
string[] columnNames = csvTable[0]; |
||||
|
||||
if (columnNames.Length < 3) |
||||
{ |
||||
// No languages defined in CSV file |
||||
return; |
||||
} |
||||
|
||||
// First assume standard text column and then look for a matching language column |
||||
int languageIndex = 2; |
||||
for (int i = 3; i < columnNames.Length; ++i) |
||||
{ |
||||
if (columnNames[i] == languageCode) |
||||
{ |
||||
languageIndex = i; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (languageIndex == 2) |
||||
{ |
||||
// Using standard text column |
||||
// Add all strings to the localized strings dict, but don't replace standard text in the scene. |
||||
// This allows string substitution to work for both standard and localized text strings. |
||||
for (int i = 1; i < csvTable.Length; ++i) |
||||
{ |
||||
string[] fields = csvTable[i]; |
||||
if (fields.Length < 3) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
localizedStrings[fields[0]] = fields[languageIndex]; |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// Using a localized language text column |
||||
// 1. Add all localized text to the localized strings dict |
||||
// 2. Update all scene text properties with localized versions |
||||
|
||||
// Cache a lookup table of characters in the scene |
||||
Dictionary<string, Character> characterDict = new Dictionary<string, Character>(); |
||||
foreach (Character character in GameObject.FindObjectsOfType<Character>()) |
||||
{ |
||||
characterDict[character.nameText] = character; |
||||
} |
||||
|
||||
// Cache a lookup table of flowcharts in the scene |
||||
Dictionary<string, Flowchart> flowchartDict = new Dictionary<string, Flowchart>(); |
||||
foreach (Flowchart flowChart in GameObject.FindObjectsOfType<Flowchart>()) |
||||
{ |
||||
flowchartDict[flowChart.localizationId] = flowChart; |
||||
} |
||||
|
||||
for (int i = 1; i < csvTable.Length; ++i) |
||||
{ |
||||
string[] fields = csvTable[i]; |
||||
|
||||
if (fields.Length < languageIndex + 1) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
string stringId = fields[0]; |
||||
string languageEntry = CSVSupport.Unescape(fields[languageIndex]); |
||||
|
||||
if (languageEntry.Length > 0) |
||||
{ |
||||
localizedStrings[stringId] = languageEntry; |
||||
PopulateTextProperty(stringId, languageEntry, flowchartDict, characterDict); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Populates the text property of a single scene object with a new text value. |
||||
*/ |
||||
public virtual bool PopulateTextProperty(string stringId, |
||||
string newText, |
||||
Dictionary<string, Flowchart> flowchartDict, |
||||
Dictionary<string, Character> characterDict) |
||||
{ |
||||
string[] idParts = stringId.Split('.'); |
||||
if (idParts.Length == 0) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
string stringType = idParts[0]; |
||||
if (stringType == "SAY") |
||||
{ |
||||
if (idParts.Length != 4) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
string flowchartId = idParts[1]; |
||||
if (!flowchartDict.ContainsKey(flowchartId)) |
||||
{ |
||||
return false; |
||||
} |
||||
Flowchart flowchart = flowchartDict[flowchartId]; |
||||
|
||||
int itemId = int.Parse(idParts[2]); |
||||
|
||||
if (flowchart != null) |
||||
{ |
||||
foreach (Say say in flowchart.GetComponentsInChildren<Say>()) |
||||
{ |
||||
if (say.itemId == itemId && |
||||
say.storyText != newText) |
||||
{ |
||||
#if UNITY_EDITOR |
||||
Undo.RecordObject(say, "Set Text"); |
||||
#endif |
||||
|
||||
say.storyText = newText; |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
else if (stringType == "MENU") |
||||
{ |
||||
if (idParts.Length != 3) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
string flowchartId = idParts[1]; |
||||
if (!flowchartDict.ContainsKey(flowchartId)) |
||||
{ |
||||
return false; |
||||
} |
||||
Flowchart flowchart = flowchartDict[flowchartId]; |
||||
|
||||
int itemId = int.Parse(idParts[2]); |
||||
|
||||
if (flowchart != null) |
||||
{ |
||||
foreach (Menu menu in flowchart.GetComponentsInChildren<Menu>()) |
||||
{ |
||||
if (menu.itemId == itemId && |
||||
menu.text != newText) |
||||
{ |
||||
#if UNITY_EDITOR |
||||
Undo.RecordObject(menu, "Set Text"); |
||||
#endif |
||||
|
||||
menu.text = newText; |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
else if (stringType == "CHARACTER") |
||||
{ |
||||
if (idParts.Length != 2) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
string characterName = idParts[1]; |
||||
if (!characterDict.ContainsKey(characterName)) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
Character character = characterDict[characterName]; |
||||
if (character != null && |
||||
character.nameText != newText) |
||||
{ |
||||
#if UNITY_EDITOR |
||||
Undo.RecordObject(character, "Set Text"); |
||||
#endif |
||||
|
||||
character.nameText = newText; |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Returns all standard text for SAY & MENU commands in the scene using an |
||||
* easy to edit custom text format. |
||||
*/ |
||||
public virtual string GetStandardText() |
||||
{ |
||||
// Collect all the text items present in the scene |
||||
Dictionary<string, TextItem> textItems = FindTextItems(); |
||||
|
||||
string textData = ""; |
||||
int rowCount = 0; |
||||
foreach (string stringId in textItems.Keys) |
||||
{ |
||||
if (!stringId.StartsWith("SAY.") && !(stringId.StartsWith("MENU."))) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
TextItem languageItem = textItems[stringId]; |
||||
|
||||
textData += "#" + stringId + "\n"; |
||||
textData += languageItem.standardText.Trim() + "\n\n"; |
||||
rowCount++; |
||||
} |
||||
|
||||
notificationText = "Exported " + rowCount + " standard text items."; |
||||
|
||||
return textData; |
||||
} |
||||
|
||||
/** |
||||
* Sets standard text on scene objects by parsing a text data file. |
||||
*/ |
||||
public virtual void SetStandardText(string textData) |
||||
{ |
||||
// Cache a lookup table of characters in the scene |
||||
Dictionary<string, Character> characterDict = new Dictionary<string, Character>(); |
||||
foreach (Character character in GameObject.FindObjectsOfType<Character>()) |
||||
{ |
||||
characterDict[character.nameText] = character; |
||||
} |
||||
|
||||
// Cache a lookup table of flowcharts in the scene |
||||
Dictionary<string, Flowchart> flowchartDict = new Dictionary<string, Flowchart>(); |
||||
foreach (Flowchart flowChart in GameObject.FindObjectsOfType<Flowchart>()) |
||||
{ |
||||
flowchartDict[flowChart.localizationId] = flowChart; |
||||
} |
||||
|
||||
string[] lines = textData.Split('\n'); |
||||
|
||||
int updatedCount = 0; |
||||
|
||||
string stringId = ""; |
||||
string buffer = ""; |
||||
foreach (string line in lines) |
||||
{ |
||||
// Check for string id line |
||||
if (line.StartsWith("#")) |
||||
{ |
||||
if (stringId.Length > 0) |
||||
{ |
||||
// Write buffered text to the appropriate text property |
||||
if (PopulateTextProperty(stringId, buffer.Trim(), flowchartDict, characterDict)) |
||||
{ |
||||
updatedCount++; |
||||
} |
||||
} |
||||
|
||||
// Set the string id for the follow text lines |
||||
stringId = line.Substring(1, line.Length - 1); |
||||
buffer = ""; |
||||
} |
||||
else |
||||
{ |
||||
buffer += line; |
||||
} |
||||
} |
||||
|
||||
// Handle last buffered entry |
||||
if (stringId.Length > 0) |
||||
{ |
||||
if (PopulateTextProperty(stringId, buffer.Trim(), flowchartDict, characterDict)) |
||||
{ |
||||
updatedCount++; |
||||
} |
||||
} |
||||
|
||||
notificationText = "Updated " + updatedCount + " standard text items."; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2 |
||||
guid: e5724422a635e425bae0af9ffe2615d6 |
||||
timeCreated: 1427886378 |
||||
licenseType: Free |
||||
MonoImporter: |
||||
serializedVersion: 2 |
||||
defaultReferences: [] |
||||
executionOrder: 0 |
||||
icon: {instanceID: 0} |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2 |
||||
guid: 3469ab31c1c9d4c2da1ae42edc001ded |
||||
folderAsset: yes |
||||
timeCreated: 1428523768 |
||||
licenseType: Free |
||||
DefaultImporter: |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
@ -0,0 +1,48 @@
|
||||
using UnityEngine; |
||||
using System.Collections; |
||||
using System.Text.RegularExpressions; |
||||
using System.Linq; |
||||
using System; |
||||
|
||||
namespace Fungus |
||||
{ |
||||
|
||||
// Some CSV utilities cobbled together from stack overflow answers |
||||
// CSV escape & unescape from http://stackoverflow.com/questions/769621/dealing-with-commas-in-a-csv-file |
||||
// http://answers.unity3d.com/questions/144200/are-there-any-csv-reader-for-unity3d-without-needi.html |
||||
public static class CSVSupport |
||||
{ |
||||
public static string Escape( string s ) |
||||
{ |
||||
s = s.Replace("\n", "\\n"); |
||||
|
||||
if ( s.Contains( QUOTE ) ) |
||||
s = s.Replace( QUOTE, ESCAPED_QUOTE ); |
||||
|
||||
if ( s.IndexOfAny( CHARACTERS_THAT_MUST_BE_QUOTED ) > -1 ) |
||||
s = QUOTE + s + QUOTE; |
||||
|
||||
return s; |
||||
} |
||||
|
||||
public static string Unescape( string s ) |
||||
{ |
||||
s = s.Replace("\\n", "\n"); |
||||
|
||||
if ( s.StartsWith( QUOTE ) && s.EndsWith( QUOTE ) ) |
||||
{ |
||||
s = s.Substring( 1, s.Length - 2 ); |
||||
|
||||
if ( s.Contains( ESCAPED_QUOTE ) ) |
||||
s = s.Replace( ESCAPED_QUOTE, QUOTE ); |
||||
} |
||||
|
||||
return s; |
||||
} |
||||
|
||||
private const string QUOTE = "\""; |
||||
private const string ESCAPED_QUOTE = "\"\""; |
||||
private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' }; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2 |
||||
guid: 4468f4dcfdfbf46b088949ea57ed6135 |
||||
timeCreated: 1427897861 |
||||
licenseType: Free |
||||
MonoImporter: |
||||
serializedVersion: 2 |
||||
defaultReferences: [] |
||||
executionOrder: 0 |
||||
icon: {instanceID: 0} |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2 |
||||
guid: 803c0d4d8bc9447d1b20e1f4fb86120f |
||||
timeCreated: 1428171346 |
||||
licenseType: Free |
||||
MonoImporter: |
||||
serializedVersion: 2 |
||||
defaultReferences: [] |
||||
executionOrder: 0 |
||||
icon: {instanceID: 0} |
||||
userData: |
||||
assetBundleName: |
||||
assetBundleVariant: |
Loading…
Reference in new issue