Making inst_to_dict and dict_to_inst recursive

Fixes #6533

Making GDScript inst_to_dict/dict_to_inst utility functions recursive.
Adding also a new macro to validate the number of the required arguments
and another to validate that an argument is boolean.
This commit is contained in:
Pablo Andres Fuente 2024-09-16 11:48:04 -03:00
parent 04692d83cb
commit f1f96e9023
10 changed files with 224 additions and 101 deletions

View file

@ -77,8 +77,10 @@
<method name="dict_to_inst">
<return type="Object" />
<param index="0" name="dictionary" type="Dictionary" />
<param index="1" name="deep" type="bool" default="false" />
<description>
Converts a [param dictionary] (created with [method inst_to_dict]) back to an Object instance. Can be useful for deserializing.
If [param deep] is [code]true[/code], the method converts any inner instances recursively. Use this option only if you're sure that instances have no circular references to each other as it can lead to an endless conversion loop.
</description>
</method>
<method name="get_stack">
@ -106,8 +108,10 @@
<method name="inst_to_dict">
<return type="Dictionary" />
<param index="0" name="instance" type="Object" />
<param index="1" name="deep" type="bool" default="false" />
<description>
Returns the passed [param instance] converted to a Dictionary. Can be useful for serializing.
If [param deep] is [code]true[/code], the method converts any inner instances recursively. Use this option only if you're sure that instances have no circular references to each other as it can lead to an endless conversion loop.
[b]Note:[/b] Cannot be used to serialize objects with built-in scripts attached or objects allocated within built-in scripts.
[codeblock]
var foo = "bar"

View file

