Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Nodes, Resources, and TypedArrays to be exported #19

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ Some macros exist in reg.h that provide automated registration for classes, func
Here is how to use them:
* ``REG_CLASS()`` - Use this macro instead of GDCLASS().
* ``REG_FUNCTION()`` - Put this in front of your function.
* ``REG_PROPERTY()`` - Put this in front of your property. Supports ``PropertyInfo`` meta parameters.
* ``REG_PROPERTY()`` - Put this in front of your property. Supports ``PropertyInfo`` meta parameters. The property's type must be one of the following:
* [Variant types](https://docs.godotengine.org/en/stable/classes/index.html#variant-types)
* `Node` or its subclass: The property must be a pointer with a default value (for instance, `nullptr`).
* `Resource` or its subclass: the property must be wrapped by Godot's `Ref<T>` template.
* `TypedArray<T>`: `T` must be one of the three types above.
* ``REG_ENUM()`` - Put this in front of your enum.

There is an example class called GDExample that you can use for reference.
Registration-code will be injected into ``extension.cpp``. Class bindings will be generated based on ``reg_class.template``.

## Known issues / Future work
* The debugger does not attach automatically to the godot process. You can still attach manually.
* Multi-dimensional array properties cannot be exported. For now, we recommend registering functions that allow GDScript to interact with the multi-dimensional array instead.

## Notes
* Intellisense / Intellij will only work after first compile.
Expand Down
6 changes: 4 additions & 2 deletions source/RegAutomation/RegAutomation.Core.Tests/Test_Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ internal class Test_Parser : ParserBase
public void TestFindParams()
{
var result = FindParams("""
REG_TEST(REG_T_KEY_1 = 0, REG_T_KEY_2 = "hi!!!", REG_T_KEY_3 = (x, y = "1"))
REG_TEST(REG_T_KEY_1 = 0, REG_T_KEY_2 = "hi!!!", REG_T_KEY_3 = (x, y = "1"), REG_T_KEY_4 = "1,2,3,4")
""", 0);
Assert.That(result.Start, Is.EqualTo(9));
Assert.That(result.End, Is.EqualTo(75));
Assert.That(result.End, Is.EqualTo(100));
Assert.That(result.Content.Keys, Is.EquivalentTo(new string[]
{
"REG_T_KEY_1",
"REG_T_KEY_2",
"REG_T_KEY_3",
"REG_T_KEY_4",
}));
Assert.That(result.Content["REG_T_KEY_1"], Is.EqualTo("0"));
Assert.That(result.Content["REG_T_KEY_2"], Is.EqualTo("\"hi!!!\""));
Assert.That(result.Content["REG_T_KEY_3"], Is.EqualTo("(x, y = \"1\")"));
Assert.That(result.Content["REG_T_KEY_4"], Is.EqualTo("\"1,2,3,4\""));
}
[Test]
public void TestFindLineNumber()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,108 @@ public void TestParse()

float my_nonregistered_float = 1;

REG_PROPERTY(REG_P_Info=(PROPERTY_HINT_RANGE, "0,20,0.01"))
REG_PROPERTY(
REG_P_HintType=PROPERTY_HINT_RANGE,
REG_P_HintString="0,20,0.01",
REG_P_UsageFlags=PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
float my_float = 0;

REG_PROPERTY()
String my_string = "my_str";

REG_PROPERTY(
REG_P_HintType=PROPERTY_HINT_RANGE,
REG_P_HintString="0,1,0.001")
TypedArray<float> float_array;

REG_PROPERTY(REG_P_ExportAsResource)
Ref<Image> image_ref;

REG_PROPERTY(REG_P_ExportAsResource)
TypedArray<Image> image_array;

REG_PROPERTY(REG_P_ExportAsNode)
Node3D *node_pointer = nullptr;

REG_PROPERTY(REG_P_ExportAsNode)
TypedArray<Node3D> node_array;
""").ToArray();
Assert.That(result.Select(macro => macro.Name), Is.EquivalentTo(new string[]
{
"my_int",
"my_float",
"my_string",
"float_array",
"image_ref",
"image_array",
"node_pointer",
"node_array"
}));
Assert.That(result.Select(macro => macro.Type), Is.EquivalentTo(new string[]
{
"int",
"float",
"String",
"float",
"Image",
"Image",
"Node3D",
"Node3D",
}));
Assert.That(result.Select(macro => macro.ReferenceType), Is.EquivalentTo(new PropertyReferenceType[]
{
PropertyReferenceType.Value,
PropertyReferenceType.Value,
PropertyReferenceType.Value,
PropertyReferenceType.Value,
PropertyReferenceType.Ref,
PropertyReferenceType.Value,
PropertyReferenceType.Pointer,
PropertyReferenceType.Value,
}));
Assert.That(result.Select(macro => macro.ExportFlags), Is.EquivalentTo(new PropertyExportFlags[]
{
PropertyExportFlags.Variant,
PropertyExportFlags.Variant,
PropertyExportFlags.Variant,
PropertyExportFlags.Variant | PropertyExportFlags.Array,
PropertyExportFlags.Resource,
PropertyExportFlags.Resource | PropertyExportFlags.Array,
PropertyExportFlags.Node,
PropertyExportFlags.Node | PropertyExportFlags.Array,
}));
Assert.That(result.Select(macro => macro.HintType), Is.EquivalentTo(new string[]
{
"PROPERTY_HINT_NONE",
"PROPERTY_HINT_RANGE",
"PROPERTY_HINT_NONE",
"PROPERTY_HINT_RANGE",
"PROPERTY_HINT_NONE",
"PROPERTY_HINT_NONE",
"PROPERTY_HINT_NONE",
"PROPERTY_HINT_NONE",
}));
Assert.That(result.Select(macro => macro.HintString), Is.EquivalentTo(new string[]
{
"\"\"",
"\"0,20,0.01\"",
"\"\"",
"\"0,1,0.001\"",
"\"\"",
"\"\"",
"\"\"",
"\"\"",
}));
Assert.That(result.Select(macro => macro.Meta), Is.EquivalentTo(new string[]
Assert.That(result.Select(macro => macro.UsageFlags), Is.EquivalentTo(new string[]
{
string.Empty,
"PROPERTY_HINT_RANGE, \"0,20,0.01\"",
string.Empty,
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY",
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_DEFAULT",
"PROPERTY_USAGE_DEFAULT",
}));
}
}
Expand Down
40 changes: 33 additions & 7 deletions source/RegAutomation/RegAutomation.Core/ParserBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,35 @@ protected static Params FindParams(string content, int startIndex)
Dictionary<string, string> properties = new Dictionary<string, string>();
int level = 0;
List<int> propertySeparatorIndices = new List<int>();
List<int> equalOperatorIndices = new List<int>();
bool isParsingString = false;
for(int i = startIndex; i < content.Length; i++)
{
if(level > 0)
// Handle strings here.
if(content[i] == '"')
{
if (level == 1 && content[i] == ',')
isParsingString = !isParsingString;
continue;
}
else if(i < content.Length - 1 && content[i] == '\\' && content[i + 1] == '"')
{
i++; // Skip the escaped double-quotes.
continue;
}
else if(isParsingString)
{
continue; // Passing through string literal, so don't check for separators until another double-quote is found.
}
if(level == 1)
{
if (content[i] == ',')
{
propertySeparatorIndices.Add(i);
}
else if (content[i] == '=')
{
equalOperatorIndices.Add(i);
}
}
if (content[i] == '(')
{
Expand All @@ -72,19 +93,24 @@ protected static Params FindParams(string content, int startIndex)
throw new Exception("Opening parenthesis not found.");
if(level != 0)
throw new Exception("Closing parenthesis not found.");
int equalOperatorIndexPointer = 0;
for(int i = 0; i < propertySeparatorIndices.Count - 1; i++)
{
int start = propertySeparatorIndices[i] + 1; // Skip the '(' or ','
int end = propertySeparatorIndices[i + 1]; // Skip the ')' or ','
string property = content.Substring(start, end - start).Trim();
int equalOperatorIndex = property.IndexOf('=');
if(equalOperatorIndex != -1)
while(equalOperatorIndexPointer < equalOperatorIndices.Count && equalOperatorIndices[equalOperatorIndexPointer] <= start)
{
equalOperatorIndexPointer++;
}
if(equalOperatorIndexPointer < equalOperatorIndices.Count && equalOperatorIndices[equalOperatorIndexPointer] < end)
{
properties[property.Substring(0, equalOperatorIndex).Trim()] = property.Substring(equalOperatorIndex + 1).Trim();
int equalOperatorIndex = equalOperatorIndices[equalOperatorIndexPointer];
properties[content[start..equalOperatorIndex].Trim()] = content[(equalOperatorIndex + 1)..end].Trim();
equalOperatorIndexPointer++;
}
else
{
properties[property.Trim()] = "";
properties[content[start..end].Trim()] = "";
}
}

Expand Down
147 changes: 147 additions & 0 deletions source/RegAutomation/RegAutomation.Core/PropertyBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System.Text;
using System.Text.RegularExpressions;

namespace RegAutomation.Core
{
public static class PropertyBinder
{
public static (string propertyBindings, string functionBindings, string functionInject) GenerateBindings(
string className,
string propertyName,
string propertyType,
PropertyReferenceType propertyReferenceType,
PropertyExportFlags propertyExportFlags,
string propertyHintType,
string propertyHintString,
string propertyUsageFlags)
{
// Figure out the variant type of the property first.
// Note that even for TypedArrays, we use the same code path to figure out the element type here as well.
string variantType;
if(CppTypeToVariantType.ContainsKey(propertyType))
{
if(propertyReferenceType is PropertyReferenceType.Pointer)
throw new NotSupportedException("Pointer to Variant type cannot be registered!");
variantType = CppTypeToVariantType[propertyType];
}
else if(propertyExportFlags.HasFlag(PropertyExportFlags.Node))
{
variantType = "OBJECT";
propertyHintType = "PROPERTY_HINT_NODE_TYPE";
propertyHintString = $"\"{propertyType}\"";
}
else if(propertyExportFlags.HasFlag(PropertyExportFlags.Resource))
{
variantType = "OBJECT";
propertyHintType = "PROPERTY_HINT_RESOURCE_TYPE";
propertyHintString = $"\"{propertyType}\"";
}
else
{
throw new NotSupportedException($"Unable to map {propertyType} to a GDScript-accessible type. Please check if it is a Variant type or a registered class that is a subclass of Node or Resource.");
}

// For TypedArrays, move the element type's info inside the hint string, and add Array property info.
if (propertyExportFlags.HasFlag(PropertyExportFlags.Array))
{
propertyHintString = $"""vformat("%s/%s:%s", Variant::{variantType}, {propertyHintType}, {propertyHintString})""";
propertyHintType = "PROPERTY_HINT_ARRAY_TYPE";
propertyType = $"TypedArray<{propertyType}>";
variantType = "ARRAY";
}

// Combining the information above gives us the property info.
string propertyInfo = $"Variant::{variantType}, \"{propertyName}\", {propertyHintType}, {propertyHintString}, {propertyUsageFlags}";

var (getter, setter) = GetGetterSetter(variantType, propertyName);
// With the getter and setter name decided, and the property info computed, we can construct the binding code.
string addPropertyStatement = $"ClassDB::add_property(\"{className}\", PropertyInfo({propertyInfo}), \"{setter}\", \"{getter}\");\n\t";
string bindGetterStatement = $"ClassDB::bind_method(D_METHOD(\"{getter}\"), &{className}::_gen_{getter});\n\t";
string bindSetterStatement = $"ClassDB::bind_method(D_METHOD(\"{setter}\", \"p\"), &{className}::_gen_{setter});\n\t";

// For Ref<T>s, re-wrap the property type with Ref to match with C++ definition.
if(propertyReferenceType is PropertyReferenceType.Ref)
propertyType = $"Ref<{propertyType}> ";
// For pointers, we insert the pointer asterisk back into the function declarations.
else if (propertyReferenceType is PropertyReferenceType.Pointer)
propertyType = $"{propertyType} *";
else
propertyType = $"{propertyType} ";

// Finally, construct the getters and setters.
string genGetterDeclaration = $"\t{propertyType}_gen_{getter}() const {{ return {propertyName}; }}\n";
string genSetterDeclaration = $"\tvoid _gen_{setter}({propertyType}p) {{ {propertyName} = p; }}\n";

return new (
addPropertyStatement,
bindGetterStatement + bindSetterStatement,
genGetterDeclaration + genSetterDeclaration);
}
private static (string get, string set) GetGetterSetter(string variant, string property)
{
switch (variant)
{
case "BOOL":
{
if (property.StartsWith("is_"))
property = property[3..];
return ("is_" + property, "set_" + property);
}
}
return ("get_" + property, "set_" + property);
}
// Rather than coming up with a hacky solution based on naming patterns that have no guarantees,
// we just make the mapping explicit so it's easier to maintain.
// This also lets us check if a Cpp type can be converted into a variant.
// See also: list of variant types (https://docs.godotengine.org/en/stable/classes/index.html#variant-types).
private static readonly Dictionary<string, string> CppTypeToVariantType = new()
{
["AABB"] = "AABB",
["Array"] = "ARRAY",
["Basis"] = "BASIS",
["bool"] = "BOOL",
["Callable"] = "CALLABLE",
["Color"] = "COLOR",
["Dictionary"] = "DICTIONARY",

// Aliases for float.
["float"] = "FLOAT",
["real_t"] = "FLOAT",
["double"] = "FLOAT",

// Aliases for int.
["int"] = "INT",
["int32_t"] = "INT",
["int64_t"] = "INT",

["NodePath"] = "NODE_PATH",
["Object"] = "OBJECT",
["PackedByteArray"] = "PACKED_BYTE_ARRAY",
["PackedColorArray"] = "PACKED_COLOR_ARRAY",
["PackedFloat32Array"] = "PACKED_FLOAT32_ARRAY",
["PackedFloat64Array"] = "PACKED_FLOAT64_ARRAY",
["PackedInt32Array"] = "PACKED_INT32_ARRAY",
["PackedInt64Array"] = "PACKED_INT64_ARRAY",
["PackedStringArray"] = "PACKED_STRING_ARRAY",
["PackedVector2Array"] = "PACKED_VECTOR2_ARRAY",
["PackedVector3Array"] = "PACKED_VECTOR3_ARRAY",
["Plane"] = "PLANE",
["Projection"] = "PROJECTION",
["Quaternion"] = "QUATERNION",
["Rect2"] = "RECT2",
["Rect2i"] = "RECT2I",
["RID"] = "RID",
["Signal"] = "SIGNAL",
["String"] = "STRING",
["StringName"] = "STRING_NAME",
["Transform2D"] = "TRANSFORM2D",
["Transform3D"] = "TRANSFORM3D",
["Vector2"] = "VECTOR2",
["Vector2i"] = "VECTOR2I",
["Vector3"] = "VECTOR3",
["Vector3i"] = "VECTOR3I",
["Vector4"] = "VECTOR4",
["Vector4i"] = "VECTOR4I",
};
}
}
Loading