334 lines
13 KiB
C#
334 lines
13 KiB
C#
using ModelRenderer.Scripts.Common;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using UnityEngine; // thêm để dùng Resources & TextAsset
|
||
|
||
namespace BrewMonster
|
||
{
|
||
public class CECStringTab
|
||
{
|
||
private readonly Dictionary<int, string> m_AStrTab = new Dictionary<int, string>();
|
||
private readonly Dictionary<int, string> m_WStrTab = new Dictionary<int, string>();
|
||
|
||
private bool m_bInit = false;
|
||
private bool m_bUnicode = false;
|
||
|
||
public CECStringTab() { }
|
||
~CECStringTab() { Release(); }
|
||
|
||
/// <summary>
|
||
/// Initialize the table directly from a Unity TextAsset (e.g. loaded via Addressables).
|
||
/// </summary>
|
||
public bool InitFromTextAsset(TextAsset textAsset, bool bUnicode)
|
||
{
|
||
Release();
|
||
m_bUnicode = bUnicode;
|
||
|
||
try
|
||
{
|
||
if (textAsset == null)
|
||
{
|
||
Debug.LogError("[CECStringTab] InitFromTextAsset failed: textAsset is null");
|
||
return false;
|
||
}
|
||
|
||
bool ok;
|
||
if (bUnicode)
|
||
{
|
||
// Unity TextAsset.text is already UTF-8 decoded.
|
||
using var sr = new StringReader(textAsset.text);
|
||
ok = ParseIntoDict(sr, isWide: true);
|
||
}
|
||
else
|
||
{
|
||
// ANSI tables are in CP936 in original PW; keep using CP936 decoder.
|
||
string content = ByteToStringUtils.ByteArrayToCP936String(textAsset.bytes);
|
||
using var sr = new StringReader(content);
|
||
ok = ParseIntoDict(sr, isWide: false);
|
||
}
|
||
|
||
m_bInit = ok;
|
||
return ok;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[CECStringTab] InitFromTextAsset failed: {e}");
|
||
Release();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public bool Init(string szFile, bool bUnicode)
|
||
{
|
||
Release();
|
||
m_bUnicode = bUnicode;
|
||
|
||
try
|
||
{
|
||
bool ok = bUnicode ? LoadWideStrings(szFile) : LoadANSIStrings(szFile);
|
||
m_bInit = ok;
|
||
return ok;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[CECStringTab] Init failed: {e}");
|
||
Release();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public void Release()
|
||
{
|
||
m_AStrTab.Clear();
|
||
m_WStrTab.Clear();
|
||
m_bInit = false;
|
||
m_bUnicode = false;
|
||
}
|
||
|
||
public string GetANSIString(int n) => m_AStrTab.TryGetValue(n, out var s) ? s : null;
|
||
public string GetWideString(int n) => m_WStrTab.TryGetValue(n, out var s) ? s : null;
|
||
public string GetWideStringObject(int n) => GetWideString(n);
|
||
public bool IsInitialized() => m_bInit;
|
||
|
||
// ==== Đọc từ Resources thay vì đường dẫn ====
|
||
|
||
protected bool LoadANSIStrings(string resourceName)
|
||
{
|
||
try
|
||
{
|
||
// If a real file path is provided (e.g. StreamingAssets), read directly from disk.
|
||
// 如果提供的是实际文件路径(例如 StreamingAssets),则直接从磁盘读取。
|
||
if (File.Exists(resourceName))
|
||
{
|
||
// ANSI tables are in CP936 in original PW; keep using CP936 decoder.
|
||
// 原版完美世界的ANSI表是CP936编码,这里保持一致。
|
||
byte[] bytes = File.ReadAllBytes(resourceName);
|
||
string content = ByteToStringUtils.ByteArrayToCP936String(bytes);
|
||
using var srFile = new StringReader(content);
|
||
return ParseIntoDict(srFile, isWide: false);
|
||
}
|
||
|
||
// Fallback to Resources (old behaviour).
|
||
// 回退到 Resources 加载(旧行为)。
|
||
TextAsset textAsset = Resources.Load<TextAsset>(resourceName);
|
||
if (textAsset == null)
|
||
{
|
||
Debug.LogError($"[CECStringTab] Resource not found: {resourceName}");
|
||
return false;
|
||
}
|
||
|
||
string resContent = ByteToStringUtils.ByteArrayToCP936String(textAsset.bytes);
|
||
using var srRes = new StringReader(resContent);
|
||
return ParseIntoDict(srRes, isWide: false);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[CECStringTab] LoadANSIStrings failed for '{resourceName}': {e}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
protected bool LoadWideStrings(string resourceName)
|
||
{
|
||
try
|
||
{
|
||
// Support absolute / relative filesystem paths (e.g. StreamingAssets/configs/*.txt)
|
||
// 支持文件系统路径(例如 StreamingAssets/configs/*.txt)
|
||
if (File.Exists(resourceName))
|
||
{
|
||
// String tables we ship in StreamingAssets are saved as UTF-8.
|
||
// 我们放在 StreamingAssets 里的字符串表保存为 UTF-8。
|
||
string content = File.ReadAllText(resourceName, Encoding.UTF8);
|
||
using var srFile = new StringReader(content);
|
||
return ParseIntoDict(srFile, isWide: true);
|
||
}
|
||
|
||
// Fallback to Resources-based loading (old behaviour)
|
||
// 回退到基于 Resources 的加载(旧行为)
|
||
TextAsset textAsset = Resources.Load<TextAsset>(resourceName);
|
||
if (textAsset == null)
|
||
{
|
||
Debug.LogError($"[CECStringTab] Resource not found: {resourceName}");
|
||
return false;
|
||
}
|
||
|
||
// Unity TextAsset.text is already UTF-8 decoded.
|
||
string resContent = textAsset.text;
|
||
using var srRes = new StringReader(resContent);
|
||
return ParseIntoDict(srRes, isWide: true);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[CECStringTab] LoadWideStrings failed for '{resourceName}': {e}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static Encoding DetectEncoding(byte[] bom)
|
||
{
|
||
if (bom.Length >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) return Encoding.UTF8;
|
||
if (bom.Length >= 2 && bom[0] == 0xFF && bom[1] == 0xFE) return Encoding.Unicode;
|
||
if (bom.Length >= 2 && bom[0] == 0xFE && bom[1] == 0xFF) return Encoding.BigEndianUnicode;
|
||
return null;
|
||
}
|
||
|
||
private bool ParseIntoDict(StringReader sr, bool isWide)
|
||
{
|
||
bool bIndexMode = false;
|
||
bool bBegan = false;
|
||
int autoIndex = 0;
|
||
|
||
var allLines = new List<string>();
|
||
string line;
|
||
while ((line = sr.ReadLine()) != null)
|
||
{
|
||
allLines.Add(line);
|
||
}
|
||
|
||
for (int i = 0; i < allLines.Count; i++)
|
||
{
|
||
var ln = allLines[i].Trim();
|
||
if (ln.Length == 0) continue;
|
||
|
||
if (ln.Equals("#_index", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
bIndexMode = true;
|
||
}
|
||
else if (ln.Equals("#_begin", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
bBegan = true;
|
||
|
||
for (int j = i + 1; j < allLines.Count; j++)
|
||
{
|
||
var payload = allLines[j].Trim();
|
||
if (payload.Length == 0) continue;
|
||
if (payload.StartsWith("#")) continue;
|
||
if (payload.StartsWith("//")) continue;
|
||
|
||
if (bIndexMode)
|
||
{
|
||
if (!TrySplitIndexAndText(payload, out int idx, out string text))
|
||
continue;
|
||
|
||
// Check if the text is a multiline quoted string
|
||
string fullText = ReadMultilineQuotedString(text, allLines, ref j);
|
||
PutString(idx, fullText, isWide);
|
||
}
|
||
else
|
||
{
|
||
PutString(autoIndex++, payload, isWide);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
return bBegan;
|
||
}
|
||
|
||
private static bool TrySplitIndexAndText(string line, out int index, out string text)
|
||
{
|
||
index = 0; text = null;
|
||
|
||
int eq = line.IndexOf('=');
|
||
if (eq >= 0)
|
||
{
|
||
var left = line.Substring(0, eq).Trim();
|
||
var right = line.Substring(eq + 1);
|
||
if (int.TryParse(left, out index))
|
||
{
|
||
text = right;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int sp = FirstWhiteSpaceIndex(line);
|
||
if (sp <= 0) return false;
|
||
|
||
var left2 = line.Substring(0, sp).Trim();
|
||
var right2 = line.Substring(sp).TrimStart();
|
||
if (int.TryParse(left2, out index))
|
||
{
|
||
if (right2.Length > 0 && (right2[0] == '"' || right2[0] == '\''))
|
||
{
|
||
text = right2;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private static int FirstWhiteSpaceIndex(string s)
|
||
{
|
||
for (int i = 0; i < s.Length; i++)
|
||
if (char.IsWhiteSpace(s[i])) return i;
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reads a multiline quoted string similar to C++ AWScriptFile.GetNextToken(false)
|
||
/// If the string starts with " but doesn't end with ", continues reading subsequent lines
|
||
/// </summary>
|
||
private string ReadMultilineQuotedString(string firstLine, List<string> allLines, ref int currentIndex)
|
||
{
|
||
// If it doesn't start with a quote, return as-is
|
||
if (string.IsNullOrEmpty(firstLine) || firstLine[0] != '"')
|
||
return firstLine;
|
||
|
||
// Check if the string is already complete (starts and ends with quotes on same line)
|
||
if (firstLine.Length >= 2 && firstLine[firstLine.Length - 1] == '"')
|
||
{
|
||
// Check if it's not an escaped quote by looking at preceding character
|
||
// Simple check: if there's more than one char and last is ", assume complete
|
||
return firstLine;
|
||
}
|
||
|
||
// The string is incomplete - need to read more lines
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.Append(firstLine);
|
||
|
||
// Continue reading lines until we find the closing quote
|
||
for (int k = currentIndex + 1; k < allLines.Count; k++)
|
||
{
|
||
string nextLine = allLines[k];
|
||
|
||
// Append newline to preserve original formatting (matching C++ behavior)
|
||
sb.Append("\n");
|
||
sb.Append(nextLine);
|
||
|
||
// Check if this line contains the closing quote
|
||
// Look for " at the end of the trimmed line
|
||
string trimmedNext = nextLine.TrimEnd();
|
||
if (trimmedNext.Length > 0 && trimmedNext[trimmedNext.Length - 1] == '"')
|
||
{
|
||
// Found the closing quote, update the index and return
|
||
currentIndex = k;
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
|
||
// If we reach here, the closing quote wasn't found - return what we have
|
||
return sb.ToString();
|
||
}
|
||
|
||
private void PutString(int id, string value, bool isWide)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
return;
|
||
|
||
// Many PW string tables wrap the payload in double quotes, e.g.:
|
||
// 12345 "^ffcb4aSome text\rMore text"
|
||
// Strip a single leading/trailing quote pair to avoid showing raw quotes in UI.
|
||
// 许多字符串表会用双引号包裹内容,这里去掉首尾各一个引号以避免在UI中显示多余的引号。
|
||
if (value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"')
|
||
{
|
||
value = value.Substring(1, value.Length - 2);
|
||
}
|
||
if (isWide) m_WStrTab[id] = value;
|
||
else m_AStrTab[id] = value;
|
||
}
|
||
}
|
||
} |