#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 allTemplates = GetAllTaskTemplates(container); if (allTemplates.Count == 0) { Debug.LogError("[TaskCsvExporter] No task templates were found to export."); return; } PrepareTemplatesForExport(container, allTemplates); List orderedTasks = BuildOrderedTaskList(container, allTemplates); List fixedDataFields = GetOrderedPublicFields(typeof(ATaskTemplFixedData)); List> 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(); temporaryContainer.LoadAllTasksFromPack(); destroyTemporaryContainer = true; return temporaryContainer; } private static TaskTemplContainerSO LoadExistingTaskContainer() { TaskTemplContainerSO container = AssetDatabase.LoadAssetAtPath(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(assetPath); if (container != null) { return container; } } return null; } private static bool HasTaskData(TaskTemplContainerSO container) { return container != null && GetAllTaskTemplates(container).Count > 0; } private static List GetAllTaskTemplates(TaskTemplContainerSO container) { if (container == null || AllTaskTemplatesField == null) { return new List(); } List templates = AllTaskTemplatesField.GetValue(container) as List; return templates?.Where(template => template != null).ToList() ?? new List(); } private static void PrepareTemplatesForExport(TaskTemplContainerSO container, IEnumerable templates) { foreach (ATaskTempl template in templates) { template.FillUnserializableDataWhenPlayGame(container); } } private static List BuildOrderedTaskList(TaskTemplContainerSO container, IReadOnlyCollection allTemplates) { var ordered = new List(allTemplates.Count); var visitedIds = new HashSet(); 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 ordered, ISet 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> BuildRows(IEnumerable tasks, IReadOnlyList fixedDataFields) { var rows = new List>(); foreach (ATaskTempl task in tasks) { var row = new Dictionary(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> 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> rows, IReadOnlyList fixedDataFields, string assetPath) { string absolutePath = GetAbsoluteProjectPath(assetPath); string directory = Path.GetDirectoryName(absolutePath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } var headers = new List(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 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(""); } 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 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(IEnumerable values) { return "[" + string.Join(",", values.Select(SerializePrimitiveElement)) + "]"; } private static string SerializePrimitiveElement(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 ReadUnicodeRows(ushort[] flatValues, int rowWidth) { var rows = new List(); 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 ReadUnicodeRows(ushort[,] matrix) { var rows = new List(); 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 ReadUnicodeRows(byte[] flatValues, int rowWidth) { var rows = new List(); 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 ReadUnicodeRows(byte[,] matrix) { var rows = new List(); 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 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 GetOrderedPublicFields(Type type) { return type .GetFields(BindingFlags.Instance | BindingFlags.Public) .OrderBy(field => field.MetadataToken) .ToList(); } } } #endif