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 }