using System; using System.Collections.Generic; 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 { /// /// Handles reading and clearing Unity Editor console log entries. /// Uses reflection to access internal LogEntry methods/properties. /// Supports incremental reading via since_token mechanism. /// public static class ReadConsole { // SessionState keys for persisting token state across domain reloads private const string SessionKeyTokenCounter = "ReadConsole_TokenCounter"; private const string SessionKeySinceToken = "ReadConsole_SinceToken"; private const string SessionKeyClearTimeTicks = "ReadConsole_ClearTimeTicks"; private const string SessionKeyEntryCountAtClear = "ReadConsole_EntryCountAtClear"; private const string SessionKeyTokenMap = "ReadConsole_TokenMap"; // Token generation state - backed by SessionState for persistence across domain reloads private static readonly object _tokenLock = new object(); // Properties that persist via SessionState private static long TokenCounter { get => SessionState.GetInt(SessionKeyTokenCounter, 0); set => SessionState.SetInt(SessionKeyTokenCounter, (int)value); } private static string CurrentSinceToken { get => SessionState.GetString(SessionKeySinceToken, null); set => SessionState.SetString(SessionKeySinceToken, value ?? string.Empty); } private static long ClearTimeTicks { get { var str = SessionState.GetString(SessionKeyClearTimeTicks, "0"); return long.TryParse(str, out var val) ? val : 0; } set => SessionState.SetString(SessionKeyClearTimeTicks, value.ToString()); } private static int EntryCountAtClear { get => SessionState.GetInt(SessionKeyEntryCountAtClear, 0); set => SessionState.SetInt(SessionKeyEntryCountAtClear, value); } // Token entry count map - serialized as JSON in SessionState private static Dictionary GetTokenEntryCountMap() { var json = SessionState.GetString(SessionKeyTokenMap, "{}"); try { var jobj = Codely.Newtonsoft.Json.Linq.JObject.Parse(json); var dict = new Dictionary(); foreach (var prop in jobj.Properties()) { if (int.TryParse(prop.Value.ToString(), out var count)) { dict[prop.Name] = count; } } return dict; } catch { return new Dictionary(); } } private static void SaveTokenEntryCountMap(Dictionary map) { var jobj = new Codely.Newtonsoft.Json.Linq.JObject(); foreach (var kvp in map) { jobj[kvp.Key] = kvp.Value; } SessionState.SetString(SessionKeyTokenMap, jobj.ToString()); } // 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 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? // 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"); 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); if (_instanceIdField == null) throw new Exception("Failed to reflect LogEntry.instanceID"); // (Calibration removed) } catch (Exception e) { Debug.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; _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 || _instanceIdField == null ) { // Log the error here as well for easier debugging in Unity Console Debug.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." ); } string action = @params["action"]?.ToString().ToLower() ?? "get"; try { if (action == "clear") { // Extract scope parameter: "all" (default) or "errors_only" string scope = (@params["scope"]?.ToString() ?? "all").ToLower(); return ClearConsole(scope); } else if (action == "get") { // Extract parameters for 'get' var types = (@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList() ?? new List { "error", "warning", "log" }; int? count = @params["count"]?.ToObject(); string filterText = @params["filterText"]?.ToString(); string sinceToken = @params["since_token"]?.ToString(); // NEW: since_token parameter string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // Legacy: timestamp filtering string format = (@params["format"]?.ToString() ?? "detailed").ToLower(); bool includeStacktrace = @params["includeStacktrace"]?.ToObject() ?? true; if (types.Contains("all")) { types = new List { "error", "warning", "log" }; // Expand 'all' } // Prioritize since_token over sinceTimestamp if (!string.IsNullOrEmpty(sinceToken)) { return GetConsoleEntriesSinceToken(sinceToken, types, count, filterText, format, includeStacktrace); } else if (!string.IsNullOrEmpty(sinceTimestampStr)) { Debug.LogWarning( "[ReadConsole] Filtering by 'since_timestamp' is not currently implemented. Use 'since_token' instead." ); } return GetConsoleEntries(types, count, filterText, format, includeStacktrace); } else { return Response.Error( $"Unknown action: '{action}'. Valid actions are 'get' or 'clear'." ); } } catch (Exception e) { Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}"); return Response.Error($"Internal error processing action '{action}': {e.Message}"); } } // --- Action Implementations --- /// /// Clears the console with optional scope. /// /// "all" clears everything (default), "errors_only" clears only error messages 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). Debug.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 // Generate a new since_token string sinceToken; lock (_tokenLock) { var clearTimeTicks = DateTime.UtcNow.Ticks; ClearTimeTicks = clearTimeTicks; var counter = TokenCounter + 1; TokenCounter = counter; sinceToken = $"{clearTimeTicks}-{counter}"; CurrentSinceToken = sinceToken; // Record that at this token, there were 0 entries (console was just cleared) EntryCountAtClear = 0; // Update the token entry count map var tokenMap = GetTokenEntryCountMap(); tokenMap[sinceToken] = 0; // Clean up old tokens (keep only last 10) if (tokenMap.Count > 10) { var oldTokens = tokenMap.Keys .Where(k => k != sinceToken) .OrderBy(k => k) .Take(tokenMap.Count - 10) .ToList(); foreach (var oldToken in oldTokens) { tokenMap.Remove(oldToken); } } SaveTokenEntryCountMap(tokenMap); } // Update state revision and console state tracking StateComposer.IncrementRevision(); StateComposer.UpdateConsoleState(sinceToken, 0, new object[0]); // Return success with the since_token in data and state_delta 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 { sinceToken = sinceToken, scope = scope }, state_delta = StateComposer.CreateConsoleDelta(sinceToken, 0, new object[0]) }; } catch (Exception e) { Debug.LogError($"[ReadConsole] Failed to clear console: {e}"); return Response.Error($"Failed to clear console: {e.Message}"); } } private static object GetConsoleEntriesSinceToken( string sinceToken, List types, int? count, string filterText, string format, bool includeStacktrace ) { int startIndex = 0; bool tokenValid = false; string currentToken; // Validate the since_token and determine start index lock (_tokenLock) { currentToken = CurrentSinceToken; if (string.IsNullOrEmpty(currentToken)) { return Response.Error( "No valid since_token available. Please call 'clear' first to obtain a since_token.", new { sinceToken = (string)null } ); } if (sinceToken == currentToken) { // Token matches current - read entries after the clear point startIndex = EntryCountAtClear; tokenValid = true; } else { // Check if it's an older but still tracked token var tokenMap = GetTokenEntryCountMap(); if (tokenMap.TryGetValue(sinceToken, out int recordedCount)) { // Token is older but still tracked - use its recorded count startIndex = recordedCount; tokenValid = true; Debug.LogWarning( $"[ReadConsole] Using older token. Provided: {sinceToken}, Current: {currentToken}. " + "Reading from recorded position." ); } else { // Token is unknown/expired - return all entries with warning Debug.LogWarning( $"[ReadConsole] since_token unknown or expired. Provided: {sinceToken}, Current: {currentToken}. " + "Returning all entries. Please clear console and use the new token." ); startIndex = 0; tokenValid = false; } } } // Get entries starting from the determined index var result = GetConsoleEntriesFromIndex(startIndex, types, count, filterText, format, includeStacktrace); // Enhance the response with since_token information if (result is System.Collections.IDictionary dict && dict.Contains("data")) { var dataObj = dict["data"]; int entryCount = 0; if (dataObj is System.Collections.IList list) { entryCount = list.Count; } // Update console state with current unread count var errors = ExtractErrorsFromResult(result); StateComposer.UpdateConsoleState(currentToken, entryCount, errors); // Add metadata about the token var enhancedData = new { entries = dataObj, sinceToken = currentToken, tokenValidated = tokenValid, providedToken = sinceToken, startIndex = startIndex, summary = new { total = entryCount, filtered = !string.IsNullOrEmpty(filterText), newSinceToken = tokenValid } }; return Response.Success( tokenValid ? $"Retrieved {entryCount} new log entries since token." : $"Retrieved {entryCount} log entries (token was invalid/expired).", enhancedData ); } return result; } /// /// Extracts error entries from a result for state tracking. /// private static object[] ExtractErrorsFromResult(object result) { var errors = new List(); if (result is System.Collections.IDictionary dict && dict.Contains("data")) { var dataObj = dict["data"]; if (dataObj is System.Collections.IList list) { foreach (var entry in list) { // Check if entry has type = Error or Exception var entryType = entry?.GetType(); if (entryType != null) { 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(); // Keep only last 10 errors } private static object GetConsoleEntries( List types, int? count, string filterText, string format, bool includeStacktrace ) { return GetConsoleEntriesFromIndex(0, types, count, filterText, format, includeStacktrace); } /// /// Gets console entries starting from a specific index. /// Used for since_token filtering. /// private static object GetConsoleEntriesFromIndex( int startIndex, List types, int? count, string filterText, string format, bool includeStacktrace ) { List formattedEntries = new List(); int retrievedCount = 0; try { // LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal _startGettingEntriesMethod.Invoke(null, null); int totalEntries = (int)_getCountMethod.Invoke(null, null); // Create instance to pass to GetEntryInternal - Ensure the type is correct 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); // Ensure startIndex is valid if (startIndex < 0) startIndex = 0; if (startIndex > totalEntries) startIndex = totalEntries; for (int i = startIndex; i < totalEntries; i++) { // Get the entry data into our instance using reflection _getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); // Extract data using reflection int mode = (int)_modeField.GetValue(logEntryInstance); string message = (string)_messageField.GetValue(logEntryInstance); string file = (string)_fileField.GetValue(logEntryInstance); int line = (int)_lineField.GetValue(logEntryInstance); // int instanceId = (int)_instanceIdField.GetValue(logEntryInstance); if (string.IsNullOrEmpty(message)) { continue; // Skip empty messages } // (Calibration removed) // --- Filtering --- // Prefer classifying severity from message/stacktrace; fallback to mode bits if needed LogType unityType = InferTypeFromMessage(message); bool isExplicitDebug = IsExplicitDebugLog(message); if (!isExplicitDebug && unityType == LogType.Log) { unityType = GetLogTypeFromMode(mode); } bool want; // Treat Exception/Assert as errors for filtering convenience 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; // Filter by text (case-insensitive) if ( !string.IsNullOrEmpty(filterText) && message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0 ) { continue; } // TODO: Filter by timestamp (requires timestamp data) // --- Formatting --- string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null; // Get first line if stack is present and requested, otherwise use full message string messageOnly = (includeStacktrace && !string.IsNullOrEmpty(stackTrace)) ? message.Split( new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries )[0] : message; object formattedEntry = null; switch (format) { case "plain": formattedEntry = messageOnly; break; case "json": case "detailed": // Treat detailed as json for structured return default: formattedEntry = new { type = unityType.ToString(), message = messageOnly, file = file, line = line, // timestamp = "", // TODO stackTrace = stackTrace, // Will be null if includeStacktrace is false or no stack found }; break; } formattedEntries.Add(formattedEntry); retrievedCount++; // Apply count limit (after filtering) if (count.HasValue && retrievedCount >= count.Value) { break; } } } catch (Exception e) { Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}"); // Ensure EndGettingEntries is called even if there's an error during iteration try { _endGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ } return Response.Error($"Error retrieving log entries: {e.Message}"); } finally { // Ensure we always call EndGettingEntries try { _endGettingEntriesMethod.Invoke(null, null); } catch (Exception e) { Debug.LogError($"[ReadConsole] Failed to call EndGettingEntries: {e}"); // Don't return error here as we might have valid data, but log it. } } // Return the filtered and formatted list (might be empty) return Response.Success( $"Retrieved {formattedEntries.Count} log entries.", formattedEntries ); } // --- 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) { // Preserve Unity's real type (no remapping); bits may vary by version 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) /// /// Classifies severity using message/stacktrace content. Works across Unity versions. /// 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; } /// /// Applies the "one level lower" remapping for filtering, like the old version. /// This ensures compatibility with the filtering logic that expects remapped types. /// 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 } } /// /// Attempts to extract the stack trace part from a log message. /// Unity log messages often have the stack trace appended after the main message, /// starting on a new line and typically indented or beginning with "at ". /// /// The complete log message including potential stack trace. /// The extracted stack trace string, or null if none is found. private static string ExtractStackTrace(string fullMessage) { if (string.IsNullOrEmpty(fullMessage)) return null; // Split into lines, removing empty ones to handle different line endings gracefully. // Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here. string[] lines = fullMessage.Split( new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries ); // If there's only one line or less, there's no separate stack trace. if (lines.Length <= 1) return null; int stackStartIndex = -1; // Start checking from the second line onwards. for (int i = 1; i < lines.Length; ++i) { // Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical. string trimmedLine = lines[i].TrimStart(); // Check for common stack trace patterns. if ( trimmedLine.StartsWith("at ") || trimmedLine.StartsWith("UnityEngine.") || trimmedLine.StartsWith("UnityEditor.") || trimmedLine.Contains("(at ") || // Covers "(at Assets/..." pattern // Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something) ( trimmedLine.Length > 0 && char.IsUpper(trimmedLine[0]) && trimmedLine.Contains('.') ) ) { stackStartIndex = i; break; // Found the likely start of the stack trace } } // If a potential start index was found... if (stackStartIndex > 0) { // Join the lines from the stack start index onwards using standard newline characters. // This reconstructs the stack trace part of the message. return string.Join("\n", lines.Skip(stackStartIndex)); } // No clear stack trace found based on the patterns. return null; } /* 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) */ } }