Skip to content

Commit

Permalink
Move StringTable CSV parser to a dedicated class
Browse files Browse the repository at this point in the history
  • Loading branch information
Interkarma committed Nov 21, 2022
1 parent 61be174 commit d921931
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 96 deletions.
116 changes: 116 additions & 0 deletions Assets/Scripts/Game/StringTableCSVParser.cs
@@ -0,0 +1,116 @@
// Project: Daggerfall Unity
// Copyright: Copyright (C) 2009-2022 Daggerfall Workshop
// Web Site: http://www.dfworkshop.net
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code: https://github.com/Interkarma/daggerfall-unity
// Original Author: Gavin Clayton (interkarma@dfworkshop.net)
// Contributors:
//
// Notes:
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;

/// <summary>
/// Basic CSV parser for Windows-styled CSV files containing StringTable Key and Value columns only.
/// This parser is intended only for importing string tables for localization. It is not a general purpose CSV parser.
/// </summary>
public class StringTableCSVParser
{
const string csvExt = ".csv";
const string textString = "Text";
const string keyString = "Key";
const string valueString = "Value";

/// <summary>
/// Loads a CSV patch file for in-game text.
/// At this time, string patch files must be in the StreamingAssets/Text folder to load.
/// TODO: Support loading CSV file from .dfmod.
/// </summary>
/// <param name="filename">Filename of StringTable CSV file.</param>
/// <returns>KeyValuePair for each row if successful, otherwise null.</returns>
public static KeyValuePair<string, string>[] Load(string filename)
{
string csvText = null;

if (!filename.EndsWith(csvExt))
filename += csvExt;

// Load patch file if present
string path = Path.Combine(Application.streamingAssetsPath, textString, filename);
if (File.Exists(path))
{
csvText = ReadAllText(path);
if (string.IsNullOrEmpty(csvText))
return null;
}
else
{
return null;
}

// Parse into CSV rows
KeyValuePair<string, string>[] rows = null;
try
{
rows = ParseCSVRows(csvText);
}
catch (Exception ex)
{
Debug.LogErrorFormat("Could not parse CSV from file '{0}'. Exception message: {1}", filename, ex.Message);
return null;
}

return rows;
}

/// <summary>
/// Parse source CSV data into key/value pairs separated by comma character.
/// Source CSV file must have only two columns for Key and Value.
/// </summary>
/// <param name="csvText">Source CSV data.</param>
/// <returns>KeyValuePair for each row.</returns>
static KeyValuePair<string, string>[] ParseCSVRows(string csvText)
{
// Regex pattern from https://gist.github.com/awwsmm/886ac0ce0cef517ad7092915f708175f
const string linePattern = "(?:,|\\n|^)(\"(?:(?:\"\")*[^\"]*)*\"|[^\",\\n]*|(?:\\n|$))";

// Split source CSV based on regex matches
char[] trimChars = { '\r', '\n', '\"', ',' };
List<KeyValuePair<string, string>> rows = new List<KeyValuePair<string, string>>();
string[] matches = (from Match m in Regex.Matches(csvText, linePattern, RegexOptions.ExplicitCapture) select m.Groups[0].Value).ToArray();
int pos = 0;
while (pos < matches.Length)
{
if (pos + 1 == matches.Length)
{
// Exit if no valid pair at end of csv (likely an empty line at end of source data)
break;
}
string key = matches[pos++].Trim(trimChars);
string value = matches[pos++].Trim(trimChars);
KeyValuePair<string, string> kvp = new KeyValuePair<string, string>(key, value);
rows.Add(kvp);
}

return rows.ToArray();
}

/// <summary>
/// Read text from a file in read-only access mode.
/// Allows modder to keep CSV open in Excel without throwing exception in game.
/// </summary>
/// <param name="file">File to open.</param>
/// <returns>All text read from file.</returns>
static string ReadAllText(string file)
{
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var textReader = new StreamReader(fileStream))
return textReader.ReadToEnd();
}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/Game/StringTableCSVParser.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 1 addition & 96 deletions Assets/Scripts/Game/StringTablePatcher.cs
Expand Up @@ -9,37 +9,24 @@
// Notes:
//

using DaggerfallWorkshop;
using DaggerfallWorkshop.Game;
using DaggerfallWorkshop.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.Localization;
using UnityEngine;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
using static UnityEditor.Localization.LocalizationTableCollection;

