1 module godot.tools.generator.util;
2
3 import godot.tools.generator.classes;
4
5 import std.range;
6 import std.algorithm.searching;
7 import std.algorithm.iteration;
8 import std.path;
9 import std.conv : text;
10 import std.string;
11
12 import asdf;
13
14 //import asdf.source.asdf.asdf;
15
16 struct TypeStruct {
17 @serdeKeys("name", "type") string name;
18 @serdeOptional string meta;
19
20 SerdeException deserializeFromAsdf(Asdf data) {
21 // here we try read 3 options, 'name' variant is for native_structs
22 name = data["type"].get!string(null);
23 meta = data["meta"].get!string(null);
24 if (name is null) {
25 string val;
26 if (auto exc = deserializeScopedString(data, val))
27 return exc;
28 name = val;
29 }
30
31 return null;
32 }
33 }
34
35 //@serdeProxy!string
36 @serdeProxy!TypeStruct
37 class Type {
38 static Type[string] typesByGodotName;
39 static Type[string] typesByDName;
40
41 static Type[] enums;
42
43 GodotClass objectClass;
44 GodotClass original; // original GodotClass associated with this Type
45 string dType;
46 string godotType;
47 bool isNativeStruct;
48
49 @property string dRef() const {
50 return isRef ? ("Ref!" ~ dType) : dType;
51 }
52
53 Type enumParent;
54
55 //alias dType this;
56
57 string asModuleName() const {
58 if (isPrimitive || isCoreType)
59 return null;
60 if (isNativeStruct)
61 return "structs"; // module godot.structs
62 return godotType.chompPrefix("_").toLower;
63 }
64
65 /// Backing opaque type to use instead of raw GDExtensionTypePtr
66 string asOpaqueType() const {
67 switch (godotType) {
68 case "TypedArray":
69 case "Array":
70 return "godot_array";
71 case "Variant":
72 return "godot_variant";
73 case "String":
74 case "StringName":
75 return "godot_string";
76 case "NodePath":
77 return "godot_node_path";
78 case "Dictionary":
79 return "godot_dictionary";
80 case "Callable":
81 return "godot_callable";
82 case "Signal":
83 return "godot_signal";
84 case "RID":
85 return "godot_rid";
86 case "Object":
87 case "Nil": // we don't need Nil at all but for now let just make it work
88 return "godot_object";
89 case "PackedByteArray":
90 case "PackedInt32Array":
91 case "PackedInt64Array":
92 case "PackedFloat32Array":
93 case "PackedFloat64Array":
94 case "PackedStringArray":
95 case "PackedVector2Array":
96 case "PackedVector3Array":
97 case "PackedColorArray":
98 //case "Nil":
99 return "GDExtensionTypePtr";
100 default:
101 break;
102 }
103
104 if (!isRef || canBeCopied)
105 return this.dType;
106
107 return "godot_object";
108 }
109
110 bool isEnum() const {
111 return godotType.startsWith("enum::");
112 }
113
114 bool isBitfield() const {
115 return godotType.startsWith("bitfield::");
116 }
117
118 bool isTypedArray() const {
119 return godotType.startsWith("typedarray::");
120 }
121
122 bool isMetaType() const {
123 return isEnum || isBitfield || isTypedArray;
124 }
125
126 bool isPointerType() const {
127 return dType.indexOf("*") != -1;
128 }
129
130 // Any type that is internally backed by godot string
131 bool isGodotStringType() const {
132 import std.algorithm : among;
133 return godotType.among("StringName", "String", "NodePath") > 0;
134 }
135
136 bool isPrimitive() const {
137 if (isEnum || isBitfield)
138 return true;
139 return only("int", "bool", "real", "float", "void", "double", "real_t",
140 "uint8_t", "int8_t", "uint16_t", "int16_t", "uint32_t", "int32_t", "uint64_t", "int64_t", // well...
141 "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64" // hope they will merge it or smth
142
143
144
145 ).canFind(stripConstPointer.godotType);
146 }
147
148 // types that have simple value semantics and doesn't require special wrappers
149 bool canBeCopied() const {
150 return only("Vector2", "Vector2i", "Vector3", "Vector3i", "Vector4", "Vector4i",
151 "Transform2D", "Transform3D", "Projection", "Rect2", "Rect2i",
152 "Color", "Plane", "AABB", "Quaternion", "Basis", "RID", "Nil"
153 ).canFind(stripConstPointer.godotType);
154 }
155
156 bool isCoreType() const {
157 if (auto arraytype = arrayType())
158 return arraytype.isCoreType();
159 // basically all types from extension_api.json from builtin_classes
160 auto coreTypes = only("AABB",
161 "Array",
162 "Basis",
163 "Callable",
164 "Color",
165 "Dictionary",
166 "GodotError",
167 "NodePath",
168 "StringName",
169 "Plane",
170 "PackedByteArray",
171 "PackedInt32Array",
172 "PackedInt64Array",
173 "PackedFloat32Array",
174 "PackedFloat64Array",
175 "PackedStringArray",
176 "PackedVector2Array",
177 "PackedVector3Array",
178 "PackedColorArray",
179 "Quaternion",
180 "Rect2",
181 "Rect2i",
182 "RID",
183 "Signal",
184 "String",
185 "Transform3D",
186 "Transform2D",
187 "TypedArray",
188 "Projection",
189 "Variant",
190 "Vector2",
191 "Vector2i",
192 "Vector3",
193 "Vector3i",
194 "Vector4",
195 "Vector4i",
196 "Nil", // why godot, why?
197 "ObjectID");
198 return coreTypes.canFind(godotType);
199 }
200
201 /// Get variant type name for method calls
202 string asNativeVariantType() const {
203 import godot.util.string;
204
205 // useless but ok
206 if (!(isCoreType || isPrimitive))
207 return "GDEXTENSION_VARIANT_TYPE_OBJECT";
208
209 return "GDEXTENSION_VARIANT_TYPE_" ~ camelToSnake(godotType).toUpper;
210 }
211
212 /// returns TypedArray type
213 Type arrayType() const {
214 if (!isTypedArray)
215 return null;
216 return Type.get(godotType["typedarray::".length .. $]);
217 }
218
219 bool isRef() const {
220 return objectClass && objectClass.is_reference;
221 }
222
223 /// type should be taken as template arg by methods to allow implicit conversion in ptrcall
224 bool acceptImplicit() const {
225 auto accept = only("Variant");
226 return accept.canFind(godotType);
227 }
228
229 /// prefix for function parameter of this type
230 string dCallParamPrefix() const {
231 if (isRef)
232 return "";
233 else if (objectClass)
234 return "";
235 else if (godotType.indexOf("const") != -1)
236 return "";
237 else
238 return "in ";
239 }
240 /// how to pass parameters of this type into ptrcall void** arg
241 string ptrCallArgPrefix() const {
242 if (isPrimitive || isCoreType)
243 return "&";
244 return "";
245 //return "cast(godot_object)"; // for both base classes and D classes (through alias this)
246 }
247
248 /// returns value name from string literal, default values in api.json uses integers instead of qualified enum values
249 string getEnumValueStr(string value) const {
250 import std.conv : to;
251 import godot.util.string;
252
253 if (!isEnum())
254 return null;
255
256 import godot.tools.generator.enums;
257
258 // global enums doesn't have parent
259 auto parent = Type.get(godot.tools.generator.enums.enumParent(godotType));
260 if (!parent)
261 parent = Type.get("CoreConstants");
262
263 // just the enum name, without parent class name
264 string innerName = splitEnumName(godotType)[1];
265
266 // FIXME why it's always parent.original?
267 auto searchInParent = parent.godotType == "CoreConstants" ? parent.original
268 : parent.original;
269
270 // HACK: core types not available here
271 // if (searchInParent is null && parent.isCoreType) {
272 // FIXME higher if throws access violation when searching later
273 if (searchInParent is null) {
274 return "cast(" ~ dType ~ ")" ~ value;
275 }
276 auto found = searchInParent.enums.find!(s => s.name == innerName);
277 if (!found.empty) {
278 foreach (pair; found.front.values)
279 if (pair.value == to!int(value))
280 return dType ~ "." ~ snakeToCamel(pair.name);
281 }
282
283 return "cast(" ~ dType ~ ")" ~ value;
284 }
285
286 /// strip constness, also strips indirections (despite the name)
287 Type stripConstPointer() const {
288 char[] unqualified = cast(char[]) godotType.replace("const ", "").dup;
289 while (unqualified[$ - 1] == '*') {
290 unqualified[$ - 1] = '\0';
291 unqualified = unqualified[0 .. $ - 1];
292 }
293 unqualified = unqualified.stripRight; // strip whitespace leftovers
294 return Type.get(cast(string) unqualified);
295 }
296
297 // same as stripConstPointer() but only strips constness, useful for return types and template params
298 Type stripConst() const {
299 char[] unqualified = cast(char[]) godotType.replace("const ", "").dup;
300 unqualified = unqualified.stripRight; // strip whitespace leftovers
301 return Type.get(cast(string) unqualified);
302 }
303
304 // removes any prefixed meta names such as "enum::" in "enum::MyClass.MyEnum"
305 Type stripMeta() const {
306 // NOTE: this method is marked const, but we want it to be convenient.
307 // this can lead to potential UB when user modifies the returned value,
308 // and there is no other way around because Type.get(godotName) will return
309 // same object as this.
310 if (isEnum) {
311 static import enumutils = godot.tools.generator.enums;
312 // returns cached parent first, otherwise extract from name string
313 if (enumParent)
314 return cast() enumParent;
315 return Type.get(enumutils.enumParent(godotType));
316 }
317 if (isBitfield) {
318 return Type.get(godotType["bitfield::".length .. $]);
319 }
320 if (isTypedArray) {
321 return Type.get(godotType["typedarray::".length .. $]);
322 }
323 return cast() this;
324 }
325
326 // Companion method for stripMeta, usually meta types is a nested types inside another class
327 // so we have to take their enclosing type
328 Type getParentType() const {
329 import std.string : lastIndexOf;
330 auto pos = godotType.lastIndexOf('.');
331 if (pos > 0) {
332 const parentName = godotType[0..pos];
333 if (auto parent = parentName in Type.typesByGodotName)
334 return *parent;
335 }
336 return null;
337 }
338
339 this(string godotName) {
340 godotType = godotName;
341 dType = godotName.escapeGodotType;
342 }
343
344 this(TypeStruct t) {
345 // here t.name usually specifies old type like int, with meta describing actual length like int64
346 if (t.meta)
347 this(t.meta);
348 else
349 this(t.name);
350 }
351
352 static Type get(string godotName) {
353 if (!godotName.length)
354 return null; // no type (used in base_class)
355 if (Type* ptr = godotName in typesByGodotName)
356 return *ptr;
357 Type ret = new Type(godotName);
358
359 static import godot.tools.generator.enums;
360
361 if (ret.isEnum) {
362 ret.enumParent = get(godot.tools.generator.enums.enumParent(godotName));
363 enums ~= ret;
364 }
365
366 typesByGodotName[godotName] = ret;
367 typesByDName[ret.dType] = ret;
368
369 return ret;
370 }
371
372 /*
373 SerdeException deserializeFromAsdf(Asdf data)
374 {
375 string val;
376 if (auto exc = deserializeScopedString(data, val))
377 return exc;
378
379 godotType = val;
380 dType = godotType.escapeGodotType;
381
382 return null;
383 }
384 */
385 /*
386 static Type deserialize(ref Asdf asdf)
387 {
388 string gn = asdf.get!string(null);
389 Type ret = get(gn);
390 return ret;
391 }
392 */
393 }
394
395 /// the default value to use for an argument if none is provided
396 string defaultTypeString(in Type type) {
397 import std.string;
398 import std.conv : text;
399
400 bool isPointer = type.dType.indexOf("*") != -1;
401
402 switch (type.dType) {
403 case "String":
404 // FIXME: might cause some issues with auto-conversion?
405 return `gs!""`;
406 case "Dictionary":
407 return type.dType ~ ".make()";
408 case "Array":
409 return type.dType ~ ".make()";
410 default: { // all default-blittable types
411 if (isPointer) {
412 return "(" ~ type.dType ~ ").init";
413 } else {
414 return type.dType ~ ".init"; // D's default initializer
415 }
416 ///return "null";
417 }
418 }
419 }
420
421 /++
422 PoolVector2Array
423 PoolColorArray
424 Array
425 Vector2
426 float
427 Color
428 bool
429 Object
430 PoolVector3Array
431 Vector3
432 Transform2D
433 RID
434 int
435 Transform
436 Rect2
437 String
438 Variant
439 PoolStringArray
440 +/
441 string escapeDefaultType(in Type type, string arg) {
442 import std.string;
443 import std.conv : text;
444
445 if (!arg || arg.length == 0)
446 return defaultTypeString(type);
447
448 if (type.isTypedArray) {
449 // examples of typedarray:
450 // Array[RDPipelineSpecializationConstant]([])
451 // Array[RID]([])
452 // Array[Array]([])
453 // []
454 // TODO: other cases such as that where it actually has values in it
455 if (arg.startsWith("Array[") && arg.endsWith("([])")) {
456 return "[]";
457 }
458 }
459 // parse the defaults in api.json
460 switch (type.dType) {
461 case "Color": // "1,1,1,1"
462 if (arg.startsWith("Color("))
463 return arg;
464 return "Color(" ~ arg ~ ")";
465 case "bool": // True, False
466 return arg.toLower;
467 case "Array": // "[]", "Null" - just use the empty one
468 case "Dictionary":
469 case "PackedByteArray":
470 case "PackedInt32Array":
471 case "PackedInt64Array":
472 case "PackedFloat32Array":
473 case "PackedFloat64Array":
474 case "PackedVector2Array":
475 case "PackedVector3Array":
476 case "PackedStringArray":
477 case "PackedColorArray":
478 return defaultTypeString(type);
479 case "Transform3D": // "1, 0, 0, 0, 1, 0, 0, 0, 1 - 0, 0, 0" TODO: parse this
480 if (arg.startsWith("Transform3D("))
481 return arg;
482 return "Transform3D(" ~ arg ~ ")";
483 case "Transform2D":
484 if (arg.startsWith("Transform2D("))
485 return arg;
486 return "Transform2D(" ~ arg ~ ")";
487 case "Projection":
488 if (arg.startsWith("Projection("))
489 return arg;
490 return "Projection(" ~ arg ~ ")";
491 case "RID": // always empty?
492 return defaultTypeString(type); // D's default initializer
493 case "Vector2": // "(0, 0)"
494 case "Vector2i": // "(0, 0)"
495 case "Vector3":
496 case "Vector3i":
497 case "Vector4":
498 case "Vector4i":
499 case "Rect2": // "(0, 0, 0, 0)"
500 case "AABB":
501 if (arg.startsWith(type.godotType)) // prevent junk like 'Vector2Vector2(0, 0)'
502 arg = arg[type.godotType.length .. $];
503 return type.dType ~ arg;
504 case "Variant":
505 if (arg == "Null")
506 return "Variant.nil";
507 else
508 return arg;
509 case "String":
510 case "StringName":
511 case "NodePath":
512 // TODO: use this instead
513 version(none) {
514 return type.dType ~ "(" ~ stripStringDefaultValueType() ~ ")";
515 }
516 if (arg[0] == '&')
517 arg = arg[1 .. $];
518 // node path has default value that includes type,
519 // must strip it from here as we deal with string helpers later
520 // example value: NodePath("")
521 if (arg.startsWith("NodePath("))
522 arg = arg["NodePath(".length..$-1];
523
524 // HACK: hack in string, trying auto-convert?
525 // if (arg.canFind('"'))
526 // return "gn!" ~ arg;
527 // return "gn!\"" ~ arg ~ "\"";
528 if (arg.canFind('"'))
529 return type.dType ~ "(" ~ arg ~ ")";
530 return type.dType ~ "(\"" ~ arg ~ "\")";
531 default: // all Object types
532 {
533 if (arg == "Null" || arg == "null")
534 return defaultTypeString(type);
535 if (arg == "[Object:null]")
536 return defaultTypeString(type);
537 if (type.isEnum)
538 return type.getEnumValueStr(arg);
539 return arg;
540 }
541 }
542 }
543
544 // Same as above except it returns only the string itself without the prepending type
545 string stripStringDefaultValueType(in Type type, string arg) {
546 // only mess up with strings
547 if (!type.isGodotStringType)
548 return arg;
549
550 if (arg[0] == '&')
551 arg = arg[1 .. $];
552 // node path has default value that includes type,
553 // must strip it from here as we deal with string helpers later
554 // example value: NodePath("")
555 if (arg.startsWith("NodePath("))
556 arg = arg["NodePath(".length..$-1];
557
558 // HACK: hack in string, trying auto-convert?
559 // if (arg.canFind('"'))
560 // return "gn!" ~ arg;
561 // return "gn!\"" ~ arg ~ "\"";
562 if (arg[0]=='"')
563 return arg;
564 else
565 return "";
566 }
567
568 string escapeGodotType(string t) {
569 import godot.tools.generator.enums : asEnumName;
570
571 t = t.chompPrefix("_");
572
573 if (t == "Object")
574 return "GodotObject";
575 if (t == "Error")
576 return "GodotError";
577 if (t == "Callable")
578 return "GodotCallable";
579 if (t == "Signal")
580 return "GodotSignal";
581 if (t == "float")
582 return "double";
583 if (t == "int")
584 return "long";
585 if (t == "Nil")
586 return "GDExtensionTypePtr";
587 if (t.startsWith("enum::"))
588 return t.asEnumName;
589 if (t.startsWith("bitfield::"))
590 return t["bitfield::".length .. $];
591 if (t.startsWith("typedarray::"))
592 return t.asTypedArray;
593 return t;
594 }
595
596 string escapeDType(string s, string godotType = "") {
597 import std.meta;
598 import std.uni, std.utf;
599
600 /// TODO: there must be a better way of doing this...
601 /// maybe one of the D parser libraries has a list of keywords and basic types
602
603 if (s.toUTF32[0].isNumber)
604 s = "_" ~ s; // can't start with a number
605
606 alias keywords = AliasSeq!(
607 "class",
608 "interface",
609 "struct",
610 "enum",
611 "bool",
612 "ubyte",
613 "byte",
614 "ushort",
615 "short",
616 "uint",
617 "int",
618 "ulong",
619 "long",
620 "cent", // really?
621 "ucent",
622 "float",
623 "double",
624 "real",
625 "char",
626 "wchar",
627 "dchar",
628 "function",
629 "delegate",
630 "override",
631 "default",
632 "case",
633 "switch",
634 "export",
635 "import",
636 "template",
637 "new",
638 "delete",
639 "return",
640 "with",
641 "align",
642 "in",
643 "out",
644 "ref",
645 "scope",
646 "auto",
647 "init",
648 "version",
649 "body",
650 "debug",
651 );
652 switch (s) {
653 case "Object":
654 return "GodotObject";
655 case "Error":
656 return "GodotError";
657 case "Signal":
658 return "GodotSignal";
659 case "Callable":
660 return "GodotCallable";
661 foreach (kw; keywords) case kw:
662 return "_" ~ s;
663 default:
664 return s;
665 }
666 }
667
668 string asTypedArray(string type) {
669 return "TypedArray!(" ~ type["typedarray::".length .. $] ~ ")";
670 }
671
672 string tab(string s, int tabs) {
673 import std.conv : to;
674 return repeat('\t', tabs).to!string ~ s;
675 }