Files
2026-04-21 10:41:04 +07:00

844 lines
30 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using ModelRenderer.Scripts.Common;
using PerfectWorld.Scripts.Task;
using UnityEditor;
using UnityEngine;
namespace BrewMonster.Scripts.Task
{
public static class TaskCsvExporter
{
private const string MenuPath = "Tools/Task/Export Tasks To CSV";
private const string OutputAssetPath = "Assets/PerfectWorld/Exports/tasks_export.csv";
private const string KnownTaskContainerAssetPath = "Assets/PerfectWorld/SO/TaskTemplContainerSO.asset";
private const int MaxSerializationDepth = 12;
private static readonly string[] CustomHeaders =
{
"TaskId",
"ParentId",
"PrevSiblingId",
"NextSiblingId",
"FirstChildId",
"Depth",
"SubCount",
"Name",
"Description",
"OkText",
"NoText",
"Tribute",
"Signature",
"DelvTaskTalkJson",
"UnqualifiedTalkJson",
"DelvItemTalkJson",
"ExeTalkJson",
"AwardTalkJson"
};
private static readonly FieldInfo AllTaskTemplatesField = typeof(TaskTemplContainerSO).GetField(
"_allTaskTemplates",
BindingFlags.Instance | BindingFlags.NonPublic);
[MenuItem(MenuPath)]
public static void ExportTasksToCsv()
{
TaskTemplContainerSO container = null;
bool destroyTemporaryContainer = false;
try
{
container = LoadOrBuildTaskContainer(out destroyTemporaryContainer);
if (container == null)
{
Debug.LogError("[TaskCsvExporter] Could not load task data from SO or pack file.");
return;
}
container.BuildTaskTemplateMap();
List<ATaskTempl> allTemplates = GetAllTaskTemplates(container);
if (allTemplates.Count == 0)
{
Debug.LogError("[TaskCsvExporter] No task templates were found to export.");
return;
}
PrepareTemplatesForExport(container, allTemplates);
List<ATaskTempl> orderedTasks = BuildOrderedTaskList(container, allTemplates);
List<FieldInfo> fixedDataFields = GetOrderedPublicFields(typeof(ATaskTemplFixedData));
List<Dictionary<string, string>> rows = BuildRows(orderedTasks, fixedDataFields);
ValidateExport(rows, orderedTasks.Count, allTemplates.Count);
WriteCsv(rows, fixedDataFields, OutputAssetPath);
AssetDatabase.ImportAsset(OutputAssetPath, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
int readableNameCount = rows.Count(row =>
row.TryGetValue("Name", out string value) && !string.IsNullOrWhiteSpace(value));
Debug.Log(
$"[TaskCsvExporter] Exported {rows.Count} task rows to {OutputAssetPath}. " +
$"SourceCount={allTemplates.Count}, NamedRows={readableNameCount}");
}
catch (Exception ex)
{
Debug.LogException(ex);
}
finally
{
if (destroyTemporaryContainer && container != null)
{
UnityEngine.Object.DestroyImmediate(container);
}
}
}
private static TaskTemplContainerSO LoadOrBuildTaskContainer(out bool destroyTemporaryContainer)
{
destroyTemporaryContainer = false;
TaskTemplContainerSO existing = LoadExistingTaskContainer();
if (HasTaskData(existing))
{
return existing;
}
string taskPackPath = Path.Combine(Application.streamingAssetsPath, "data/tasks.data");
if (!File.Exists(taskPackPath))
{
throw new FileNotFoundException(
$"Task pack file not found at '{taskPackPath}'.",
taskPackPath);
}
TaskTemplContainerSO temporaryContainer = ScriptableObject.CreateInstance<TaskTemplContainerSO>();
temporaryContainer.LoadAllTasksFromPack();
destroyTemporaryContainer = true;
return temporaryContainer;
}
private static TaskTemplContainerSO LoadExistingTaskContainer()
{
TaskTemplContainerSO container = AssetDatabase.LoadAssetAtPath<TaskTemplContainerSO>(KnownTaskContainerAssetPath);
if (container != null)
{
return container;
}
string[] guids = AssetDatabase.FindAssets("t:TaskTemplContainerSO");
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
container = AssetDatabase.LoadAssetAtPath<TaskTemplContainerSO>(assetPath);
if (container != null)
{
return container;
}
}
return null;
}
private static bool HasTaskData(TaskTemplContainerSO container)
{
return container != null && GetAllTaskTemplates(container).Count > 0;
}
private static List<ATaskTempl> GetAllTaskTemplates(TaskTemplContainerSO container)
{
if (container == null || AllTaskTemplatesField == null)
{
return new List<ATaskTempl>();
}
List<ATaskTempl> templates = AllTaskTemplatesField.GetValue(container) as List<ATaskTempl>;
return templates?.Where(template => template != null).ToList() ?? new List<ATaskTempl>();
}
private static void PrepareTemplatesForExport(TaskTemplContainerSO container, IEnumerable<ATaskTempl> templates)
{
foreach (ATaskTempl template in templates)
{
template.FillUnserializableDataWhenPlayGame(container);
}
}
private static List<ATaskTempl> BuildOrderedTaskList(TaskTemplContainerSO container, IReadOnlyCollection<ATaskTempl> allTemplates)
{
var ordered = new List<ATaskTempl>(allTemplates.Count);
var visitedIds = new HashSet<uint>();
foreach (ATaskTempl topTask in container.TopTaskTemplates)
{
TraverseTaskTree(topTask, ordered, visitedIds);
}
foreach (ATaskTempl template in allTemplates.OrderBy(template => template.m_ID))
{
TraverseTaskTree(template, ordered, visitedIds);
}
return ordered;
}
private static void TraverseTaskTree(ATaskTempl task, ICollection<ATaskTempl> ordered, ISet<uint> visitedIds)
{
if (task == null || !visitedIds.Add(task.m_ID))
{
return;
}
ordered.Add(task);
ATaskTempl child = task.m_pFirstChild;
while (child != null)
{
TraverseTaskTree(child, ordered, visitedIds);
child = child.m_pNextSibling;
}
}
private static List<Dictionary<string, string>> BuildRows(IEnumerable<ATaskTempl> tasks, IReadOnlyList<FieldInfo> fixedDataFields)
{
var rows = new List<Dictionary<string, string>>();
foreach (ATaskTempl task in tasks)
{
var row = new Dictionary<string, string>(StringComparer.Ordinal)
{
["TaskId"] = task.m_ID.ToString(CultureInfo.InvariantCulture),
["ParentId"] = task.ParentID.ToString(CultureInfo.InvariantCulture),
["PrevSiblingId"] = task.PrevSiblingID.ToString(CultureInfo.InvariantCulture),
["NextSiblingId"] = task.NextSiblingID.ToString(CultureInfo.InvariantCulture),
["FirstChildId"] = task.FirstChildID.ToString(CultureInfo.InvariantCulture),
["Depth"] = task.m_uDepth.ToString(CultureInfo.InvariantCulture),
["SubCount"] = task.m_nSubCount.ToString(CultureInfo.InvariantCulture),
["Name"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_FixedData.m_szName),
["Description"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_pwstrDescript),
["OkText"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_pwstrOkText),
["NoText"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_pwstrNoText),
["Tribute"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_pwstrTribute),
["Signature"] = ByteToStringUtils.UshortArrayToUnicodeString(task.m_FixedData.m_pszSignature),
["DelvTaskTalkJson"] = SerializeJsonValue(task.m_DelvTaskTalk, typeof(talk_proc), "m_DelvTaskTalk", 0),
["UnqualifiedTalkJson"] = SerializeJsonValue(task.m_UnqualifiedTalk, typeof(talk_proc), "m_UnqualifiedTalk", 0),
["DelvItemTalkJson"] = SerializeJsonValue(task.m_DelvItemTalk, typeof(talk_proc), "m_DelvItemTalk", 0),
["ExeTalkJson"] = SerializeJsonValue(task.m_ExeTalk, typeof(talk_proc), "m_ExeTalk", 0),
["AwardTalkJson"] = SerializeJsonValue(task.m_AwardTalk, typeof(talk_proc), "m_AwardTalk", 0)
};
object boxedFixedData = task.m_FixedData;
foreach (FieldInfo field in fixedDataFields)
{
object value = field.GetValue(boxedFixedData);
row[field.Name] = SerializeColumnValue(value, field.FieldType, field.Name);
}
rows.Add(row);
}
return rows;
}
private static void ValidateExport(IReadOnlyCollection<Dictionary<string, string>> rows, int traversedCount, int sourceCount)
{
if (rows.Count != traversedCount)
{
throw new InvalidOperationException(
$"CSV row count mismatch. Rows={rows.Count}, Traversed={traversedCount}.");
}
if (traversedCount != sourceCount)
{
throw new InvalidOperationException(
$"Task traversal mismatch. Traversed={traversedCount}, Source={sourceCount}.");
}
}
private static void WriteCsv(IEnumerable<Dictionary<string, string>> rows, IReadOnlyList<FieldInfo> fixedDataFields, string assetPath)
{
string absolutePath = GetAbsoluteProjectPath(assetPath);
string directory = Path.GetDirectoryName(absolutePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var headers = new List<string>(CustomHeaders.Length + fixedDataFields.Count);
headers.AddRange(CustomHeaders);
headers.AddRange(fixedDataFields.Select(field => field.Name));
var builder = new StringBuilder(1024 * 1024);
builder.AppendLine(string.Join(",", headers.Select(CsvEscape)));
foreach (Dictionary<string, string> row in rows)
{
string[] values = headers
.Select(header => row.TryGetValue(header, out string value) ? value : string.Empty)
.Select(CsvEscape)
.ToArray();
builder.AppendLine(string.Join(",", values));
}
File.WriteAllText(absolutePath, builder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
}
private static string GetAbsoluteProjectPath(string assetPath)
{
string projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? Directory.GetCurrentDirectory();
return Path.Combine(projectRoot, assetPath);
}
private static string CsvEscape(string value)
{
value ??= string.Empty;
bool needsQuotes =
value.IndexOf(',') >= 0 ||
value.IndexOf('"') >= 0 ||
value.IndexOf('\n') >= 0 ||
value.IndexOf('\r') >= 0;
if (!needsQuotes)
{
return value;
}
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
private static string SerializeColumnValue(object value, Type declaredType, string path)
{
if (value == null)
{
return string.Empty;
}
Type type = Nullable.GetUnderlyingType(declaredType) ?? declaredType;
if (TrySerializeLeafValue(value, type, path, false, out string leafValue))
{
return leafValue;
}
if (type.IsArray)
{
return SerializeJsonValue(value, type, path, 0);
}
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
{
return SerializeEnumerableValue((IEnumerable)value, path, 0);
}
return SerializeJsonValue(value, type, path, 0);
}
private static string SerializeJsonValue(object value, Type declaredType, string path, int depth)
{
if (value == null)
{
return "null";
}
if (depth >= MaxSerializationDepth)
{
return QuoteJson("<max-depth>");
}
Type type = Nullable.GetUnderlyingType(declaredType) ?? declaredType;
if (TrySerializeLeafValue(value, type, path, true, out string leafValue))
{
return leafValue;
}
if (type.IsArray)
{
return SerializeArrayValue((Array)value, path, depth + 1);
}
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
{
return SerializeEnumerableValue((IEnumerable)value, path, depth + 1);
}
return SerializeObjectValue(value, type, path, depth + 1);
}
private static string SerializeObjectValue(object value, Type type, string path, int depth)
{
List<FieldInfo> fields = GetOrderedPublicFields(type);
if (fields.Count == 0)
{
return QuoteJson(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty);
}
var builder = new StringBuilder();
builder.Append('{');
for (int i = 0; i < fields.Count; i++)
{
FieldInfo field = fields[i];
if (i > 0)
{
builder.Append(',');
}
object fieldValue = field.GetValue(value);
string fieldPath = string.IsNullOrEmpty(path) ? field.Name : path + "." + field.Name;
builder.Append(QuoteJson(field.Name));
builder.Append(':');
builder.Append(SerializeJsonValue(fieldValue, field.FieldType, fieldPath, depth));
}
builder.Append('}');
return builder.ToString();
}
private static string SerializeArrayValue(Array array, string path, int depth)
{
if (array == null)
{
return "null";
}
Type elementType = array.GetType().GetElementType() ?? typeof(object);
if (array.Rank == 2)
{
return SerializeMatrixValue(array, elementType, path, depth);
}
var builder = new StringBuilder();
builder.Append('[');
int index = 0;
foreach (object item in array)
{
if (index++ > 0)
{
builder.Append(',');
}
builder.Append(SerializeJsonValue(item, elementType, path + "[]", depth));
}
builder.Append(']');
return builder.ToString();
}
private static string SerializeMatrixValue(Array matrix, Type elementType, string path, int depth)
{
if (elementType == typeof(ushort) && IsTaskCharField(path))
{
return SerializeUnicodeRowArray(ReadUnicodeRows((ushort[,])matrix));
}
if (elementType == typeof(byte) && IsByteTextField(path))
{
return SerializeUnicodeRowArray(ReadUnicodeRows((byte[,])matrix));
}
var builder = new StringBuilder();
builder.Append('[');
int rows = matrix.GetLength(0);
int cols = matrix.GetLength(1);
for (int row = 0; row < rows; row++)
{
if (row > 0)
{
builder.Append(',');
}
builder.Append('[');
for (int col = 0; col < cols; col++)
{
if (col > 0)
{
builder.Append(',');
}
object elementValue = matrix.GetValue(row, col);
builder.Append(SerializeJsonValue(elementValue, elementType, path + "[]", depth));
}
builder.Append(']');
}
builder.Append(']');
return builder.ToString();
}
private static string SerializeEnumerableValue(IEnumerable enumerable, string path, int depth)
{
var builder = new StringBuilder();
builder.Append('[');
bool first = true;
foreach (object item in enumerable)
{
if (!first)
{
builder.Append(',');
}
first = false;
Type itemType = item?.GetType() ?? typeof(object);
builder.Append(SerializeJsonValue(item, itemType, path + "[]", depth));
}
builder.Append(']');
return builder.ToString();
}
private static bool TrySerializeLeafValue(object value, Type type, string path, bool jsonContext, out string serializedValue)
{
if (type == typeof(string))
{
serializedValue = jsonContext
? QuoteJson((string)value)
: (string)value ?? string.Empty;
return true;
}
if (type == typeof(bool))
{
serializedValue = ((bool)value) ? "true" : "false";
return true;
}
if (IsNumericType(type))
{
serializedValue = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
return true;
}
if (type.IsEnum)
{
string enumValue = $"{Convert.ToUInt64(value, CultureInfo.InvariantCulture)}:{value}";
serializedValue = jsonContext ? QuoteJson(enumValue) : enumValue;
return true;
}
if (type == typeof(ushort[]))
{
string text = SerializeUshortArray((ushort[])value, path);
serializedValue = jsonContext && LooksLikeJson(text) ? text : (jsonContext ? QuoteJson(text) : text);
return true;
}
if (type == typeof(byte[]))
{
string text = SerializeByteArray((byte[])value, path);
serializedValue = jsonContext && LooksLikeJson(text) ? text : (jsonContext ? QuoteJson(text) : text);
return true;
}
if (type == typeof(char))
{
string charValue = Convert.ToString(value, CultureInfo.InvariantCulture);
serializedValue = jsonContext ? QuoteJson(charValue) : charValue;
return true;
}
serializedValue = null;
return false;
}
private static string SerializeUshortArray(ushort[] values, string path)
{
if (values == null || values.Length == 0)
{
return string.Empty;
}
if (IsTaskCharField(path))
{
return SerializeUnicodeRowArray(ReadUnicodeRows(values, TaskTemplConstants.TASK_AWARD_MAX_DISPLAY_CHAR_LEN));
}
if (IsLikelyTextField(path))
{
return ByteToStringUtils.UshortArrayToUnicodeString(values);
}
return SerializePrimitiveArray(values);
}
private static string SerializeByteArray(byte[] values, string path)
{
if (values == null || values.Length == 0)
{
return string.Empty;
}
if (IsByteTextField(path))
{
return SerializeUnicodeRowArray(ReadUnicodeRows(values, TaskTemplConstants.TASK_AWARD_MAX_DISPLAY_CHAR_LEN));
}
if (IsLikelyTextField(path))
{
return DecodeByteArrayAsUnicode(values);
}
return SerializePrimitiveArray(values);
}
private static string SerializePrimitiveArray<T>(IEnumerable<T> values)
{
return "[" + string.Join(",", values.Select(SerializePrimitiveElement)) + "]";
}
private static string SerializePrimitiveElement<T>(T value)
{
if (value is null)
{
return "null";
}
Type type = value.GetType();
if (type == typeof(bool))
{
return ((bool)(object)value) ? "true" : "false";
}
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
}
private static List<string> ReadUnicodeRows(ushort[] flatValues, int rowWidth)
{
var rows = new List<string>();
if (flatValues == null || flatValues.Length == 0 || rowWidth <= 0)
{
return rows;
}
int rowCount = flatValues.Length / rowWidth;
for (int row = 0; row < rowCount; row++)
{
var buffer = new ushort[rowWidth];
Array.Copy(flatValues, row * rowWidth, buffer, 0, rowWidth);
rows.Add(ByteToStringUtils.UshortArrayToUnicodeString(buffer));
}
return rows;
}
private static List<string> ReadUnicodeRows(ushort[,] matrix)
{
var rows = new List<string>();
if (matrix == null)
{
return rows;
}
int rowCount = matrix.GetLength(0);
int colCount = matrix.GetLength(1);
for (int row = 0; row < rowCount; row++)
{
var buffer = new ushort[colCount];
for (int col = 0; col < colCount; col++)
{
buffer[col] = matrix[row, col];
}
rows.Add(ByteToStringUtils.UshortArrayToUnicodeString(buffer));
}
return rows;
}
private static List<string> ReadUnicodeRows(byte[] flatValues, int rowWidth)
{
var rows = new List<string>();
if (flatValues == null || flatValues.Length == 0 || rowWidth <= 0)
{
return rows;
}
int rowCount = flatValues.Length / rowWidth;
for (int row = 0; row < rowCount; row++)
{
var buffer = new byte[rowWidth];
Array.Copy(flatValues, row * rowWidth, buffer, 0, rowWidth);
rows.Add(DecodeByteArrayAsUnicode(buffer));
}
return rows;
}
private static List<string> ReadUnicodeRows(byte[,] matrix)
{
var rows = new List<string>();
if (matrix == null)
{
return rows;
}
int rowCount = matrix.GetLength(0);
int colCount = matrix.GetLength(1);
for (int row = 0; row < rowCount; row++)
{
var buffer = new byte[colCount];
for (int col = 0; col < colCount; col++)
{
buffer[col] = matrix[row, col];
}
rows.Add(DecodeByteArrayAsUnicode(buffer));
}
return rows;
}
private static string DecodeByteArrayAsUnicode(byte[] values)
{
if (values == null || values.Length == 0)
{
return string.Empty;
}
var widened = new ushort[values.Length];
for (int i = 0; i < values.Length; i++)
{
widened[i] = values[i];
}
return ByteToStringUtils.UshortArrayToUnicodeString(widened);
}
private static string SerializeUnicodeRowArray(IEnumerable<string> rows)
{
return "[" + string.Join(",", rows.Select(QuoteJson)) + "]";
}
private static string QuoteJson(string value)
{
value ??= string.Empty;
var builder = new StringBuilder(value.Length + 8);
builder.Append('"');
foreach (char ch in value)
{
switch (ch)
{
case '\\':
builder.Append("\\\\");
break;
case '"':
builder.Append("\\\"");
break;
case '\n':
builder.Append("\\n");
break;
case '\r':
builder.Append("\\r");
break;
case '\t':
builder.Append("\\t");
break;
default:
builder.Append(ch);
break;
}
}
builder.Append('"');
return builder.ToString();
}
private static bool LooksLikeJson(string value)
{
if (string.IsNullOrEmpty(value))
{
return false;
}
return value[0] == '[' || value[0] == '{' || value == "null";
}
private static bool IsTaskCharField(string path)
{
return path?.IndexOf("TaskChar", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsByteTextField(string path)
{
return path?.IndexOf("pszExp", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsLikelyTextField(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
string lastSegment = path;
int separatorIndex = path.LastIndexOf('.');
if (separatorIndex >= 0 && separatorIndex < path.Length - 1)
{
lastSegment = path.Substring(separatorIndex + 1);
}
return lastSegment.IndexOf("name", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("desc", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("sign", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("tribute", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("str", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("psz", StringComparison.OrdinalIgnoreCase) >= 0
|| lastSegment.IndexOf("wsz", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsNumericType(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return true;
default:
return false;
}
}
private static List<FieldInfo> GetOrderedPublicFields(Type type)
{
return type
.GetFields(BindingFlags.Instance | BindingFlags.Public)
.OrderBy(field => field.MetadataToken)
.ToList();
}
}
}
#endif