[Serializable]
public class StringTablePatcher : ITablePostprocessor
{
const string csvExt = ".csv";
const string textString = "Text";
const string keyString = "Key";
const string valueString = "Value";

public void PostprocessTable(LocalizationTable table)
{
// Only supports StringTable input
if (!(table is StringTable stringTable))
return;

// Load table patch data (if present)
KeyValuePair<string, string>[] rows = LoadCSVPatchFile(table.TableCollectionName + csvExt);
KeyValuePair<string, string>[] rows = StringTableCSVParser.Load(table.TableCollectionName);
if (rows == null || rows.Length == 0)
return;

Expand All @@ -54,88 +41,6 @@ public void PostprocessTable(LocalizationTable table)
}
}

/// <summary>
/// Loads a CSV patch file for in-game text.
/// </summary>
/// <param name="filename">Filename of patch file including .csv extension. Patch file must be in the StreamingAssets/Text folder to load.</param>
/// <returns>KeyValuePair for each row.</returns>
KeyValuePair<string, string>[] LoadCSVPatchFile(string filename)
{
string csvText = null;

// Load patch file if present
string path = Path.Combine(Application.streamingAssetsPath, textString, filename);
if (File.Exists(path))
{
csvText = ReadAllText(path);
if (string.IsNullOrEmpty(csvText))
return null;
}
else
{
return null;
}

// Parse into CSV rows
KeyValuePair<string, string>[] rows = null;
try
{
rows = ParseCSVRows(csvText);
}
catch (Exception ex)
{
Debug.LogErrorFormat("Could not parse CSV from file '{0}'. Exception message: {1}", filename, ex.Message);
return null;
}

return rows;
}

/// <summary>
/// Read text from a file in read-only access mode.
/// Allows modder to keep CSV open in Excel without throwing exception in game.
/// </summary>
/// <param name="file">File to open.</param>
/// <returns>All text read from file.</returns>
string ReadAllText(string file)
{
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var textReader = new StreamReader(fileStream))
return textReader.ReadToEnd();
}

/// <summary>
/// Parse source CSV data into key/value pairs separated by comma character.
/// Source CSV file must have only two columns for Key and Value.
/// </summary>
/// <param name="csvText">Source CSV data.</param>
/// <returns>KeyValuePair for each row.</returns>
KeyValuePair<string, string>[] ParseCSVRows(string csvText)
{
// Regex pattern from https://gist.github.com/awwsmm/886ac0ce0cef517ad7092915f708175f
const string linePattern = "(?:,|\\n|^)(\"(?:(?:\"\")*[^\"]*)*\"|[^\",\\n]*|(?:\\n|$))";

// Split source CSV based on regex matches
char[] trimChars = { '\r', '\n', '\"', ',' };
List<KeyValuePair<string, string>> rows = new List<KeyValuePair<string, string>>();
string[] matches = (from Match m in Regex.Matches(csvText, linePattern, RegexOptions.ExplicitCapture) select m.Groups[0].Value).ToArray();
int pos = 0;
while (pos < matches.Length)
{
if (pos + 1 == matches.Length)
{
// Exit if no valid pair at end of csv (likely an empty line at end of source data)
break;
}
string key = matches[pos++].Trim(trimChars);
string value = matches[pos++].Trim(trimChars);
KeyValuePair<string, string> kvp = new KeyValuePair<string, string>(key, value);
rows.Add(kvp);
}

return rows.ToArray();
}

/// <summary>
/// TablePostprocessor field does not appear to be exposed in-editor yet.
/// Below used one-time to add patcher to localization settings.
Expand Down
1 change: 1 addition & 0 deletions Assets/Scripts/Game/UserInterface/DaggerfallFont.cs
Expand Up @@ -632,6 +632,7 @@ float GetGlyphSpacing()

/// <summary>
/// Replace TMP font asset using a .ttf or .otf font in StreamingAssets/Fonts.
/// TODO: Support loading font file from .dfmod.
/// </summary>
/// <param name="filename">Filename of replacement font including .ttf extension. Font file must be present in StreamingAssets/Fonts to load.</param>
/// <param name="source">Source TMP font for initial character table population.</param>
Expand Down

0 comments on commit d921931

Please sign in to comment.