debug console init

godot-4.2
John Montagu, the 4th Earl of Sandvich 2023-09-24 18:51:23 -07:00
parent 213358a2ba
commit ad29c9cd29
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
14 changed files with 755 additions and 38 deletions

View File

@ -0,0 +1,29 @@
namespace SupaLidlGame.Debug;
internal sealed class CharIterator : Iterator<char>
{
public CharIterator(string str) : base(str.ToCharArray())
{
}
public CharIterator(char[] chars) : base(chars)
{
}
public override char MoveNext()
{
char c = base.MoveNext();
if (c == '\n')
{
Line++;
Column = 0;
}
else
{
Column++;
}
return c;
}
}

View File

@ -2,21 +2,97 @@ using Godot;
namespace SupaLidlGame.Debug;
public partial class DebugConsole : Node
public sealed partial class DebugConsole : Control
{
public void SetProp(
Utils.World world,
string entityName,
string property,
string value)
private Node _context;
public Node Context
{
var ent = world.CurrentMap.Entities.GetNodeOrNull(entityName);
if (ent is not null)
get => _context;
private set
{
ent.Set(property, value);
if (value is not null)
{
_context = value;
if (_entry is not null)
{
_entry.PlaceholderText = "Enter Godot expression from " +
_context.GetPath();
GetParent<Window>().Title = "Supa Developer Console: " +
_context.GetPath();
}
}
}
}
private Entry _entry;
private RichTextLabel _output;
private Node ctx => Context;
public override void _Ready()
{
_entry = GetNode<Entry>("%Entry");
_output = GetNode<RichTextLabel>("%Output");
Context = Utils.World.Instance;
_entry.ConsoleInput += (string input) =>
{
try
{
Execute(input);
}
catch (InterpreterException ex)
{
_output.Text += ex.Message + '\n';
}
};
}
enum PathType
{
Node,
Property
};
public Variant From(NodePath path)
{
CharIterator iterator = new(path);
Variant variant = Context ?? this;
foreach (var subpath in NodePathParser.ParseNodePath(iterator))
{
if (variant.VariantType == Variant.Type.Object)
{
if (variant.AsGodotObject() is Node n)
{
if (subpath.Type == NodePathTokenType.Node)
{
if (subpath.Path != "")
{
variant = n.GetNode(subpath.Path);
}
}
else
{
variant = n.GetIndexed(subpath.Path);
}
}
}
}
return variant;
}
public void SetProp(NodePath path, Variant value)
{
var node = GetNode(path.GetAsPropertyPath());
//var ent = CurrentMap.Entities.GetNodeOrNull(entityName);
//if (ent is not null)
//{
// ent.Set(property, value);
//}
}
public string CallMethod(
Utils.World world,
string entityName,
@ -31,4 +107,54 @@ public partial class DebugConsole : Node
}
return "";
}
public void Print(string text)
{
GD.Print(text);
}
public void Execute(string str)
{
str = Sanitizer.Sanitize(str);
string inputMirror = $"[b]{Context.GetPath()}:[/b] {str}";
_output.Text += inputMirror + "\n";
var context = Context;
Godot.Expression exp = new();
string[] reserved = { "from", "set_context", "context" };
Godot.Collections.Array reservedMap = new();
reservedMap.Add(new Callable(this, MethodName.From));
reservedMap.Add(new Callable(this, MethodName.SetContext));
reservedMap.Add(Context);
var err = exp.Parse(str, reserved);
if (err != Error.Ok)
{
throw new InterpreterException(
"Error occurred while parsing Godot.Expression: \n" +
exp.GetErrorText(),
0, 0);
}
Variant result = exp.Execute(reservedMap, context);
if (exp.HasExecuteFailed())
{
throw new InterpreterException(
"Error occurred while evaluating Godot.Expression: \n" +
exp.GetErrorText(),
0, 0);
}
// send result to output
if (result.VariantType != Variant.Type.Nil)
{
_output.Text += result + "\n";
}
}
public void SetContext(Node node)
{
Context = node;
}
}

