Skip to content

Commit

Permalink
Allow Nodes, Resources, and TypedArrays to be exported
Browse files Browse the repository at this point in the history
  • Loading branch information
chocola-mint committed Nov 15, 2023
1 parent 342dec8 commit 7d33bd3
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 71 deletions.
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 above types.
* ``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
150 changes: 150 additions & 0 deletions source/RegAutomation/RegAutomation.Core/PropertyBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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.
// We can't put this on propertyType because the asterisk is placed right next to the type for the getter,
// but right next to the parameter for the setter.
string pointerSymbol;
if (propertyReferenceType is PropertyReferenceType.Pointer)
pointerSymbol = "*";
else
pointerSymbol = "";

// Finally, construct the getters and setters.
string genGetterDeclaration = $"\t{propertyType}{pointerSymbol} _gen_{getter}() const {{ return {propertyName}; }}\n";
string genSetterDeclaration = $"\tvoid _gen_{setter}({propertyType} {pointerSymbol}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

0 comments on commit 7d33bd3

Please sign in to comment.