using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER using UnityEngine.InputSystem; #endif #if UNITY_EDITOR && UNITY_2021_1_OR_NEWER using Screen = UnityEngine.Device.Screen; // To support Device Simulator on Unity 2021.1+ #endif // Receives debug entries and custom events (e.g. Clear, Collapse, Filter by Type) // and notifies the recycled list view of changes to the list of debug entries // // - Vocabulary - // Debug/Log entry: a Debug.Log/LogError/LogWarning/LogException/LogAssertion request made by // the client and intercepted by this manager object // Debug/Log item: a visual (uGUI) representation of a debug entry // // There can be a lot of debug entries in the system but there will only be a handful of log items // to show their properties on screen (these log items are recycled as the list is scrolled) // An enum to represent filtered log types namespace IngameDebugConsole { public enum DebugLogFilter { None = 0, Info = 1, Warning = 2, Error = 4, All = ~0 } public enum PopupVisibility { Always = 0, WhenLogReceived = 1, Never = 2 } public class DebugLogManager : MonoBehaviour { public static DebugLogManager Instance { get; private set; } #pragma warning disable 0649 [Header( "Properties" )] [SerializeField] [HideInInspector] [Tooltip( "If enabled, console window will persist between scenes (i.e. not be destroyed when scene changes)" )] private bool singleton = true; [SerializeField] [HideInInspector] [Tooltip( "Minimum height of the console window" )] private float minimumHeight = 200f; [SerializeField] [HideInInspector] [Tooltip( "If enabled, console window can be resized horizontally, as well" )] private bool enableHorizontalResizing = false; [SerializeField] [HideInInspector] [Tooltip( "If enabled, console window's resize button will be located at bottom-right corner. Otherwise, it will be located at bottom-left corner" )] private bool resizeFromRight = true; [SerializeField] [HideInInspector] [Tooltip( "Minimum width of the console window" )] private float minimumWidth = 240f; [SerializeField] [HideInInspector] [Tooltip( "Opacity of the console window" )] [Range( 0f, 1f )] private float logWindowOpacity = 1f; [SerializeField] [HideInInspector] [Tooltip( "Opacity of the popup" )] [Range( 0f, 1f )] internal float popupOpacity = 1f; [SerializeField] [HideInInspector] [Tooltip( "Determines when the popup will show up (after the console window is closed)" )] private PopupVisibility popupVisibility = PopupVisibility.Always; [SerializeField] [HideInInspector] [Tooltip( "Determines which log types will show the popup on screen" )] private DebugLogFilter popupVisibilityLogFilter = DebugLogFilter.All; [SerializeField] [HideInInspector] [Tooltip( "If enabled, console window will initially be invisible" )] private bool startMinimized = false; [SerializeField] [HideInInspector] [Tooltip( "If enabled, pressing the Toggle Key will show/hide (i.e. toggle) the console window at runtime" )] private bool toggleWithKey = false; #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER [SerializeField] [HideInInspector] public InputAction toggleBinding = new InputAction( "Toggle Binding", type: InputActionType.Button, binding: "/backquote", expectedControlType: "Button" ); #else [SerializeField] [HideInInspector] private KeyCode toggleKey = KeyCode.BackQuote; #endif [SerializeField] [HideInInspector] [Tooltip( "If enabled, the console window will have a searchbar" )] private bool enableSearchbar = true; [SerializeField] [HideInInspector] [Tooltip( "Width of the canvas determines whether the searchbar will be located inside the menu bar or underneath the menu bar. This way, the menu bar doesn't get too crowded on narrow screens. This value determines the minimum width of the canvas for the searchbar to appear inside the menu bar" )] private float topSearchbarMinWidth = 360f; [SerializeField] [HideInInspector] [Tooltip( "If enabled, the console window will continue receiving logs in the background even if its GameObject is inactive. But the console window's GameObject needs to be activated at least once because its Awake function must be triggered for this to work" )] private bool receiveLogsWhileInactive = false; [SerializeField] [HideInInspector] private bool receiveInfoLogs = true, receiveWarningLogs = true, receiveErrorLogs = true, receiveExceptionLogs = true; [SerializeField] [HideInInspector] [Tooltip( "If enabled, the arrival times of logs will be recorded and displayed when a log is expanded" )] private bool captureLogTimestamps = false; [SerializeField] [HideInInspector] [Tooltip( "If enabled, timestamps will be displayed for logs even if they aren't expanded" )] internal bool alwaysDisplayTimestamps = false; [SerializeField] [HideInInspector] [Tooltip( "If the number of logs reach this limit, the oldest log(s) will be deleted to limit the RAM usage. It's recommended to set this value as low as possible" )] private int maxLogCount = int.MaxValue; [SerializeField] [HideInInspector] [Tooltip( "How many log(s) to delete when the threshold is reached (all logs are iterated during this operation so it should neither be too low nor too high)" )] private int logsToRemoveAfterMaxLogCount = 16; [SerializeField] [HideInInspector] [Tooltip( "While the console window is hidden, incoming logs will be queued but not immediately processed until the console window is opened (to avoid wasting CPU resources). When the log queue exceeds this limit, the first logs in the queue will be processed to enforce this limit. Processed logs won't increase RAM usage if they've been seen before (i.e. collapsible logs) but this is not the case for queued logs, so if a log is spammed every frame, it will fill the whole queue in an instant. Which is why there is a queue limit" )] private int queuedLogLimit = 256; [SerializeField] [HideInInspector] [Tooltip( "If enabled, the command input field at the bottom of the console window will automatically be cleared after entering a command" )] private bool clearCommandAfterExecution = true; [SerializeField] [HideInInspector] [Tooltip( "Console keeps track of the previously entered commands. This value determines the capacity of the command history (you can scroll through the history via up and down arrow keys while the command input field is focused)" )] private int commandHistorySize = 15; [SerializeField] [HideInInspector] [Tooltip( "If enabled, while typing a command, all of the matching commands' signatures will be displayed in a popup" )] private bool showCommandSuggestions = true; [SerializeField] [HideInInspector] [Tooltip( "If enabled, on Android platform, logcat entries of the application will also be logged to the console with the prefix \"LOGCAT: \". This may come in handy especially if you want to access the native logs of your Android plugins (like Admob)" )] private bool receiveLogcatLogsInAndroid = false; #pragma warning disable 0414 #if UNITY_2018_3_OR_NEWER // On older Unity versions, disabling CS0169 is problematic: "Cannot restore warning 'CS0169' because it was disabled globally" #pragma warning disable 0169 #endif [SerializeField] [HideInInspector] [Tooltip( "Native logs will be filtered using these arguments. If left blank, all native logs of the application will be logged to the console. But if you want to e.g. see Admob's logs only, you can enter \"-s Ads\" (without quotes) here" )] private string logcatArguments; #if UNITY_2018_3_OR_NEWER #pragma warning restore 0169 #endif #pragma warning restore 0414 [SerializeField] [HideInInspector] [Tooltip( "If enabled, on Android and iOS devices with notch screens, the console window will be repositioned so that the cutout(s) don't obscure it" )] private bool avoidScreenCutout = true; [SerializeField] [HideInInspector] [Tooltip( "If enabled, on Android and iOS devices with notch screens, the console window's popup won't be obscured by the screen cutouts" )] internal bool popupAvoidsScreenCutout = false; [SerializeField] [Tooltip( "If a log is longer than this limit, it will be truncated. This helps avoid reaching Unity's 65000 vertex limit for UI canvases" )] private int maxLogLength = 10000; #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL [SerializeField] [HideInInspector] [Tooltip( "If enabled, on standalone platforms, command input field will automatically be focused (start receiving keyboard input) after opening the console window" )] private bool autoFocusOnCommandInputField = true; #endif [Header( "Visuals" )] [SerializeField] private DebugLogItem logItemPrefab; [SerializeField] private Text commandSuggestionPrefab; // Visuals for different log types [SerializeField] private Sprite infoLog; [SerializeField] private Sprite warningLog; [SerializeField] private Sprite errorLog; private Sprite[] logSpriteRepresentations; // Visuals for resize button [SerializeField] private Sprite resizeIconAllDirections; [SerializeField] private Sprite resizeIconVerticalOnly; [SerializeField] private Color collapseButtonNormalColor; [SerializeField] private Color collapseButtonSelectedColor; [SerializeField] private Color filterButtonsNormalColor; [SerializeField] private Color filterButtonsSelectedColor; [SerializeField] private string commandSuggestionHighlightStart = ""; [SerializeField] private string commandSuggestionHighlightEnd = ""; [Header( "Internal References" )] [SerializeField] private RectTransform logWindowTR; internal RectTransform canvasTR; [SerializeField] private RectTransform logItemsContainer; [SerializeField] private RectTransform commandSuggestionsContainer; [SerializeField] private InputField commandInputField; [SerializeField] private Button hideButton; [SerializeField] private Button clearButton; [SerializeField] private Image collapseButton; [SerializeField] private Image filterInfoButton; [SerializeField] private Image filterWarningButton; [SerializeField] private Image filterErrorButton; [SerializeField] private Text infoEntryCountText; [SerializeField] private Text warningEntryCountText; [SerializeField] private Text errorEntryCountText; [SerializeField] private RectTransform searchbar; [SerializeField] private RectTransform searchbarSlotTop; [SerializeField] private RectTransform searchbarSlotBottom; [SerializeField] private Image resizeButton; [SerializeField] private GameObject snapToBottomButton; // Canvas group to modify visibility of the log window [SerializeField] private CanvasGroup logWindowCanvasGroup; [SerializeField] private DebugLogPopup popupManager; [SerializeField] private ScrollRect logItemsScrollRect; private RectTransform logItemsScrollRectTR; private Vector2 logItemsScrollRectOriginalSize; // Recycled list view to handle the log items efficiently [SerializeField] private DebugLogRecycledListView recycledListView; #pragma warning restore 0649 private bool isLogWindowVisible = true; public bool IsLogWindowVisible { get { return isLogWindowVisible; } } public bool PopupEnabled { get { return popupManager.gameObject.activeSelf; } set { popupManager.gameObject.SetActive( value ); } } private bool screenDimensionsChanged = true; private float logWindowPreviousWidth; // Number of entries filtered by their types private int infoEntryCount = 0, warningEntryCount = 0, errorEntryCount = 0; private bool entryCountTextsDirty; // Number of new entries received this frame private int newInfoEntryCount = 0, newWarningEntryCount = 0, newErrorEntryCount = 0; // Filters to apply to the list of debug entries to show private bool isCollapseOn = false; private DebugLogFilter logFilter = DebugLogFilter.All; // Search filter private string searchTerm; private bool isInSearchMode; // If the last log item is completely visible (scrollbar is at the bottom), // scrollbar will remain at the bottom when new debug entries are received [System.NonSerialized] public bool SnapToBottom = true; // List of unique debug entries (duplicates of entries are not kept) private DynamicCircularBuffer collapsedLogEntries; private DynamicCircularBuffer collapsedLogEntriesTimestamps; // Dictionary to quickly find if a log already exists in collapsedLogEntries private Dictionary collapsedLogEntriesMap; // The order the collapsedLogEntries are received // (duplicate entries have the same value) private DynamicCircularBuffer uncollapsedLogEntries; private DynamicCircularBuffer uncollapsedLogEntriesTimestamps; // Filtered list of debug entries to show private DynamicCircularBuffer logEntriesToShow; private DynamicCircularBuffer timestampsOfLogEntriesToShow; // The log entry that must be focused this frame private int indexOfLogEntryToSelectAndFocus = -1; // Whether or not logs list view should be updated this frame private bool shouldUpdateRecycledListView = true; // Logs that should be registered in Update-loop private DynamicCircularBuffer queuedLogEntries; private DynamicCircularBuffer queuedLogEntriesTimestamps; private object logEntriesLock; private int pendingLogToAutoExpand; // Command suggestions that match the currently entered command private List commandSuggestionInstances; private int visibleCommandSuggestionInstances = 0; private List matchingCommandSuggestions; private List commandCaretIndexIncrements; private string commandInputFieldPrevCommand; private string commandInputFieldPrevCommandName; private int commandInputFieldPrevParamCount = -1; private int commandInputFieldPrevCaretPos = -1; private int commandInputFieldPrevCaretArgumentIndex = -1; // Value of the command input field when autocomplete was first requested private string commandInputFieldAutoCompleteBase; private bool commandInputFieldAutoCompletedNow; // Pools for memory efficiency private Stack pooledLogEntries; private Stack pooledLogItems; /// Variables used by private bool anyCollapsedLogRemoved; private int removedLogEntriesToShowCount; // History of the previously entered commands private CircularBuffer commandHistory; private int commandHistoryIndex = -1; private string unfinishedCommand; // StringBuilder used by various functions internal StringBuilder sharedStringBuilder; // Offset of DateTime.Now from DateTime.UtcNow private System.TimeSpan localTimeUtcOffset; // Last recorded values of Time.realtimeSinceStartup and Time.frameCount on the main thread (because these Time properties can't be accessed from other threads) #if !IDG_OMIT_ELAPSED_TIME private float lastElapsedSeconds; #endif #if !IDG_OMIT_FRAMECOUNT private int lastFrameCount; #endif private DebugLogEntryTimestamp dummyLogEntryTimestamp; // Required in ValidateScrollPosition() function private PointerEventData nullPointerEventData; private System.Action poolLogEntryAction; private System.Action removeUncollapsedLogEntryAction; private System.Predicate shouldRemoveCollapsedLogEntryPredicate; private System.Predicate shouldRemoveLogEntryToShowPredicate; private System.Action updateLogEntryCollapsedIndexAction; // Callbacks for log window show/hide events public System.Action OnLogWindowShown, OnLogWindowHidden; #if UNITY_EDITOR private bool isQuittingApplication; #endif #if !UNITY_EDITOR && UNITY_ANDROID private DebugLogLogcatListener logcatListener; #endif private void Awake() { // Only one instance of debug console is allowed if( !Instance ) { Instance = this; // If it is a singleton object, don't destroy it between scene changes if( singleton ) DontDestroyOnLoad( gameObject ); } else if( Instance != this ) { Destroy( gameObject ); return; } pooledLogEntries = new Stack( 64 ); pooledLogItems = new Stack( 16 ); commandSuggestionInstances = new List( 8 ); matchingCommandSuggestions = new List( 8 ); commandCaretIndexIncrements = new List( 8 ); queuedLogEntries = new DynamicCircularBuffer( Mathf.Clamp( queuedLogLimit, 16, 4096 ) ); commandHistory = new CircularBuffer( commandHistorySize ); logEntriesLock = new object(); sharedStringBuilder = new StringBuilder( 1024 ); canvasTR = (RectTransform) transform; logItemsScrollRectTR = (RectTransform) logItemsScrollRect.transform; logItemsScrollRectOriginalSize = logItemsScrollRectTR.sizeDelta; // Associate sprites with log types logSpriteRepresentations = new Sprite[5]; logSpriteRepresentations[(int) LogType.Log] = infoLog; logSpriteRepresentations[(int) LogType.Warning] = warningLog; logSpriteRepresentations[(int) LogType.Error] = errorLog; logSpriteRepresentations[(int) LogType.Exception] = errorLog; logSpriteRepresentations[(int) LogType.Assert] = errorLog; // Initially, all log types are visible filterInfoButton.color = filterButtonsSelectedColor; filterWarningButton.color = filterButtonsSelectedColor; filterErrorButton.color = filterButtonsSelectedColor; resizeButton.sprite = enableHorizontalResizing ? resizeIconAllDirections : resizeIconVerticalOnly; collapsedLogEntries = new DynamicCircularBuffer( 128 ); collapsedLogEntriesMap = new Dictionary( 128, new DebugLogEntryContentEqualityComparer() ); uncollapsedLogEntries = new DynamicCircularBuffer( 256 ); logEntriesToShow = new DynamicCircularBuffer( 256 ); if( captureLogTimestamps ) { collapsedLogEntriesTimestamps = new DynamicCircularBuffer( 128 ); uncollapsedLogEntriesTimestamps = new DynamicCircularBuffer( 256 ); timestampsOfLogEntriesToShow = new DynamicCircularBuffer( 256 ); queuedLogEntriesTimestamps = new DynamicCircularBuffer( queuedLogEntries.Capacity ); } recycledListView.Initialize( this, logEntriesToShow, timestampsOfLogEntriesToShow, logItemPrefab.Transform.sizeDelta.y ); if( minimumWidth < 100f ) minimumWidth = 100f; if( minimumHeight < 200f ) minimumHeight = 200f; if( !resizeFromRight ) { RectTransform resizeButtonTR = (RectTransform) resizeButton.GetComponentInParent().transform; resizeButtonTR.anchorMin = new Vector2( 0f, resizeButtonTR.anchorMin.y ); resizeButtonTR.anchorMax = new Vector2( 0f, resizeButtonTR.anchorMax.y ); resizeButtonTR.pivot = new Vector2( 0f, resizeButtonTR.pivot.y ); ( (RectTransform) commandInputField.transform ).anchoredPosition += new Vector2( resizeButtonTR.sizeDelta.x, 0f ); } if( enableSearchbar ) searchbar.GetComponent().onValueChanged.AddListener( SearchTermChanged ); else { searchbar = null; searchbarSlotTop.gameObject.SetActive( false ); searchbarSlotBottom.gameObject.SetActive( false ); } filterInfoButton.gameObject.SetActive( receiveInfoLogs ); filterWarningButton.gameObject.SetActive( receiveWarningLogs ); filterErrorButton.gameObject.SetActive( receiveErrorLogs || receiveExceptionLogs ); if( commandSuggestionsContainer.gameObject.activeSelf ) commandSuggestionsContainer.gameObject.SetActive( false ); // Register to UI events commandInputField.onValidateInput += OnValidateCommand; commandInputField.onValueChanged.AddListener( OnEditCommand ); commandInputField.onEndEdit.AddListener( OnEndEditCommand ); hideButton.onClick.AddListener( HideLogWindow ); clearButton.onClick.AddListener( ClearLogs ); collapseButton.GetComponent