30
Debug/Entry.cs 100644
View File

@ -0,0 +1,30 @@
using Godot;
namespace SupaLidlGame.Debug;
public partial class Entry : LineEdit
{
[Signal]
public delegate void ConsoleInputEventHandler(string input);
public override void _Ready()
{
GuiInput += OnGuiInput;
}
public void OnGuiInput(InputEvent @event)
{
if (@event is InputEventKey key)
{
if (key.KeyLabel == Key.Enter && !key.Pressed)
{
EmitSignal(SignalName.ConsoleInput, Text);
if (!key.CtrlPressed)
{
Text = "";
}
}
}
}
}

46
Debug/Iterator.cs 100644
View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
namespace SupaLidlGame.Debug;
public class Iterator<T> where T : struct
{
public int Line { get; protected set; } = 1;
public int Column { get; protected set; } = 0;
public int Index { get; protected set; } = -1;
protected List<T> _elements;
public Iterator(T[] elements)
{
_elements = new List<T>(elements);
}
public Iterator(List<T> elements)
{
_elements = new List<T>(elements);
}
public T GetNext(int offset = 0)
{
if (Index + offset + 1 < _elements.Count)
{
return _elements[Index + offset + 1];
}
return default;
}
public virtual T MoveNext()
{
T next = GetNext();
Index++;
return next;
}
public virtual void MoveBack()
{
Index--;
}
}

View File

@ -0,0 +1,65 @@
using Godot;
using System.Collections.Generic;
namespace SupaLidlGame.Debug;
internal static class NodePathParser
{
internal static IEnumerable<NodePathToken> ParseNodePath(CharIterator iterator)
{
// Some/Node/Path:And:Property/More/Paths
// ->
// Some/Node/Path (Node)
// :And:Property (Property)
// More/Paths (Node)
NodePathTokenType curType = NodePathTokenType.Node;
string path = "";
while (iterator.GetNext() != '\0')
{
char curChar = iterator.MoveNext();
if (curChar == ':')
{
// if we have been parsing a nodepath, yield a nodepath
if (curType == NodePathTokenType.Node)
{
if (path.Length > 0)
{
yield return new NodePathToken(path, curType);
path = "";
curType = NodePathTokenType.Property;
}
}
else
{
path += curChar;
}
}
else if (curChar == '/')
{
// if we have been parsing property, yield a property
if (curType == NodePathTokenType.Property)
{
yield return new NodePathToken(path, curType);
path = "";
curType = NodePathTokenType.Node;
}
else
{
path += curChar;
}
}
else
{
path += curChar;
}
}
// reached the end
if (path.Length > 0)
{
yield return new NodePathToken(path, curType);
}
}
}

View File

@ -0,0 +1,20 @@
using Godot;
public enum NodePathTokenType
{
Node,
Property
}
public struct NodePathToken
{
public NodePath Path { get; set; }
public NodePathTokenType Type { get; set; }
public NodePathToken(NodePath path, NodePathTokenType type)
{
Path = path;
Type = type;
}
}

136
Debug/Sanitizer.cs 100644
View File