@ -56,6 +56,20 @@
return; \
}
#define VALIDATE_MIN_MAX_ARG_COUNT(m_min_count, m_max_count) \
if (p_arg_count < m_min_count) { \
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; \
r_error.expected = m_min_count; \
*r_ret = Variant(); \
return; \
} \
if (p_arg_count > m_max_count) { \
r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; \
r_error.expected = m_max_count; \
*r_ret = Variant(); \
return; \
}
#define VALIDATE_ARG_INT(m_arg) \
if (p_args[m_arg]->get_type() != Variant::INT) { \
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; \
@ -74,11 +88,22 @@
return; \
}
#define VALIDATE_ARG_BOOL(m_arg) \
if (p_args[m_arg]->get_type() != Variant::BOOL) { \
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; \
r_error.argument = m_arg; \
r_error.expected = Variant::BOOL; \
*r_ret = Variant(); \
return; \
}
#else
#define VALIDATE_ARG_COUNT(m_count)
#define VALIDATE_MIN_MAX_ARG_COUNT(m_min_count, m_max_count)
#define VALIDATE_ARG_INT(m_arg)
#define VALIDATE_ARG_NUM(m_arg)
#define VALIDATE_ARG_BOOL(m_arg)
#endif
@ -242,96 +267,108 @@ struct GDScriptUtilityFunctionsDefinitions {
}
}
static inline void inst_to_dict(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) {
VALIDATE_ARG_COUNT(1);
static inline Variant _inst_to_dict(const Variant *p_var, bool p_deep, int p_recursion_count, Callable::CallError &r_error) {
if (p_recursion_count > MAX_RECURSION) {
ERR_PRINT("Max recursion reached");
return Variant();
}
if (p_args[0]->get_type() == Variant::NIL) {
*r_ret = Variant();
} else if (p_args[0]->get_type() != Variant::OBJECT) {
if (p_var->get_type() == Variant::NIL) {
return Variant();
} else if (p_var->get_type() != Variant::OBJECT) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::OBJECT;
*r_ret = Variant();
} else {
Object *obj = *p_args[0];
if (!obj) {
*r_ret = Variant();
} else if (!obj->get_script_instance() || obj->get_script_instance()->get_language() != GDScriptLanguage::get_singleton()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
*r_ret = RTR("Not a script with an instance");
return;
} else {
GDScriptInstance *ins = static_cast<GDScriptInstance *>(obj->get_script_instance());
Ref<GDScript> base = ins->get_script();
if (base.is_null()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
*r_ret = RTR("Not based on a script");
return;
}
GDScript *p = base.ptr();
String path = p->get_script_path();
Vector<StringName> sname;
while (p->_owner) {
sname.push_back(p->local_name);
p = p->_owner;
}
sname.reverse();
if (!path.is_resource_file()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
*r_ret = Variant();
*r_ret = RTR("Not based on a resource file");
return;
}
NodePath cp(sname, Vector<StringName>(), false);
Dictionary d;
d["@subpath"] = cp;
d["@path"] = path;
for (const KeyValue<StringName, GDScript::MemberInfo> &E : base->member_indices) {
if (!d.has(E.key)) {
d[E.key] = ins->members[E.value.index];
}
}
*r_ret = d;
}
return Variant();
}
}
static inline void dict_to_inst(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) {
VALIDATE_ARG_COUNT(1);
if (p_args[0]->get_type() != Variant::DICTIONARY) {
Object *obj = *p_var;
if (!obj) {
return Variant();
} else if (!obj->get_script_instance() || obj->get_script_instance()->get_language() != GDScriptLanguage::get_singleton()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
*r_ret = Variant();
return;
return RTR("Not a script with an instance");
}
Dictionary d = *p_args[0];
GDScriptInstance *ins = static_cast<GDScriptInstance *>(obj->get_script_instance());
Ref<GDScript> base = ins->get_script();
if (base.is_null()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
return RTR("Not based on a script");
}
GDScript *p = base.ptr();
String path = p->get_script_path();
Vector<StringName> sname;
while (p->_owner) {
sname.push_back(p->local_name);
p = p->_owner;
}
sname.reverse();
if (!path.is_resource_file()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
return RTR("Not based on a resource file");
}
NodePath cp(sname, Vector<StringName>(), false);
Dictionary d;
d["@subpath"] = cp;
d["@path"] = path;
for (const KeyValue<StringName, GDScript::MemberInfo> &E : base->member_indices) {
if (!d.has(E.key)) {
Variant member = ins->members[E.value.index];
if (p_deep && member.get_type() == Variant::OBJECT) {
member = _inst_to_dict(&member, p_deep, ++p_recursion_count, r_error);
}
d[E.key] = member;
}
}
return d;
}
static inline void inst_to_dict(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) {
VALIDATE_MIN_MAX_ARG_COUNT(1, 2);
bool deep = false;
if (p_arg_count > 1) {
VALIDATE_ARG_BOOL(1);
deep = *p_args[1];
}
*r_ret = _inst_to_dict(p_args[0], deep, 1, r_error);
}
static inline Variant _dict_to_inst(const Variant *p_var, bool p_deep, int p_recursion_count, Callable::CallError &r_error) {
if (p_recursion_count > MAX_RECURSION) {
ERR_PRINT("Max recursion reached");
return Variant();
}
if (p_var->get_type() != Variant::DICTIONARY) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::DICTIONARY;
return Variant();
}
Dictionary d = *p_var;
if (!d.has("@path")) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::OBJECT;
*r_ret = RTR("Invalid instance dictionary format (missing @path)");
return;
return RTR("Invalid instance dictionary format (missing @path)");
}
Ref<Script> scr = ResourceLoader::load(d["@path"]);
@ -339,8 +376,7 @@ struct GDScriptUtilityFunctionsDefinitions {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::OBJECT;
*r_ret = RTR("Invalid instance dictionary format (can't load script at @path)");
return;
return RTR("Invalid instance dictionary format (can't load script at @path)");
}
Ref<GDScript> gdscr = scr;
@ -349,9 +385,7 @@ struct GDScriptUtilityFunctionsDefinitions {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::OBJECT;
*r_ret = Variant();
*r_ret = RTR("Invalid instance dictionary format (invalid script at @path)");
return;
return RTR("Invalid instance dictionary format (invalid script at @path)");
}
NodePath sub;
@ -365,41 +399,45 @@ struct GDScriptUtilityFunctionsDefinitions {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::OBJECT;
*r_ret = Variant();
*r_ret = RTR("Invalid instance dictionary (invalid subclasses)");
return;
return RTR("Invalid instance dictionary (invalid subclasses)");
}
}
*r_ret = gdscr->_new(nullptr, -1 /*skip initializer*/, r_error);
Variant ret = gdscr->_new(nullptr, -1 /*skip initializer*/, r_error);
if (r_error.error != Callable::CallError::CALL_OK) {
*r_ret = RTR("Cannot instantiate GDScript class.");
return;
return RTR("Cannot instantiate GDScript class.");
}
GDScriptInstance *ins = static_cast<GDScriptInstance *>(static_cast<Object *>(*r_ret)->get_script_instance());
GDScriptInstance *ins = static_cast<GDScriptInstance *>(static_cast<Object *>(ret)->get_script_instance());
Ref<GDScript> gd_ref = ins->get_script();
for (KeyValue<StringName, GDScript::MemberInfo> &E : gd_ref->member_indices) {
if (d.has(E.key)) {
ins->members.write[E.value.index] = d[E.key];
Variant member = d[E.key];
if (p_deep && member.get_type() == Variant::DICTIONARY) {
member = _dict_to_inst(&member, p_deep, ++p_recursion_count, r_error);
}
ins->members.write[E.value.index] = member;
}
}
return ret;
}
static inline void dict_to_inst(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) {
VALIDATE_MIN_MAX_ARG_COUNT(1, 2);
bool deep = false;
if (p_arg_count > 1) {
VALIDATE_ARG_BOOL(1);
deep = *p_args[1];
}
*r_ret = _dict_to_inst(p_args[0], deep, 1, r_error);
}
static inline void Color8(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) {
if (p_arg_count < 3) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error.expected = 3;
*r_ret = Variant();
return;
}
if (p_arg_count > 4) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
r_error.expected = 4;
*r_ret = Variant();
return;
}
VALIDATE_MIN_MAX_ARG_COUNT(3, 4);
VALIDATE_ARG_INT(0);
VALIDATE_ARG_INT(1);
@ -717,8 +755,8 @@ void GDScriptUtilityFunctions::register_functions() {
REGISTER_FUNC(_char, true, Variant::STRING, ARG("char", Variant::INT));
REGISTER_VARARG_FUNC(range, false, Variant::ARRAY);
REGISTER_CLASS_FUNC(load, false, "Resource", ARG("path", Variant::STRING));
REGISTER_FUNC(inst_to_dict, false, Variant::DICTIONARY, ARG("instance", Variant::OBJECT));
REGISTER_FUNC(dict_to_inst, false, Variant::OBJECT, ARG("dictionary", Variant::DICTIONARY));
REGISTER_FUNC_DEF(inst_to_dict, false, false, Variant::DICTIONARY, ARG("instance", Variant::OBJECT), ARG("deep", Variant::BOOL));
REGISTER_FUNC_DEF(dict_to_inst, false, false, Variant::OBJECT, ARG("dictionary", Variant::DICTIONARY), ARG("deep", Variant::BOOL));
REGISTER_FUNC_DEF(Color8, true, 255, Variant::COLOR, ARG("r8", Variant::INT), ARG("g8", Variant::INT), ARG("b8", Variant::INT), ARG("a8", Variant::INT));
REGISTER_VARARG_FUNC(print_debug, false, Variant::NIL);
REGISTER_FUNC_NO_ARGS(print_stack, false, Variant::NIL);

View file

@ -0,0 +1,9 @@
extends Resource
@export var text: String
@export var qux: Resource
func _init(p_text = "", p_qux = null):
text = p_text
qux = p_qux

View file

@ -0,0 +1,9 @@
[gd_resource type="Resource" load_steps=2 format=3 uid="uid://cqd4xsl30rr7b"]
[ext_resource type="Script" path="./bar.notest.gd" id="1_bar"]
[ext_resource type="Script" path="./qux.tres" id="1_qux"]
[resource]
script = ExtResource("1_bar")
text = "lorem ipsum"
qux = ExtResource("1_qux")

View file

@ -0,0 +1,9 @@
extends Resource
@export var number: int
@export var bar: Resource
func _init(p_number = 0, p_bar = null):
number = p_number
bar = p_bar

View file

@ -0,0 +1,9 @@
[gd_resource type="Resource" load_steps=2 format=3 uid="uid://cqd4xsl30rr7a"]
[ext_resource type="Script" path="./foo.notest.gd" id="1_foo"]
[ext_resource type="Script" path="./bar.tres" id="1_bar"]
[resource]
script = ExtResource("1_foo")
number = 42
bar = ExtResource("1_bar")

View file

@ -0,0 +1,27 @@
extends Resource
var foo = preload("./foo.tres")
func test():
var obj:Object = foo
var dict:Dictionary = inst_to_dict(obj)
var dict_ok = true
dict_ok = dict_ok && dict.get("number") == 42
dict_ok = dict_ok && dict.get("bar") is Resource
if not dict_ok:
printerr("Can't convert instance to dictionary properly")
dict = inst_to_dict(obj, true)
print(dict.keys())
print(dict.values())
var inst = dict_to_inst(dict, true)
var equals = true
equals = equals && foo.number == inst.number
equals = equals && foo.bar.text == inst.bar.text
equals = equals && foo.bar.qux.decimal == inst.bar.qux.decimal
if not equals:
printerr("Can't revert from instance to dictionary properly")
print('ok')

View file

@ -0,0 +1,4 @@
GDTEST_OK
["@subpath", "@path", &"number", &"bar"]
[^"", "res://utility_functions/foo.notest.gd", 42, { "@subpath": ^"", "@path": "res://utility_functions/bar.notest.gd", &"text": "lorem ipsum", &"qux": { "@subpath": ^"", "@path": "res://utility_functions/qux.notest.gd", &"decimal": 0.5 } }]
ok

View file

@ -0,0 +1,7 @@
extends Resource
@export var decimal: float
func _init(p_decimal = ""):
decimal = p_decimal

View file

@ -0,0 +1,7 @@
[gd_resource type="Resource" load_steps=2 format=3 uid="uid://cqd4xsl30rr7c"]
[ext_resource type="Script" path="./qux.notest.gd" id="1_qux"]
[resource]
script = ExtResource("1_qux")
decimal = 0.5