```
feat(gameplay): 更新单位属性和材质配置 - 调整多个单位的基础生命值和攻击力数值平衡 - 修改CoreBattleGround材质的纹理缩放比例 - 更新地砖材质颜色为白色 - 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持 - 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法 - 为水晶矿添加中立阵营设置和光球视觉效果同步 - 调整预制体中的全局缩放参数 - 为狼人和石巨人添加NavMeshAgent组件支持 - 优化水晶矿血条显示位置计算方式 ```
This commit is contained in:
1 parent
0e7de5f885
commit
53d95802e5
448 files changed
+133333
-472
No files matched your search
+831
@@ -0,0 +1,831 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers; // For Response class
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles reading and clearing Unity Editor console log entries.
|
||||
/// Uses reflection to access internal LogEntry methods/properties.
|
||||
/// To read only the output of an operation, clear the console first and read after.
|
||||
/// </summary>
|
||||
public static class ReadConsole
|
||||
{
|
||||
private const int MaxConsoleEntries = 1000;
|
||||
|
||||
// Reflection members for accessing internal LogEntry data
|
||||
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
|
||||
private static MethodInfo _startGettingEntriesMethod;
|
||||
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
|
||||
private static MethodInfo _clearMethod;
|
||||
private static MethodInfo _getCountMethod;
|
||||
private static MethodInfo _getEntryMethod;
|
||||
private static PropertyInfo _consoleFlagsProperty;
|
||||
private static MethodInfo _setConsoleFlagMethod;
|
||||
private static MethodInfo _setFilteringTextMethod;
|
||||
private static MethodInfo _getFilteringTextMethod;
|
||||
private static FieldInfo _modeField;
|
||||
private static FieldInfo _messageField;
|
||||
private static FieldInfo _fileField;
|
||||
private static FieldInfo _lineField;
|
||||
private static FieldInfo _instanceIdField;
|
||||
|
||||
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
|
||||
|
||||
private const int ConsoleFlagCollapse = 1 << 0;
|
||||
private const int ConsoleFlagLogLevelLog = 1 << 7;
|
||||
private const int ConsoleFlagLogLevelWarning = 1 << 8;
|
||||
private const int ConsoleFlagLogLevelError = 1 << 9;
|
||||
|
||||
// Static constructor for reflection setup
|
||||
static ReadConsole()
|
||||
{
|
||||
try
|
||||
{
|
||||
Type logEntriesType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntries"
|
||||
);
|
||||
if (logEntriesType == null)
|
||||
throw new Exception("Could not find internal type UnityEditor.LogEntries");
|
||||
|
||||
|
||||
|
||||
// Include NonPublic binding flags as internal APIs might change accessibility
|
||||
BindingFlags staticFlags =
|
||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
BindingFlags instanceFlags =
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
_startGettingEntriesMethod = logEntriesType.GetMethod(
|
||||
"StartGettingEntries",
|
||||
staticFlags
|
||||
);
|
||||
if (_startGettingEntriesMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
|
||||
|
||||
// Try reflecting EndGettingEntries based on warning message
|
||||
_endGettingEntriesMethod = logEntriesType.GetMethod(
|
||||
"EndGettingEntries",
|
||||
staticFlags
|
||||
);
|
||||
if (_endGettingEntriesMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
|
||||
|
||||
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
|
||||
if (_clearMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.Clear");
|
||||
|
||||
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
|
||||
if (_getCountMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.GetCount");
|
||||
|
||||
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
|
||||
if (_getEntryMethod == null)
|
||||
throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
|
||||
|
||||
// Optional: bypass Console toolbar filters (severity / Collapse / search).
|
||||
_consoleFlagsProperty = logEntriesType.GetProperty("consoleFlags", staticFlags);
|
||||
_setConsoleFlagMethod = logEntriesType.GetMethod(
|
||||
"SetConsoleFlag",
|
||||
staticFlags,
|
||||
null,
|
||||
new[] { typeof(int), typeof(bool) },
|
||||
null
|
||||
);
|
||||
_setFilteringTextMethod = logEntriesType.GetMethod(
|
||||
"SetFilteringText",
|
||||
staticFlags,
|
||||
null,
|
||||
new[] { typeof(string) },
|
||||
null
|
||||
);
|
||||
_getFilteringTextMethod = logEntriesType.GetMethod(
|
||||
"GetFilteringText",
|
||||
staticFlags,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null
|
||||
);
|
||||
|
||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntry"
|
||||
);
|
||||
if (logEntryType == null)
|
||||
throw new Exception("Could not find internal type UnityEditor.LogEntry");
|
||||
|
||||
_modeField = logEntryType.GetField("mode", instanceFlags);
|
||||
if (_modeField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.mode");
|
||||
|
||||
_messageField = logEntryType.GetField("message", instanceFlags);
|
||||
if (_messageField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.message");
|
||||
|
||||
_fileField = logEntryType.GetField("file", instanceFlags);
|
||||
if (_fileField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.file");
|
||||
|
||||
_lineField = logEntryType.GetField("line", instanceFlags);
|
||||
if (_lineField == null)
|
||||
throw new Exception("Failed to reflect LogEntry.line");
|
||||
|
||||
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags)
|
||||
?? logEntryType.GetField("entityId", instanceFlags);
|
||||
// Optional: log entry instance id is not used when reading messages.
|
||||
|
||||
// (Calibration removed)
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CodelyLogger.LogError(
|
||||
$"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}"
|
||||
);
|
||||
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
|
||||
_startGettingEntriesMethod =
|
||||
_endGettingEntriesMethod =
|
||||
_clearMethod =
|
||||
_getCountMethod =
|
||||
_getEntryMethod =
|
||||
null;
|
||||
_consoleFlagsProperty = null;
|
||||
_setConsoleFlagMethod = null;
|
||||
_setFilteringTextMethod = null;
|
||||
_getFilteringTextMethod = null;
|
||||
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Handler ---
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Check if ALL required reflection members were successfully initialized.
|
||||
if (
|
||||
_startGettingEntriesMethod == null
|
||||
|| _endGettingEntriesMethod == null
|
||||
|| _clearMethod == null
|
||||
|| _getCountMethod == null
|
||||
|| _getEntryMethod == null
|
||||
|| _modeField == null
|
||||
|| _messageField == null
|
||||
|| _fileField == null
|
||||
|| _lineField == null
|
||||
)
|
||||
{
|
||||
// Log the error here as well for easier debugging in Unity Console
|
||||
CodelyLogger.LogError(
|
||||
"[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."
|
||||
);
|
||||
return Response.Error(
|
||||
"ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."
|
||||
);
|
||||
}
|
||||
|
||||
return ActionRouter.Route(@params, ActionHandlers, defaultAction: "get");
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Func<JObject, object>> ActionHandlers =
|
||||
new Dictionary<string, Func<JObject, object>>
|
||||
{
|
||||
{ "clear", ClearAction },
|
||||
{ "get", GetAction },
|
||||
};
|
||||
|
||||
private static object ClearAction(JObject @params)
|
||||
{
|
||||
string scope = (@params["scope"]?.ToString() ?? "all").ToLower();
|
||||
return ClearConsole(scope);
|
||||
}
|
||||
|
||||
private static object GetAction(JObject @params)
|
||||
{
|
||||
var types =
|
||||
(@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList()
|
||||
?? new List<string> { "error", "warning", "log" };
|
||||
int? count = @params["count"]?.ToObject<int?>();
|
||||
string filterText = @params["filterText"]?.ToString();
|
||||
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString();
|
||||
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
|
||||
bool includeStacktrace =
|
||||
@params["includeStacktrace"]?.ToObject<bool?>() ?? true;
|
||||
|
||||
if (types.Contains("all"))
|
||||
{
|
||||
types = new List<string> { "error", "warning", "log" };
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sinceTimestampStr))
|
||||
{
|
||||
CodelyLogger.LogWarning(
|
||||
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented."
|
||||
);
|
||||
}
|
||||
|
||||
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the complete post-compile console for <c>start_compilation_pipeline</c> and
|
||||
/// normalizes the public command response into a stable nested payload. Keeping this
|
||||
/// adapter here ensures the standalone console tool and the compilation pipeline use the
|
||||
/// exact same filtering, formatting, truncation and full-log-file behavior.
|
||||
/// </summary>
|
||||
internal static JObject ReadForCompilationPipeline()
|
||||
{
|
||||
object raw = HandleCommand(new JObject
|
||||
{
|
||||
["action"] = "get",
|
||||
["types"] = new JArray("all"),
|
||||
["format"] = "detailed",
|
||||
["includeStacktrace"] = true,
|
||||
});
|
||||
|
||||
JObject response;
|
||||
try
|
||||
{
|
||||
response = raw as JObject ?? JObject.FromObject(raw);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new JObject
|
||||
{
|
||||
["included"] = true,
|
||||
["cleared"] = true,
|
||||
["read_success"] = false,
|
||||
["entries"] = new JArray(),
|
||||
["error"] = $"Failed to serialize console response: {e.Message}",
|
||||
};
|
||||
}
|
||||
|
||||
bool readSuccess = response?["success"]?.ToObject<bool?>() == true;
|
||||
var result = new JObject
|
||||
{
|
||||
["included"] = true,
|
||||
["cleared"] = true,
|
||||
["read_success"] = readSuccess,
|
||||
["entries"] = response?["data"] is JArray entries ? entries : new JArray(),
|
||||
["truncated"] = response?["truncated"]?.ToObject<bool?>() ?? false,
|
||||
};
|
||||
|
||||
CopyIfPresent(response, result, "totalCount");
|
||||
CopyIfPresent(response, result, "returnedCount");
|
||||
CopyIfPresent(response, result, "fullLogFile");
|
||||
|
||||
if (!readSuccess)
|
||||
{
|
||||
result["error"] =
|
||||
response?["message"]?.ToString()
|
||||
?? response?["error"]?.ToString()
|
||||
?? "Failed to read the Unity console after compilation.";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void CopyIfPresent(JObject source, JObject destination, string name)
|
||||
{
|
||||
if (source?[name] != null)
|
||||
destination[name] = source[name].DeepClone();
|
||||
}
|
||||
|
||||
// --- Action Implementations ---
|
||||
|
||||
/// <summary>
|
||||
/// Clears the console with optional scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">"all" clears everything (default), "errors_only" clears only error messages</param>
|
||||
private static object ClearConsole(string scope = "all")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (scope == "errors_only")
|
||||
{
|
||||
// Note: Unity's LogEntries.Clear() clears everything.
|
||||
// For errors_only, we would need to iterate and selectively remove,
|
||||
// which is not directly supported by Unity's API.
|
||||
// For now, we log a warning and clear all (future enhancement could filter).
|
||||
CodelyLogger.LogWarning("[ReadConsole] 'errors_only' scope is not fully supported by Unity's internal API. Clearing all messages.");
|
||||
}
|
||||
|
||||
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
|
||||
|
||||
StateComposer.UpdateConsoleState(0, new object[0]);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = scope == "errors_only"
|
||||
? "Console cleared (errors_only scope limited by Unity API - all messages cleared)."
|
||||
: "Console cleared successfully.",
|
||||
data = new
|
||||
{
|
||||
scope = scope
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CodelyLogger.LogError($"[ReadConsole] Failed to clear console: {e}");
|
||||
return Response.Error($"Failed to clear console: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object[] ExtractErrorsFromList(System.Collections.IList list)
|
||||
{
|
||||
var errors = new List<object>();
|
||||
|
||||
if (list == null) return errors.ToArray();
|
||||
|
||||
foreach (var entry in list)
|
||||
{
|
||||
var entryType = entry?.GetType();
|
||||
if (entryType == null) continue;
|
||||
|
||||
var typeProp = entryType.GetProperty("type");
|
||||
var typeValue = typeProp?.GetValue(entry)?.ToString();
|
||||
if (typeValue == "Error" || typeValue == "Exception")
|
||||
{
|
||||
var messageProp = entryType.GetProperty("message");
|
||||
var fileProp = entryType.GetProperty("file");
|
||||
var lineProp = entryType.GetProperty("line");
|
||||
|
||||
errors.Add(new
|
||||
{
|
||||
message = messageProp?.GetValue(entry)?.ToString(),
|
||||
file = fileProp?.GetValue(entry)?.ToString(),
|
||||
line = lineProp?.GetValue(entry)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Take(10).ToArray();
|
||||
}
|
||||
|
||||
private static object GetConsoleEntries(
|
||||
List<string> types,
|
||||
int? count,
|
||||
string filterText,
|
||||
string format,
|
||||
bool includeStacktrace
|
||||
)
|
||||
{
|
||||
return GetConsoleEntriesFromIndex(0, types, count, filterText, format, includeStacktrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets console entries starting from a specific index.
|
||||
/// </summary>
|
||||
private static object GetConsoleEntriesFromIndex(
|
||||
int startIndex,
|
||||
List<string> types,
|
||||
int? count,
|
||||
string filterText,
|
||||
string format,
|
||||
bool includeStacktrace
|
||||
)
|
||||
{
|
||||
List<object> formattedEntries = new List<object>();
|
||||
|
||||
SafeEndGettingEntries();
|
||||
using (BeginConsoleUiBypass())
|
||||
{
|
||||
try
|
||||
{
|
||||
object started = _startGettingEntriesMethod.Invoke(null, null);
|
||||
int totalEntries = started is int n ? n : (int)_getCountMethod.Invoke(null, null);
|
||||
|
||||
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
|
||||
"UnityEditor.LogEntry"
|
||||
);
|
||||
if (logEntryType == null)
|
||||
throw new Exception(
|
||||
"Could not find internal type UnityEditor.LogEntry during GetConsoleEntries."
|
||||
);
|
||||
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
||||
|
||||
if (startIndex < 0) startIndex = 0;
|
||||
if (startIndex > totalEntries) startIndex = totalEntries;
|
||||
|
||||
for (int i = startIndex; i < totalEntries; i++)
|
||||
{
|
||||
object[] getArgs = { i, logEntryInstance };
|
||||
if (_getEntryMethod.Invoke(null, getArgs) is bool ok && !ok)
|
||||
continue;
|
||||
logEntryInstance = getArgs[1];
|
||||
|
||||
int mode = (int)_modeField.GetValue(logEntryInstance);
|
||||
string message = (string)_messageField.GetValue(logEntryInstance);
|
||||
string file = (string)_fileField.GetValue(logEntryInstance);
|
||||
int line = (int)_lineField.GetValue(logEntryInstance);
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
continue;
|
||||
|
||||
LogType unityType = InferTypeFromMessage(message);
|
||||
bool isExplicitDebug = IsExplicitDebugLog(message);
|
||||
if (!isExplicitDebug && unityType == LogType.Log)
|
||||
unityType = GetLogTypeFromMode(mode);
|
||||
|
||||
bool want;
|
||||
if (unityType == LogType.Exception)
|
||||
want = types.Contains("error") || types.Contains("exception");
|
||||
else if (unityType == LogType.Assert)
|
||||
want = types.Contains("error") || types.Contains("assert");
|
||||
else
|
||||
want = types.Contains(unityType.ToString().ToLowerInvariant());
|
||||
|
||||
if (!want) continue;
|
||||
|
||||
if (
|
||||
!string.IsNullOrEmpty(filterText)
|
||||
&& message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0
|
||||
)
|
||||
continue;
|
||||
|
||||
// LogEntry.message embeds the callstack in the text itself, so the
|
||||
// stack must always be split off; includeStacktrace only decides
|
||||
// whether the split-off part is returned in the stackTrace field.
|
||||
// IMPORTANT: the message body may itself be multi-line — take every
|
||||
// line before the stack start, not only the first line.
|
||||
SplitMessageAndStack(message, out string messageOnly, out string embeddedStack);
|
||||
string stackTrace = includeStacktrace ? embeddedStack : null;
|
||||
|
||||
object formattedEntry;
|
||||
switch (format)
|
||||
{
|
||||
case "plain":
|
||||
formattedEntry = messageOnly;
|
||||
break;
|
||||
case "json":
|
||||
case "detailed":
|
||||
default:
|
||||
formattedEntry = new
|
||||
{
|
||||
type = unityType.ToString(),
|
||||
message = messageOnly,
|
||||
file = file,
|
||||
line = line,
|
||||
stackTrace = stackTrace,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
formattedEntries.Add(formattedEntry);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CodelyLogger.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
|
||||
SafeEndGettingEntries();
|
||||
return Response.Error($"Error retrieving log entries: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SafeEndGettingEntries();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the composed editor state's console section fresh on every read.
|
||||
StateComposer.UpdateConsoleState(formattedEntries.Count, ExtractErrorsFromList(formattedEntries));
|
||||
|
||||
int totalCount = formattedEntries.Count;
|
||||
int returnLimit = count.HasValue ? Math.Min(count.Value, MaxConsoleEntries) : MaxConsoleEntries;
|
||||
|
||||
if (totalCount > returnLimit)
|
||||
{
|
||||
string tempFilePath = WriteFullLogToTempFile(formattedEntries);
|
||||
var truncatedEntries = formattedEntries.Take(returnLimit).ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Console has {totalCount} entries, showing first {returnLimit}. Full log saved to temp file.",
|
||||
data = truncatedEntries,
|
||||
truncated = true,
|
||||
totalCount = totalCount,
|
||||
returnedCount = truncatedEntries.Count,
|
||||
fullLogFile = tempFilePath,
|
||||
hint = $"To read the complete log, use the file reading tool on: {tempFilePath}"
|
||||
};
|
||||
}
|
||||
|
||||
var finalEntries = count.HasValue ? formattedEntries.Take(count.Value).ToList() : formattedEntries;
|
||||
return Response.Success(
|
||||
$"Retrieved {finalEntries.Count} log entries.",
|
||||
finalEntries
|
||||
);
|
||||
}
|
||||
|
||||
private sealed class ConsoleUiBypassRestore : IDisposable
|
||||
{
|
||||
private readonly int _savedFlags;
|
||||
private readonly string _savedFilter;
|
||||
|
||||
public ConsoleUiBypassRestore(int savedFlags, string savedFilter)
|
||||
{
|
||||
_savedFlags = savedFlags;
|
||||
_savedFilter = savedFilter;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SafeEndGettingEntries();
|
||||
if (_setFilteringTextMethod != null)
|
||||
{
|
||||
try { _setFilteringTextMethod.Invoke(null, new object[] { _savedFilter ?? string.Empty }); }
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
if (_consoleFlagsProperty != null)
|
||||
{
|
||||
try { _consoleFlagsProperty.SetValue(null, _savedFlags, null); }
|
||||
catch (Exception e)
|
||||
{
|
||||
CodelyLogger.LogError($"[ReadConsole] Failed to restore consoleFlags: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadConsoleFlags()
|
||||
{
|
||||
if (_consoleFlagsProperty == null) return 0;
|
||||
try { return (int)_consoleFlagsProperty.GetValue(null, null); }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static ConsoleUiBypassRestore BeginConsoleUiBypass()
|
||||
{
|
||||
int savedFlags = ReadConsoleFlags();
|
||||
string savedFilter = _getFilteringTextMethod?.Invoke(null, null) as string;
|
||||
if (_setConsoleFlagMethod == null)
|
||||
return new ConsoleUiBypassRestore(savedFlags, savedFilter);
|
||||
|
||||
SafeEndGettingEntries();
|
||||
_setConsoleFlagMethod.Invoke(null, new object[] { ConsoleFlagCollapse, false });
|
||||
_setConsoleFlagMethod.Invoke(null, new object[] { ConsoleFlagLogLevelLog, true });
|
||||
_setConsoleFlagMethod.Invoke(null, new object[] { ConsoleFlagLogLevelWarning, true });
|
||||
_setConsoleFlagMethod.Invoke(null, new object[] { ConsoleFlagLogLevelError, true });
|
||||
if (_setFilteringTextMethod != null)
|
||||
_setFilteringTextMethod.Invoke(null, new object[] { string.Empty });
|
||||
|
||||
return new ConsoleUiBypassRestore(savedFlags, savedFilter);
|
||||
}
|
||||
|
||||
private static void SafeEndGettingEntries()
|
||||
{
|
||||
if (_endGettingEntriesMethod == null) return;
|
||||
try { _endGettingEntriesMethod.Invoke(null, null); }
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
// Mapping bits from LogEntry.mode. These may vary by Unity version.
|
||||
private const int ModeBitError = 1 << 0;
|
||||
private const int ModeBitAssert = 1 << 1;
|
||||
private const int ModeBitWarning = 1 << 2;
|
||||
private const int ModeBitLog = 1 << 3;
|
||||
private const int ModeBitException = 1 << 4; // often combined with Error bits
|
||||
private const int ModeBitScriptingError = 1 << 9;
|
||||
private const int ModeBitScriptingWarning = 1 << 10;
|
||||
private const int ModeBitScriptingLog = 1 << 11;
|
||||
private const int ModeBitScriptingException = 1 << 18;
|
||||
private const int ModeBitScriptingAssertion = 1 << 22;
|
||||
|
||||
private static LogType GetLogTypeFromMode(int mode)
|
||||
{
|
||||
if ((mode & (ModeBitException | ModeBitScriptingException)) != 0) return LogType.Exception;
|
||||
if ((mode & (ModeBitError | ModeBitScriptingError)) != 0) return LogType.Error;
|
||||
if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) return LogType.Assert;
|
||||
if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) return LogType.Warning;
|
||||
return LogType.Log;
|
||||
}
|
||||
|
||||
// (Calibration helpers removed)
|
||||
|
||||
/// <summary>
|
||||
/// Classifies severity using message/stacktrace content. Works across Unity versions.
|
||||
/// </summary>
|
||||
private static LogType InferTypeFromMessage(string fullMessage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullMessage)) return LogType.Log;
|
||||
|
||||
// Fast path: look for explicit Debug API names in the appended stack trace
|
||||
// e.g., "UnityEngine.Debug:LogError (object)" or "LogWarning"
|
||||
if (fullMessage.IndexOf("LogError", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Error;
|
||||
if (fullMessage.IndexOf("LogWarning", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Warning;
|
||||
|
||||
// Compiler diagnostics (C#): "warning CSxxxx" / "error CSxxxx"
|
||||
if (fullMessage.IndexOf(" warning CS", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| fullMessage.IndexOf(": warning CS", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Warning;
|
||||
if (fullMessage.IndexOf(" error CS", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| fullMessage.IndexOf(": error CS", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Error;
|
||||
|
||||
// Exceptions (avoid misclassifying compiler diagnostics)
|
||||
if (fullMessage.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Exception;
|
||||
|
||||
// Unity assertions
|
||||
if (fullMessage.IndexOf("Assertion", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return LogType.Assert;
|
||||
|
||||
return LogType.Log;
|
||||
}
|
||||
|
||||
private static bool IsExplicitDebugLog(string fullMessage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullMessage)) return false;
|
||||
if (fullMessage.IndexOf("Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (fullMessage.IndexOf("UnityEngine.Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the "one level lower" remapping for filtering, like the old version.
|
||||
/// This ensures compatibility with the filtering logic that expects remapped types.
|
||||
/// </summary>
|
||||
private static LogType GetRemappedTypeForFiltering(LogType unityType)
|
||||
{
|
||||
switch (unityType)
|
||||
{
|
||||
case LogType.Error:
|
||||
return LogType.Warning; // Error becomes Warning
|
||||
case LogType.Warning:
|
||||
return LogType.Log; // Warning becomes Log
|
||||
case LogType.Assert:
|
||||
return LogType.Assert; // Assert remains Assert
|
||||
case LogType.Log:
|
||||
return LogType.Log; // Log remains Log
|
||||
case LogType.Exception:
|
||||
return LogType.Warning; // Exception becomes Warning
|
||||
default:
|
||||
return LogType.Log; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
private static string WriteFullLogToTempFile(List<object> entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
string tempDir = Path.Combine(Application.dataPath, "..", "Temp");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
string fileName = $"unity_console_{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid().ToString("N").Substring(0, 8)}.json";
|
||||
string filePath = Path.GetFullPath(Path.Combine(tempDir, fileName));
|
||||
|
||||
var jArray = new JArray();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry is string s)
|
||||
{
|
||||
jArray.Add(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
jArray.Add(JObject.FromObject(entry));
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(filePath, jArray.ToString(Codely.Newtonsoft.Json.Formatting.Indented), System.Text.Encoding.UTF8);
|
||||
return filePath;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CodelyLogger.LogError($"[ReadConsole] Failed to write full log to temp file: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a Unity LogEntry.message into the log body (which may itself be multi-line)
|
||||
/// and the appended callstack. Previously only the first line was kept as the message,
|
||||
/// which truncated multi-line log bodies whenever a stack was present.
|
||||
/// </summary>
|
||||
internal static void SplitMessageAndStack(
|
||||
string fullMessage,
|
||||
out string messageBody,
|
||||
out string stackTrace
|
||||
)
|
||||
{
|
||||
messageBody = fullMessage ?? "";
|
||||
stackTrace = null;
|
||||
if (string.IsNullOrEmpty(fullMessage))
|
||||
return;
|
||||
|
||||
string[] lines = fullMessage.Split(
|
||||
new[] { '\r', '\n' },
|
||||
StringSplitOptions.None
|
||||
);
|
||||
|
||||
// Drop trailing empty lines from the split, but keep internal blank lines in the body.
|
||||
int lastNonEmpty = lines.Length - 1;
|
||||
while (lastNonEmpty >= 0 && string.IsNullOrEmpty(lines[lastNonEmpty]))
|
||||
lastNonEmpty--;
|
||||
if (lastNonEmpty < 0)
|
||||
{
|
||||
messageBody = "";
|
||||
return;
|
||||
}
|
||||
|
||||
int stackStartIndex = -1;
|
||||
for (int i = 1; i <= lastNonEmpty; ++i)
|
||||
{
|
||||
string trimmedLine = lines[i].TrimStart();
|
||||
if (
|
||||
trimmedLine.StartsWith("at ")
|
||||
|| trimmedLine.StartsWith("UnityEngine.")
|
||||
|| trimmedLine.StartsWith("UnityEditor.")
|
||||
|| trimmedLine.Contains("(at ")
|
||||
|| (
|
||||
trimmedLine.Length > 0
|
||||
&& char.IsUpper(trimmedLine[0])
|
||||
&& trimmedLine.Contains(".")
|
||||
)
|
||||
)
|
||||
{
|
||||
stackStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stackStartIndex > 0)
|
||||
{
|
||||
messageBody = string.Join(
|
||||
"\n",
|
||||
lines,
|
||||
0,
|
||||
stackStartIndex
|
||||
).TrimEnd('\r', '\n');
|
||||
stackTrace = string.Join(
|
||||
"\n",
|
||||
lines,
|
||||
stackStartIndex,
|
||||
lastNonEmpty - stackStartIndex + 1
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
messageBody = string.Join("\n", lines, 0, lastNonEmpty + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to extract the stack trace part from a log message.
|
||||
/// Prefer <see cref="SplitMessageAndStack"/> when you also need the message body.
|
||||
/// </summary>
|
||||
private static string ExtractStackTrace(string fullMessage)
|
||||
{
|
||||
SplitMessageAndStack(fullMessage, out _, out string stackTrace);
|
||||
return stackTrace;
|
||||
}
|
||||
|
||||
/* LogEntry.mode bits exploration (based on Unity decompilation/observation):
|
||||
May change between versions.
|
||||
|
||||
Basic Types:
|
||||
kError = 1 << 0 (1)
|
||||
kAssert = 1 << 1 (2)
|
||||
kWarning = 1 << 2 (4)
|
||||
kLog = 1 << 3 (8)
|
||||
kFatal = 1 << 4 (16) - Often treated as Exception/Error
|
||||
|
||||
Modifiers/Context:
|
||||
kAssetImportError = 1 << 7 (128)
|
||||
kAssetImportWarning = 1 << 8 (256)
|
||||
kScriptingError = 1 << 9 (512)
|
||||
kScriptingWarning = 1 << 10 (1024)
|
||||
kScriptingLog = 1 << 11 (2048)
|
||||
kScriptCompileError = 1 << 12 (4096)
|
||||
kScriptCompileWarning = 1 << 13 (8192)
|
||||
kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play
|
||||
kMayIgnoreLineNumber = 1 << 15 (32768)
|
||||
kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button
|
||||
kDisplayPreviousErrorInStatusBar = 1 << 17 (131072)
|
||||
kScriptingException = 1 << 18 (262144)
|
||||
kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI
|
||||
kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior
|
||||
kGraphCompileError = 1 << 21 (2097152)
|
||||
kScriptingAssertion = 1 << 22 (4194304)
|
||||
kVisualScriptingError = 1 << 23 (8388608)
|
||||
|
||||
Example observed values:
|
||||
Log: 2048 (ScriptingLog) or 8 (Log)
|
||||
Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning)
|
||||
Error: 513 (ScriptingError | Error) or 1 (Error)
|
||||
Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination
|
||||
Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user