diff --git a/Scenes/test.tscn b/Scenes/test.tscn index 17ef2f97..10675170 100644 --- a/Scenes/test.tscn +++ b/Scenes/test.tscn @@ -1,7 +1,6 @@ -[gd_scene load_steps=19 format=4 uid="uid://bv451a8wgty4u"] +[gd_scene load_steps=18 format=4 uid="uid://bv451a8wgty4u"] [ext_resource type="PackedScene" uid="uid://bghghp5ep4w2j" path="res://Scenes/player.tscn" id="2_8mh54"] -[ext_resource type="PackedScene" uid="uid://cxmcqehjjy82j" path="res://Scenes/reisen.tscn" id="3_8k37m"] [ext_resource type="PackedScene" uid="uid://rp4jhx0tuh24" path="res://Scenes/fragola.tscn" id="4_s7wq6"] [ext_resource type="PackedScene" uid="uid://bj28qiai2x2ar" path="res://Scenes/Barrel.tscn" id="5_3uba3"] [ext_resource type="PackedScene" uid="uid://uaf5r6cd71hu" path="res://Scenes/Furniture/LargeTank.tscn" id="6_nkauc"] @@ -280,6 +279,7 @@ position = Vector2(-766, -74) [node name="CameraController" type="Camera2D" parent="."] script = ExtResource("6_t8ide") +pixel_snap = false [node name="Player" parent="." instance=ExtResource("2_8mh54")] position = Vector2(-790, -165) @@ -301,8 +301,5 @@ polygon = PackedVector2Array(95, 57, 46, 58, -83, 61, -91, -37, 88, -37, 114, -1 [node name="Fragola" parent="." instance=ExtResource("4_s7wq6")] position = Vector2(-743, -117) -[node name="CharacterBody2D" parent="." instance=ExtResource("3_8k37m")] -position = Vector2(78, -15) - [node name="Enemy" parent="." instance=ExtResource("18_ixcwn")] position = Vector2(-687, -10) diff --git a/Scripts/Enemy.cs b/Scripts/Enemy.cs index 88e91cc5..b833d1bb 100644 --- a/Scripts/Enemy.cs +++ b/Scripts/Enemy.cs @@ -27,6 +27,7 @@ public partial class Enemy : Area2D, IDestructible private Timer _cooldownTimer; + [DebugGUIPrint] private int _ammo = 0; // Called when the node enters the scene tree for the first time. diff --git a/Scripts/MainMenu.cs b/Scripts/MainMenu.cs index 9e7151a2..1d040c01 100644 --- a/Scripts/MainMenu.cs +++ b/Scripts/MainMenu.cs @@ -9,7 +9,7 @@ public partial class MainMenu : Control [Export] public string MainMenuScene { get; set; } - + // Called when the node enters the scene tree for the first time. public override void _Ready() { diff --git a/Scripts/PlayerMovement.cs b/Scripts/PlayerMovement.cs index ebd4423f..d6521358 100644 --- a/Scripts/PlayerMovement.cs +++ b/Scripts/PlayerMovement.cs @@ -39,6 +39,7 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible [Export] public float Health = 4f; + [DebugGUIPrint] private float _currentHealth = 0f; [Export] public double RateOfFire = 0.4f; diff --git a/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs b/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs new file mode 100644 index 00000000..6e0983d4 --- /dev/null +++ b/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs @@ -0,0 +1,33 @@ +using Godot; +using System; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class DebugGUIGraphAttribute : Attribute +{ + public float min { get; private set; } + public float max { get; private set; } + public Color color { get; private set; } + public int group { get; private set; } + public bool autoScale { get; private set; } + + public DebugGUIGraphAttribute( + // Line color + float r = 1, + float g = 1, + float b = 1, + // Values at top/bottom of graph + float min = 0, + float max = 1, + // Offset position on screen + int group = 0, + // Auto-adjust min/max to fit the values + bool autoScale = true + ) + { + color = new Color(r, g, b, 0.9f); + this.min = min; + this.max = max; + this.group = group; + this.autoScale = autoScale; + } +} \ No newline at end of file diff --git a/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs b/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs new file mode 100644 index 00000000..c20ac6e9 --- /dev/null +++ b/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs @@ -0,0 +1,4 @@ +using System; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class DebugGUIPrintAttribute : Attribute { } \ No newline at end of file diff --git a/addons/DebugGUI/DebugGUI.cs b/addons/DebugGUI/DebugGUI.cs new file mode 100644 index 00000000..cf5d8938 --- /dev/null +++ b/addons/DebugGUI/DebugGUI.cs @@ -0,0 +1,313 @@ +using Godot; +using System.IO; +using WeavUtils; + +public partial class DebugGUI : Control +{ + // Other scripts may use us right off the bat, so we make sure we initialize first + public DebugGUI() + { + ProcessPhysicsPriority = int.MinValue; + } + + static DebugGUI Instance; + + #region Settings + + public static class Settings + { + const string DEBUGGUI_SETTINGS_DIR = "DebugGUI/Settings/"; + + public static void Init() + { + if (!Engine.IsEditorHint()) return; + + // Inits defaults or load current if present + Load(); + + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", enableGraphs); + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", enableLogs); + + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", backgroundColor); + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", scrubberColor); + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", graphWidth); + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", graphHeight); + ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", temporaryLogLifetime); + + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", true); + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", true); + + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", true); + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", true); + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", true); + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", true); + ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", true); + + var err = ProjectSettings.Save(); + if(err != Error.Ok) + { + GD.PrintErr(err); + } + } + + public static void Load() + { + textFont = ThemeDB.FallbackFont; + + enableGraphs = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", + true + ).AsBool(); + enableLogs = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", + true + ).AsBool(); + + backgroundColor = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", + new Color(0f, 0f, 0f, 0.7f) + ).AsColor(); + scrubberColor = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", + new Color(1f, 1f, 0f, 0.7f) + ).AsColor(); + graphWidth = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", + 300 + ).AsInt32(); + graphHeight = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", + 100 + ).AsInt32(); + temporaryLogLifetime = ProjectSettings.GetSetting( + $"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", + 5 + ).AsDouble(); + } + + public static bool enableGraphs; + public static bool enableLogs; + + public static Color backgroundColor; + public static Color scrubberColor; + public static int graphWidth; + public static int graphHeight; + public static double temporaryLogLifetime; + + public static Font textFont; + } + + #endregion + + #region Graph + + /// + /// Set the properties of a graph. + /// + /// The graph's key + /// The graph's label + /// Value at the bottom of the graph box + /// Value at the top of the graph box + /// The graph's ordinal position on screen + /// The graph's color + public static void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale) + { + if (Settings.enableGraphs) + Instance?.graphWindow.SetGraphProperties(key, label, min, max, group, color, autoScale); + } + + /// + /// Set the properties of a graph. + /// + /// The graph's key + /// The graph's label + /// Value at the bottom of the graph box + /// Value at the top of the graph box + /// The graph's ordinal position on screen + /// The graph's color + public static void SetGraphProperties(GodotObject key, string label, float min, float max, int group, Color color, bool autoScale) + { + SetGraphProperties((object)key, label, min, max, group, color, autoScale); + } + + /// + /// Add a data point to a graph. + /// + /// The graph's key + /// Value to be added + public static void Graph(object key, float val) + { + if (Settings.enableGraphs) + Instance?.graphWindow.Graph(key, val); + } + + /// + /// Add a data point to a graph. + /// + /// The graph's key + /// Value to be added + public static void Graph(GodotObject key, float val) + { + Graph((object)key, val); + } + + /// + /// Remove an existing graph. + /// + /// The graph's key + public static void RemoveGraph(object key) + { + if (Settings.enableGraphs) + Instance?.graphWindow.RemoveGraph(key); + } + + /// + /// Remove an existing graph. + /// + /// The graph's key + public static void RemoveGraph(GodotObject key) + { + RemoveGraph((object)key); + } + + /// + /// Resets a graph's data. + /// + /// The graph's key + public static void ClearGraph(object key) + { + if (Settings.enableGraphs) + Instance?.graphWindow.ClearGraph(key); + } + + /// + /// Resets a graph's data. + /// + /// The graph's key + public static void ClearGraph(GodotObject key) + { + ClearGraph((object)key); + } + + /// + /// Export graphs to a json file. See path in log. + /// + public static void ExportGraphs() + { + if (Instance == null || !Settings.enableGraphs) + return; + + string dateTimeStr = Time.GetDatetimeStringFromSystem().Replace(':', '-'); + string filename = $"debuggui_graph_export_{dateTimeStr}.json"; + + using var file = Godot.FileAccess.Open( + "user://" + filename, + Godot.FileAccess.ModeFlags.Write + ); + + if (file == null) + { + GD.Print("DebugGUI graph export failed: " + Godot.FileAccess.GetOpenError()); + } + else + { + file.StoreString(Instance.graphWindow.ToJson()); + GD.Print($"Wrote graph data to {Path.Combine(OS.GetUserDataDir(), filename)}"); + } + } + + #endregion + + #region Log + + /// + /// Create or update an existing message with the same key. + /// + public static void LogPersistent(object key, string message) + { + if (Settings.enableLogs) + Instance?.logWindow.LogPersistent(key, message); + } + + /// + /// Create or update an existing message with the same key. + /// + public static void LogPersistent(GodotObject key, string message) + { + LogPersistent((object)key, message); + } + + /// + /// Remove an existing persistent message. + /// + public static void RemovePersistent(object key) + { + if (Settings.enableLogs) + Instance?.logWindow.RemovePersistent(key); + } + + /// + /// Remove an existing persistent message. + /// + public static void RemovePersistent(GodotObject key) + { + RemovePersistent((object)key); + } + + /// + /// Clears all persistent logs. + /// + public static void ClearPersistent() + { + if (Settings.enableLogs) + Instance?.logWindow.ClearPersistent(); + } + + /// + /// Print a temporary message. + /// + public static void Log(object message) + { + Log(message.ToString()); + } + + /// + /// Print a temporary message. + /// + public static void Log(string message) + { + if (Settings.enableLogs) + Instance?.logWindow.Log(message); + } + + #endregion + + /// + /// Re-scans for DebugGUI attribute holders (i.e. [DebugGUIGraph] and [DebugGUIPrint]) + /// + public static void ForceReinitializeAttributes() + { + if (Instance == null) return; + + Instance.graphWindow.ReinitializeAttributes(); + Instance.logWindow.ReinitializeAttributes(); + } + + GraphWindow graphWindow; + LogWindow logWindow; + + public override void _Ready() + { + Instance = this; + Settings.Load(); + + if (Settings.enableGraphs) + { + AddChild(graphWindow = new()); + } + if (Settings.enableGraphs) + { + AddChild(logWindow = new()); + } + } +} diff --git a/addons/DebugGUI/DebugGUISettingsInitializer.cs b/addons/DebugGUI/DebugGUISettingsInitializer.cs new file mode 100644 index 00000000..bbbcd885 --- /dev/null +++ b/addons/DebugGUI/DebugGUISettingsInitializer.cs @@ -0,0 +1,20 @@ +#if TOOLS +using Godot; + +[Tool] +public partial class DebugGUISettingsInitializer : EditorPlugin +{ + const string DEBUGGUI_RES_PATH = "res://addons/DebugGUI/DebugGUI.cs"; + + public override void _EnterTree() + { + DebugGUI.Settings.Init(); + AddAutoloadSingleton(nameof(DebugGUI), DEBUGGUI_RES_PATH); + } + + public override void _ExitTree() + { + RemoveAutoloadSingleton(nameof(DebugGUI)); + } +} +#endif diff --git a/addons/DebugGUI/Examples/DebugGUIExamples.cs b/addons/DebugGUI/Examples/DebugGUIExamples.cs new file mode 100644 index 00000000..89fc7d25 --- /dev/null +++ b/addons/DebugGUI/Examples/DebugGUIExamples.cs @@ -0,0 +1,148 @@ +using Godot; +using System.Collections.Generic; +using System.Linq; + +public partial class DebugGUIExamples : Node +{ + /* * * * + * + * [DebugGUIGraph] + * Renders the variable in a graph on-screen. Attribute based graphs will updates every _Process. + * Lets you optionally define: + * max, min - The range of displayed values + * r, g, b - The RGB color of the graph (0~1) + * group - Graphs can be grouped into the same window and overlaid + * autoScale - If true the graph will readjust min/max to fit the data + * + * [DebugGUIPrint] + * Draws the current variable continuously on-screen as + * $"{GameObject name} {variable name}: {value}" + * + * For more control, these features can be accessed manually. + * DebugGUI.SetGraphProperties(key, ...) - Set the properties of the graph with the provided key + * DebugGUI.Graph(key, value) - Push a value to the graph + * DebugGUI.LogPersistent(key, value) - Print a persistent log entry on screen + * DebugGUI.Log(value) - Print a temporary log entry on screen + * + * See DebugGUI.cs for more info + * + * * * */ + + // Disable Field Unused warning +#pragma warning disable 0414 + + // Works with regular fields + [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 0, autoScale: true)] + float SinField; + + // As well as properties + [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 1, autoScale: true)] + float CosProperty { get { return Mathf.Cos(time * 6); } } + + // Also works for expression-bodied properties + [DebugGUIGraph(min: -1, max: 1, r: 1, g: 0.3f, b: 1)] + float SinProperty => Mathf.Sin((time + Mathf.Pi / 2) * 6); + + // User inputs, print and graph in one! + [DebugGUIPrint, DebugGUIGraph(group: 1, r: 1, g: 0.3f, b: 0.3f)] + float mouseX; + [DebugGUIPrint, DebugGUIGraph(group: 1, r: 0, g: 1, b: 0)] + float mouseY; + + Queue deltaTimeBuffer = new(); + double smoothDeltaTime => deltaTimeBuffer.Sum() / deltaTimeBuffer.Count; + float time; + float physicsTime; + bool wasMouseDown; + + public override void _Ready() + { + + // Init smooth DT + for (int i = 0; i < 10; i++) + { + deltaTimeBuffer.Enqueue(0); + } + + // Log (as opposed to LogPersistent) will disappear automatically after some time. + DebugGUI.Log("Hello! I will disappear after some time!"); + + // Set up graph properties using our graph keys + DebugGUI.SetGraphProperties("smoothFrameRate", "SmoothFPS", 0, 200, 2, new Color(0, 1, 1), false); + DebugGUI.SetGraphProperties("frameRate", "FPS", 0, 200, 2, new Color(1, 0.5f, 1), false); + DebugGUI.SetGraphProperties("fixedFrameRateSin", "FixedSin", -1, 1, 3, new Color(1, 1, 0), true); + } + + public override void _Process(double delta) + { + time += (float)delta; + + // Update smooth delta time queue + deltaTimeBuffer.Dequeue(); + deltaTimeBuffer.Enqueue(delta); + + // Update the fields our attributes are graphing + SinField = Mathf.Sin(time * 6); + + // Update graphed mouse XY values + var mousePos = GetViewport().GetMousePosition(); + var viewportRect = GetViewport().GetVisibleRect(); + mouseX = Mathf.Clamp(mousePos.X, 0, viewportRect.Size.X); + mouseY = Mathf.Clamp(mousePos.Y, 0, viewportRect.Size.Y); + + // Manual persistent logging + DebugGUI.LogPersistent("smoothFrameRate", "SmoothFPS: " + (1 / smoothDeltaTime).ToString("F3")); + DebugGUI.LogPersistent("frameRate", "FPS: " + (1 / delta).ToString("F3")); + + // Manual logging of mouse clicks + if (Input.IsMouseButtonPressed(MouseButton.Left)) + { + if (!wasMouseDown) + { + wasMouseDown = true; + DebugGUI.Log(string.Format( + "Mouse down ({0}, {1})", + mouseX.ToString("F3"), + mouseY.ToString("F3") + )); + } + } + else + { + wasMouseDown = false; + } + + if (smoothDeltaTime != 0) + { + DebugGUI.Graph("smoothFrameRate", 1 / (float)smoothDeltaTime); + } + if (delta != 0) + { + DebugGUI.Graph("frameRate", 1 / (float)delta); + } + + if (Input.IsKeyPressed(Key.Space)) + { + QueueFree(); + } + } + + public override void _PhysicsProcess(double delta) + { + physicsTime += (float)delta; + + // Manual graphing + DebugGUI.Graph("fixedFrameRateSin", Mathf.Sin(physicsTime * 6)); + } + + public override void _ExitTree() + { + // Clean up our logs and graphs when this object leaves tree + DebugGUI.RemoveGraph("frameRate"); + DebugGUI.RemoveGraph("fixedFrameRateSin"); + DebugGUI.RemoveGraph("smoothFrameRate"); + + DebugGUI.RemovePersistent("frameRate"); + DebugGUI.RemovePersistent("smoothFrameRate"); + } +} \ No newline at end of file diff --git a/addons/DebugGUI/Examples/DebugGUIExamples.tscn b/addons/DebugGUI/Examples/DebugGUIExamples.tscn new file mode 100644 index 00000000..807a8c39 --- /dev/null +++ b/addons/DebugGUI/Examples/DebugGUIExamples.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=3 format=3 uid="uid://cwxrep5n7yml0"] + +[ext_resource type="Script" path="res://addons/DebugGUI/Examples/DebugGUIExamples.cs" id="1_herxx"] +[ext_resource type="Script" path="res://addons/DebugGUI/Examples/debugGUI_examples.gd" id="2_acgyp"] + +[node name="C# Example" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_herxx") + +[node name="gdscript example" type="Node" parent="."] +script = ExtResource("2_acgyp") diff --git a/addons/DebugGUI/Examples/debugGUI_examples.gd b/addons/DebugGUI/Examples/debugGUI_examples.gd new file mode 100644 index 00000000..7b6dfb78 --- /dev/null +++ b/addons/DebugGUI/Examples/debugGUI_examples.gd @@ -0,0 +1,9 @@ +extends Node + +func _ready(): + DebugGUI.SetGraphProperties(self, "from gdscript", 0.0, 10.0, 1, Color.WHITE, true) + DebugGUI.Log(self) + DebugGUI.Log("This can be done from gdscript too!") + +func _process(_delta): + DebugGUI.Graph(self, sin(Time.get_ticks_msec() / 100.0)) diff --git a/addons/DebugGUI/Windows/DebugGUIWindow.cs b/addons/DebugGUI/Windows/DebugGUIWindow.cs new file mode 100644 index 00000000..f6493f8d --- /dev/null +++ b/addons/DebugGUI/Windows/DebugGUIWindow.cs @@ -0,0 +1,61 @@ +using Godot; +using System; + +namespace WeavUtils +{ + // Draggable window clamped to the corners + public abstract partial class DebugGUIWindow : Control + { + protected const int outOfScreenClampPadding = 30; + + static bool dragInProgress; + bool dragged; + + new public virtual Rect2 GetRect() + { + return base.GetRect(); + } + + public override void _Input(InputEvent @event) + { + if (@event is InputEventMouseButton mb) + { + if (!dragInProgress && mb.Pressed && mb.ButtonIndex == MouseButton.Middle) + { + if (GetRect().HasPoint(mb.Position)) + { + dragged = true; + dragInProgress = true; + } + } + if (mb.IsReleased() && mb.ButtonIndex == MouseButton.Middle) + { + if (dragged) dragInProgress = false; + dragged = false; + } + } + + if (@event is InputEventMouseMotion motion) + { + if (dragged) + { + Move(motion.Relative); + } + } + } + + protected void Move(Vector2 delta = default) + { + Position += delta; + + var viewportRect = GetViewportRect(); + + // Limit graph window offset so we can't get lost off screen + Position = Position.Clamp( + -GetRect().Size + Vector2.One * outOfScreenClampPadding, + viewportRect.Size - Vector2.One * outOfScreenClampPadding + ); + } + + } +} \ No newline at end of file diff --git a/addons/DebugGUI/Windows/GraphWindow.cs b/addons/DebugGUI/Windows/GraphWindow.cs new file mode 100644 index 00000000..b4e99962 --- /dev/null +++ b/addons/DebugGUI/Windows/GraphWindow.cs @@ -0,0 +1,693 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Reflection; +using static DebugGUI.Settings; + +namespace WeavUtils +{ + public partial class GraphWindow : DebugGUIWindow + { + const int graphLabelFontSize = 12; + const int graphLabelPadding = 5; + const int graphBlockPadding = 3; + const int scrubberBackgroundWidth = 55; + const int windowOutOfScreenPadding = 30; + + List graphs = new(); + HashSet attributeContainers = new(); + Dictionary typeInstanceCounts = new(); + Dictionary graphDictionary = new(); + Dictionary> attributeKeys = new(); + Dictionary> debugGUIGraphFields = new(); + Dictionary> debugGUIGraphProperties = new(); + SortedDictionary> graphGroups = new(); + + bool freezeGraphs; + float graphLabelBoxWidth; + + public override void _Ready() + { + Name = nameof(GraphWindow); + + // Register attributes of all nodes present at start + // See also: ReinitializeAttributes() + RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); + + // Default to top right + Position = new Vector2(GetViewportRect().Size.X - GetRect().Size.X, 0); + } + + public override void _Process(double delta) + { + if (!Input.IsMouseButtonPressed(MouseButton.Left)) + { + freezeGraphs = false; + } + + if (!freezeGraphs) + { + CallDeferred(nameof(PollGraphAttributes)); + } + + QueueRedraw(); + + // Clean up any nodes queued to free + CallDeferred(nameof(CleanUpDeletedAttributes)); + } + + public override void _Draw() + { + int groupNum = 0; + foreach (var group in graphGroups.Values) + { + DrawGraphGroup(group, groupNum); + groupNum++; + } + } + + public void Graph(object key, float val) + { + if (!graphDictionary.ContainsKey(key)) + { + CreateGraph(key); + } + + if (freezeGraphs) return; + + graphDictionary[key].Push(val); + // Todo: optimize away? + RecalculateGraphLabelWidth(); + } + + public void CreateGraph(object key) + { + AddGraph(key, new GraphContainer(graphWidth)); + RecalculateGraphLabelWidth(); + } + + public void ClearGraph(object key) + { + if (graphDictionary.ContainsKey(key)) + graphDictionary[key].Clear(); + } + + public void RemoveGraph(object key) + { + if (graphDictionary.ContainsKey(key)) + { + var graph = graphDictionary[key]; + graphs.Remove(graph); + graphDictionary.Remove(key); + graphGroups[graph.group].Remove(graph); + if (graphGroups[graph.group].Count == 0) + { + graphGroups.Remove(graph.group); + } + RecalculateGraphLabelWidth(); + } + } + + public void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale) + { + if (graphDictionary.ContainsKey(key)) + { + RemoveGraph(key); + } + + var graph = new GraphContainer(graphWidth, group); + AddGraph(key, graph); + + graph.name = label; + graph.SetMinMax(min, max); + graph.color = color; + graph.autoScale = autoScale; + } + + public void ReinitializeAttributes() + { + // Clean up graphs + List toRemove = new List(); + foreach (var key in graphDictionary.Keys) + { + if (key is GraphAttributeKey) + toRemove.Add(key); + } + foreach (var key in toRemove) + { + RemoveGraph(key); + } + + attributeContainers = new(); + debugGUIGraphFields = new(); + debugGUIGraphProperties = new(); + typeInstanceCounts = new(); + attributeKeys = new(); + + RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); + } + + public string ToJson() + { + var data = new Godot.Collections.Array(); + + foreach (var node in graphs) + { + data.Add(node.ToDataVariant()); + } + + return data.ToString(); + } + + public override Rect2 GetRect() + { + RefreshRect(); + return base.GetRect(); + } + + private void AddGraph(object key, GraphContainer graph) + { + graph.OnLabelSizeChange += RefreshRect; + + graphDictionary.Add(key, graph); + graphs.Add(graph); + + if (!graphGroups.ContainsKey(graph.group)) + { + graphGroups.Add(graph.group, new List()); + } + + graphGroups[graph.group].Add(graph); + + RecalculateGraphLabelWidth(); + } + + private void PollGraphAttributes() + { + foreach (var node in attributeContainers) + { + if (node != null && attributeKeys.ContainsKey(node)) + { + foreach (var key in attributeKeys[node]) + { + if (key.memberInfo is FieldInfo fieldInfo) + { + float? val = fieldInfo.GetValue(node) as float?; + if (val != null) + graphDictionary[key].Push(val.Value); + } + else if (key.memberInfo is PropertyInfo propertyInfo) + { + float? val = propertyInfo.GetValue(node, null) as float?; + if (val != null) + graphDictionary[key].Push(val.Value); + } + } + } + } + } + + GraphContainer lastPressedGraphLabel; + private void DrawGraphGroup(List group, int groupNum) + { + var mousePos = GetLocalMousePosition(); + + Vector2 graphBlockSize = new Vector2(graphWidth + graphBlockPadding, graphHeight + graphBlockPadding); + + var groupOrigin = new Vector2(0, graphBlockSize.Y * groupNum); + var groupGraphRect = new Rect2( + groupOrigin.X + graphLabelBoxWidth + graphBlockPadding, + groupOrigin.Y, + graphWidth, + graphHeight + ); + + // Label background + DrawRect(new Rect2( + groupOrigin.X, + groupOrigin.Y, + graphLabelBoxWidth, + graphHeight), + backgroundColor); + + // Graph background + DrawRect(new Rect2( + groupOrigin.X + graphBlockPadding + graphLabelBoxWidth, + groupOrigin.Y, + graphBlockSize.X, + graphHeight), + backgroundColor); + + // Magic padding offsets + Vector2 textOrigin = groupOrigin + new Vector2(0, 14); + Vector2 minMaxOrigin = groupOrigin + new Vector2(graphLabelBoxWidth - 10, 16); + foreach (var graph in group) + { + var textSize = textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize); + textOrigin.Y += textSize.Y; + var maxWidthOfMinMaxStrings = Mathf.Max( + textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X, + textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X + ); + minMaxOrigin += Vector2.Left * (maxWidthOfMinMaxStrings + graphLabelPadding); + + // Label button logic + var labelRect = new Rect2(textOrigin - textSize + new Vector2(graphLabelBoxWidth - (graphLabelPadding * 2), graphLabelPadding), textSize); + // Enable disable + var isHovered = labelRect.HasPoint(mousePos); + var isPressed = isHovered && Input.IsMouseButtonPressed(MouseButton.Left); + + // Button click + if (lastPressedGraphLabel == graph && !isPressed && isHovered) + { + graph.visible = !graph.visible; + } + + if (isPressed) + { + lastPressedGraphLabel = graph; + } + else if (lastPressedGraphLabel == graph) + { + lastPressedGraphLabel = null; + } + + var graphColor = graph.GetModifiedColor(isHovered); + + // Name + DrawString( + textFont, + textOrigin - new Vector2(textSize.X + 10 - graphLabelBoxWidth, 0), + graph.name, + fontSize: graphLabelFontSize, + modulate: graphColor, + alignment: HorizontalAlignment.Right + ); + + // Max + DrawString( + textFont, + minMaxOrigin, + graph.maxString, + modulate: graphColor, + fontSize: graphLabelFontSize, + alignment: HorizontalAlignment.Right + ); + + // Min + DrawString( + textFont, + minMaxOrigin + new Vector2(0, graphHeight - 20), + graph.minString, + modulate: graphColor, + fontSize: graphLabelFontSize, + alignment: HorizontalAlignment.Right + ); + + // Graph + if (graph.visible) + { + graph.Draw(groupGraphRect, this); + } + } + + // Scrubber + if (groupGraphRect.HasPoint(mousePos)) + { + if (Input.IsMouseButtonPressed(MouseButton.Left)) + { + freezeGraphs = true; + } + + // Background + Vector2 scrubberOrigin = new Vector2(mousePos.X, groupOrigin.Y); + if (mousePos.X > groupGraphRect.End.X - scrubberBackgroundWidth) + { + scrubberOrigin.X -= scrubberBackgroundWidth; + } + + var rect = new Rect2( + scrubberOrigin, + scrubberBackgroundWidth, + graphHeight + ); + DrawRect(rect, backgroundColor); + + DrawLine( + new Vector2( + mousePos.X, + groupOrigin.Y + ), new Vector2( + mousePos.X, + groupOrigin.Y + graphHeight + ), + scrubberColor + ); + + // Scrubber labels + Vector2 textPos = scrubberOrigin + new Vector2(graphLabelPadding, graphLabelPadding * 3); + var groupMousePosX = (mousePos.X - groupOrigin.X); + int sampleIndex = (int)(groupGraphRect.Size.X - groupMousePosX + graphLabelBoxWidth + graphBlockPadding); + foreach (GraphContainer graph in group) + { + var text = graph.GetValue(sampleIndex).ToString("F3"); + DrawString( + textFont, + textPos, + text, + modulate: graph.color, + fontSize: graphLabelFontSize + ); + textPos.Y += textFont.GetHeight(graphLabelFontSize); + } + } + } + + private Rect2 GetGraphWindowRect() + { + return new Rect2( + new Vector2(-graphLabelBoxWidth, 0) + Position, + graphWidth + graphLabelBoxWidth + graphBlockPadding, + (graphHeight + graphBlockPadding) * graphGroups.Count + ); + } + + private void RegisterAttributes(Node node) + { + foreach (Node child in node.GetChildren()) + { + GD.Print(child); + RegisterAttributes(child); + } + + Type nodeType = node.GetType(); + + HashSet uniqueAttributeContainers = new(); + // Fields + { + // Retreive the fields from the mono instance + FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + // search all fields/properties for the [DebugGUIVar] attribute + for (int i = 0; i < objectFields.Length; i++) + { + DebugGUIGraphAttribute graphAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIGraphAttribute)) as DebugGUIGraphAttribute; + + if (graphAttribute != null) + { + // Can't cast to float so we don't bother registering it + if (objectFields[i].GetValue(node) as float? == null) + { + GD.PrintErr(string.Format("Cannot cast {0}.{1} to float. This member will be ignored.", nodeType.Name, objectFields[i].Name)); + continue; + } + + uniqueAttributeContainers.Add(node); + if (!debugGUIGraphFields.ContainsKey(nodeType)) + debugGUIGraphFields.Add(nodeType, new HashSet()); + if (!debugGUIGraphProperties.ContainsKey(nodeType)) + debugGUIGraphProperties.Add(nodeType, new HashSet()); + + debugGUIGraphFields[nodeType].Add(objectFields[i]); + GraphContainer graph = + new GraphContainer(graphWidth, graphAttribute.group) + { + name = objectFields[i].Name, + max = graphAttribute.max, + min = graphAttribute.min, + autoScale = graphAttribute.autoScale + }; + graph.OnLabelSizeChange += RefreshRect; + if (!graphAttribute.color.Equals(default(Color))) + graph.color = graphAttribute.color; + + var key = new GraphAttributeKey(objectFields[i]); + if (!attributeKeys.ContainsKey(node)) + attributeKeys.Add(node, new List()); + attributeKeys[node].Add(key); + + AddGraph(key, graph); + } + } + } + + // Properties + { + PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + for (int i = 0; i < objectProperties.Length; i++) + { + if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIGraphAttribute)) is DebugGUIGraphAttribute graphAttribute) + { + // Can't cast to float so we don't bother registering it + if (objectProperties[i].GetValue(node, null) as float? == null) + { + GD.PrintErr("Cannot cast " + objectProperties[i].Name + " to float. This member will be ignored."); + continue; + } + + uniqueAttributeContainers.Add(node); + + if (!debugGUIGraphFields.ContainsKey(nodeType)) + debugGUIGraphFields.Add(nodeType, new HashSet()); + if (!debugGUIGraphProperties.ContainsKey(nodeType)) + debugGUIGraphProperties.Add(nodeType, new HashSet()); + + debugGUIGraphProperties[nodeType].Add(objectProperties[i]); + + GraphContainer graph = + new GraphContainer(graphWidth, graphAttribute.group) + { + name = objectProperties[i].Name, + max = graphAttribute.max, + min = graphAttribute.min, + autoScale = graphAttribute.autoScale + }; + graph.OnLabelSizeChange += RefreshRect; + if (!graphAttribute.color.Equals(default(Color))) + graph.color = graphAttribute.color; + + var key = new GraphAttributeKey(objectProperties[i]); + if (!attributeKeys.ContainsKey(node)) + attributeKeys.Add(node, new List()); + attributeKeys[node].Add(key); + + AddGraph(key, graph); + } + } + } + + foreach (var attributeContainer in uniqueAttributeContainers) + { + attributeContainers.Add(attributeContainer); + Type type = attributeContainer.GetType(); + if (!typeInstanceCounts.ContainsKey(type)) + typeInstanceCounts.Add(type, 0); + typeInstanceCounts[type]++; + } + } + + private void CleanUpDeletedAttributes() + { + // Clear out associated keys + foreach (var node in attributeContainers) + { + if (node.IsQueuedForDeletion()) + { + var keys = attributeKeys[node]; + foreach (var key in keys) + { + RemoveGraph(key); + } + attributeKeys.Remove(node); + + Type type = node.GetType(); + typeInstanceCounts[type]--; + if (typeInstanceCounts[type] == 0) + { + if (debugGUIGraphFields.ContainsKey(type)) + debugGUIGraphFields.Remove(type); + if (debugGUIGraphProperties.ContainsKey(type)) + debugGUIGraphProperties.Remove(type); + } + } + } + + // Finally clear out removed nodes + attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion()); + } + + void RefreshRect() + { + var lastWidth = Size.X; + RecalculateGraphLabelWidth(); + Size = new Vector2( + graphWidth + graphLabelBoxWidth + graphBlockPadding, + (graphHeight + graphBlockPadding) * graphGroups.Count); + // Grow to the left instead of right + Position += new Vector2(lastWidth - Size.X, 0); + } + + void RecalculateGraphLabelWidth() + { + float width = 0; + foreach (var group in graphGroups.Values) + { + float minMaxWidth = graphLabelPadding; + foreach (var graph in group) + { + // Names + width = Mathf.Max(textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize).X, width); + + // Minmax labels per group + var maxWidthOfMinMaxStrings = Mathf.Max( + textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X, + textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X + ); + minMaxWidth += maxWidthOfMinMaxStrings + graphLabelPadding; + } + width = Mathf.Max(minMaxWidth, width); + } + graphLabelBoxWidth = width + graphLabelPadding * 2; + } + + private class GraphContainer + { + public Action OnLabelSizeChange; + + public string name; + + // Value at the top of the graph + public float max = 1; + // Value at the bottom of the graph + public float min = 0; + public bool autoScale; + public Color color; + // Graph order on screen + public readonly int group; + + private int currentIndex; + private readonly float[] values; + private readonly Vector2[] graphPoints; + + public string minString = null; + public string maxString = null; + public bool visible = true; + + public Color GetModifiedColor(bool highlighted) + { + if (!highlighted && visible) return color; + color.ToHsv(out float h, out float s, out float v); + + if (!visible) v *= 0.3f; + if (highlighted) v *= (v > 0.9f ? 0.7f : 1.2f); + return Color.FromHsv(h, s, v); + } + + public void SetMinMax(float min, float max) + { + OnLabelSizeChange?.Invoke(); + this.min = min; + this.max = max; + + minString = min.ToString("F2"); + maxString = max.ToString("F2"); + } + + public GraphContainer(int width, int group = 0) + { + this.group = group; + values = new float[width]; + graphPoints = new Vector2[width]; + SetMinMax(min, max); + } + + // Add a data point to the beginning of the graph + public void Push(float val) + { + if (autoScale && (val > max || val < min)) + { + SetMinMax(Mathf.Min(val, min), Mathf.Max(val, max)); + } + else + { + // Prevent drawing outside frame + val = Mathf.Clamp(val, min, max); + } + + values[currentIndex] = val; + currentIndex = (currentIndex + 1) % values.Length; + } + + public void Clear() + { + for (int i = 0; i < values.Length; i++) + { + values[i] = 0; + } + } + + public void Draw(Rect2 rect, CanvasItem canvasItem) + { + int num = values.Length; + for (int i = 0; i < num; i++) + { + float value = values[Mod(currentIndex - i - 1, values.Length)]; + // Note flipped inverse lerp min max to account for y = down in godot + graphPoints[i] = new Vector2( + rect.Position.X + (rect.Size.X * ((float)i / num)), + rect.Position.Y + (Mathf.InverseLerp(max, min, value) * graphHeight) + ); + } + + canvasItem.DrawPolyline(graphPoints, color); + } + + public float GetValue(int index) + { + return values[Mod(currentIndex + index, values.Length)]; + } + + class DataExport + { + public string name; + public float[] values; + + public DataExport(string name, float[] values) + { + this.name = name; + this.values = values; + } + } + + public Variant ToDataVariant() + { + var vals = new float[values.Length]; + for (int i = 0; i < vals.Length; i++) + { + vals[i] = values[Mod(currentIndex + i, values.Length)]; + } + + var dict = new Godot.Collections.Dictionary(); + dict.Add("name", name); + dict.Add("values", vals); + + return dict; + } + + private static int Mod(int n, int m) + { + return ((n % m) + m) % m; + } + } + + public class GraphAttributeKey + { + public MemberInfo memberInfo; + public GraphAttributeKey(MemberInfo memberInfo) + { + this.memberInfo = memberInfo; + } + } + } +} \ No newline at end of file diff --git a/addons/DebugGUI/Windows/LogWindow.cs b/addons/DebugGUI/Windows/LogWindow.cs new file mode 100644 index 00000000..f03a07a3 --- /dev/null +++ b/addons/DebugGUI/Windows/LogWindow.cs @@ -0,0 +1,308 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Text; +using System.Reflection; +using static DebugGUI.Settings; + +namespace WeavUtils +{ + public partial class LogWindow : DebugGUIWindow + { + List transientLogs = new(); + HashSet attributeContainers = new(); + Dictionary typeCache = new(); + Dictionary typeInstanceCounts = new(); + Dictionary persistentLogs = new(); + Dictionary> attributeKeys = new(); + Dictionary> debugGUIPrintFields = new(); + Dictionary> debugGUIPrintProperties = new(); + + StringBuilder persistentLogStringBuilder = new(); + + double time; + + public override void _Ready() + { + Name = nameof(LogWindow); + + // Register attributes of all nodes present at start + // See also: ReinitializeAttributes() + RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); + } + + public override void _Process(double delta) + { + time += delta; + + // Clean up expired logs + int expiredCt = 0; + for (int i = 0; i < transientLogs.Count; i++) + { + if (transientLogs[i].expiryTime <= time) + { + expiredCt++; + } + } + transientLogs.RemoveRange(0, expiredCt); + + if (debugGUIPrintFields.Count + debugGUIPrintProperties.Count + persistentLogs.Count + transientLogs.Count > 0) + { + QueueRedraw(); + } + + CallDeferred(nameof(CleanUpDeletedAttributes)); + } + + public override void _Draw() + { + persistentLogStringBuilder.Clear(); + + var viewportRect = GetViewportRect(); + var lineHeight = textFont.GetHeight(); + + foreach (var node in attributeContainers) + { + Type type = typeCache[node]; + if (debugGUIPrintFields.ContainsKey(type)) + { + foreach (var field in debugGUIPrintFields[type]) + { + persistentLogStringBuilder.AppendLine($"{node.Name} {field.Name}: {field.GetValue(node)}"); + } + } + if (debugGUIPrintProperties.ContainsKey(type)) + { + foreach (var property in debugGUIPrintProperties[type]) + { + persistentLogStringBuilder.AppendLine($"{node.Name} {property.Name}: {property.GetValue(node, null)}"); + } + } + } + + foreach (var log in persistentLogs.Values) + { + persistentLogStringBuilder.AppendLine(log); + } + + if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0) + { + persistentLogStringBuilder.AppendLine(); + } + + var persistentLogStr = persistentLogStringBuilder.ToString(); + var textSize = textFont.GetMultilineStringSize(persistentLogStr); + Size = textSize + Vector2.One * 10; + + float transientLogY = textSize.Y; + + foreach (var log in transientLogs) + { + var size = textFont.GetStringSize(log.text); + textSize = new Vector2( + Mathf.Max(size.X, textSize.X), + textSize.Y + size.Y + ); + } + + + var backgroundRect = new Rect2(Vector2.Zero, textSize.X + 10, textSize.Y + 10); + DrawRect(backgroundRect, backgroundColor); + // Draw a little bit extra for the draggable area + DrawRect(new Rect2(0, 0, GetRect().Size), new Color(1, 1, 1, 0.05f)); + + // Draw persistent logs + DrawMultilineString(textFont, new Vector2(0, textFont.GetHeight()), persistentLogStr); + + // Draw separator + if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0) + { + DrawDashedLine( + new Vector2(0, transientLogY), + new Vector2(textSize.X, transientLogY), + Colors.White, + 2 + ); + } + + // Draw transient logs + for (int i = transientLogs.Count - 1; i >= 0; i--) + { + transientLogY += lineHeight; + // Clear up transient logs going off screen + if (transientLogY > viewportRect.Size.Y) + { + transientLogs.RemoveRange(0, i + 1); + break; + } + + var log = transientLogs[i]; + DrawString(textFont, new Vector2(0, transientLogY), log.text); + } + } + + public void Log(string str) + { + transientLogs.Add(new TransientLog(str, time + temporaryLogLifetime)); + } + + public void LogPersistent(object key, string message) + { + if (persistentLogs.ContainsKey(key)) + persistentLogs[key] = message; + else + persistentLogs.Add(key, message); + } + + public void RemovePersistent(object key) + { + if (persistentLogs.ContainsKey(key)) + { + persistentLogs.Remove(key); + } + } + + public void ClearPersistent() + { + persistentLogs.Clear(); + } + + public void ReinitializeAttributes() + { + // Clean up graphs + List toRemove = new List(); + foreach (var key in persistentLogs.Keys) + { + if (key is PersistentLogAttributeKey) + toRemove.Add(key); + } + foreach (var key in toRemove) + { + persistentLogs.Remove(key); + } + + attributeContainers = new(); + debugGUIPrintFields = new(); + debugGUIPrintProperties = new(); + typeInstanceCounts = new(); + attributeKeys = new(); + } + + private void RegisterAttributes(Node node) + { + foreach (Node child in node.GetChildren()) + { + GD.Print(child); + RegisterAttributes(child); + } + + Type nodeType = node.GetType(); + + + HashSet uniqueAttributeContainers = new(); + + // Fields + { + // Retreive the fields from the mono instance + FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + // search all fields/properties for the [DebugGUIVar] attribute + for (int i = 0; i < objectFields.Length; i++) + { + DebugGUIPrintAttribute printAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIPrintAttribute)) as DebugGUIPrintAttribute; + + if (printAttribute != null) + { + uniqueAttributeContainers.Add(node); + typeCache[node] = node.GetType(); + if (!debugGUIPrintFields.ContainsKey(nodeType)) + { + debugGUIPrintFields.Add(nodeType, new HashSet()); + } + + GD.Print("Found field " + objectFields[i].Name + " on " + node); + debugGUIPrintFields[nodeType].Add(objectFields[i]); + } + } + } + + // Properties + { + PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + for (int i = 0; i < objectProperties.Length; i++) + { + if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIPrintAttribute)) is DebugGUIPrintAttribute) + { + uniqueAttributeContainers.Add(node); + typeCache[node] = node.GetType(); + + if (!debugGUIPrintProperties.ContainsKey(nodeType)) + { + debugGUIPrintProperties.Add(nodeType, new HashSet()); + } + debugGUIPrintProperties[nodeType].Add(objectProperties[i]); + } + } + } + + foreach (var attributeContainer in uniqueAttributeContainers) + { + attributeContainers.Add(attributeContainer); + Type type = attributeContainer.GetType(); + if (!typeInstanceCounts.ContainsKey(type)) + typeInstanceCounts.Add(type, 0); + typeInstanceCounts[type]++; + } + } + + private void CleanUpDeletedAttributes() + { + // Clear out associated keys + foreach (var node in attributeContainers) + { + if (node.IsQueuedForDeletion()) + { + attributeKeys.Remove(node); + typeCache.Remove(node); + + Type type = node.GetType(); + typeInstanceCounts[type]--; + if (typeInstanceCounts[type] == 0) + { + if (debugGUIPrintFields.ContainsKey(type)) + debugGUIPrintFields.Remove(type); + if (debugGUIPrintProperties.ContainsKey(type)) + debugGUIPrintProperties.Remove(type); + } + } + } + + // Finally clear out removed nodes + attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion()); + } + + private struct TransientLog + { + public string text; + public double expiryTime; + + public TransientLog(string text, double expiryTime) + { + this.text = text; + this.expiryTime = expiryTime; + } + } + + // Wrapper to differentiate attributes from + // manually created logs + public class PersistentLogAttributeKey + { + public MemberInfo memberInfo; + public PersistentLogAttributeKey(MemberInfo memberInfo) + { + this.memberInfo = memberInfo; + } + } + } +} diff --git a/addons/DebugGUI/plugin.cfg b/addons/DebugGUI/plugin.cfg new file mode 100644 index 00000000..5e860a25 --- /dev/null +++ b/addons/DebugGUI/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="DebugGUI" +description="" +author="WeaverDev" +version="1.0" +script="DebugGUISettingsInitializer.cs" diff --git a/project.godot b/project.godot index 14737ad1..817e1f76 100644 --- a/project.godot +++ b/project.godot @@ -8,21 +8,36 @@ config_version=5 +[DebugGUI] + +Settings/enableGraphs=true +Settings/enableLogs=true +Settings/backgroundColor=Color(0, 0, 0, 0.7) +Settings/scrubberColor=Color(1, 1, 0, 0.7) +Settings/graphWidth=300 +Settings/graphHeight=100 +Settings/temporaryLogLifetime=5.0 + [application] config/name="Cirno" -run/main_scene="res://Scenes/game.tscn" +run/main_scene="res://Scenes/test.tscn" config/features=PackedStringArray("4.3", "C#", "GL Compatibility") config/icon="res://icon.svg" [autoload] -DebugStats="*res://Scenes/debug_stats.tscn" +DebugStats="res://Scenes/debug_stats.tscn" +DebugGUI="*res://addons/DebugGUI/DebugGUI.cs" [display] -window/size/viewport_width=1920 -window/size/viewport_height=1080 +window/size/viewport_width=320 +window/size/viewport_height=160 +window/size/window_width_override=1920 +window/size/window_height_override=1080 +window/stretch/mode="canvas_items" +window/stretch/scale_mode="integer" [dotnet] @@ -30,7 +45,7 @@ project/assembly_name="Cirno" [editor_plugins] -enabled=PackedStringArray("res://addons/smoothing/plugin.cfg") +enabled=PackedStringArray("res://addons/DebugGUI/plugin.cfg", "res://addons/smoothing/plugin.cfg") [input]