1 module godot.tools.generator.methods;
2 
3 import godot.util.string;
4 import godot.tools.generator.classes;
5 import godot.tools.generator.enums;
6 import godot.tools.generator.util;
7 
8 import asdf;
9 
10 import std.range;
11 import std.algorithm.searching;
12 import std.algorithm.iteration;
13 import std.path;
14 import std.conv : text;
15 import std.string;
16 
17 import std.typecons;
18 
19 class GodotMethod {
20     @serdeOptional
21     string name; // constructors doesn't have name
22     @serdeOptional @serdeKeys("return_type", "return_value")
23     Type return_type;
24     @serdeOptional
25     bool is_editor;
26     @serdeOptional
27     bool is_noscript;
28     @serdeOptional
29     bool is_const;
30     @serdeOptional
31     bool is_virtual;
32     @serdeOptional
33     bool is_static;
34     @serdeOptional
35     string category;
36     @serdeOptional @serdeKeys("is_vararg", "has_varargs")
37     bool has_varargs;
38     @serdeOptional
39     bool is_from_script;
40     @serdeOptional
41     uint hash;
42     @serdeOptional
43     GodotArgument[] arguments;
44 
45     void finalizeDeserialization(Asdf data) {
46         // FIXME: why data is here if it's not used?
47         // Superbelko: Because this is post-serialize event and we only doing some adjustments here
48         if (!return_type)
49             return_type = Type.get("void");
50         foreach (i, ref arg; arguments) {
51             arg.index = i;
52             arg.parent = this;
53         }
54     }
55 
56 @serdeIgnore:
57     GodotClass parent;
58 
59     // indicates that this method is a helper methods that simply 
60     // redirectes all arguments to the specidied method, for example string types helpers
61     GodotMethod redirectsTo; 
62 
63     string ddoc;
64 
65     Constructor isConstructor() const {
66         return null;
67     }
68     // Operator isOperator() { return null; }
69     // Indexer isIndexer() { return null; }
70 
71     bool same(in GodotMethod other) const {
72         return name == other.name && is_const == other.is_const;
73     }
74 
75     bool needsStringHelpers() const {
76         static bool anyString (GodotArgument a) { 
77             return a.type.stripConst.isGodotStringType; 
78         }
79         // const all the way...
80         return (cast(GodotArgument[]) arguments).canFind!anyString();
81     }
82 
83     string templateArgsString() const {
84         string ret = "";
85         bool first = true; // track first arg to skip comma
86         foreach (i, ref arg; arguments) {
87             if (arg.type.acceptImplicit) {
88                 if (first)
89                     first = false;
90                 else
91                     ret ~= ", ";
92                 ret ~= text(arg.type.godotType, "Arg", i);
93             }
94         }
95         if (has_varargs) {
96             if (!first)
97                 ret ~= ", ";
98             ret ~= "VarArgs...";
99         }
100         // template parens only if it actually is a template
101         if (ret.length != 0)
102             ret = text("(", ret, ")");
103         return ret;
104     }
105 
106     string argsString() const {
107         string ret = "(";
108 
109         foreach (i, ref arg; arguments) {
110             // FIXME: do it prettier
111             string typeString = "";
112 
113             if (i != 0) ret ~= ", ";
114             if (arg.type.acceptImplicit) {
115                 ret ~= text(arg.type.dCallParamPrefix, arg.type.godotType, "Arg", i);
116                 typeString = text(arg.type.godotType, "Arg", i);
117             } else {
118                 ret ~= text(arg.type.dCallParamPrefix, arg.type.dType);
119                 typeString = arg.type.dType;
120             }
121 
122             ret ~= " " ~ arg.name.escapeDType;
123 
124             // HACK: look at GodotArgument
125             // FIXME: Causes forward reference
126             if (arg.default_value != "\0") {
127                 if (arg.type.isBitfield || arg.type.isEnum) {
128                     ret ~= " = cast(" ~ typeString ~ ") " ~ escapeDefaultType(arg.type, arg.default_value);
129                 } else {
130                     // This probably should be in StringHelper method class
131                     if (redirectsTo && redirectsTo.arguments[i].type.isGodotStringType) {
132                         ret ~= " = " ~ stripStringDefaultValueType(redirectsTo.arguments[i].type, arg.default_value);
133                     }
134                     else {
135                         ret ~= " = " ~ escapeDefaultType(arg.type, arg.default_value);
136                     }
137                 }
138             }
139         }
140         if (has_varargs) {
141             if (arguments.length != 0)
142                 ret ~= ", ";
143             ret ~= "VarArgs varArgs";
144         }
145         ret ~= ")";
146         return ret;
147     }
148 
149     /++
150 	Outputs binding method declaration with meta information. 
151 	e.g.:
152 
153 		@GodotName("insert") @MethodHash(0) GodotMethod!(long, long, Variant) method_insert;
154 	+/
155     string binding() const {
156         string ret;
157 
158         ret ~= "\t\t@GodotName(\"" ~ name ~ "\") GodotMethod!(" ~ return_type.dType;
159         foreach (ai, const arg; arguments) {
160             ret ~= ", " ~ arg.type.dType;
161         }
162         if (has_varargs)
163             ret ~= ", GodotVarArgs";
164         ret ~= ") " ~ wrapperIdentifier ~ ";\n";
165 
166         return ret;
167     }
168 
169     /// Function pointer name for this method
170     /// 	"constructor_new_0", "method_normalize", ...
171     string wrapperIdentifier() const {
172         return functionKindName ~ "_" ~ name.snakeToCamel.escapeDType;
173     }
174 
175     /// Function type name used in some cases: like "method", "ctor", "getter", etc...
176     string functionKindName() const {
177         return "method";
178     }
179 
180     /++ 
181 	Formats whole method including function signature and body with implementation.
182 	e.g.:
183     ```d
184     string getSlice(in string delimiter, in long slice) const {
185 		if (!GDExtensionClassBinding.method_getSlice.mb)
186 			GDExtensionClassBinding.method_getSlice.mb = _godot_api.variant_get_ptr_builtin_method(GDEXTENSION_VARIANT_TYPE_STRING, "get_slice", 3535100402);
187 		return toDString(callBuiltinMethod!(String)(cast(GDExtensionPtrBuiltInMethod) GDExtensionClassBinding.method_getSlice.mb, cast(void*) &_godot_object, cast() toGodotString(delimiter), cast() slice));
188     }
189     ```
190 	+/
191     string source() const {
192         string ret;
193 
194         // ddoc comment (if any)
195         ret ~= "\t/**\n\t" ~ ddoc.replace("\n", "\n\t") ~ "\n\t*/\n";
196         ret ~= "\t";
197 
198         ret ~= signature();
199 
200         ret ~= " {\n";
201 
202         ret ~= body_();
203 
204         ret ~= "\t}\n";
205 
206         if (needsStringHelpers && !isConstructor()) {
207             ret ~= "\n";
208 
209             // copy-paste a method
210             auto m = new StringHelperGodotMethod(); {
211                 m.name = this.name;
212                 m.is_editor = this.is_editor;
213                 m.is_noscript = this.is_noscript;
214                 m.is_const = this.is_const;
215                 m.is_virtual = this.is_virtual;
216                 m.is_static = this.is_static;
217                 m.category = this.category;
218                 m.has_varargs = this.has_varargs;
219                 m.is_from_script = this.is_from_script;
220                 m.hash = this.hash;
221                 m.parent = cast() this.parent;
222                 m.return_type = cast() this.return_type;
223                 m.redirectsTo = cast() this;
224             }
225             GodotArgument[] newargs;
226             foreach(a; arguments) {
227                 // replace string types with plain D string
228                 if (a.type.isGodotStringType)
229                     newargs ~= GodotArgument(a.name, Type.get("string"), a.default_value, a.index, m);
230                 else
231                     newargs ~= GodotArgument(a.name, cast() a.type, a.default_value, a.index, m);
232             }
233             m.arguments = newargs;
234             ret ~= m.source();
235         }
236 
237         return ret;
238     }
239 
240     /// Formats function signature, e.g.
241     ///   Array slice(in long begin, in long end, in long step, in bool deep) const
242     string signature() const {
243         string ret;
244 
245         // optional static modifier
246         if (isConstructor) {
247             ret ~= "static ";
248         }
249         // note that even though it strips constness of return type the method is still marked const
250         // const in D is transitive, which means compiler should disallow modifying returned reference types
251         ret ~= return_type.stripConst.dRef ~ " ";
252         // none of the types (Classes/Core/Primitive) are pointers in D
253         // Classes are reference types; the others are passed by value.
254         ret ~= name.snakeToCamel.escapeDType;
255 
256         ret ~= templateArgsString;
257         ret ~= argsString;
258 
259         // function const attribute
260         if (is_const)
261             ret ~= " const";
262         else if (name == "callv" && parent.name.godotType == "Object")
263             ret ~= " const"; /// HACK
264 
265         return ret;
266     }
267 
268     /// Formats body containing implementation, omitting outer braces
269     string body_() const {
270         string ret;
271 
272         // load function pointer
273         ret ~= "\t\tif (!GDExtensionClassBinding." ~ wrapperIdentifier ~ ".mb) {\n";
274         // tab() will indent it correctly starting from first element
275         ret ~= loader().split('\n').map!(s => s.tab(3)).join('\n') ~ "\n";
276         ret ~= "\t\t}\n";
277 
278         if (is_virtual || has_varargs) {
279             // keep it like this for now, serves as example.
280             // function will put normal arguments first, then varargs
281             // next, in order to call that function we need actually array of pointers
282             // after that we call the function with array of pointers instead of plain args array
283             version (none)
284                 if (name == "emit_signal") {
285                     // two tabs
286                     ret ~= `		Variant[varArgs.length+2] _GODOT_args;
287 	_GODOT_args[0] = String("emit_signal");
288 	_GODOT_args[1] = signal;
289 	foreach(vai, VA; VarArgs) {
290 		_GODOT_args[vai+2] = Variant(varArgs[vai]);
291 	}
292 	Variant*[varArgs.length+2] _args;
293 	foreach(i; 0.._GODOT_args.length) {
294 		_args[i] = &_GODOT_args[i];
295 	}
296 	Variant ret;
297 	GDExtensionCallError err;
298 	_godot_api.object_method_bind_call(GDExtensionClassBinding.method_emitSignal.mb, _godot_object.ptr, cast(void**) _args.ptr, _GODOT_args.length, cast(void*) &ret, &err);
299 	debug if (int code = ret.as!int()) {
300 		import godot.api;
301 		print("signal error: ", signal, " code: ", code);
302 	}
303 	return cast(GodotError) err.error;`;
304                 }
305 
306             // static array must have at least 1 element
307             import std.algorithm : max;
308 
309             int argsLength = max(1, (cast(int) arguments.length));
310             // choose between varargs and regular function for arguments
311             if (has_varargs) {
312                 ret ~= "\t\tVariant[varArgs.length+" ~ text(argsLength) ~ "] _GODOT_args;\n";
313                 ret ~= "\t\tVariant*[varArgs.length+" ~ text(argsLength) ~ "] _args;\n";
314             } else {
315                 ret ~= "\t\tVariant[" ~ text(argsLength) ~ "] _GODOT_args;\n";
316                 ret ~= "\t\tVariant*[" ~ text(argsLength) ~ "] _args;\n";
317 
318             }
319             foreach (i, const arg; arguments) {
320                 // gathers normal parameters in variant array to be later used as pointers
321                 ret ~= "\t\t_GODOT_args[" ~ text(cast(int) i) ~ "] = " ~ escapeDType(arg.name) ~ ";\n";
322             }
323 
324             if (has_varargs) {
325                 // copy varargs after regular args
326                 ret ~= "\t\tforeach(vai, VA; VarArgs) {\n";
327                 ret ~= "\t\t\t_GODOT_args[vai+" ~ text(
328                     cast(int) arguments.length) ~ "] = Variant(varArgs[vai]);\n";
329                 ret ~= "\t\t}\n";
330             }
331 
332             // make pointer array
333             ret ~= "\t\tforeach(i; 0.._GODOT_args.length) {\n";
334             ret ~= "\t\t\t_args[i] = &_GODOT_args[i];\n";
335             ret ~= "\t\t}\n";
336 
337             //ret ~= "\t\tStringName _GODOT_method_name = StringName(\""~name~"\");\n";
338 
339             ret ~= "\t\tVariant ret;\n";
340             ret ~= "\t\tGDExtensionCallError err;\n";
341 
342             // there is subtle difference, we pass &godot_object for builtins and .ptr for any other object
343             // but normally they should work just with &godot_object, we had issues with that in the past though
344             // so here we now use real ptr value instead
345             if (parent.isBuiltinClass)
346                 ret ~= "\t\t_godot_api.object_method_bind_call(GDExtensionClassBinding." ~ wrapperIdentifier ~ ".mb, cast(void*) &_godot_object, cast(void**) _args.ptr, _GODOT_args.length, cast(void*) &ret, &err);\n";
347             else
348                 ret ~= "\t\t_godot_api.object_method_bind_call(GDExtensionClassBinding." ~ wrapperIdentifier ~ ".mb, cast(void*) _godot_object.ptr, cast(void**) _args.ptr, _GODOT_args.length, cast(void*) &ret, &err);\n";
349             // ret ~= "\t\t";
350             // DMD 2.101 complains about Type* pointers escaping function scope
351             // So instead of returning it directly make a temporary pointer variable
352             if (return_type.dType != "void") {
353                 if (return_type.isPointerType) {
354                     ret ~= "\t\tauto r = ";
355                 }
356                 else {
357                     ret ~= "\t\treturn ";
358                 }
359 
360                 if (return_type.dType != "Variant") {
361                     ret ~= "ret.as!(RefOrT!(" ~ return_type.stripConst.dType ~ "))";
362                     if (return_type.isPointerType) {
363                         ret ~= ";\n\t\treturn r";
364                     }
365                 } 
366                 else {
367                     ret ~= "ret";
368                 }
369                 ret ~= ";\n";
370             }
371         } else { // end varargs/virtual impl
372             // adds temp variable for static ctor
373             if (isConstructor) {
374                 ret ~= "\t\t";
375                 ret ~= parent.name.canBeCopied ? parent.name.dType : parent.name.asOpaqueType;
376                 ret ~= " _godot_object;\n";
377             }
378             // omit return for constructors, it will be wrapped and returned later
379             if (return_type.dType != "void" && !(isConstructor && parent.name.isCoreType)) {
380                 ret ~= "\t\treturn ";
381             } else {
382                 ret ~= "\t\t";
383             }
384 
385             ret ~= callType() ~ "!(" ~ return_type.dType ~ ")(";
386             if (parent.isBuiltinClass)
387                 ret ~= "cast(GDExtensionPtrBuiltInMethod) ";
388             ret ~= "GDExtensionClassBinding." ~ wrapperIdentifier;
389             if (parent.isBuiltinClass) // Adds method pointer accessor instead of template itself
390                 ret ~= ".mb";
391             ret ~= ", ";
392             if (parent.isBuiltinClass)
393                 ret ~= "cast(void*) &_godot_object";
394             else
395                 ret ~= "_godot_object";
396             foreach (ai, const arg; arguments) {
397                 // FIXME: const cast hack
398                 // FIXME: make auto-cast in escapeDType?
399                 // FIXME: StringName pointer wrapping should be in call handlers
400                 //        it also relies on that ugly cast.
401                 //        The problem is that for some reason that call expects StringName**
402                 //        and unlike C++ I haven't come with a way to do that
403                 if (arg.type.godotType == "StringName" && callType == "callBuiltinMethod")
404                     ret ~= ", cast(void*) " ~ arg.name.escapeDType(arg.type.godotType); 
405                 else
406                     ret ~= ", cast() " ~ arg.name.escapeDType(arg.type.godotType); 
407             }
408             ret ~= ");\n";
409             // wrap temporary object
410             if (isConstructor) {
411                 if (parent.name.canBeCopied) {
412                     ret ~= "\t\treturn _godot_object;\n";
413                 } else {
414                     ret ~= "\t\treturn " ~ return_type.dType ~ "(_godot_object);\n";
415                 }
416             }
417         } // end normal method impl
418 
419         return ret;
420     }
421 
422     /// call type wrapper, "ptrcall", "callv", "callBuiltinMethod", etc...
423     string callType() const {
424         if (parent.isBuiltinClass)
425             return "callBuiltinMethod";
426         //if (has_varargs)
427         //	return "callv";
428         return "ptrcall";
429     }
430 
431     /// formats function pointer loader, e.g.
432     /// 	GDExtensionClassBinding.method_append.mb = _godot_api.clasdb_get_methodbind("class", "method", hash);
433     string loader() const {
434         char[] buf;
435         buf ~= "StringName classname = StringName(\"" ~ parent.name.godotType ~ "\");\n";
436         buf ~= "StringName methodname = StringName(\"" ~ name ~ "\");\n";
437         // probably better to move in its own subclass
438         if (parent.isBuiltinClass) {
439             return cast(string) buf ~ format(`GDExtensionClassBinding.%s.mb = _godot_api.variant_get_ptr_builtin_method(%s, cast(GDExtensionStringNamePtr) methodname, %d);`,
440                 wrapperIdentifier,
441                 parent.name.asNativeVariantType,
442                 hash
443             );
444         }
445 
446         return cast(string) buf ~ format(`GDExtensionClassBinding.%s.mb = _godot_api.classdb_get_method_bind(cast(GDExtensionStringNamePtr) classname, cast(GDExtensionStringNamePtr) methodname, %d);`,
447             wrapperIdentifier,
448             hash,
449         );
450     }
451 }
452 
453 struct GodotArgument {
454     string name;
455     Type type;
456     
457     // HACK: when godot doesn't want to specifically
458     // tell you default it leaves it empty ("default_value": "")
459     // so when asdf hits it sets default_value to []
460     // which is the same as if it's undefined
461     @serdeOptional
462     string default_value = "\0";
463 
464 @serdeIgnore:
465 
466     size_t index;
467     GodotMethod parent;
468 }
469 
470 class GodotProperty {
471     string name;
472     Type type;
473     @serdeOptional
474     string getter, setter;
475     @serdeOptional
476     int index = -1;
477 
478 @serdeIgnore:
479 
480     string ddoc;
481 
482     string getterSource(in GodotMethod m) const {
483         string retType = m.return_type.dType;
484         string ret;
485         ret ~= "\t/**\n\t" ~ ddoc.replace("\n", "\n\t") ~ "\n\t*/\n";
486         ret ~= "\t@property " ~ retType ~ " " ~ name.replace("/", "_")
487         // ret ~= "\t@property " ~ m.return_type.dType ~ " " ~ name.replace("/", "_")
488             .snakeToCamel.escapeDType ~ "() {\n"; /// TODO: const?
489         ret ~= "\t\treturn " ~ getter.snakeToCamel.escapeDType ~ "(";
490         if (index != -1) {
491             // add cast to enum types
492             if (m.arguments[0].type.isEnum)
493                 ret ~= "cast(" ~ m.arguments[0].type.dType ~ ") ";
494             ret ~= text(index);
495         }
496         ret ~= ");\n";
497         ret ~= "\t}\n";
498         return ret;
499     }
500 
501     string setterSource(in GodotMethod m) const {
502         string setType = m.arguments[$ - 1].type.dType;
503         string ret;
504         ret ~= "\t/// ditto\n";
505         ret ~= "\t@property void " ~ name.replace("/", "_")
506             .snakeToCamel.escapeDType ~ "(" ~ setType ~ " v) {\n";
507             // .snakeToCamel.escapeDType ~ "(" ~ m.arguments[$ - 1].type.dType ~ " v) {\n";
508         ret ~= "\t\t" ~ setter.snakeToCamel.escapeDType ~ "(";
509         if (index != -1) {
510             // add cast to enum types
511             if (m.arguments[0].type.isEnum)
512                 ret ~= "cast(" ~ m.arguments[0].type.dType ~ ") ";
513             ret ~= text(index) ~ ", ";
514         }
515         ret ~= "v);\n";
516         ret ~= "\t}\n";
517         return ret;
518     }
519 }
520 
521 
522 class StringHelperGodotMethod : GodotMethod {
523     override string body_() const {
524         // simply forwards all arguments to the actual method but wrap any string types
525         string ret;
526 
527         // wrap string types before calling the method
528         foreach (i, const arg; arguments) {
529             auto realType = redirectsTo.arguments[i].type;
530             if (realType.isGodotStringType) {
531                 // writes something like this:
532                 // StringName arg3 = StringName(p_name);
533                 ret ~= "\t\t" ~ escapeDType(realType.dType) ~ " arg" ~ text(cast(int) i) 
534                     ~ " = " ~ escapeDType(realType.dType) ~ "(" ~ escapeDType(arg.name) ~ ");\n";
535             }
536             //ret ~= "\t\t_GODOT_args[" ~ text(cast(int) i) ~ "] = " ~ escapeDType(arg.name) ~ ";\n";
537         }
538 
539         if (return_type.dType != "void") {
540             ret ~= "\t\treturn ";
541         }
542         else {
543             ret ~= "\t\t";
544         }
545 
546         // now write the normal D method(not a godot one) call with wrapped arguments in place of D strings
547         ret ~= name.snakeToCamel.escapeDType ~ "(";
548         foreach (ai, const arg; arguments) {
549             if (ai) {
550                 ret ~= ", ";
551             }
552             if (redirectsTo.arguments[ai].type.isGodotStringType) {
553                 ret ~= "arg" ~ text(cast(int) ai);
554             }
555             else {
556                 ret ~= arg.name.escapeDType; 
557             }
558         }
559         if (has_varargs) {
560             if (arguments.length != 0)
561                 ret ~= ", ";
562             ret ~= "varArgs";
563         }
564         ret ~= ");\n";
565         return ret;
566     }
567 }