@ -0,0 +1,136 @@
using Godot;
using System.Text.RegularExpressions;
namespace SupaLidlGame.Debug;
public static class Sanitizer
{
private static Regex _nonAlphanum = new("[^a-zA-Z0-9_]");
private static Regex _nonNodeName = new("[^a-zA-Z0-9_\\-\\/]");
private static string ScanString(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == '"')
{
return ret;
}
else if (c == '\\')
{
char escape = iterator.MoveNext();
switch (escape)
{
case 'n':
ret += '\n';
break;
case 't':
ret += '\t';
break;
case '\0':
throw new InterpreterException("Unexpected EOL",
iterator.Line, iterator.Column); default:
ret += escape;
break;
}
}
ret += c;
}
throw new InterpreterException("Unexpected EOL, expected '\"'",
iterator.Line, iterator.Column);
}
private static string ScanNodePath(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == '"')
{
return ScanString(iterator);
}
else if (_nonNodeName.IsMatch(c.ToString()))
{
iterator.MoveBack();
return ret;
}
ret += c;
}
return ret;
}
private static string ScanUntilOrEOL(CharIterator iterator, char delim)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == delim)
{
return ret;
}
ret += c;
}
return ret;
}
private static string ScanGlobalCommand(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (_nonAlphanum.IsMatch(c.ToString()))
{
iterator.MoveBack();
return ret;
}
ret += c;
}
return ret;
}
public static string Sanitize(string input)
{
CharIterator iterator = new(input);
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == '$')
{
string nodePath = ScanNodePath(iterator);
ret += $"from.call(\"{nodePath}\")";
}
else if (c == '"')
{
string str = ScanString(iterator);
ret += $"\"{str}\"";
}
else if (c == '\\')
{
// \global -> global.call
string command = ScanGlobalCommand(iterator);
ret += $"{command}.call";
}
else
{
ret += c;
}
}
return ret;
}
}

49
Debug/Token.cs 100644
View File

@ -0,0 +1,49 @@
namespace SupaLidlGame.Debug;
public enum TokenType
{
None,
Identifier,
String,
GodotExpression,
Command,
End
};
public struct Token
{
public TokenType Type { get; set; }
public string Value { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public Token(TokenType type, string value, int line, int col)
{
Type = type;
Value = value;
Line = line;
Column = col;
}
public bool CompareTypeValue(Token token)
{
return Type == token.Type && Value == token.Value;
}
public override bool Equals(object obj)
{
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(Token left, Token right) => left.Equals(right);
public static bool operator !=(Token left, Token right) => !left.Equals(right);
}

185
Debug/Tokenizer.cs 100644
View File

@ -0,0 +1,185 @@
using System.Collections.Generic;
namespace SupaLidlGame.Debug;
internal sealed class Tokenizer
{
private static readonly HashSet<char> WHITESPACE = new HashSet<char> { ' ', '\n' };
private static string ScanString(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == '"')
{
return ret;
}
else if (c == '\\')
{
char escape = iterator.MoveNext();
switch (escape)
{
case 'n':
ret += '\n';
break;
case 't':
ret += '\t';
break;
case '\0':
throw new InterpreterException("Unexpected EOL",
iterator.Line, iterator.Column);
default:
ret += escape;
break;
}
}
ret += c;
}
throw new InterpreterException("Unexpected EOL, expected '\"'",
iterator.Line, iterator.Column);
}
private static string ScanNodePath(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == '"')
{
ret += ScanString(iterator);
}
else if (WHITESPACE.Contains(c))
{
return ret;
}
ret += c;
}
return ret;
}
/*
private static string ScanUntil(CharIterator iterator)
{
}
*/
private static string ScanExpression(CharIterator iterator)
{
int level = 0;
string exp = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == '(')
{
level++;
}
else if (c == ')')
{
level--;
}
if (level < 0)
{
return exp;
}
exp += c;
}
return exp;
}
private static string ScanUntilOrEOL(CharIterator iterator, char delim)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == delim)
{
return ret;
}
ret += c;
}
return ret;
}
public static IEnumerable<Token> Tokenize(CharIterator iterator)
{
System.Diagnostics.Debug.Print("hi");
while (iterator.GetNext() != '\0')
{
char curChar = iterator.MoveNext();
System.Diagnostics.Debug.Print(curChar.ToString());
int line = iterator.Line;
int col = iterator.Column;
if (WHITESPACE.Contains(curChar))
{
continue;
}
else if (curChar == '\\')
{
string command = ScanUntilOrEOL(iterator, ' ');
if (command == "")
{
throw new InterpreterException(
"Expected a command name",
iterator.Line,
iterator.Column);
}
yield return new Token(TokenType.Command,
command,
line,
col);
}
else if (curChar == '(')
{
string exp = ScanExpression(iterator);
yield return new Token(TokenType.GodotExpression,
exp,
line,
col);
}
else if (curChar == '"')
{
yield return new Token(TokenType.String,
ScanString(iterator),
line,
col);
}
else
{
// parse this as expression
string exp = ScanUntilOrEOL(iterator, ' ');
yield return new Token(TokenType.GodotExpression,
exp,
line,
col);
}
/*
else if (curChar == '$')
{
yield return new Token(TokenType.NodePath,
ScanNodePath(iterator),
line,
col);
}
*/
}
yield return new Token(TokenType.End, "",
iterator.Line, iterator.Column);
}
}

View File

@ -0,0 +1,13 @@
namespace SupaLidlGame;
public class InterpreterException : System.Exception
{
public int Line { get; set; }
public int Column { get; set; }
public InterpreterException(string msg, int line, int column) : base(msg)
{
Line = line;
Column = column;
}
}

View File

@ -4,28 +4,6 @@ namespace SupaLidlGame.Extensions;
public static class NodeExtensions
{
/// <summary>
/// Iterates through each ancestor until it finds an ancestor of type
/// <c>T</c>
/// </summary>
[System.Obsolete]
public static T GetAncestorDeprecated<T>(this Node node) where T : Node
{
Node parent;
while ((parent = node.GetParent()) != null)
{
if (parent is T t)
{
return t;
}
node = parent;
}
return null;
}
/// <summary>
/// A version <c>GetNode</c> that returns null rather than cause an
/// exception if the node is not found or is not the same type.

View File

@ -29,6 +29,7 @@ stretch = true
stretch_shrink = 3
[node name="UIViewport" type="SubViewport" parent="SubViewportContainer"]
disable_3d = true
transparent_bg = true
handle_input_locally = false
size = Vector2i(640, 360)

View File

@ -1,4 +1,7 @@
[gd_scene format=3 uid="uid://be8bc4eivsg4s"]
[gd_scene load_steps=3 format=3 uid="uid://be8bc4eivsg4s"]
[ext_resource type="Script" path="res://Debug/DebugConsole.cs" id="1_3fw5a"]
[ext_resource type="Script" path="res://Debug/Entry.cs" id="2_kdlsh"]
[node name="DebugUI" type="Control"]
layout_mode = 3
@ -8,11 +11,47 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
[node name="Window" type="Window" parent="."]
disable_3d = true
gui_embed_subwindows = true
title = "Supa Developer Console"
position = Vector2i(32, 32)
size = Vector2i(1280, 720)
always_on_top = true
[node name="DebugConsole" type="Control" parent="Window"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 0
grow_vertical = 2
script = ExtResource("1_3fw5a")
[node name="VBoxContainer" type="VBoxContainer" parent="Window/DebugConsole"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Output" type="RichTextLabel" parent="Window/DebugConsole/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_font_sizes/normal_font_size = 24
theme_override_font_sizes/bold_font_size = 24
bbcode_enabled = true
text = "[b]/root/World:[/b] \\echo :CurrentPlayer:Health
100
"
scroll_following = true
[node name="Entry" type="LineEdit" parent="Window/DebugConsole/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 24
placeholder_text = "Enter a GDScript expression or \\command..."
draw_control_chars = true
script = ExtResource("2_kdlsh")

View File

@ -23,8 +23,8 @@ EventBus="*res://Events/EventBus.cs"
BaseUI="*res://UI/Base.tscn"
World="*res://Scenes/Level.tscn"
AudioManager="*res://Audio/AudioManager.cs"
DebugConsole="*res://Debug/DebugConsole.cs"
Panku="*res://addons/panku_console/console.tscn"
DebugUi="*res://UI/Debug/DebugUI.tscn"
[dialogue_manager]